Postgres real backend: psycopg adapter, schema, migration
Replace the PostgresDatabase skeleton with a real implementation: psycopg (v3) first, psycopg2 second, lazy load, missing driver still raises PostgresDriverMissingError with no silent SQLite fallback. A sqlite3-style adapter (PgConnection/PgCursor/PgRow) translates ? placeholders to %s outside string literals, maps UNIQUE/FK violations, lock errors and connection failures to explicit Postgres error types, and handles INSERT OR REPLACE, PRAGMA table_info, AUTOINCREMENT and FTS5 refusal. POSTGRES_SCHEMA covers all business tables with tasks result columns; server init_db uses it under CLOUD_DB_BACKEND=postgres, SQLite path unchanged. migrate_sqlite_to_postgres.py copies rows with ON CONFLICT DO NOTHING and reseeds identity sequences. Tests: cloud 239 (235 pass, 4 skipped without a real server), host 27 pass; real-server tests skip unless CLOUD_TEST_PG_DSN is set.
This commit is contained in:
1 parent
a19e77ee44
commit
dc3f572b2b
10 files changed
+1542
-66
No files matched your search
@@ -207,22 +207,94 @@ class PostgresSkeletonTestCase(_EnvCleanMixin, unittest.TestCase):
|
||||
self.assertEqual(db2.host, "explicit-host")
|
||||
self.assertEqual(db2.dbname, "env-db")
|
||||
|
||||
def test_postgres_methods_are_skeleton_not_fake_success(self):
|
||||
# 绕过 __init__(驱动探测)构造骨架实例,验证方法体本身
|
||||
# 就是明确的 NotImplementedError,而不是假装成功。
|
||||
skeleton = object.__new__(PostgresDatabase)
|
||||
for name in ("connect", "read_all", "transaction",
|
||||
"is_locked_error"):
|
||||
method = getattr(skeleton, name)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
if name in ("connect", "transaction"):
|
||||
method(None) if name == "transaction" else method()
|
||||
elif name == "is_locked_error":
|
||||
method(ValueError("x"))
|
||||
else:
|
||||
method(None, "SELECT 1")
|
||||
with self.assertRaises(NotImplementedError):
|
||||
skeleton.execute_with_retry(None, "SELECT 1", ())
|
||||
def test_postgres_methods_are_implemented_via_fake_driver(self):
|
||||
# 骨架已由真实实现取代:用 fake 驱动(不连真实 Postgres)
|
||||
# 验证 connect / read_all / transaction / is_locked_error
|
||||
# 均可用,且驱动唯一冲突被映射成模块内完整性错误。
|
||||
from cloud.db import (
|
||||
PgConnection,
|
||||
PostgresIntegrityError,
|
||||
PostgresOperationalError,
|
||||
)
|
||||
|
||||
class _FakeUnique(Exception):
|
||||
pass
|
||||
|
||||
class _FakeCursor:
|
||||
def __init__(self):
|
||||
self.executed = []
|
||||
self.description = [("id",), ("name",)]
|
||||
|
||||
def execute(self, sql, params=()):
|
||||
self.executed.append((sql, params))
|
||||
if "dup" in sql:
|
||||
raise _FakeUnique("duplicate key")
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
return [("t1", "hello")]
|
||||
|
||||
def fetchone(self):
|
||||
return ("t1", "hello")
|
||||
|
||||
@property
|
||||
def rowcount(self):
|
||||
return 1
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
class _FakeConn:
|
||||
def __init__(self):
|
||||
self.cursor_obj = _FakeCursor()
|
||||
self.committed = False
|
||||
self.rolled_back = False
|
||||
|
||||
def cursor(self):
|
||||
return self.cursor_obj
|
||||
|
||||
def commit(self):
|
||||
self.committed = True
|
||||
|
||||
def rollback(self):
|
||||
self.rolled_back = True
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
class _FakeDriver:
|
||||
UniqueViolation = _FakeUnique
|
||||
OperationalError = Exception
|
||||
Error = Exception
|
||||
|
||||
def __init__(self):
|
||||
self.conn = _FakeConn()
|
||||
|
||||
def connect(self, **kwargs):
|
||||
return self.conn
|
||||
|
||||
fake = _FakeDriver()
|
||||
with mock.patch("cloud.db._load_postgres_driver",
|
||||
return_value=("psycopg", fake)):
|
||||
db = PostgresDatabase(host="test-host", dbname="test-db")
|
||||
conn = db.connect()
|
||||
self.assertIsInstance(conn, PgConnection)
|
||||
rows = db.read_all(conn, "SELECT * FROM t WHERE id = ?",
|
||||
("t1",))
|
||||
self.assertEqual(rows[0]["name"], "hello")
|
||||
self.assertEqual(rows[0][0], "t1")
|
||||
# 唯一冲突 -> 模块内错误,不是驱动原始异常、不是 sqlite 异常。
|
||||
with self.assertRaises(PostgresIntegrityError):
|
||||
db.execute_with_retry(conn, "SELECT dup", ())
|
||||
# 事务回滚:第二条失败后 rollback 被调用且异常原样抛出。
|
||||
with self.assertRaises(PostgresIntegrityError):
|
||||
with db.transaction(conn):
|
||||
conn.execute("SELECT dup", ())
|
||||
self.assertTrue(fake.conn.rolled_back)
|
||||
# 锁判定:本模块锁错误 True,其它错误 False。
|
||||
self.assertTrue(db.is_locked_error(
|
||||
PostgresOperationalError("deadlock", locked=True)))
|
||||
self.assertFalse(db.is_locked_error(ValueError("x")))
|
||||
|
||||
def test_postgres_repr_never_contains_password(self):
|
||||
skeleton = object.__new__(PostgresDatabase)
|
||||
|
||||
Reference in new issue
Block a user