Repository navigation
fix(objectql)!: the in-process engine refuses a search-field override that names no field, as the doors and the shared declarations do - #28108
Conversation
… that names no field find, findOne, count and aggregate read an empty searchFields, an empty object-form search.fields, or a comma string with no name in it as no override, and scanned the object's whole default search set. The Data API doors refuse that value with 400 VALIDATION_ERROR and the shared declarations refuse it too; the engine now refuses it the same way, in the declaration's own words, before any driver read. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyzHgmAWpXVdUctRKU
…override naming no field Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyzHgmAWpXVdUctRKU
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cyzHgmAWpXVdUctRKU
📓 Docs Drift CheckThis PR changes 1 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2ee7f2c41017aa1b5e018434cb0dba64d6a1c0ae && git checkout 2ee7f2c41017aa1b5e018434cb0dba64d6a1c0ae
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 59547058aeb3eccde6f267293367dabf5ef82e59 a43bf0a98999f25655ccd18cf4db62842456aead && git checkout -B drift-repro 59547058aeb3eccde6f267293367dabf5ef82e59 && git merge --no-ff a43bf0a98999f25655ccd18cf4db62842456aead
node scripts/docs-audit/affected-docs.mjs --json 59547058aeb3eccde6f267293367dabf5ef82e59
|
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Contract reviewServed-tier: Read on this head: card #27732 (body and its 4 comments: 6519259214, 6519918549 triage, 6520880971 claim, 6523412292 os-dev-report), PR #28108 (body, 5-file list, net diff Check-runs at my reading: complete and ① Derived judgmentsAccept-set changes the diff implies, each read in the code, not the PR body:
② Semver level
③ Boundary flags
Dev deviations, each answered:
Dev
Item 2 of the card (the comma-string asymmetry) — triage 6519918549 ruled it by design and not changed; the PR records it and widens nothing. Premise holds. No flag outstanding at the lane boundary; nothing escalated beyond the three should-fixes. ④ Design judgmentThe narrowing, read against the published spec on Tried to break it:
Findings: Must-fix: none. Should-fix S1 — file the runtime seam carrier (noted, not filed). Should-fix S2 — one stale sentence in the spec, for #28059 to carry. Should-fix S3 — file the all-unknown override tolerance. Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Seat ACCEPT · PR #28108 · head
|
Requested by Elon · project thread
Fixes #27732
Clause-②: no (narrowing) — the engine refuses the empty override the shared declaration already refuses (triage 6519918549)
Metadata-protocol: none
Skills-docs: updated here —
content/docs/protocol/objectql/query-syntax.mdx(the search section) says the engine refuses an override that names no field,400 VALIDATION_ERROR, as at the ingress.skills/**needs no change:git grep -n "searchFields\|search\.fields" -- skillsat this head finds only non-empty overrides (skills/objectstack-query/SKILL.md§ Full-Text Search, its key tables at lines 25-26, its eval, andskills/objectstack-data/SKILL.md:125), and no skill emits or recommends an empty one.content/docs/api/data-api.mdxalready states the door's refusal.维护者速读
find/findOne/count/aggregate遇到「一个字段都没写」的搜索字段覆盖(searchFields: []、对象形search: { query, fields: [] }、或只有逗号和空格的字符串)时直接拒绝,答400 VALIDATION_ERROR,与数据接口的门(data: a blank or comma-only$searchFieldsis read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484)和共享声明(spec/protocol: the publishedsearchFieldsdeclarations still admit an empty list that the data door now refuses; doessearchFieldsfollowselectinto a declared accept set, and does the engine follow? #27683)一致;措辞取自 spec 声明本身,不另写一份。null仍搜默认字段,写了字段名的覆盖照旧缩小。回滚即还原本 PR。packages/objectql/src/engine.ts的refuseSearchOverrideNamingNoField与expandSearchOnAst开头两行调用;packages/objectql/src/engine-empty-search-fields-refusal.test.ts。pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-empty-search-fields-refusal.test.ts,预期 24/24 通过;在main上同文件 15 条失败(四个动词 × 三种空覆盖,加无搜索词、系统上下文、REST 对等)。What changes
ObjectQL.expandSearchOnAstis the one search expanderfind,findOne,countandaggregatereach (the last two throughexpandSearchOnOptions). It now opens by judging the two override slots, before the expansion and so before any driver read:mainfind/findOne/count/aggregatewithsearch: 'harbour', searchFields: []400 VALIDATION_ERROR,param: 'searchFields', no driver readsearch: { query: 'harbour', fields: [] }(item 1)param: 'search.fields'searchFields: ' , '(theGETtransport spelling, which reaches the engine as sent)param: 'searchFields'searchFields: []with no search termnullsearchFields: ['name']/search.fields: ['name']FullTextSearchSchema.shape.fields.safeParse([]), which isSEARCH_FIELDS_NAMES_NO_FIELD("Name at least one field, or leave the key out to search the default search fields"), the same wordsQuerySchema.searchFieldsrefuses with. The test asserts the two schemas agree. No spec change.status: 400,code: 'VALIDATION_ERROR', the envelopeassertSearchFieldsAreSearchablethrows at the door for the same value, somapDataErrorserves both callers the same status and code. The test pins it againstfindDataon the same engine.searchFieldsand falls back tosearch.fields; the declarations refuse each on its own. The engine refuses each on its own too, so it never admits a value either declaration refuses.fields: []as every column while every published projection declaration refuses an empty projection (found on #26954) #27008 engine stage put its refusal in a sibling module. This one is a module-private function inengine.ts(+40/−1), because the claim's file surface namesengine.tsand the one expander already sits there.engine-search-empty-set.test.ts("an explicit searchFields narrowing does not rescue it") listed[]among the overrides that still reach the empty-search-set refusal (INVALID_FIELD,param: 'search'). An empty override no longer reaches the expansion: it is refused first,VALIDATION_ERROR. The[]entry is dropped there, with a pointer to the new file; the three named overrides it keeps still pin the empty-set refusal.Item 2, the comma-string asymmetry: by design, not changed
Triage 6519918549: "The comma string is the query-string transport encoding of the
GETform; a JSON body carries the declared array. That is by design."POST /data/:object/querykeeps refusing a stringsearchFieldsat the body schema, and theGETform keeps accepting the comma string. This PR widens nothing.Not here: the engine-side declarations (#28059)
EngineQueryOptionsSchema.searchFields,EngineAggregateOptionsSchema.searchFieldsandEngineCountOptionsSchema.searchFields(packages/spec/src/data/data-engine.zod.tslines 190, 514, 559) still declarez.array(z.string())with no minimum. Flipping them is apackages/specedit, outside this claim's file surface, so the seat filed it as #28059 for the spec lane. Until it lands those three declarations admit[]while the engine refuses it; theirsearchmembers already refusesearch.fields: []throughFullTextSearchSchema.Tests
New
packages/objectql/src/engine-empty-search-fields-refusal.test.ts, 24 cases, on a realObjectQLover a driver double that evaluates$or/$icontainsand records each read.harbourhitsnameon two rows andcityon a third, so a narrowing is visible in the answer.find,findOne,count,aggregate):searchFields: [],search: { query, fields: [] }andsearchFields: ' , 'are each refused withstatus400,codeVALIDATION_ERRORand the namedparam, the declaration's words in the message, and zero driver reads.findanswersa1, a2,count2,aggregateeast 1 / west 1); an absent override searches the default set (a1, a2, a3, 3, east 2 / west 1).findDatawithsearchFields: '') and the engine map to the same status and code;searchFields: nullsearches the default set.Reproduction on main, red/green, ablation
Step 1 of the order, a probe on
origin/mainbc496bf7with this file's driver double (every row the same on all four verbs):Red leg, the new file against the
mainengine:Tests 15 failed | 9 passed (24), every refusal case "expected a refusal, got an answer"; the nine controls green. This head:Tests 24 passed (24).Ablation, two legs, through
scripts/ablation-replace.mjs(the test imports./engine.js, which vitest resolves tosrc/, so the mutation is on the resolved source):searchFieldscall replaced by a marker: anchor 1 → 0, blobb26bca3e→4903e59e.Tests 11 failed | 13 passed (24): thesearchFields: []and comma-string cases on all four verbs, no-term, system-context and REST parity. Restored: blob == HEADb26bca3e,git diff HEADempty.search.fieldscall replaced by a marker: anchor 1 → 0, blobb26bca3e→3cef55e0.Tests 4 failed | 20 passed (24): thesearch: { query, fields: [] }case on all four verbs. Restored the same way.Acceptance notes
searchFields: the global search sweep (protocol-record-reads.ts, passes it only when the queryable set is narrower and skips an object left with none, so never empty); the REST export (data-action-routes.ts, keeps an empty list sofindDatarefuses it, which it already did);findData's stored-metadata narrowing (returns a non-empty list or refuses). None passes an empty override.refuseOrNarrowStoredMetadataEvaluateinpackages/runtime/src/stored-metadata-reader-seam.tsrunsnarrowStoredMetadataSearchbefore the engine. For a stored-metadata object, an in-processsearchFields: []with a search term is replaced there by the narrowed default set, so it reaches the engine non-empty and is not refused. That is the pre-existing "empty is absent" reading one layer up, on one object family, outside this file surface.narrowStoredMetadataSearch's docblock ("an empty override is ABSENT to the engine") is stale after this PR. Carrier: seat.resolveSearchFields's tolerance; the doors refuse itINVALID_FIELD). That is the search twin of objectql: an in-process projection whose every column is unknown is answered with every column (REST refuses it 400 INVALID_FIELD) #27165's all-unknown projection and is not this card.Verification
All on head
a43bf0a9unless named.node scripts/pm/dispatch-gates.mjs --commands(first stderr line namesobjectstack-ai/objectstackat the head) derived 93 families; each exit code recorded before any pipe. 93/93 exit 0.--ran: "93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN" (exit 0). A whole-treeturbo run build(exit 0) ran first, so no family hit a missingdist/.bdbb4799. 92/93:check:query-options-erasureexit 1, "test surface grew 236 → 240", the fouras anybags in the new test'srun()helper. Fixed ina43bf0a9by naming the bypassed contract (as unknown as EngineQueryOptions/EngineCountOptions/EngineAggregateOptions, the form the gate asks for); the ratchet reads 236, at the ceiling. The second pass above is the full 93 again.pnpm lint(eslint . --no-inline-config): exit 0 ona43bf0a9.@objectstack/objectql.typecheck(tsc pluscheck:test-typecheck) exit 0 ona43bf0a9.testunder the verify lock onbdbb4799:Test Files 494 passed (494),Tests 9616 passed (9616). The only later change is the four type assertions in the new test file; that file andengine-search-empty-set.test.tsre-ran ona43bf0a9: 35/35.pnpm --filter @objectstack/dogfood testunder the lock, after the whole-tree build) onbdbb4799: VERDICT command-exit 1,Test Files 1 failed | 357 passed | 3 skipped (361),Tests 1 failed | 2880 passed | 20 skipped (2901). The one red is main-red main red (Dogfood Regression Gate 3/3) since 3e0fd7a8 (#27964): the api-action door dogfood pin still expectssys_metadata_activationin the disabled-action sentence that #27842 rewrote #28082, pre-existing, not this diff:test/api-action-door-forward.dogfood.test.ts:178still expectssys_metadata_activationin theACTION_DISABLEDmessage that PR feat(spec,metadata-protocol,rest,objectql,runtime): GET /meta/object serves switched-off packaged actions beside the item, and the disabled-action refusal reads as a plain sentence #27964 rewrote; it is the same onorigin/main6b125a7a.a43bf0a9changes only type assertions in a unit test file, which the dogfood battery does not import.test:repo, the type-check lanes, Build Docs); this PR's check-runs answer them.Serial
3e0fd7a8;origin/mainwas merged into this branch afterwards (bdbb4799, no conflict).git merge-base --is-ancestor 3e0fd7a8 HEADexit 0 (controlbc496bf7: exit 0);git merge-tree --write-tree origin/main HEADexit 0.src/engine.ts(23,903 lines, +7,501 in 30 days) along itsObjectQLseams, one move-only slice per PR #25522 (seat 2's engine cuts): the last comment (6513988645, 03:08Z) released cut 20 back topm:queue; no live cut claim, and no open PR touches the search region.Generated by Claude Code