fix: PostgreSQL 目标库不存在时 initialize() 自动建库并发布 0.2.1
This commit is contained in:
@@ -66,6 +66,10 @@ Database.from_config({"type": "postgresql", "url": "postgresql://user:pwd@127.0.
|
|||||||
| ------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `__init__` | `(db_path=None, url=None)` | 二选一必填,否则抛 `ValueError`;`db_path` 自动创建父目录 |
|
| `__init__` | `(db_path=None, url=None)` | 二选一必填,否则抛 `ValueError`;`db_path` 自动创建父目录 |
|
||||||
| `initialize` | `(generate_schemas=True, echo=False, **kwargs)` | 创建异步引擎,`SQLModel.metadata.create_all` 自动建表(`generate_schemas=False` 可跳过,交由外部迁移工具),构建 `sessionmaker` |
|
| `initialize` | `(generate_schemas=True, echo=False, **kwargs)` | 创建异步引擎,`SQLModel.metadata.create_all` 自动建表(`generate_schemas=False` 可跳过,交由外部迁移工具),构建 `sessionmaker` |
|
||||||
|
|
||||||
|
> PostgreSQL:`initialize()` 检测到目标库不存在时(如全新 PG 实例),会以相同凭据连接
|
||||||
|
> `postgres` 维护库自动 `CREATE DATABASE`(多进程并发启动安全),因此无需在部署流程中
|
||||||
|
> 手工建库;要求连接账号具备 `CREATEDB` 权限(默认超级用户 `postgres` 满足)。
|
||||||
| `get_session` | `() -> AsyncSession` | 返回新的异步会话;未初始化则抛 `RuntimeError` |
|
| `get_session` | `() -> AsyncSession` | 返回新的异步会话;未初始化则抛 `RuntimeError` |
|
||||||
| `dispose` | `async () -> None` | 关闭异步引擎并清除内部状态 |
|
| `dispose` | `async () -> None` | 关闭异步引擎并清除内部状态 |
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ requires = ["hatchling"]
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "chongming-database"
|
name = "chongming-database"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
description = "异步数据库封装:SQLite(aiosqlite)+ PostgreSQL(asyncpg),基于 SQLModel/SQLAlchemy"
|
description = "异步数据库封装:SQLite(aiosqlite)+ PostgreSQL(asyncpg),基于 SQLModel/SQLAlchemy"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
from sqlalchemy.engine import make_url
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
logger = logging.getLogger("chongming_database")
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
"""异步数据库封装,支持 SQLite 与 PostgreSQL 两种后端。
|
"""异步数据库封装,支持 SQLite 与 PostgreSQL 两种后端。
|
||||||
@@ -66,6 +72,47 @@ class Database:
|
|||||||
return self._url
|
return self._url
|
||||||
return f"sqlite+aiosqlite:///{self._db_path}"
|
return f"sqlite+aiosqlite:///{self._db_path}"
|
||||||
|
|
||||||
|
async def _ensure_database(self) -> None:
|
||||||
|
"""PostgreSQL 目标库不存在时自动创建(SQLite 文件本就自动生成,无需此步骤)。
|
||||||
|
|
||||||
|
全新 PG 实例(如部署环境刚起容器)里只有 postgres 维护库,若不先建库,
|
||||||
|
initialize() 首次连接即报 ``database "xxx" does not exist``。
|
||||||
|
这里以相同凭据连接 postgres 维护库检查并执行 CREATE DATABASE。
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- CREATE DATABASE 不能运行在事务块内,因此不走 SQLAlchemy 引擎
|
||||||
|
(其连接默认开事务),直接用 asyncpg 裸连接(语句自动提交)。
|
||||||
|
- 要求连接账号具备 CREATEDB 权限;部署中通常以超级用户(postgres)运行。
|
||||||
|
- 并发启动时多个进程可能同时判断"库不存在",DuplicateDatabaseError 忽略即可。
|
||||||
|
"""
|
||||||
|
url = make_url(self._build_url())
|
||||||
|
if url.get_backend_name() != "postgresql":
|
||||||
|
return
|
||||||
|
db_name = url.database
|
||||||
|
if not db_name or db_name in ("postgres", "template0", "template1"):
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = await asyncpg.connect(
|
||||||
|
host=url.host,
|
||||||
|
port=url.port,
|
||||||
|
user=url.username,
|
||||||
|
password=url.password,
|
||||||
|
database="postgres",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
exists = await conn.fetchval(
|
||||||
|
"SELECT 1 FROM pg_database WHERE datname = $1", db_name
|
||||||
|
)
|
||||||
|
if exists is None:
|
||||||
|
quoted = db_name.replace('"', '""')
|
||||||
|
await conn.execute(f'CREATE DATABASE "{quoted}"')
|
||||||
|
logger.info("PostgreSQL 数据库 %s 不存在,已自动创建", db_name)
|
||||||
|
except asyncpg.DuplicateDatabaseError:
|
||||||
|
# 多进程并发启动的竞态:另一进程刚创建成功
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
async def initialize(
|
async def initialize(
|
||||||
self,
|
self,
|
||||||
generate_schemas: bool = True,
|
generate_schemas: bool = True,
|
||||||
@@ -86,11 +133,17 @@ class Database:
|
|||||||
PostgreSQL:DDL 是事务性的,用 begin() 包裹 create_all,
|
PostgreSQL:DDL 是事务性的,用 begin() 包裹 create_all,
|
||||||
同样 checkfirst=True + "already exists" 兜底。
|
同样 checkfirst=True + "already exists" 兜底。
|
||||||
|
|
||||||
|
PostgreSQL 建库说明:目标库不存在时(全新 PG 实例),会先以相同凭据
|
||||||
|
连接 postgres 维护库自动 CREATE DATABASE(见 _ensure_database),
|
||||||
|
因此无需在部署流程中手工建库;要求连接账号具备 CREATEDB 权限。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
generate_schemas: 是否自动建表(False 表示 schema 由外部迁移工具管理)
|
generate_schemas: 是否自动建表(False 表示 schema 由外部迁移工具管理)
|
||||||
echo: 是否打印 SQL(调试用)
|
echo: 是否打印 SQL(调试用)
|
||||||
engine_kwargs: 透传给 create_async_engine(如 pool_size 等)
|
engine_kwargs: 透传给 create_async_engine(如 pool_size 等)
|
||||||
"""
|
"""
|
||||||
|
if self._url is not None:
|
||||||
|
await self._ensure_database()
|
||||||
self._engine = create_async_engine(
|
self._engine = create_async_engine(
|
||||||
self._build_url(),
|
self._build_url(),
|
||||||
echo=echo,
|
echo=echo,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
import pytest
|
import pytest
|
||||||
from sqlmodel import Field, SQLModel, select
|
from sqlmodel import Field, SQLModel, select
|
||||||
|
|
||||||
@@ -72,3 +73,37 @@ async def test_postgres_generate_schemas_false():
|
|||||||
await session.execute(select(1))
|
await session.execute(select(1))
|
||||||
finally:
|
finally:
|
||||||
await db.dispose()
|
await db.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_postgres_auto_create_missing_database():
|
||||||
|
"""目标库不存在时 initialize() 应自动建库(回归:database "x" does not exist)。"""
|
||||||
|
from sqlalchemy.engine import make_url
|
||||||
|
|
||||||
|
base = make_url(PG_URL)
|
||||||
|
db_name = f"chongming_test_autocreate_{uuid.uuid4().hex[:8]}"
|
||||||
|
url = base.set(database=db_name).render_as_string(hide_password=False)
|
||||||
|
|
||||||
|
db = Database.from_url(url)
|
||||||
|
try:
|
||||||
|
await db.initialize() # 此前这里会因库不存在而抛 OperationalError
|
||||||
|
async with db.get_session() as session:
|
||||||
|
cur = await session.execute(select(1))
|
||||||
|
assert cur.scalar() == 1
|
||||||
|
# 幂等:库已存在时重复 initialize 也应成功(先释放旧引擎连接,便于清理)
|
||||||
|
await db.dispose()
|
||||||
|
await db.initialize()
|
||||||
|
finally:
|
||||||
|
await db.dispose()
|
||||||
|
# 清理:连 postgres 维护库删除测试库
|
||||||
|
maint = base.set(database="postgres")
|
||||||
|
conn = await asyncpg.connect(
|
||||||
|
host=maint.host,
|
||||||
|
port=maint.port,
|
||||||
|
user=maint.username,
|
||||||
|
password=maint.password,
|
||||||
|
database="postgres",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await conn.execute(f'DROP DATABASE IF EXISTS "{db_name}"')
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user