Skip to content

driver-sql 的 LIKE 转义(自标 P0 的过滤器旁路)只在 SQLite 上验证过 —— live PG + MySQL 矩阵已存在、testkit 也已存在,LIKE 族一个用例都没接进去 #5589

Description

@os-zhuang

#5567(analytics 三个 SQL 编译器不转义 LIKE 比较值)时,为了给 PR 里的「方言确认」找一个能站住的断言面,去查 LIKE … ESCAPE 在本仓到底被哪些引擎跑过。发现的覆盖缺口,按 Prime Directive #10 单独记在这里,unassigned。

不属于 #5567 的范围面:那一单裁的是 analytics 三个编译器自己不转义(已修,PR #5587);本条裁的是 driver-sql 那份正确实现的验证面只覆盖三个已发布方言中的一个。两者都不是「逻辑错」——本条目前没有已知的错误行为,所以是 observation-class。

事实

driver-sqlapplyLike(packages/plugins/driver-sql/src/sql-driver.ts ~:6276)转义 \ / % / _ 并绑显式 ESCAPE ?,它头上的注释自己把未转义的 % 标为 P0 过滤器旁路:

`%` matches every row (a filter-bypass, P0). Binds an explicit `ESCAPE '\'`

它的回归守卫是 packages/plugins/driver-sql/src/sql-driver-like-escape.test.ts(文件头写着 "P0-3 regression")。该文件硬编码单一方言:

driver = new SqlDriver({
  client: 'better-sqlite3',
  connection: { filename: ':memory:' },
  useNullAsDefault: true,
});

而 CI 里已经有 live Postgres 16 + MySQL 8.0 的服务容器作业(.github/workflows/ci.yml,Temporal Conformance (live PG + MySQL)),它注入 OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL,并且跑的就是整个 driver-sql 套件:

pnpm --filter @objectstack/driver-sql test

也就是说这个守卫在那个作业里也跑了,只是因为自己写死了 client,仍然只在 SQLite 上跑一遍。

实测:driver-sql 里读那两个 URL 的测试文件共 8 个,其中提及 $contains / LIKE / startsWith / endsWith 的有 0 个:

$ grep -rln "OS_TEST_POSTGRES_URL\|OS_TEST_MYSQL_URL" packages/plugins/driver-sql/src/
sql-driver-temporal-conformance.test.ts
live-dialect-matrix.testkit.ts
sql-driver-or-filter.test.ts
sql-driver-pagination-conformance.test.ts
sql-driver-date-now-default-live.test.ts
sql-driver-datetime-postgres-timezone.test.ts
sql-driver-datetime-mysql-storage.test.ts
sql-driver-time-live-dialects.test.ts
→ 其中含 LIKE 族的:0 个

注意 live-dialect-matrix.testkit.ts 已经是一个可复用的方言矩阵 testkit(ADR-0053 D-A3 那条轴用的),所以缺的不是基础设施,只是没人把 LIKE 族接上去。

为什么值得记一笔

  1. 方言差异在这条路径上是真实的,不是理论的。 三家默认转义符不一致 —— PG 默认反斜杠、MySQL 默认 \(除非 NO_BACKSLASH_ESCAPES)、SQLite 没有默认转义符。applyLike 的注释正是靠这一点解释它为什么必须绑显式 ESCAPE。恰恰是「唯一没有默认转义符」的那一个,成了唯一被跑到的那一个:守卫覆盖的是差异最小的一端。
  2. MySQL 那一侧多一条文档约束没人验证过。 MySQL 手册说 ESCAPE 的实参 "must evaluate as a constant at execution time"。applyLike 绑的是占位符 ESCAPE ?;经 knex + mysql2 的默认 query() 路径它会被客户端插值成字面量(因此满足该条件),但这是推断,不是实测 —— 而且它依赖 mysql2 走 query() 而不是 server-side prepared execute()。若哪天该路径改成真正的服务端预处理语句,这条约束是否仍满足,没有任何用例会告诉我们。
  3. 同一个 P0 的另一半刚刚才被发现过一次。 analytics 侧三个 SQL 编译器都不转义 LIKE 比较值:$contains: '_admin' 命中 xyadmin$contains: '50%' 命中 off 5012 now —— driver-sql 自己把这条旁路标为 P0 —— 实测 #5567 证明这族 pattern 拼接的缺陷会在同一个仓里被重新犯一遍(analytics 三处,全部漏)。一个只覆盖 1/3 方言的守卫,对「换个方言重新犯一遍」是看不见的。
  4. 不是「declared ≠ enforced」的代码面,而是它的测试面。 实现是对的;被断言的只是它在一个引擎上是对的。

建议(不代裁决)

sql-driver-like-escape.test.ts 的三个用例(字面 % / 字面 _ / 普通子串)接到已有的 live-dialect-matrix.testkit.ts 上,再补一条字面 \ 的用例 —— 后者是三方言分歧最大的一格(SQLite 上 \ 本就是字面量,PG/MySQL 上它是默认转义符),也是 #5587 里唯一只能靠文档 + 模拟论证、无法在 SQLite 上做前红后绿的一格。URL 缺失时按现有惯例 skip,OS_EXPECT_LIVE_DIALECT_MATRIX 已经负责把「本该跑却 skip」变成命名的红。

未验证的部分

  • 没有在真实 PG / MySQL 上跑过任何 LIKE 用例(本条正是这个缺口本身),所以不声称那两个方言上今天有错误行为;文档口径与 SQLite 实测都指向 applyLike 是对的。
  • 没有核实 knex + mysql2 在本仓配置下是否可能走服务端预处理路径(上面第 2 点的前提)。
  • 严重度请 PM 按 triage 定:按「今天没有用户会撞到」记为 observation-class(finding,不入 pm:queue),但它守的是一条被自己标为 P0 的旁路,所以两个方向都不敢自判 —— 只把实测摆在这里。

关联:#5567 / PR #5587(analytics 侧同族缺陷,本条在其「方言确认」环节被发现)、#4191(ADR-0053 D-A3 storage-form 轴的同类覆盖缺口 —— 不同范围面,那条是 Field.datetime / Field.time 的存储形态,不含 LIKE,故本条独立立单而非挂为其子单)、#5499(冻结裁决明确 driver-sql 在冻结范围)。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions