Repository navigation
refactor(driver-sql): move the presumed-timestamp update out of sql-driver.ts into sql-driver-presumed-timestamp.ts (#25521 cut 21) - #27162
Conversation
…river.ts into sql-driver-presumed-timestamp.ts The bodies of updateWithPresumedTimestamp (the UPDATE whose updated_at stamp is a presumption, recovering once per table when the column is not there) and resolveUpdatedAtColumnPresence (the columnInfo() question that recovery asks) move unedited into a new seam module. Both methods stay on the class with their docblocks and signatures, and hand the bodies exactly the members they read through PresumedTimestampHost: knex, logger, physicalTableByObject, physicalSchemaByObject and updatedAtColumnState as getters, attemptWithoutPoisoning and resolveUpdatedAtColumnPresence as late-bound closures. Move-only: no behaviour change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oBtKR8pYepVfbCoGV
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 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 881e3aea52cc0c26d5499764dcfde3a11d35b755 && git checkout 881e3aea52cc0c26d5499764dcfde3a11d35b755
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b15b2fcd028a33c6e54ea5aba373ebb884e82957 0379b7673a7d87b745e0aafd6dcfcba8a3a33650 && git checkout -B drift-repro b15b2fcd028a33c6e54ea5aba373ebb884e82957 && git merge --no-ff 0379b7673a7d87b745e0aafd6dcfcba8a3a33650
node scripts/docs-audit/affected-docs.mjs --json b15b2fcd028a33c6e54ea5aba373ebb884e82957
|
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Part of #25521
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6270097840
Ruling-ref: 6348891160
维护者速读
sql-driver.ts里「更新时updated_at列是推定存在的,若该表其实没有这一列就每表一次地降级重试」这一个机制(updateWithPresumedTimestamp与它询问数据库列是否存在的resolveUpdatedAtColumnPresence两个方法体)原样搬进新建的sql-driver-presumed-timestamp.ts;类上的方法、文档注释和签名全部保留,只把方法体转交过去,宿主接口PresumedTimestampHost恰好列出方法体读的 7 个成员。src/sql-driver.ts(22,096 lines, +2,533 in 30 days) along itsSqlDriverseams, one move-only slice per PR #25521 拆分超大文件的第 21 刀;第 20 刀合入后重新测量,这是系列里最后一个收益超过 −30 行的单一机制,sql-driver.ts减少 34 行。本刀之后余下的每个机制都在 −29 行或以内,或被系列限制钉住,席位可据此收束该系列。AsyncFunction,返回值仍是同一个 Promise,系列既有形态)。回滚即还原本 PR。sql-driver-presumed-timestamp.ts(头注释、PresumedTimestampHost、两个函数体);sql-driver.ts中这两个方法的转交桩;sql-driver.ts新增的 3 行 import。pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-24835-update-leaves-caller-data.test.ts src/sql-driver-11176-bulk-and-merge-updated-at.test.ts,预期全部通过。What changes
Cut 21 of the
sql-driver.tssplit (#24586 ruling A, recorded in6270097840). The claim is6463125783. Cut 20 (#27127) merged first, as80064b2556; this cut opens with a fresh seam table taken onorigin/main1900c02cb(below) and takes the one remaining area that is a single mechanism, moves cleanly and pays more than -30: the presumed-timestamp update. The bodies ofupdateWithPresumedTimestampandresolveUpdatedAtColumnPresencemove unedited into a new seam module,packages/drivers/driver-sql/src/sql-driver-presumed-timestamp.ts, through its hostPresumedTimestampHost.updateWithPresumedTimestampissues an UPDATE whoseupdated_atstamp is a presumption (a managed object registered without DDL), fenced in a savepoint inside a caller transaction, and, if the write fails, asks the database whether the column is there; anabsentanswer settles the table, logs one warning and re-issues the caller's payload without the stamp.resolveUpdatedAtColumnPresenceis that question: acolumnInfo()catalog read on the physical table,nullwhen it cannot be answered. Its only caller isupdateWithPresumedTimestamp, so the two are one mechanism. The two callers of the update,updateandupdateMany, stay in the class and still hand it theissuecallback that owns the WHERE and the tenant scope.Why a new module. No existing seam module holds anything about update stamping; the nearest (
sql-driver-autonumber.ts) only sharesattemptWithoutPoisoning, which stays on the class. The new module importssql-driver.tsfor a type only (import type { SqlDriver }), plusimport typeofKnexandDriverOptions, so it adds no evaluation cycle, and the bodies read no module-level declaration, so nothing gainsexportand nothing else moves.PresumedTimestampHostlists exactly the seven members the bodies read, each typed as the class member (SqlDriver['NAME']): theknex,logger,physicalTableByObject,physicalSchemaByObjectandupdatedAtColumnStatefields (as getters), and the protectedattemptWithoutPoisoningandresolveUpdatedAtColumnPresencemethods (as late-bound closures). SoupdateWithPresumedTimestampstill reachesresolveUpdatedAtColumnPresencethrough the driver: a subclass override or an instance spy of it is still the one called (probe readings 4 and 5).The change is move-only, and the card stays open for the seat to close the series. There is no
docs/adr/**edit, nopackages/spec/**edit, no anchor entry change, no gate edit, no test edit and no new gate. Branchclaude/project-thread-ta8bkd(the platform assigned it to this card thread, as the claim records). The move is one commit,0379b7673, on1900c02cb.Net line count (the seat's rule).
sql-driver.tsgoes from 18,367 lines to 18,333: -34../sql-driver-presumed-timestamp.jsimport, placed after cut 20's seam import).sql-driver-presumed-timestamp.tsis new: 130 lines (a 42-line header, three type imports, the 16-line host and the two functions with their one-line docblocks).Path readers. No test, script, doc, ADR or anchor entry reads either body by path, so none is migrated.
sql-driver-11176-bulk-and-merge-updated-at.test.tsnamesupdateWithPresumedTimestampin a comment, still true: the method is still on the class.Serial with the file's in-flight PRs. I read the file list of every open PR on
objectstack-ai/objectstackat the start (27 open, after #27127 merged) and again before opening this one (19 open). None touchespackages/drivers/driver-sql/**.Seam table, fresh, on
1900c02cbsql-driver.tsis 18,367 lines. The 30-day column countsgit blamelines from commits on or after 2026-09-28T08:10Z.scripts/pm/git-history.mjs count --ref=origin/maincounts 78 first-parent commits on the file in 30 days and 32 in the last 7 (the split cuts are most of them). In all, 3,313 lines (18.0%) are from the last 30 days.SqlDriverspans lines 4250-18292 with 350 members: 243 instance methods, 13 accessors and 32 statics, all 32 private or protected.!andsatisfiesbefore it readsthis. Sections are the class's own banner comments. The file is byte-identical to refactor(driver-sql): move date bucketing out of sql-driver.ts into sql-driver-date-bucket.ts (#25521 cut 20) #27127's head7c6f6f1fe8, where I took a pre-measure while it was in the queue.name…assertDialectEmitsconnect…disconnectfind…deletebulkCreate…countrawStatementFault…noteRuntimeIndexDdlbeginTransaction…rollbackTransactionaggregate…unresolvableAggregateColumnRefusaldistinct…unresolvableDistinctColumnRefusalfindWithWindowFunctions…findWithWindowFunctionsexplain…analyzeQuerysyncSchema…reclaimSpacerotationStateByTable…migrateMysqlTimeColumnssetDeferredDdl…flushDeferredSchemaDdldialectName…createNullSafeUniqueIndexlistPhysicalColumns…introspectSchemagetKnex…auditMissingTenanttableNameForBuilder…applyFalseConstantmapSortField…mapAggregateFuncbuildWindowFunction…buildWindowFunctionnowColumnDefault…applyDeclaredColumnDefaultsqliteFilename…isJsonFieldformatInput…formatOutputintrospectColumnOrder…introspectUniqueConstraintsPricing the areas
Eligibility is as cuts 11-20 defined it: an annotated return type or not public; no
getBuilder,this.FIELD.delete/insert/update(, private-static,super, bare-thisorthis-write site; notinsertOnlyUpsertColumns; not a member pinned in place (createColumn,applyFilterCondition,formatInput/formatOutput,initObjects,ensureShardTable,computeTenantField,migrateMysqlDatetimeColumns/migrateMysqlTimeColumns,compileFilters). Each area is priced per mechanism (cut 19's correction), as its stubs minus its bodies, then net of its import edits. The leading row was measured by running the move script on a scratch copy of the file; the rest are the stub estimate (a stub costs 4 lines, +2 per field read, +1 per method read).updateWithPresumedTimestamp-31,resolveUpdatedAtColumnPresence-6, measured)resolveUpdatedAtColumnPresence's only caller isupdateWithPresumedTimestamp.migrateMysqlDatetimeColumns-39,migrateMysqlTimeColumns-32logDurabilityFailure, whichcheck:durability-log-levelresolves within one file only (cut 12).createDatabase-35,explainRowSizeOverflow-45,applySqliteJournalMode-46,applyAutoVacuumIncremental-54, the hash-shadow members (-57 to -83),varcharColumnChars-57ensureSequencesTable-41this.registerManagedObjectMetadata-74this.FIELD.delete(sites (tenant-audit census).upsert,aggregate,bulkCreate,findRows,distinct,findWithWindowFunctions,create,analyzeQuery)getBuilder-building areas (check:tenant-chokepoint'sSCAN_FILESdoes not cover the seam modules).presentReadValue-29)compileFilters(-75) is refused (inferred return type).Why
Why this area, as one area
updated_atand recover once if the column is not there", and its one sub-question, "does the physical table carryupdated_at". Their callers and every member they read stay in the class.presentReadValue).getBuilder:resolveUpdatedAtColumnPresencereads the table's catalog withcolumnInfo()on Knex directly (the same kind of readcheck:tenant-chokepoint's header names forlistPhysicalColumns, with no rows to scope), andupdateWithPresumedTimestampbuilds nothing itself (the caller'sissuecallback builds and scopes the statement, and stays inupdate/updateMany). Neither body reads a static, has athis.FIELD.delete/insert/update(site, usessuperor barethis, or assigns tothis(updatedAtColumnState.set(…)mutates the map the getter returns, the same map). Both methods are protected with annotated return types (aPromiseofboolean | null, and ofnumber).check:tenant-chokepoint's D8 predicates, measured with its ownnullArmsOf: 4 at base and 4 at head, all insql-driver.ts, 0 in the moved bodies and 0 in the module.resolveUpdatedAtColumnPresence'scatchreturnsnulland calls nologDurabilityFailure; neither member appears incheck:durability-log-level --listat base.check:durability-log-level --listprints 165 lines at both ends, identical except that tworunWideningAltersseams insql-driver.tsmove 34 lines up andlogDurabilityFailure's line 3 down (the import).TursoDriverandSqliteWasmDriveroverride neither method nor a member they read (SqliteWasmDriverassignsthis.loggerin its constructor; the host readsloggerthrough a getter at call time). The tests that reach these paths (sql-driver-24835-update-leaves-caller-data.test.ts,sql-driver-11176-bulk-and-merge-updated-at.test.ts) callupdate/updateManyor readupdatedAtStampIsPresumed/updatedAtColumnStatethrough(driver as any); all still reach the class.sql-driver.ts#…pointer or asql-driver.ts::…key, and noscripts/adr-anchors/**record names them or the presumed stamp.The shape (#23050 shape B, as cuts 1-20 apply it)
async(it returns the moved async function's promise, as every earlier async stub in this file does, for example cut 11's autonumber stubs). Its body is now one host literal plusNAME.call(host, …), marked[#25521 cut 21]. The stub cannot throw before the call, so a failure is still a rejected promise (probe readings 4 and 6).knexorloggerreplaced after construction is the one read (probe readings 3, 4, 5).sql-driver.tsimport comment names cut 21 in 2 lines. Neither cites anADR-NNNNletter. The methods' docblocks ([commit 479fba50d]tags, no ADR letter) stay insql-driver.ts.Verification
ADR anchors
check:adr-anchorscounts 1900 decision-letter citations at base1900c02cb(a clean detached worktree) and 1900 at the move commit, over 91 anchored files at both ends; the two outputs are byte-identical. Both runs exit 0.mainmoved.mainmoved 11 commits under the cut (to3346da26df, none inpackages/drivers/**). Its tip counts 1900 letters (a clean detached worktree), and so does a local, unpushed merge of the move commit with it; both exit 0. I did not mergemaininto this branch:git merge-tree --write-treeof the head with it exits 0, so the tree is not stale.check:adr-symbol-anchorsoutput is byte-identical at both ends.The published
.d.tsdoes not changeThis is measured under the series bar (ruling
6348891160, item 1).pnpm turbo run build --filter=@objectstack/driver-sql --forceat each end, against the same dependency tree. The base leg writes basesql-driver.tsinto this worktree and removes the new module, under an EXIT/INT/TERM trap that restores HEAD. The restore is proved by the HEAD blob hashes, an emptygit diff HEADand 0 status entries (RESTORE-OK 89b6a55f6a… 28c6eab163…).dist/index.d.tsanddist/index.d.mtsis the same at base and at the move commit:0f33bf3febaac3c7aa96f68a4226d752766b8203ab7acade523b69f9aaa44d74(unchanged since cut 8);cmpexit 0. These two are the dist's only declaration files (no chunk), so the declaration-reorder bar of ruling6441032442does not come into play.protectedfromupdateWithPresumedTimestampin a copy of the headindex.d.ts(1 hit, then 0). The copy hashes to0f3d0fcb2cc407bc147f3506f3ada0018db26ad0a581c40e6a62355bb73222ab, withcmpexit 1.requireof each dist gives 59 names (sha105757ce5aeat both ends); the ESM namespace ofindex.mjsgives the same 59 at both ends (sha105757ce5ae). Neither moved function norPresumedTimestampHostis among them..call(hostsites inindex.mjsgo from 70 at base to 72 at head (the two stubs).Move-only proof
The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base
1900c02cbagainst the move commit0379b7673. It is a scratch script and is not committed.thisresolveUpdatedAtColumnPresenceupdateWithPresumedTimestampasyncat both ends (the module functions carry it).async, and the signature are identical to base.sql-driver.ts. 180 of 181 base statements are byte-identical; the other is the class. The head adds one statement, the new import (its 2-line comment is its leading trivia).PROOF OK, exit 0.scripts/ablation-replace.mjs, I changedif (columns.length === 0) return null;to=== 1in the module (anchor 1 hit, then 0, blob28c6eab163to8b1b9275bb, checked on disk). The proof then reportsresolveUpdatedAtColumnPresenceas differing (body), printsPROOF FAILED, and exits 1. The restore was proved (blob28c6eab163equals HEAD, emptygit diff HEAD, 0 status entries).Tests
Each end ran under one
os-verify-lockhold, after building that end's driver-sql dist. I compared the vitest JSON per test name and status, base1900c02cbagainst the move commit0379b7673. Every part's exit code was captured on its own (build, probe, three suites: all 0 at both legs).@objectstack/driver-sql@objectstack/driver-turso, against that end's built driver-sql dist@objectstack/driver-sqlite-wasmtsc --noEmit -p .in driver-sql exits 0 on the move commit.pnpm exec eslint --no-inline-config packages/drivers/driver-sqlexits 0 (driver-sql has nolintscript of its own).update()on a presumed table without and with a physicalupdated_at, twice each (the recovery, the warning, theabsent/presentsettling, then the settled path);update()inside a caller transaction (the savepoint fence; the transaction is still usable afterwards) andupdateMany()outside and inside one;resolveUpdatedAtColumnPresencedirectly: present, absent, a missing table, aphysicalTableByObjectalias, aphysicalSchemaByObjectschema, on a transaction, a throwingcolumnInfo()and an empty one (knexreplaced on the instance), thenknexrestored;updateWithPresumedTimestampdirectly with fakeissuecallbacks over every branch (absent / unknown / present answers, with and without a transaction), with an instance spy onresolveUpdatedAtColumnPresenceand onattemptWithoutPoisoning(both reached, arguments recorded), the caller's payload left intact, andloggerandupdatedAtColumnStatereplaced on the instance (the replacements are the ones read);resolveUpdatedAtColumnPresence(answeringfalse, thennull) and ofattemptWithoutPoisoning, reached fromupdate();null);Functionat head where it wasAsyncFunctionat base (typefunctionand arity 2 / 4 identical). That is the stub droppingasync, as every earlier async stub of this series does; no test, consumer or.d.tsreads it.test:repo. Not run locally; it is CI's run on this PR.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 48 families for this diff (the same 48 cuts 17-20 derived). I ran each one on its own and captured its exit code before any pipe, after a fullpnpm buildof the move commit (116 of 116 tasks).0379b7673: 48 run, all 48 exit 0.--ranreconciles:48 derived famil(ies) accounted for — 48 run, 0 NOT-MEASURED (a DERIVED zero — all 48 recorded an exit code and none of them is 3), exit 0. (I fetched the pinned fixture commit ofcheck-plugin-teardown-shape.mjs --self-testinto this shallow clone first.)check-adr-anchors,check-adr-symbol-anchors,check-tenant-audit-census,check-platform-checklist --changed-from origin/main,check-commit-card-trailers --range origin/main..HEAD,eslint --no-inline-config packages/drivers/driver-sql,check-durability-degradation-log-level --list. In a clean detached worktree at base1900c02cband on the move commit,check-adr-anchors,check-adr-symbol-anchorsandcheck-tenant-audit-censusprint byte-identical output;check-object-def-param-keysdiffers only in its file count (3392 to 3393, the new module);check-durability-degradation-log-level --listprints the same 165 lines, line numbers aside.check:object-def-param-keyscompares 7 override parameter positions over 3393 files with every control firing;check:tenant-chokepointreads 24getBuilder()bindings across 3 files;check:durability-log-levelreads 48 durability-critical catch seams and 89 read seams;check:dual-build-cjs-loadsloads 112 entry points across 69 packages;check:driver-conformancecovers 55 cells, 0 in its DEBT ledger).The 48 derived commands:
node scripts/check-closing-keyword-parity.mjs,node scripts/check-closing-keyword-parity.mjs --self-test,node scripts/check-comment-mask-adoption.mjs,node scripts/check-comment-mask-adoption.mjs --self-test,node scripts/check-comment-mask-corpus.mjs,node scripts/check-dts-emitted.mjs --self-test,node scripts/check-issue-citations.mjs,node scripts/check-plugin-teardown-shape.mjs,node scripts/check-plugin-teardown-shape.mjs --self-test,node scripts/ci/assert-turbo-remote-cache.mjs --self-test,node scripts/ci/plan-test-core-shards.mjs --self-test,node scripts/ci/prune-turbo-cache.mjs --self-test,node scripts/docs-audit/check-affected-docs.mjs,node scripts/docs-audit/check-drift-comment.mjs,pnpm --filter @objectstack/spec run check:duration-unit-keys,pnpm check:ci-filter-parity,pnpm check:closed-ceremony-text,pnpm check:cross-package-test-inputs,pnpm check:dispatcher-error-vocabulary,pnpm check:doc-authoring,pnpm check:driver-conformance,pnpm check:driver-memory-census,pnpm check:dts-closure,pnpm check:dual-build-cjs-loads,pnpm check:durability-log-level,pnpm check:error-status-conformance,pnpm check:gitlink-declared,pnpm check:issue-citations,pnpm check:keyed-text-bounds,pnpm check:lean-entry-closure,pnpm check:logger-receiver-detach,pnpm check:nul-bytes,pnpm check:object-def-param-keys,pnpm check:org-identifier,pnpm check:page-declaration-shape,pnpm check:platform-object-tenancy-census,pnpm check:published-files,pnpm check:refd-timer-probe,pnpm check:registry-log-declared,pnpm check:rest-log-spy-declared,pnpm check:slot-lookup,pnpm check:sourcemap-no-sources-content,pnpm check:system-context-census,pnpm check:tenant-chokepoint,pnpm check:test-source-alias,pnpm check:tier-file-adoption,pnpm check:undeclared-dep-imports,pnpm check:watch-hint-literal.Acceptance notes
check:tenant-chokepoint'sSCAN_FILESdoes not listsql-driver-presumed-timestamp.ts. The moved bodies hold 0 tenant predicates and build through nogetBuilder(onecolumnInfo()catalog read, and the caller'sissuecallback). Noted, not filed: wideningSCAN_FILESwould be a gate change.sql-driver.tshas the same 30{@link X}sites (21 names) that are neither a top-level binding nor aSqlDrivermember at base and at head (the name set is identical). The module has none.sql-driver.tspays more than -30 under the net-line rule and the series limits: the largest free one ispresentReadValueat -29, and every larger member is pinned (createColumn,applyFilterCondition,formatInput/formatOutput,initObjects,ensureShardTable,compileFilters, the two MySQL widenings) or blocked by a series limit (getBuilder-building doors, private-static readers,thiswriters,this.FIELD.delete(sites). The seat can close the series on this cut.Human Minimum
packages/specchange.PresumedTimestampHost, in a new module thatindex.tsdoes not re-export.eslint --no-inline-configpasses (repo-widepnpm lintis CI's run).main, one pull request.skip-changeset; both declaration lines above.packages/specnot touched.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oBtKR8pYepVfbCoGV