diff --git a/.changeset/autonumber-format-defallback-driver-sql.md b/.changeset/autonumber-format-defallback-driver-sql.md new file mode 100644 index 0000000000..1f23613771 --- /dev/null +++ b/.changeset/autonumber-format-defallback-driver-sql.md @@ -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. diff --git a/packages/drivers/driver-sql/src/sql-driver-autonumber-suffix.test.ts b/packages/drivers/driver-sql/src/sql-driver-autonumber-suffix.test.ts index f14e440a9a..b859a8f547 100644 --- a/packages/drivers/driver-sql/src/sql-driver-autonumber-suffix.test.ts +++ b/packages/drivers/driver-sql/src/sql-driver-autonumber-suffix.test.ts @@ -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']); diff --git a/packages/drivers/driver-sql/src/sql-driver.ts b/packages/drivers/driver-sql/src/sql-driver.ts index a72ef68f07..543f3cc91a 100644 --- a/packages/drivers/driver-sql/src/sql-driver.ts +++ b/packages/drivers/driver-sql/src/sql-driver.ts @@ -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. @@ -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 }); } } @@ -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.