feat(spec,objectql): IDataEngine.find/findOne accept the author state — engine fills SortNode.order's declared default (#6300) - #7269
Conversation
… — engine fills SortNode.order's declared default (#6300) ADR-0122's core argument — the first key an author writes must default correctly — now holds on the engine's primary read entry. find/findOne's query parameter flips from EngineQueryOptionsParsed (z.infer) back to EngineQueryOptions (z.input), the author-state shape count already took, and ObjectQL fills the one consumed default (SortNode.order → 'asc') by running each authored sort node through SortNodeSchema before the QueryAST is built — recursively through expand — so the declared default stays single-sourced in packages/spec. Phase-1 measured delta between the two states: orderBy[].order (the one default anything consumes — every driver already coalesced its absence to 'asc'), the three inert search flags (read by no executor, deleted from the AST unread), and their recursion through expand. context is .partial() (no delta); where/fields/limit/offset/top carry no defaults or transforms. Widening for typed callers; the one behavior change is for type-bypassing callers only: a malformed sort node (retired 'direction' spelling, unknown key) is refused with SortNodeSchema's own prescription instead of silently dropped-or-honored per driver (#4721's class), matching the wire path's normalizeSortNodes. Closes #6300 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SaEx5461eovGX7AS9aLDV
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…eline on the merged tree Both #7249 and this PR ratcheted scripts/query-options-erasure-baseline.json; the merged tree carries both reductions, so the file is re-measured with --update rather than hand-merged (the resolution rule declared in the PR body). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SaEx5461eovGX7AS9aLDV
|
Merge-queue ejection (MERGE_CONFLICT) resolved. The conflict was exactly the one declared in the PR body's in-flight section: #7249 merged and both PRs ratchet Merged-tree verification before push: spec + objectql rebuild, objectql typecheck, and the three affected suites (author-state pins, findOne contract, query-expression conformance) — 145/145 green; the engine-side fill was written to #7249's Not re-queueing myself (dispatch says no auto-merge from this seat) — ready for re-queue when the queue owner is. Generated by Claude Code |
Premise (verified on fresh
origin/main@f40c5b4)IDataEngine.find/findOne(packages/spec/src/contracts/data-engine.ts:168,183) still tookEngineQueryOptionsParsed(z.infer), as ADR-0122 第二期:major 窗口翻转裸名为解析态语义 + XInput 去留 #6083 pinned them;ObjectQL.find/findOnematched.order: undefinedto drivers.countalready tookEngineCountOptions(z.input) — the asymmetry the card notes.Phase-1 measured default census (parsed vs author state of
EngineQueryOptionsSchema)orderBy[].orderSortNodeSchema.default('asc')(query.zod.ts:64)engine.tsnever reads.order. Every driver coalesces absence to'asc':sql-driver.ts:4864,4923,8574,8673,memory-driver.ts:1339,mongodb-driver.ts:664,mongodb-aggregation.ts:101,remote-transport.ts:1565. The only thing requiring parsed state was the type ofQueryASTat AST construction.search.fuzzy/search.operator/search.highlightfalse/'or'/false(query.zod.ts:417,418,422)[EXPERIMENTAL — not enforced](#4286);expandSearchToFilterreads only.query/.q(search-filter.ts:64), andexpandSearchOnAstdeletessearchoff the AST before middlewares/hooks/drivers see it.expand(nestedQuerySchema, recursive)ast.expand; the nested read re-entersengine.find(engine.tsexpand path), which fills again.contextExecutionContextSchema.partial().partial()erases the defaults' requiredness — no delta.where/fields/limit/offset/topfilter.zod.tspins "no.default(), no.transform()")Driver-side status-quo measurement the card asked for: an
order: undefinedsort node behaves as'asc'on every driver today — identical to the schema's declared default. So filling it changes no query's answer.Bound verdict: the set is exactly
SortNode.order+ close kin → proceed (STOP guard not triggered).Changes
packages/spec/src/contracts/data-engine.tsfind/findOnequeryparam:EngineQueryOptionsParsed→EngineQueryOptions(z.input, ADR-0122), with a doc note on the flip and thecountsymmetry restored.packages/objectql/src/engine.tsfillQueryAstDefaults: runs each authored sort node throughSortNodeSchema(recursively throughexpand) before theQueryASTis built — the schema's own defaulting parse, so declared defaults stay single-sourced in spec.searchdeliberately NOT parsed (measured: its defaults are read by nothing and the key is deleted from the AST unread; parsing would refuse the wire-tolerated comma-stringsearch.fieldsshape pinned inquery-expression-conformance.test.ts). Deletes the twoas anyon the nested-expand re-entry (fields/orderBy).packages/objectql/src/engine-author-state-query.test.tspackages/objectql/src/engine-filter-array-lowering.test.ts,hook-input-shape-contract.test.tsEngineQueryOptionsParsed→EngineQueryOptions).scripts/query-options-erasure-baseline.jsonengine.ts9 → 8); test surface unchanged at 256 — the new deliberate off-contract probes use the sanctionedas unknown as EngineQueryOptionsspelling, which the ratchet doesn't count..changeset/engine-author-state-find.md@objectstack/specminor,@objectstack/objectqlminor.Cost of the schema-parse choice, measured: one small-object
SortNodeSchema.parseper authored sort node, only whenorderByis present. Behavior delta of that choice: for typed callers — none (pure widening; every previously-compiling call still compiles,z.infervalues are validz.input). For type-BYPASSING callers only: a malformed sort node (retireddirectionspelling, unknown key) is now refused with the schema's own prescription instead of silently dropped-or-honored per driver (one query, two orders — #4721's class;normalizeSortNodesalready refuses it on the wire path). Stated in the changeset.Reverse verification (direction predicted first)
Prediction written before execution: the defaulted spelling must behave as the schema's declared
'asc'— identical to explicit-asc, exact reverse of explicit-desc on a tie-free fixture.find({orderBy:[{field}]})uncast vsIDataEngineAlpha,Beta,Gamma'asc''desc'Gamma,Beta,Alpha)order:'asc', neverundefined[{field:'name',order:'asc'}]expandquery filled on its own readorder:'asc'findOneorder-lessorderByis a legal #4419 predicatelimit:1ordernever clobbereddescpreserved on ASTdirectionspelling refused with rename prescription/order//frobnicate/In-flight check (incl. #7178)
EngineQueryOptionsSchema.searchrejects the bare query string that ADR-0061 D1 calls canonical — so every engine caller that wants it mustas any, losing the whole query's checking #7178 / PR fix(spec):EngineQueryOptionsSchema.searchaccepts the canonical bare query string (#7178) #7249 (EngineQueryOptionsSchema.searchunion, sibling spec-lane session; branch pushed during this session): no code collision — this PR touches neitherdata-engine.zod.tsnordatabase-loader.tsnorengine-findone-contract.test.ts, and the engine-sidesearchhandling here was written to fix(spec):EngineQueryOptionsSchema.searchaccepts the canonical bare query string (#7178) #7249's union (a stringsearchpasses through the fill untouched). One mechanical file overlap: both PRs regeneratescripts/query-options-erasure-baseline.json. Whichever lands second re-runspnpm check:query-options-erasure --updateon the rebased tree and commits the one-line result — the file is machine-derived, nothing to hand-merge.registerHookrefuses an emptyobjecttarget and a self-cancelling scope (#6573) #7210 (registerHookrefusals) touchesengine.tsin the hook-registration region (~L850–1550) — disjoint from this PR's find/findOne region; no conflict.having's $icontains comparand shape (#7158) #7236 (having$icontainsgate) —having-filter.tsonly; no overlap.Gates
packages/specbuild (before any dist-derived check, per gen:api-surface / gen:export-origins read the dist without asserting it is FRESH — a stale dist launders a phantom breaking removal into the committed baseline #7122) ✅check:generated— all 11 artifacts up to date ✅check:api-surface— public API surface + factory signatures unchanged (exports did not change → no dual-snapshot needed) ✅check:spec-parsed-alias(ADR-0122 convention) ✅check:engine-double-contract✅check:query-options-erasure— self-test + ratchet hold; baseline ratcheted down and committed ✅turbo run test typecheckfor@objectstack/spec+@objectstack/objectql+@objectstack/runtime— 35/35 tasks green (objectql 2915 tests, spec + runtime suites, all typechecks) ✅/tmp/os-heavy-verify.lock✅Special-inspection items
searchleft unparsed is a measured decision, not an omission — first draft parsed it throughFullTextSearchSchemaandquery-expression-conformance.test.tswent red: the wire path deliberately hands the enginesearch.fieldsas a comma-string (and theqspelling), which the schema refuses. The parse was dropped; the doc onfillQueryAstDefaultsrecords both measurements. Reviewers should confirm they agree the type-level claim (covered by the helper's one documented cast, value deleted unread) is acceptable untilEngineQueryOptionsSchema.searchrejects the bare query string that ADR-0061 D1 calls canonical — so every engine caller that wants it mustas any, losing the whole query's checking #7178's union lands.as anycallers (malformed sort nodes) — deliberate, per-schema strictness riding with the defaulting parse; if triage judges it must ride a major instead of a minor, only the changeset needs editing.'asc') is why this ships as a widening minor — flagged for the reviewer to confirm.Closes #6300
Generated by Claude Code