Filed unassigned by the #15683 dev while landing the temporal half of the declared-type gate. Recording only — no severity asserted, routing is triage's. Dedup: one targeted MCP search_issues (2026-09-10) returned no open card on this cell; a control query in the same session hit its target, so the empty result is a reading.
Measured
Compile-shape probe against SqlDriver at origin/main + the #15683 branch (the behaviour below is on main and is NOT introduced by that branch — its temporal limb carries the carve-out this one lacks). Object registered through registerExternalObject, dialect better-sqlite3, filter { FIELD: { $contains: 'true' } }:
| declared field |
compiled WHERE |
verdict |
{ type: 'boolean', multiple: true } |
select * from `probe_tbl` where 1 = 0 |
⚠️ the membership filter is dead |
{ type: 'number', multiple: true } |
select * from `probe_tbl` where `nums` GLOB '*true*' |
correct |
{ type: 'tags' } |
select * from `probe_tbl` where `tags_` GLOB '*true*' |
correct |
Why the boolean row is a defect
A multiple: true field is stored as a JSON TEXT array (SqlDriver.isJsonField = JSON_COLUMN_TYPES.has(type) || !!field.multiple). On such a column $contains is not a substring test at all — it is the MEMBERSHIP spelling, and it is the ONE operator #7398 left working there after refusing the equality family. sql-driver-json-column-operator-refusal.test.ts pins it in prose as well: "$contains unchanged … It is the only working membership spelling and downstream code depends on it".
Compiling it to 1 = 0 turns a working membership filter into "matches nothing" — the fail-CLOSED direction that suite's own table calls out.
Where it comes from
SqlDriver.isNonTextColumn reads numericFields and booleanFields. The two registries are filled asymmetrically, in both initObjects and registerExternalObject:
if (type === 'boolean' || type === 'toggle') {
booleanCols.push(name); // no `multiple` condition
}
if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) {
numericCols.push(name); // carries the carve-out
}
booleanFields exists for READ COERCION (stored 1/0 back to a JS boolean), a question that legitimately applies to a multi-valued column too, so narrowing the registry is probably the wrong repair — three other seams read it. The #15683 branch spells the equivalent condition at the predicate instead, for the temporal limb only:
(this.temporalFieldKind(table, localField) !== null && !this.isJsonColumn(table, localField))
The same !this.isJsonColumn(table, localField) on the boolean limb is the candidate shape. It was deliberately NOT taken on the #15683 branch: that branch implements the temporal ruling, and the boolean limb is #14079's cell, so widening it there would have been a second ruling riding on the first.
Repro
class P extends SqlDriver {
compileWhere(where: FilterCondition): string {
const b = this.getKnex()('probe_tbl');
this.applyFilters(b, where);
return b.toString();
}
}
const d = new P({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
d.registerExternalObject({ name: 'probe_tbl', fields: { flags: { type: 'boolean', multiple: true } } });
d.compileWhere({ flags: { $contains: 'true' } }); // -> `where 1 = 0`
toggle is in the same registry arm and is expected to answer identically; not separately probed.
Related
#14079 (the non-string-column row and the gate this sits in) · #15683 (the temporal half, where the carve-out was first spelled) · #7398 (the JSON-column refusal family and the $contains membership spelling it preserves)
Filed unassigned by the #15683 dev while landing the temporal half of the declared-type gate. Recording only — no severity asserted, routing is triage's. Dedup: one targeted MCP
search_issues(2026-09-10) returned no open card on this cell; a control query in the same session hit its target, so the empty result is a reading.Measured
Compile-shape probe against
SqlDriveratorigin/main+ the #15683 branch (the behaviour below is onmainand is NOT introduced by that branch — its temporal limb carries the carve-out this one lacks). Object registered throughregisterExternalObject, dialectbetter-sqlite3, filter{ FIELD: { $contains: 'true' } }:{ type: 'boolean', multiple: true }select * from `probe_tbl` where 1 = 0{ type: 'number', multiple: true }select * from `probe_tbl` where `nums` GLOB '*true*'{ type: 'tags' }select * from `probe_tbl` where `tags_` GLOB '*true*'Why the boolean row is a defect
A
multiple: truefield is stored as a JSON TEXT array (SqlDriver.isJsonField=JSON_COLUMN_TYPES.has(type) || !!field.multiple). On such a column$containsis not a substring test at all — it is the MEMBERSHIP spelling, and it is the ONE operator #7398 left working there after refusing the equality family.sql-driver-json-column-operator-refusal.test.tspins it in prose as well: "$containsunchanged … It is the only working membership spelling and downstream code depends on it".Compiling it to
1 = 0turns a working membership filter into "matches nothing" — the fail-CLOSED direction that suite's own table calls out.Where it comes from
SqlDriver.isNonTextColumnreadsnumericFieldsandbooleanFields. The two registries are filled asymmetrically, in bothinitObjectsandregisterExternalObject:booleanFieldsexists for READ COERCION (stored 1/0 back to a JS boolean), a question that legitimately applies to a multi-valued column too, so narrowing the registry is probably the wrong repair — three other seams read it. The #15683 branch spells the equivalent condition at the predicate instead, for the temporal limb only:The same
!this.isJsonColumn(table, localField)on the boolean limb is the candidate shape. It was deliberately NOT taken on the #15683 branch: that branch implements the temporal ruling, and the boolean limb is #14079's cell, so widening it there would have been a second ruling riding on the first.Repro
toggleis in the same registry arm and is expected to answer identically; not separately probed.Related
#14079 (the non-string-column row and the gate this sits in) · #15683 (the temporal half, where the carve-out was first spelled) · #7398 (the JSON-column refusal family and the
$containsmembership spelling it preserves)