@@ -154,6 +154,57 @@ function sqlColumnExpr(spec: ReturnType<typeof mapColumnType>): string {
154154 return `${ spec . fnName } (${ dbName } )` ;
155155}
156156
157+ /** The raw validator/limit facts shared by both constraint walks. Walks the
158+ * field's validators ONCE, bucketed by subtype, plus the `@maxLength` attr.
159+ * Both `constraintsCell()` (Storage cell) and `buildConstraintRow()`
160+ * (Constraints row) consume these — the SINGLE source of truth for the
161+ * validator emission, so the two presentations can't DRIFT. Each caller
162+ * arranges the parts into its own layout (one cell vs. limits/rules columns)
163+ * but the emission ORDER and exact strings come from here:
164+ * regex pattern → maxLength-from-@maxLength → length-validator (min/max) →
165+ * numeric-validator (min/max). */
166+ interface ValidatorParts {
167+ /** `@maxLength` attr value if a finite number, else undefined. */
168+ maxLenAttr : number | undefined ;
169+ /** "pattern `...`" entries from regex validators. */
170+ regexParts : string [ ] ;
171+ /** "minLength: N" / "maxLength: N" entries from length validators. */
172+ lengthParts : string [ ] ;
173+ /** "min: N" / "max: N" entries from numeric validators. */
174+ numericParts : string [ ] ;
175+ }
176+
177+ function collectValidatorParts ( field : MetaField ) : ValidatorParts {
178+ const maxLenAttr = field . ownAttr ( FIELD_ATTR_MAX_LENGTH ) ;
179+ const regexParts : string [ ] = [ ] ;
180+ const lengthParts : string [ ] = [ ] ;
181+ const numericParts : string [ ] = [ ] ;
182+ for ( const v of field . validators ( ) ) {
183+ if ( v . subType === VALIDATOR_SUBTYPE_REGEX ) {
184+ const pattern = v . ownAttr ( VALIDATOR_ATTR_PATTERN ) ;
185+ if ( typeof pattern === "string" && pattern . length > 0 ) {
186+ regexParts . push ( `pattern \`${ pattern } \`` ) ;
187+ }
188+ } else if ( v . subType === VALIDATOR_SUBTYPE_LENGTH ) {
189+ const min = v . ownAttr ( VALIDATOR_ATTR_MIN ) ;
190+ const max = v . ownAttr ( VALIDATOR_ATTR_MAX ) ;
191+ if ( typeof min === "number" ) lengthParts . push ( `minLength: ${ min } ` ) ;
192+ if ( typeof max === "number" && typeof maxLenAttr !== "number" ) lengthParts . push ( `maxLength: ${ max } ` ) ;
193+ } else if ( v . subType === VALIDATOR_SUBTYPE_NUMERIC ) {
194+ const min = v . ownAttr ( VALIDATOR_ATTR_MIN ) ;
195+ const max = v . ownAttr ( VALIDATOR_ATTR_MAX ) ;
196+ if ( typeof min === "number" ) numericParts . push ( `min: ${ min } ` ) ;
197+ if ( typeof max === "number" ) numericParts . push ( `max: ${ max } ` ) ;
198+ }
199+ }
200+ return {
201+ maxLenAttr : typeof maxLenAttr === "number" ? maxLenAttr : undefined ,
202+ regexParts,
203+ lengthParts,
204+ numericParts,
205+ } ;
206+ }
207+
157208function constraintsCell (
158209 entity : MetaObject ,
159210 field : MetaField ,
@@ -191,34 +242,9 @@ function constraintsCell(
191242 }
192243 }
193244
194- // Walk validators once, bucket by subtype. We re-emit in the original
195- // emission order to preserve byte-identity with the docs-file-basic
196- // conformance fixture: regex pattern → maxLength-from-@maxLength →
197- // length-validator (min/max) → numeric-validator (min/max).
198- const maxLenAttr = field . ownAttr ( FIELD_ATTR_MAX_LENGTH ) ;
199- const regexParts : string [ ] = [ ] ;
200- const lengthParts : string [ ] = [ ] ;
201- const numericParts : string [ ] = [ ] ;
202- for ( const v of field . validators ( ) ) {
203- if ( v . subType === VALIDATOR_SUBTYPE_REGEX ) {
204- const pattern = v . ownAttr ( VALIDATOR_ATTR_PATTERN ) ;
205- if ( typeof pattern === "string" && pattern . length > 0 ) {
206- regexParts . push ( `pattern \`${ pattern } \`` ) ;
207- }
208- } else if ( v . subType === VALIDATOR_SUBTYPE_LENGTH ) {
209- const min = v . ownAttr ( VALIDATOR_ATTR_MIN ) ;
210- const max = v . ownAttr ( VALIDATOR_ATTR_MAX ) ;
211- if ( typeof min === "number" ) lengthParts . push ( `minLength: ${ min } ` ) ;
212- if ( typeof max === "number" && typeof maxLenAttr !== "number" ) lengthParts . push ( `maxLength: ${ max } ` ) ;
213- } else if ( v . subType === VALIDATOR_SUBTYPE_NUMERIC ) {
214- const min = v . ownAttr ( VALIDATOR_ATTR_MIN ) ;
215- const max = v . ownAttr ( VALIDATOR_ATTR_MAX ) ;
216- if ( typeof min === "number" ) numericParts . push ( `min: ${ min } ` ) ;
217- if ( typeof max === "number" ) numericParts . push ( `max: ${ max } ` ) ;
218- }
219- }
245+ const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts ( field ) ;
220246 parts . push ( ...regexParts ) ;
221- if ( typeof maxLenAttr === "number" ) {
247+ if ( maxLenAttr !== undefined ) {
222248 parts . push ( `maxLength: ${ maxLenAttr } ` ) ;
223249 }
224250 parts . push ( ...lengthParts , ...numericParts ) ;
@@ -285,33 +311,11 @@ function buildConstraintRow(
285311 }
286312 }
287313
288- // Same emission order as constraintsCell(): regex pattern →
289- // maxLength-from-@maxLength → length-validator (min/max) →
290- // numeric-validator (min/max).
291- const maxLenAttr = field . ownAttr ( FIELD_ATTR_MAX_LENGTH ) ;
292- const regexParts : string [ ] = [ ] ;
293- const lengthParts : string [ ] = [ ] ;
294- const numericParts : string [ ] = [ ] ;
295- for ( const v of field . validators ( ) ) {
296- if ( v . subType === VALIDATOR_SUBTYPE_REGEX ) {
297- const pattern = v . ownAttr ( VALIDATOR_ATTR_PATTERN ) ;
298- if ( typeof pattern === "string" && pattern . length > 0 ) {
299- regexParts . push ( `pattern \`${ pattern } \`` ) ;
300- }
301- } else if ( v . subType === VALIDATOR_SUBTYPE_LENGTH ) {
302- const min = v . ownAttr ( VALIDATOR_ATTR_MIN ) ;
303- const max = v . ownAttr ( VALIDATOR_ATTR_MAX ) ;
304- if ( typeof min === "number" ) lengthParts . push ( `minLength: ${ min } ` ) ;
305- if ( typeof max === "number" && typeof maxLenAttr !== "number" ) lengthParts . push ( `maxLength: ${ max } ` ) ;
306- } else if ( v . subType === VALIDATOR_SUBTYPE_NUMERIC ) {
307- const min = v . ownAttr ( VALIDATOR_ATTR_MIN ) ;
308- const max = v . ownAttr ( VALIDATOR_ATTR_MAX ) ;
309- if ( typeof min === "number" ) numericParts . push ( `min: ${ min } ` ) ;
310- if ( typeof max === "number" ) numericParts . push ( `max: ${ max } ` ) ;
311- }
312- }
314+ // Same validator facts as constraintsCell() (shared walk), arranged across
315+ // the Limits / Rules columns instead of one cell.
316+ const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts ( field ) ;
313317 rules . push ( ...regexParts ) ;
314- if ( typeof maxLenAttr === "number" ) limits . push ( `maxLength: ${ maxLenAttr } ` ) ;
318+ if ( maxLenAttr !== undefined ) limits . push ( `maxLength: ${ maxLenAttr } ` ) ;
315319 limits . push ( ...lengthParts , ...numericParts ) ;
316320
317321 const fk = fkMap . get ( field . name ) ;
0 commit comments