Repository navigation
[finding] a per-aggregation filter with $contains on a multiple lookup counts 0 on every driver while the same where finds the rows: the engine's aggregation evaluator never matches a stored array #20873
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:engine·area:api·pm:queue. Direction: the aggregation evaluator answers$contains/$inon a stored array by the declared membership. Serial with #20822Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T16:01Z. ⛔ Not a claim, ⛔ not a dispatch.Triage:
packages/objectql/src/having-filter.ts(matchesAggregationFilter) ⇒domain:engine.Why p2. A silent wrong count (
m: 0instead of 2) on every driver, for a documented operator on a common field shape.Direction.
- The evaluator answers
$containson a stored array as membership, and$inper element where the operator's declaration says so, by the same membership rulewhereanswers. - If a shared membership predicate exists (the reference semantics the drivers pin), call it. ⛔ No third copy.
havingshares the evaluator, so measure it and pin it.- Pins, on memory, SQLite and PostgreSQL: the card's table,
m: 2; a scalar$containson a string is unchanged (the control).
Serial. #20822 (#5930 step 4, dispatched) deletes F8's whole-day copies in the same file. This card is not folded in (a different meaning), and is dispatched after #20822's F8 part merges, or rides it if that seat agrees in writing on #20822.
- The evaluator answers
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionspm:queue→pm:blocked: serial behind #20822's F8, as triage graded; held fordomain:engine#2domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG·os-litant· 2026-09-30T23:22Z. ⛔ Not a claim.- Whose card: the maintainer named this card for seat 2 in chat: 「你是第2个席位,你只处理 p2 的卡片,[finding] a per-aggregation
filterwith$containson a multiple lookup counts 0 on every driver while the samewherefinds the rows: the engine's aggregation evaluator never matches a stored array #20873、[finding] driver-memory answers$containson a stored array by substring per element (u1matches a row storingu10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874、[finding]$existswith a non-boolean comparand ("yes",1) is accepted at every door and inverted on driver-memory: it returns the rows with NO value; the spec declares$exists: z.boolean(), and its$nulltwin is refused #20897」. Seat 2 takes it through the full claim protocol when it unblocks. - Why blocked: triage 5914962540: "dispatched after #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's F8 part merges, or rides it if that seat agrees in writing on #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822". F8 is in #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's group 3 (driver-mongodbF6,formulaF7,havingF8), its last group, which is not claimed yet. Both edits land inpackages/objectql/src/having-filter.tscheckCondition: this card's$contains/$inarms, F8's whole-day copies. - Asked: seat 1, in writing, on #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 (5921444893): A, seat 2 dispatches this card ahead of group 3; B, triage's order stands (the default); C, group 3 carries this card. - Unblocks when: #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 closes (the body'sBlocked-by: #20822line, added in this act), or seat 1 answers A or C on #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822, whichever comes first. - For the dispatch, already known: seat 2's [finding]
$existswith a non-boolean comparand ("yes",1) is accepted at every door and inverted on driver-memory: it returns the rows with NO value; the spec declares$exists: z.boolean(), and its$nulltwin is refused #20897 dev (dispatched now) measures a non-boolean$existsthrough the aggregationfilterandhavingdoors without editing this file (its$existsarm reads the flag by truthiness). Whatever it measures is a candidate rider on this card's dispatch, in the same file and the same evaluator.
- Whose card: the maintainer named this card for seat 2 in chat: 「你是第2个席位,你只处理 p2 的卡片,[finding] a per-aggregation
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01Ujdtvqs7ree7WyQmEDwEnG
Account:os-litant(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20873-aggregation-filter-array-membership
Worktree:objectstack-issue-20873
Domain:domain:engine
Seat:domain:engine#2(seat post #20966)
Maintainer's direct dispatch (provenance): who, the maintainer; verbatim, 「#20873 现在派」; where, this seat's chat, after the seat reported #20873 serial behind #20822's F8. It overrides the serial order in triage 5914962540 ("dispatched after #20822's F8 part merges"); the transition 5921472356 is superseded, and the body'sBlocked-by: #20822line is removed in this act. Triage's direction otherwise stands.
File surface (triage's direction 5914962540):packages/objectql/src/having-filter.ts,checkCondition: the$containsarm answers a stored array by whole-element membership (a string keeps substring), and the$inarm answers per element where the operator's declaration says so, by the same membership rulewhereanswers.havingshares the evaluator: measured and pinned too.- pins, on memory, SQLite and PostgreSQL where the suite runs it: the card's table (
m: 2); a scalar$containson a string unchanged (the control). .changeset/20873-*.md.
Stop on breach and explain in the report.
- ⛔ Not #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's F8 (wholeDayUpperBoundand the$lte/$betweenwhole-day arms): group 3 mergesmainafter this card, or this card mergesmainafter group 3, whichever lands second. - ⛔ Not the
$exists/$nullflag arms orassertConditionIsEvaluable's flag gate: that is [finding] the aggregationfilterandhavingread a non-boolean$existsby truthiness and DROP a non-boolean$null, on every driver: the engine evaluates both in-process, and its gate refuses only$empty#20981 (filed bare, awaiting triage). - ⛔ Not
driver-memory([finding] driver-memory answers$containson a stored array by substring per element (u1matches a row storingu10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874, seat 2, in flight) and ⛔ notformulamatches-filter.ts(PR fix(formula,plugin-security): the cross-class field-comparison refusal leads with its remedy, so REST callers read the fix (#20869) #20972, refusal text: the cross-class field-comparison refusal (972 characters) is cut at the 500-character client bound before its remedy sentence, so no caller of/dataorsecurity/explainreads the fix #20869, in flight).
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5921472356
Serial constraints cleared: read at 2026-10-01T00:08Z againstorigin/main5f6b63a6f. having-filter.tshas no commit sincef6ccca4a4. No open PR touchespackages/objectql/srcproduction files (14 open PRs read; PR fix(formula,plugin-security): the cross-class field-comparison refusal leads with its remedy, so REST callers read the fix (#20869) #20972 touchesformulaonly). Noclaude/issue-20822-g3*branch exists: #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's group 3 is not claimed. Seat 1 is told on #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 in this act.- [finding] driver-memory answers
$containson a stored array by substring per element (u1matches a row storingu10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874 (seat 2, in flight onclaude/issue-20874-memory-contains-membership) builds the same stored-array membership rule fordriver-memory. This card reads that branch, and reuses its predicate if it lives whereobjectqlcan import it; a second JS copy is named in the report, ⛔ never silent. Clause-②: no: the evaluator is pulled back to the membership reading the spec's$containsdocblock declares andwherealready answers; no key, code or accepted shape is added.
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20873,
"status": "done",
"branch": "claude/issue-20873-aggregation-filter-array-membership",
"pr": "#21004",
"session": "session_01Ujdtvqs7ree7WyQmEDwEnG",
"premise_still_valid": true,
"summary": "having-filter.ts checkCondition: on a DECLARED JSON-stored field (STRUCTURED_JSON_TYPES or isMultiValueField, the population driver-sql's isJsonColumn and #20874's isJsonStoredField read), the per-aggregation filter's $contains asks membership (storedArrayHasMember: the comparand's text names a string, number, boolean or null element, driver-sql's jsonMembershipCandidates set, array-only) and $notContains is its exact complement (null/absent rows satisfy it, #5298); scalar text columns and having keep substring. The declared set is threaded from in-memory-aggregation.ts (the one place the engine hands the declared field map to the filter) through matchesAggregationFilter/matchesHaving; applyHaving passes none. The card's table through POST /api/v1/data/:object/query on SQLite and live PostgreSQL 16.14: owners $contains u1 m 0 -> 2 (where 2), u10 0 -> 1, $notContains u1 6 -> 4 (where 4), tags $contains red 0 -> 2, $or any-of 0 -> 2, title control 3 -> 3. Fork = declared column (H2): on every public-door fixture both readings pick the same rows (structured-JSON $contains is refused by the engine's text-operator door; the write door wraps scalars into arrays); they differ only for direct callers, where declared = SQL where. H3: $in/$nin left as they are (SQL where refuses 400, memory where answers membership). H4: having only sees an array via min/max of a multi-valued field, itself three answers; having's text-projection substring pinned. H5: $notContains mirror confirmed (6 vs 4) and changed under the bounded exemption. H6: no importable predicate; second JS copy named with its shared home (spec/data) in Acceptance notes. Claim surface to amend: + in-memory-aggregation.ts, + the $notContains arm, + 2 test files.",
"tests": "HEAD 90ba78d (merge of origin/main 7fa67da onto fix a818817 / pins c82375c / changeset e9d4728). objectql: vitest --project local src/engine-aggregate-filter-array-membership.test.ts 30 passed; whole local project 349 files / 6851 passed on 90ba78d; repo project 1/5 passed. rest: OS_TEST_POSTGRES_URL=(private PG 16.14) vitest --project local src/aggregation-filter-array-membership.test.ts 18 passed (9 sqlite + 9 live postgres), 9 named skips (mysql); 9 aggregation-adjacent rest files on 90ba78d with PG live 106 passed / 34 skipped. typecheck objectql + rest green; both new test files in their tsconfig.test.json program (--listFiles 1/1). Ablation via scripts/ablation-replace.mjs from the committed fix, restore proven blob == HEAD + git diff HEAD empty each leg: A1 $contains arm reverted -> 15/30 red (membership, member-text, declared-fork rows); A2 $notContains arm reverted -> 3 red; B declaredJsonStoredFields emptied, objectql rebuilt, ablation-dist-preflight marker present in 4 built files -> rest 12 red (6 membership rows x sqlite + pg), controls green; restore leg rebuilt, marker absent from all 14 built files, tree clean, 18 passed. Driver conformance ledger: before 212d613 and after 90ba78d both '50 covered cell(s), 0 in the DEBT ledger, 0 exempt'. Lint (declared narrowing, 90ba78d): eslint --no-inline-config --format json over the 4 touched TS files -> 4 files 0 errors 0 warnings; each file resolves to a config (--print-config); eslint.config.mjs enables no type-aware linting (lines 327-328), so untouched files' verdicts cannot move.",
"mcp_calls": "0",
"api_writes": "3 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, each executed by fleet-write.yml as objectstack-fleet[bot]): pr_create -> POST /repos/objectstack-ai/objectstack/pulls (#21004, draft, body read back byte-identical 14376 B); assign via label-write.mjs -> POST /repos//issues/21004/assignees (os-litant, read back); this os-dev-report comment via post-stamped.mjs -> POST /repos//issues/20873/comments. Plus 5 git pushes of the branch (not REST). Labels: zero-write (the order names none; skip-changeset does not apply).",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/query on SQLite and live PostgreSQL 16.14, base 212d613 and head 90ba78d: where { owners: { $in: ['u1','u9'] } } answers 400 INVALID_FILTER (driver-sql's JSON-column gate) while the same per-aggregation filter answers 200 m: 0, and $nin answers 200 m: 6, counting d1 and d3, the rows holding u1 it was asked to exclude (fail-open); memory's where answers membership (d1, d3) for $in · evidence: having-filter.ts $in/$nin arms compare the whole stored array (listHolds); no column-type gate on the per-aggregation position · dedupe words: per-aggregation filter $in multiple lookup silently 0 · aggregation filter $nin json column fail-open · JSON column equality refusal per-aggregation filter",
"class: b · reach: exception security (an RLS read scope over-reaches) + named producer relation-filter-lowering.ts ($or of $contains per id on a multi-valued relation) · Seam: spec:FILTER_OPERATORS.$contains (membership on a multiple: true / JSON_COLUMN_TYPES column) → runtime:service-analytics read-scope-sql compileScopedFilterToSql | native-sql-strategy contains | driver-turso RemoteTransport.buildWhereSQL | driver-mongodb translateFieldOperators | formula matchesFilterCondition · evidence: compileScopedFilterToSql({ owners: { $contains: 'u1' } }) measured at 212d613 -> SQLite instr("t"."owners", ?) > 0, admitting a row holding ["u10"]; PostgreSQL "t"."owners" LIKE ? ESCAPE ? over a json column; formula matchesFilterCondition measured: ['u1','u2'] $contains 'u1' -> false; turso remote pushLike and mongo bare $regex read in code (not measured). Contract text: 'On a multiple: true field or a JSON_COLUMN_TYPES member, $contains: v is a MEMBERSHIP test ... answered identically on every SQL dialect' · dedupe words: $contains membership json column read scope over-reach · remote transport contains multi-valued substring · formula $contains stored array membership",
"class: a · reach: POST /api/v1/data/:object/query on live PostgreSQL 16.14: groupBy title + max(owners) on a multiple lookup -> 500 DATABASE_ERROR; SQLite -> 200 with the serialized JSON TEXT as the max; in-memory aggregation -> the array · evidence: probe at 212d613 and 90ba78d; count_distinct and groupBy on such fields are already refused at the engine door, min/max are not · dedupe words: min max aggregation multi-value field json column · max over json column DATABASE_ERROR postgres · aggregate function json-stored field door",
"class: a · reach: POST /api/v1/data/:object/query where on a multiple lookup: $startsWith 'u1' -> PostgreSQL 500 DATABASE_ERROR, SQLite 200 n 0 (serialized text starts with a bracket); $icontains 'U1' -> PostgreSQL 500, SQLite 200 n 3 (counts ['u10'], substring across the serialization); memory answers per element (3) · evidence: probe at 212d613 / 90ba78d; #20874's docblock records $startsWith as unruled over a stored array · dedupe words: startsWith icontains multiple lookup postgres 500 · text operator json column DATABASE_ERROR · stored array text operators unruled",
"carrier: none · noted, not filed: the membership candidate rule now lives as three readers (driver-sql jsonMembershipCandidates on main, driver-memory containsMemberCandidates on #20874's branch, objectql storedArrayHasMember here), none importable by the others; one shared home would be @objectstack/spec/data beside asciiCaseInsensitiveContains (PR Acceptance notes)",
"carrier: none · noted, not filed: no shared conformance kit drives stored-array membership (FILTER_TEXT_CASES has no array rows); three packages carry literal u1/u10/redwood fixtures (PR Acceptance notes)"
],
"gates": {
"derived_at": "90ba78d9",
"derived": 63,
"run": 61,
"exit_0": 61,
"not_measured": [
"pnpm check:dual-build-cjs-loads (exit 3 PREREQUISITE NOT MET: 42 packages without dist/)",
"pnpm check:type-check-debt (exit 3 PREREQUISITE NOT MET: 5 workspace deps without built types)"
],
"unrun": 0,
"reconciliation": "dispatch-gates --ran: 63 derived famil(ies) accounted for — 61 run, 2 NOT-MEASURED",
"note": "check-engine-split-ratio refused on the shallow checkout first; green after git fetch --shallow-since=2026-06-26 origin main"
},
"deviations": [
"claim surface: in-memory-aggregation.ts (threads the declared set), the $notContains arm (H5 bounded exemption, all four conditions) and two test files (objectql, rest) are beyond the claim's named file surface; PM to amend",
"memory pin is engine-level over the read shape find() presents (measured identical on memory/SQLite/PostgreSQL): a real InMemoryDriver test consumer is barred by check:driver-memory-census",
"PostgreSQL/MySQL REST cells are named skips in CI (no job provisions OS_TEST_POSTGRES_URL for packages/rest); the PostgreSQL cell ran locally against a private PG 16.14 instance in /tmp/os-pg-dev20873 (outside the root-only scratchpad because postgres refuses root), started, stopped by its recorded PID and removed",
"git fetch --shallow-since=2026-06-26 origin main deepened the checkout for check-engine-split-ratio; it advanced the shared origin/main ref; origin/main 7fa67da was then merged into the branch (no overlap with the diff) and the closure rebuilt",
"the first full objectql run spelled --maxWorkers=2 after a bare --, which vitest may discard (concurrency only; the whole local project ran); the merged-head run used exec vitest run --project local --maxWorkers=2",
"commit trailers are AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the pre-push hook passed them"
],
"files_changed": [
".changeset/20873-aggregation-filter-array-membership.md",
"packages/objectql/src/engine-aggregate-filter-array-membership.test.ts",
"packages/objectql/src/having-filter.ts",
"packages/objectql/src/in-memory-aggregation.ts",
"packages/rest/src/aggregation-filter-array-membership.test.ts"
]
}objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (amendment of claim 5921990075: same session, same branch; the file surface follows the report 5922812043)
Session:session_01Ujdtvqs7ree7WyQmEDwEnG
Account:os-litant(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20873-aggregation-filter-array-membership
Worktree:objectstack-issue-20873
Domain:domain:engine
Seat:domain:engine#2(seat post #20966; runs the lane queue by the maintainer's order recorded on #6367)
File surface, as delivered in PR #21004 (head90ba78d9): the original surface plus three declared additions.packages/objectql/src/having-filter.tscheckCondition:$containson a DECLARED JSON-stored field (STRUCTURED_JSON_TYPESorisMultiValueField) asks membership; a string keeps substring.$in/$ninare unchanged (measured: SQLwhererefuses them on a JSON column, so there is no one answer to follow).- Added: the
$notContainsarm, its exact complement, under os-dev rule 3's bounded in-place exemption (conditions stated in the PR body). - Added:
packages/objectql/src/in-memory-aggregation.ts, which threads the declared set from the engine's field map intomatchesAggregationFilter.applyHavingpasses none. - Added:
packages/objectql/src/engine-aggregate-filter-array-membership.test.tsandpackages/rest/src/aggregation-filter-array-membership.test.ts(a declared cross-lane touch,domain:cli, test only). .changeset/20873-aggregation-filter-array-membership.md.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5922812043
Serial constraints cleared: read at 2026-10-01T01:26Z againstorigin/mainf8178ffec.- The F8 whole-day arms (
wholeDayUpperBound,$lte/$between) and the$exists/$nullarms are untouched in the diff (#5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 3 and [finding] the aggregationfilterandhavingread a non-boolean$existsby truthiness and DROP a non-boolean$null, on every driver: the engine evaluates both in-process, and its gate refuses only$empty#20981 follow in this file). - PR fix(driver-memory): $contains on a multi-valued or JSON-stored field is membership, on every face #20984 ([finding] driver-memory answers
$containson a stored array by substring per element (u1matches a row storingu10), where the SQL drivers answer membership; the spec docblock records the gap against a card that answers 404 #20874) has landed asf8178ffecon other files; this branch mergedorigin/mainat7fa67dada, and nothing it touches moved since. Clause-②: no: no input is refused or newly accepted. The answers move to the declared membership readingwherealready gives, and no public export is added:declaredJsonStoredFieldsis exported fromhaving-filter.tsonly, andpackages/objectql/src/index.tsnameshaving-filternowhere (0 hits; control: the same grep findsexport { ObjectQL, … } from './engine.js'among the entry's 87 export lines).
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT — PR #21004 @
90ba78d9(a per-aggregationfilter$contains/$notContainson a declared multi-valued or JSON-stored field asks membership, aswheredoes)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T02:07Z. Judged against GitHub, not the report (5922812043).- Form: draft PR on
main. The body opensFixes #20873/Clause-②: no, and that is its only closing keyword. The seat corrected two sentences of the body before this record: the published-entry-points sentence (in-memory-aggregation.tsIS on objectql's entry points, with unchanged signatures) and the copy count (three copies of the member rule onmainonce this lands, not two). The squash message is the PR body, so the corrections ride into the commit. - Scope: 5 files, all inside the amended claim 5922857157:
packages/objectql/src/having-filter.ts(the$contains/$notContainsarms anddeclaredJsonStoredFields),in-memory-aggregation.ts(threads the declared set), the objectql engine-level suite, the REST suite inpackages/rest(a declared cross-lane touch), and.changeset/20873-aggregation-filter-array-membership.md(patch,@objectstack/objectql). Not governed (check-governed-merges --pr 21004: NOT governed). 610 changed lines. - Contract review: at tier, PASS on this head (5923194896). It found the accept set unchanged both ways, the published surface unmoved, the member rule identical to
driver-sql's, the population identical to the one fix(driver-memory): $contains on a multi-valued or JSON-stored field is membership, on every face #20984 landed,havingleft on substring and pinned,$in/$nincorrectly left alone, andClause-②: nowithpatchcorrect. - CI on
90ba78d9: 48 check-runs. That is 41 success, includingCheck Changeset,Lint & Repo Gates,Test Core1–6, the four Type Check jobs,Build Core,DogfoodandTemporal Conformance, plus the claim guard andCheck Changesetre-run after the body edit. The other 7 are skips, all on the roster (check-expected-skips --pr 21004: OK, exit 0). Mergeable state: clean. - Tests (dev's evidence):
- objectql: the new suite 30 passed, the whole local project 6851 passed.
- rest: 18 passed (9 SQLite, 9 live PostgreSQL 16.14). In CI the PostgreSQL / MySQL cells are named skips; the SQLite cell runs in
Test Core. - Ablations: A1 (
$containsarm) 15/30 red, A2 ($notContainsarm) 3 red, B (declared set emptied, rebuilt) rest 12 red. Each restore was proven clean. - Gates: 61 of 63 derived families run with exit 0; 2 not measured, prerequisite missing.
- Driver conformance ledger: 50 / 0 / 0 before and after.
- Deviations, all accepted by the review:
- The memory cell is engine-level over the
find()read shape, becausecheck:driver-memory-censusbars a newInMemoryDriverconsumer. - The PostgreSQL cell is local evidence only.
- The
$notContainsmirror arm moved under os-dev rule 3's bounded exemption; the amendment names it.
- The memory cell is engine-level over the
- Findings (
out_of_scope_findings):- [0] per-aggregation
$in/$ninon a multi-valued field answer 200 wherewhereis a 400 on the SQL family;$ninfails open → filed [finding] a per-aggregationfilter$ninon a multi-valued field counts the rows it was asked to exclude, and$incounts none, where the samewhereis refused 400: the aggregation evaluator has no JSON-column equality gate #21007. - [1] the
$containsmembership contract is unanswered on the turso remote transport, the service-analytics SQL compilers (an RLS read-scope over-reach),driver-mongodbandformula→ [finding]$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987 (the existing family card,priority:p1·security). New measurements and the relation-filter producer are added in 5922891789. - [2]
min/maxover a multi-valued field is answered three ways (an array on memory, serialized text on SQLite, a 500 on PostgreSQL). The dev measured it on amultiple: truelookup → carrier [finding]max/minover a JSON-stored field answers per driver at the engine (memory an object, SQLite a string, PostgreSQL 500): the compatibility table refuses them, but the engine aggregate door enforces only itscount_distinctrow #20914, in flight: the table'smin/maxrows hold nolookup, so enforcing the table at the aggregate door refuses that pair. The table is keyed by type, not multiplicity, so amultiple: truefield of amin/max-compatible type passes it. That half is unmeasured → Acceptance notes: the seat reads it against [finding]max/minover a JSON-stored field answers per driver at the engine (memory an object, SQLite a string, PostgreSQL 500): the compatibility table refuses them, but the engine aggregate door enforces only itscount_distinctrow #20914's PR at collection. - [3]
$startsWith/$icontainson a multi-valued lookup: a PostgreSQL 500, and a wrong count on SQLite → filed [finding]$startsWith/$icontainson a multi-valued lookup answer 500 on PostgreSQL and a wrong count on SQLite: the text operators other than$containsreach a JSON column unrefused and unruled #21009. - [4] the review's ③ escalation: the membership rule now has three module-private copies (
driver-sql,driver-memory, objectql) and no shared predicate. Triage's "⛔ No third copy" had no callee to honour → recorded on [finding]$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987 as the convergence item (5922891789). One home is@objectstack/spec/data, besideasciiCaseInsensitiveContains, and whoever fixes the family's JS faces reads from it, not a fourth copy. The single-value media population edge (review ① 4) rides the same item. - [5] no shared conformance kit for stored-array membership (
FILTER_TEXT_CASEShas no array rows) → Acceptance notes, carrier none.
- [0] per-aggregation
- Landing: this seat readies and arms the PR through the queue.
- Form: draft PR on
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsMeasured evidence for this card, from #20918's dev
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T02:48Z · ⛔ Not a claim.- Source:
os-dev-report5923712419on analytics: on the ObjectQL strategy a$notover a multi-valued lookup ($contains) is refused 400, because the NULL-safe guard reaches driver-sql as$ne: nullon a JSON column, where the engine answers the rows #20918,out_of_scope_findings[1], measured on PR fix(service-analytics): the ObjectQL strategy hands the engine its null tests as $null, so a $not over a multi-valued lookup gets the engine's rows (#20918) #21036's branch. - Reading: a dataset measure filter of
$notover$contains, or of$notContains, on a multiple lookup counts 5 throughengine.aggregate's per-aggregation filter.engine.findover the samewherecounts 3, and so do both analytics strategies after PR fix(service-analytics): the ObjectQL strategy hands the engine its null tests as $null, so a $not over a multi-valued lookup gets the engine's rows (#20918) #21036. - The fixture is
packages/rest/src/analytics-not-multivalue-null-guard.test.tson PR fix(service-analytics): the ObjectQL strategy hands the engine its null tests as $null, so a $not over a multi-valued lookup gets the engine's rows (#20918) #21036 (lands with it). - Dedupe words:
aggregation filter $contains multiple lookup·engine.aggregate per-aggregation filter multi-valued.
Generated by Claude Code
- Source:
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded — PR #21004 as
d67b94280(the card closes)domain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T03:28Z.-
Merged through the merge queue at 2026-10-01T03:27Z as squash
d67b94280onmain. Two readings agree: the PR object andorigin/main's log. -
The squash carries the PR's 5 files (596 insertions, 14 deletions):
having-filter.ts, with the membership$contains/$notContainsarms on a declared JSON-stored field;in-memory-aggregation.ts, which threads the declared set;- the objectql and REST suites;
- the changeset.
-
Records it landed on: ACCEPT 5923289437 and contract review PASS 5923194896, both @
90ba78d9. -
The evidence posted after the ACCEPT (5923731991, from analytics: on the ObjectQL strategy a
$notover a multi-valued lookup ($contains) is refused 400, because the NULL-safe guard reaches driver-sql as$ne: nullon a JSON column, where the engine answers the rows #20918's dev) measured, on a base without this change, a dataset measure filter that counts 5 throughengine.aggregatewhereengine.findcounts 3:- shapes:
$notover$contains, or$notContains, on a multiple lookup; - this change: its
$notContainsarm is the exact complement of membership, and a$notover$containscomplements the membership reading. Both are pinned in the two suites, against their livewheretwins, on the card's fixture.
The seat expects the dataset reading to agree with
findfromd67b94280on. That is a re-measure for analytics: on the ObjectQL strategy a$notover a multi-valued lookup ($contains) is refused 400, because the NULL-safe guard reaches driver-sql as$ne: nullon a JSON column, where the engine answers the rows #20918's carrier, PR fix(service-analytics): the ObjectQL strategy hands the engine its null tests as $null, so a $not over a multi-valued lookup gets the engine's rows (#20918) #21036, not a claim made here. - shapes:
-
The card closes
completedby the PR'sFixes #20873.pm:dispatchedis removed in the same act as this record. -
Unlocked by this landing: [finding] a per-aggregation
filter$ninon a multi-valued field counts the rows it was asked to exclude, and$incounts none, where the samewhereis refused 400: the aggregation evaluator has no JSON-column equality gate #21007 (Blocked-by: #20873, triage 5923286198) goespm:blocked→pm:queue(its own release comment). It is next in thehaving-filter.tsserial queue, ahead of [finding] the aggregationfilterandhavingread a non-boolean$existsby truthiness and DROP a non-boolean$null, on every driver: the engine evaluates both in-process, and its gate refuses only$empty#20981.
-
- added 4 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a defect with a named landing site:
packages/objectql/src/having-filter.tsmatchesAggregationFilter, the engine's own evaluator for a per-aggregationfilter. Finding class (a), with a (b) half.reach:measured at the public doorPOST /api/v1/data/:object/queryon SQLite, PostgreSQL 16 and the in-memory driver. The measurement is the #20802 dev's, atorigin/main688ddef3c3with an uncommitted probe (os-dev-report5912874377 on #20802,out_of_scope_findings[0]).Filed by the
domain:engineexecution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY,os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens
The object has a multi-valued lookup
owners. Rowsd1andd3store an array that containsu1.where: { owners: { $contains: 'u1' } }d1,d3(2 rows) on all three driversaggregations: [{ count, alias n }, { count, alias m, filter: { owners: { $contains: 'u1' } } }]m: 0on all three driversm: 2The per-aggregation
filterruns on the engine's evaluator (matchesAggregationFilter), not on the driver.$containson any non-string value.$inlist by whole value.So a stored array never matches.
FILTER_OPERATORS'$containsdocblock (@objectstack/spec) declares membership over a stored array, andwhereanswers it that way. The b half: the evaluator does not do what the operator's declaration says for this storage form.Scope for whoever takes it (⛔ not a ruling)
filteranswers$contains(and$inwhere it applies) on a stored array aswheredoes on every driver, by the declared membership semantics.havingshares the evaluator; measure it too.domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822) deletes this face's hand-copied whole-day copies (F8). This is a different meaning, but the same file. Serial with #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822, or folded into it if triage prefers.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:groupBy/aggregationssilently dropssearch— grouped counts under a search are the unsearched counts #20358 (searchdropped undergroupBy), objectql + REST: a per-aggregationfilter(andhaving) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on adatefield counts 1 where thewheretwin counts 3 #20176 (temporal comparand type-blind), service-analytics: object-form analyticswhereskips the shared comparand-shape face's other arms ($innull member,$gt: null, null/blank$betweenbound, scalar$in) that the FilterArray spelling refuses 400 #20010 (analyticswhereshape arms) and objectql: a per-aggregationfilterrefuses an unknown operator only when rows exist —aggregations: [{ filter: { amount: { $median: 1 } } }]answers 400 on a populated table and 200 on an empty one #20122 (unknown operator only on populated tables) are other defects of the same evaluator. None covers stored-array membership.Dedupe words:
per-aggregation filter $contains multiple lookup count 0·matchesAggregationFilter stored array membership·aggregation filter any memberGenerated by Claude Code