uql-orm 0.80.0 → 0.82.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.
Files changed (123) hide show
  1. package/README.md +3 -3
  2. package/dist/browser/querier/httpQuerier.d.ts +2 -2
  3. package/dist/browser/querier/httpQuerier.js +2 -1
  4. package/dist/browser/type/clientQuerier.d.ts +2 -2
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +9 -8
  7. package/dist/bunSql/bunSql.util.js +2 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +15 -5
  9. package/dist/dialect/abstractSqlDialect.js +109 -40
  10. package/dist/dialect/operators.d.ts +7 -1
  11. package/dist/dialect/operators.js +13 -1
  12. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  13. package/dist/dialect/vectorSqlDialect.js +4 -0
  14. package/dist/entity/metadata/definition.d.ts +1 -2
  15. package/dist/entity/metadata/definition.js +37 -39
  16. package/dist/http/handler.js +5 -4
  17. package/dist/http/query.d.ts +1 -1
  18. package/dist/http/query.js +2 -2
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  22. package/dist/migrate/assertCliConfig.js +7 -6
  23. package/dist/migrate/bin.js +0 -0
  24. package/dist/migrate/builder/expressions.d.ts +2 -0
  25. package/dist/migrate/builder/expressions.js +26 -1
  26. package/dist/migrate/cli-config.js +5 -4
  27. package/dist/migrate/cli.js +1 -1
  28. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  30. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  31. package/dist/migrate/ddl/indexDdl.js +4 -3
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +12 -10
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +10 -7
  38. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  39. package/dist/migrate/ddl/tableDdl.d.ts +3 -2
  40. package/dist/migrate/ddl/tableDdl.js +11 -8
  41. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  42. package/dist/migrate/drift/driftDetector.js +21 -21
  43. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  44. package/dist/migrate/generator/definitionToNode.js +9 -20
  45. package/dist/migrate/generator/mongoCommand.js +2 -1
  46. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  47. package/dist/migrate/generator/mongoSchemaGenerator.js +17 -24
  48. package/dist/migrate/index.d.ts +2 -1
  49. package/dist/migrate/index.js +1 -0
  50. package/dist/migrate/indexPredicate.js +2 -1
  51. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
  52. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -19
  53. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  54. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  55. package/dist/migrate/introspection/mongoIntrospector.js +5 -4
  56. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  57. package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
  58. package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
  59. package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
  60. package/dist/migrate/introspection/postgresIntrospector.js +63 -46
  61. package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
  62. package/dist/migrate/migrationTarget.js +2 -1
  63. package/dist/migrate/migrator.d.ts +5 -0
  64. package/dist/migrate/migrator.js +35 -45
  65. package/dist/migrate/schemaChange.d.ts +18 -0
  66. package/dist/migrate/schemaChange.js +37 -0
  67. package/dist/migrate/schemaGenerator.d.ts +13 -14
  68. package/dist/migrate/schemaGenerator.js +88 -181
  69. package/dist/migrate/triggerSql.d.ts +1 -1
  70. package/dist/migrate/triggerSql.js +77 -61
  71. package/dist/mongo/mongoDialect.d.ts +1 -3
  72. package/dist/mongo/mongoDialect.js +9 -14
  73. package/dist/mongo/mongodbQuerier.js +3 -7
  74. package/dist/querier/abstractQuerier.d.ts +9 -4
  75. package/dist/querier/abstractQuerier.js +26 -19
  76. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  77. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  78. package/dist/querier/abstractSqlQuerier.js +1 -1
  79. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  80. package/dist/querier/queryError.d.ts +2 -2
  81. package/dist/schema/indexDifferences.d.ts +22 -6
  82. package/dist/schema/indexDifferences.js +23 -8
  83. package/dist/schema/matchByKey.d.ts +10 -0
  84. package/dist/schema/matchByKey.js +18 -0
  85. package/dist/schema/schemaAST.d.ts +6 -2
  86. package/dist/schema/schemaAST.js +7 -3
  87. package/dist/schema/schemaASTBuilder.d.ts +2 -0
  88. package/dist/schema/schemaASTBuilder.js +17 -11
  89. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  90. package/dist/schema/schemaASTDiffer.js +15 -36
  91. package/dist/schema/types.d.ts +14 -15
  92. package/dist/type/dialect.d.ts +23 -1
  93. package/dist/type/entity.d.ts +15 -11
  94. package/dist/type/logger.d.ts +2 -2
  95. package/dist/type/migration.d.ts +32 -50
  96. package/dist/type/querier.d.ts +3 -3
  97. package/dist/type/query.d.ts +3 -13
  98. package/dist/type/queryAggregate.d.ts +4 -10
  99. package/dist/type/queryRaw.d.ts +17 -3
  100. package/dist/type/queryRaw.js +2 -1
  101. package/dist/type/queryWhere.d.ts +7 -7
  102. package/dist/type/universalQuerier.d.ts +3 -3
  103. package/dist/type/vector.d.ts +2 -1
  104. package/dist/type/vector.js +2 -1
  105. package/dist/util/ddlExpression.util.d.ts +5 -1
  106. package/dist/util/ddlExpression.util.js +6 -2
  107. package/dist/util/dialect.util.d.ts +13 -5
  108. package/dist/util/dialect.util.js +28 -20
  109. package/dist/util/field.util.d.ts +4 -4
  110. package/dist/util/field.util.js +10 -2
  111. package/dist/util/hook.util.d.ts +1 -1
  112. package/dist/util/hook.util.js +8 -1
  113. package/dist/util/index.d.ts +1 -0
  114. package/dist/util/index.js +1 -0
  115. package/dist/util/logger.d.ts +3 -3
  116. package/dist/util/object.util.js +3 -2
  117. package/dist/util/raw.d.ts +6 -7
  118. package/dist/util/raw.js +10 -12
  119. package/dist/util/sqlLiteral.js +3 -2
  120. package/dist/util/triggerWrite.d.ts +15 -0
  121. package/dist/util/triggerWrite.js +20 -0
  122. package/package.json +1 -1
  123. package/skills/uql-orm/SKILL.md +3 -3
@@ -1,10 +1,12 @@
1
+ import type { IndexSchema } from '../type/index.js';
1
2
  import type { IndexNode } from './types.js';
2
3
  /**
3
4
  * What an introspector reports about an index, and so all a diff may compare; apart from `IndexFeature`, what an engine emits.
4
- * `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it.
5
+ * `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it;
6
+ * `distance` is the metric a vector index was built for.
5
7
  * `textIndex` is a text index's weights and language, kept by an engine that lists its fields in no declared order.
6
8
  */
7
- export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'textIndex';
9
+ export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'distance' | 'textIndex';
8
10
  type ComparableIndex = Pick<IndexNode, 'name' | 'entries' | 'unique'>;
9
11
  /**
10
12
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
@@ -17,16 +19,30 @@ export declare function indexSignature(index: ComparableIndex): string;
17
19
  */
18
20
  export declare function indexNameStem(name: string): string;
19
21
  /**
20
- * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
21
- * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
22
+ * Pairs by name, then what is left by shape: an index the database has under another name is still
23
+ * the one asked for. What stays unpaired is created or dropped.
22
24
  */
23
- export declare function indexChanges<I extends ComparableIndex>(table: string, declared: readonly I[], current: readonly IndexNode[], keyOf?: (index: ComparableIndex) => string): {
25
+ export declare function pairIndexes<S extends ComparableIndex, T extends ComparableIndex>(source: readonly S[], target: readonly T[], normalizeName?: (name: string) => string): {
26
+ created: S[];
27
+ dropped: T[];
28
+ matched: (readonly [S, T])[];
29
+ };
30
+ /**
31
+ * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
32
+ * `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
33
+ * dropped: any other may have been made outside the ORM.
34
+ */
35
+ export declare function indexChanges<I extends IndexSchema>(table: string, declared: readonly I[], current: readonly IndexNode[], facets: ReadonlySet<IndexFacet>): {
24
36
  toAdd: I[];
25
37
  toDrop: IndexNode[];
38
+ toAlter: {
39
+ from: IndexNode;
40
+ to: I;
41
+ }[];
26
42
  };
27
43
  /**
28
44
  * What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
29
45
  * path or a predicate is reprinted by the engine, so never compared.
30
46
  */
31
- export declare function describeIndexDifferences(source: IndexNode, target: IndexNode, facets: ReadonlySet<IndexFacet>): string[];
47
+ export declare function describeIndexDifferences(source: IndexSchema, target: IndexSchema, facets: ReadonlySet<IndexFacet>): string[];
32
48
  export {};
@@ -1,6 +1,7 @@
1
- import { isVectorIndexType } from '../type/vector.js';
1
+ import { indexDistance, isVectorIndexType } from '../type/vector.js';
2
2
  import { fulltextConfig } from '../util/dialect.util.js';
3
3
  import { derivedIndexName } from '../util/sql.util.js';
4
+ import { matchByKey } from './matchByKey.js';
4
5
  /** An entry the engine reprints in its own words, so never compared as written. */
5
6
  function isReprinted(entry) {
6
7
  return Boolean(entry.expression || entry.jsonPath || entry.jsonArray);
@@ -26,17 +27,27 @@ export function indexNameStem(name) {
26
27
  return bare.replace(/__/g, '_');
27
28
  }
28
29
  /**
29
- * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
30
- * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
30
+ * Pairs by name, then what is left by shape: an index the database has under another name is still
31
+ * the one asked for. What stays unpaired is created or dropped.
31
32
  */
32
- export function indexChanges(table, declared, current, keyOf = indexSignature) {
33
- const present = new Set(current.map(keyOf));
34
- const wanted = new Set(declared.map(keyOf));
33
+ export function pairIndexes(source, target, normalizeName = (name) => name) {
34
+ const byName = matchByKey(source, target, (index) => normalizeName(indexNameStem(index.name)));
35
+ const byShape = matchByKey(byName.created, byName.dropped, indexSignature);
36
+ return { created: byShape.created, dropped: byShape.dropped, matched: [...byName.matched, ...byShape.matched] };
37
+ }
38
+ /**
39
+ * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
40
+ * `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
41
+ * dropped: any other may have been made outside the ORM.
42
+ */
43
+ export function indexChanges(table, declared, current, facets) {
44
+ const { created, dropped, matched } = pairIndexes(declared, current);
35
45
  const claimed = new Set(declared.map((index) => index.name));
36
46
  const owned = (index) => claimed.has(index.name) || hasDerivedName(table, index);
37
47
  return {
38
- toAdd: declared.filter((index) => !present.has(keyOf(index))),
39
- toDrop: current.filter((index) => !wanted.has(keyOf(index)) && owned(index)),
48
+ toAdd: created,
49
+ toDrop: dropped.filter(owned),
50
+ toAlter: matched.flatMap(([to, from]) => (describeIndexDifferences(to, from, facets).length ? [{ from, to }] : [])),
40
51
  };
41
52
  }
42
53
  /**
@@ -85,6 +96,10 @@ export function describeIndexDifferences(source, target, facets) {
85
96
  if (facets.has('accessMethod') && (source.type ?? 'btree') !== (target.type ?? 'btree')) {
86
97
  differences.push(`type: ${target.type ?? 'btree'} -> ${source.type ?? 'btree'}`);
87
98
  }
99
+ const bothVector = isVectorIndexType(source.type) && isVectorIndexType(target.type);
100
+ if (facets.has('distance') && bothVector && indexDistance(source) !== indexDistance(target)) {
101
+ differences.push(`distance: ${indexDistance(target)} -> ${indexDistance(source)}`);
102
+ }
88
103
  if (facets.has('vector') && isVectorIndexType(source.type) !== isVectorIndexType(target.type)) {
89
104
  const [expected, actual] = [source, target].map((index) => (isVectorIndexType(index.type) ? 'yes' : 'no'));
90
105
  differences.push(`vector index: ${actual} -> ${expected}`);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The only three ways two keyed collections can differ, which is the shape of every comparison in
3
+ * the schema diff: tables, columns, indexes and relationships all key and then split the same way.
4
+ * One counterpart each, so two items sharing a key (a duplicate index) are not collapsed into one.
5
+ */
6
+ export declare function matchByKey<S, T>(source: Iterable<S>, target: Iterable<T>, key: (item: S | T) => string): {
7
+ created: S[];
8
+ dropped: T[];
9
+ matched: (readonly [S, T])[];
10
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The only three ways two keyed collections can differ, which is the shape of every comparison in
3
+ * the schema diff: tables, columns, indexes and relationships all key and then split the same way.
4
+ * One counterpart each, so two items sharing a key (a duplicate index) are not collapsed into one.
5
+ */
6
+ export function matchByKey(source, target, key) {
7
+ const unpaired = Map.groupBy(target, key);
8
+ const created = [];
9
+ const matched = [];
10
+ for (const item of source) {
11
+ const counterpart = unpaired.get(key(item))?.shift();
12
+ if (counterpart === undefined)
13
+ created.push(item);
14
+ else
15
+ matched.push([item, counterpart]);
16
+ }
17
+ return { created, dropped: [...unpaired.values()].flat(), matched };
18
+ }
@@ -1,6 +1,10 @@
1
- import type { IndexNode, RelationshipNode, TableNode } from './types.js';
1
+ import type { PrimaryKeySchema } from '../type/migration.js';
2
+ import type { IndexFacet } from './indexDifferences.js';
3
+ import type { ColumnNode, IndexNode, RelationshipNode, TableNode } from './types.js';
2
4
  /** A table node with its collections empty, ready to be filled. */
3
- export declare function createTableNode(name: string, schema?: string, comment?: string): TableNode;
5
+ export declare function createTableNode(name: string, schema?: string, indexFacets?: ReadonlySet<IndexFacet>): TableNode;
6
+ /** The key the columns' own flags say, in their order, for a source that reports no key of its own. */
7
+ export declare function keyOfColumns(columns: Iterable<Pick<ColumnNode, 'name' | 'isPrimaryKey'>>): PrimaryKeySchema | undefined;
4
8
  /** A database schema as a graph: tables, the foreign keys between them, and their indexes. */
5
9
  export declare class SchemaAST {
6
10
  readonly tables: Map<string, TableNode>;
@@ -1,19 +1,23 @@
1
1
  import { qualifyName } from '../util/sql.util.js';
2
2
  import { createOrder, dropOrder } from './dependencyGraph.js';
3
3
  /** A table node with its collections empty, ready to be filled. */
4
- export function createTableNode(name, schema, comment) {
4
+ export function createTableNode(name, schema, indexFacets = new Set()) {
5
5
  return {
6
6
  name,
7
7
  schema,
8
- comment,
8
+ indexFacets,
9
9
  columns: new Map(),
10
- primaryKey: [],
11
10
  indexes: [],
12
11
  checks: [],
13
12
  incomingRelations: [],
14
13
  outgoingRelations: [],
15
14
  };
16
15
  }
16
+ /** The key the columns' own flags say, in their order, for a source that reports no key of its own. */
17
+ export function keyOfColumns(columns) {
18
+ const keyColumns = [...columns].filter((column) => column.isPrimaryKey).map((column) => column.name);
19
+ return keyColumns.length ? { columns: keyColumns } : undefined;
20
+ }
17
21
  /** A database schema as a graph: tables, the foreign keys between them, and their indexes. */
18
22
  export class SchemaAST {
19
23
  tables = new Map();
@@ -25,6 +25,8 @@ export interface BuildSchemaASTOptions {
25
25
  compileIndexPredicate?: (where: EntityWhereMeta<object>, entity: Type<object>, indexName: string) => string;
26
26
  /** Whether a weighted fulltext index declares one of its own for each heavier column, as MySQL scores through one. */
27
27
  textScoreIndexes?: boolean;
28
+ /** Whether a column a vector index covers is `NOT NULL` whatever the entity declares, as MariaDB demands. */
29
+ vectorIndexRequiresNotNull?: boolean;
28
30
  }
29
31
  /**
30
32
  * Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
@@ -4,8 +4,9 @@ import { fulltextWeights, textWeightSteps } from '../util/dialect.util.js';
4
4
  import { isAutoIncrement, isInlinedExpression, isSoleIdField } from '../util/field.util.js';
5
5
  import { definedEntries } from '../util/object.util.js';
6
6
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
7
+ import { UqlUsageError } from '../util/uqlError.js';
7
8
  import { resolveColumnCanonicalType } from './canonicalType.js';
8
- import { createTableNode, SchemaAST } from './schemaAST.js';
9
+ import { createTableNode, keyOfColumns, SchemaAST } from './schemaAST.js';
9
10
  import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
10
11
  /**
11
12
  * Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
@@ -26,6 +27,7 @@ export function buildSchemaAST(entities, options = {}) {
26
27
  compileDdl,
27
28
  compileIndexPredicate: options.compileIndexPredicate ?? compileDdl,
28
29
  textScoreIndexes: options.textScoreIndexes ?? false,
30
+ vectorIndexRequiresNotNull: options.vectorIndexRequiresNotNull ?? false,
29
31
  };
30
32
  for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
31
33
  for (const entity of entities) {
@@ -36,7 +38,13 @@ export function buildSchemaAST(entities, options = {}) {
36
38
  }
37
39
  /** The `compileDdl` of a build given no dialect, which has nothing to render an entity's SQL with. */
38
40
  function refuseDdl() {
39
- throw new TypeError('building the schema of an entity that declares SQL (a check, a stored computed column, an index expression or predicate) needs a dialect to render it: pass `compileDdl`, as `buildEntityAST` does');
41
+ throw new UqlUsageError('building the schema of an entity that declares SQL (a check, a stored computed column, an index expression or predicate) needs a dialect to render it: pass `compileDdl`, as `buildEntityAST` does');
42
+ }
43
+ /** The entries a vector index of `meta` covers: the members it names, and any expression. */
44
+ function vectorIndexedEntries(meta) {
45
+ return new Set((meta.indexes ?? [])
46
+ .filter((index) => index.type === 'vector')
47
+ .flatMap((index) => index.columns.map((entry) => entry.column)));
40
48
  }
41
49
  /**
42
50
  * Add a table from entity metadata.
@@ -44,8 +52,9 @@ function refuseDdl() {
44
52
  function addTableFromEntity(ctx, meta) {
45
53
  const tableName = ctx.resolveTableName(meta);
46
54
  const table = createTableNode(tableName, ctx.resolveSchema(meta));
47
- const { columns, primaryKey } = table;
48
- table.checks?.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
55
+ const { columns } = table;
56
+ table.checks.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
57
+ const notNull = ctx.vectorIndexRequiresNotNull ? vectorIndexedEntries(meta) : new Set();
49
58
  // Add columns from fields
50
59
  for (const [key, field] of definedEntries(meta.fields)) {
51
60
  // An inlined expression has no column; a stored one is a column like any other.
@@ -60,7 +69,7 @@ function addTableFromEntity(ctx, meta) {
60
69
  type,
61
70
  // A primary key is NOT NULL in every engine, whatever the entity's property says: `id?: number`
62
71
  // is optional because the database assigns it, not because the column accepts a null.
63
- nullable: isPrimaryKey ? false : (field.nullable ?? true),
72
+ nullable: isPrimaryKey || notNull.has(key) ? false : (field.nullable ?? true),
64
73
  defaultValue: field.defaultValue,
65
74
  isPrimaryKey,
66
75
  isAutoIncrement: isAutoIncrement(field, isSoleKey),
@@ -74,10 +83,8 @@ function addTableFromEntity(ctx, meta) {
74
83
  references: undefined,
75
84
  };
76
85
  columns.set(columnName, column);
77
- if (field.isId) {
78
- primaryKey.push(column);
79
- }
80
86
  }
87
+ table.primaryKey = keyOfColumns(columns.values());
81
88
  ctx.ast.addTable(table);
82
89
  }
83
90
  /** The node an entity maps to, found under the key {@link SchemaAST} stores it by. */
@@ -157,11 +164,10 @@ function addForeignKeyIndexes(ctx, meta, table) {
157
164
  });
158
165
  }
159
166
  }
160
- /** Whether the key, a unique column or an index already leads with `columns`, which is all a lookup needs. */
167
+ /** Whether the key or an index already leads with `columns`, which is all a lookup needs. */
161
168
  function isIndexedBy(table, columns) {
162
169
  const leads = (indexed) => columns.every((column, at) => indexed[at] === column);
163
- return (leads(table.primaryKey.map((column) => column.name)) ||
164
- (columns.length === 1 && table.columns.get(columns[0])?.isUnique === true) ||
170
+ return (leads(table.primaryKey?.columns ?? []) ||
165
171
  table.indexes.some((index) => leads(index.entries.map((entry) => entry.expression || entry.jsonPath || entry.jsonArray ? undefined : entry.column))));
166
172
  }
167
173
  /** An `include` column is named like any other, so a naming strategy has to reach it too. */
@@ -1,4 +1,3 @@
1
- import { type IndexFacet } from './indexDifferences.js';
2
1
  import type { SchemaAST } from './schemaAST.js';
3
2
  import type { CanonicalType } from './types.js';
4
3
  import type { ColumnDiff, ForeignKeyAction, IndexDiff, RelationshipDiff, RelationshipNode, SchemaDiffResult, TableDiff, TableNode } from './types.js';
@@ -8,8 +7,6 @@ import type { ColumnDiff, ForeignKeyAction, IndexDiff, RelationshipDiff, Relatio
8
7
  export interface DiffOptions {
9
8
  /** Compare indexes */
10
9
  compareIndexes?: boolean;
11
- /** What the target side can report about an index, normally an introspector's `indexFacets`. Anything left out is not compared. */
12
- indexFacets?: ReadonlySet<IndexFacet>;
13
10
  /** Compare foreign keys/relationships */
14
11
  compareRelationships?: boolean;
15
12
  /** Ignore case differences in names */
@@ -38,3 +35,5 @@ export declare function referentialActions(rel: RelationshipNode): {
38
35
  readonly onDelete: ForeignKeyAction;
39
36
  readonly onUpdate: ForeignKeyAction;
40
37
  };
38
+ /** Two defaults compared as written, where no dialect reprints them: `now()` and `CURRENT_TIMESTAMP` are one. */
39
+ export declare function defaultsEqualAsWritten(expected: unknown, actual: unknown): boolean;
@@ -1,37 +1,21 @@
1
1
  import { areTypesEqual, isBreakingTypeChange } from './canonicalType.js';
2
- import { describeIndexDifferences, indexNameStem } from './indexDifferences.js';
2
+ import { describeIndexDifferences, pairIndexes } from './indexDifferences.js';
3
+ import { matchByKey } from './matchByKey.js';
3
4
  import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
4
5
  /**
5
6
  * Default diff options.
6
7
  */
7
8
  const DEFAULT_OPTIONS = {
8
9
  compareIndexes: true,
9
- indexFacets: new Set(),
10
10
  compareRelationships: true,
11
11
  normalizeType: (type) => type,
12
- defaultsEqual: (expected, actual) => normalizeDefault(expected) === normalizeDefault(actual),
12
+ defaultsEqual: defaultsEqualAsWritten,
13
13
  ignoreCase: false,
14
14
  excludeTables: [],
15
15
  };
16
16
  function nameNormalizer(opts) {
17
17
  return opts.ignoreCase ? (name) => name.toLowerCase() : (name) => name;
18
18
  }
19
- /**
20
- * The only three ways two keyed collections can differ, which is the shape of every comparison here:
21
- * tables, columns, indexes and relationships all key by name and then split the same way.
22
- */
23
- function matchByKey(source, target, key) {
24
- const sourceByKey = new Map([...source].map((item) => [key(item), item]));
25
- const targetByKey = new Map([...target].map((item) => [key(item), item]));
26
- return {
27
- created: [...sourceByKey].filter(([at]) => !targetByKey.has(at)).map(([, item]) => item),
28
- dropped: [...targetByKey].filter(([at]) => !sourceByKey.has(at)).map(([, item]) => item),
29
- matched: [...sourceByKey].flatMap(([at, item]) => {
30
- const counterpart = targetByKey.get(at);
31
- return counterpart ? [[item, counterpart]] : [];
32
- }),
33
- };
34
- }
35
19
  /** How a relationship diff names the pair it is about, whichever way it differs. */
36
20
  function relationEnds(relation) {
37
21
  return { name: relation.name, fromTable: relation.from.table.name, toTable: relation.to.table.name };
@@ -86,12 +70,12 @@ export function diffTable(source, target, options = {}) {
86
70
  }
87
71
  /** The two keys where they hold different columns, compared in order and never by the name the engine gave them. */
88
72
  function diffPrimaryKey(source, target) {
89
- const expected = source.primaryKey.map((column) => column.name);
90
- const actual = target.primaryKey.map((column) => column.name);
73
+ const expected = source.primaryKey?.columns ?? [];
74
+ const actual = target.primaryKey?.columns ?? [];
91
75
  if (expected.length === actual.length && expected.every((column, i) => column === actual[i])) {
92
76
  return undefined;
93
77
  }
94
- return { table: source.name, expected, actual, actualName: target.primaryKeyName };
78
+ return { table: source.name, expected: source.primaryKey, actual: target.primaryKey };
95
79
  }
96
80
  /**
97
81
  * Compare columns between two tables.
@@ -120,17 +104,14 @@ function diffTableColumns(source, target, opts) {
120
104
  .filter((diff) => diff !== undefined),
121
105
  ];
122
106
  }
123
- /**
124
- * Compare indexes between two tables.
125
- */
107
+ /** Compare indexes between two tables, paired by {@link pairIndexes}, in what the target's reader reports. */
126
108
  function diffTableIndexes(source, target, opts) {
127
- const normalizeName = nameNormalizer(opts);
128
- const { created, dropped, matched } = matchByKey(source.indexes, target.indexes, (index) => normalizeName(indexNameStem(index.name)));
109
+ const { created, dropped, matched } = pairIndexes(source.indexes, target.indexes, nameNormalizer(opts));
129
110
  return [
130
111
  ...created.map((index) => ({ name: index.name, table: source.name, type: 'create', expected: index })),
131
112
  ...dropped.map((index) => ({ name: index.name, table: target.name, type: 'drop', actual: index })),
132
113
  ...matched
133
- .map(([sourceIndex, targetIndex]) => diffIndex(source.name, sourceIndex, targetIndex, opts.indexFacets))
114
+ .map(([sourceIndex, targetIndex]) => diffIndex(source.name, sourceIndex, targetIndex, target.indexFacets))
134
115
  .filter((diff) => diff !== undefined),
135
116
  ];
136
117
  }
@@ -158,12 +139,9 @@ function diffColumn(tableName, source, target, opts) {
158
139
  if (!impliedNotNull && source.nullable !== target.nullable) {
159
140
  differences.push(`nullable: ${target.nullable} -> ${source.nullable}`);
160
141
  }
161
- // Compare unique constraint
162
- if (source.isUnique !== target.isUnique) {
163
- differences.push(`unique: ${target.isUnique} -> ${source.isUnique}`);
164
- }
165
142
  // Not compared, since no statement this generator emits could settle a difference: `isAutoIncrement`,
166
- // `enum` (a check the database reprints), `generatedAs`, and `comment`.
143
+ // `enum` (a check the database reprints), `generatedAs`, and `comment`. Nor `isUnique`: a unique
144
+ // column is a unique index, compared with the indexes.
167
145
  // Compare default values (if both defined)
168
146
  if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
169
147
  differences.push(`default: ${target.defaultValue ?? 'NULL'} -> ${source.defaultValue ?? 'NULL'}`);
@@ -262,9 +240,10 @@ function formatType(type) {
262
240
  result += ' unsigned';
263
241
  return result;
264
242
  }
265
- /**
266
- * Normalize default values for comparison.
267
- */
243
+ /** Two defaults compared as written, where no dialect reprints them: `now()` and `CURRENT_TIMESTAMP` are one. */
244
+ export function defaultsEqualAsWritten(expected, actual) {
245
+ return normalizeDefault(expected) === normalizeDefault(actual);
246
+ }
268
247
  function normalizeDefault(value) {
269
248
  if (value === undefined || value === null)
270
249
  return '';
@@ -1,4 +1,5 @@
1
- import type { IndexSchema } from '../type/migration.js';
1
+ import type { IndexSchema, PrimaryKeySchema } from '../type/migration.js';
2
+ import type { IndexFacet } from './indexDifferences.js';
2
3
  /**
3
4
  * Type categories universal across SQL dialects.
4
5
  * These represent logical/semantic types, not specific SQL types.
@@ -109,18 +110,17 @@ export interface TableNode {
109
110
  readonly schema?: string;
110
111
  /** Map of column name to column node */
111
112
  readonly columns: Map<string, ColumnNode>;
112
- /** Primary key columns, in key order (supports composite keys) */
113
- readonly primaryKey: ColumnNode[];
114
- /**
115
- * What the constraint is called, where a name is known: read back from the database on an
116
- * introspected table, absent on one built from entities, where nothing has named it yet. A `DROP`
117
- * is the only thing that needs it - see {@link TableSchema.primaryKeyName}.
118
- */
119
- primaryKeyName?: string;
113
+ /** The table's key, named where the database reported a name; none on a table without one. */
114
+ primaryKey?: PrimaryKeySchema;
120
115
  /** Indexes on this table */
121
116
  readonly indexes: IndexNode[];
122
- /** `CHECK` constraints on this table. Optional: a node can be built without ever naming one. */
123
- readonly checks?: CheckSchema[];
117
+ /**
118
+ * What the introspector that read this table reports about an index, and so all an index diff against
119
+ * it may compare. None on a table built from entities.
120
+ */
121
+ readonly indexFacets: ReadonlySet<IndexFacet>;
122
+ /** `CHECK` constraints on this table. */
123
+ readonly checks: CheckSchema[];
124
124
  /** Optional table comment */
125
125
  readonly comment?: string;
126
126
  /** Relationships pointing TO this table (other tables referencing this one) */
@@ -214,10 +214,9 @@ export interface TableDiff {
214
214
  */
215
215
  export interface PrimaryKeyDiff {
216
216
  readonly table: string;
217
- readonly expected: string[];
218
- readonly actual: string[];
219
- /** What the *actual* side calls its constraint, which is the only name a `DROP` can use. */
220
- readonly actualName?: string;
217
+ readonly expected?: PrimaryKeySchema;
218
+ /** Named as the database reported it, which is the only name a `DROP` can use. */
219
+ readonly actual?: PrimaryKeySchema;
221
220
  }
222
221
  /**
223
222
  * Difference between two index definitions.
@@ -1,6 +1,7 @@
1
- import type { EntityMeta, UpdatePayload } from './entity.js';
1
+ import type { EntityMeta, EntityPredicate, UpdatePayload } from './entity.js';
2
2
  import type { Query, QueryConflictPaths, QueryPage, QueryRenderOptions, QuerySearch, RelationQuery } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateOp, QueryGroupMap } from './queryAggregate.js';
4
+ import type { QueryRawRenderOptions } from './queryRaw.js';
4
5
  import type { QueryWhere } from './queryWhere.js';
5
6
  import type { Type } from './utility.js';
6
7
  import type { QueryVectorQuery } from './vector.js';
@@ -231,6 +232,25 @@ export interface TriggerFeatures {
231
232
  */
232
233
  readonly before: boolean;
233
234
  }
235
+ /** Where DDL's SQL sits: the row a trigger's predicate reads, as its prefix, and a set-based body's rows. */
236
+ export type DdlRenderOptions = Pick<QueryComparisonOptions, 'escapedPrefix' | 'operand'> & Pick<QueryRawRenderOptions, 'rows'>;
237
+ /**
238
+ * A write a trigger's body runs, as `insertInto`, `updateTable` and `deleteFrom` state it. Held untyped
239
+ * here, past those helpers' typing, since the dialect renders it by the entity's metadata alone.
240
+ */
241
+ export type TriggerWrite = {
242
+ readonly entity: Type<object>;
243
+ } & ({
244
+ readonly kind: 'insert';
245
+ readonly row: Readonly<Record<string, unknown>>;
246
+ } | {
247
+ readonly kind: 'update';
248
+ readonly set: Readonly<Record<string, unknown>>;
249
+ readonly where: EntityPredicate<object>;
250
+ } | {
251
+ readonly kind: 'delete';
252
+ readonly where: EntityPredicate<object>;
253
+ });
234
254
  /**
235
255
  * What a SQL statement is rendered through, as a `raw` callback and a query context see it:
236
256
  * `AbstractSqlDialect` is the one implementation.
@@ -262,6 +282,8 @@ export interface SqlQueryDialect {
262
282
  update<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryRenderOptions): void;
263
283
  /** An upsert of one record or many by their conflict paths. */
264
284
  upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
285
+ /** A write in a trigger's body; `rows` is where a set-based engine's body reads its rows from. */
286
+ triggerWrite(ctx: QueryContext, write: TriggerWrite, rows?: string): void;
265
287
  /** A delete of the records the query matches, a soft delete where the entity has one. */
266
288
  delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryRenderOptions): void;
267
289
  /**
@@ -1,7 +1,7 @@
1
1
  import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js';
2
2
  import type { SqlDialectName } from './dialect.js';
3
3
  import type { FilterOptions, RelationQuery } from './query.js';
4
- import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
4
+ import type { ColumnRef, QueryRaw, RawFor, RelationAggregate } from './queryRaw.js';
5
5
  import type { QueryWhere } from './queryWhere.js';
6
6
  import type { AtLeastOne, Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
7
7
  import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
@@ -37,6 +37,10 @@ export type WritableKey<E> = {
37
37
  }[FieldKey<E>];
38
38
  /** A whole-record write as a caller supplies one: {@link EntityData} without the fields it cannot write. */
39
39
  export type EntityWrite<E> = EntityData<E, WritableKey<E>>;
40
+ /** A row a trigger's body writes: each writable field its value, or SQL - a row's ref most often. */
41
+ export type WriteRow<E, F extends keyof E = WritableKey<E>> = {
42
+ readonly [K in F]?: E[K] | RawFor<QueryRaw, E[K]>;
43
+ };
40
44
  /**
41
45
  * The property an entity brands with {@link versionKey} as its optimistic lock, `never` where it
42
46
  * brands none. The brand is what carries `@Field({ version: true })` to the type level, since a
@@ -114,7 +118,7 @@ export type JsonArrayFields<T> = {
114
118
  */
115
119
  export type JsonUpdateOp<T = unknown> = {
116
120
  readonly $set?: Partial<T>;
117
- readonly $unset?: unknown extends T ? string[] : (keyof T & string)[];
121
+ readonly $unset?: unknown extends T ? readonly string[] : readonly (keyof T & string)[];
118
122
  readonly $push?: JsonArrayFields<T>;
119
123
  readonly $pull?: JsonArrayFields<T>;
120
124
  };
@@ -133,7 +137,7 @@ export type FieldUpdateOp<T extends number | bigint = number | bigint> = Exactly
133
137
  /** The {@link FieldUpdateOp} a field takes: `never` on one it has no operator for, which is any but a number. */
134
138
  type FieldUpdateOpFor<V> = [NonNullable<V>] extends [number] ? FieldUpdateOp<number> : [NonNullable<V>] extends [bigint] ? FieldUpdateOp<bigint> : never;
135
139
  /** What an update takes beyond the value: `null` to clear an optional member, `raw` SQL, and update operators. */
136
- type UpdateExtra<V, Raw> = (undefined extends V ? null : never) | Raw | JsonUpdateOpFor<V> | FieldUpdateOpFor<V>;
140
+ type UpdateExtra<V, Raw> = (undefined extends V ? null : never) | RawFor<Raw, V> | JsonUpdateOpFor<V> | FieldUpdateOpFor<V>;
137
141
  /**
138
142
  * What a whole-record write persists: the fields and relations with their declared optionality, a
139
143
  * related row's alike, and no methods. Two mapped types, since asking each key costs a conditional.
@@ -411,6 +415,10 @@ export type RelationReference<O, E> = {
411
415
  readonly foreign: F;
412
416
  };
413
417
  }[FieldKey<E>];
418
+ /** The fields of `E` whose value is a `T`, however optional: `FieldKeyOf<E, number | bigint>` are the ones a sum adds up. */
419
+ export type FieldKeyOf<E, T> = {
420
+ readonly [K in FieldKey<E>]-?: [NonNullable<E[K]>] extends [T] ? K : never;
421
+ }[FieldKey<E>];
414
422
  /** The fields of `O` that can hold any value `V` takes. */
415
423
  type FieldKeyHolding<O, V> = {
416
424
  readonly [K in keyof O]-?: [NonNullable<V>] extends [NonNullable<O[K]>] ? K : never;
@@ -446,16 +454,12 @@ export type KeyMap<E> = {
446
454
  };
447
455
  /** The fields of `E` as {@link ColumnRef}s, for SQL that names them: `refs(User)`, or a definition's callback. */
448
456
  export type RefMap<E, F extends keyof E = FieldKey<E>> = {
449
- readonly [K in F]-?: ColumnRef<K & string>;
457
+ readonly [K in F]-?: ColumnRef<K & string, E[K]>;
450
458
  };
451
459
  /** SQL a definition writes: `raw`, or a callback reading the fields off its refs, bivariant so the registry can hold it. */
452
460
  export type EntitySql<E> = QueryRaw | {
453
461
  sql(refs: RefMap<E>): QueryRaw;
454
462
  }['sql'];
455
- /** The fields of `C` a `sum` or an `avg` can add up. */
456
- type NumericKey<C> = {
457
- readonly [K in FieldKey<C>]-?: [NonNullable<C[K]>] extends [number | bigint] ? K : never;
458
- }[FieldKey<C>];
459
463
  /** One field of `C`, read off its refs: `(item) => item.amount`. */
460
464
  type PickRef<C, K extends keyof C> = (refs: RefMap<C>) => ColumnRef<K & string>;
461
465
  /**
@@ -466,11 +470,11 @@ type PickRef<C, K extends keyof C> = (refs: RefMap<C>) => ColumnRef<K & string>;
466
470
  export type RelationRef<C> = {
467
471
  count(q?: AggregateFilter<C>): RelationAggregate<number, true>;
468
472
  count(q: AggregatePage<C>): RelationAggregate<number, false>;
469
- sum<K extends NumericKey<C>>(pick: PickRef<C, K>, q?: AggregateFilter<C>): RelationAggregate<NonNullable<C[K]>, true>;
470
- sum<K extends NumericKey<C>>(pick: PickRef<C, K>, q: AggregateTopRows<C>): RelationAggregate<NonNullable<C[K]>, false>;
473
+ sum<K extends FieldKeyOf<C, number | bigint>>(pick: PickRef<C, K>, q?: AggregateFilter<C>): RelationAggregate<NonNullable<C[K]>, true>;
474
+ sum<K extends FieldKeyOf<C, number | bigint>>(pick: PickRef<C, K>, q: AggregateTopRows<C>): RelationAggregate<NonNullable<C[K]>, false>;
471
475
  min<K extends FieldKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<NonNullable<C[K]> | null, false>;
472
476
  max<K extends FieldKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<NonNullable<C[K]> | null, false>;
473
- avg<K extends NumericKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<number | null, false>;
477
+ avg<K extends FieldKeyOf<C, number | bigint>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<number | null, false>;
474
478
  };
475
479
  /**
476
480
  * What an aggregate reads of the related rows. The predicate is an {@link EntityPredicate} rather than a
@@ -12,9 +12,9 @@ export interface Logger {
12
12
  * @param values - The parameters passed to the query.
13
13
  * @param duration - The time it took to execute the query in milliseconds.
14
14
  */
15
- logQuery?(query: string, values?: unknown[], duration?: number): void;
15
+ logQuery?(query: string, values?: readonly unknown[], duration?: number): void;
16
16
  /** Logs a query that took longer than the threshold, its values `undefined` unless `logValues` is on. */
17
- logSlowQuery?(query: string, values?: unknown[], duration?: number): void;
17
+ logSlowQuery?(query: string, values?: readonly unknown[], duration?: number): void;
18
18
  /**
19
19
  * Logs a warning.
20
20
  */