Skip to content

De-fallback driver-sql's autonumber format — drop the two hardcoded || '{0000}' sites for the declared default (#6555 half 3/3) #7263

Description

@os-zhuang

Blocked-by: #6555's spec-half PR (branch claude/issue-6555-autonumber-default-format) — the declared default must exist before this reads it.

Execution half of the maintainer's route-3 ruling on #6555. Filed unassigned and unrouted; triage routes it (the ruling says drivers).

The ruling (verbatim, comment 5225535766)

Maintainer ruling (2026-08-08): Route 3, with the default fixed at {0000}. The default format for autonumber fields becomes a declared contract default in FieldSchema (spec side), and both hand-written fallbacks go away: driver-sql drops its hardcoded fallback and the engine fallback path stops emitting bare integers — both sides read the declared default.

Rationale (three-axis review): routes 1 and 2 both fix the fork but leave "the default" living in two hand-written fallbacks — and route 2 would break number-shape continuity for already-stored SQL data, the largest install base (business). Route 3 is the only one that puts the default itself into the contract: declared = enforced (long-term), and an AI metadata author sees the default in the schema instead of guessing per-driver behavior (containment).

Compatibility note (must be in the changeset): choosing {0000} keeps stored driver-sql data undisturbed; engine-fallback deployments flip from bare 1 to 0001 for newly issued numbers. Counter continuity itself is unaffected (#6468 pinned it).

For the dispatch: contract-first ordering — the spec default lands first (spec lane), the two de-fallback changes follow (engine-core / drivers as routed by triage).

The change

packages/drivers/driver-sql/src/sql-driver.ts — two sites, re-measured on origin/main @ 3566e55 (they drifted again from the :4330/:4412 triage cited, and from the :4322/:4404 in #6555's body):

  • :5350-5353 — initObjects
  • :5432-5435 — the external-object registration path

Both spell the same six lines:

const rawFmt = (typeof field.autonumberFormat === 'string' && field.autonumberFormat)
  ? field.autonumberFormat
  : (typeof field.format === 'string' && field.format ? field.format : '');
const fmt = rawFmt || '{0000}';

Replace each with the contract:

const fmt = resolveAutonumberFormat(field);

resolveAutonumberFormat is exported from @objectstack/spec/data by the blocking PR, and was written to reproduce exactly this precedence — canonical autonumberFormat, then the format shorthand (#1603), then DEFAULT_AUTONUMBER_FORMAT for anything that is not a non-empty string.

Behaviour-neutral by construction — that is the point of this card

Unlike its engine sibling, this half moves nothing: resolveAutonumberFormat's truthiness rule was deliberately taken from these very lines so already-stored SQL record numbers keep their shape, per the ruling's compatibility note. It should land as a pure de-duplication: the default stops being written down in the driver and is read from the contract instead.

That makes the review question a narrow one — prove the substitution is exact, including the format: '' and non-string cases, not just the happy path.

Suggested gates

  • All eight packages/drivers/driver-sql/src/sql-driver-autonumber-*.test.ts suites — every one must stay green unchanged. A test that needs editing here is a signal the substitution is not exact.
  • In particular the legacy case in sql-driver-autonumber-suffix.test.ts, which pins '0011' and names this divergence in its comment (PR fix(objectql,driver-sql): 播种解析按声明的 suffix 定位计数器,两侧收敛 (#6468) #6553) — with this card landed, that comment can point at the declared default instead of at a local fallback.
  • packages/runtime/src/autonumber-seed-cross-side-parity.integration.test.ts.
  • Changeset carrying the compatibility note verbatim.

Not in this card

The engine's applyAutonumbers path — its own card. #6555 stays open until both land.

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Triage: domain:drivers + pm:blocked.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop, drivers lane. Unblocked — pm:blocked → pm:queue + pm:dispatched.

    Session: session_01Hg9Pkg5nDedCRihRsdeCdX
    Branch: claude/issue-7263-autonumber-format-defallback
    Worktree: objectstack-issue-7263 (cloud session — own container, Opus)

    Unlock verified, not assumed. Triage's instruction here was "On unlock, re-verify both sites at the merged ref before dispatch" — done:

    "Behaviour-neutral by construction" is verified, not taken on faith. The resolver's precedence (canonical autonumberFormat → format shorthand → DEFAULT_AUTONUMBER_FORMAT) and its non-empty-string test are character-for-character what the two driver sites do, and its docstring says so deliberately: "A key holding anything other than a NON-EMPTY string counts as undeclared — which is deliberately the SQL driver's long-standing truthiness rule, not the engine's ??." That covers the format: '' and non-string cases the card asks to prove, not just the happy path.

    One nuance the card raises, bounded explicitly. sql-driver-autonumber-suffix.test.ts pins '0011' and its comment (:167-172) explains it as "this driver substitutes {0000} for a missing format (see initObjects)". After this card that sentence is stale and may be re-pointed at the declared default — but ⚠️ only the prose. The expect(r.rec_no).toBe('0011') assertion, its inputs, and every other case must not move. And note the second half of that comment — the engine emitting bare 11 — stays true until #7262 lands, so do not delete it as though this card fixed the divergence.

    ⛔ The engine half #7262 is engine-core's. Do not touch applyAutonumbers. #6555 stays open until both land.


    Generated by Claude Code

  4. claude commented on Aug 10, 2026

    @claude
    Contributor

    os-dev report

    Landed as draft PR #7334 (branch claude/issue-7263-autonumber-format-defallback, commit 088a1b8, base 06be54ec3). The card's premise held: the substitution is exact, and I found no input where old and new disagree in a reachable way.

    What changed and where

    {0000} is read from the contract now instead of being written down in the driver. Located by content (git grep -n "rawFmt"), never by line number.

    Site Final line
    import — resolveAutonumberFormat added to the existing @objectstack/spec/data import sql-driver.ts:11
    initObjects sql-driver.ts:5356
    external-object registration path sql-driver.ts:5433

    Both are now const fmt = resolveAutonumberFormat(field);. No dependency added — @objectstack/spec was already in dependencies and the subpath already imported. Site 2's #1603 comment was removed as redundant; the resolver's docstring carries it.

    git grep "rawFmt\|'{0000}'" packages/drivers/driver-sql/src/sql-driver.ts now returns nothing — no residual fallback.

    Exactness proof

    A differential harness ran the removed expressions and resolveAutonumberFormat over 484 field documents — both spellings across 22 value shapes: absent key, undefined, null, '', non-empty strings, '0', whitespace, numbers, NaN, booleans, arrays, objects, boxed String, Symbol, function, BigInt, null-prototype object.

    484/484 returned the identical string. DEFAULT_AUTONUMBER_FORMAT === '{0000}' exactly. The edge cases the card named, measured rather than assumed:

    Input old new
    format: '' '{0000}' '{0000}'
    autonumberFormat: '' '{0000}' '{0000}'
    both '' '{0000}' '{0000}'
    autonumberFormat: '' + format: 'SO-{000}' 'SO-{000}' 'SO-{000}'
    autonumberFormat: 42 (non-string) '{0000}' '{0000}'
    format: 42 (non-string) '{0000}' '{0000}'
    no keys at all '{0000}' '{0000}'

    One shape does differ, and it is unreachable. A null/undefined field threw TypeError under the old expression and returns the default under resolveAutonumberFormat (which guards with field?.). It cannot be reached: both sites evaluate const type = field.type || 'string' several branches earlier, which throws on a null field before the autonumber branch is entered. Not a behaviour change any caller can observe — flagging it only because "no input where they disagree" would otherwise be an overstatement.

    (Theoretical footnote: the old expression read each key up to three times, the resolver reads it once, so a getter returning different values per read could diverge. Metadata documents are parsed JSON; no getters exist on this path.)

    Test results

    Gate Result
    Eight autonumber suites — baseline, pre-edit 8 files / 64 tests passed
    Eight autonumber suites — post-edit 8 files / 64 tests passed
    autonumber-seed-cross-side-parity.integration.test.ts — pre & post 1 file / 5 tests passed
    Full driver-sql package suite 81 passed / 4 skipped (85 files), 1156 passed / 48 skipped (1204 tests)
    typecheck (@objectstack/driver-sql) pass
    eslint touched files / full repo (eslint . --no-inline-config) pass / pass (exit 0)
    check:empty-changeset / check:adr-0087-registration / check-changeset-no-major pass / pass / pass

    The eight suites, confirmed present (see "card corrections" below on the glob): batch-resync, persistence, resync, suffix, tokens, tx, autonumber, upsert-autonumber-immutable.

    No test weakened

    $ git diff --stat HEAD~1 -- '*.test.ts'
     .../driver-sql/src/sql-driver-autonumber-suffix.test.ts | 11 ++++++-----
     1 file changed, 6 insertions(+), 5 deletions(-)
    

    One test file, the bounded prose exception only. Verified mechanically that every changed line in it is a comment — git diff -U0 | grep '^[+-]' | grep -v '^[+-]\s*//' returns empty. expect(r.rec_no).toBe('0011'), its inputs, and every other case are byte-identical. Nothing deleted, loosened, retried or skipped anywhere.

    The comment at :167-173 now points at the declared default, and keeps its second half true as instructed — it still says the engine emits the bare 11, now explicitly "until #7262 lands the other half" rather than reading as though this card fixed the divergence.

    Reverse verification

    Predictions stated before measuring; three faults injected at both call sites.

    Fault Predicted Measured
    A — default dropped (undeclared → '') 1 red: suffix, '11' vs '0011' ✅ exactly that. 1 failed / 63 passed; AssertionError: expected '11' to be '0011'
    B — format shorthand branch lost broad red, ~7 of 8 files ⚠️ 8 of 8 files, 45 failed / 19 passed
    C — truthiness rule → bare typeof (the engine's ??) no red — no suite declares format: '' ✅ 8 files / 64 tests still green

    Fault B came out redder than predicted, and the fixture was at fault, not the code. My survey grep (grep -c "[^r]format:") reported 0 occurrences in suffix.test.ts, so I predicted it would survive. It misses the ES6 shorthand — the helper writes { type: 'autonumber', format }, not format:. All eight suites exercise the shorthand spelling. Corrected before drawing any conclusion from it.

    Fault C is the load-bearing result. The subtlest wrong substitution — differing from the driver's rule only on the empty string — leaves all 64 tests green. The suites cannot see that case. That is why the exactness claim rests on the 484-document harness and not on the suite, and it is the direct answer to the card's "prove it, don't assert it".

    CI — per job

    All green on 088a1b8: 23 success, 2 skipped, 0 failed.

    ADR maintainer approval · Auto Label · Build Core · Check Changeset · Check Documentation Links · Check PR Size · Console Pin Freshness · Dogfood Regression Gate (+ shards 1/3, 2/3, 3/3) · Dogfood Verify CLI · ESLint · Flag docs affected by code changes · No other open PR may claim the same issue · Temporal Conformance (live PG + MySQL) · Test Core (+ shards 1/3, 2/3, 3/3) · TypeScript Type Check · Vercel Preview Comments · filter — all success. Skipped: Build Docs, Console Pin Gate (no docs/console changes in the diff).

    Changeset

    .changeset/autonumber-format-defallback-driver-sql.md, "@objectstack/driver-sql": patch, carrying the ruling's compatibility note verbatim:

    choosing {0000} keeps stored driver-sql data undisturbed; engine-fallback deployments flip from bare 1 to 0001 for newly issued numbers. Counter continuity itself is unaffected (#6468 pinned it).

    It also records the 484-document differential result and notes that #6555's divergence persists until #7262 lands.

    What the card got wrong / worth knowing

    1. The glob in the acceptance bar does not match the eight files it names. packages/drivers/driver-sql/src/sql-driver-autonumber-*.test.ts matches only six; sql-driver-autonumber.test.ts and sql-driver-upsert-autonumber-immutable.test.ts need *autonumber*.test.ts. The eight names are all correct and all exist — a copy-paste of the glob alone would have silently run six suites and reported the bar met.
    2. The anchors did not drift a fifth time. The card said to assume they had moved again; at 06be54ec3 they were still exactly where the PM measured them (:5359 / :5441 for the const fmt line; the four-line blocks start at :5356 / :5438). Located by content anyway — the instruction cost nothing and remains the right one.
    3. "Four lines" vs the issue body's "six lines". Both are right about different sites: site 1 is four lines, site 2 is those four plus a two-line #1603 comment. Not a defect, just worth not tripping over.
    4. Coverage finding for the queue, not for this card. Fault A produced exactly one red — a single assertion in all eight suites covers the format-less default path — and no suite declares format: '' at all. The de-fallback is safe because the resolver is character-identical, not because the suite would have caught a regression. When De-fallback the engine's autonumber render path — read the declared {0000} default instead of parsing the empty string (#6555 half 2/3) #7262 changes the engine side, that thinness is worth knowing: the engine half is the one that actually moves behaviour, and this suite would not see an empty-string mistake there either.

    ⛔ Out of scope respected: applyAutonumbers and packages/objectql untouched; content/docs/releases/ untouched; issue not re-claimed or re-assigned.


    Generated by Claude Code

  5. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    PM step-7 gate: ACCEPTED. PR #7334 @ 088a1b863 — marking ready and enabling auto-merge.

    Verified independently off the branch and the code, not off the report:

    Claim Result
    No residual fallback ✅ git grep "rawFmt|'{0000}'" on sql-driver.ts @ 088a1b863 returns nothing
    Both sites substituted ✅ :5356 and :5433, each const fmt = resolveAutonumberFormat(field);
    Zero new dependencies ✅ symbol added to the @objectstack/spec/data import already on :11
    Only one test file touched, comment-only ✅ every changed line in suffix.test.ts is a comment; expect(r.rec_no).toBe('0011'), its inputs and every other case byte-identical
    Comment kept its still-true half ✅ retains "the engine's fallback still emits the bare 11", now scoped "until #7262 lands the other half"
    Changeset carries the note verbatim ✅ patch → @objectstack/driver-sql
    CI on this head ✅ 25 checks completed — 23 success, 2 skipped (Build Docs, Console Pin Gate, both correctly paths-filtered for a driver-sql/src + .changeset/ diff). No failure, no in_progress. Temporal Conformance (live PG + MySQL) green, so this ran against live Postgres and MySQL, not just SQLite

    The exactness claim I re-derived rather than accepted. Before reading this report I built my own differential matrix from the authoritative sources — 484 cases, 22 value shapes across both spellings — and got 0 mismatches, with a positive control that fires 22 mismatches on a deliberately broken variant so the harness is demonstrably live. Same number, same verdict, two independent harnesses. I had also separately concluded the field?. divergence is unreachable, by the same route: both loops evaluate const type = field.type || 'string' several branches earlier. The getter footnote is a fair catch and correctly dismissed — these are parsed JSON documents.

    The glob defect is mine and the correction is right. I verified it: sql-driver-autonumber-*.test.ts matches 6 of the 8 files I named — it misses sql-driver-autonumber.test.ts (no hyphen after autonumber) and sql-driver-upsert-autonumber-immutable.test.ts (different prefix). *autonumber*.test.ts matches all eight. A dev that ran my glob and reported "all eight green" would have run six and been wrong through no fault of its own. That flaw is in this card's Suggested gates section too, which this seat wrote — I'm correcting it here rather than leaving it to bite the next reader. Fourth time a dev has falsified one of my dispatch facts and been right.

    Fault C is the result that earns this accept, and it deserves to be read carefully. The subtlest wrong substitution — one differing from the driver's rule only on the empty string — leaves all 64 tests green. I confirmed the mechanism: git grep "format: ''\|autonumberFormat: ''" across every driver-sql test file returns nothing. No suite declares an empty-string format, so no suite can see that case. This is exactly why the card asked for proof rather than assertion, and it means the accept rests on the 484-document harness, not on the green suite. A reviewer who had only seen "64/64 green" would have had no evidence at all for the property that actually matters.

    Credit where the discipline showed: fault B came out redder than predicted and the report says the fixture was wrong, not the code — the survey grep grep -c "[^r]format:" missed the ES6 shorthand { type: 'autonumber', format }. That is the dispatch's "if a predicted red doesn't appear, suspect your fixture" rule applied in the mirror direction, caught and corrected before any conclusion was drawn from it. Reverse verification that only ever confirms predictions isn't doing any work.

    Two notes carried forward rather than acted on here:

    ⛔ applyAutonumbers / packages/objectql untouched, content/docs/releases/ untouched, no re-claim. #6555 stays open until #7262 lands.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions