Found while implementing #6520 ($icontains on every JS evaluation face). Out of that card's scope — it is the $contains family, not $icontains, and it predates that work — so it is filed rather than fixed there.
Measured on origin/main @ f5a9bc2f3, by reading the source
MemoryAnalyticsService has two exits for one normalized filter tree, and they disagree about what the LIKE family MEANS.
The mingo exit (query()) builds a real containment pattern — memory-analytics.ts, CUBE_OPERATOR_TO_MONGO_PREDICATE:
contains: ({ raw, substring }) => ({ $regex: substring(raw[0]) }),
where substring is the driver's own filterSubstringPattern(value) = new RegExp(escapeRegex(value), 'i') — a substring match.
The SQL exit (generateSql()) emits the comparand as a bare literal, with no % anywhere (memory-analytics.ts, the WHERE builder):
const sqlOp = this.operatorToSql(filter.operator); // 'contains' -> 'LIKE'
const comparand = this.comparandsFor(cube, filter.member, filter.values)[0];
whereClauses.push(`${fieldPath} ${sqlOp} ${this.toSqlLiteral(comparand)}`);
toSqlLiteral only quotes and escapes quotes; it adds no wildcards. So { name: { $contains: 'acme' } } echoes
which is an equality (case-folded on SQLite, exact on Postgres), while query() returns every row containing acme.
notContains has the mirror of the same bug via NOT LIKE.
Why this matters
This is the #5333 / #3650 class — "a rendering that contradicts execution is worse than no rendering" — reached through driver-memory's analytics face instead of service-analytics' echo. An author who runs the echoed statement to reproduce a chart gets a narrower row set than the chart, so the filter reads as broken in the opposite direction from #5333's widening. service-analytics has an enumerated regression test for exactly this property on its own three compilers (objectql-echo-operator-coverage.test.ts); driver-memory's analytics generateSql() has none, which is why this survived.
Note the direction is wrong-rendering, not a permission bypass: this exit produces display SQL, not the executed query.
Scope
A fix should also consider whether the comparand needs LIKE-escaping here (% / _ in an author's comparand), which service-analytics' likePattern already does for the compilers on that side.
Suggested check
Pin generateSql()'s WHERE against query()'s row set for each member of the LIKE family, the way objectql-echo-operator-coverage.test.ts pins the echo against execution — an assertion on the SQL string alone would not have caught the missing % either.
Found while implementing #6520 (
$icontainson every JS evaluation face). Out of that card's scope — it is the$containsfamily, not$icontains, and it predates that work — so it is filed rather than fixed there.Measured on
origin/main@f5a9bc2f3, by reading the sourceMemoryAnalyticsServicehas two exits for one normalized filter tree, and they disagree about what the LIKE family MEANS.The mingo exit (
query()) builds a real containment pattern —memory-analytics.ts,CUBE_OPERATOR_TO_MONGO_PREDICATE:where
substringis the driver's ownfilterSubstringPattern(value)=new RegExp(escapeRegex(value), 'i')— a substring match.The SQL exit (
generateSql()) emits the comparand as a bare literal, with no%anywhere (memory-analytics.ts, the WHERE builder):toSqlLiteralonly quotes and escapes quotes; it adds no wildcards. So{ name: { $contains: 'acme' } }echoeswhich is an equality (case-folded on SQLite, exact on Postgres), while
query()returns every row containingacme.notContainshas the mirror of the same bug viaNOT LIKE.Why this matters
This is the #5333 / #3650 class — "a rendering that contradicts execution is worse than no rendering" — reached through driver-memory's analytics face instead of
service-analytics' echo. An author who runs the echoed statement to reproduce a chart gets a narrower row set than the chart, so the filter reads as broken in the opposite direction from #5333's widening.service-analyticshas an enumerated regression test for exactly this property on its own three compilers (objectql-echo-operator-coverage.test.ts); driver-memory's analyticsgenerateSql()has none, which is why this survived.Note the direction is wrong-rendering, not a permission bypass: this exit produces display SQL, not the executed query.
Scope
contains-> should renderLIKE '%v%'notContains->NOT LIKE '%v%'startsWith/endsWithare not inoperatorToSql's table at all, so they fall to its|| '='fallback and render as equality too — same defect, one step further along. That|| '='fallback is itself the silent-wrong-answer shape driver-memory analytics 面的$notContains编译成裸 mingo{$not: 'x'},该谓词不约束任何行 —— 结果被放大到全表 #5374 and driver-memory 的 analytics 面静默丢弃大半个 filter:$or/$not整条丢,$between/$startsWith/$null/$regex因无 cube 映射而丢 —— 聚合结果被放大 #5345 removed from this file's sibling tables.$icontains(driver-memory 两面 / driver-mongodb / objectqlhaving/ formula)—— SQL 族已实现,同一 filter 在内存 double 上抛错 #6520 added anicontains: 'LIKE'row to that same table. It is consistent with itscontainsneighbour and therefore inherits this same missing-wildcard defect; it was added explicitly so the operator would not fall to the=fallback, which would have been strictly worse. Fixing this issue fixes that row with the others.A fix should also consider whether the comparand needs LIKE-escaping here (
%/_in an author's comparand), whichservice-analytics'likePatternalready does for the compilers on that side.Suggested check
Pin
generateSql()'s WHERE againstquery()'s row set for each member of the LIKE family, the wayobjectql-echo-operator-coverage.test.tspins the echo against execution — an assertion on the SQL string alone would not have caught the missing%either.