add: PostgreSQL 支持(asyncpg 驱动,from_url/from_config + 集成测试)
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"""Database 构造与 from_config 的纯 API 校验(无 IO)。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from chongming_database import Database
|
||||
|
||||
|
||||
async def test_constructor_requires_exactly_one_arg():
|
||||
# 两个都不给
|
||||
with pytest.raises(ValueError):
|
||||
Database()
|
||||
# 两个都给
|
||||
with pytest.raises(ValueError):
|
||||
Database("x.db", url="postgresql+asyncpg://localhost/db")
|
||||
|
||||
|
||||
async def test_from_config_sqlite_requires_path():
|
||||
with pytest.raises(ValueError):
|
||||
Database.from_config({"type": "sqlite"})
|
||||
|
||||
|
||||
async def test_from_config_postgres_requires_url():
|
||||
with pytest.raises(ValueError):
|
||||
Database.from_config({"type": "postgresql"})
|
||||
|
||||
|
||||
async def test_from_config_unsupported_type():
|
||||
with pytest.raises(NotImplementedError):
|
||||
Database.from_config({"type": "mysql", "url": "mysql://localhost/db"})
|
||||
|
||||
|
||||
async def test_postgres_url_normalized_to_asyncpg():
|
||||
db = Database.from_url("postgresql://user:pwd@127.0.0.1:5432/mydb")
|
||||
assert db._build_url() == "postgresql+asyncpg://user:pwd@127.0.0.1:5432/mydb"
|
||||
|
||||
|
||||
async def test_explicit_driver_url_untouched():
|
||||
db = Database(url="postgresql+asyncpg://user:pwd@127.0.0.1:5432/mydb")
|
||||
assert db._build_url() == "postgresql+asyncpg://user:pwd@127.0.0.1:5432/mydb"
|
||||
@@ -0,0 +1,74 @@
|
||||
"""PostgreSQL 后端集成测试(需 CHONGMING_TEST_PG_URL 环境变量,否则跳过)。"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from sqlmodel import Field, SQLModel, select
|
||||
|
||||
from chongming_database import Database
|
||||
|
||||
PG_URL = os.environ.get("CHONGMING_TEST_PG_URL")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not PG_URL, reason="CHONGMING_TEST_PG_URL 未设置,跳过 PostgreSQL 集成测试"
|
||||
)
|
||||
|
||||
|
||||
def _make_item_table(tablename: str):
|
||||
"""动态生成随机表名的 SQLModel 表,避免污染共享 PG 实例。"""
|
||||
|
||||
class Item(SQLModel, table=True):
|
||||
__tablename__ = tablename
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
name: str = Field(max_length=50)
|
||||
|
||||
return Item
|
||||
|
||||
|
||||
async def test_postgres_crud():
|
||||
Item = _make_item_table(f"pg_test_item_{uuid.uuid4().hex[:8]}")
|
||||
db = Database.from_url(PG_URL)
|
||||
await db.initialize()
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
session.add(Item(name="pg"))
|
||||
await session.commit()
|
||||
async with db.get_session() as session:
|
||||
rows = (await session.execute(select(Item))).scalars().all()
|
||||
assert [r.name for r in rows] == ["pg"]
|
||||
finally:
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_postgres_bare_url_scheme():
|
||||
"""裸 postgresql:// 应被归一化为 postgresql+asyncpg:// 后正常连接。"""
|
||||
bare = PG_URL.replace("postgresql+asyncpg://", "postgresql://", 1)
|
||||
db = Database.from_url(bare)
|
||||
await db.initialize()
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
await session.execute(select(1))
|
||||
finally:
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_from_config_postgres_end_to_end():
|
||||
db = Database.from_config({"type": "postgresql", "url": PG_URL})
|
||||
await db.initialize()
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
await session.execute(select(1))
|
||||
finally:
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_postgres_generate_schemas_false():
|
||||
db = Database.from_url(PG_URL)
|
||||
await db.initialize(generate_schemas=False)
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
await session.execute(select(1))
|
||||
finally:
|
||||
await db.dispose()
|
||||
@@ -0,0 +1,71 @@
|
||||
"""SQLite 后端回归测试(tmp_path,无需外部服务)。"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlmodel import Field, SQLModel, select
|
||||
|
||||
from chongming_database import Database
|
||||
|
||||
|
||||
class SqliteItem(SQLModel, table=True):
|
||||
__tablename__ = "backend_test_item"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
name: str = Field(max_length=50)
|
||||
|
||||
|
||||
async def test_sqlite_crud(tmp_path: Path):
|
||||
db = Database(tmp_path / "nested" / "test.db")
|
||||
await db.initialize()
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
session.add(SqliteItem(name="a"))
|
||||
await session.commit()
|
||||
async with db.get_session() as session:
|
||||
rows = (await session.execute(select(SqliteItem))).scalars().all()
|
||||
assert [r.name for r in rows] == ["a"]
|
||||
finally:
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_sqlite_initialize_is_idempotent(tmp_path: Path):
|
||||
"""重复 initialize(模拟多进程各自初始化)不应报错。"""
|
||||
db = Database(tmp_path / "test.db")
|
||||
await db.initialize()
|
||||
try:
|
||||
await db.initialize()
|
||||
finally:
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_get_session_before_initialize_raises(tmp_path: Path):
|
||||
db = Database(tmp_path / "test.db")
|
||||
try:
|
||||
with pytest.raises(RuntimeError):
|
||||
db.get_session()
|
||||
finally:
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_from_config_sqlite_end_to_end(tmp_path: Path):
|
||||
db = Database.from_config(
|
||||
{"type": "sqlite", "database_path": str(tmp_path / "cfg.db")}
|
||||
)
|
||||
await db.initialize()
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
await session.execute(select(SqliteItem))
|
||||
finally:
|
||||
await db.dispose()
|
||||
|
||||
|
||||
async def test_generate_schemas_false_skips_ddl(tmp_path: Path):
|
||||
"""generate_schemas=False 时只建引擎不建表,会话仍可用。"""
|
||||
db = Database(tmp_path / "noschema.db")
|
||||
await db.initialize(generate_schemas=False)
|
||||
try:
|
||||
async with db.get_session() as session:
|
||||
await session.execute(select(1))
|
||||
finally:
|
||||
await db.dispose()
|
||||
Reference in New Issue
Block a user