From 28c3c8d8abdacd5d12d536979c56e2c169fdf749 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Wed, 19 Aug 2026 16:03:42 +0800 Subject: [PATCH] Release v1.0.91: document no SQLite FK between business tables --- CHANGELOG.md | 6 +++++ SKILL.md | 2 +- assets/examples/version-response.json | 2 +- development/CHECKLIST.md | 1 + development/DEVELOPMENT.md | 1 + references/ACTIONS.md | 2 +- references/SCHEMA.md | 25 ++++++++++++++++--- scripts/db/display_metadata_validator.py | 27 ++++++++++++++++++++- scripts/util/constants.py | 2 +- tests/test_display_metadata.py | 31 ++++++++++++++++++++++++ tests/test_docs_standards.py | 3 +++ 11 files changed, 93 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a30d639..f457a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.91 + +- 沉淀「表与表之间禁止 SQLite 外键」契约:只存业务 id,关联与级联在 repository/service;界面用 `lookup` +- `display_metadata_validator` 扫描业务表 DDL,含 `FOREIGN KEY` / `REFERENCES` 时报错 +- 交活 Checklist 新增 A6-6;`lookup` 文档用语改为「跨表引用 id」,避免与 SQLite 外键混淆 + ## 1.0.90 - 沉淀数据管理表格说明契约:`_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help` diff --git a/SKILL.md b/SKILL.md index 7c1f066..5063b50 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.90 +version: 1.0.91 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/assets/examples/version-response.json b/assets/examples/version-response.json index 8b5992a..d893e97 100644 --- a/assets/examples/version-response.json +++ b/assets/examples/version-response.json @@ -1,4 +1,4 @@ { - "version": "1.0.0", + "version": "1.0.91", "skill": "your-skill-slug" } diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index 6f413b4..2545dbe 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -129,6 +129,7 @@ checklist_version: 1.0 - [ ] A6-3 **P1** 重复执行同一业务键时,结果符合宣称(更新/去重/追加),无莫名脏数据或静默丢数据 - [ ] A6-4 **P1** 「零结果成功」与「失败」可区分 - [ ] A6-5 **P1** 若产出文件:落在约定数据位置且可找到,不丢在不明临时路径 +- [ ] A6-6 **P0** 有业务表时:`sqlite_master` 中业务表 DDL **不得**含 SQLite `FOREIGN KEY` / `REFERENCES`;跨表只存业务 id,关联与级联在 repository/service;界面名称用 `lookup`(见 `references/SCHEMA.md`);可跑 `validate_template_database` / `init-db` 后自检 --- diff --git a/development/DEVELOPMENT.md b/development/DEVELOPMENT.md index 9cfeeab..f898952 100644 --- a/development/DEVELOPMENT.md +++ b/development/DEVELOPMENT.md @@ -527,6 +527,7 @@ assets/actions.json → placements / bind / executionProfile / entry - `db/connection.py` 只做连接和建表(模板默认只建 `_jiangchang_*` 元数据表;业务表按需 CREATE) + **业务表之间禁止 SQLite `FOREIGN KEY` / `REFERENCES`**:只存业务 id,存在性 / 级联 / 删除在 repository/service 处理(见 [`references/SCHEMA.md`](../references/SCHEMA.md)「表与表之间:禁止 SQLite 外键」) - `db/_repository.py` 只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表) diff --git a/references/ACTIONS.md b/references/ACTIONS.md index 35ff898..b671cca 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -622,7 +622,7 @@ HINT: | 字段 | 必填 | 说明 | |------|------|------| | `ref_table` | 是 | 同库被引用表 | -| `value_column` | 是 | 提交给 CLI 的值(编号 / 外键) | +| `value_column` | 是 | 提交给 CLI 的值(业务 id / 编号) | | `label_column` | 是 | 主文案列 | | `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) | | `filter_column` / `filter_value` | 否 | 等值过滤 | diff --git a/references/SCHEMA.md b/references/SCHEMA.md index fab5592..028f42d 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -89,6 +89,23 @@ metadata: 若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。 +## 表与表之间:禁止 SQLite 外键 + +业务表之间的「属于哪条父记录 / 引用哪条来源」**只存业务 id**(整数或文本标量),**禁止**在 DDL 中写 SQLite `FOREIGN KEY` / `REFERENCES`,也**不要**指望 `PRAGMA foreign_keys = ON` 替你校验或级联。 + +| 要做的事 | 正确做法 | 禁止 | +|----------|----------|------| +| 子表指向父表 | 子表列如 `case_id INTEGER NOT NULL`;写入前在 repository/service 查父表是否存在 | `FOREIGN KEY (case_id) REFERENCES parent(id)` | +| 父表记「最新一版」 | 父表列如 `latest_draft_id INTEGER NOT NULL DEFAULT 0`;由 service 更新指针 | 同上 | +| 删除 / 覆盖 / 级联清理 | repository/service 内显式 `DELETE` / `UPDATE`;需要时用 Action `steps` 的 `deleteByFk` | 依赖 DB 级联或宿主 CRUD 被 FK 拦住 | +| 数据管理里显示名称 | `_jiangchang_columns.display_type = lookup` + `options_json` 对象(见下文「跨表引用 lookup」) | 把 lookup 当成 DDL 外键 | + +**为什么:** 宿主数据管理对业务表执行通用增删改,会直接写 SQLite。一旦 DDL 带外键,删除、迁移 rename 父表、或父表 id 变更时极易出现「本地数据操作失败」且难排查。关联语义属于技能业务,必须在代码里写清。 + +**迁移注意:** 避免「`ALTER TABLE parent RENAME TO parent_old` → 建新 parent → 删 old」这类会隐性改写子表 FK 指向的套路;优先 `ADD COLUMN` + 拷列,或重建子表且**不带** FK。 + +`display_metadata_validator` 与 `validate_template_database` 会扫描业务表 DDL;含 `FOREIGN KEY` / `REFERENCES` 时报错(见 `development/CHECKLIST.md` A6-6)。 + ## 标准数据同步语义(外源 → 本地库) 适用于「同步 / refresh / collect」类能力。使用通用名称表述,勿把具体平台表名写进模板实现。 @@ -291,7 +308,7 @@ upsert_column_metadata( | `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 | | `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` | | (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` | -| `lookup` | 外键式标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 关联键值 | +| `lookup` | 跨表引用 id 标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 业务 id(非 SQLite 外键) | | (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) | | `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` | | `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 | @@ -554,9 +571,9 @@ CREATE TABLE demo_items ( 宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。 -### 2)表关联 lookup(对象,推荐用于外键式字段) +### 2)跨表引用 lookup(对象,用于关联 id 字段) -当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组): +当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单下拉**契约,**不是** SQLite 外键: ```json { @@ -572,7 +589,7 @@ CREATE TABLE demo_items ( | 字段 | 必填 | 说明 | |---|---|---| | `ref_table` | 是 | 同库被引用表名(snake_case 标识符) | -| `value_column` | 是 | 写入当前字段的值列(编号 / 外键) | +| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) | | `label_column` | 是 | 下拉主文案列(名称) | | `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 | | `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) | diff --git a/scripts/db/display_metadata_validator.py b/scripts/db/display_metadata_validator.py index 4950ed8..2d1c10b 100644 --- a/scripts/db/display_metadata_validator.py +++ b/scripts/db/display_metadata_validator.py @@ -30,6 +30,7 @@ _ENGLISH_PLACEHOLDER_NAME_PATTERNS = ( ) _KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") _CJK_RE = re.compile(r"[\u4e00-\u9fff]") +_FK_DDL_PATTERN = re.compile(r"\bFOREIGN\s+KEY\b|\bREFERENCES\b", re.I) @dataclass @@ -412,6 +413,27 @@ def _table_exists(cursor, table_name: str) -> bool: return cursor.fetchone() is not None +def validate_no_sqlite_foreign_keys(cursor) -> tuple[list[str], list[str]]: + """业务表 DDL 不得含 FOREIGN KEY / REFERENCES(关联走 repository/service)。""" + errors: list[str] = [] + cursor.execute( + """ + SELECT name, sql FROM sqlite_master + WHERE type = 'table' AND name NOT LIKE 'sqlite_%' + """ + ) + for table_name, sql in cursor.fetchall(): + if table_name in METADATA_TABLES: + continue + ddl = str(sql or "") + if ddl and _FK_DDL_PATTERN.search(ddl): + errors.append( + f"业务表 {table_name!r} 的 DDL 含 SQLite FOREIGN KEY/REFERENCES;" + "请改为业务 id 列 + repository/service 逻辑关联(见 references/SCHEMA.md)" + ) + return errors, [] + + def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: bool = True) -> ValidationReport: report = ValidationReport() cur = conn.cursor() @@ -425,6 +447,9 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b if not has_tables_meta or not has_columns_meta: return report + fk_errors, _ = validate_no_sqlite_foreign_keys(cur) + report.errors.extend(fk_errors) + for table_name in iter_user_tables(cur): cur.execute( "SELECT display_name FROM _jiangchang_tables WHERE table_name = ?", @@ -514,5 +539,5 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b def validate_template_database(conn: sqlite3.Connection) -> ValidationReport: - """模板自检入口:校验展示元数据完整性。模板默认无业务表。""" + """模板自检入口:校验展示元数据完整性,并扫描业务表 DDL 是否含 SQLite 外键。""" return validate_display_metadata(conn) diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 4ffaf17..f97db33 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.90" +SKILL_VERSION = "1.0.91" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/test_display_metadata.py b/tests/test_display_metadata.py index ffb9bb0..92ebfc1 100644 --- a/tests/test_display_metadata.py +++ b/tests/test_display_metadata.py @@ -225,6 +225,37 @@ class TestDisplayMetadata(unittest.TestCase): finally: conn.close() + def test_validate_rejects_sqlite_foreign_keys_on_business_tables(self) -> None: + from db.display_metadata import ( + _JIANGCHANG_COLUMNS_DDL, + _JIANGCHANG_TABLES_DDL, + ) + from db.display_metadata_validator import validate_display_metadata + + conn = sqlite3.connect(":memory:") + try: + conn.execute(_JIANGCHANG_TABLES_DDL) + conn.execute(_JIANGCHANG_COLUMNS_DDL) + conn.execute("CREATE TABLE parent_rows (id INTEGER PRIMARY KEY, title TEXT)") + conn.execute( + """ + CREATE TABLE child_rows ( + id INTEGER PRIMARY KEY, + parent_id INTEGER NOT NULL, + FOREIGN KEY (parent_id) REFERENCES parent_rows(id) + ) + """ + ) + report = validate_display_metadata(conn) + self.assertFalse(report.ok) + self.assertTrue( + any("FOREIGN KEY" in err or "REFERENCES" in err for err in report.errors), + msg=f"expected FK error, got: {report.errors}", + ) + self.assertTrue(any("child_rows" in err for err in report.errors)) + finally: + conn.close() + def test_validate_missing_both_metadata_tables(self) -> None: conn = sqlite3.connect(":memory:") try: diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index 2ec1c86..31c8646 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -350,6 +350,9 @@ class TestDocsStandards(unittest.TestCase): "默认展示列", "grid_help", "表格说明 `grid_help`", + "表与表之间:禁止 SQLite 外键", + "禁止 SQLite 外键", + "跨表引用 lookup", ): self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")