做 #5557 (convertFilter 把 contains 送成未声明的 $regex)时,为了给「比较值带元字符」的用例挑一个能站住的断言面,必须逐个核对 $contains 落到各个编译器之后到底绑出什么 pattern 。在这一步发现的分叉。
不属于 #5557 的范围面 (那一单裁的是「送 $regex 还是送 $contains」这个算子选择,修完之后 $contains 到达驱动,driver-sql 会正确转义;本条裁的是 analytics 自己那三个 SQL 编译器收到正确的 $contains 之后 怎么拼 LIKE pattern),按 Prime Directive #10 单独记在这里,unassigned。也不在 #5526 的范围面内:#5526 裁的是 unknown 到 string 再回来的类型 往返有损('007' 绑成 7),本条的字符串一路都保真,坏在它被原样插进了一个 pattern 位置。
位置
三处,都是 values[0] 直接做字符串拼接,没有转义、也没有 ESCAPE 子句:
对照:packages/plugins/driver-sql/src/sql-driver.ts 的 applyLike(约 :6276)是 转义的,而且绑了显式 ESCAPE '\'(注释说 SQLite 不认默认转义符),它自己在 applyContainsLike 头上写着:
% matches every row (a filter-bypass, P0). Binds an explicit ESCAPE '\'
也就是同一族 pattern 拼接,驱动那一侧已经按 P0 处理过了,analytics 这三处没有跟上。
实测
绑定层(三个编译器,name 为字符串维度):
编译器
算子
比较值
绑出的 param
read-scope-sql
$contains
_admin
%_admin%
read-scope-sql
$startsWith
_admin
_admin%
read-scope-sql
$endsWith
_admin
%_admin
read-scope-sql
$contains
50%
%50%%
read-scope-sql
$contains
a_b
%a_b%
native-sql-strategy
$contains
_admin
%_admin%
native-sql-strategy
$contains
50%
%50%%
objectql-strategy(回显)
$contains
_admin
%_admin%
objectql-strategy(回显)
$contains
50%
%50%%
行结果(真 SQLite / sql.js,4 行:x_admin / xyadmin / off 50% now / off 5012 now,经 compileScopedFilterToSql 编译后执行):
where
取到
应为
{name: {$contains: '_admin'}}
['1','2']
['1']
{name: {$contains: '50%'}}
['3','4']
['3']
_ 在 LIKE 里是单字符通配 ,所以 xyadmin 被 _admin 命中;% 是多字符通配,所以 off 5012 now 被 50% 命中。两条方向都是放大 。
为什么是 bug
read-scope-sql.ts 这一处的放大是越权,不是降级的筛选。 这个编译器编的是 ADR-0021 D-C 的读作用域(tenant + RLS),整个文件的其余部分是 fail-closed 写的($in 不是数组就抛、嵌套关系就抛、未知算子就抛)—— 唯独 LIKE 族安静地把作者的字面量当成 pattern。一条 {owner_name: {$contains: '_admin'}} 形状的作用域会多放进它没打算放的行。这与 $null 的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347 / driver-memory 的实时查询路径根本不支持 $not —— mingo 抛无 code 的 MingoError,CEL !expr 降下来的 RLS scope 在该驱动上直接 500 #5324 在同一个文件上立的规矩(「在读作用域上,放大是越权,不是降级的筛选」)是同一句话。
_ 撞车是真实业务形状,不是构造出来的边角。 下划线在机器名 / 前缀约定里到处都是(sys_user、crm_account、_admin、app_),而 ObjectStack 自己的命名规范(Prime Directive Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3 :machine names → snake_case)保证了每一个 snake_case 比较值都带至少一个 LIKE 通配符。作者按名字筛一个 snake_case 值,几乎必然踩到。% 那半更容易被看见(折扣、百分比),但 _ 那半更普遍且完全无声。
declared ≠ enforced,而且驱动这半已经做对了。 同一个 $contains 在 driver-sql 上是转义的字面子串、在 analytics 的三个编译器上是 pattern —— 同一个 dashboard 走 NativeSQLStrategy(有 executeRawSql)和走 ObjectQLStrategy(经引擎到 driver-sql)会返回不同行集,这正是 ObjectQLStrategy.convertFilter 把 $contains 送成未声明的 $regex(比较值不转义),三个同族算子早在 #4128 已改成规范算子 —— 实测 #5557 论证第 3 条的形状,只是换了一层。
回显那一处方向相反,但也不一致。 objectql-strategy.ts 的 LIKE_SQL_OPS 只喂给 /analytics/sql 的回显字符串,不执行 —— 但它存在的唯一理由是复现执行(/analytics/sql 回显的 SQL 丢掉 $startsWith / $endsWith 谓词:回显比实际执行的查询更宽,无法复现结果 #5333 的原话)。若只修执行侧不修回显侧,回显又会和执行分叉,是 fix(analytics): enforce read scope on the ObjectQL aggregate path (#3597) #3601 / analytics: 让 executeAggregate 桥携带 ExecutionContext(#3597 的纵深防御第二层)+ 两处残留无 scope 调用 #3602 / analytics: ObjectQLStrategy 静默忽略 timeDimensions[].dateRange —— 恰好是 date-granularity 图表必走的那条路 #3650 那一族。三处应当一起看。
建议(不代裁决)
未验证的部分
关联:#5557 / PR #5566 (同一轮核对里发现;它裁的是算子选择,修完之后本条才是唯一剩下的转义缺口)、#5333 / PR #5558 (LIKE_SQL_OPS 这张表的来源)、#4706 ($regex 在 driver-sql 上编译成子串 LIKE 的语义待裁)、#5234 (driver-sql 的 LIKE 族对象比较值)、#5444 (driver-memory analytics 回显的 contains 少了 % —— 方向相反的同族缺陷)、#5347 / #5324 (read-scope-sql 上「放大即越权」的先例)。
做 #5557(
convertFilter把contains送成未声明的$regex)时,为了给「比较值带元字符」的用例挑一个能站住的断言面,必须逐个核对$contains落到各个编译器之后到底绑出什么 pattern。在这一步发现的分叉。不属于 #5557 的范围面(那一单裁的是「送
$regex还是送$contains」这个算子选择,修完之后$contains到达驱动,driver-sql会正确转义;本条裁的是 analytics 自己那三个 SQL 编译器收到正确的$contains之后怎么拼 LIKE pattern),按 Prime Directive #10 单独记在这里,unassigned。也不在 #5526 的范围面内:#5526 裁的是unknown到string再回来的类型往返有损('007'绑成7),本条的字符串一路都保真,坏在它被原样插进了一个 pattern 位置。位置
三处,都是
values[0]直接做字符串拼接,没有转义、也没有ESCAPE子句:packages/services/service-analytics/src/read-scope-sql.ts的compileOperator(约 :237):packages/services/service-analytics/src/strategies/native-sql-strategy.ts的likePattern(约 :666)—— 这是实际执行的那一条;packages/services/service-analytics/src/strategies/objectql-strategy.ts的LIKE_SQL_OPS(约 :39)——/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词:回显比实际执行的查询更宽,无法复现结果 #5333 / PR fix(service-analytics):/analytics/sql回显补上$startsWith/$endsWith谓词 (#5333) #5558 新建的表,只用于/analytics/sql回显。对照:
packages/plugins/driver-sql/src/sql-driver.ts的applyLike(约 :6276)是转义的,而且绑了显式ESCAPE '\'(注释说 SQLite 不认默认转义符),它自己在applyContainsLike头上写着:也就是同一族 pattern 拼接,驱动那一侧已经按 P0 处理过了,analytics 这三处没有跟上。
实测
绑定层(三个编译器,
name为字符串维度):read-scope-sql$contains_admin%_admin%read-scope-sql$startsWith_admin_admin%read-scope-sql$endsWith_admin%_adminread-scope-sql$contains50%%50%%read-scope-sql$containsa_b%a_b%native-sql-strategy$contains_admin%_admin%native-sql-strategy$contains50%%50%%objectql-strategy(回显)$contains_admin%_admin%objectql-strategy(回显)$contains50%%50%%行结果(真 SQLite /
sql.js,4 行:x_admin/xyadmin/off 50% now/off 5012 now,经compileScopedFilterToSql编译后执行):where{name: {$contains: '_admin'}}['1','2']['1']{name: {$contains: '50%'}}['3','4']['3']_在 LIKE 里是单字符通配,所以xyadmin被_admin命中;%是多字符通配,所以off 5012 now被50%命中。两条方向都是放大。为什么是 bug
read-scope-sql.ts这一处的放大是越权,不是降级的筛选。 这个编译器编的是 ADR-0021 D-C 的读作用域(tenant + RLS),整个文件的其余部分是 fail-closed 写的($in不是数组就抛、嵌套关系就抛、未知算子就抛)—— 唯独 LIKE 族安静地把作者的字面量当成 pattern。一条{owner_name: {$contains: '_admin'}}形状的作用域会多放进它没打算放的行。这与$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347 / driver-memory 的实时查询路径根本不支持$not—— mingo 抛无 code 的 MingoError,CEL!expr降下来的 RLS scope 在该驱动上直接 500 #5324 在同一个文件上立的规矩(「在读作用域上,放大是越权,不是降级的筛选」)是同一句话。_撞车是真实业务形状,不是构造出来的边角。 下划线在机器名 / 前缀约定里到处都是(sys_user、crm_account、_admin、app_),而 ObjectStack 自己的命名规范(Prime Directive Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3:machine names →snake_case)保证了每一个 snake_case 比较值都带至少一个 LIKE 通配符。作者按名字筛一个snake_case值,几乎必然踩到。%那半更容易被看见(折扣、百分比),但_那半更普遍且完全无声。$contains在driver-sql上是转义的字面子串、在 analytics 的三个编译器上是 pattern —— 同一个 dashboard 走NativeSQLStrategy(有executeRawSql)和走ObjectQLStrategy(经引擎到 driver-sql)会返回不同行集,这正是ObjectQLStrategy.convertFilter把$contains送成未声明的$regex(比较值不转义),三个同族算子早在 #4128 已改成规范算子 —— 实测 #5557 论证第 3 条的形状,只是换了一层。objectql-strategy.ts的LIKE_SQL_OPS只喂给/analytics/sql的回显字符串,不执行 —— 但它存在的唯一理由是复现执行(/analytics/sql回显的 SQL 丢掉$startsWith/$endsWith谓词:回显比实际执行的查询更宽,无法复现结果 #5333 的原话)。若只修执行侧不修回显侧,回显又会和执行分叉,是 fix(analytics): enforce read scope on the ObjectQL aggregate path (#3597) #3601 / analytics: 让 executeAggregate 桥携带 ExecutionContext(#3597 的纵深防御第二层)+ 两处残留无 scope 调用 #3602 / analytics: ObjectQLStrategy 静默忽略 timeDimensions[].dateRange —— 恰好是 date-granularity 图表必走的那条路 #3650 那一族。三处应当一起看。建议(不代裁决)
escapeLikePattern(v)(转义\、%、_)+ 每处LIKE ? ESCAPE '\',与driver-sql的applyLike对齐 —— 三个编译器都改,回显那一处也改(否则回显与执行再次分叉)。escapeLikePattern放哪里需要裁:driver-sql自己那份是私有方法,analytics 不依赖任何驱动,所以要么在@objectstack/core起一个共享的、要么各自实现(第二份实现正是 A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 那一族缺陷的来源)。ESCAPE子句的方言差异需要确认:SQLite / Postgres / MySQL 对LIKE … ESCAPE '\'的支持我只在 SQLite(sql.js)上跑过。read-scope-sql.ts的输出是拼进上层语句的,bind()只管?,所以ESCAPE加在哪一层也要一起看。$notContains同样受影响(方向是缩小:多排除行),表里没单独列,机制同一条。未验证的部分
sql.js)上实测了行集。没有在 Postgres / MySQL 上跑,也没有确认各自的ESCAPE方言。$contains的行集;$startsWith/$endsWith/$notContains只测到绑定层(pattern 里带着未转义的_/%),没有逐个跑行集。where带_或%的比较值。严重度请 PM 按 triage 定 —— 我倾向它比读起来严重(snake_case是本仓强制的命名规范,见上第 2 点),但ObjectQLStrategy.convertFilter把$contains送成未声明的$regex(比较值不转义),三个同族算子早在 #4128 已改成规范算子 —— 实测 #5557 的分诊经验是这类判断两个方向都不可靠,所以只把实测摆在这里。关联:#5557 / PR #5566(同一轮核对里发现;它裁的是算子选择,修完之后本条才是唯一剩下的转义缺口)、#5333 / PR #5558(
LIKE_SQL_OPS这张表的来源)、#4706($regex在 driver-sql 上编译成子串 LIKE 的语义待裁)、#5234(driver-sql 的 LIKE 族对象比较值)、#5444(driver-memory analytics 回显的contains少了%—— 方向相反的同族缺陷)、#5347 / #5324(read-scope-sql上「放大即越权」的先例)。