Repository navigation
objectql: a stored field's expression is computed on insert before the autonumber is issued, so an expression that reads the record number stores null (or its has() fallback) until a later update by id #25073
Description
Ruled: 6335312761 · letter A · 2026-10-22T07:31Z
Filing gate ① a product defect with reach measured. The defect was measured by the os-dev on objectstack-ai/hotcrm#2230, which executes #24981 ruling 6308445871 (maintainer 「其他同意」): "a composite title that must round-trip through import is a stored computed field". The run was on HotCRM main c8a7a704 with @objectstack/* 18.0.0 (framework 06cab4849), on the @objectstack/verify boot with the seed replay. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.
Reader (who acts): objectstack domain:engine triage. The fix lands here. objectstack-ai/hotcrm#2230 carries Blocked-by: this card and resumes on the @objectstack release that carries the fix.
The contract
FieldSchema.expression (packages/spec/src/data/field.zod.ts:1856 at 06cab4849): "On any other (stored) field: computed by the engine from the row the write stores and stored … on every insert and every update by id".
A row the insert stores carries its issued record number. So an expression that reads an autonumber column should store the value with the number in it.
The finding, as measured
- Setup: HotCRM's nine record titles switched locally from
Field.formulato a storedField.text, with the sameexpression(the shape the ruling prescribes). The change was not committed. - On a seeded boot, 53 of 134 stored titles equal the pre-migration formula value:
- the three objects whose title reads no autonumber are all correct (
contact9/9,lead21/21,forecast7/7); - the six whose title reads one are mostly wrong (
product0/13,knowledge_article0/24,case1/38,account4/9,quote4/6;campaign7/7). - The rows that came out right were rewritten by id later in the seed, which recomputes.
- the three objects whose title reads no autonumber are all correct (
- Single inserts as the dev admin through REST (
POST /api/v1/data/<object>):crm_caseanswers 201 and storesdisplay_titlenull besidecase_numberCASE-00039. The expression isrecord.case_number + " - " + record.subject.crm_productstores "Probe 2230 product" withoutPRD-0014. Itshas(record.product_code) ? … : record.namefallback took the empty branch.- Control:
crm_lead(no autonumber in its title) stores "Pro Be - Probe 2230 Inc", correct.
- Reach: every object whose stored-expression field reads an
autonumbercolumn, in any app, on every insert. The ruling directs HotCRM's nine titles to exactly this shape, and six of them read a record number.
Seat review (source at 06cab4849, unchanged on origin/main 688052e147)
packages/objectql/src/engine.ts, insert path:
computeStoredExpressionsruns at:15807(before the hooks) and again at:16410("after the hooks and the strips").applyAutonumbersissues the record number afterwards, at:16466. Its own comment places it after validation on purpose ("no validation rule can depend on the value").- Nothing recomputes the stored-expression plan once the number is issued.
- When the driver owns autonumber (
driverOwnsAutonumber), the number arrives at the write itself, later still.
Direction (the reader's call):
- Recompute the stored-expression plan for the fields whose expression reads an issued
autonumber, afterapplyAutonumbersand before the row is written; - or issue the number before the second stored-expression pass.
- Either way, the driver-owned path needs an answer too.
- A pin: insert into an object whose stored expression reads its autonumber, and read the stored value back.
Dedupe
Method: every objectstack-ai/objectstack issue title (every state), listed through the repo-scoped issues?state=all endpoint and refreshed with since=2026-10-21. Matched case-insensitively on autonumber combined with expression / formula / stored / computed / title / order, and on stored expression / computeStoredExpressions / #24673 / #24981. The search API is refused for this session.
Results:
- Related:
- import: a lookup cell that names the record by the target object's title field answers reference_not_found — resolveRef never reads nameField, and a formula title is not matchable #24981 (the ruling that prescribes this shape);
- spec(data): once PR #24673 lands, a client value for a stored-expression field is overwritten with no report —
DroppedFieldsEvent.reasonhas no value for "written, but recomputed", soonFieldsDropped/droppedFieldsstay silent #24690 and feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778 (closed: reporting a client value a stored expression replaces); - objectql 18.x: under
strictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000 (open: refusing such a write understrictReadonlyWrites); - ADR-0079 D3, the validate-record-title lint and formulas.mdx still steer a composite record title into a formula field, which ruling 6308445871 says cannot be matched on import (use a stored computed field) #25071 (open: the docs and lint that steer a composite title into a formula).
- None is about the insert order against autonumber. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 30 (the lane's p1 and p2 too while seat 2 is vacant, by the maintainer's 「engine 车道有任务为什么不派发」) · 2026-10-22T05:07Z · p1,
bug,domain:engine. The lane's only open p1 inpm:queue, after #25144 landed (504d8b3acd). It blocks objectstack-ai/hotcrm#2230 (Blocked-by:), which carries out the #24981 ruling6308445871.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25073-stored-expression-after-autonumberWorktree:
objectstack-issue-25073Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)Premise, re-read at this claim on
origin/main504d8b3acd(the card is a day old): it holds, and one path is wider than the card says.computeStoredExpressionsinside the insert body.applyAutonumbersissues the engine's number after it, per row.driver-sqladvertisessupports.autonumber: true, andsqlite-wasmandtursoinherit it. SoapplyAutonumbersreturns[], andSqlDriver.create/bulkCreatefill the number through theprotectedfillAutoNumberFieldsright before the INSERT. On that path the engine never holds the number before the write. This is the path HotCRM measured.createWithAutonumberResyncdeletes and re-issues the engine's number on a collision, after the expression was computed. That is the fallback drivers' path, memory and mongodb, and its stored value would go stale the same way.File surface:
autonumberfield gets the issued number on each of the three paths:applyAutonumbers), on single and batch inserts and in partial mode;createWithAutonumberResync;driver-sqlcreate/bulkCreate).packages/objectql/src:autonumberonce the number exists, before the row is written. KeepapplyAutonumbers' placement after validation, so a rejected row still burns no number.afterInserthooks, the response and every reader see the final stored value, and no reader can ever see the row without it.needs_decision, before building it: a route that needs a newIDataDrivermember, a newDriverOptionsfield, or a call into a driver's non-contract method.instanceof SqlDriveror driver-name branch.driver-sqlpath (SQLite) and the engine-issued path, single and batch, plus the collision re-issue case;has()fallback shape;@objectstack/objectqlpatch(and any package the measured route moves).Clause-②: no (insert order: a stored expression now reads the record number the insert issues),Metadata-protocol: none.⛔ Not in scope:
packages/spec, includingFieldSchema.expression's text (it already says the insert's row);strictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000 (strictReadonlyWrites), ADR-0079 D3, the validate-record-title lint and formulas.mdx still steer a composite record title into a formula field, which ruling 6308445871 says cannot be matched on import (use a stored computed field) #25071 (docs and lint), and Store the 9 formula record titles as stored computed fields and backfill them, so import can match them and sort and search can use them (objectstack#24981 ruling A) hotcrm#2230 itself;driver-memory/driver-mongodbbeyond what the engine's own fallback path needs (the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freeze).Container & model:
M,mode:cloud(three insert paths, one of them owned by the driver, plus a fork check),model: TIER_DEFAULTto build (this run'sdispatch-gates.mjs --tier --paths packages/objectql/src/engine.ts: "no path-derived mandate"). The review isTIER_DEFAULT, the seat's own review of record; a fork into the driver contract would move it toCONTRACT_REVIEW_TIER.Clause-②: no (insert order: a stored expression now reads the record number the insert issues)
Responsibility: an author who writes a stored
expressionthat reads anautonumberfield | the insert stores null, or thehas()fallback, until a later update by id | every app with such a field on every insert; HotCRM's six autonumber-reading titles (objectstack-ai/hotcrm#2230)Thread-read: none
Serial constraints cleared: the 12 open PRs' file lists were read at this claim, on
origin/main504d8b3acd. None touchespackages/objectql/src. #24876 (changeset-release/main, 294 files) touches none.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T05:09Z:mode:cloud. The dev session issession_017BrhF7dagr9BaqR2ssbhUQ. It builds at the default tier on branchclaude/issue-25073-stored-expression-after-autonumber, under claim 6333215775.autonumberfield gets the issued number on each of three insert paths. Each reading is shown with a probe, covering atomicity, whatafterInsertand the response see, hooks fired, collisions, and batch and partial mode:applyAutonumbers);createWithAutonumberResync);driver-sql, the production path HotCRM measured).applyAutonumberskept after validation;has()fallback;patchchangeset:Clause-②: no (insert order: a stored expression now reads the record number the insert issues),Metadata-protocol: none.IDataDrivermember, a newDriverOptionsfield or a driver's non-contract method isneeds_decision, and so is a route that cannot keep the insert one atomic act. The dev reports the measured routes with a four-axis recommendation before building that path.packages/spec; a backfill of stored rows; objectql 18.x: understrictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000; ADR-0079 D3, the validate-record-title lint and formulas.mdx still steer a composite record title into a formula field, which ruling 6308445871 says cannot be matched on import (use a stored computed field) #25071; Store the 9 formula record titles as stored computed fields and backfill them, so import can match them and sort and search can use them (objectstack#24981 ruling A) hotcrm#2230.TIER_DEFAULTreview of record before the queue. A fork into the driver contract would move it toCONTRACT_REVIEW_TIER.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25073, "status": "needs_decision", "branch": "claude/issue-25073-stored-expression-after-autonumber", "pr": null, "session": "session_017BrhF7dagr9BaqR2ssbhUQ (dispatched by seat session_01JtSQQ3EWsbNPzyYMug1tXy, claim 6333215775 names this branch)", "premise_still_valid": true, "summary": "Measured before any edit, on origin/main 504d8b3acd. Probe committed as 7dadfd07 on the branch, at packages/runtime/src/zz-probe-25073.integration.test.ts. No product code was changed and no PR was opened. The defect reproduces on all three paths. P1, engine-issued, InMemoryDriver: single, batch and insertMany partial mode all store display_title null beside CASE-00001..00005, and the product has() shape stores 'Probe product' without PRD-0001. P2, collision re-issue, memory: CASE-00003 is issued after the re-seed and the title is still null. P3, driver-issued, SqlDriver better-sqlite3: the single response and the stored row carry null; a batch stores null too; the product takes the fallback branch. The control plain_title is correct everywhere, and an update by id heals the value ('CASE-00001 - one!'). P6: an afterInsert hook sees null and so does the response. Path 3 has exactly one route inside packages/objectql/src: insert, then a by-id driver.update of the dependent fields, inside a unit the engine opens. P4 measures what that unit costs. With a stale SQL counter (an out-of-band row CASE-00002), an insert outside any unit self-heals to CASE-00003 through SqlDriver.create's own re-seed. The same insert inside engine.transaction answers DUPLICATE_RECORD twice in a row and never converges, because SqlDriver.create sets mayRetry false when options.transaction is set. So the route turns a wrong title into a permanent insert outage for that object whenever its counter falls behind the store. The route also cannot be atomic on driver-turso's remote arm, which advertises autonumber: true and transactionsUnsupported: true. Under the fork rule this is needs_decision. I am not landing paths 1 and 2 alone; the reasons are in open_questions.", "tests": "Probe only. Command: bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/zz-probe-25073.integration.test.ts'. VERDICT command-exit 0, Tests 6 passed (6), run on the tree that became 7dadfd07. Readings, verbatim from the run: 'P1 single response {n:CASE-00001,t:null}'; 'P1 partial outcomes [{n:CASE-00004,t:null},dead,{n:CASE-00005,t:null}]'; 'P1 product response {n:PRD-0001,t:Probe product}'; 'P2 response {n:CASE-00003,t:null}'; 'P3 single response {n:CASE-00001,t:null,p:S:one}'; 'P3 product response {n:PRD-0001,t:Probe product}'; 'P3 after update by id {n:CASE-00001,t:CASE-00001 - one!}'; 'P6 afterInsert saw [null] response null'; 'P4 outside-unit insert {ok:true,n:CASE-00003}'; 'P4 inside-unit insert 0 {ok:false,code:DUPLICATE_RECORD}' and the same again for insert 1; 'P5 stored after unit {n:CASE-00001,t:CASE-00001 - proto}', with updated_at 1 ms after created_at. Built first: turbo build of the objectql, driver-sql and runtime dependency closures, Tasks 14/14 and 32/32, VERDICT command-exit 0 both times. No ablation: no tested behaviour changed.", "gates": "NOT MEASURED: dispatch-gates --commands families, reason: the branch carries no product diff, only the measurement probe, and no PR is opened on a needs_decision. When the build resumes, the derived families are owed on the real diff.", "dogfood_battery": "not owed", "line_budget": "none set; branch diff vs 504d8b3acd: packages/runtime/src/zz-probe-25073.integration.test.ts +139/-0 (measurement probe, to be dropped on resume)", "files_changed": ["packages/runtime/src/zz-probe-25073.integration.test.ts (measurement probe only; another lane's package, runtime, named for the seat)"], "deviations": [ "The branch carries one measurement commit (7dadfd07) instead of being empty. This is the measurement-branch push the needs_decision budget allows. Whoever resumes deletes the probe file in the first commit.", "Turso remote arm: read from code, not probed (turso-driver.ts sets supports.transactionsUnsupported to this.transportMode === 'remote', and the driver inherits autonumber: true). MongoDB: not probed. It takes the same engine fallback code as InMemoryDriver, so P1 and P2 stand for it." ], "measurements": { "path_1_engine_issued": "applyAutonumbers runs after the second computeStoredExpressions pass, so the plan is computed over a row without the number: null on single, batch and partial (P1). Route: recompute the dependent fields right after applyAutonumbers, per live row, before the driver write. Atomic by construction (one create or bulkCreate). afterInsert and the response see the value. No update hooks. Validation stays before issue, so a rejected row burns no number. Partial mode: dead rows are skipped already.", "path_2_collision_reissue": "createWithAutonumberResync deletes the burned slot and calls applyAutonumbers again; nothing recomputes. Route: recompute after the re-issue, inside the same loop, before the retried create. One write, atomic. Same costs as path 1.", "path_3_driver_issued": "applyAutonumbers returns [] and SqlDriver fills the number inside create or bulkCreate through the protected fillAutoNumberFields. The engine first holds the number in the row the driver returns. Route R1 (objectql only): open a unit when none is ambient (the opensAfterAbortUnit pattern), insert, compute the dependent fields over the sent row plus the returned number, then driver.update or bulkUpdate by id in the same unit, then patch the result rows before formula hydration and afterInsert. Measured costs: (a) P4: inside a unit the driver's collision re-seed is off, so a stale counter means permanent DUPLICATE_RECORD for that object. Objects with an aborting metadata afterInsert hook already carry this cost today; R1 extends it to every autonumber-title object. (b) Turso remote has no unit, so the atomicity bar is unmet there. (c) An object on a non-default datasource cannot get a unit from engine.transaction. (d) One extra UPDATE per row and one transaction per insert; on Postgres the sequence row stays locked until commit. (e) updated_at is restamped 1 ms after created_at (P5). (f) No update hooks or audit events, by construction (a driver-level update). (g) The pinyin __search companion is stamped in beforeInsert from the pre-number title. Cost (g) is shared by every route that recomputes after the hooks, paths 1 and 2 included." }, "mcp_calls": "2 — mcp__github__issue_read (get, get_comments), read-only; no MCP write tool", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/25073/comments (this report) through the fleet-write relay; git pushes: 2 to the branch (the empty branch, then the probe commit 7dadfd07)", "open_questions": [ { "question": "Path 3 (driver-issued, driver-sql, sqlite-wasm and turso, which is HotCRM's production path): how does a stored expression get the record number the driver issues?", "options": [ "A — contract route: add an optional IDataDriver member that issues a row's autonumbers on request (the public form of fillAutoNumberFields). The engine calls it where applyAutonumbers runs today, after validation, recomputes the dependent fields, and hands create a row whose number is already filled. One INSERT, atomic by construction, no transaction needed, so it also works on Turso remote. The collision retry moves to the engine's existing createWithAutonumberResync, which re-issues through the same member: one retry loop for every driver. Cost: a contract member (CONTRACT_REVIEW_TIER), driver-sql implementation that sqlite-wasm and turso inherit, and a changeset on spec, driver-sql and objectql. Size: M.", "B — R1, objectql only: insert, then a same-unit by-id driver update. Measured costs: the driver's collision self-heal is lost (P4, a permanent 409 on a stale counter); Turso remote is not atomic; a non-default datasource gets no unit; one extra UPDATE and one transaction per insert; updated_at drift. Size: S–M.", "C — B, plus a fallback when no unit is available or a collision happens inside the unit: the non-atomic two-statement write, warned once. It breaks the dispatch's atomicity bar on purpose.", "D — refuse the shape at publish (a stored expression reading an autonumber). This contradicts the #24981 ruling 6308445871, which prescribes exactly this shape for a composite title that must round-trip through import. Not recommended." ], "recommendation": "A, on the four axes. Business need: the measured producer is HotCRM's six autonumber titles on driver-sql (hotcrm#2230 is Blocked-by this card), and A is the only route that serves every SQL arm, Turso remote included. Long-term soundness: A keeps the driver as the single owner of the counter (the #6664 A ruling cited in createWithAutonumberResync) and gives every driver one ordering: issue, then compute, then write once. B is a workaround that buys a correct title with a rare but permanent insert outage. Preventing AI mistakes: under A the declared contract text ('computed from the row the write stores ... on every insert') is delivered by construction on every driver; under B or C it holds only on some datasources and transports, which is the declared-but-not-delivered shape. Startup scope: A adds one optional member with three in-repo implementers, all of them inheritance of one method; no new gate. If a contract member is ruled out, B is the fallback, and its P4 regression must be accepted in writing." }, { "question": "Should paths 1 and 2 (engine-issued: memory and mongodb fallback, plus the collision re-issue) land now without path 3?", "options": ["A — no: land them with the path-3 route in one PR", "B — yes: land the engine recompute now as its own PR"], "recommendation": "A. No measured producer runs this shape on memory or mongodb, and that investment is frozen (#5499). Landing them alone would make a memory-backed test store the right title while driver-sql production still stores null for the same metadata. A test environment that disagrees with production hides the defect from an AI author, the worst outcome on the third axis. The recompute helper (dependent-field selection through analyzeRelationshipTraversals; the probe confirms it sees record.x, has(record.x), record.?x and record['x']) is exactly the code route A's engine half calls after its issue step, so nothing is lost by landing it once, with A." }, { "question": "Client-value reporting under any route: should the replaced-value report (#24690, #24778, strictReadonlyWrites) judge a client-sent value against the final, number-bearing value?", "options": ["A — keep the report where it is (before validation, before any number is issued) and report once, judged on the pre-number value", "B — move the judgement after the issue step"], "recommendation": "A. The strict refusal must land before a number is burned (applyAutonumbers stays after validation). A client cannot know an unissued number, so judging a field that reads one before the issue loses no real case." } ], "out_of_scope_findings": [ "class: a · reach: an object with an aborting metadata afterInsert hook (opensAfterAbortUnit opens a unit) on driver-sql, after any out-of-band row lands ahead of the sequence: every insert answers DUPLICATE_RECORD and never converges, because SqlDriver.create skips its #5495 re-seed when options.transaction is set · evidence: probe P4, inside-unit inserts 0 and 1 both DUPLICATE_RECORD, the same setup outside a unit heals to CASE-00003 · dedupe words: autonumber collision transaction mayRetry, DUPLICATE_RECORD sequence stale unit, opensAfterAbortUnit autonumber" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
[待裁] 新建记录时,引用编号的存储字段存成空值:编号由 SQL 驱动在写库前一刻才生成,引擎要怎样拿到它?
domain:engineseat 1(seat post #6367)·os-project-manager·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T05:42Z。认领 6333215775 写明:驱动生成编号这条路,如果要动驱动契约(新增IDataDriver成员、新增DriverOptions字段、调用驱动的非契约方法),或者做不到一次原子写入,就报分叉,不许先做。dev 先实测、没改产品代码,报了needs_decision(报告 6333405453)。分支上只有一个测量探针提交7dadfd07,没有开 PR。本席复核了实测,选项落在公开契约的形状上,现有规范定不了,按 SKILL〈升级与决策〉交维护者裁决。认领 6333215775 同笔释放。一句话问题: 标题写成「工单号 + 主题」的对象,新建工单时标题存成空值,或者只剩主题;要等下一次按 id 修改这条记录才会补上。HotCRM 有六个对象的标题是这种写法。
背景(实测)
dev 在
origin/main504d8b3acd上用真实引擎跑了探针(packages/runtime/src/zz-probe-25073.integration.test.ts,6/6 通过,只读测量)。CASE-00001…00005。产品对象的has()兜底写法存成「Probe product」,没有PRD-0001。CASE-00003,标题仍是 null。driver-sql,SQLite):单条的响应和落库的行都是 null,批量也是 null。这是 HotCRM 生产走的路径。afterInsert钩子和响应看到的也都是 null。CASE-00001 - one!)。driver-sql声明supports.autonumber: true,driver-sqlite-wasm和driver-turso继承这一声明。SqlDriver.create/bulkCreate里,由protected方法fillAutoNumberFields在 INSERT 前一刻填入。SqlDriver.create会关掉自己的编号冲突自愈(const mayRetry = options?.transaction === undefined)。CASE-00003;事务内两次插入都回DUPLICATE_RECORD,永远不收敛。driver-turso远程模式不支持事务(transactionsUnsupported: this.transportMode === 'remote'),做不到原子。Governing text
packages/spec/src/data/field.zod.ts(FieldSchema.expression):「On any other (stored) field: computed by the engine from the row the write stores and stored … on every insert and every update by id」。插入落库的那一行带着它发出的编号,所以契约要求标题里有号。docs/adr/0034-transactional-writes-and-ambient-transaction.md§3「Autonumber (DB-native autonumber sequence (multi-instance correctness) #1603)」:「have the engine defer autonumber generation to the driver's sequence」。计数器归驱动。undeclared-field-write-driver-split.integration.test.tsis stale — a secondpackages/runtimetest consumer (#6468) is outside #5704's "in this one place" ruling #6664 A(维护者 2026-08-08,engine.ts的createWithAutonumberResync注释与scripts/driver-memory-census.ledger.json引用):谁拥有计数器只能有一个答案,SQL 由驱动自己的序列回答。Metadata-protocol: none。A 改的是 spec 里的运行时驱动契约IDataDriver(packages/spec/src/contracts/data-driver.ts),新增一个可选成员,属于Clause-②: yes (widening)。B、C 不动契约。D 改元数据的接受面,是收窄。前提(复核命令)
git grep -n -E "computeStoredExpressions\(|this.applyAutonumbers\(" origin/main -- packages/objectql/src/engine.ts(插入体内computeStoredExpressions那一行在applyAutonumbers之前)git grep -n "autonumber: true," origin/main -- packages/drivers/driver-sql/src/sql-driver.tsgit grep -n "protected async fillAutoNumberFields" origin/main -- packages/drivers/driver-sql/src/sql-driver.tsgit grep -n "const mayRetry = options?.transaction === undefined" origin/main -- packages/drivers/driver-sql/src/sql-driver.tsgit grep -n "transactionsUnsupported: this.transportMode === 'remote'" origin/main -- packages/drivers/driver-turso/src/turso-driver.ts选项
IDataDriver新增一个可选成员,按需为一行发出编号(就是fillAutoNumberFields的公开形式)。引擎在现在applyAutonumbers的位置(校验之后)调用它,算出依赖编号的字段,再把带号的整行交给create。编号冲突重试统一走引擎已有的createWithAutonumberResync。三条路径同一个 PR 修domain:engine· 1 PR(spec 契约 +driver-sql+objectql,sqlite-wasm与turso继承)· M · 契约审查档 · 无实测先例(仓库历史在d29fdcfa97被压平,契约成员的增补读不到具名 PR 与行数)afterInsertupdated_at比created_at晚约 1 msdomain:engine· 1 PR · S–M · 无先例业务含义直译:
四轴(业务立场)
DEFAULT nextval()在 BEFORE INSERT 触发器之前求值,触发器算的列能读到编号。Salesforce 的自动编号在保存时就生成,同一次保存里的字段更新能用上它。undeclared-field-write-driver-split.integration.test.tsis stale — a secondpackages/runtimetest consumer (#6468) is outside #5704's "in this one place" ruling #6664 A 与 ADR-0034 §3。三种驱动只剩一个顺序、一套冲突重试。Blocked-by:等本卡。生产走driver-sql,只有 A 对所有 SQL 形态都成立。只看①选 A;②③④ 是否翻转:否(②的生产路径只有 A 全覆盖,③④ 也不翻)。
推荐:A
RemoteTransport(driver-turso remote: generate auto_number values on the RemoteTransport — the #6944 appetite door now has measured demand (a hosted HotCRM environment cannot create an account: 501) #21113),新成员在远程面上能否直接继承,要在实现时实测。mayRetry也是这个限制,A 不会更差,但也不修它。__search在beforeInsert里按未带号的标题生成,所有在钩子之后重算的路线都有这个问题(dev 的成本 g)。A 的 PR 里要一并确认它是否需要重算。子问题(本席的执行参数,随裁决一并定)
DroppedFieldsEvent.reasonhas no value for "written, but recomputed", soonFieldsDropped/droppedFieldsstay silent #24690、feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778、strictReadonlyWrites)? 维持现在的位置:校验之前、发号之前,按未带号的值判定一次。严格模式的拒绝必须早于烧号,而客户端本来就不可能知道一个还没发出的号,所以不损失任何真实情形。裁后执行
expressionis computed on insert before the autonumber is issued, so an expression that reads the record number stores null (or itshas()fallback) until a later update by id #25073,沿用分支claude/issue-25073-stored-expression-after-autonumber,第一笔提交删掉探针。packages/spec的IDataDriver(向domain:spec席位 [PM seat] domain:spec — ⏳ vacant #6017 做跨车道声明)、driver-sql与objectql。声明为Clause-②: yes (widening)、Metadata-protocol: none,入队前按CONTRACT_REVIEW_TIER审查。@objectstack版本发布后自行恢复。pm:blocked。另记(不立卡): dev 的界外发现。现在就有元数据
afterInsert钩子会中止写入的对象(引擎为它开事务)。这类对象在driver-sql上一旦计数器落后,每次新建都回DUPLICATE_RECORD且不收敛(探针 P4 同理)。缺公开入口的可达性实测和具名生产者,按立卡门暂不立卡,记在此处与席位贴。相关: #24981(裁决 6308445871)、objectstack-ai/hotcrm#2230、#6664、#8283、#21113、ADR-0034、dev 报告 6333405453。
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Ruling: batch #384 item 1 · letter A · maintainer 「同意」 2026-10-22T07:31Z
Director seat, summon #47 continuation 2,
session_01TQvbE2ENCniJr1ffCzbPc6(os-bill, writing through the relay). The maintainer asked for this card by number (「25073 决裁」); it was presented in this session's chat as batch #384 item 1, from thedomain:engineseat 1 decision6333681610and the dev report6333405453, and the maintainer answered 「同意」. The director's delta re-checked onorigin/mainb6e87070: the insert body's secondcomputeStoredExpressionspass (packages/objectql/src/engine.ts:16952) runs beforeapplyAutonumbers(:17059);driver-sqldeclaresautonumber: true(sql-driver.ts:5449) and fills the number in theprotectedfillAutoNumberFields(:8422); inside a unitmayRetryis off (:7700,:9733,:10114); the Turso remote face hastransactionsUnsupported(turso-driver.ts:1886). Two readings the director adds: (1) the Turso remote face already issues the number on the driver, one layer above the transport, by callingfillAutoNumberFieldsfor every write (turso-driver.ts:251-330: "generation happens HERE, on the driver, and not insideRemoteTransport"), so a public issue-on-request member is inherited by that face by construction and needs no remote endpoint — the card's confidence gap narrows, and the implementation still measures it; (2)IDataDriveralready carries thirteen optional members (aggregate?,updateMany?,deleteMany?,explain?,reclaimSpace?, …), so the shape of A has precedent even though the sizes of those additions are not readable from the flattened history. Thread read to its end at this write:6333681610is the newest comment.Governing text:FieldSchema.expression(packages/spec/src/data/field.zod.ts: "computed by the engine from the row the write stores and stored … on every insert and every update by id"); ADR-0034 §3 (the engine defers autonumber generation to the driver's sequence); #6664 A (one owner of the counter); the #24981 ruling6308445871(a composite title that must round-trip through import is a stored computed field).What the ruling binds
IDataDrivergains one optional member that issues a row's autonumbers on request — the public form offillAutoNumberFields. The engine calls it whereapplyAutonumbersruns today, after validation, recomputes the stored fields whose expression reads an issued autonumber, and handscreate/bulkCreatea row whose number is already filled: one INSERT, atomic by construction, no transaction.applyAutonumbersstays after validation, so a rejected row burns no number. The collision retry moves to the engine's existingcreateWithAutonumberResync, re-issuing through the same member: one retry loop for every driver. ⛔ Noinstanceof SqlDriveror driver-name branch.DroppedFieldsEvent.reasonhas no value for "written, but recomputed", soonFieldsDropped/droppedFieldsstay silent #24690, feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778,strictReadonlyWrites) stays where it is, before validation and before any number is issued, judged once on the pre-number value (sub-question 2: A).__searchcompanion is stamped inbeforeInsertfrom the pre-number title (the dev's cost g, shared by every route). The PR measures it; if the stored search text omits the number, it is recomputed in the same write step, because a search column without the number is the same defect in another column.has()fallback shape; controls (an expression that reads no autonumber; an update by id); an ablation (remove the recompute, the autonumber pins go red, the controls stay green). The Turso remote face is measured with the inherited member; a face that does not inherit is a fork to report, not to patch around.Clause-②: yes (widening)on the driver contract,Metadata-protocol: none; packagesspec,driver-sql,objectql(sqlite-wasmandtursoinherit); aCONTRACT_REVIEW_TIERreview before the queue; the contract member is declared across lanes to thedomain:specseat (seat post [PM seat] domain:spec — ⏳ vacant #6017).DUPLICATE_RECORDoutage on a stale counter, not atomic on Turso remote, no unit on a non-default datasource); C (a non-atomic two-statement write on purpose); D (refuses the shape the import: a lookup cell that names the record by the target object's title field answers reference_not_found — resolveRef never reads nameField, and a formula title is not matchable #24981 ruling prescribes).afterInserthook ondriver-sqlnever converges after a stale counter) stays noted on this card, as the seat recorded; it is filed only on a measured public-door reach.Effort of the chosen letter (the card's column):
domain:engine· 1 PR (spec contract +driver-sql+objectql) · M · contract-review tier · no measured precedent (the history is flattened atd29fdcfa97). Confidence gaps carried: the inherited member on the Turso remote face (narrowed by reading, still to be measured); a caller-owned PostgreSQL transaction aborts on a failed INSERT today and A does not change that.State:
needs-user-decision→pm:queue(domain:engine, p1).domain:engineseat 1 (seat post #6367) re-claims and re-dispatches on branchclaude/issue-25073-stored-expression-after-autonumber; the first commit deletes the probepackages/runtime/src/zz-probe-25073.integration.test.ts. objectstack-ai/hotcrm#2230 stays blocked on this card until a release carries the fix.Generated by Claude Code