@drzl/validation-core 3.13.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 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,
@@ -84849,6 +84850,54 @@ function validateAffix(affix, schemaSuffix) {
84849
84850
  return issues;
84850
84851
  }
84851
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
+
84852
84901
  // src/index.ts
84853
84902
  function isGeneratedColumn(c5, _primaryKeyColumns = []) {
84854
84903
  return c5.isGenerated;
@@ -84921,6 +84970,7 @@ async function formatCode(code, filePath, fmt) {
84921
84970
  moduleSpecifier,
84922
84971
  parseCheck,
84923
84972
  pascalCase,
84973
+ renderDuplicateFinder,
84924
84974
  resolveAffix,
84925
84975
  resolveConfiguredImport,
84926
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.
@@ -284,6 +284,30 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
284
284
  */
285
285
  declare function describeSet(set: ColumnSet): string;
286
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
+
287
311
  interface Table {
288
312
  name: string;
289
313
  tsName: string;
@@ -461,4 +485,4 @@ declare function updateColumns(table: Table): Column[];
461
485
  declare function selectColumns(table: Table): Column[];
462
486
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
463
487
 
464
- 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, 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.
@@ -284,6 +284,30 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
284
284
  */
285
285
  declare function describeSet(set: ColumnSet): string;
286
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
+
287
311
  interface Table {
288
312
  name: string;
289
313
  tsName: string;
@@ -461,4 +485,4 @@ declare function updateColumns(table: Table): Column[];
461
485
  declare function selectColumns(table: Table): Column[];
462
486
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
463
487
 
464
- 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, 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
@@ -379,6 +379,54 @@ function validateAffix(affix, schemaSuffix) {
379
379
  return issues;
380
380
  }
381
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
+
382
430
  // src/index.ts
383
431
  function isGeneratedColumn(c, _primaryKeyColumns = []) {
384
432
  return c.isGenerated;
@@ -450,6 +498,7 @@ export {
450
498
  moduleSpecifier,
451
499
  parseCheck,
452
500
  pascalCase,
501
+ renderDuplicateFinder,
453
502
  resolveAffix,
454
503
  resolveConfiguredImport,
455
504
  schemaName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "3.13.0",
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.11.0"
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
  }