@drzl/validation-core 3.22.3 → 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
@@ -153,7 +153,7 @@ function measureExpression(measure, variable) {
153
153
  }
154
154
  }
155
155
  function lengthCheckLabel(check) {
156
- 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";
157
157
  const rule = `${fn}(${check.column}) ${check.operator} ${check.value}`;
158
158
  return check.name ? `${check.name}: ${rule}` : rule;
159
159
  }
@@ -404,7 +404,13 @@ function foldDisjunctionToSet(branches, name) {
404
404
  sets: [{ column, values, kind, name }]
405
405
  };
406
406
  }
407
- 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) {
408
414
  const expr = unwrap((expression ?? "").trim());
409
415
  if (!expr) return { ok: false, reason: "empty expression" };
410
416
  if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
@@ -481,11 +487,12 @@ function parseCheck(expression, name) {
481
487
  const lengthOf = expr.match(LENGTH_OF);
482
488
  if (lengthOf) {
483
489
  const op2 = lengthOf[3] === "!=" ? "<>" : lengthOf[3];
484
- const unit = lengthOf[1].toLowerCase() === "octet_length" ? "bytes" : "characters";
490
+ const fn = lengthOf[1].toLowerCase();
491
+ const unit = lengthUnit(fn, dialect);
485
492
  return {
486
493
  ok: true,
487
494
  checks: [],
488
- lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, name }]
495
+ lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, fn, name }]
489
496
  };
490
497
  }
491
498
  const cardinalityOf = expr.match(CARDINALITY_OF);
@@ -992,7 +999,7 @@ function classifyTableChecks(table) {
992
999
  const out = [];
993
1000
  for (const k of table.checks ?? []) {
994
1001
  const expression = (k.expression ?? "").trim();
995
- const parsed = parseCheck(k.expression, k.name);
1002
+ const parsed = parseCheck(k.expression, k.name, table.dialect);
996
1003
  if (!parsed.ok) {
997
1004
  out.push({
998
1005
  ...k.name ? { name: k.name } : {},
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
  /**
@@ -587,21 +596,11 @@ type ParsedCheck = {
587
596
  reason: string;
588
597
  };
589
598
  /**
590
- * Parse one check expression.
591
- *
592
- * Deliberately narrow. `BETWEEN` is included because it is common and means exactly two
593
- * inclusive bounds; `AND` of arbitrary predicates is not, because getting its scope wrong would
594
- * silently change what is enforced.
595
- *
596
- * **The order below is load bearing, in both directions.** `BETWEEN` is matched before the `AND`
597
- * split because it *holds* an `AND`; splitting first turned every `BETWEEN` into an unparseable
598
- * pair and dropped a constraint that had been enforced. The unary `IS` predicates are matched
599
- * *after* the splits for the mirror-image reason: none of them holds an `AND` or an `OR`, and
600
- * their trailing operand is greedy, so matching first would let `a IS DISTINCT FROM 5 AND b > 0`
601
- * swallow the second predicate. `OR` is split before `AND` because SQL binds `AND` tighter, and
602
- * 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.
603
602
  */
604
- declare function parseCheck(expression: string | undefined, name?: string): ParsedCheck;
603
+ declare function parseCheck(expression: string | undefined, name?: string, dialect?: string): ParsedCheck;
605
604
  /**
606
605
  * A number-kind literal, spelled for the wire type of the column it constrains.
607
606
  *
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
  /**
@@ -587,21 +596,11 @@ type ParsedCheck = {
587
596
  reason: string;
588
597
  };
589
598
  /**
590
- * Parse one check expression.
591
- *
592
- * Deliberately narrow. `BETWEEN` is included because it is common and means exactly two
593
- * inclusive bounds; `AND` of arbitrary predicates is not, because getting its scope wrong would
594
- * silently change what is enforced.
595
- *
596
- * **The order below is load bearing, in both directions.** `BETWEEN` is matched before the `AND`
597
- * split because it *holds* an `AND`; splitting first turned every `BETWEEN` into an unparseable
598
- * pair and dropped a constraint that had been enforced. The unary `IS` predicates are matched
599
- * *after* the splits for the mirror-image reason: none of them holds an `AND` or an `OR`, and
600
- * their trailing operand is greedy, so matching first would let `a IS DISTINCT FROM 5 AND b > 0`
601
- * swallow the second predicate. `OR` is split before `AND` because SQL binds `AND` tighter, and
602
- * 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.
603
602
  */
604
- declare function parseCheck(expression: string | undefined, name?: string): ParsedCheck;
603
+ declare function parseCheck(expression: string | undefined, name?: string, dialect?: string): ParsedCheck;
605
604
  /**
606
605
  * A number-kind literal, spelled for the wire type of the column it constrains.
607
606
  *
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ function measureExpression(measure, variable) {
52
52
  }
53
53
  }
54
54
  function lengthCheckLabel(check) {
55
- 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";
56
56
  const rule = `${fn}(${check.column}) ${check.operator} ${check.value}`;
57
57
  return check.name ? `${check.name}: ${rule}` : rule;
58
58
  }
@@ -303,7 +303,13 @@ function foldDisjunctionToSet(branches, name) {
303
303
  sets: [{ column, values, kind, name }]
304
304
  };
305
305
  }
306
- 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) {
307
313
  const expr = unwrap((expression ?? "").trim());
308
314
  if (!expr) return { ok: false, reason: "empty expression" };
309
315
  if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
@@ -380,11 +386,12 @@ function parseCheck(expression, name) {
380
386
  const lengthOf = expr.match(LENGTH_OF);
381
387
  if (lengthOf) {
382
388
  const op2 = lengthOf[3] === "!=" ? "<>" : lengthOf[3];
383
- const unit = lengthOf[1].toLowerCase() === "octet_length" ? "bytes" : "characters";
389
+ const fn = lengthOf[1].toLowerCase();
390
+ const unit = lengthUnit(fn, dialect);
384
391
  return {
385
392
  ok: true,
386
393
  checks: [],
387
- lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, name }]
394
+ lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, fn, name }]
388
395
  };
389
396
  }
390
397
  const cardinalityOf = expr.match(CARDINALITY_OF);
@@ -891,7 +898,7 @@ function classifyTableChecks(table) {
891
898
  const out = [];
892
899
  for (const k of table.checks ?? []) {
893
900
  const expression = (k.expression ?? "").trim();
894
- const parsed = parseCheck(k.expression, k.name);
901
+ const parsed = parseCheck(k.expression, k.name, table.dialect);
895
902
  if (!parsed.ok) {
896
903
  out.push({
897
904
  ...k.name ? { name: k.name } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "3.22.3",
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.3"
27
+ "@drzl/analyzer": "^1.21.4"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "prettier": ">=3"