发现于 #6418 的实施(照 view-definition-active-index.ts 写 sys_metadata 的同类排重查询时逐行比对到)。不在 PR #6770 范围内——那条的文件面明确要求 view-definition-active-index.ts 的行为保持 byte-identical。
事实
packages/metadata-protocol/src/migrations/view-definition-active-index.ts 的 buildDuplicateProbeSql():
SELECT name, organization_id, owner, COUNT(*) AS duplicate_rows
FROM sys_view_definition WHERE state = 'active'
GROUP BY name, COALESCE(organization_id, '__global__'), COALESCE(owner, '')
HAVING COUNT(*) > 1
organization_id 与 owner 在 SELECT 列表里是裸列,但在 GROUP BY 里只以 COALESCE(...) 表达式的形式出现。PostgreSQL 的 GROUP BY 语义要求 select 列表中的非聚合列必须逐字出现在 GROUP BY 里(或函数依赖于其中的列);包在表达式里不算。于是这条查询在 PG 上直接报:
ERROR: column "sys_view_definition.organization_id" must appear in the GROUP BY clause
or be used in an aggregate function
SQLite 宽容,所以现有测试(view-definition-active-index.test.ts 的 a pre-existing duplicate pair blocks the tightening,真 SQLite)照常绿——它确实证明了这条查询能列出冲突行,但只在 SQLite 上。
函数注释目前写的是「Dialect-neutral: COALESCE, GROUP BY and HAVING are ANSI on every engine this platform runs on」。COALESCE/GROUP BY/HAVING 三个构件确实是 ANSI,但这条查询整体不是。
后果
这条查询不是装饰字符串——它被塞进 error 级降级报告里交给运维,ADR-0120 D4 的「name the offending rows」就靠它。而它失效的恰好是能建这个索引的两种方言之一:
- SQLite:能建 partial index,冲突分支可达,查询能跑 ✅
- PostgreSQL:能建 partial index,冲突分支可达,查询报错 ❌
- MySQL/MariaDB:建不了 partial index,走的是
unsupported 分支(同一条查询也附在那条消息里,同样跑不了——不过那条路径上 PG 不参与)
也就是说,PG 上的运维照着报错信息复制粘贴,拿到的是第二个 SQL 错误,而不是冲突行清单。
修法(已有现成参照,就在隔壁)
PR #6770 给 sys_metadata 写同类查询时把折叠列通过它自己的 COALESCE 投影并起别名,而不是投影裸列:
SELECT type, name, organization_id, COALESCE(package_id, '') AS package_id_key, COUNT(*) AS duplicate_rows
FROM sys_metadata WHERE state = 'active'
GROUP BY type, name, organization_id, COALESCE(package_id, '') HAVING COUNT(*) > 1
每个裸投影都是裸的 GROUP BY 项,被折叠的那个只经由自己的表达式投影 —— 两种方言都合法。见 packages/metadata-protocol/src/migrations/overlay-index.ts 的 buildOverlayDuplicateProbeSql() 与对应测试用例 the duplicate-listing query is groupable on PostgreSQL, not only SQLite。
view-definition 这边有两个折叠列(organization_id → '__global__',owner → ''),照抄即可,同时把那句 "Dialect-neutral" 注释改成如实描述。
严重度自评(不可靠,交 triage 定)
只在降级路径上可见,且只影响运维拿到的那一条排查命令,不影响任何写入或索引本身。但它属于「声明了却交付不了」那一类:报告承诺给出冲突行,在 PG 上给不出。按 #4949 的口径如实上报,不自行判级。
发现于 #6418 的实施(照
view-definition-active-index.ts写sys_metadata的同类排重查询时逐行比对到)。不在 PR #6770 范围内——那条的文件面明确要求view-definition-active-index.ts的行为保持 byte-identical。事实
packages/metadata-protocol/src/migrations/view-definition-active-index.ts的buildDuplicateProbeSql():organization_id与owner在 SELECT 列表里是裸列,但在 GROUP BY 里只以COALESCE(...)表达式的形式出现。PostgreSQL 的GROUP BY语义要求 select 列表中的非聚合列必须逐字出现在 GROUP BY 里(或函数依赖于其中的列);包在表达式里不算。于是这条查询在 PG 上直接报:SQLite 宽容,所以现有测试(
view-definition-active-index.test.ts的a pre-existing duplicate pair blocks the tightening,真 SQLite)照常绿——它确实证明了这条查询能列出冲突行,但只在 SQLite 上。函数注释目前写的是「Dialect-neutral:
COALESCE,GROUP BYandHAVINGare ANSI on every engine this platform runs on」。COALESCE/GROUP BY/HAVING三个构件确实是 ANSI,但这条查询整体不是。后果
这条查询不是装饰字符串——它被塞进
error级降级报告里交给运维,ADR-0120 D4 的「name the offending rows」就靠它。而它失效的恰好是能建这个索引的两种方言之一:unsupported分支(同一条查询也附在那条消息里,同样跑不了——不过那条路径上 PG 不参与)也就是说,PG 上的运维照着报错信息复制粘贴,拿到的是第二个 SQL 错误,而不是冲突行清单。
修法(已有现成参照,就在隔壁)
PR #6770 给
sys_metadata写同类查询时把折叠列通过它自己的 COALESCE 投影并起别名,而不是投影裸列:每个裸投影都是裸的 GROUP BY 项,被折叠的那个只经由自己的表达式投影 —— 两种方言都合法。见
packages/metadata-protocol/src/migrations/overlay-index.ts的buildOverlayDuplicateProbeSql()与对应测试用例the duplicate-listing query is groupable on PostgreSQL, not only SQLite。view-definition这边有两个折叠列(organization_id→'__global__',owner→''),照抄即可,同时把那句 "Dialect-neutral" 注释改成如实描述。严重度自评(不可靠,交 triage 定)
只在降级路径上可见,且只影响运维拿到的那一条排查命令,不影响任何写入或索引本身。但它属于「声明了却交付不了」那一类:报告承诺给出冲突行,在 PG 上给不出。按 #4949 的口径如实上报,不自行判级。