Repository navigation
fix(objectql)!: the in-process engine refuses an empty projection, as every published declaration does - #27161
Conversation
…shed declaration does ObjectQL.find / findOne read fields: [] as every column, and an expand entry's fields: [] the same way, while the HTTP doors and EngineQueryOptionsSchema.fields refuse it. The entry now refuses it with the declaration's own words and the doors' 400 VALIDATION_ERROR, before any driver read. An absent fields still reads every column. Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87 Co-authored-by: Claude <noreply@anthropic.com>
…on refusal Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87 Co-authored-by: Claude <noreply@anthropic.com>
…caller's limit Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87 Co-authored-by: Claude <noreply@anthropic.com>
…ombinator it does not implement Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 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 8a460b521bf82cc06dadf2d44e12415f4f261cc4 && git checkout 8a460b521bf82cc06dadf2d44e12415f4f261cc4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b8615afaba7a9e6511b601ebc9f15deededea5b0 21ac7e6a13b8562f78e7a88f432e22eedf709702 && git checkout -B drift-repro b8615afaba7a9e6511b601ebc9f15deededea5b0 && git merge --no-ff 21ac7e6a13b8562f78e7a88f432e22eedf709702
node scripts/docs-audit/affected-docs.mjs --json b8615afaba7a9e6511b601ebc9f15deededea5b0
|
…ine's refusal the node now carries The engine refuses an empty projection (#27008), so a get_record node authored with fields: [] fails with that refusal instead of reading the whole row. The control for an absent fields stays; the crud-nodes comment says what the empty list now meets. Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
Doubtful, not wrong: the error message's Nothing in ① is wrong. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #27008
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: updated here —
content/docs/data-modeling/queries.mdx(Field Selection) says an emptyfieldsis refused on the data API and byengine.find()/findOne(), top level andexpandalike;skills/**teachesfieldsonly as a non-empty list and needs no change维护者速读
ObjectQL.find/findOne遇到空投影fields: [](以及任意层expand条目里的fields: [])时直接拒绝,答400 VALIDATION_ERROR,与 REST 门对select: []的回答相同;措辞直接取自 spec 声明EngineQueryOptionsSchema.fields("Name at least one field"),不另写一份。[]当成"不投影",插件、钩子、流程误传空列表时会悄悄拿回全部列(含 owner、组织、审计列)。一条规则:空投影被拒,永不读作全部列。fields仍读全部列,非空投影照旧。回滚即 revert。packages/objectql/src/empty-projection-door.ts(拒绝本体与 expand 递归);packages/objectql/src/engine.ts两处入口调用;packages/objectql/src/engine-empty-projection-refusal.test.ts。pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-empty-projection-refusal.test.ts,预期 10/10 通过;在main上同文件 6 条失败(find/findOne/系统上下文/expand 两层/REST 对等)。What changes
packages/objectql/src/empty-projection-door.ts(new) holdsassertProjectionNamesAField, called at the entry ofObjectQL.findandObjectQL.findOne, right after the option-key gate and before any driver read:mainfind(obj, { fields: [] })400 VALIDATION_ERROR, no driver readfindOne(obj, { where, fields: [] })find(obj, { fields: [], context: { isSystem: true } })expand: { rel: { fields: [] } }, at any depthexpand['rel'].fields)fieldsabsent, ornullfields: ['title']EngineQueryOptionsSchema.shape.fields.safeParse([]), whose issue message is the spec'sPROJECTION_NAMES_NO_FIELD("Name at least one field"). The constant stays module-private indata-engine.zod.ts; no spec change.status: 400andcode: 'VALIDATION_ERROR', the shapeprojectionFieldsValuethrows at the HTTP doors, somapDataErrorserves both callers the same status and code. The test pins it againstfindData({ query: { select: [] } })on the same engine.expandis judged at the entry. An entry's projection is forwarded to the related object's ownfind, which now refuses it, butexpandRelatedRecordscatches a sub-read failure and keeps the bare foreign key. A refusal raised only there would reach the caller as a silently unexpanded field. Judged at the parent's entry, it reaches the caller as a refusal.countandaggregatetake no projection (fieldsis not inENGINE_COUNT_OPTION_KEYS/ENGINE_AGGREGATE_OPTION_KEYS; the key gate refuses it).DataEngineVectorFindRequestSchemahas no engine implementation (vectorFindis declared inspec/contractsonly). The scoped repository'sfind/findOnedelegate to the engine's.engine.tsgains 5 lines (an import and the two calls); the body lives in its own module, in the direction of the [maintainer] refactor(objectql): splitsrc/engine.ts(23,903 lines, +7,501 in 30 days) along itsObjectQLseams, one move-only slice per PR #25522 split.Why
Card #27008, triage
6458590093: "the engine refuses an empty projection with the declarations' own refusal. One rule (#26811's line: 'an empty projection is refused, never read as every column')." #26817, #26909 and #26954 (PR #27007) made every published declaration of a read's projection refuse[]. The in-process engine never parses its options with those schemas, and its projection guards ran only whenast.fields.length > 0, so the empty list reached the driver and every driver served it as no projection.Tests
New
packages/objectql/src/engine-empty-projection-refusal.test.ts, 10 cases, on a realObjectQLover a driver double that projects exactly (an empty list answers every column, asSqlDriverandInMemoryDriverdo) and records each read:mapDataError'sstatus400 andcodeVALIDATION_ERROR, the declaration's words in the message, and zero driver reads:find,findOne, a system-contextfind, anexpandentry, a second-levelexpandentry.findData({ select: [] })and the engine's refusal map to the same status and code.fieldsreads every column;fields: nullreads every column;fields: ['title']projects onfindandfindOne; anexpandwith a named nested projection still expands.Red on main, green on this head, ablation
Red leg, the new file against
origin/main07d18e69engine source (before the fix commit):This head:
Tests 10 passed (10).Ablation, one leg, through
scripts/ablation-replace.mjs(the test imports./engine.js, so the mutation is on the resolved source): the top-level arm ofassertProjectionNamesAFieldreplaced by a marker; anchor 1 → 0, blob19c6bcd1→45086f3d.Tests 4 failed | 6 passed (10): thefind,findOne, system-context and REST-parity cases; the twoexpandcases stay green on their own arm. Restored: blob == HEAD (19c6bcd1),git diff HEADempty.Acceptance notes
packages,ui,examples,appsandskillsthat passfields: []to an engine read: 0. Computed projections handed tofind/findOne(37 call sites by a call-text scan; blind to an options object built away from the call): every one is a non-empty literal or constant, or is guarded non-empty (summaryKeyColumns.length > 0). FLS does not rewriteast.fields(it masks rows), and the two middlewares that touchast.fields(approvals' reader-locale labels, audit's served fields) only add columns. One pass-through: the flowget_recordnode hands its authoredcfg.fieldsto the engine unchanged (crud-nodes.ts), andGetRecordConfigSchema.fieldsisz.array(z.string()).optional(), which accepts[]. No in-repo flow authors it. Such a node read every column before; it now fails with the engine's refusal (success: false, the refusal carried byrefusalCarriedBy). That answer is loud and deliberate, not a widening; the declaration gap is reported to the seat (see the report).engine.find(obj, { fields: ['nope'] })drops the unknown plain column (the finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 tolerance, kept by ruling) and then falls back to*when nothing is left: measured at this head, the driver receives nofieldsand every column comes back. The REST door refuses the same request400 INVALID_FIELD. This is outside the ruling here (it is not an empty projection from the caller) and touches the finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 ruling, so it goes to the seat as an open question.expand's nested declaration.EngineQueryOptionsSchema.expandisz.record(QuerySchema), andQuerySchema.fields(query.zod.ts) isz.array(FieldNodeSchema).optional(), which still accepts[]. The engine now refuses what that declaration admits. Reported to the seat as a spec follow-up rather than edited here (outside the claim's file surface).Clause-②: no (narrowing), as claimed: a shape that read before is refused; nothing is widened.Metadata-protocol: none: no authored metadata key is added, removed or re-shaped, and no save or publish door refuses anything new.Verification
All at head
2e447bbdunless a line says otherwise.Local runs
pnpm --filter '@objectstack/objectql...' build: VERDICT command-exit 0.pnpm --filter @objectstack/objectql build && typecheck && testat26ebfb48: VERDICT command-exit 0; Test Files 477 passed (477), Tests 8898 passed (8898). The two later commits change only the new suite's driver double; that file at this head: Tests 10 passed (10).pnpm build(full, at26ebfb48; no source file changed after it): VERDICT command-exit 0, Tasks 116 successful, 116 total.eslint . --no-inline-configscoped topackages/objectql: exit 0.pnpm --filter @objectstack/dogfood test, owed by the narrowing arm): VERDICT command-exit 0; Test Files 348 passed | 3 skipped (351); Tests 2803 passed | 20 skipped (2823).Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 93 commands for the 5 changed paths, all run at this head.--ranreconciliation: "93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED". Every one exited 0.check-plugin-teardown-shape.mjs --self-testran aftergit fetch --depth=1of its pinned fixture commit;check:type-check-debtran under the verify lock (VERDICT command-exit 0).The first sweep (at
26ebfb48) had two reds, both in the new suite's driver double, fixed ina446baefand2e447bbd:check:objectql-double-limit(the double ignoredlimit; it now applies it after the filter) andcheck:where-matcher(an unhandled$key was read as a field name; the double now throws on it).Hand-run beside the derived list:
check-platform-checklist.mjs --changed-from origin/main,check-commit-card-trailers.mjs --range origin/main..HEAD,check-nul-bytes.mjs: exit 0 each.Human Minimum
packages/specholds no runtime logic. (Not touched.)pnpm lintpasses (scoped topackages/objectql; the repo-wide run is CI's).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87
Generated by Claude Code