Part of #4706(裁决 Q1=A / Q2=A)。从 #5702 实施期实测拆出 —— #5702 交付了 $regex 拒收与 $icontains 实现,没有交付这一条,原因见下「为什么不能在 #5702 里顺手做」。
两个方向的同一个缺陷
SqlDriver.applyLike 发的 SQL 里,大小写由方言决定,不由契约决定。两个算子各在一半方言上跑偏:
| 算子 |
契约(#4706) |
SQLite / turso / sqlite-wasm |
Postgres |
MySQL |
$contains / $notContains / $startsWith / $endsWith |
大小写敏感(Q2=A) |
❌ LIKE 自带 ASCII 折叠 |
✅ 恰好正确 |
⚠️ 随 collation |
$icontains |
折叠 ASCII only(Q1=A) |
✅ SQLite lower() 只折 ASCII |
❌ LOWER() 折全 Unicode |
❌ 同左 |
两格都是过匹配(返回了不该返回的行),不是漏匹配。
实测(better-sqlite3,FILTER_TEXT_ROWS 九行,origin/main + PR of #5702)
$contains 'acme' → ["1","2"] 契约要求 ["2"] (行 1 = "ACME Corp")
$contains 'ACME' → ["1","2"] 契约要求 ["1"]
$icontains 'acme' → ["1","2"] ✅
$icontains 'café' → ["4"] ✅(É 不折)
顺带一条对分诊有用的读数:在 SQLite 上 $contains 与 $icontains 对任意比较数返回完全相同的行 —— 因为 LIKE 已经折了 ASCII,外面再套一层 LOWER() 是幂等的。也就是说这条不修,$icontains 在 SQLite 上是行为不可观测的(它的正确性只能靠编译出的 SQL 文本和 PG 见证)。#5702 的 remote-transport-text-predicates.test.ts / sql-driver-icontains-and-retired-operators.test.ts 已把这两条如实钉住。
为什么不能在 #5702 里顺手做(这是本单独立的理由)
修 $contains 的大小写需要在 SQLite 上换构造(GLOB / instr() / 二进制 collation —— LIKE 不受 collation 影响,只能靠 PRAGMA case_sensitive_like,而那是连接级全局开关)。这个下译在三处并存,必须同批移动:
packages/drivers/driver-sql/src/sql-driver.ts 的 applyLike;
packages/drivers/driver-turso/src/remote-transport.ts 的 pushLike(remote 不走 knex,自带一份);
- RLS / 分析面的孪生实现 ——
read-scope-sql 与 packages/services/service-analytics/src/like-pattern.ts(后者的注释自陈「held to THIS expression, character for character」)。
只改 driver 不改第 3 组,等于一条权限规则编出两个行集 —— #3948 那一类 P0,比现状更糟。所以它不是 domain:drivers 单车道能闭环的改动。
验收
Part of #4706(裁决 Q1=A / Q2=A)。从 #5702 实施期实测拆出 —— #5702 交付了
$regex拒收与$icontains实现,没有交付这一条,原因见下「为什么不能在 #5702 里顺手做」。两个方向的同一个缺陷
SqlDriver.applyLike发的 SQL 里,大小写由方言决定,不由契约决定。两个算子各在一半方言上跑偏:$contains/$notContains/$startsWith/$endsWithLIKE自带 ASCII 折叠$icontainslower()只折 ASCIILOWER()折全 Unicode两格都是过匹配(返回了不该返回的行),不是漏匹配。
实测(better-sqlite3,
FILTER_TEXT_ROWS九行,origin/main+ PR of #5702)顺带一条对分诊有用的读数:在 SQLite 上
$contains与$icontains对任意比较数返回完全相同的行 —— 因为LIKE已经折了 ASCII,外面再套一层LOWER()是幂等的。也就是说这条不修,$icontains在 SQLite 上是行为不可观测的(它的正确性只能靠编译出的 SQL 文本和 PG 见证)。#5702 的remote-transport-text-predicates.test.ts/sql-driver-icontains-and-retired-operators.test.ts已把这两条如实钉住。为什么不能在 #5702 里顺手做(这是本单独立的理由)
修
$contains的大小写需要在 SQLite 上换构造(GLOB/instr()/ 二进制 collation ——LIKE不受 collation 影响,只能靠PRAGMA case_sensitive_like,而那是连接级全局开关)。这个下译在三处并存,必须同批移动:packages/drivers/driver-sql/src/sql-driver.ts的applyLike;packages/drivers/driver-turso/src/remote-transport.ts的pushLike(remote 不走 knex,自带一份);read-scope-sql与packages/services/service-analytics/src/like-pattern.ts(后者的注释自陈「held to THIS expression, character for character」)。只改 driver 不改第 3 组,等于一条权限规则编出两个行集 —— #3948 那一类 P0,比现状更糟。所以它不是
domain:drivers单车道能闭环的改动。验收
FILTER_TEXT_CASES的 5 条大小写用例($contains×2 /$startsWith/$endsWith/$notContains)一致判绿;$icontains的 ASCII-only 边界在 live PG / MySQL 格上也成立(live-dialect-matrix.testkit.ts已有基建,driver-sql 的 LIKE 转义(自标 P0 的过滤器旁路)只在 SQLite 上验证过 —— live PG + MySQL 矩阵已存在、testkit 也已存在,LIKE 族一个用例都没接进去 #5589 的方言矩阵加算子轴即可);scripts/check-driver-conformance.mjs的FILTER_TEXT_CASESDEBT 行随之清偿(drivers:$regex响亮拒收 +$icontains各后端实现(#4706 裁决 B 案 · 驱动半边) #5702 已把$regex/$icontains两个半边从这些行里划掉,剩下的就是本单)。