@drzl/validation-core 3.12.0 → 3.14.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/dist/index.cjs +71 -1
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +70 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -84460,6 +84460,7 @@ __export(index_exports2, {
|
|
|
84460
84460
|
moduleSpecifier: () => moduleSpecifier,
|
|
84461
84461
|
parseCheck: () => parseCheck,
|
|
84462
84462
|
pascalCase: () => pascalCase,
|
|
84463
|
+
renderDuplicateFinder: () => renderDuplicateFinder,
|
|
84463
84464
|
resolveAffix: () => resolveAffix,
|
|
84464
84465
|
resolveConfiguredImport: () => resolveConfiguredImport,
|
|
84465
84466
|
schemaName: () => schemaName,
|
|
@@ -84473,6 +84474,7 @@ module.exports = __toCommonJS(index_exports2);
|
|
|
84473
84474
|
// src/checks.ts
|
|
84474
84475
|
var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
|
|
84475
84476
|
var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
|
|
84477
|
+
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;
|
|
84476
84478
|
var LENGTH_OF = /^\s*(?:length|char_length)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
|
|
84477
84479
|
function splitTopLevelAnd(expr) {
|
|
84478
84480
|
const parts = [];
|
|
@@ -84595,6 +84597,7 @@ function parseCheck(expression, name) {
|
|
|
84595
84597
|
const sets = [];
|
|
84596
84598
|
const rows = [];
|
|
84597
84599
|
const lengths = [];
|
|
84600
|
+
const cardinalities = [];
|
|
84598
84601
|
for (const part of parts) {
|
|
84599
84602
|
const parsed = parseCheck(part, name);
|
|
84600
84603
|
if (!parsed.ok)
|
|
@@ -84603,13 +84606,15 @@ function parseCheck(expression, name) {
|
|
|
84603
84606
|
if (parsed.sets) sets.push(...parsed.sets);
|
|
84604
84607
|
if (parsed.rows) rows.push(...parsed.rows);
|
|
84605
84608
|
if (parsed.lengths) lengths.push(...parsed.lengths);
|
|
84609
|
+
if (parsed.cardinalities) cardinalities.push(...parsed.cardinalities);
|
|
84606
84610
|
}
|
|
84607
84611
|
return {
|
|
84608
84612
|
ok: true,
|
|
84609
84613
|
checks,
|
|
84610
84614
|
...sets.length ? { sets } : {},
|
|
84611
84615
|
...rows.length ? { rows } : {},
|
|
84612
|
-
...lengths.length ? { lengths } : {}
|
|
84616
|
+
...lengths.length ? { lengths } : {},
|
|
84617
|
+
...cardinalities.length ? { cardinalities } : {}
|
|
84613
84618
|
};
|
|
84614
84619
|
}
|
|
84615
84620
|
const lengthOf = expr.match(LENGTH_OF);
|
|
@@ -84621,6 +84626,22 @@ function parseCheck(expression, name) {
|
|
|
84621
84626
|
lengths: [{ column: lengthOf[1], operator: op4, value: lengthOf[3], name }]
|
|
84622
84627
|
};
|
|
84623
84628
|
}
|
|
84629
|
+
const cardinalityOf = expr.match(CARDINALITY_OF);
|
|
84630
|
+
if (cardinalityOf) {
|
|
84631
|
+
const op4 = cardinalityOf[3] === "!=" ? "<>" : cardinalityOf[3];
|
|
84632
|
+
return {
|
|
84633
|
+
ok: true,
|
|
84634
|
+
checks: [],
|
|
84635
|
+
cardinalities: [
|
|
84636
|
+
{
|
|
84637
|
+
column: cardinalityOf[1] ?? cardinalityOf[2],
|
|
84638
|
+
operator: op4,
|
|
84639
|
+
value: cardinalityOf[4],
|
|
84640
|
+
...name ? { name } : {}
|
|
84641
|
+
}
|
|
84642
|
+
]
|
|
84643
|
+
};
|
|
84644
|
+
}
|
|
84624
84645
|
const inList = expr.match(IN_LIST);
|
|
84625
84646
|
if (inList) {
|
|
84626
84647
|
const raw = splitTopLevelCommas(inList[2]);
|
|
@@ -84829,6 +84850,54 @@ function validateAffix(affix, schemaSuffix) {
|
|
|
84829
84850
|
return issues;
|
|
84830
84851
|
}
|
|
84831
84852
|
|
|
84853
|
+
// src/duplicates.ts
|
|
84854
|
+
function usableKeys(table) {
|
|
84855
|
+
return (table.unique ?? []).filter((k5) => k5.columns.length > 0);
|
|
84856
|
+
}
|
|
84857
|
+
function renderDuplicateFinder(table, fnName, rowType) {
|
|
84858
|
+
const keys = usableKeys(table);
|
|
84859
|
+
if (!keys.length) return void 0;
|
|
84860
|
+
const constraints = keys.map((k5, i) => {
|
|
84861
|
+
const name = k5.name ?? k5.columns.join("_");
|
|
84862
|
+
return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k5.columns)} }${i === keys.length - 1 ? "" : ","}`;
|
|
84863
|
+
}).join("\n");
|
|
84864
|
+
return `/**
|
|
84865
|
+
* Rows in \`rows\` that collide with an earlier row on a unique constraint.
|
|
84866
|
+
*
|
|
84867
|
+
* Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
|
|
84868
|
+
* checks the half that needs no database: whether the batch collides with itself. A batch that
|
|
84869
|
+
* passes here can still collide with rows already stored.
|
|
84870
|
+
*
|
|
84871
|
+
* A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
|
|
84872
|
+
* where NULL is not equal to NULL and a unique index therefore permits repeats.
|
|
84873
|
+
*/
|
|
84874
|
+
export function ${fnName}(
|
|
84875
|
+
rows: readonly ${rowType}[]
|
|
84876
|
+
): Array<{ index: number; constraint: string; firstIndex: number }> {
|
|
84877
|
+
const constraints = [
|
|
84878
|
+
${constraints}
|
|
84879
|
+
] as const;
|
|
84880
|
+
const seen = constraints.map(() => new Map<string, number>());
|
|
84881
|
+
const out: Array<{ index: number; constraint: string; firstIndex: number }> = [];
|
|
84882
|
+
|
|
84883
|
+
for (let i = 0; i < rows.length; i++) {
|
|
84884
|
+
const row = rows[i] as Record<string, unknown>;
|
|
84885
|
+
for (let c = 0; c < constraints.length; c++) {
|
|
84886
|
+
const cols = constraints[c].columns;
|
|
84887
|
+
const values = cols.map((col) => row?.[col]);
|
|
84888
|
+
if (values.some((v) => v === null || v === undefined)) continue;
|
|
84889
|
+
// JSON, so a composite key compares by value and \`[1, "2"]\` never collides with
|
|
84890
|
+
// \`["1", 2]\`. A join on a separator would.
|
|
84891
|
+
const key = JSON.stringify(values);
|
|
84892
|
+
const first = seen[c].get(key);
|
|
84893
|
+
if (first === undefined) seen[c].set(key, i);
|
|
84894
|
+
else out.push({ index: i, constraint: constraints[c].name, firstIndex: first });
|
|
84895
|
+
}
|
|
84896
|
+
}
|
|
84897
|
+
return out;
|
|
84898
|
+
}`;
|
|
84899
|
+
}
|
|
84900
|
+
|
|
84832
84901
|
// src/index.ts
|
|
84833
84902
|
function isGeneratedColumn(c5, _primaryKeyColumns = []) {
|
|
84834
84903
|
return c5.isGenerated;
|
|
@@ -84901,6 +84970,7 @@ async function formatCode(code, filePath, fmt) {
|
|
|
84901
84970
|
moduleSpecifier,
|
|
84902
84971
|
parseCheck,
|
|
84903
84972
|
pascalCase,
|
|
84973
|
+
renderDuplicateFinder,
|
|
84904
84974
|
resolveAffix,
|
|
84905
84975
|
resolveConfiguredImport,
|
|
84906
84976
|
schemaName,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Column, Analysis } from '@drzl/analyzer';
|
|
1
|
+
import { Table as Table$1, Column, Analysis } from '@drzl/analyzer';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* One place that decides what a generated module is called, on disk and in an import.
|
|
@@ -244,6 +244,18 @@ interface LengthCheck {
|
|
|
244
244
|
value: string;
|
|
245
245
|
name?: string;
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* A constraint on an array column's element count, from `CHECK (cardinality(tags) > 0)`.
|
|
249
|
+
*
|
|
250
|
+
* The array analogue of `LengthCheck`, and free of the question that one carries: an element
|
|
251
|
+
* count is the same number in SQL and in JavaScript, with no encoding involved.
|
|
252
|
+
*/
|
|
253
|
+
interface CardinalityCheck {
|
|
254
|
+
column: string;
|
|
255
|
+
operator: ColumnCheck['operator'];
|
|
256
|
+
value: string;
|
|
257
|
+
name?: string;
|
|
258
|
+
}
|
|
247
259
|
/** A check that was understood, or the reason it was not. */
|
|
248
260
|
type ParsedCheck = {
|
|
249
261
|
ok: true;
|
|
@@ -251,6 +263,7 @@ type ParsedCheck = {
|
|
|
251
263
|
sets?: ColumnSet[];
|
|
252
264
|
rows?: RowCheck[];
|
|
253
265
|
lengths?: LengthCheck[];
|
|
266
|
+
cardinalities?: CardinalityCheck[];
|
|
254
267
|
} | {
|
|
255
268
|
ok: false;
|
|
256
269
|
reason: string;
|
|
@@ -271,6 +284,30 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
|
|
|
271
284
|
*/
|
|
272
285
|
declare function describeSet(set: ColumnSet): string;
|
|
273
286
|
|
|
287
|
+
/**
|
|
288
|
+
* A duplicate finder for a batch of rows, emitted beside the schemas.
|
|
289
|
+
*
|
|
290
|
+
* A validator checks one row at a time, so uniqueness is the one constraint it structurally
|
|
291
|
+
* cannot see: whether a value is unique is a fact about the table, not about the row. That is
|
|
292
|
+
* fine for a single insert, where the database answers immediately. It is not fine for a bulk
|
|
293
|
+
* insert, where a batch of a thousand rows fails whole on one collision and the error names a
|
|
294
|
+
* constraint rather than a row.
|
|
295
|
+
*
|
|
296
|
+
* What *is* checkable without the database is whether a batch collides with **itself**, and that
|
|
297
|
+
* is worth checking, because it is the half a user can fix before sending anything.
|
|
298
|
+
*
|
|
299
|
+
* The emitted function is plain TypeScript with no reference to any validation library, so all
|
|
300
|
+
* four generators emit the same thing. It is rendered from one place for that reason.
|
|
301
|
+
*/
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* `findDuplicate<Table>s` for a table with unique constraints, or nothing.
|
|
305
|
+
*
|
|
306
|
+
* `rowType` is the name of the insert type, which is what a caller has in hand before an insert.
|
|
307
|
+
* Passed in rather than derived, because each generator names its types differently.
|
|
308
|
+
*/
|
|
309
|
+
declare function renderDuplicateFinder(table: Table$1, fnName: string, rowType: string): string | undefined;
|
|
310
|
+
|
|
274
311
|
interface Table {
|
|
275
312
|
name: string;
|
|
276
313
|
tsName: string;
|
|
@@ -448,4 +485,4 @@ declare function updateColumns(table: Table): Column[];
|
|
|
448
485
|
declare function selectColumns(table: Table): Column[];
|
|
449
486
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
450
487
|
|
|
451
|
-
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COLUMN_FORMATS, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, parseCheck, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
|
488
|
+
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, parseCheck, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Column, Analysis } from '@drzl/analyzer';
|
|
1
|
+
import { Table as Table$1, Column, Analysis } from '@drzl/analyzer';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* One place that decides what a generated module is called, on disk and in an import.
|
|
@@ -244,6 +244,18 @@ interface LengthCheck {
|
|
|
244
244
|
value: string;
|
|
245
245
|
name?: string;
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* A constraint on an array column's element count, from `CHECK (cardinality(tags) > 0)`.
|
|
249
|
+
*
|
|
250
|
+
* The array analogue of `LengthCheck`, and free of the question that one carries: an element
|
|
251
|
+
* count is the same number in SQL and in JavaScript, with no encoding involved.
|
|
252
|
+
*/
|
|
253
|
+
interface CardinalityCheck {
|
|
254
|
+
column: string;
|
|
255
|
+
operator: ColumnCheck['operator'];
|
|
256
|
+
value: string;
|
|
257
|
+
name?: string;
|
|
258
|
+
}
|
|
247
259
|
/** A check that was understood, or the reason it was not. */
|
|
248
260
|
type ParsedCheck = {
|
|
249
261
|
ok: true;
|
|
@@ -251,6 +263,7 @@ type ParsedCheck = {
|
|
|
251
263
|
sets?: ColumnSet[];
|
|
252
264
|
rows?: RowCheck[];
|
|
253
265
|
lengths?: LengthCheck[];
|
|
266
|
+
cardinalities?: CardinalityCheck[];
|
|
254
267
|
} | {
|
|
255
268
|
ok: false;
|
|
256
269
|
reason: string;
|
|
@@ -271,6 +284,30 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
|
|
|
271
284
|
*/
|
|
272
285
|
declare function describeSet(set: ColumnSet): string;
|
|
273
286
|
|
|
287
|
+
/**
|
|
288
|
+
* A duplicate finder for a batch of rows, emitted beside the schemas.
|
|
289
|
+
*
|
|
290
|
+
* A validator checks one row at a time, so uniqueness is the one constraint it structurally
|
|
291
|
+
* cannot see: whether a value is unique is a fact about the table, not about the row. That is
|
|
292
|
+
* fine for a single insert, where the database answers immediately. It is not fine for a bulk
|
|
293
|
+
* insert, where a batch of a thousand rows fails whole on one collision and the error names a
|
|
294
|
+
* constraint rather than a row.
|
|
295
|
+
*
|
|
296
|
+
* What *is* checkable without the database is whether a batch collides with **itself**, and that
|
|
297
|
+
* is worth checking, because it is the half a user can fix before sending anything.
|
|
298
|
+
*
|
|
299
|
+
* The emitted function is plain TypeScript with no reference to any validation library, so all
|
|
300
|
+
* four generators emit the same thing. It is rendered from one place for that reason.
|
|
301
|
+
*/
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* `findDuplicate<Table>s` for a table with unique constraints, or nothing.
|
|
305
|
+
*
|
|
306
|
+
* `rowType` is the name of the insert type, which is what a caller has in hand before an insert.
|
|
307
|
+
* Passed in rather than derived, because each generator names its types differently.
|
|
308
|
+
*/
|
|
309
|
+
declare function renderDuplicateFinder(table: Table$1, fnName: string, rowType: string): string | undefined;
|
|
310
|
+
|
|
274
311
|
interface Table {
|
|
275
312
|
name: string;
|
|
276
313
|
tsName: string;
|
|
@@ -448,4 +485,4 @@ declare function updateColumns(table: Table): Column[];
|
|
|
448
485
|
declare function selectColumns(table: Table): Column[];
|
|
449
486
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
450
487
|
|
|
451
|
-
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COLUMN_FORMATS, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, parseCheck, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
|
488
|
+
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, parseCheck, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import "./chunk-ZZGILH5A.js";
|
|
|
3
3
|
// src/checks.ts
|
|
4
4
|
var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
|
|
5
5
|
var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
|
|
6
|
+
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;
|
|
6
7
|
var LENGTH_OF = /^\s*(?:length|char_length)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(>=|<=|<>|!=|>|<|=)\s*(\d+)\s*$/i;
|
|
7
8
|
function splitTopLevelAnd(expr) {
|
|
8
9
|
const parts = [];
|
|
@@ -125,6 +126,7 @@ function parseCheck(expression, name) {
|
|
|
125
126
|
const sets = [];
|
|
126
127
|
const rows = [];
|
|
127
128
|
const lengths = [];
|
|
129
|
+
const cardinalities = [];
|
|
128
130
|
for (const part of parts) {
|
|
129
131
|
const parsed = parseCheck(part, name);
|
|
130
132
|
if (!parsed.ok)
|
|
@@ -133,13 +135,15 @@ function parseCheck(expression, name) {
|
|
|
133
135
|
if (parsed.sets) sets.push(...parsed.sets);
|
|
134
136
|
if (parsed.rows) rows.push(...parsed.rows);
|
|
135
137
|
if (parsed.lengths) lengths.push(...parsed.lengths);
|
|
138
|
+
if (parsed.cardinalities) cardinalities.push(...parsed.cardinalities);
|
|
136
139
|
}
|
|
137
140
|
return {
|
|
138
141
|
ok: true,
|
|
139
142
|
checks,
|
|
140
143
|
...sets.length ? { sets } : {},
|
|
141
144
|
...rows.length ? { rows } : {},
|
|
142
|
-
...lengths.length ? { lengths } : {}
|
|
145
|
+
...lengths.length ? { lengths } : {},
|
|
146
|
+
...cardinalities.length ? { cardinalities } : {}
|
|
143
147
|
};
|
|
144
148
|
}
|
|
145
149
|
const lengthOf = expr.match(LENGTH_OF);
|
|
@@ -151,6 +155,22 @@ function parseCheck(expression, name) {
|
|
|
151
155
|
lengths: [{ column: lengthOf[1], operator: op2, value: lengthOf[3], name }]
|
|
152
156
|
};
|
|
153
157
|
}
|
|
158
|
+
const cardinalityOf = expr.match(CARDINALITY_OF);
|
|
159
|
+
if (cardinalityOf) {
|
|
160
|
+
const op2 = cardinalityOf[3] === "!=" ? "<>" : cardinalityOf[3];
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
checks: [],
|
|
164
|
+
cardinalities: [
|
|
165
|
+
{
|
|
166
|
+
column: cardinalityOf[1] ?? cardinalityOf[2],
|
|
167
|
+
operator: op2,
|
|
168
|
+
value: cardinalityOf[4],
|
|
169
|
+
...name ? { name } : {}
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
154
174
|
const inList = expr.match(IN_LIST);
|
|
155
175
|
if (inList) {
|
|
156
176
|
const raw = splitTopLevelCommas(inList[2]);
|
|
@@ -359,6 +379,54 @@ function validateAffix(affix, schemaSuffix) {
|
|
|
359
379
|
return issues;
|
|
360
380
|
}
|
|
361
381
|
|
|
382
|
+
// src/duplicates.ts
|
|
383
|
+
function usableKeys(table) {
|
|
384
|
+
return (table.unique ?? []).filter((k) => k.columns.length > 0);
|
|
385
|
+
}
|
|
386
|
+
function renderDuplicateFinder(table, fnName, rowType) {
|
|
387
|
+
const keys = usableKeys(table);
|
|
388
|
+
if (!keys.length) return void 0;
|
|
389
|
+
const constraints = keys.map((k, i) => {
|
|
390
|
+
const name = k.name ?? k.columns.join("_");
|
|
391
|
+
return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k.columns)} }${i === keys.length - 1 ? "" : ","}`;
|
|
392
|
+
}).join("\n");
|
|
393
|
+
return `/**
|
|
394
|
+
* Rows in \`rows\` that collide with an earlier row on a unique constraint.
|
|
395
|
+
*
|
|
396
|
+
* Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
|
|
397
|
+
* checks the half that needs no database: whether the batch collides with itself. A batch that
|
|
398
|
+
* passes here can still collide with rows already stored.
|
|
399
|
+
*
|
|
400
|
+
* A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
|
|
401
|
+
* where NULL is not equal to NULL and a unique index therefore permits repeats.
|
|
402
|
+
*/
|
|
403
|
+
export function ${fnName}(
|
|
404
|
+
rows: readonly ${rowType}[]
|
|
405
|
+
): Array<{ index: number; constraint: string; firstIndex: number }> {
|
|
406
|
+
const constraints = [
|
|
407
|
+
${constraints}
|
|
408
|
+
] as const;
|
|
409
|
+
const seen = constraints.map(() => new Map<string, number>());
|
|
410
|
+
const out: Array<{ index: number; constraint: string; firstIndex: number }> = [];
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < rows.length; i++) {
|
|
413
|
+
const row = rows[i] as Record<string, unknown>;
|
|
414
|
+
for (let c = 0; c < constraints.length; c++) {
|
|
415
|
+
const cols = constraints[c].columns;
|
|
416
|
+
const values = cols.map((col) => row?.[col]);
|
|
417
|
+
if (values.some((v) => v === null || v === undefined)) continue;
|
|
418
|
+
// JSON, so a composite key compares by value and \`[1, "2"]\` never collides with
|
|
419
|
+
// \`["1", 2]\`. A join on a separator would.
|
|
420
|
+
const key = JSON.stringify(values);
|
|
421
|
+
const first = seen[c].get(key);
|
|
422
|
+
if (first === undefined) seen[c].set(key, i);
|
|
423
|
+
else out.push({ index: i, constraint: constraints[c].name, firstIndex: first });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
362
430
|
// src/index.ts
|
|
363
431
|
function isGeneratedColumn(c, _primaryKeyColumns = []) {
|
|
364
432
|
return c.isGenerated;
|
|
@@ -430,6 +498,7 @@ export {
|
|
|
430
498
|
moduleSpecifier,
|
|
431
499
|
parseCheck,
|
|
432
500
|
pascalCase,
|
|
501
|
+
renderDuplicateFinder,
|
|
433
502
|
resolveAffix,
|
|
434
503
|
resolveConfiguredImport,
|
|
435
504
|
schemaName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drzl/validation-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@drzl/analyzer": "^1.
|
|
14
|
+
"@drzl/analyzer": "^1.14.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"tsup": "^8.5.1",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsup src/index.ts --dts --format esm,cjs",
|
|
39
|
-
"test": "vitest run"
|
|
39
|
+
"test": "vitest run --testTimeout=20000"
|
|
40
40
|
}
|
|
41
41
|
}
|