@drzl/validation-core 3.18.0 → 3.22.0
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/README.md +34 -9
- package/dist/index.cjs +1307 -92
- package/dist/index.d.cts +887 -5
- package/dist/index.d.ts +887 -5
- package/dist/index.js +1280 -91
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AFFIX_PREFIX_PATTERN: () => AFFIX_PREFIX_PATTERN,
|
|
33
34
|
AFFIX_PROBE_TABLE: () => AFFIX_PROBE_TABLE,
|
|
35
|
+
AFFIX_SUFFIX_PATTERN: () => AFFIX_SUFFIX_PATTERN,
|
|
34
36
|
CODEPOINT_LENGTH: () => CODEPOINT_LENGTH,
|
|
35
37
|
COERCIBLE_DATE_STRING: () => COERCIBLE_DATE_STRING,
|
|
36
38
|
COLUMN_FORMATS: () => COLUMN_FORMATS,
|
|
39
|
+
CONSTRAINTS_MODULE: () => CONSTRAINTS_MODULE,
|
|
37
40
|
DEFAULT_IMPORT_EXTENSION: () => DEFAULT_IMPORT_EXTENSION,
|
|
38
41
|
DEFAULT_MODE_PREFIX: () => DEFAULT_MODE_PREFIX,
|
|
39
42
|
DEFAULT_NESTED_DEPTH: () => DEFAULT_NESTED_DEPTH,
|
|
@@ -43,33 +46,56 @@ __export(index_exports, {
|
|
|
43
46
|
MAX_NESTED_DEPTH: () => MAX_NESTED_DEPTH,
|
|
44
47
|
NAME_MODES: () => NAME_MODES,
|
|
45
48
|
NESTED_PREFIX: () => NESTED_PREFIX,
|
|
49
|
+
NUMERIC_CANON_NAME: () => NUMERIC_CANON_NAME,
|
|
50
|
+
NUMERIC_CANON_SOURCE: () => NUMERIC_CANON_SOURCE,
|
|
46
51
|
applyTableCase: () => applyTableCase,
|
|
52
|
+
applyWirePolicy: () => applyWirePolicy,
|
|
53
|
+
buildBrandPlan: () => buildBrandPlan,
|
|
47
54
|
buildNestedPlan: () => buildNestedPlan,
|
|
55
|
+
canonicalMembers: () => canonicalMembers,
|
|
56
|
+
canonicalNumericText: () => canonicalNumericText,
|
|
57
|
+
classifyTableChecks: () => classifyTableChecks,
|
|
58
|
+
columnMetaFacts: () => columnMetaFacts,
|
|
59
|
+
comparisonWire: () => comparisonWire,
|
|
48
60
|
describeSet: () => describeSet,
|
|
61
|
+
fileWriter: () => fileWriter,
|
|
49
62
|
formatCode: () => formatCode,
|
|
50
63
|
importSpecifier: () => importSpecifier,
|
|
51
64
|
insertColumns: () => insertColumns,
|
|
52
65
|
isGeneratedColumn: () => isGeneratedColumn,
|
|
53
66
|
isIntegerColumn: () => isIntegerColumn,
|
|
67
|
+
isProjectInstallPath: () => isProjectInstallPath,
|
|
68
|
+
lengthCheckLabel: () => lengthCheckLabel,
|
|
69
|
+
lengthMeasure: () => lengthMeasure,
|
|
70
|
+
measureExpression: () => measureExpression,
|
|
54
71
|
moduleFileName: () => moduleFileName,
|
|
55
72
|
moduleSpecifier: () => moduleSpecifier,
|
|
73
|
+
needsNumericCanon: () => needsNumericCanon,
|
|
56
74
|
nestedArmNotes: () => nestedArmNotes,
|
|
57
75
|
nestedNodeColumns: () => nestedNodeColumns,
|
|
58
76
|
nestedSchemaName: () => nestedSchemaName,
|
|
59
77
|
nestedTypeName: () => nestedTypeName,
|
|
60
78
|
nonFiniteAccepted: () => nonFiniteAccepted,
|
|
79
|
+
nonFiniteRefused: () => nonFiniteRefused,
|
|
61
80
|
parseCheck: () => parseCheck,
|
|
62
81
|
parsesToADate: () => parsesToADate,
|
|
63
82
|
pascalCase: () => pascalCase,
|
|
83
|
+
renderConstraintsModule: () => renderConstraintsModule,
|
|
64
84
|
renderDuplicateFinder: () => renderDuplicateFinder,
|
|
65
85
|
resolveAffix: () => resolveAffix,
|
|
86
|
+
resolveBranding: () => resolveBranding,
|
|
66
87
|
resolveConfiguredImport: () => resolveConfiguredImport,
|
|
88
|
+
resolveConstraints: () => resolveConstraints,
|
|
67
89
|
resolveNestedDepth: () => resolveNestedDepth,
|
|
68
90
|
schemaName: () => schemaName,
|
|
69
91
|
selectColumns: () => selectColumns,
|
|
92
|
+
tableConstraints: () => tableConstraints,
|
|
93
|
+
tableMetaFacts: () => tableMetaFacts,
|
|
70
94
|
typeName: () => typeName,
|
|
71
95
|
updateColumns: () => updateColumns,
|
|
72
|
-
validateAffix: () => validateAffix
|
|
96
|
+
validateAffix: () => validateAffix,
|
|
97
|
+
wireLiteralFit: () => wireLiteralFit,
|
|
98
|
+
wireNumberLiteral: () => wireNumberLiteral
|
|
73
99
|
});
|
|
74
100
|
module.exports = __toCommonJS(index_exports);
|
|
75
101
|
var import_node_child_process = require("child_process");
|
|
@@ -79,11 +105,34 @@ var import_node_path2 = __toESM(require("path"), 1);
|
|
|
79
105
|
var import_node_url = require("url");
|
|
80
106
|
|
|
81
107
|
// src/checks.ts
|
|
108
|
+
function lengthMeasure(column, check) {
|
|
109
|
+
if (column.arrayDimensions) return void 0;
|
|
110
|
+
if (column.shape?.kind === "buffer") return "byteLength";
|
|
111
|
+
if (column.shape) return void 0;
|
|
112
|
+
if (column.tsType !== "string") return void 0;
|
|
113
|
+
return check.unit === "bytes" ? "utf8Bytes" : "codePoints";
|
|
114
|
+
}
|
|
115
|
+
function measureExpression(measure, variable) {
|
|
116
|
+
switch (measure) {
|
|
117
|
+
case "codePoints":
|
|
118
|
+
return `[...${variable}].length`;
|
|
119
|
+
case "utf8Bytes":
|
|
120
|
+
return `new TextEncoder().encode(${variable}).length`;
|
|
121
|
+
case "byteLength":
|
|
122
|
+
return `${variable}.length`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function lengthCheckLabel(check) {
|
|
126
|
+
const fn = check.unit === "bytes" ? "octet_length" : "length";
|
|
127
|
+
const rule = `${fn}(${check.column}) ${check.operator} ${check.value}`;
|
|
128
|
+
return check.name ? `${check.name}: ${rule}` : rule;
|
|
129
|
+
}
|
|
82
130
|
var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
|
|
83
131
|
var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
|
|
84
132
|
var CARDINALITY_OF = /^\s*(?:cardinality\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)|array_length\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*1\s*\))\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
|
|
85
|
-
var LENGTH_OF = /^\s*(
|
|
86
|
-
|
|
133
|
+
var LENGTH_OF = /^\s*(length|char_length|octet_length)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
|
|
134
|
+
var WORD = /[A-Za-z0-9_]/;
|
|
135
|
+
function splitTopLevel(expr, keyword) {
|
|
87
136
|
const parts = [];
|
|
88
137
|
let depth = 0;
|
|
89
138
|
let inString = false;
|
|
@@ -103,18 +152,64 @@ function splitTopLevelAnd(expr) {
|
|
|
103
152
|
}
|
|
104
153
|
if (c === "(") depth++;
|
|
105
154
|
else if (c === ")") depth--;
|
|
106
|
-
else if (depth === 0 &&
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
155
|
+
else if (depth === 0 && WORD.test(c)) {
|
|
156
|
+
if (i > 0 && WORD.test(expr[i - 1])) continue;
|
|
157
|
+
if (expr.slice(i, i + keyword.length).toUpperCase() !== keyword) continue;
|
|
158
|
+
const after = expr[i + keyword.length];
|
|
159
|
+
if (after !== void 0 && WORD.test(after)) continue;
|
|
160
|
+
parts.push(expr.slice(start, i));
|
|
161
|
+
i += keyword.length - 1;
|
|
162
|
+
start = i + 1;
|
|
113
163
|
}
|
|
114
164
|
}
|
|
115
165
|
parts.push(expr.slice(start));
|
|
116
166
|
return parts;
|
|
117
167
|
}
|
|
168
|
+
function hasLogicalNot(expr) {
|
|
169
|
+
let inString = false;
|
|
170
|
+
for (let i = 0; i < expr.length; i++) {
|
|
171
|
+
const c = expr[i];
|
|
172
|
+
if (inString) {
|
|
173
|
+
if (c === "'") {
|
|
174
|
+
if (expr[i + 1] === "'") i++;
|
|
175
|
+
else inString = false;
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (c === "'") {
|
|
180
|
+
inString = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (c !== "N" && c !== "n") continue;
|
|
184
|
+
if (i > 0 && WORD.test(expr[i - 1])) continue;
|
|
185
|
+
if (!/^NOT($|[^A-Za-z0-9_])/i.test(expr.slice(i))) continue;
|
|
186
|
+
if (/(^|[^A-Za-z0-9_])IS\s+$/i.test(expr.slice(0, i))) continue;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
var COMBINING = /(?:^|\s)(\|\||[+\-*/%])(?:\s)/;
|
|
192
|
+
function combiningOperator(expr) {
|
|
193
|
+
let inString = false;
|
|
194
|
+
let bare = "";
|
|
195
|
+
for (let i = 0; i < expr.length; i++) {
|
|
196
|
+
const c = expr[i];
|
|
197
|
+
if (inString) {
|
|
198
|
+
if (c === "'") {
|
|
199
|
+
if (expr[i + 1] === "'") i++;
|
|
200
|
+
else inString = false;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (c === "'") {
|
|
205
|
+
inString = true;
|
|
206
|
+
bare += " ";
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
bare += c;
|
|
210
|
+
}
|
|
211
|
+
return COMBINING.exec(bare)?.[1];
|
|
212
|
+
}
|
|
118
213
|
function unwrap(expr) {
|
|
119
214
|
let e = expr.trim();
|
|
120
215
|
while (e.startsWith("(") && e.endsWith(")")) {
|
|
@@ -145,16 +240,16 @@ function unwrap(expr) {
|
|
|
145
240
|
}
|
|
146
241
|
return e;
|
|
147
242
|
}
|
|
148
|
-
function splitTopLevelCommas(
|
|
243
|
+
function splitTopLevelCommas(list2) {
|
|
149
244
|
const parts = [];
|
|
150
245
|
let depth = 0;
|
|
151
246
|
let inString = false;
|
|
152
247
|
let start = 0;
|
|
153
|
-
for (let i = 0; i <
|
|
154
|
-
const c =
|
|
248
|
+
for (let i = 0; i < list2.length; i++) {
|
|
249
|
+
const c = list2[i];
|
|
155
250
|
if (inString) {
|
|
156
251
|
if (c === "'") {
|
|
157
|
-
if (
|
|
252
|
+
if (list2[i + 1] === "'") i++;
|
|
158
253
|
else inString = false;
|
|
159
254
|
}
|
|
160
255
|
continue;
|
|
@@ -163,14 +258,17 @@ function splitTopLevelCommas(list) {
|
|
|
163
258
|
else if (c === "(") depth++;
|
|
164
259
|
else if (c === ")") depth--;
|
|
165
260
|
else if (c === "," && depth === 0) {
|
|
166
|
-
parts.push(
|
|
261
|
+
parts.push(list2.slice(start, i));
|
|
167
262
|
start = i + 1;
|
|
168
263
|
}
|
|
169
264
|
}
|
|
170
|
-
parts.push(
|
|
265
|
+
parts.push(list2.slice(start));
|
|
171
266
|
return parts;
|
|
172
267
|
}
|
|
173
268
|
var BETWEEN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+BETWEEN\s+(.+?)\s+AND\s+(.+?)\s*$/i;
|
|
269
|
+
var IS_NULL = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IS\s+(NOT\s+)?NULL\s*$/i;
|
|
270
|
+
var IS_DISTINCT = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IS\s+(NOT\s+)?DISTINCT\s+FROM\s+(.+?)\s*$/i;
|
|
271
|
+
var IS_BOOLEAN = /^\s*[A-Za-z_][A-Za-z0-9_]*\s+IS\s+(?:NOT\s+)?(TRUE|FALSE|UNKNOWN)\s*$/i;
|
|
174
272
|
function literal(raw) {
|
|
175
273
|
const t = raw.trim();
|
|
176
274
|
if (/^-?\d+(\.\d+)?$/.test(t)) return { value: t, kind: "number" };
|
|
@@ -178,12 +276,111 @@ function literal(raw) {
|
|
|
178
276
|
if (m) return { value: m[1].replace(/''/g, "'"), kind: "string" };
|
|
179
277
|
return void 0;
|
|
180
278
|
}
|
|
279
|
+
function columnsOf(parsed) {
|
|
280
|
+
const out = /* @__PURE__ */ new Set();
|
|
281
|
+
for (const c of parsed.checks) out.add(c.column);
|
|
282
|
+
for (const s of parsed.sets ?? []) out.add(s.column);
|
|
283
|
+
for (const l of parsed.lengths ?? []) out.add(l.column);
|
|
284
|
+
for (const a of parsed.cardinalities ?? []) out.add(a.column);
|
|
285
|
+
for (const n of parsed.nulls ?? []) out.add(n.column);
|
|
286
|
+
for (const r of parsed.rows ?? []) {
|
|
287
|
+
out.add(r.left);
|
|
288
|
+
out.add(r.right);
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
function parseDisjunction(branches, name) {
|
|
293
|
+
const guarded = [];
|
|
294
|
+
const rest = [];
|
|
295
|
+
for (const b of branches) {
|
|
296
|
+
const m = unwrap(b).match(IS_NULL);
|
|
297
|
+
if (m && !m[2]) guarded.push(m[1]);
|
|
298
|
+
else rest.push(b);
|
|
299
|
+
}
|
|
300
|
+
const reduced = (() => {
|
|
301
|
+
if (!rest.length)
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
reason: "every branch of the OR is a null test, which is a rule about the row"
|
|
305
|
+
};
|
|
306
|
+
if (rest.length === 1) return parseCheck(rest[0], name);
|
|
307
|
+
return foldDisjunctionToSet(rest, name);
|
|
308
|
+
})();
|
|
309
|
+
if (!reduced.ok || !guarded.length) return reduced;
|
|
310
|
+
if (reduced.nulls?.length)
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
reason: "a null test guarded by IS NULL, which is true of every row rather than a narrowing"
|
|
314
|
+
};
|
|
315
|
+
const named = columnsOf(reduced);
|
|
316
|
+
const stray = guarded.filter((g) => !named.has(g));
|
|
317
|
+
if (stray.length)
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
reason: `${stray.map((s) => `"${s}"`).join(" and ")} IS NULL guards a predicate that does not name it`
|
|
321
|
+
};
|
|
322
|
+
return reduced;
|
|
323
|
+
}
|
|
324
|
+
function foldDisjunctionToSet(branches, name) {
|
|
325
|
+
let column;
|
|
326
|
+
let kind;
|
|
327
|
+
const values = [];
|
|
328
|
+
for (const branch of branches) {
|
|
329
|
+
const parsed = parseCheck(branch, name);
|
|
330
|
+
if (!parsed.ok)
|
|
331
|
+
return { ok: false, reason: `part of an OR was not understood: ${parsed.reason}` };
|
|
332
|
+
if (parsed.rows?.length)
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
reason: "a branch of the OR compares two columns, which is a rule about the row"
|
|
336
|
+
};
|
|
337
|
+
if (parsed.lengths?.length || parsed.cardinalities?.length)
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
reason: "a branch of the OR is a count rather than a value, so the OR states no set"
|
|
341
|
+
};
|
|
342
|
+
if (parsed.nulls?.length)
|
|
343
|
+
return { ok: false, reason: "a branch of the OR is a null test rather than a value" };
|
|
344
|
+
const set = parsed.sets?.[0];
|
|
345
|
+
const range = parsed.checks.find((c) => c.operator !== "=");
|
|
346
|
+
if (range)
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
reason: `a branch of the OR is a range (${range.column} ${range.operator} ${range.value}) rather than a set of values`
|
|
350
|
+
};
|
|
351
|
+
if (parsed.checks.length + (parsed.sets?.length ?? 0) !== 1)
|
|
352
|
+
return { ok: false, reason: "a branch of the OR states more than one thing" };
|
|
353
|
+
const here = set ? { column: set.column, kind: set.kind, values: set.values } : {
|
|
354
|
+
column: parsed.checks[0].column,
|
|
355
|
+
kind: parsed.checks[0].kind,
|
|
356
|
+
values: [parsed.checks[0].value]
|
|
357
|
+
};
|
|
358
|
+
if (column === void 0) {
|
|
359
|
+
column = here.column;
|
|
360
|
+
kind = here.kind;
|
|
361
|
+
}
|
|
362
|
+
if (here.column !== column)
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
reason: `the OR branches constrain different columns (${column}, ${here.column}), so it states a rule about the row rather than about a field`
|
|
366
|
+
};
|
|
367
|
+
if (here.kind !== kind)
|
|
368
|
+
return { ok: false, reason: "the OR branches mix a string and a number" };
|
|
369
|
+
for (const v of here.values) if (!values.includes(v)) values.push(v);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: true,
|
|
373
|
+
checks: [],
|
|
374
|
+
sets: [{ column, values, kind, name }]
|
|
375
|
+
};
|
|
376
|
+
}
|
|
181
377
|
function parseCheck(expression, name) {
|
|
182
378
|
const expr = unwrap((expression ?? "").trim());
|
|
183
379
|
if (!expr) return { ok: false, reason: "empty expression" };
|
|
184
380
|
if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
|
|
185
|
-
|
|
186
|
-
if (
|
|
381
|
+
const branches = splitTopLevel(expr, "OR");
|
|
382
|
+
if (branches.length > 1) return parseDisjunction(branches, name);
|
|
383
|
+
if (hasLogicalNot(expr)) return { ok: false, reason: "contains NOT" };
|
|
187
384
|
const between = expr.match(BETWEEN);
|
|
188
385
|
if (between) {
|
|
189
386
|
const lo = literal(between[2]);
|
|
@@ -198,13 +395,14 @@ function parseCheck(expression, name) {
|
|
|
198
395
|
]
|
|
199
396
|
};
|
|
200
397
|
}
|
|
201
|
-
const parts =
|
|
398
|
+
const parts = splitTopLevel(expr, "AND");
|
|
202
399
|
if (parts.length > 1) {
|
|
203
400
|
const checks = [];
|
|
204
401
|
const sets = [];
|
|
205
402
|
const rows = [];
|
|
206
403
|
const lengths = [];
|
|
207
404
|
const cardinalities = [];
|
|
405
|
+
const nulls = [];
|
|
208
406
|
for (const part of parts) {
|
|
209
407
|
const parsed = parseCheck(part, name);
|
|
210
408
|
if (!parsed.ok)
|
|
@@ -214,6 +412,7 @@ function parseCheck(expression, name) {
|
|
|
214
412
|
if (parsed.rows) rows.push(...parsed.rows);
|
|
215
413
|
if (parsed.lengths) lengths.push(...parsed.lengths);
|
|
216
414
|
if (parsed.cardinalities) cardinalities.push(...parsed.cardinalities);
|
|
415
|
+
if (parsed.nulls) nulls.push(...parsed.nulls);
|
|
217
416
|
}
|
|
218
417
|
return {
|
|
219
418
|
ok: true,
|
|
@@ -221,16 +420,42 @@ function parseCheck(expression, name) {
|
|
|
221
420
|
...sets.length ? { sets } : {},
|
|
222
421
|
...rows.length ? { rows } : {},
|
|
223
422
|
...lengths.length ? { lengths } : {},
|
|
224
|
-
...cardinalities.length ? { cardinalities } : {}
|
|
423
|
+
...cardinalities.length ? { cardinalities } : {},
|
|
424
|
+
...nulls.length ? { nulls } : {}
|
|
225
425
|
};
|
|
226
426
|
}
|
|
427
|
+
const isNull = expr.match(IS_NULL);
|
|
428
|
+
if (isNull) {
|
|
429
|
+
return {
|
|
430
|
+
ok: true,
|
|
431
|
+
checks: [],
|
|
432
|
+
nulls: [{ column: isNull[1], notNull: !!isNull[2], ...name ? { name } : {} }]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const isDistinct = expr.match(IS_DISTINCT);
|
|
436
|
+
if (isDistinct) {
|
|
437
|
+
const value2 = literal(isDistinct[3]);
|
|
438
|
+
if (!value2) return { ok: false, reason: "the right side of IS DISTINCT FROM is not a literal" };
|
|
439
|
+
const column = isDistinct[1];
|
|
440
|
+
const negated = !!isDistinct[2];
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
checks: [
|
|
444
|
+
{ column, operator: negated ? "=" : "<>", value: value2.value, kind: value2.kind, name }
|
|
445
|
+
],
|
|
446
|
+
...negated ? { nulls: [{ column, notNull: true, ...name ? { name } : {} }] } : {}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (IS_BOOLEAN.test(expr))
|
|
450
|
+
return { ok: false, reason: "a boolean IS test, whose literal this version does not read" };
|
|
227
451
|
const lengthOf = expr.match(LENGTH_OF);
|
|
228
452
|
if (lengthOf) {
|
|
229
|
-
const op2 = lengthOf[
|
|
453
|
+
const op2 = lengthOf[3] === "!=" ? "<>" : lengthOf[3];
|
|
454
|
+
const unit = lengthOf[1].toLowerCase() === "octet_length" ? "bytes" : "characters";
|
|
230
455
|
return {
|
|
231
456
|
ok: true,
|
|
232
457
|
checks: [],
|
|
233
|
-
lengths: [{ column: lengthOf[
|
|
458
|
+
lengths: [{ column: lengthOf[2], operator: op2, value: lengthOf[4], unit, name }]
|
|
234
459
|
};
|
|
235
460
|
}
|
|
236
461
|
const cardinalityOf = expr.match(CARDINALITY_OF);
|
|
@@ -270,8 +495,16 @@ function parseCheck(expression, name) {
|
|
|
270
495
|
]
|
|
271
496
|
};
|
|
272
497
|
}
|
|
498
|
+
const combining = combiningOperator(expr);
|
|
499
|
+
const arithmetic = () => ({
|
|
500
|
+
ok: false,
|
|
501
|
+
reason: `columns combined with "${combining}", which this version does not evaluate`
|
|
502
|
+
});
|
|
273
503
|
const cmp = expr.match(COMPARISON);
|
|
274
|
-
if (!cmp)
|
|
504
|
+
if (!cmp) {
|
|
505
|
+
if (combining) return arithmetic();
|
|
506
|
+
return { ok: false, reason: "not a single comparison this version understands" };
|
|
507
|
+
}
|
|
275
508
|
const value = literal(cmp[3]);
|
|
276
509
|
if (!value) {
|
|
277
510
|
const right = cmp[3].trim();
|
|
@@ -279,6 +512,7 @@ function parseCheck(expression, name) {
|
|
|
279
512
|
const op2 = cmp[2] === "!=" ? "<>" : cmp[2];
|
|
280
513
|
return { ok: true, checks: [], rows: [{ left: cmp[1], right, operator: op2, name }] };
|
|
281
514
|
}
|
|
515
|
+
if (combining) return arithmetic();
|
|
282
516
|
return { ok: false, reason: "right side is not a literal" };
|
|
283
517
|
}
|
|
284
518
|
const op = cmp[2] === "!=" ? "<>" : cmp[2];
|
|
@@ -287,68 +521,123 @@ function parseCheck(expression, name) {
|
|
|
287
521
|
checks: [{ column: cmp[1], operator: op, value: value.value, kind: value.kind, name }]
|
|
288
522
|
};
|
|
289
523
|
}
|
|
524
|
+
function wireNumberLiteral(column, value) {
|
|
525
|
+
return column.tsType === "bigint" && /^-?\d+$/.test(value) ? `${value}n` : value;
|
|
526
|
+
}
|
|
290
527
|
function describeSet(set) {
|
|
291
528
|
const shown = set.values.map((v) => set.kind === "string" ? `'${v}'` : v).join(", ");
|
|
292
529
|
return `${set.name ? `${set.name}: ` : ""}${set.column} IN (${shown})`;
|
|
293
530
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
var TS_EXTENSIONS = [
|
|
301
|
-
{ ext: ".mts", js: ".mjs", none: ".mjs" },
|
|
302
|
-
{ ext: ".cts", js: ".cjs", none: ".cjs" },
|
|
303
|
-
{ ext: ".tsx", js: ".js", none: "" },
|
|
304
|
-
{ ext: ".ts", js: ".js", none: "" }
|
|
305
|
-
];
|
|
306
|
-
function moduleFileName(tsName, fileSuffix) {
|
|
307
|
-
return `${tsName}${fileSuffix}`;
|
|
308
|
-
}
|
|
309
|
-
function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
|
|
310
|
-
for (const { ext, js, none } of TS_EXTENSIONS) {
|
|
311
|
-
if (!relativePath.endsWith(ext)) continue;
|
|
312
|
-
const stem = relativePath.slice(0, -ext.length);
|
|
313
|
-
if (importExtension === "ts") return relativePath;
|
|
314
|
-
return `${stem}${importExtension === "none" ? none : js}`;
|
|
315
|
-
}
|
|
316
|
-
return relativePath;
|
|
531
|
+
function comparisonWire(c) {
|
|
532
|
+
if (c.shape || c.arrayDimensions) return "opaque";
|
|
533
|
+
if (c.tsType === "number") return "number";
|
|
534
|
+
if (c.tsType === "bigint") return "bigint";
|
|
535
|
+
if (c.tsType !== "string") return "opaque";
|
|
536
|
+
return c.dbType === "NUMERIC" || c.dbType === "BIGINT" ? "numeric-string" : "text";
|
|
317
537
|
}
|
|
318
|
-
function
|
|
319
|
-
|
|
538
|
+
function canonicalNumericText(text) {
|
|
539
|
+
const m = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(text.trim());
|
|
540
|
+
if (!m || !m[2] && !m[3]) return void 0;
|
|
541
|
+
const int = (m[2] ?? "").replace(/^0+/, "");
|
|
542
|
+
const frac = (m[3] ?? "").replace(/0+$/, "");
|
|
543
|
+
if (!int && !frac) return "0";
|
|
544
|
+
return (m[1] === "-" ? "-" : "") + (int || "0") + (frac ? "." + frac : "");
|
|
320
545
|
}
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return importSpecifier(withTsExtension(withIndex), importExtension);
|
|
546
|
+
function canonicalMembers(values) {
|
|
547
|
+
const out = [];
|
|
548
|
+
for (const v of values) {
|
|
549
|
+
const c = canonicalNumericText(v);
|
|
550
|
+
if (c !== void 0 && !out.includes(c)) out.push(c);
|
|
327
551
|
}
|
|
328
|
-
|
|
329
|
-
const rel = import_node_path.default.relative(outDirAbs, resolved).split(import_node_path.default.sep).join("/");
|
|
330
|
-
const prefixed = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
|
|
331
|
-
return importSpecifier(withTsExtension(prefixed), importExtension);
|
|
552
|
+
return out;
|
|
332
553
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
554
|
+
var NUMERIC_CANON_NAME = "DrzlNumericCanon";
|
|
555
|
+
var NUMERIC_CANON_SOURCE = `/**
|
|
556
|
+
* The canonical spelling of the plain decimal text a numeric wire carries, or null for anything
|
|
557
|
+
* else. The driver spells one value many ways by declared scale ('1', '1.00', '1.0000000000',
|
|
558
|
+
* measured) and the database compares them as numbers, so equality is decided on this form:
|
|
559
|
+
* sign normalised, leading integer zeros and trailing fraction zeros stripped, a bare trailing
|
|
560
|
+
* dot dropped. String arithmetic on purpose: Number() is not usable here, because a numeric
|
|
561
|
+
* column carries more digits than a double holds and rounding would merge values the database
|
|
562
|
+
* keeps distinct.
|
|
563
|
+
*/
|
|
564
|
+
const ${NUMERIC_CANON_NAME} = (s: string): string | null => {
|
|
565
|
+
const m = /^([+-]?)(\\d*)(?:\\.(\\d*))?$/.exec(s.trim());
|
|
566
|
+
if (!m || (!m[2] && !m[3])) return null;
|
|
567
|
+
const int = (m[2] ?? '').replace(/^0+/, '');
|
|
568
|
+
const frac = (m[3] ?? '').replace(/0+$/, '');
|
|
569
|
+
if (!int && !frac) return '0';
|
|
570
|
+
return (m[1] === '-' ? '-' : '') + (int || '0') + (frac ? '.' + frac : '');
|
|
571
|
+
};
|
|
572
|
+
`;
|
|
573
|
+
function wireLiteralFit(c, q) {
|
|
574
|
+
const wire = comparisonWire(c);
|
|
575
|
+
if (wire === "opaque") return { fit: "keep" };
|
|
576
|
+
if (wire === "text") {
|
|
577
|
+
if (q.kind === "string") return { fit: "keep" };
|
|
578
|
+
return {
|
|
579
|
+
fit: "unenforced",
|
|
580
|
+
reason: `"${c.name ?? "?"}" is a text column, and the database compares a number literal against it by coercion rules that differ per dialect, which no exact predicate restates`
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const canon = q.values.map((v) => canonicalNumericText(v));
|
|
584
|
+
const bad = q.values[canon.findIndex((x) => x === void 0)];
|
|
585
|
+
if (wire === "numeric-string") {
|
|
586
|
+
if (bad !== void 0)
|
|
587
|
+
return {
|
|
588
|
+
fit: "unenforced",
|
|
589
|
+
reason: `'${bad}' is not plain decimal text, and the driver spells one numeric value many ways, so no exact comparison can be stated`
|
|
590
|
+
};
|
|
591
|
+
if (q.comparison === "range") return { fit: "respell", values: canon };
|
|
592
|
+
return { fit: "canonical", canon: canonicalMembers(q.values) };
|
|
593
|
+
}
|
|
594
|
+
if (q.kind === "number") return { fit: "keep" };
|
|
595
|
+
if (bad !== void 0)
|
|
596
|
+
return {
|
|
597
|
+
fit: "unenforced",
|
|
598
|
+
reason: `'${bad}' is quoted text on a ${wire} wire, and it is not plain decimal, so the numeric comparison the database performs cannot be restated exactly`
|
|
599
|
+
};
|
|
600
|
+
return { fit: "respell", values: canonicalMembers(q.values) };
|
|
337
601
|
}
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
602
|
+
function applyWirePolicy(columns, checks, sets) {
|
|
603
|
+
const byName = new Map(columns.map((c) => [c.name, c]));
|
|
604
|
+
const outChecks = [];
|
|
605
|
+
const outSets = [];
|
|
606
|
+
const unenforced = [];
|
|
607
|
+
for (const k of checks) {
|
|
608
|
+
const c = byName.get(k.column);
|
|
609
|
+
if (!c) {
|
|
610
|
+
outChecks.push(k);
|
|
611
|
+
continue;
|
|
344
612
|
}
|
|
345
|
-
|
|
613
|
+
const fit = wireLiteralFit(c, {
|
|
614
|
+
kind: k.kind,
|
|
615
|
+
values: [k.value],
|
|
616
|
+
comparison: k.operator === "=" || k.operator === "<>" ? "equality" : "range"
|
|
617
|
+
});
|
|
618
|
+
if (fit.fit === "unenforced") unenforced.push({ column: k.column, reason: fit.reason, check: k });
|
|
619
|
+
else if (fit.fit === "respell") outChecks.push({ ...k, kind: "number", value: fit.values[0] });
|
|
620
|
+
else outChecks.push(k);
|
|
346
621
|
}
|
|
622
|
+
for (const s of sets) {
|
|
623
|
+
const c = byName.get(s.column);
|
|
624
|
+
if (!c) {
|
|
625
|
+
outSets.push(s);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const fit = wireLiteralFit(c, { kind: s.kind, values: s.values, comparison: "equality" });
|
|
629
|
+
if (fit.fit === "unenforced") unenforced.push({ column: s.column, reason: fit.reason, set: s });
|
|
630
|
+
else if (fit.fit === "respell") outSets.push({ ...s, kind: "number", values: fit.values });
|
|
631
|
+
else outSets.push(s);
|
|
632
|
+
}
|
|
633
|
+
return { checks: outChecks, sets: outSets, unenforced };
|
|
347
634
|
}
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
635
|
+
function needsNumericCanon(columns, checks, sets) {
|
|
636
|
+
return columns.some(
|
|
637
|
+
(c) => comparisonWire(c) === "numeric-string" && (sets.some((s) => s.column === c.name) || checks.some(
|
|
638
|
+
(k) => k.column === c.name && (k.operator === "=" || k.operator === "<>")
|
|
639
|
+
))
|
|
640
|
+
);
|
|
352
641
|
}
|
|
353
642
|
|
|
354
643
|
// src/naming.ts
|
|
@@ -406,8 +695,14 @@ function schemaName(mode, tsName, affix) {
|
|
|
406
695
|
function typeName(mode, tsName, affix) {
|
|
407
696
|
return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
|
|
408
697
|
}
|
|
409
|
-
var
|
|
410
|
-
var
|
|
698
|
+
var ID_START = "A-Za-z_$";
|
|
699
|
+
var ID_PART = "A-Za-z0-9_$";
|
|
700
|
+
var PREFIX_BODY = `[${ID_START}][${ID_PART}]*`;
|
|
701
|
+
var SUFFIX_BODY = `[${ID_PART}]+`;
|
|
702
|
+
var PREFIX_RE = new RegExp(`^${PREFIX_BODY}$`);
|
|
703
|
+
var SUFFIX_RE = new RegExp(`^${SUFFIX_BODY}$`);
|
|
704
|
+
var AFFIX_PREFIX_PATTERN = `^(?:${PREFIX_BODY})?$`;
|
|
705
|
+
var AFFIX_SUFFIX_PATTERN = `^(?:${SUFFIX_BODY})?$`;
|
|
411
706
|
function validateAffix(affix, schemaSuffix) {
|
|
412
707
|
const issues = [];
|
|
413
708
|
if (!affix) return issues;
|
|
@@ -457,7 +752,808 @@ function validateAffix(affix, schemaSuffix) {
|
|
|
457
752
|
return issues;
|
|
458
753
|
}
|
|
459
754
|
|
|
755
|
+
// src/branding.ts
|
|
756
|
+
function resolveBranding(opt) {
|
|
757
|
+
if (!opt) return void 0;
|
|
758
|
+
if (opt === true) return { foreignKeys: true, aliases: true };
|
|
759
|
+
if (opt.enabled === false) return void 0;
|
|
760
|
+
return { foreignKeys: opt.foreignKeys !== false, aliases: opt.aliases !== false };
|
|
761
|
+
}
|
|
762
|
+
var at = (tsName, column) => `${tsName}\0${column}`;
|
|
763
|
+
function referenceOf(table, columnName) {
|
|
764
|
+
const col = table.columns.find((c) => c.name === columnName);
|
|
765
|
+
if (col?.references) return { sqlTable: col.references.table, column: col.references.column };
|
|
766
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
767
|
+
const i = fk.columns.indexOf(columnName);
|
|
768
|
+
if (i === -1) continue;
|
|
769
|
+
const target = fk.foreignColumns[i];
|
|
770
|
+
if (target === void 0) continue;
|
|
771
|
+
return { sqlTable: fk.foreignTable, column: target };
|
|
772
|
+
}
|
|
773
|
+
return void 0;
|
|
774
|
+
}
|
|
775
|
+
function isKeyColumn(table, columnName) {
|
|
776
|
+
return (table.primaryKey?.columns ?? []).includes(columnName);
|
|
777
|
+
}
|
|
778
|
+
function buildBrandPlan(tables, opt) {
|
|
779
|
+
const resolved = resolveBranding(opt);
|
|
780
|
+
if (!resolved) return void 0;
|
|
781
|
+
const notes = [];
|
|
782
|
+
const byTs = /* @__PURE__ */ new Map();
|
|
783
|
+
const dupTs = /* @__PURE__ */ new Set();
|
|
784
|
+
for (const t of tables) {
|
|
785
|
+
if (byTs.has(t.tsName)) dupTs.add(t.tsName);
|
|
786
|
+
byTs.set(t.tsName, t);
|
|
787
|
+
}
|
|
788
|
+
for (const name of dupTs) {
|
|
789
|
+
byTs.delete(name);
|
|
790
|
+
notes.push(
|
|
791
|
+
`two tables are exported as "${name}", so nothing on either is branded: a brand token is built from the export name and the two would be indistinguishable.`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const bySqlName = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const t of byTs.values()) {
|
|
796
|
+
const list2 = bySqlName.get(t.name) ?? [];
|
|
797
|
+
list2.push(t);
|
|
798
|
+
bySqlName.set(t.name, list2);
|
|
799
|
+
}
|
|
800
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
801
|
+
const cache = /* @__PURE__ */ new Map();
|
|
802
|
+
const resolveToken = (tsName, columnName) => {
|
|
803
|
+
const start = at(tsName, columnName);
|
|
804
|
+
if (cache.has(start)) return cache.get(start);
|
|
805
|
+
const origin = byTs.get(tsName)?.columns.find((c) => c.name === columnName);
|
|
806
|
+
if (!origin || origin.arrayDimensions) {
|
|
807
|
+
cache.set(start, void 0);
|
|
808
|
+
return void 0;
|
|
809
|
+
}
|
|
810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
811
|
+
let curTable = tsName;
|
|
812
|
+
let curColumn = columnName;
|
|
813
|
+
let answer;
|
|
814
|
+
let terminal;
|
|
815
|
+
for (; ; ) {
|
|
816
|
+
const key = at(curTable, curColumn);
|
|
817
|
+
if (seen.has(key)) {
|
|
818
|
+
notes.push(
|
|
819
|
+
`${tsName}.${columnName} references a cycle of foreign keys, so it is not branded.`
|
|
820
|
+
);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
seen.add(key);
|
|
824
|
+
const table = byTs.get(curTable);
|
|
825
|
+
const col = table?.columns.find((c) => c.name === curColumn);
|
|
826
|
+
if (!table || !col) break;
|
|
827
|
+
const ref = resolved.foreignKeys ? referenceOf(table, curColumn) : void 0;
|
|
828
|
+
if (ref) {
|
|
829
|
+
const candidates = bySqlName.get(ref.sqlTable) ?? [];
|
|
830
|
+
if (candidates.length !== 1) {
|
|
831
|
+
notes.push(
|
|
832
|
+
candidates.length === 0 ? `${tsName}.${columnName} references a table "${ref.sqlTable}" that is not in this analysis, so it is not branded.` : `${tsName}.${columnName} references "${ref.sqlTable}", which names more than one table here, so it is not branded.`
|
|
833
|
+
);
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
const next = candidates[0];
|
|
837
|
+
if (!next.columns.some((c) => c.name === ref.column)) {
|
|
838
|
+
notes.push(
|
|
839
|
+
`${tsName}.${columnName} references ${next.tsName}.${ref.column}, which is not a column of that table, so it is not branded.`
|
|
840
|
+
);
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
curTable = next.tsName;
|
|
844
|
+
curColumn = ref.column;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (isKeyColumn(table, curColumn)) {
|
|
848
|
+
answer = `${curTable}.${curColumn}`;
|
|
849
|
+
terminal = col;
|
|
850
|
+
}
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
if (answer && terminal && terminal.tsType !== origin.tsType) {
|
|
854
|
+
notes.push(
|
|
855
|
+
`${tsName}.${columnName} is a ${origin.tsType} and ${answer} is a ${terminal.tsType}, so it is not branded.`
|
|
856
|
+
);
|
|
857
|
+
answer = void 0;
|
|
858
|
+
}
|
|
859
|
+
cache.set(start, answer);
|
|
860
|
+
return answer;
|
|
861
|
+
};
|
|
862
|
+
for (const t of byTs.values()) {
|
|
863
|
+
for (const c of t.columns) {
|
|
864
|
+
const token = resolveToken(t.tsName, c.name);
|
|
865
|
+
if (token) tokens.set(at(t.tsName, c.name), token);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
869
|
+
if (resolved.aliases) {
|
|
870
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
871
|
+
const draft = [];
|
|
872
|
+
for (const t of byTs.values()) {
|
|
873
|
+
for (const c of t.columns) {
|
|
874
|
+
const token = tokens.get(at(t.tsName, c.name));
|
|
875
|
+
if (token !== `${t.tsName}.${c.name}`) continue;
|
|
876
|
+
const alias = pascalCase(t.tsName) + pascalCase(c.name);
|
|
877
|
+
draft.push({ tsName: t.tsName, entry: { alias, column: c.name, token } });
|
|
878
|
+
claimed.set(alias, [...claimed.get(alias) ?? [], `${t.tsName}.${c.name}`]);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
for (const { tsName, entry } of draft) {
|
|
882
|
+
const owners = claimed.get(entry.alias) ?? [];
|
|
883
|
+
if (owners.length > 1) continue;
|
|
884
|
+
aliases.set(tsName, [...aliases.get(tsName) ?? [], entry]);
|
|
885
|
+
}
|
|
886
|
+
for (const [alias, owners] of claimed) {
|
|
887
|
+
if (owners.length > 1) {
|
|
888
|
+
notes.push(
|
|
889
|
+
`${owners.join(" and ")} both name their brand alias "${alias}", so neither is exported. The schemas are unaffected; refer to the type as the select type's property instead.`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
brandOf: (tsName, columnName) => tokens.get(at(tsName, columnName)),
|
|
896
|
+
aliasesFor: (tsName) => aliases.get(tsName) ?? [],
|
|
897
|
+
any: tokens.size > 0,
|
|
898
|
+
notes
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/constraints.ts
|
|
903
|
+
function labelled(name, text) {
|
|
904
|
+
return name ? `${name}: ${text}` : text;
|
|
905
|
+
}
|
|
906
|
+
function literalText(value, kind) {
|
|
907
|
+
return kind === "string" ? `'${value}'` : value;
|
|
908
|
+
}
|
|
909
|
+
function columnCheckText(k) {
|
|
910
|
+
return labelled(k.name, `${k.column} ${k.operator} ${literalText(k.value, k.kind)}`);
|
|
911
|
+
}
|
|
912
|
+
function setText(k) {
|
|
913
|
+
return labelled(
|
|
914
|
+
k.name,
|
|
915
|
+
`${k.column} IN (${k.values.map((v) => literalText(v, k.kind)).join(", ")})`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
var lengthText = lengthCheckLabel;
|
|
919
|
+
function cardinalityText(k) {
|
|
920
|
+
return labelled(k.name, `cardinality(${k.column}) ${k.operator} ${k.value}`);
|
|
921
|
+
}
|
|
922
|
+
function rowText(k) {
|
|
923
|
+
return labelled(k.name, `${k.left} ${k.operator} ${k.right}`);
|
|
924
|
+
}
|
|
925
|
+
function nullText(k) {
|
|
926
|
+
return labelled(k.name, `${k.column} IS ${k.notNull ? "NOT NULL" : "NULL"}`);
|
|
927
|
+
}
|
|
928
|
+
function shapeArticle(c) {
|
|
929
|
+
switch (c.shape?.kind) {
|
|
930
|
+
case "json":
|
|
931
|
+
return "a JSON";
|
|
932
|
+
case "buffer":
|
|
933
|
+
return "a binary";
|
|
934
|
+
case "numberVector":
|
|
935
|
+
return "a vector";
|
|
936
|
+
case "bitstring":
|
|
937
|
+
return "a bit-string";
|
|
938
|
+
case "byteString":
|
|
939
|
+
return "a byte-string";
|
|
940
|
+
case "custom":
|
|
941
|
+
return "a customType";
|
|
942
|
+
default:
|
|
943
|
+
return "a structured";
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function takesScalarChecks(c) {
|
|
947
|
+
return !c.arrayDimensions && !c.shape;
|
|
948
|
+
}
|
|
949
|
+
function foldsIntoBounds(c, k) {
|
|
950
|
+
if (c.arrayDimensions || c.shape) return false;
|
|
951
|
+
if (c.tsType !== "number" && c.tsType !== "bigint") return false;
|
|
952
|
+
return k.kind === "number" && k.operator !== "=" && k.operator !== "<>";
|
|
953
|
+
}
|
|
954
|
+
function statesCap(c, hasSet) {
|
|
955
|
+
if (c.shape || hasSet) return false;
|
|
956
|
+
if (c.enumValues && c.enumValues.length) return false;
|
|
957
|
+
if (c.tsType !== "string") return false;
|
|
958
|
+
return !c.format;
|
|
959
|
+
}
|
|
960
|
+
function classifyTableChecks(table) {
|
|
961
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
962
|
+
const out = [];
|
|
963
|
+
for (const k of table.checks ?? []) {
|
|
964
|
+
const expression = (k.expression ?? "").trim();
|
|
965
|
+
const parsed = parseCheck(k.expression, k.name);
|
|
966
|
+
if (!parsed.ok) {
|
|
967
|
+
out.push({
|
|
968
|
+
...k.name ? { name: k.name } : {},
|
|
969
|
+
expression,
|
|
970
|
+
parts: [
|
|
971
|
+
{
|
|
972
|
+
text: labelled(k.name, expression),
|
|
973
|
+
columns: [],
|
|
974
|
+
place: "none",
|
|
975
|
+
reason: parsed.reason
|
|
976
|
+
}
|
|
977
|
+
]
|
|
978
|
+
});
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const parts = [];
|
|
982
|
+
const place = (column, text, guard, guardReason, extra = {}) => {
|
|
983
|
+
const c = byName.get(column);
|
|
984
|
+
if (!c) {
|
|
985
|
+
parts.push({
|
|
986
|
+
text,
|
|
987
|
+
columns: [column],
|
|
988
|
+
place: "none",
|
|
989
|
+
reason: `"${column}" is not a column of that table`
|
|
990
|
+
});
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (!guard(c)) {
|
|
994
|
+
parts.push({ text, columns: [column], place: "none", reason: guardReason(c) });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
parts.push({ text, columns: [column], place: "column", ...extra });
|
|
998
|
+
};
|
|
999
|
+
const notScalar = (c) => c.arrayDimensions ? `"${c.name}" is an array, and the clause describes a scalar` : `"${c.name}" is a structured column, and the clause describes a scalar`;
|
|
1000
|
+
const literalFit = (column, kind, values, comparison) => {
|
|
1001
|
+
const col = byName.get(column);
|
|
1002
|
+
return col ? wireLiteralFit(col, { kind, values, comparison }) : { fit: "keep" };
|
|
1003
|
+
};
|
|
1004
|
+
for (const c of parsed.checks) {
|
|
1005
|
+
const fit = literalFit(
|
|
1006
|
+
c.column,
|
|
1007
|
+
c.kind,
|
|
1008
|
+
[c.value],
|
|
1009
|
+
c.operator === "=" || c.operator === "<>" ? "equality" : "range"
|
|
1010
|
+
);
|
|
1011
|
+
if (fit.fit === "unenforced") {
|
|
1012
|
+
parts.push({
|
|
1013
|
+
text: columnCheckText(c),
|
|
1014
|
+
columns: [c.column],
|
|
1015
|
+
place: "none",
|
|
1016
|
+
reason: fit.reason
|
|
1017
|
+
});
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
const shown = fit.fit === "respell" ? { ...c, kind: "number", value: fit.values[0] } : c;
|
|
1021
|
+
const col = byName.get(c.column);
|
|
1022
|
+
place(c.column, columnCheckText(shown), takesScalarChecks, notScalar, {
|
|
1023
|
+
...col && foldsIntoBounds(col, shown) ? { bound: { column: c.column, operator: shown.operator, value: shown.value } } : {}
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
for (const s of parsed.sets ?? []) {
|
|
1027
|
+
const fit = literalFit(s.column, s.kind, s.values, "equality");
|
|
1028
|
+
if (fit.fit === "unenforced") {
|
|
1029
|
+
parts.push({ text: setText(s), columns: [s.column], place: "none", reason: fit.reason });
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const shown = fit.fit === "respell" ? { ...s, kind: "number", values: fit.values } : s;
|
|
1033
|
+
place(s.column, setText(shown), takesScalarChecks, notScalar, {
|
|
1034
|
+
set: { column: s.column, values: shown.values, kind: shown.kind }
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
for (const l of parsed.lengths ?? []) {
|
|
1038
|
+
place(
|
|
1039
|
+
l.column,
|
|
1040
|
+
lengthText(l),
|
|
1041
|
+
(c) => lengthMeasure(c, l) !== void 0,
|
|
1042
|
+
(c) => c.arrayDimensions ? `"${c.name}" is an array, so it has no ${l.unit === "bytes" ? "bytes" : "characters"} to count` : c.shape ? `"${c.name}" is ${shapeArticle(c)} column, whose ${l.unit === "bytes" ? "byte" : "character"} count in JavaScript is not the one the database took` : `"${c.name}" is not a string, so it has no ${l.unit === "bytes" ? "bytes" : "characters"} to count`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
for (const a of parsed.cardinalities ?? []) {
|
|
1046
|
+
place(
|
|
1047
|
+
a.column,
|
|
1048
|
+
cardinalityText(a),
|
|
1049
|
+
(c) => !!c.arrayDimensions,
|
|
1050
|
+
(c) => `"${c.name}" is not an array, so it has no elements to count`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
for (const n of parsed.nulls ?? []) {
|
|
1054
|
+
const text = nullText(n);
|
|
1055
|
+
if (!byName.has(n.column)) {
|
|
1056
|
+
parts.push({
|
|
1057
|
+
text,
|
|
1058
|
+
columns: [n.column],
|
|
1059
|
+
place: "none",
|
|
1060
|
+
reason: `"${n.column}" is not a column of that table`
|
|
1061
|
+
});
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
parts.push(
|
|
1065
|
+
n.notNull ? { text, columns: [n.column], place: "column", shape: "notNull" } : {
|
|
1066
|
+
text,
|
|
1067
|
+
columns: [n.column],
|
|
1068
|
+
place: "none",
|
|
1069
|
+
reason: "the column may hold only NULL, which these schemas do not narrow it to"
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
for (const r of parsed.rows ?? []) {
|
|
1074
|
+
const text = rowText(r);
|
|
1075
|
+
const missing = [r.left, r.right].filter((n) => !byName.has(n));
|
|
1076
|
+
parts.push(
|
|
1077
|
+
missing.length ? {
|
|
1078
|
+
text,
|
|
1079
|
+
columns: [r.left, r.right],
|
|
1080
|
+
place: "none",
|
|
1081
|
+
reason: `${missing.map((n) => `"${n}"`).join(" and ")} ${missing.length > 1 ? "are not columns" : "is not a column"} of that table`
|
|
1082
|
+
} : { text, columns: [r.left, r.right], place: "row" }
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
out.push({ ...k.name ? { name: k.name } : {}, expression, parts });
|
|
1086
|
+
}
|
|
1087
|
+
return out;
|
|
1088
|
+
}
|
|
1089
|
+
var list = (cols) => cols.join(", ");
|
|
1090
|
+
function foreignKeyRule(fk) {
|
|
1091
|
+
const target = fk.foreignSchema ? `${fk.foreignSchema}.${fk.foreignTable}` : fk.foreignTable;
|
|
1092
|
+
return `FOREIGN KEY (${list(fk.columns)}) REFERENCES ${target} (${list(fk.foreignColumns)})` + (fk.onDelete ? ` ON DELETE ${fk.onDelete}` : "") + (fk.onUpdate ? ` ON UPDATE ${fk.onUpdate}` : "");
|
|
1093
|
+
}
|
|
1094
|
+
function uniquifier() {
|
|
1095
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1096
|
+
return (id) => {
|
|
1097
|
+
const n = seen.get(id) ?? 0;
|
|
1098
|
+
seen.set(id, n + 1);
|
|
1099
|
+
return n === 0 ? id : `${id}_${n + 1}`;
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function tableConstraints(table) {
|
|
1103
|
+
const out = [];
|
|
1104
|
+
const id = uniquifier();
|
|
1105
|
+
const named = (key, fallback) => key.name ? { id: id(key.name), name: key.name } : { id: id(fallback) };
|
|
1106
|
+
if (table.primaryKey?.columns.length) {
|
|
1107
|
+
const pk = table.primaryKey;
|
|
1108
|
+
out.push({
|
|
1109
|
+
...named(pk, `${table.name}_pkey`),
|
|
1110
|
+
kind: "primaryKey",
|
|
1111
|
+
columns: [...pk.columns],
|
|
1112
|
+
rule: `PRIMARY KEY (${list(pk.columns)})`,
|
|
1113
|
+
// No per-row validator can check a key: whether a value is already taken is a fact about
|
|
1114
|
+
// the table. `duplicateFinder` checks the half that needs no database, and even that only
|
|
1115
|
+
// answers whether a batch collides with itself.
|
|
1116
|
+
enforced: false
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
for (const u of table.unique ?? []) {
|
|
1120
|
+
if (!u.columns.length) continue;
|
|
1121
|
+
out.push({
|
|
1122
|
+
...named(u, `${table.name}_${u.columns.join("_")}_key`),
|
|
1123
|
+
kind: "unique",
|
|
1124
|
+
columns: [...u.columns],
|
|
1125
|
+
rule: `UNIQUE (${list(u.columns)})`,
|
|
1126
|
+
enforced: false
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
1130
|
+
if (!fk.columns.length) continue;
|
|
1131
|
+
out.push({
|
|
1132
|
+
...named(fk, `${table.name}_${fk.columns.join("_")}_fkey`),
|
|
1133
|
+
kind: "foreignKey",
|
|
1134
|
+
columns: [...fk.columns],
|
|
1135
|
+
rule: foreignKeyRule(fk),
|
|
1136
|
+
// The referenced row either exists or it does not, and only the database knows.
|
|
1137
|
+
enforced: false,
|
|
1138
|
+
references: {
|
|
1139
|
+
table: fk.foreignTable,
|
|
1140
|
+
...fk.foreignSchema ? { schema: fk.foreignSchema } : {},
|
|
1141
|
+
columns: [...fk.foreignColumns],
|
|
1142
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
1143
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
const classified = classifyTableChecks(table);
|
|
1148
|
+
classified.forEach((k, i) => {
|
|
1149
|
+
const columns = [];
|
|
1150
|
+
for (const p of k.parts) for (const c of p.columns) if (!columns.includes(c)) columns.push(c);
|
|
1151
|
+
const messages = k.parts.filter((p) => p.place !== "none" && !p.bound && !p.set && !p.shape).map((p) => p.text);
|
|
1152
|
+
const bounds = k.parts.filter((p) => p.bound).map((p) => p.bound);
|
|
1153
|
+
const set = k.parts.find((p) => p.set)?.set;
|
|
1154
|
+
const unenforced = k.parts.filter((p) => p.place === "none").map((p) => ({ part: p.text, reason: p.reason ?? "not translated" }));
|
|
1155
|
+
out.push({
|
|
1156
|
+
...k.name ? { id: id(k.name), name: k.name } : { id: id(`${table.name}_check_${i + 1}`) },
|
|
1157
|
+
kind: "check",
|
|
1158
|
+
columns,
|
|
1159
|
+
rule: `CHECK (${k.expression})`,
|
|
1160
|
+
enforced: k.parts.some((p) => p.place !== "none"),
|
|
1161
|
+
...unenforced.length ? { unenforced } : {},
|
|
1162
|
+
...messages.length ? { messages } : {},
|
|
1163
|
+
...bounds.length ? { bounds } : {},
|
|
1164
|
+
...set ? { values: set } : {}
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
const setColumns = new Set(
|
|
1168
|
+
classified.flatMap((k) => k.parts.filter((p) => p.set).map((p) => p.set.column))
|
|
1169
|
+
);
|
|
1170
|
+
for (const c of table.columns) {
|
|
1171
|
+
if (!statesCap(c, setColumns.has(c.name))) continue;
|
|
1172
|
+
if (c.maxLength !== void 0) {
|
|
1173
|
+
const message = `at most ${c.maxLength} characters`;
|
|
1174
|
+
out.push({
|
|
1175
|
+
id: id(`${table.name}_${c.name}_maxlength`),
|
|
1176
|
+
kind: "maxLength",
|
|
1177
|
+
columns: [c.name],
|
|
1178
|
+
rule: message,
|
|
1179
|
+
enforced: true,
|
|
1180
|
+
messages: [message]
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
if (c.maxBytes !== void 0) {
|
|
1184
|
+
const message = `at most ${c.maxBytes} bytes`;
|
|
1185
|
+
out.push({
|
|
1186
|
+
id: id(`${table.name}_${c.name}_maxbytes`),
|
|
1187
|
+
kind: "maxBytes",
|
|
1188
|
+
columns: [c.name],
|
|
1189
|
+
rule: message,
|
|
1190
|
+
enforced: true,
|
|
1191
|
+
messages: [message]
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return {
|
|
1196
|
+
table: table.name,
|
|
1197
|
+
...table.schema ? { schema: table.schema } : {},
|
|
1198
|
+
constraints: out
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function resolveConstraints(opt) {
|
|
1202
|
+
if (!opt) return void 0;
|
|
1203
|
+
if (opt === true) return { errorMap: true };
|
|
1204
|
+
if (opt.enabled === false) return void 0;
|
|
1205
|
+
return { errorMap: opt.errorMap !== false };
|
|
1206
|
+
}
|
|
1207
|
+
var CONSTRAINTS_MODULE = "constraints.ts";
|
|
1208
|
+
var TYPES = `/** What a constraint is. */
|
|
1209
|
+
export type DrzlConstraintKind =
|
|
1210
|
+
| 'primaryKey'
|
|
1211
|
+
| 'unique'
|
|
1212
|
+
| 'foreignKey'
|
|
1213
|
+
| 'check'
|
|
1214
|
+
| 'maxLength'
|
|
1215
|
+
| 'maxBytes';
|
|
1216
|
+
|
|
1217
|
+
/** One constraint on one table. */
|
|
1218
|
+
export interface DrzlConstraint {
|
|
1219
|
+
/** Stable within the table. The SQL constraint name where the declaration has one. */
|
|
1220
|
+
id: string;
|
|
1221
|
+
/** The SQL constraint name, absent where the declaration did not give one. */
|
|
1222
|
+
name?: string;
|
|
1223
|
+
kind: DrzlConstraintKind;
|
|
1224
|
+
/** The columns the constraint is about, in declaration order. */
|
|
1225
|
+
columns: string[];
|
|
1226
|
+
/** The rule as a sentence, for a form with nothing better to show. */
|
|
1227
|
+
rule: string;
|
|
1228
|
+
/** Whether a generated schema can reject a row for this constraint. */
|
|
1229
|
+
enforced: boolean;
|
|
1230
|
+
/** The clauses nothing in these schemas checks, and why. */
|
|
1231
|
+
unenforced?: { part: string; reason: string }[];
|
|
1232
|
+
/** The exact messages the generated schemas attach for this constraint. */
|
|
1233
|
+
messages?: string[];
|
|
1234
|
+
/** Bounds folded into a column's range, which is where the constraint name is lost. */
|
|
1235
|
+
bounds?: { column: string; operator: string; value: string }[];
|
|
1236
|
+
/** A set of literals folded into an enum, which is the other place it is lost. */
|
|
1237
|
+
values?: { column: string; values: string[]; kind: 'number' | 'string' };
|
|
1238
|
+
/** Where a foreign key points. */
|
|
1239
|
+
references?: {
|
|
1240
|
+
table: string;
|
|
1241
|
+
schema?: string;
|
|
1242
|
+
columns: string[];
|
|
1243
|
+
onDelete?: string;
|
|
1244
|
+
onUpdate?: string;
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/** Every constraint on one table. */
|
|
1249
|
+
export interface DrzlTableConstraints {
|
|
1250
|
+
/** The SQL table name, which is not the Drizzle export name this is exported under. */
|
|
1251
|
+
table: string;
|
|
1252
|
+
/** The SQL schema, present only when the table names one. */
|
|
1253
|
+
schema?: string;
|
|
1254
|
+
constraints: DrzlConstraint[];
|
|
1255
|
+
}`;
|
|
1256
|
+
var MATCHER = `/** A validation issue traced back to the constraint that caused it. */
|
|
1257
|
+
export interface DrzlConstraintMatch {
|
|
1258
|
+
constraint: DrzlConstraint;
|
|
1259
|
+
/**
|
|
1260
|
+
* The column to put the message on.
|
|
1261
|
+
*
|
|
1262
|
+
* Taken from the issue where the library named one, and from the constraint where it did not.
|
|
1263
|
+
* Valibot reports a row-level check with an empty path, so without the fallback a form would
|
|
1264
|
+
* have a message and nowhere to show it.
|
|
1265
|
+
*/
|
|
1266
|
+
column?: string;
|
|
1267
|
+
/**
|
|
1268
|
+
* How the constraint was identified.
|
|
1269
|
+
*
|
|
1270
|
+
* \`message\` is an exact match on a string these schemas wrote, and is the only tier that is
|
|
1271
|
+
* certain. \`bound\` matched the numeric bound the library put on the issue against a bound this
|
|
1272
|
+
* constraint folded, which is what a folded CHECK leaves to match on once its name is gone.
|
|
1273
|
+
* \`column\` is the last resort: the column has exactly one constraint stated in the validator's
|
|
1274
|
+
* own vocabulary, so nothing else on the issue can be wrong about which.
|
|
1275
|
+
*/
|
|
1276
|
+
matchedBy: 'message' | 'bound' | 'column';
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* The column an issue is about, across the path shapes these libraries use.
|
|
1281
|
+
*
|
|
1282
|
+
* zod and ArkType spell a path item as the key itself; valibot spells it as an object carrying
|
|
1283
|
+
* one. The last key is taken rather than the first, so an issue inside an array or a nested
|
|
1284
|
+
* payload names the field rather than the collection holding it.
|
|
1285
|
+
*/
|
|
1286
|
+
function drzlIssueColumn(issue: any): string | undefined {
|
|
1287
|
+
const path = issue?.path;
|
|
1288
|
+
if (!Array.isArray(path)) return undefined;
|
|
1289
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
1290
|
+
const item: any = path[i];
|
|
1291
|
+
if (typeof item === 'string') return item;
|
|
1292
|
+
if (item && typeof item === 'object' && typeof item.key === 'string') return item.key;
|
|
1293
|
+
}
|
|
1294
|
+
return undefined;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* The numeric bound an issue reports, as a decimal string, or nothing.
|
|
1299
|
+
*
|
|
1300
|
+
* Measured rather than guessed: zod 4.4.3 puts \`minimum\`/\`maximum\` on a \`too_small\`/\`too_big\`
|
|
1301
|
+
* issue and valibot 1.4.2 puts \`requirement\` on a \`min_value\`/\`max_value\` one. A bigint bound
|
|
1302
|
+
* is read too, since a 64 bit column's range is not representable as a number.
|
|
1303
|
+
*/
|
|
1304
|
+
function drzlIssueBound(issue: any): string | undefined {
|
|
1305
|
+
for (const key of ['minimum', 'maximum', 'requirement']) {
|
|
1306
|
+
const v = issue?.[key];
|
|
1307
|
+
if (typeof v === 'number' || typeof v === 'bigint') return String(v);
|
|
1308
|
+
}
|
|
1309
|
+
return undefined;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* The constraint a validation issue came from, or nothing.
|
|
1314
|
+
*
|
|
1315
|
+
* Three tiers, because a constraint does not always survive into the issue in the same form. Every
|
|
1316
|
+
* constraint stated as a predicate carries a message these schemas wrote, and that is an exact
|
|
1317
|
+
* lookup. A numeric CHECK is deliberately folded into the column's own range instead, so the
|
|
1318
|
+
* failure is worded by the library and the constraint name is nowhere in it; the bound is, and it
|
|
1319
|
+
* is matched on that. A set constraint becomes an enum and leaves neither, so it is resolved by
|
|
1320
|
+
* the column alone, which is safe only because a column can carry one such constraint at most.
|
|
1321
|
+
*
|
|
1322
|
+
* The two folds are kept apart rather than pooled, and that is what stops the third tier
|
|
1323
|
+
* over-claiming. A folded bound always reports its bound, in every library measured, so an issue
|
|
1324
|
+
* on that column carrying **no** bound is not that constraint: it is the field failing to be a
|
|
1325
|
+
* number at all. Pooling them answered \`invalid_type\` on a column with a numeric CHECK with the
|
|
1326
|
+
* CHECK, which is a rule the row did not break.
|
|
1327
|
+
*
|
|
1328
|
+
* Returns nothing rather than a guess, for the same reason. A \`too_small\` reporting the column's
|
|
1329
|
+
* own type bound has no matching constraint and gets no answer.
|
|
1330
|
+
*/
|
|
1331
|
+
export function constraintForIssue(
|
|
1332
|
+
table: string,
|
|
1333
|
+
issue: unknown
|
|
1334
|
+
): DrzlConstraintMatch | undefined {
|
|
1335
|
+
const ledger = constraintsByTable[table];
|
|
1336
|
+
if (!ledger) return undefined;
|
|
1337
|
+
const raw: any = issue;
|
|
1338
|
+
const column = drzlIssueColumn(raw);
|
|
1339
|
+
const message = typeof raw?.message === 'string' ? raw.message : undefined;
|
|
1340
|
+
|
|
1341
|
+
if (message !== undefined) {
|
|
1342
|
+
for (const constraint of ledger.constraints) {
|
|
1343
|
+
if (!constraint.messages || constraint.messages.indexOf(message) < 0) continue;
|
|
1344
|
+
if (column !== undefined && constraint.columns.indexOf(column) < 0) continue;
|
|
1345
|
+
return {
|
|
1346
|
+
constraint,
|
|
1347
|
+
column: column ?? constraint.columns[0],
|
|
1348
|
+
matchedBy: 'message',
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (column === undefined) return undefined;
|
|
1354
|
+
|
|
1355
|
+
const bound = drzlIssueBound(raw);
|
|
1356
|
+
if (bound !== undefined) {
|
|
1357
|
+
const hit = ledger.constraints.find(
|
|
1358
|
+
(c) => c.bounds && c.bounds.some((b) => b.column === column && b.value === bound)
|
|
1359
|
+
);
|
|
1360
|
+
return hit ? { constraint: hit, column, matchedBy: 'bound' } : undefined;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const sets = ledger.constraints.filter((c) => c.values && c.values.column === column);
|
|
1364
|
+
return sets.length === 1 ? { constraint: sets[0], column, matchedBy: 'column' } : undefined;
|
|
1365
|
+
}`;
|
|
1366
|
+
function constName(tsName) {
|
|
1367
|
+
const safe = tsName.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
1368
|
+
return `${/^[0-9]/.test(safe) ? `_${safe}` : safe}Constraints`;
|
|
1369
|
+
}
|
|
1370
|
+
function renderConstraintsModule(tables, opts = {}) {
|
|
1371
|
+
const entries = tables.map((t) => ({ tsName: t.tsName, facts: tableConstraints(t) }));
|
|
1372
|
+
const consts = entries.map(
|
|
1373
|
+
(e) => `/** Every constraint on \`${e.facts.table}\`. */
|
|
1374
|
+
export const ${constName(e.tsName)}: DrzlTableConstraints = ${JSON.stringify(e.facts, null, 2)};`
|
|
1375
|
+
).join("\n\n");
|
|
1376
|
+
const record = `/** Every table's constraints, keyed by the Drizzle export name its schemas are named after. */
|
|
1377
|
+
export const constraintsByTable: Record<string, DrzlTableConstraints> = {
|
|
1378
|
+
` + entries.map((e) => ` ${JSON.stringify(e.tsName)}: ${constName(e.tsName)},`).join("\n") + `
|
|
1379
|
+
};`;
|
|
1380
|
+
return [
|
|
1381
|
+
"/**",
|
|
1382
|
+
" * Every CHECK, unique constraint, primary and foreign key on each table, as data.",
|
|
1383
|
+
" *",
|
|
1384
|
+
" * Generated beside the schemas rather than derived from them: a validator states what a value",
|
|
1385
|
+
" * must look like and says nothing about which constraint said so, and the two constraints a",
|
|
1386
|
+
" * per-row schema cannot check at all, uniqueness and a foreign key, are not in it in any form.",
|
|
1387
|
+
" */",
|
|
1388
|
+
TYPES,
|
|
1389
|
+
consts,
|
|
1390
|
+
record,
|
|
1391
|
+
...opts.errorMap ? [MATCHER] : []
|
|
1392
|
+
].join("\n\n");
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/emit.ts
|
|
1396
|
+
var realFs = null;
|
|
1397
|
+
function nodeFs() {
|
|
1398
|
+
return realFs ??= import("fs/promises");
|
|
1399
|
+
}
|
|
1400
|
+
function fileWriter(sink) {
|
|
1401
|
+
if (!sink) {
|
|
1402
|
+
return {
|
|
1403
|
+
async mkdir(dir, options) {
|
|
1404
|
+
return (await nodeFs()).mkdir(dir, options);
|
|
1405
|
+
},
|
|
1406
|
+
async writeFile(file, contents, encoding = "utf8") {
|
|
1407
|
+
return (await nodeFs()).writeFile(file, contents, encoding);
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
async mkdir(dir) {
|
|
1413
|
+
await sink.mkdir(dir);
|
|
1414
|
+
return void 0;
|
|
1415
|
+
},
|
|
1416
|
+
async writeFile(file, contents) {
|
|
1417
|
+
await sink.writeFile(file, contents);
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/files.ts
|
|
1423
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
1424
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1425
|
+
var IMPORT_EXTENSIONS = ["js", "none", "ts"];
|
|
1426
|
+
var DEFAULT_IMPORT_EXTENSION = "js";
|
|
1427
|
+
var TS_EXTENSIONS = [
|
|
1428
|
+
{ ext: ".mts", js: ".mjs", none: ".mjs" },
|
|
1429
|
+
{ ext: ".cts", js: ".cjs", none: ".cjs" },
|
|
1430
|
+
{ ext: ".tsx", js: ".js", none: "" },
|
|
1431
|
+
{ ext: ".ts", js: ".js", none: "" }
|
|
1432
|
+
];
|
|
1433
|
+
function moduleFileName(tsName, fileSuffix) {
|
|
1434
|
+
return `${tsName}${fileSuffix}`;
|
|
1435
|
+
}
|
|
1436
|
+
function importSpecifier(relativePath, importExtension = DEFAULT_IMPORT_EXTENSION) {
|
|
1437
|
+
for (const { ext, js, none } of TS_EXTENSIONS) {
|
|
1438
|
+
if (!relativePath.endsWith(ext)) continue;
|
|
1439
|
+
const stem = relativePath.slice(0, -ext.length);
|
|
1440
|
+
if (importExtension === "ts") return relativePath;
|
|
1441
|
+
return `${stem}${importExtension === "none" ? none : js}`;
|
|
1442
|
+
}
|
|
1443
|
+
return relativePath;
|
|
1444
|
+
}
|
|
1445
|
+
function moduleSpecifier(tsName, fileSuffix, importExtension = DEFAULT_IMPORT_EXTENSION) {
|
|
1446
|
+
return importSpecifier(`./${moduleFileName(tsName, fileSuffix)}`, importExtension);
|
|
1447
|
+
}
|
|
1448
|
+
function resolveConfiguredImport(configured, outDirAbs, cwd, importExtension = DEFAULT_IMPORT_EXTENSION) {
|
|
1449
|
+
if (isPackageSpecifier(configured)) return configured;
|
|
1450
|
+
const targetAbs = import_node_path.default.isAbsolute(configured) ? configured : import_node_path.default.resolve(configured.startsWith(".") ? outDirAbs : cwd, configured);
|
|
1451
|
+
const withIndex = pointsAtDirectory(targetAbs) ? `${configured}/index` : configured;
|
|
1452
|
+
if (configured.startsWith(".")) {
|
|
1453
|
+
return importSpecifier(withTsExtension(withIndex), importExtension);
|
|
1454
|
+
}
|
|
1455
|
+
const resolved = pointsAtDirectory(targetAbs) ? import_node_path.default.join(targetAbs, "index") : targetAbs;
|
|
1456
|
+
const rel = import_node_path.default.relative(outDirAbs, resolved).split(import_node_path.default.sep).join("/");
|
|
1457
|
+
const prefixed = !rel ? "." : rel.startsWith(".") ? rel : `./${rel}`;
|
|
1458
|
+
return importSpecifier(withTsExtension(prefixed), importExtension);
|
|
1459
|
+
}
|
|
1460
|
+
function isPackageSpecifier(p) {
|
|
1461
|
+
if (p.startsWith(".") || import_node_path.default.isAbsolute(p)) return false;
|
|
1462
|
+
if (p.startsWith("@") || p.includes(":")) return true;
|
|
1463
|
+
return !p.includes("/");
|
|
1464
|
+
}
|
|
1465
|
+
function pointsAtDirectory(absPath) {
|
|
1466
|
+
try {
|
|
1467
|
+
return import_node_fs.default.statSync(absPath).isDirectory();
|
|
1468
|
+
} catch {
|
|
1469
|
+
for (const ext of [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"]) {
|
|
1470
|
+
if (import_node_fs.default.existsSync(`${absPath}${ext}`)) return false;
|
|
1471
|
+
}
|
|
1472
|
+
return !/\.[a-z]+$/i.test(import_node_path.default.basename(absPath));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function withTsExtension(p) {
|
|
1476
|
+
if (/\.(ts|tsx|mts|cts)$/.test(p)) return p;
|
|
1477
|
+
if (/\.(js|mjs|cjs)$/.test(p)) return p.replace(/\.(js|mjs|cjs)$/, ".ts");
|
|
1478
|
+
return `${p}.ts`;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// src/meta.ts
|
|
1482
|
+
function classifyChecks(table) {
|
|
1483
|
+
const perColumn = /* @__PURE__ */ new Map();
|
|
1484
|
+
const rows = [];
|
|
1485
|
+
const unenforced = [];
|
|
1486
|
+
for (const check of classifyTableChecks(table)) {
|
|
1487
|
+
for (const part of check.parts) {
|
|
1488
|
+
if (part.place === "none") {
|
|
1489
|
+
unenforced.push(part.text);
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (part.place === "row") {
|
|
1493
|
+
rows.push(part.text);
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
const column = part.columns[0];
|
|
1497
|
+
const list2 = perColumn.get(column) ?? [];
|
|
1498
|
+
list2.push(part.text);
|
|
1499
|
+
perColumn.set(column, list2);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return { perColumn, rows, unenforced };
|
|
1503
|
+
}
|
|
1504
|
+
function columnDescription(facts) {
|
|
1505
|
+
const parts = [];
|
|
1506
|
+
if (facts.maxLength !== void 0) parts.push(`at most ${facts.maxLength} characters`);
|
|
1507
|
+
if (facts.maxBytes !== void 0) parts.push(`at most ${facts.maxBytes} bytes`);
|
|
1508
|
+
for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
|
|
1509
|
+
return parts.length ? parts.join(". ") : void 0;
|
|
1510
|
+
}
|
|
1511
|
+
function tableDescription(facts) {
|
|
1512
|
+
const parts = [];
|
|
1513
|
+
for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
|
|
1514
|
+
if (facts.unenforcedChecks?.length) {
|
|
1515
|
+
parts.push(
|
|
1516
|
+
`not enforced by this schema, the database also checks: ${facts.unenforcedChecks.join("; ")}`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
return parts.length ? parts.join(". ") : void 0;
|
|
1520
|
+
}
|
|
1521
|
+
function columnMetaFacts(column, table, opts = {}) {
|
|
1522
|
+
const checks = classifyChecks(table).perColumn.get(column.name);
|
|
1523
|
+
const facts = {
|
|
1524
|
+
...column.sqlType ? { sqlType: column.sqlType } : {},
|
|
1525
|
+
...column.maxLength !== void 0 ? { maxLength: column.maxLength } : {},
|
|
1526
|
+
...column.maxBytes !== void 0 ? { maxBytes: column.maxBytes } : {},
|
|
1527
|
+
...column.hasDefault ? { hasDefault: true } : {},
|
|
1528
|
+
...column.isGenerated ? { generated: true } : {},
|
|
1529
|
+
...checks?.length ? { checks } : {}
|
|
1530
|
+
};
|
|
1531
|
+
if (!opts.description) return facts;
|
|
1532
|
+
const description = columnDescription(facts);
|
|
1533
|
+
return description ? { ...facts, description } : facts;
|
|
1534
|
+
}
|
|
1535
|
+
function tableMetaFacts(table, opts) {
|
|
1536
|
+
const { rows, unenforced } = classifyChecks(table);
|
|
1537
|
+
const pk = table.primaryKey?.columns ?? [];
|
|
1538
|
+
const unique = (table.unique ?? []).map((k) => k.columns).filter((c) => c.length > 0);
|
|
1539
|
+
const facts = {
|
|
1540
|
+
table: table.name,
|
|
1541
|
+
...table.schema ? { schema: table.schema } : {},
|
|
1542
|
+
...opts.dialect ? { dialect: opts.dialect } : {},
|
|
1543
|
+
mode: opts.mode,
|
|
1544
|
+
...pk.length ? { primaryKey: pk } : {},
|
|
1545
|
+
...unique.length ? { unique } : {},
|
|
1546
|
+
...table.readOnly ? { readOnly: true } : {},
|
|
1547
|
+
...rows.length ? { checks: rows } : {},
|
|
1548
|
+
...unenforced.length ? { unenforcedChecks: unenforced } : {}
|
|
1549
|
+
};
|
|
1550
|
+
if (!opts.description) return facts;
|
|
1551
|
+
const description = tableDescription(facts);
|
|
1552
|
+
return description ? { ...facts, description } : facts;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
460
1555
|
// src/nested.ts
|
|
1556
|
+
var import_analyzer = require("@drzl/analyzer");
|
|
461
1557
|
var NESTED_PREFIX = "Nested";
|
|
462
1558
|
function nestedSchemaName(mode, tsName, affix) {
|
|
463
1559
|
return NESTED_PREFIX + schemaName(mode, tsName, affix);
|
|
@@ -485,13 +1581,14 @@ var KINDS_BY_MODE = {
|
|
|
485
1581
|
};
|
|
486
1582
|
var KIND_ORDER = { many: 0, manyToMany: 1, one: 2 };
|
|
487
1583
|
function omittedColumnsFor(parent, child) {
|
|
488
|
-
const
|
|
1584
|
+
const parentName = (0, import_analyzer.qualifiedTableName)(parent);
|
|
1585
|
+
const back = (child.foreignKeys ?? []).filter((fk) => (0, import_analyzer.qualifiedForeignTable)(fk) === parentName);
|
|
489
1586
|
if (back.length === 1) return { omitted: [...back[0].columns] };
|
|
490
1587
|
if (back.length === 0) return { omitted: [] };
|
|
491
1588
|
const named = back.map((fk) => fk.columns.join("+")).join(", ");
|
|
492
1589
|
return {
|
|
493
1590
|
omitted: [],
|
|
494
|
-
note: `${child.tsName} has ${back.length} foreign keys to ${
|
|
1591
|
+
note: `${child.tsName} has ${back.length} foreign keys to ${parentName} (${named}), so which one this relation uses is not stated. None were omitted: supply them yourself.`
|
|
495
1592
|
};
|
|
496
1593
|
}
|
|
497
1594
|
function buildNestedPlan(root, tables, relations, mode, depth) {
|
|
@@ -500,14 +1597,15 @@ function buildNestedPlan(root, tables, relations, mode, depth) {
|
|
|
500
1597
|
}
|
|
501
1598
|
function buildNode(table, omitted, tables, relations, mode, depth) {
|
|
502
1599
|
if (depth <= 0) return { table, omitted, arms: [] };
|
|
503
|
-
const
|
|
1600
|
+
const byName = new Map(tables.map((t) => [(0, import_analyzer.qualifiedTableName)(t), t]));
|
|
504
1601
|
const allowed = KINDS_BY_MODE[mode];
|
|
505
1602
|
const columnNames = new Set(table.columns.map((c) => c.name));
|
|
506
1603
|
const taken = /* @__PURE__ */ new Set();
|
|
507
1604
|
const arms = [];
|
|
508
|
-
const
|
|
1605
|
+
const self = (0, import_analyzer.qualifiedTableName)(table);
|
|
1606
|
+
const candidates = relations.filter((r) => r.from === self && allowed.has(r.kind)).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
|
|
509
1607
|
for (const rel of candidates) {
|
|
510
|
-
const child =
|
|
1608
|
+
const child = byName.get(rel.to);
|
|
511
1609
|
if (!child) continue;
|
|
512
1610
|
const key = child.tsName;
|
|
513
1611
|
if (columnNames.has(key)) continue;
|
|
@@ -543,7 +1641,13 @@ function nestedNodeColumns(columnsForMode, node) {
|
|
|
543
1641
|
|
|
544
1642
|
// src/duplicates.ts
|
|
545
1643
|
function usableKeys(table) {
|
|
546
|
-
|
|
1644
|
+
const keys = [];
|
|
1645
|
+
const pk = table.primaryKey;
|
|
1646
|
+
if (pk && pk.columns.length > 0) {
|
|
1647
|
+
keys.push({ name: pk.name ?? `${table.name}_pkey`, columns: pk.columns });
|
|
1648
|
+
}
|
|
1649
|
+
keys.push(...(table.unique ?? []).filter((k) => k.columns.length > 0));
|
|
1650
|
+
return keys;
|
|
547
1651
|
}
|
|
548
1652
|
function renderDuplicateFinder(table, fnName, rowType) {
|
|
549
1653
|
const keys = usableKeys(table);
|
|
@@ -553,14 +1657,15 @@ function renderDuplicateFinder(table, fnName, rowType) {
|
|
|
553
1657
|
return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k.columns)} }${i === keys.length - 1 ? "" : ","}`;
|
|
554
1658
|
}).join("\n");
|
|
555
1659
|
return `/**
|
|
556
|
-
* Rows in \`rows\` that collide with an earlier row on a unique constraint.
|
|
1660
|
+
* Rows in \`rows\` that collide with an earlier row on the primary key or a unique constraint.
|
|
557
1661
|
*
|
|
558
1662
|
* Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
|
|
559
1663
|
* checks the half that needs no database: whether the batch collides with itself. A batch that
|
|
560
1664
|
* passes here can still collide with rows already stored.
|
|
561
1665
|
*
|
|
562
1666
|
* A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
|
|
563
|
-
* where NULL is not equal to NULL and a unique index therefore permits repeats.
|
|
1667
|
+
* where NULL is not equal to NULL and a unique index therefore permits repeats. Rows that leave
|
|
1668
|
+
* a generated primary key to the database therefore report nothing on it.
|
|
564
1669
|
*/
|
|
565
1670
|
export function ${fnName}(
|
|
566
1671
|
rows: readonly ${rowType}[]
|
|
@@ -597,7 +1702,48 @@ var COLUMN_FORMATS = {
|
|
|
597
1702
|
// Sign, decimals, exponents, NaN/Infinity, surrounding whitespace, and the underscore digit
|
|
598
1703
|
// separators and 0x/0o/0b integer literals Postgres 16 added. Agrees with Postgres on all 43
|
|
599
1704
|
// probes, `1_000` and `0xDEAD_beef` through to `1__0`, `_1`, `0x` and `1e+`.
|
|
600
|
-
numeric: "^\\s*([+-]?(0[xX][0-9a-fA-F](_?[0-9a-fA-F])*|0[oO][0-7](_?[0-7])*|0[bB][01](_?[01])*)|[+-]?(\\d(_?\\d)*(\\.(\\d(_?\\d)*)?)?|\\.\\d(_?\\d)*)([eE][+-]?\\d(_?\\d)*)?|[+-]?(NaN|Infinity))\\s*$"
|
|
1705
|
+
numeric: "^\\s*([+-]?(0[xX][0-9a-fA-F](_?[0-9a-fA-F])*|0[oO][0-7](_?[0-7])*|0[bB][01](_?[01])*)|[+-]?(\\d(_?\\d)*(\\.(\\d(_?\\d)*)?)?|\\.\\d(_?\\d)*)([eE][+-]?\\d(_?\\d)*)?|[+-]?(NaN|Infinity))\\s*$",
|
|
1706
|
+
// What Postgres itself parses into a `bigint`, for a `bigint({ mode: 'string' })` column whose
|
|
1707
|
+
// value goes to the server as text. `int8in` is a pure integer parser: an optional sign against
|
|
1708
|
+
// the digits, decimal or a `0x`/`0o`/`0b` literal, single `_` separators between digits and one
|
|
1709
|
+
// permitted directly after the base prefix, leading zeros, and surrounding whitespace. Measured
|
|
1710
|
+
// against a real Postgres through PGlite over 16160 probes, boundary sweeps and random shapes:
|
|
1711
|
+
// **zero** values the server takes and this refuses.
|
|
1712
|
+
//
|
|
1713
|
+
// Two things it deliberately does not say.
|
|
1714
|
+
//
|
|
1715
|
+
// **The magnitude.** Every one of the 4474 probes this admits and the server refuses is a value
|
|
1716
|
+
// outside the signed 64 bit range, and nothing else: the syntax half is complete. The exact
|
|
1717
|
+
// bound is expressible, since leading zeros and separators make it a per-digit ladder rather
|
|
1718
|
+
// than a digit count, and it was built and verified at 16160/16160 against the server. It is
|
|
1719
|
+
// not shipped, because at 1237 characters and around twenty alternation branches it exhausts
|
|
1720
|
+
// ArkType's type-level instantiation budget: that generator states a format as a regex literal
|
|
1721
|
+
// inside the type expression, and the emitted module then fails to compile with TS2589.
|
|
1722
|
+
// Measured on arktype 2.2.3, this 101-character pattern compiles and the ladder does not, as
|
|
1723
|
+
// `COLUMN_FORMATS.numeric` at 176 characters already does not. Emitting a module that does not
|
|
1724
|
+
// typecheck is a worse failure than the bound it would buy, and the bound is unreachable by any
|
|
1725
|
+
// value the probe pools carry. The ArkType defect is already reported and carved out of the
|
|
1726
|
+
// parity gate's typecheck stage; when it is fixed, the ladder is what goes here.
|
|
1727
|
+
//
|
|
1728
|
+
// **Whitespace exactly.** Postgres pads with C `isspace`, which is the six ASCII characters, and
|
|
1729
|
+
// JS `\s` also admits NBSP and the Unicode spaces. That admits a handful of strings the server
|
|
1730
|
+
// refuses, which is the safe direction, and it is what `numeric` above already does.
|
|
1731
|
+
pgBigint: "^\\s*[+-]?(\\d(_?\\d)*|0[xX]_?[\\da-fA-F](_?[\\da-fA-F])*|0[oO]_?[0-7](_?[0-7])*|0[bB]_?[01](_?[01])*)\\s*$",
|
|
1732
|
+
// The same column on MySQL, which parses it as a *decimal number* and then rounds. Measured
|
|
1733
|
+
// against MySQL 8.4.11: `'12.5'` stores 13, `'1.5'` stores 2, `'.5'` stores 1, `'1e3'` stores
|
|
1734
|
+
// 1000, and `'92233720368547758070e-1'` stores the int64 maximum. It refuses the two spellings
|
|
1735
|
+
// Postgres takes, `'0x1f'` and `'1_000'`, both as "Data truncated". So no single pattern serves
|
|
1736
|
+
// both servers: their union admits `'12.5'` on Postgres, which is one of the fourteen values
|
|
1737
|
+
// that made this a defect, and their intersection turns away values each server really stores.
|
|
1738
|
+
//
|
|
1739
|
+
// Shared by the signed and the unsigned spelling, and this is why neither magnitude nor sign is
|
|
1740
|
+
// stated here: the value the range applies to is the *rounded* one, so the text does not
|
|
1741
|
+
// determine whether it fits. `'9223372036854775807.4'` is stored and `'9223372036854775807.6'`
|
|
1742
|
+
// is refused; on a `bigint unsigned`, `'-0.4'` and `'-1e-1'` are both stored as 0 while `'-0.5'`
|
|
1743
|
+
// is refused. A pattern cannot do that arithmetic, and guessing at it turns away working rows.
|
|
1744
|
+
// Over 3319 probes against each of a signed and an unsigned column: zero values the server takes
|
|
1745
|
+
// and this refuses.
|
|
1746
|
+
mysqlBigint: "^\\s*[+-]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d*)?\\s*$"
|
|
601
1747
|
};
|
|
602
1748
|
var COERCIBLE_DATE_STRING = "^(?!\\s*[+-])(?!\\s*\\d*\\.?\\d*(?:[eE][+-]?\\d+)?\\s*$)";
|
|
603
1749
|
function parsesToADate(expr) {
|
|
@@ -612,15 +1758,39 @@ function nonFiniteAccepted(c) {
|
|
|
612
1758
|
if (c.tsType !== "number" || c.shape) return { nan: false, infinity: false };
|
|
613
1759
|
return { nan: c.allowsNaN === true, infinity: c.allowsInfinity === true };
|
|
614
1760
|
}
|
|
1761
|
+
function nonFiniteRefused(c) {
|
|
1762
|
+
if (c.tsType !== "number" || c.shape) return { nan: false, infinity: false };
|
|
1763
|
+
return { nan: c.allowsNaN === false, infinity: c.allowsInfinity === false };
|
|
1764
|
+
}
|
|
1765
|
+
function notNullByCheck(table) {
|
|
1766
|
+
const out = /* @__PURE__ */ new Set();
|
|
1767
|
+
for (const k of table.checks ?? []) {
|
|
1768
|
+
const parsed = parseCheck(k.expression, k.name);
|
|
1769
|
+
if (!parsed.ok) continue;
|
|
1770
|
+
for (const n of parsed.nulls ?? []) if (n.notNull) out.add(n.column);
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
function withCheckNullability(table, cols) {
|
|
1775
|
+
const notNull = notNullByCheck(table);
|
|
1776
|
+
if (!notNull.size) return cols;
|
|
1777
|
+
return cols.map((c) => c.nullable && notNull.has(c.name) ? { ...c, nullable: false } : c);
|
|
1778
|
+
}
|
|
615
1779
|
function insertColumns(table) {
|
|
616
|
-
return
|
|
1780
|
+
return withCheckNullability(
|
|
1781
|
+
table,
|
|
1782
|
+
table.columns.filter((c) => !isGeneratedColumn(c))
|
|
1783
|
+
);
|
|
617
1784
|
}
|
|
618
1785
|
function updateColumns(table) {
|
|
619
1786
|
const pkCols = table.primaryKey?.columns ?? [];
|
|
620
|
-
return
|
|
1787
|
+
return withCheckNullability(
|
|
1788
|
+
table,
|
|
1789
|
+
table.columns.filter((c) => !isGeneratedColumn(c) && !pkCols.includes(c.name))
|
|
1790
|
+
);
|
|
621
1791
|
}
|
|
622
1792
|
function selectColumns(table) {
|
|
623
|
-
return table.columns;
|
|
1793
|
+
return withCheckNullability(table, table.columns);
|
|
624
1794
|
}
|
|
625
1795
|
var reportedEngines = /* @__PURE__ */ new Set();
|
|
626
1796
|
var ENGINE_PACKAGE = { prettier: "prettier", biome: "@biomejs/biome" };
|
|
@@ -646,11 +1816,30 @@ function nearestExistingDir(from) {
|
|
|
646
1816
|
dir = parent;
|
|
647
1817
|
}
|
|
648
1818
|
}
|
|
1819
|
+
function isProjectInstallPath(manifestPath) {
|
|
1820
|
+
return manifestPath.split(import_node_path2.default.sep).includes("node_modules");
|
|
1821
|
+
}
|
|
1822
|
+
function biomeManifest(startDir) {
|
|
1823
|
+
const anchors = [startDir, process.cwd()];
|
|
1824
|
+
let lastError;
|
|
1825
|
+
for (const anchor of anchors) {
|
|
1826
|
+
let resolved;
|
|
1827
|
+
try {
|
|
1828
|
+
const require_ = (0, import_node_module.createRequire)((0, import_node_url.pathToFileURL)(import_node_path2.default.join(anchor, "noop.js")));
|
|
1829
|
+
resolved = require_.resolve("@biomejs/biome/package.json");
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
lastError = err;
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
if (isProjectInstallPath(resolved)) return resolved;
|
|
1835
|
+
lastError = new Error(
|
|
1836
|
+
`@biomejs/biome resolved to ${resolved}, which is not part of this project's installed dependencies. Add @biomejs/biome to the project to format with it.`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
throw lastError ?? new Error("@biomejs/biome could not be resolved");
|
|
1840
|
+
}
|
|
649
1841
|
function biomeBinary(startDir) {
|
|
650
|
-
const
|
|
651
|
-
const manifestPath = require_.resolve("@biomejs/biome/package.json", {
|
|
652
|
-
paths: [startDir, process.cwd()]
|
|
653
|
-
});
|
|
1842
|
+
const manifestPath = biomeManifest(startDir);
|
|
654
1843
|
const manifest = JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
|
|
655
1844
|
const relative = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.biome;
|
|
656
1845
|
if (typeof relative !== "string") {
|
|
@@ -719,10 +1908,13 @@ async function formatCode(code, filePath, fmt) {
|
|
|
719
1908
|
}
|
|
720
1909
|
// Annotate the CommonJS export names for ESM import in node:
|
|
721
1910
|
0 && (module.exports = {
|
|
1911
|
+
AFFIX_PREFIX_PATTERN,
|
|
722
1912
|
AFFIX_PROBE_TABLE,
|
|
1913
|
+
AFFIX_SUFFIX_PATTERN,
|
|
723
1914
|
CODEPOINT_LENGTH,
|
|
724
1915
|
COERCIBLE_DATE_STRING,
|
|
725
1916
|
COLUMN_FORMATS,
|
|
1917
|
+
CONSTRAINTS_MODULE,
|
|
726
1918
|
DEFAULT_IMPORT_EXTENSION,
|
|
727
1919
|
DEFAULT_MODE_PREFIX,
|
|
728
1920
|
DEFAULT_NESTED_DEPTH,
|
|
@@ -732,31 +1924,54 @@ async function formatCode(code, filePath, fmt) {
|
|
|
732
1924
|
MAX_NESTED_DEPTH,
|
|
733
1925
|
NAME_MODES,
|
|
734
1926
|
NESTED_PREFIX,
|
|
1927
|
+
NUMERIC_CANON_NAME,
|
|
1928
|
+
NUMERIC_CANON_SOURCE,
|
|
735
1929
|
applyTableCase,
|
|
1930
|
+
applyWirePolicy,
|
|
1931
|
+
buildBrandPlan,
|
|
736
1932
|
buildNestedPlan,
|
|
1933
|
+
canonicalMembers,
|
|
1934
|
+
canonicalNumericText,
|
|
1935
|
+
classifyTableChecks,
|
|
1936
|
+
columnMetaFacts,
|
|
1937
|
+
comparisonWire,
|
|
737
1938
|
describeSet,
|
|
1939
|
+
fileWriter,
|
|
738
1940
|
formatCode,
|
|
739
1941
|
importSpecifier,
|
|
740
1942
|
insertColumns,
|
|
741
1943
|
isGeneratedColumn,
|
|
742
1944
|
isIntegerColumn,
|
|
1945
|
+
isProjectInstallPath,
|
|
1946
|
+
lengthCheckLabel,
|
|
1947
|
+
lengthMeasure,
|
|
1948
|
+
measureExpression,
|
|
743
1949
|
moduleFileName,
|
|
744
1950
|
moduleSpecifier,
|
|
1951
|
+
needsNumericCanon,
|
|
745
1952
|
nestedArmNotes,
|
|
746
1953
|
nestedNodeColumns,
|
|
747
1954
|
nestedSchemaName,
|
|
748
1955
|
nestedTypeName,
|
|
749
1956
|
nonFiniteAccepted,
|
|
1957
|
+
nonFiniteRefused,
|
|
750
1958
|
parseCheck,
|
|
751
1959
|
parsesToADate,
|
|
752
1960
|
pascalCase,
|
|
1961
|
+
renderConstraintsModule,
|
|
753
1962
|
renderDuplicateFinder,
|
|
754
1963
|
resolveAffix,
|
|
1964
|
+
resolveBranding,
|
|
755
1965
|
resolveConfiguredImport,
|
|
1966
|
+
resolveConstraints,
|
|
756
1967
|
resolveNestedDepth,
|
|
757
1968
|
schemaName,
|
|
758
1969
|
selectColumns,
|
|
1970
|
+
tableConstraints,
|
|
1971
|
+
tableMetaFacts,
|
|
759
1972
|
typeName,
|
|
760
1973
|
updateColumns,
|
|
761
|
-
validateAffix
|
|
1974
|
+
validateAffix,
|
|
1975
|
+
wireLiteralFit,
|
|
1976
|
+
wireNumberLiteral
|
|
762
1977
|
});
|