- attempt_ledger.ensure: relax gap-3 outcome NOT NULL on Postgres (ALTER COLUMN outcome DROP NOT NULL, probe-gated); without it every resume write on a gap-3-migrated PG table fails with NotNullViolation. SQLite keeps NOT NULL (no such ALTER). - db.PgCursor: PRAGMA table_info fake rows had the notnull flag inverted (NO -> 0); now matches sqlite semantics (1 = NOT NULL). No caller read row[3] before, so this was latent. - ledger_onboarding: stale 'PG path unverified' notes updated to point at the real-PG proof. - tests: pragma_info notnull pin (fake driver) + _column_notnull probe pins (fresh + gap-3 SQLite). Proof: 28/28 checks on real Postgres 16.15 (see verification/ codex-task-attempts-pg-proof-20261106.md).
This commit is contained in:
1 parent
698398dafb
commit
2fcde375a4
5 files changed
+104
-16
No files matched your search
@@ -250,6 +250,25 @@ class AdapterFakeDriverTestCase(unittest.TestCase):
|
||||
self.assertIn("%s", fake.conn.cursor_obj.executed[0][0])
|
||||
self.assertNotIn("?", fake.conn.cursor_obj.executed[0][0])
|
||||
|
||||
def test_pragma_info_notnull_flag_matches_sqlite(self):
|
||||
# PG 实证(2026-11-06)抓到的真 bug:适配层把
|
||||
# information_schema.is_nullable 翻反了(NO -> 0),
|
||||
# 与 sqlite PRAGMA table_info 的 notnull 语义相反。
|
||||
# 此前无调用方读 row[3],故只潜伏未炸;ensure 的
|
||||
# outcome 放宽依赖该标志为真,此处钉住。
|
||||
rows = [(1, "task_id", "text", "NO", None, 1),
|
||||
(2, "outcome", "text", "YES", None, 0)]
|
||||
fake = _FakeDriver(_FakeCursor(rows=rows))
|
||||
db = _make_db(fake)
|
||||
conn = db.connect()
|
||||
info = conn.execute(
|
||||
"PRAGMA table_info(task_attempts)").fetchall()
|
||||
by_name = {row[1]: row for row in info}
|
||||
self.assertEqual(by_name["task_id"][3], 1)
|
||||
self.assertEqual(by_name["outcome"][3], 0)
|
||||
self.assertEqual([row[1] for row in info],
|
||||
["task_id", "outcome"])
|
||||
|
||||
def test_unique_violation_maps_to_integrity_error(self):
|
||||
import sqlite3
|
||||
fake = _FakeDriver(_FakeCursor(fail_sequence=[_FakeUnique("dup")]))
|
||||
|
||||
@@ -30,6 +30,7 @@ from cloud.attempt_ledger import ( # noqa: E402
|
||||
SQLITE_DDL,
|
||||
TASK_ATTEMPTS_DDL,
|
||||
UNIFIED_COLUMNS,
|
||||
_column_notnull,
|
||||
aggregate_attempts,
|
||||
ensure_task_attempts_table,
|
||||
read_attempt_rows,
|
||||
@@ -133,6 +134,31 @@ class TestFreshAndMigrations(unittest.TestCase):
|
||||
self.assertEqual(self.conn.execute(
|
||||
"SELECT COUNT(*) FROM task_attempts").fetchone()[0], 1)
|
||||
|
||||
def test_column_notnull_probe(self):
|
||||
# PG 实证(2026-11-06):ensure 的 outcome 放宽依赖
|
||||
# PRAGMA row[3] 的 notnull 语义;SQLite 侧钉住探针本身。
|
||||
ensure_task_attempts_table(self.conn)
|
||||
flags = _column_notnull(self.conn, "task_attempts")
|
||||
self.assertFalse(flags["outcome"])
|
||||
self.assertTrue(flags["task_id"])
|
||||
self.assertTrue(flags["attempt_no"])
|
||||
self.assertTrue(flags["is_defect"])
|
||||
|
||||
def test_column_notnull_probe_sees_gap3_outcome(self):
|
||||
# gap-3 形态的 outcome 是 NOT NULL;探针必须报 True,
|
||||
# 否则 PG 上的放宽不会触发,恢复写路仍会炸。
|
||||
self.conn.execute(GAP3_DDL)
|
||||
flags = _column_notnull(self.conn, "task_attempts")
|
||||
self.assertTrue(flags["outcome"])
|
||||
# SQLite 上放宽语句不合法、被 ensure 吞掉:迁移本身
|
||||
# 不抛错、列集仍齐,但 outcome 保持 NOT NULL(需重建
|
||||
# 表才能改,见批总结诚实边界;本批只修 PG 路径)。
|
||||
ensure_task_attempts_table(self.conn)
|
||||
self.assertEqual(set(_columns(self.conn)),
|
||||
set(UNIFIED_COLUMNS))
|
||||
self.assertTrue(
|
||||
_column_notnull(self.conn, "task_attempts")["outcome"])
|
||||
|
||||
def test_old_shape_migrates_with_rows_intact(self):
|
||||
self.conn.execute(OLD_DDL)
|
||||
self.conn.execute(
|
||||
|
||||
Reference in new issue
Block a user