Skip to content

De-fallback the engine's autonumber render path — read the declared {0000} default instead of parsing the empty string (#6555 half 2/3) #7262

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 engine-core).

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/objectql/src/engine.tsapplyAutonumbers, currently (re-measured on origin/main @ 3566e55; the line drifted from the :2115 triage cited):

// :2553
const fmt = (def as any).autonumberFormat ?? (def as any).format;
// :2554
const tokens = parseAutonumberFormat(typeof fmt === 'string' ? fmt : '');

An undeclared format parses to an empty token list, which renderAutonumber renders through its width === null branch as a bare counter1, 2, …. Replace both lines with the contract:

const tokens = parseAutonumberFormat(resolveAutonumberFormat(def as never));

resolveAutonumberFormat / DEFAULT_AUTONUMBER_FORMAT are exported from @objectstack/spec/data by the blocking PR; engine.ts:24 already imports from that entry.

This one MOVES behaviour — unlike its driver-sql sibling

This is the half the ruling's compatibility note is about. A deployment whose driver does not advertise supports.autonumber (the in-memory driver, any driver without the capability) flips from 1 to 0001 for newly issued numbers on format-less fields. Counter continuity is untouched — #6468 pinned seeding parity, and both sides already agreed on the counter value.

Two consequences worth handling explicitly:

Suggested gates

  • packages/objectql autonumber suites (engine-autonumber-*.test.ts).
  • packages/runtime/src/autonumber-seed-cross-side-parity.integration.test.ts — the cross-side pin; this card is what should make it able to assert a shared rendering, not just a shared counter. Worth extending it with the bug report's exact reproduction: { rec_no: { type: 'autonumber' } }, rows 1 / 2 / 10 already stored, both sides expected to mint 0011.
  • Changeset carrying the compatibility note verbatim.

Not in this card

driver-sql's two || '{0000}' sites — its own card. #6555 stays open until both land.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions