Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .changeset/autonumber-format-defallback-driver-sql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
"@objectstack/driver-sql": patch
---

refactor(driver-sql): read the autonumber default from the contract instead of a hardcoded fallback (#7263)

Execution half 3/3 of the maintainer's route-3 ruling on #6555. `{0000}` is now a
declared contract default (`DEFAULT_AUTONUMBER_FORMAT`, landed with
`resolveAutonumberFormat` in `@objectstack/spec/data`), so this driver stops
writing the default down for itself.

Two sites in `sql-driver.ts` — `initObjects` and the external-object
registration path — each spelled the same four lines by hand:

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

Both are now `const fmt = resolveAutonumberFormat(field);`. That is the whole
change: one symbol added to an import this file already had, no new dependency,
and the `#1603` comment about honouring both spellings retired to the resolver's
own docstring, which carries it.

**Behaviour-neutral, by construction and by measurement.** `resolveAutonumberFormat`'s
precedence — canonical `autonumberFormat`, then the `format` shorthand, then the
declared default, with anything that is not a **non-empty string** counting as
undeclared — was deliberately taken from these very lines, including their
truthiness rule (not the engine's `??`). A differential check over 484 field
documents, spanning both spellings across 22 value shapes (absent key,
`undefined`, `null`, `''`, non-empty strings, numbers, booleans, `NaN`, arrays,
objects, a boxed `String`, `Symbol`, function, `BigInt`), found the old
expressions and the resolver returning the identical string in every case —
`format: ''`, `autonumberFormat: ''` and the non-string values included, not just
the happy path.

Compatibility note, per the ruling: 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).

The engine half of the same ruling is #7262; #6555 stays open until it lands, so
a format-less field still renders `0001` on SQL and a bare `1` on the engine's
in-memory fallback until then. This half moves neither.
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,12 @@ describe('SqlDriver autonumber seeding — the counter is located by the declare
// lexicographic one — so the counter continues at 11.
//
// The RENDERING of a format-less field is a separate, pre-existing matter
// this fix does not touch: this driver substitutes `{0000}` for a missing
// format (see `initObjects`), so 11 renders `0011` here while the engine's
// fallback emits the bare `11`. That divergence is in the render default,
// not in the seeding parse #6468 is about, so the cross-side parity test
// uses explicitly-formatted fields.
// this fix does not touch: a format-less field resolves to the contract
// default `{0000}` (`resolveAutonumberFormat`, #6555), so 11 renders
// `0011` here — while the engine's fallback still emits the bare `11`
// until #7262 lands the other half. That divergence is in the render
// default, not in the seeding parse #6468 is about, so the cross-side
// parity test uses explicitly-formatted fields.
await initRec();
await seedRows(['1', '2', '10']);

Expand Down
14 changes: 3 additions & 11 deletions packages/drivers/driver-sql/src/sql-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import type { DriverOptions, FilterCondition, SchemaMode } from '@objectstack/spec/data';
import { parseAutonumberFormat, renderAutonumber, readAutonumberCounter, missingFieldValues, isTenancyDisabled, type AutonumberToken } from '@objectstack/spec/data';
import { parseAutonumberFormat, renderAutonumber, resolveAutonumberFormat, readAutonumberCounter, missingFieldValues, isTenancyDisabled, type AutonumberToken } from '@objectstack/spec/data';
// The DECLARED aggregate vocabulary (#5907). Read from the spec so this driver's
// "the protocol has no such function" refusal cannot drift from what
// `AggregationNodeSchema.function` actually admits.
Expand Down Expand Up @@ -5353,10 +5353,7 @@ export class SqlDriver implements IDataDriver {
if (type === 'datetime') datetimeCols.push(name);
if (type === 'time') timeCols.push(name);
if (type === 'auto_number' || type === 'autonumber') {
const rawFmt = (typeof field.autonumberFormat === 'string' && field.autonumberFormat)
? field.autonumberFormat
: (typeof field.format === 'string' && field.format ? field.format : '');
const fmt = rawFmt || '{0000}';
const fmt = resolveAutonumberFormat(field);
autoNumberCols.push({ name, format: fmt, tokens: parseAutonumberFormat(fmt), tenantField });
}
}
Expand Down Expand Up @@ -5433,12 +5430,7 @@ export class SqlDriver implements IDataDriver {
(this.timeFields[tableName] ??= new Set()).add(name);
}
if (type === 'auto_number' || type === 'autonumber') {
// Honor either the spec-canonical `autonumberFormat` or the
// shorthand `format` (both appear in metadata) — see #1603.
const rawFmt = (typeof field.autonumberFormat === 'string' && field.autonumberFormat)
? field.autonumberFormat
: (typeof field.format === 'string' && field.format ? field.format : '');
const fmt = rawFmt || '{0000}';
const fmt = resolveAutonumberFormat(field);
// Tokenize once: the renderer resolves date tokens (`{YYYYMMDD}`),
// field interpolation (`{island_zone}`) and the sequence slot at
// fill time. The counter scopes to whatever renders before the slot.
Expand Down
Loading