Repository navigation
fix(service-automation): get_record names id in the projection it hands the engine, so output.id is the row id whatever fields names - #22355
Conversation
…projection omits id Red on main: the SQL drivers return exactly the columns a projection names, so a get_record whose fields omit id answers output.id undefined. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…ds the engine
The single-row branch answers output.id from the row it read. The SQL
drivers return exactly the columns a projection names, so a fields list
without id read a row with no id and {node.id} was empty. The branch now
appends id to a non-empty projection that does not name it; no fields,
an empty list, a projection naming id, and the find branch are unchanged.
The record carries the id the engine returned.
Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ
Co-authored-by: Claude <noreply@anthropic.com>
…t-record-id-projection Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…ormula projection The formula-projection pins landed on main pin the exact column set on both branches. The single-row branch now names id in its projection, so its served row carries id beside the named fields; the list branch is unchanged. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 6 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 bd00bfb3d8924ddb8eb41ec2db55281f583fc946 && git checkout bd00bfb3d8924ddb8eb41ec2db55281f583fc946
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b7e01fbbd2c3340ae95e54e2ec220314e9ae728e f68b7d55c5d8390c5dbd136014423cdcc85d2dd1 && git checkout -B drift-repro b7e01fbbd2c3340ae95e54e2ec220314e9ae728e && git merge --no-ff f68b7d55c5d8390c5dbd136014423cdcc85d2dd1
node scripts/docs-audit/affected-docs.mjs --json b7e01fbbd2c3340ae95e54e2ec220314e9ae728e
|
Fixes #22344
Clause-②: no
A
get_recordnode's single-row (findOne) branch answersoutput: { record, id, object }, and a later node reads thatidas{read.id}. The node handed the author'sfieldsto the engine unchanged, and the SQL drivers return exactly the columns a projection names, so a projection withoutidansweredoutput.idundefined with no error. The branch now namesidin the projection it hands the engine.Measured first: the defect is real on
main(H1)Measured on
origin/main35afb1587plus the red-first test commit4abddae9a, through the realObjectQL(ObjectKernel+ObjectQLPlugin) and the registeredget_recordexecutor,runAs: 'system', one row{ title: 'alpha', status: 'open' }:fieldsoutput.idrecordkeysdriver-sql(better-sqlite3:memory:)['title']undefined[title]driver-sqlidincludeddriver-memory['title'][id, title]driver-memoryThe memory row is the contrast: its
projectFieldsaddsidto every projection itself, so the defect showed on the SQL drivers only. The flow-level shape the card names was red too:start → get_record(fields: ['title']) → update_record(filter: { id: '{read.id}' })was refused withupdate_record: refusing to run — 1 filter condition(s) resolved to nothing and were dropped from the query: {read.id} (at id).Where
idis added (H2)projectionNamingId(fields)inbuiltin/crud-nodes.tsreturns the author'sfieldswithidappended, and thefindOnebranch hands that toserveFamilyReadas the node's projection:idis handed over as written, soidis never doubled;fieldsabsent or[]is returned unchanged, so a whole-row read stays a whole-row read and no projection is invented;findbranch (limit > 1) does not call it and is byte-unchanged.Placement, measured: adding
idbeforeserveFamilyRead(shipped) and adding it inside the projection callback (fields: projectionNamingId(projection), a one-off mutation, not committed) give the same answers on both paths, 23/23 across the threeget_recordsuites atf6e3ac4ba. On the stored-metadata path the family projection only adds and then dropstype, andidsurvives either way. BeforeserveFamilyReadis the shipped form so that the family door sees the node's whole projection as the node's own, instead of a column it does not know about arriving after it decided.recordcarriesidwhen the author did not name it (H3)The record the node serves, and binds to its
outputVariable, carries theidthe engine returned beside the named columns. The reasons:{rec.id}through theoutputVariablethen agrees with{read.id}; stripping it would leave the same defect on the variable spelling, which an author is as likely to write;driver-memory(table above).Two existing pins asserted the exact column set on the single-row branch, and each now expects the named columns plus
id:get-record-stored-metadata-family.integration.test.ts: the body-without-type projection now serves[checksum, id, metadata, name], andtypeis still taken back off;get-record-formula-projection.integration.test.ts, which landed with PR fix(objectql): a find/findOne projection that names a formula field returns that projection, not every stored column #22337 while this branch was open (merged in fromorigin/main54c3ce10c): theonebranch cases now includeid: 'line_1', and thelistbranch cases are unchanged. That PR's engine-level trimming is untouched. Itsfieldsprojection is now handed to the engine withidnamed, which the trim honours.Pins (H4),
get-record-id-projection.integration.test.ts{ title: 'alpha' });fieldsomittingid:output.idis the row id,recordis{ id, title }, and the engine was handed['title', 'id'](red onmain);fieldsnamingid: handed as written (['id', 'title']), soidappears once;fieldsandfields: []: no projection is invented, andoutputequals{ record: WHOLE_ROW, id, object }with the engine's own whole-row answer asWHOLE_ROW;update_recordfiltering on{read.id}after a narrowedget_recordcloses exactly that row, andrecis{ id, title }(red onmain).Ablation, one-off, after the fix was committed (no permanent mutation file)
Each leg was run through
scripts/ablation-replace.mjsin wrap mode. Each anchor hit once, the mutation was verified on disk (anchor count and blob hash), and the restore was proven: the blob equalsHEAD(94641f32745d) andgit diff HEADis empty, with a belttraponEXIT INT TERM. The subject is imported from source (./crud-nodes.js), so nodist/leg applies. Final run atf6e3ac4baover the threeget_recordsuites:return [...fields, 'id']replaced byreturn fields(fix reverted){read.id}, family projection, and the 4 formulaonecasesfields.includes('id')guard dropped['id', 'title', 'id']fields.length === 0guard dropped['id']fieldsanswered['id']idadded inside the callback insteadThe first M3 attempt at
d6ccba323was a no-op. Its replacement text was a substring of its anchor, soablation-replacerefused it (replacement count 1 to 1) and never ran the tests. It was re-anchored as M3b and run.Verification
pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2atf6e3ac4ba: 178 files / 2177 tests passed. Atd6ccba323, before the merge, it was 177 / 2169.pnpm --filter @objectstack/service-automation run typecheckatf6e3ac4ba:tsc --noEmitclean.check:test-typecheckOK, 0 files / 0 errors;tsconfig.test.jsonincludessrc/**/*, so the new test is compiled.f68b7d55cchanges only the changeset's wording againstf6e3ac4ba(git diff --stat: one file,.changeset/).--no-inline-config,--format json) on the 4 changed.tsfiles: 4 files, 0 errors, 0 warnings. All 4 fall under thepackages/**/*.{ts,…}config objects (--print-configresolves each). The config enables no type-aware linting (noparserOptions.project, pereslint.config.mjs), so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintis CI's.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) atf68b7d55c; all 64 run there and all 64 exited 0.--ranreconciliation:64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3). The first pass atd6ccba323readcheck:dual-build-cjs-loadsas PREREQUISITE NOT MET (no workspacedist/). After this package was built it passed:106 published require entry point(s) across 66 package(s) load. CI's own jobs were not run locally: Test Core, Dogfood, Temporal, Build Core, the workspace type-check lanes, and the roster and wide-population families thatdispatch-gatesnames outside its 64.Acceptance notes
GetRecordConfigSchema.fields(spec) and the descriptor'sconfigSchemadescribefieldsas "only these fields are read (default: all)". On the single-row branch the served record now also carriesid.packages/specis outside this card, and the descriptor carries the same sentence as the spec, so neither was edited. A wording follow-up would nameidthere. Owner: none;domain:servicesseat to decide.findbranch (limit > 1) declares no top-levelid. Its rows still carry only the named columns on the SQL drivers whenfieldsomitsid. That is the author's projection, and this PR leaves it unchanged by the card's scope.driver-mongodbwas not measured, because the package's tests do not depend on it.Generated by Claude Code