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 在冻结范围)。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions