From 8afcd3253e299c2dab2286b4014787247264c296 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 01:55:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(automations):=20=E5=AE=A1=E8=AE=A1=E8=A1=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=98=BE=E5=BC=8F=E6=8F=92=20TEXT=20id?= =?UTF-8?q?=EF=BC=8CPG=20identity=20=E4=B8=8B=E4=B8=8D=E5=86=8D=E4=B8=A2?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧 _audit_row 显式插 uuid 当 audit_log.id:SQLite 下能跑, 生产 PG 的 id 是 BIGINT GENERATED BY DEFAULT AS IDENTITY, 插 TEXT 直接报错、审计行全丢(best-effort 吞掉后无声无息, #156 留下的已知局限)。修法:INSERT 一律不带 id,让两端 自增/identity 生成;表不存在时先按 PG 口径建、再按 SQLite 口径建、逐个重试——automations 按设计不 import 其它 cloud 模块,所以用先插、失败再按口径重试做方言探测。 单测:test_automation_run_due 4 项(含新增 PG-like 假连接 1 项:只接受不带 id 的 INSERT,旧实现在此应失败)。 --- cloud/automations.py | 48 +++++++++++++++++++------- cloud/tests/test_automation_run_due.py | 38 ++++++++++++++++++++ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/cloud/automations.py b/cloud/automations.py index 6082fa3..31248df 100644 --- a/cloud/automations.py +++ b/cloud/automations.py @@ -566,26 +566,50 @@ def _automation_quota_exceeded(conn, owner): def _audit_row(conn, project_id, action, detail, now): - """best-effort 写审计行:失败不影响自动化运行本身。""" + """best-effort 写审计行:失败不影响自动化运行本身。 + + 旧实现显式插 TEXT uuid 当 id:SQLite 下能跑,但生产 PG 的 + audit_log.id 是 BIGINT GENERATED BY DEFAULT AS IDENTITY, + 插 TEXT 直接报错、审计行全丢(best-effort 吞掉后无声无息)。 + 修法:INSERT 一律不带 id,让两端自增/identity 生成; + 表不存在时先按 PG 口径建、再按 SQLite 口径建、逐个重试, + 哪边建表成功就用哪边——automations 按设计不 import 其它 + cloud 模块,所以用“先插、失败再按口径重试”做方言探测, + 不靠 import 判断后端。 + """ + params = ("system", project_id, action, detail, now) try: conn.execute( - "CREATE TABLE IF NOT EXISTS audit_log(" - "id TEXT PRIMARY KEY, " - "user_id TEXT, " - "project_id TEXT, " - "action TEXT, " - "detail TEXT, " - "created_at DOUBLE PRECISION)") - conn.execute( - "INSERT INTO audit_log(id, user_id, project_id, action," - " detail, created_at) VALUES(?, 'system', ?, ?, ?, ?)", - (uuid.uuid4().hex, project_id, action, detail, now)) + "INSERT INTO audit_log(user_id, project_id, action," + " detail, created_at) VALUES(?, ?, ?, ?, ?)", params) conn.commit() + return except Exception: try: conn.rollback() except Exception: pass + for ddl in ( + "CREATE TABLE IF NOT EXISTS audit_log(" + "id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " + "user_id TEXT, project_id TEXT, action TEXT, detail TEXT, " + "created_at DOUBLE PRECISION)", + "CREATE TABLE IF NOT EXISTS audit_log(" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "user_id TEXT, project_id TEXT, action TEXT, detail TEXT, " + "created_at REAL)"): + try: + conn.execute(ddl) + conn.execute( + "INSERT INTO audit_log(user_id, project_id, action," + " detail, created_at) VALUES(?, ?, ?, ?, ?)", params) + conn.commit() + return + except Exception: + try: + conn.rollback() + except Exception: + pass def run_due_automation(conn, automation_id, now=None): diff --git a/cloud/tests/test_automation_run_due.py b/cloud/tests/test_automation_run_due.py index af2d00b..77150a3 100644 --- a/cloud/tests/test_automation_run_due.py +++ b/cloud/tests/test_automation_run_due.py @@ -6,6 +6,7 @@ import time import unittest from cloud.automations import ( + _audit_row, create_automation, ensure_automations_tables, list_automation_runs, @@ -48,6 +49,43 @@ def _set_quota(db_path, user_id, micros): conn.close() +class AuditRowPgIdentityTestCase(unittest.TestCase): + """模拟 PG:id 是 BIGINT identity,显式插 TEXT uuid 会报错。 + + 假连接只接受不带 id 的 INSERT;旧实现(带 TEXT id)在此 + 假连接上应失败、新实现应成功且审计行落盘。 + """ + + class _PgLikeConn: + def __init__(self): + self.rows = [] + self.committed = False + + def execute(self, sql, params=()): + if sql.strip().upper().startswith("INSERT INTO AUDIT_LOG(ID"): + raise ValueError("column \"id\" is of type bigint") + if sql.strip().upper().startswith("INSERT INTO AUDIT_LOG"): + self.rows.append(params) + return None + raise ValueError("unexpected sql in fake PG conn") + + def commit(self): + self.committed = True + + def rollback(self): + pass + + def test_insert_without_id_succeeds_on_pg_like_conn(self): + conn = self._PgLikeConn() + _audit_row(conn, "p1", "automation_run_created", + "auto1 -> task1", 123.0) + self.assertTrue(conn.committed) + self.assertEqual(len(conn.rows), 1) + # 参数里没有 uuid:第一位是 system,第二位是 project_id。 + self.assertEqual(conn.rows[0][0], "system") + self.assertEqual(conn.rows[0][1], "p1") + + class RunDueAutomationTestCase(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory()