@@ -21,7 +21,7 @@ import type { WriteObservabilityOptions } from '@objectstack/spec/contracts';
2121// engine is what `metadata-protocol.validateData` returns, so letting the two
2222// drift would put a translation layer between a verdict and its contract.
2323import type { ValidateDataIssue , ValidateDataResponse } from '@objectstack/spec/api' ;
24- import { parseAutonumberFormat , renderAutonumber , missingFieldValues , isTenancyDisabled , FILE_REFERENCE_TYPES , REFERENCE_VALUE_TYPES , referenceTargetOf , isFileIdToken , RAW_FILE_VALUES_CONTEXT_KEY , isCurrentUserDefaultToken , isNowDefaultToken } from '@objectstack/spec/data' ;
24+ import { parseAutonumberFormat , renderAutonumber , readAutonumberCounter , missingFieldValues , isTenancyDisabled , FILE_REFERENCE_TYPES , REFERENCE_VALUE_TYPES , referenceTargetOf , isFileIdToken , RAW_FILE_VALUES_CONTEXT_KEY , isCurrentUserDefaultToken , isNowDefaultToken } from '@objectstack/spec/data' ;
2525// [#5158] Door 2's lowering sink — the SAME pair the protocol face (Door 1)
2626// runs, so `FilterArray` has exactly one lowering in the product.
2727import { isFilterAST , parseFilterAST , VALID_AST_OPERATORS } from '@objectstack/spec/data' ;
@@ -352,39 +352,30 @@ interface IssuedAutonumber {
352352 * (#6806) so both readings can never drift apart — a divergence here is a
353353 * duplicate record number, which is the harm the whole family is about.
354354 *
355- * `prefix` and `suffix` are `renderAutonumber`'s own declared output; this
356- * function derives no format understanding of its own (see `seedAutonumber`'s
357- * "Locating the counter inside a stored value" section for the full rationale):
355+ * The ANCHORED reading itself is NOT this package's (#6560): it is the inverse
356+ * of `renderAutonumber`'s composition, so it lives beside it as spec's
357+ * {@link readAutonumberCounter}, which the SQL driver's `scanMaxNumericTail`
358+ * calls over the same two strings. This function adds only the piece the two
359+ * sides genuinely do not share:
358360 *
359- * - **Either one declared ⇒ the slot is ANCHORED**: the counter is the digit
360- * run at the START of what follows the prefix, after removing the declared
361- * suffix when this row carries it. The suffix is stripped when it matches,
362- * never required to match — a dynamic suffix renders differently per row
363- * while the counter scope is the rendered PREFIX, so those rows share this
364- * counter and must still be read.
365- * - **Neither declared ⇒ UNANCHORED**: the legacy reading — the LAST digit
366- * run of the whole value.
361+ * - **Either `prefix` or `suffix` declared ⇒ ANCHORED**: spec answers, and its
362+ * TSDoc carries the rationale (counter after the prefix, suffix stripped
363+ * when it matches and never required to match, out-of-scope values read as
364+ * `undefined`).
365+ * - **Neither declared ⇒ UNANCHORED**: this engine's own legacy reading — the
366+ * LAST digit run of the whole value — kept byte-for-byte by PR #6553. The
367+ * SQL driver's legacy reading of the same case is deliberately DIFFERENT (it
368+ * concatenates every digit), which is exactly why spec refuses to answer for
369+ * an unanchored slot instead of picking one of the two.
367370 *
368- * A value outside the scope (it does not carry the rendered prefix) reads as
369- * `undefined`: it belongs to another counter and must not lift this one.
370- *
371- * Both branches use linear `/\d+/` forms — a backtracking lookahead here is a
372- * polynomial-ReDoS sink on stored values full of zeros (CodeQL
371+ * The unanchored branch uses the linear `/\d+/g` — a backtracking lookahead here
372+ * is a polynomial-ReDoS sink on stored values full of zeros (CodeQL
373373 * js/polynomial-redos).
374374 */
375- function readAutonumberCounter ( value : string , prefix : string , suffix : string ) : number | undefined {
376- if ( prefix && ! value . startsWith ( prefix ) ) return undefined ;
377- const anchored = prefix !== '' || suffix !== '' ;
378- let digits : string | undefined ;
379- if ( anchored ) {
380- let core = value . slice ( prefix . length ) ;
381- if ( suffix && core . endsWith ( suffix ) ) core = core . slice ( 0 , core . length - suffix . length ) ;
382- const head = core . match ( / ^ \d + / ) ;
383- digits = head ? head [ 0 ] : undefined ;
384- } else {
385- const runs = value . match ( / \d + / g) ;
386- digits = runs ? runs [ runs . length - 1 ] : undefined ;
387- }
375+ function readStoredAutonumberCounter ( value : string , prefix : string , suffix : string ) : number | undefined {
376+ if ( prefix !== '' || suffix !== '' ) return readAutonumberCounter ( value , prefix , suffix ) ;
377+ const runs = value . match ( / \d + / g) ;
378+ const digits = runs ? runs [ runs . length - 1 ] : undefined ;
388379 if ( ! digits ) return undefined ;
389380 const n = parseInt ( digits , 10 ) ;
390381 return Number . isFinite ( n ) ? n : undefined ;
@@ -2698,9 +2689,9 @@ export class ObjectQL implements IObjectQLEngine {
26982689 * (#6806) — the free half of the resync, and the one that closes the shape
26992690 * #5495's PROBE1 measured on a warm database.
27002691 *
2701- * The value is parsed with {@link readAutonumberCounter }, i.e. by exactly the
2702- * anchoring rules #6468 gave the seeding scan, against the prefix/suffix this
2703- * record's own format renders. So adopting is the same reading a cold re-seed
2692+ * The value is parsed with {@link readStoredAutonumberCounter }, i.e. by
2693+ * exactly the anchoring rules #6468 gave the seeding scan, against the
2694+ * prefix/suffix this record's own format renders. So adopting is the same reading a cold re-seed
27042695 * would perform over the same row — which is the invariant to hold on to: a
27052696 * warm counter must answer what a restart would answer.
27062697 *
@@ -2717,8 +2708,8 @@ export class ObjectQL implements IObjectQLEngine {
27172708 * persisted, so the first generating insert's own scan reads it.
27182709 * - **Never lowers.** A counter that has already issued numbers must not go
27192710 * back over them; the max is a floor that only rises.
2720- * - **Only within this record's scope.** `readAutonumberCounter` returns
2721- * `undefined` for a value that does not carry the rendered prefix, so a
2711+ * - **Only within this record's scope.** The reading returns `undefined`
2712+ * for a value that does not carry the rendered prefix, so a
27222713 * historical import into last month's date scope cannot lift THIS
27232714 * month's counter. Its own scope's counter is left untouched, which is
27242715 * harmless: a scope is derived from the write instant, so a past scope's
@@ -2753,7 +2744,7 @@ export class ObjectQL implements IObjectQLEngine {
27532744 const counterKey = `${ object } .${ field } .${ probe . scope } ` ;
27542745 const seeded = this . autonumberCounters . get ( counterKey ) ;
27552746 if ( seeded == null ) return ; // not seeded yet — the first seed scan will read this row
2756- const supplied = readAutonumberCounter ( value , probe . prefix , probe . suffix ) ;
2747+ const supplied = readStoredAutonumberCounter ( value , probe . prefix , probe . suffix ) ;
27572748 if ( supplied == null || supplied <= seeded ) return ;
27582749 this . autonumberCounters . set ( counterKey , supplied ) ;
27592750 this . logger . debug ( 'Autonumber counter lifted to an externally supplied value' , {
@@ -2933,15 +2924,19 @@ export class ObjectQL implements IObjectQLEngine {
29332924 *
29342925 * - **Either one declared ⇒ the slot is ANCHORED**: the counter is the digit
29352926 * run at the START of what follows the prefix, after removing the declared
2936- * suffix when this row carries it.
2927+ * suffix when this row carries it. That half is spec's
2928+ * `readAutonumberCounter` — the declared inverse of `renderAutonumber`,
2929+ * which the SQL driver's scan calls too, so one edit moves both sides
2930+ * (#6560).
29372931 * - **Neither declared ⇒ UNANCHORED**: the legacy reading is kept — the LAST
29382932 * digit run of the whole value. A format with no `{0..0}` slot renders a
29392933 * bare trailing counter, and values predating any format have no anchor to
2940- * read from, so this stays exactly as it was.
2934+ * read from, so this stays exactly as it was. The SQL driver's legacy
2935+ * reading of this case differs on purpose, which is why it stays per-side.
29412936 *
2942- * That reading is {@link readAutonumberCounter }, module-level rather than
2943- * inline, because #6806's resync must read an exempt writer's supplied value
2944- * by exactly these rules.
2937+ * The two together are {@link readStoredAutonumberCounter }, module-level
2938+ * rather than inline, because #6806's resync must read an exempt writer's
2939+ * supplied value by exactly these rules.
29452940 *
29462941 * The suffix is *stripped when it matches*, never *required* to match: a
29472942 * dynamic suffix renders differently per row (`{000}-{YYYY}` is `-2025` on
@@ -3017,12 +3012,14 @@ export class ObjectQL implements IObjectQLEngine {
30173012 for ( const r of page ) {
30183013 const v = r ?. [ field ] ;
30193014 if ( v == null ) continue ;
3020- // The reading itself lives in `readAutonumberCounter` (the section
3021- // above describes it) because #6806's adopt-on-exempt-write resync
3022- // must read a supplied value by the SAME rules this scan reads a
3023- // stored one — two copies of it would drift into two different
3024- // answers for one row, which is a duplicate record number.
3025- const counter = readAutonumberCounter ( String ( v ) , prefix , suffix ) ;
3015+ // The reading itself lives in `readStoredAutonumberCounter` (the
3016+ // section above describes it) because #6806's adopt-on-exempt-write
3017+ // resync must read a supplied value by the SAME rules this scan reads
3018+ // a stored one — two copies of it would drift into two different
3019+ // answers for one row, which is a duplicate record number. Its
3020+ // anchored half is spec's `readAutonumberCounter`, the one the SQL
3021+ // driver's own scan calls (#6560).
3022+ const counter = readStoredAutonumberCounter ( String ( v ) , prefix , suffix ) ;
30263023 if ( counter != null ) max = Math . max ( max , counter ) ;
30273024 }
30283025 }
0 commit comments