@drzl/validation-core 3.22.2 → 3.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -55,6 +55,7 @@ __export(index_exports, {
55
55
  canonicalMembers: () => canonicalMembers,
56
56
  canonicalNumericText: () => canonicalNumericText,
57
57
  classifyTableChecks: () => classifyTableChecks,
58
+ codePointCompare: () => codePointCompare,
58
59
  columnMetaFacts: () => columnMetaFacts,
59
60
  comparisonWire: () => comparisonWire,
60
61
  describeSet: () => describeSet,
@@ -67,6 +68,7 @@ __export(index_exports, {
67
68
  isProjectInstallPath: () => isProjectInstallPath,
68
69
  lengthCheckLabel: () => lengthCheckLabel,
69
70
  lengthMeasure: () => lengthMeasure,
71
+ measureCompare: () => measureCompare,
70
72
  measureExpression: () => measureExpression,
71
73
  moduleFileName: () => moduleFileName,
72
74
  moduleSpecifier: () => moduleSpecifier,
@@ -112,6 +114,34 @@ function lengthMeasure(column, check) {
112
114
  if (column.tsType !== "string") return void 0;
113
115
  return check.unit === "bytes" ? "utf8Bytes" : "codePoints";
114
116
  }
117
+ function codePointCompare(variable, operator, bound) {
118
+ const units = `${variable}.length`;
119
+ const spread2 = `[...${variable}].length`;
120
+ switch (operator) {
121
+ case "<=":
122
+ case "<":
123
+ return `(${units} ${operator} ${bound} || ${spread2} ${operator} ${bound})`;
124
+ case ">=":
125
+ case ">":
126
+ return `(${units} ${operator} ${bound} && ${spread2} ${operator} ${bound})`;
127
+ case "===":
128
+ return `(${units} >= ${bound} && ${spread2} === ${bound})`;
129
+ case "!==":
130
+ return `(${units} < ${bound} || ${spread2} !== ${bound})`;
131
+ }
132
+ }
133
+ function measureCompare(measure, variable, operator, bound) {
134
+ const js = {
135
+ ">=": ">=",
136
+ ">": ">",
137
+ "<=": "<=",
138
+ "<": "<",
139
+ "=": "===",
140
+ "<>": "!=="
141
+ }[operator];
142
+ if (measure === "codePoints") return codePointCompare(variable, js, bound);
143
+ return `${measureExpression(measure, variable)} ${js} ${bound}`;
144
+ }
115
145
  function measureExpression(measure, variable) {
116
146
  switch (measure) {
117
147
  case "codePoints":
@@ -123,7 +153,7 @@ function measureExpression(measure, variable) {
123
153
  }
124
154
  }
125
155
  function lengthCheckLabel(check) {
126
- const fn = check.unit === "bytes" ? "octet_length" : "length";
156
+ const fn = check.fn ? check.fn === "char_length" ? "length" : check.fn : check.unit === "bytes" ? "octet_length" : "length";
127
157
  const rule = `${fn}(${check.column}) ${check.operator} ${check.value}`;
128
158
  return check.name ? `${check.name}: ${rule}` : rule;
129
159
  }
@@ -374,7 +404,13 @@ function foldDisjunctionToSet(branches, name) {
374
404
  sets: [{ column, values, kind, name }]
375
405
  };
376
406
  }
377
- function parseCheck(expression, name) {
407
+ function lengthUnit(fn, dialect) {
408
+ const name = fn.toLowerCase();
409
+ if (name === "octet_length") return "bytes";
410
+ if (name === "char_length") return "characters";
411
+ return dialect === "mysql" ? "bytes" : "characters";
412
+ }
413
+ function parseCheck(expression, name, dialect) {
378
414
  const expr = unwrap((expression ?? "").trim());
379
415
  if (!expr) return { ok: false, reason: "empty expression" };
380
416
  if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
@@ -451,11 +487,12 @@ function parseCheck(expression, name) {
451
487
  const lengthOf = expr.match(LENGTH_OF);
452
488
  if (lengthOf) {
453
489
  const op2 = lengthOf[3] === "!=" ? "<>" : lengthOf[3];
454
- const unit = lengthOf[1].toLowerCase() === "octet_length" ? "bytes" : "characters";
490
+ const fn = lengthOf[1].toLowerCase();
491
+ const unit = lengthUnit(fn, dialect);
455
492
  return {
456
493
  ok: true,
457
494
  checks: [],
458
- lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, name }]
495
+ lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, fn, name }]
459
496
  };
460
497
  }
461
498
  const cardinalityOf = expr.match(CARDINALITY_OF);
@@ -962,7 +999,7 @@ function classifyTableChecks(table) {
962
999
  const out = [];
963
1000
  for (const k of table.checks ?? []) {
964
1001
  const expression = (k.expression ?? "").trim();
965
- const parsed = parseCheck(k.expression, k.name);
1002
+ const parsed = parseCheck(k.expression, k.name, table.dialect);
966
1003
  if (!parsed.ok) {
967
1004
  out.push({
968
1005
  ...k.name ? { name: k.name } : {},
@@ -1944,6 +1981,7 @@ async function formatCode(code, filePath, fmt) {
1944
1981
  canonicalMembers,
1945
1982
  canonicalNumericText,
1946
1983
  classifyTableChecks,
1984
+ codePointCompare,
1947
1985
  columnMetaFacts,
1948
1986
  comparisonWire,
1949
1987
  describeSet,
@@ -1956,6 +1994,7 @@ async function formatCode(code, filePath, fmt) {
1956
1994
  isProjectInstallPath,
1957
1995
  lengthCheckLabel,
1958
1996
  lengthMeasure,
1997
+ measureCompare,
1959
1998
  measureExpression,
1960
1999
  moduleFileName,
1961
2000
  moduleSpecifier,
package/dist/index.d.cts CHANGED
@@ -452,6 +452,15 @@ interface LengthCheck {
452
452
  * a given column.
453
453
  */
454
454
  unit?: 'characters' | 'bytes';
455
+ /**
456
+ * The function as written, before the engine decided what it counts.
457
+ *
458
+ * `unit` used to imply this, because the two were the same question: `octet_length` meant bytes
459
+ * and everything else meant characters. On MySQL `length()` also means bytes, so deriving the
460
+ * name back from the unit relabelled a user's `length(name) <= 5` as `octet_length(name) <= 5`,
461
+ * which is a constraint they did not write. The label the ledger matches on has to be theirs.
462
+ */
463
+ fn?: 'length' | 'char_length' | 'octet_length';
455
464
  name?: string;
456
465
  }
457
466
  /**
@@ -504,6 +513,35 @@ declare function lengthMeasure(column: {
504
513
  * TypeBox reach it off the row object as `o["blob"]`. `CODEPOINT_LENGTH` is the fixed-variable
505
514
  * spelling of the first arm and stays for the call sites that already use it.
506
515
  */
516
+ /**
517
+ * A code-point count compared against a bound, without spreading the string when it cannot matter.
518
+ *
519
+ * `[...v].length` allocates an array of code points, and it sits on the path every ordinary row
520
+ * takes. A UTF-16 unit count is free and is never *smaller* than a code-point count, since only a
521
+ * surrogate pair costs two units for one code point. That one fact settles every operator here:
522
+ *
523
+ * `<=`, `<` units within the bound means code points are too short-circuit to ACCEPT
524
+ * `>=`, `>` units under the bound means code points are too short-circuit to REJECT
525
+ * `===` units under the bound means code points cannot equal it short-circuit to REJECT
526
+ * `!==` units under the bound means code points cannot equal it short-circuit to ACCEPT
527
+ *
528
+ * So the spread runs only for a string long enough in units to be in doubt, which for a cap is the
529
+ * only string that was ever in doubt. Measured over a 20000-string random pool plus the awkward
530
+ * cases (emoji, combining marks, a lone surrogate, empty, CJK at the bound): the two forms answer
531
+ * identically on every one. Throughput on the check alone, cap 64: ordinary ASCII rows 23171k/s
532
+ * against 215029k/s, rows at the cap 4250k/s against 235657k/s.
533
+ *
534
+ * Only the code-point measurement is rewritten. A `byteLength` reads `v.length` already and has
535
+ * nothing to avoid, and the UTF-8 arm has its own sound short-circuits in both directions but a
536
+ * different allocation to weigh, so it is left for its own measurement rather than folded in here.
537
+ */
538
+ declare function codePointCompare(variable: string, operator: '>=' | '>' | '<=' | '<' | '===' | '!==', bound: string | number): string;
539
+ /**
540
+ * A length comparison over whichever measurement the column answers, short-circuited where that is
541
+ * sound. The one place an operator and a count meet, so no generator has to re-derive which
542
+ * rewrites are safe; see `codePointCompare`.
543
+ */
544
+ declare function measureCompare(measure: LengthMeasure, variable: string, operator: LengthCheck['operator'], bound: string | number): string;
507
545
  declare function measureExpression(measure: LengthMeasure, variable: string): string;
508
546
  /**
509
547
  * A count constraint as the sentence every emitted schema attaches and the ledger keys on.
@@ -558,21 +596,11 @@ type ParsedCheck = {
558
596
  reason: string;
559
597
  };
560
598
  /**
561
- * Parse one check expression.
562
- *
563
- * Deliberately narrow. `BETWEEN` is included because it is common and means exactly two
564
- * inclusive bounds; `AND` of arbitrary predicates is not, because getting its scope wrong would
565
- * silently change what is enforced.
566
- *
567
- * **The order below is load bearing, in both directions.** `BETWEEN` is matched before the `AND`
568
- * split because it *holds* an `AND`; splitting first turned every `BETWEEN` into an unparseable
569
- * pair and dropped a constraint that had been enforced. The unary `IS` predicates are matched
570
- * *after* the splits for the mirror-image reason: none of them holds an `AND` or an `OR`, and
571
- * their trailing operand is greedy, so matching first would let `a IS DISTINCT FROM 5 AND b > 0`
572
- * swallow the second predicate. `OR` is split before `AND` because SQL binds `AND` tighter, and
573
- * no SQL operator spells an `OR` inside itself the way `BETWEEN` spells an `AND`.
599
+ * `dialect` decides what `length()` counts, and nothing else in this parser. Optional, and absent
600
+ * means the Postgres reading, which is what every caller got before it existed: a caller that does
601
+ * not know its dialect keeps the answer it already had rather than being given a new one.
574
602
  */
575
- declare function parseCheck(expression: string | undefined, name?: string): ParsedCheck;
603
+ declare function parseCheck(expression: string | undefined, name?: string, dialect?: string): ParsedCheck;
576
604
  /**
577
605
  * A number-kind literal, spelled for the wire type of the column it constrains.
578
606
  *
@@ -1668,4 +1696,4 @@ declare function isProjectInstallPath(manifestPath: string): boolean;
1668
1696
  */
1669
1697
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
1670
1698
 
1671
- export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, columnMetaFacts, comparisonWire, describeSet, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureExpression, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
1699
+ export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, codePointCompare, columnMetaFacts, comparisonWire, describeSet, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureCompare, measureExpression, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
package/dist/index.d.ts CHANGED
@@ -452,6 +452,15 @@ interface LengthCheck {
452
452
  * a given column.
453
453
  */
454
454
  unit?: 'characters' | 'bytes';
455
+ /**
456
+ * The function as written, before the engine decided what it counts.
457
+ *
458
+ * `unit` used to imply this, because the two were the same question: `octet_length` meant bytes
459
+ * and everything else meant characters. On MySQL `length()` also means bytes, so deriving the
460
+ * name back from the unit relabelled a user's `length(name) <= 5` as `octet_length(name) <= 5`,
461
+ * which is a constraint they did not write. The label the ledger matches on has to be theirs.
462
+ */
463
+ fn?: 'length' | 'char_length' | 'octet_length';
455
464
  name?: string;
456
465
  }
457
466
  /**
@@ -504,6 +513,35 @@ declare function lengthMeasure(column: {
504
513
  * TypeBox reach it off the row object as `o["blob"]`. `CODEPOINT_LENGTH` is the fixed-variable
505
514
  * spelling of the first arm and stays for the call sites that already use it.
506
515
  */
516
+ /**
517
+ * A code-point count compared against a bound, without spreading the string when it cannot matter.
518
+ *
519
+ * `[...v].length` allocates an array of code points, and it sits on the path every ordinary row
520
+ * takes. A UTF-16 unit count is free and is never *smaller* than a code-point count, since only a
521
+ * surrogate pair costs two units for one code point. That one fact settles every operator here:
522
+ *
523
+ * `<=`, `<` units within the bound means code points are too short-circuit to ACCEPT
524
+ * `>=`, `>` units under the bound means code points are too short-circuit to REJECT
525
+ * `===` units under the bound means code points cannot equal it short-circuit to REJECT
526
+ * `!==` units under the bound means code points cannot equal it short-circuit to ACCEPT
527
+ *
528
+ * So the spread runs only for a string long enough in units to be in doubt, which for a cap is the
529
+ * only string that was ever in doubt. Measured over a 20000-string random pool plus the awkward
530
+ * cases (emoji, combining marks, a lone surrogate, empty, CJK at the bound): the two forms answer
531
+ * identically on every one. Throughput on the check alone, cap 64: ordinary ASCII rows 23171k/s
532
+ * against 215029k/s, rows at the cap 4250k/s against 235657k/s.
533
+ *
534
+ * Only the code-point measurement is rewritten. A `byteLength` reads `v.length` already and has
535
+ * nothing to avoid, and the UTF-8 arm has its own sound short-circuits in both directions but a
536
+ * different allocation to weigh, so it is left for its own measurement rather than folded in here.
537
+ */
538
+ declare function codePointCompare(variable: string, operator: '>=' | '>' | '<=' | '<' | '===' | '!==', bound: string | number): string;
539
+ /**
540
+ * A length comparison over whichever measurement the column answers, short-circuited where that is
541
+ * sound. The one place an operator and a count meet, so no generator has to re-derive which
542
+ * rewrites are safe; see `codePointCompare`.
543
+ */
544
+ declare function measureCompare(measure: LengthMeasure, variable: string, operator: LengthCheck['operator'], bound: string | number): string;
507
545
  declare function measureExpression(measure: LengthMeasure, variable: string): string;
508
546
  /**
509
547
  * A count constraint as the sentence every emitted schema attaches and the ledger keys on.
@@ -558,21 +596,11 @@ type ParsedCheck = {
558
596
  reason: string;
559
597
  };
560
598
  /**
561
- * Parse one check expression.
562
- *
563
- * Deliberately narrow. `BETWEEN` is included because it is common and means exactly two
564
- * inclusive bounds; `AND` of arbitrary predicates is not, because getting its scope wrong would
565
- * silently change what is enforced.
566
- *
567
- * **The order below is load bearing, in both directions.** `BETWEEN` is matched before the `AND`
568
- * split because it *holds* an `AND`; splitting first turned every `BETWEEN` into an unparseable
569
- * pair and dropped a constraint that had been enforced. The unary `IS` predicates are matched
570
- * *after* the splits for the mirror-image reason: none of them holds an `AND` or an `OR`, and
571
- * their trailing operand is greedy, so matching first would let `a IS DISTINCT FROM 5 AND b > 0`
572
- * swallow the second predicate. `OR` is split before `AND` because SQL binds `AND` tighter, and
573
- * no SQL operator spells an `OR` inside itself the way `BETWEEN` spells an `AND`.
599
+ * `dialect` decides what `length()` counts, and nothing else in this parser. Optional, and absent
600
+ * means the Postgres reading, which is what every caller got before it existed: a caller that does
601
+ * not know its dialect keeps the answer it already had rather than being given a new one.
574
602
  */
575
- declare function parseCheck(expression: string | undefined, name?: string): ParsedCheck;
603
+ declare function parseCheck(expression: string | undefined, name?: string, dialect?: string): ParsedCheck;
576
604
  /**
577
605
  * A number-kind literal, spelled for the wire type of the column it constrains.
578
606
  *
@@ -1668,4 +1696,4 @@ declare function isProjectInstallPath(manifestPath: string): boolean;
1668
1696
  */
1669
1697
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
1670
1698
 
1671
- export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, columnMetaFacts, comparisonWire, describeSet, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureExpression, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
1699
+ export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, codePointCompare, columnMetaFacts, comparisonWire, describeSet, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureCompare, measureExpression, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
package/dist/index.js CHANGED
@@ -13,6 +13,34 @@ function lengthMeasure(column, check) {
13
13
  if (column.tsType !== "string") return void 0;
14
14
  return check.unit === "bytes" ? "utf8Bytes" : "codePoints";
15
15
  }
16
+ function codePointCompare(variable, operator, bound) {
17
+ const units = `${variable}.length`;
18
+ const spread2 = `[...${variable}].length`;
19
+ switch (operator) {
20
+ case "<=":
21
+ case "<":
22
+ return `(${units} ${operator} ${bound} || ${spread2} ${operator} ${bound})`;
23
+ case ">=":
24
+ case ">":
25
+ return `(${units} ${operator} ${bound} && ${spread2} ${operator} ${bound})`;
26
+ case "===":
27
+ return `(${units} >= ${bound} && ${spread2} === ${bound})`;
28
+ case "!==":
29
+ return `(${units} < ${bound} || ${spread2} !== ${bound})`;
30
+ }
31
+ }
32
+ function measureCompare(measure, variable, operator, bound) {
33
+ const js = {
34
+ ">=": ">=",
35
+ ">": ">",
36
+ "<=": "<=",
37
+ "<": "<",
38
+ "=": "===",
39
+ "<>": "!=="
40
+ }[operator];
41
+ if (measure === "codePoints") return codePointCompare(variable, js, bound);
42
+ return `${measureExpression(measure, variable)} ${js} ${bound}`;
43
+ }
16
44
  function measureExpression(measure, variable) {
17
45
  switch (measure) {
18
46
  case "codePoints":
@@ -24,7 +52,7 @@ function measureExpression(measure, variable) {
24
52
  }
25
53
  }
26
54
  function lengthCheckLabel(check) {
27
- const fn = check.unit === "bytes" ? "octet_length" : "length";
55
+ const fn = check.fn ? check.fn === "char_length" ? "length" : check.fn : check.unit === "bytes" ? "octet_length" : "length";
28
56
  const rule = `${fn}(${check.column}) ${check.operator} ${check.value}`;
29
57
  return check.name ? `${check.name}: ${rule}` : rule;
30
58
  }
@@ -275,7 +303,13 @@ function foldDisjunctionToSet(branches, name) {
275
303
  sets: [{ column, values, kind, name }]
276
304
  };
277
305
  }
278
- function parseCheck(expression, name) {
306
+ function lengthUnit(fn, dialect) {
307
+ const name = fn.toLowerCase();
308
+ if (name === "octet_length") return "bytes";
309
+ if (name === "char_length") return "characters";
310
+ return dialect === "mysql" ? "bytes" : "characters";
311
+ }
312
+ function parseCheck(expression, name, dialect) {
279
313
  const expr = unwrap((expression ?? "").trim());
280
314
  if (!expr) return { ok: false, reason: "empty expression" };
281
315
  if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
@@ -352,11 +386,12 @@ function parseCheck(expression, name) {
352
386
  const lengthOf = expr.match(LENGTH_OF);
353
387
  if (lengthOf) {
354
388
  const op2 = lengthOf[3] === "!=" ? "<>" : lengthOf[3];
355
- const unit = lengthOf[1].toLowerCase() === "octet_length" ? "bytes" : "characters";
389
+ const fn = lengthOf[1].toLowerCase();
390
+ const unit = lengthUnit(fn, dialect);
356
391
  return {
357
392
  ok: true,
358
393
  checks: [],
359
- lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, name }]
394
+ lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, fn, name }]
360
395
  };
361
396
  }
362
397
  const cardinalityOf = expr.match(CARDINALITY_OF);
@@ -863,7 +898,7 @@ function classifyTableChecks(table) {
863
898
  const out = [];
864
899
  for (const k of table.checks ?? []) {
865
900
  const expression = (k.expression ?? "").trim();
866
- const parsed = parseCheck(k.expression, k.name);
901
+ const parsed = parseCheck(k.expression, k.name, table.dialect);
867
902
  if (!parsed.ok) {
868
903
  out.push({
869
904
  ...k.name ? { name: k.name } : {},
@@ -1844,6 +1879,7 @@ export {
1844
1879
  canonicalMembers,
1845
1880
  canonicalNumericText,
1846
1881
  classifyTableChecks,
1882
+ codePointCompare,
1847
1883
  columnMetaFacts,
1848
1884
  comparisonWire,
1849
1885
  describeSet,
@@ -1856,6 +1892,7 @@ export {
1856
1892
  isProjectInstallPath,
1857
1893
  lengthCheckLabel,
1858
1894
  lengthMeasure,
1895
+ measureCompare,
1859
1896
  measureExpression,
1860
1897
  moduleFileName,
1861
1898
  moduleSpecifier,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "3.22.2",
3
+ "version": "3.22.4",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "sideEffects": false,
26
26
  "dependencies": {
27
- "@drzl/analyzer": "^1.21.2"
27
+ "@drzl/analyzer": "^1.21.4"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "prettier": ">=3"