uql-orm 0.79.0 → 0.81.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 (110) hide show
  1. package/dist/browser/uql-browser.min.js.map +2 -2
  2. package/dist/cockroachdb/cockroachDialect.js +5 -1
  3. package/dist/dialect/abstractDialect.d.ts +1 -31
  4. package/dist/dialect/abstractDialect.js +3 -27
  5. package/dist/dialect/abstractSqlDialect.d.ts +25 -56
  6. package/dist/dialect/abstractSqlDialect.js +78 -145
  7. package/dist/dialect/aliases.d.ts +5 -0
  8. package/dist/dialect/aliases.js +5 -0
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
  10. package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
  11. package/dist/dialect/operators.d.ts +66 -0
  12. package/dist/dialect/operators.js +129 -0
  13. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
  14. package/dist/dialect/pgLikeSqlDialect.js +16 -3
  15. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  16. package/dist/dialect/vectorSqlDialect.js +4 -0
  17. package/dist/entity/decorator/entity.d.ts +6 -1
  18. package/dist/entity/decorator/entity.js +12 -1
  19. package/dist/entity/index.d.ts +1 -1
  20. package/dist/entity/index.js +1 -1
  21. package/dist/entity/metadata/definition.d.ts +6 -1
  22. package/dist/entity/metadata/definition.js +19 -0
  23. package/dist/migrate/builder/expressions.d.ts +2 -0
  24. package/dist/migrate/builder/expressions.js +24 -0
  25. package/dist/migrate/cli.js +1 -1
  26. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  27. package/dist/migrate/codegen/entityTypes.js +1 -2
  28. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  30. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
  31. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +6 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +25 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +8 -6
  38. package/dist/migrate/ddl/tableDdl.d.ts +5 -2
  39. package/dist/migrate/ddl/tableDdl.js +13 -7
  40. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  41. package/dist/migrate/drift/driftDetector.js +21 -21
  42. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  43. package/dist/migrate/generator/definitionToNode.js +9 -20
  44. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +5 -1
  45. package/dist/migrate/generator/mongoSchemaGenerator.js +20 -18
  46. package/dist/migrate/index.d.ts +2 -1
  47. package/dist/migrate/index.js +1 -0
  48. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +16 -6
  49. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +18 -4
  50. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  51. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
  52. package/dist/migrate/introspection/mongoIntrospector.js +7 -3
  53. package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
  54. package/dist/migrate/introspection/mssqlIntrospector.js +9 -0
  55. package/dist/migrate/introspection/mysqlIntrospector.d.ts +16 -5
  56. package/dist/migrate/introspection/mysqlIntrospector.js +39 -2
  57. package/dist/migrate/introspection/postgresIntrospector.d.ts +30 -21
  58. package/dist/migrate/introspection/postgresIntrospector.js +70 -43
  59. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  60. package/dist/migrate/introspection/sqliteIntrospector.js +13 -8
  61. package/dist/migrate/migrator.d.ts +33 -1
  62. package/dist/migrate/migrator.js +111 -43
  63. package/dist/migrate/schemaChange.d.ts +18 -0
  64. package/dist/migrate/schemaChange.js +37 -0
  65. package/dist/migrate/schemaGenerator.d.ts +25 -15
  66. package/dist/migrate/schemaGenerator.js +130 -182
  67. package/dist/migrate/storage/databaseStorage.d.ts +4 -0
  68. package/dist/migrate/storage/databaseStorage.js +14 -8
  69. package/dist/migrate/triggerSql.d.ts +24 -0
  70. package/dist/migrate/triggerSql.js +229 -0
  71. package/dist/mongo/mongoDialect.d.ts +0 -21
  72. package/dist/mongo/mongoDialect.js +105 -100
  73. package/dist/mongo/mongodbQuerier.js +17 -1
  74. package/dist/mssql/mssqlDialect.d.ts +18 -7
  75. package/dist/mssql/mssqlDialect.js +77 -33
  76. package/dist/mssql/mssqlQuerier.js +2 -2
  77. package/dist/schema/canonicalType.d.ts +6 -1
  78. package/dist/schema/canonicalType.js +14 -0
  79. package/dist/schema/indexDifferences.d.ts +22 -6
  80. package/dist/schema/indexDifferences.js +23 -8
  81. package/dist/schema/matchByKey.d.ts +10 -0
  82. package/dist/schema/matchByKey.js +18 -0
  83. package/dist/schema/schemaAST.d.ts +6 -2
  84. package/dist/schema/schemaAST.js +7 -3
  85. package/dist/schema/schemaASTBuilder.d.ts +4 -8
  86. package/dist/schema/schemaASTBuilder.js +20 -29
  87. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  88. package/dist/schema/schemaASTDiffer.js +15 -36
  89. package/dist/schema/types.d.ts +14 -15
  90. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  91. package/dist/sqlite/sqliteDialect.js +12 -3
  92. package/dist/type/dialect.d.ts +69 -9
  93. package/dist/type/entity.d.ts +96 -3
  94. package/dist/type/migration.d.ts +43 -44
  95. package/dist/type/query.d.ts +13 -4
  96. package/dist/type/queryWhere.d.ts +4 -2
  97. package/dist/util/ddlExpression.util.d.ts +5 -1
  98. package/dist/util/ddlExpression.util.js +6 -2
  99. package/dist/util/field.util.d.ts +9 -1
  100. package/dist/util/field.util.js +14 -2
  101. package/dist/util/fieldOption.util.d.ts +2 -2
  102. package/dist/util/fieldOption.util.js +2 -2
  103. package/dist/util/raw.d.ts +9 -1
  104. package/dist/util/raw.js +36 -11
  105. package/dist/util/sql.util.d.ts +12 -0
  106. package/dist/util/sql.util.js +24 -3
  107. package/dist/util/uqlError.d.ts +2 -0
  108. package/dist/util/uqlError.js +4 -0
  109. package/package.json +4 -4
  110. package/skills/uql-orm/SKILL.md +11 -6
@@ -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.
@@ -36,7 +36,7 @@ export declare class SqliteDialect extends AbstractSqlDialect {
36
36
  */
37
37
  protected appendDefaultInsertValue(ctx: QueryContext, field: FieldOptions | undefined): void;
38
38
  protected readonly caseInsensitiveMatch = "native";
39
- protected get neOp(): string;
39
+ neExpr(field: string, ph: string): string;
40
40
  normalizeValue(value: unknown): unknown;
41
41
  /**
42
42
  * `OFFSET` is only legal after a `LIMIT` here too, so a bare `$skip` needs one - `-1` being
@@ -17,7 +17,6 @@ function ftsQuery(columns, value) {
17
17
  }
18
18
  /** What SQLite and the engines derived from it have. */
19
19
  export const SQLITE_FEATURES = {
20
- ifNotExists: true,
21
20
  indexIfNotExists: true,
22
21
  schemas: false, // SQLite's namespaces are attached database files, not declared objects
23
22
  dropTableCascade: false,
@@ -41,6 +40,16 @@ export const SQLITE_FEATURES = {
41
40
  narrowVectorTypes: false,
42
41
  vectorTuningNeedsTransaction: false,
43
42
  serialDeclaresPrimaryKey: true,
43
+ triggers: {
44
+ preamble: '',
45
+ assignsRow: false,
46
+ body: 'inline',
47
+ guards: 'clause',
48
+ layout: 'timingFirst',
49
+ rows: 'row',
50
+ scope: 'schema',
51
+ before: true,
52
+ },
44
53
  };
45
54
  export class SqliteDialect extends AbstractSqlDialect {
46
55
  features = SQLITE_FEATURES;
@@ -114,8 +123,8 @@ export class SqliteDialect extends AbstractSqlDialect {
114
123
  // SQLite's `LIKE` already ignores case on both sides, for ASCII - and only ASCII, with or without
115
124
  // `NOCASE`, so folding the pattern here would break the accented text the engine leaves alone.
116
125
  caseInsensitiveMatch = 'native';
117
- get neOp() {
118
- return 'IS NOT';
126
+ neExpr(field, ph) {
127
+ return `${field} IS NOT ${ph}`;
119
128
  }
120
129
  normalizeValue(value) {
121
130
  if (value instanceof Date)
@@ -1,5 +1,5 @@
1
1
  import type { EntityMeta, UpdatePayload } from './entity.js';
2
- import type { Query, QueryConflictPaths, QueryOptions, QueryPage, QuerySearch, RelationQuery } from './query.js';
2
+ import type { Query, QueryConflictPaths, QueryPage, QueryRenderOptions, QuerySearch, RelationQuery } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateOp, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
5
5
  import type { Type } from './utility.js';
@@ -7,7 +7,7 @@ import type { QueryVectorQuery } from './vector.js';
7
7
  /**
8
8
  * comparison options.
9
9
  */
10
- export type QueryComparisonOptions = QueryOptions & {
10
+ export type QueryComparisonOptions = QueryRenderOptions & {
11
11
  /**
12
12
  * Whether this fragment is rendered as an operand of an enclosing `AND`/`OR`/`NOT`. An operand
13
13
  * parenthesizes itself when it emits more than one term, so no fragment ever depends on the
@@ -78,7 +78,6 @@ export type InsertIdSource = 'returning' | 'firstId';
78
78
  * Features of the database engine (SQL syntax layer).
79
79
  */
80
80
  export interface DialectFeatures {
81
- readonly ifNotExists: boolean;
82
81
  readonly indexIfNotExists: boolean;
83
82
  /**
84
83
  * Whether the engine has namespaces a table can sit behind. `false` leaves every table
@@ -176,6 +175,61 @@ export interface SqlDialectFeatures extends DialectFeatures {
176
175
  readonly vectorTuningNeedsTransaction: boolean;
177
176
  /** Whether the serial column type states `PRIMARY KEY` itself, as SQLite's `AUTOINCREMENT` must. */
178
177
  readonly serialDeclaresPrimaryKey: boolean;
178
+ /** How the engine spells a trigger. One value rather than a flag each, as {@link rowLocks} is. */
179
+ readonly triggers: TriggerFeatures;
180
+ }
181
+ /**
182
+ * What a trigger body calls the rows it reads. Row-based engines hand it a record on each side; SQL
183
+ * Server hands it the two tables of the set it touched.
184
+ */
185
+ export type TriggerRowName = 'NEW' | 'OLD' | 'inserted' | 'deleted';
186
+ /** How a dialect spells a trigger, once {@link SqlDialectFeatures.triggers} names its shape. */
187
+ export interface TriggerFeatures {
188
+ /**
189
+ * Where the body lives: a function of its own that the trigger names (the Postgres family), or inside
190
+ * the `CREATE TRIGGER` itself (everywhere else).
191
+ */
192
+ readonly body: 'function' | 'inline';
193
+ /**
194
+ * How it states which rows it fires for: `UPDATE OF` beside a `WHEN` (`'clause'`), or, where there is
195
+ * no usable `WHEN` - the MySQL family, CockroachDB, SQL Server - the same condition wrapping the body,
196
+ * as `IF c THEN ... END IF;` (`'thenEndIf'`) or T-SQL's `IF c BEGIN ... END` (`'beginEnd'`).
197
+ */
198
+ readonly guards: 'clause' | 'thenEndIf' | 'beginEnd';
199
+ /**
200
+ * Whether it fires once per row, with a row on each side, or once per statement over the set it
201
+ * touched. SQL Server is the only one here that is set-based, reading `inserted` and `deleted`.
202
+ */
203
+ readonly rows: 'row' | 'set';
204
+ /**
205
+ * Where a trigger's name is unique, and so what a `DROP` has to name: per table on the Postgres
206
+ * family, which spells `DROP TRIGGER x ON t`, and per schema everywhere else, which spells
207
+ * `DROP TRIGGER x`. Two tables may carry the same trigger name only under `'table'`.
208
+ */
209
+ readonly scope: 'table' | 'schema';
210
+ /**
211
+ * Where the table sits in the statement: after the timing, `BEFORE UPDATE ON t`, or ahead of it and
212
+ * behind an `AS`, `ON t AFTER UPDATE AS` - which is T-SQL's shape and nobody else's.
213
+ */
214
+ readonly layout: 'timingFirst' | 'tableFirst';
215
+ /**
216
+ * What every body opens with, or `''`. T-SQL wants `SET NOCOUNT ON`: a trigger running its own DML
217
+ * otherwise sends a rowcount of its own back, and the client reads that as what the original statement
218
+ * affected. Nothing else here needs a preamble.
219
+ */
220
+ readonly preamble: string;
221
+ /**
222
+ * Whether a body may assign to the row it was handed, `NEW."col" := ...`. SQLite forbids writing `NEW`
223
+ * at all, and SQL Server is handed a set rather than a row, so on both a trigger that fills a column
224
+ * has to restate the row as an `UPDATE` after the write instead.
225
+ */
226
+ readonly assignsRow: boolean;
227
+ /**
228
+ * Whether it can fire before the write, which is what a stamp needs. SQL Server has only `AFTER` and
229
+ * `INSTEAD OF`, and `INSTEAD OF` would make the trigger responsible for performing the write itself,
230
+ * so a `before*` event is refused there rather than silently made to mean something else.
231
+ */
232
+ readonly before: boolean;
179
233
  }
180
234
  /**
181
235
  * What a SQL statement is rendered through, as a `raw` callback and a query context see it:
@@ -186,6 +240,12 @@ export interface SqlQueryDialect {
186
240
  * The SQL dialect name.
187
241
  */
188
242
  readonly dialectName: SqlDialectName;
243
+ /**
244
+ * The engine whose SQL this one also accepts, which is itself unless it is a fork: CockroachDB runs
245
+ * Postgres's PL/pgSQL and MariaDB runs MySQL's. What lets a body, or any other hand-written SQL, be
246
+ * declared once for a family rather than copied per member.
247
+ */
248
+ readonly dialectFamily: SqlDialectName;
189
249
  /**
190
250
  * the escape character for identifiers.
191
251
  */
@@ -193,17 +253,17 @@ export interface SqlQueryDialect {
193
253
  /** What the engine can do. */
194
254
  readonly features: SqlDialectFeatures;
195
255
  /** A read; with `totalAlias`, every row also carries the unpaged match count under that alias. */
196
- find<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions, totalAlias?: string): void;
256
+ find<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryRenderOptions, totalAlias?: string): void;
197
257
  /** A count of the records matching the filter, or of those a page of it takes. */
198
- count<E>(ctx: QueryContext, entity: Type<E>, q: QueryPage<E>, opts?: QueryOptions): void;
258
+ count<E>(ctx: QueryContext, entity: Type<E>, q: QueryPage<E>, opts?: QueryRenderOptions): void;
199
259
  /** An insert of one record or many. */
200
- insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void;
260
+ insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryRenderOptions): void;
201
261
  /** An update of the records the query matches. */
202
- update<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): void;
262
+ update<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryRenderOptions): void;
203
263
  /** An upsert of one record or many by their conflict paths. */
204
264
  upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
205
265
  /** A delete of the records the query matches, a soft delete where the entity has one. */
206
- delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
266
+ delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryRenderOptions): void;
207
267
  /**
208
268
  * escape an identifier.
209
269
  * @param val the value to be escaped
@@ -237,7 +297,7 @@ export interface SqlQueryDialect {
237
297
  /**
238
298
  * Build an aggregate query.
239
299
  */
240
- aggregate<E, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(ctx: QueryContext, entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): void;
300
+ aggregate<E, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(ctx: QueryContext, entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryRenderOptions): void;
241
301
  /**
242
302
  * Get the placeholder for a parameter at the given index (1-based).
243
303
  * Default: '?' for MySQL/MariaDB/SQLite, '$n' for PostgreSQL.
@@ -1,8 +1,9 @@
1
1
  import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js';
2
+ import type { SqlDialectName } from './dialect.js';
2
3
  import type { FilterOptions, RelationQuery } from './query.js';
3
4
  import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
4
5
  import type { QueryWhere } from './queryWhere.js';
5
- import type { Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
6
+ import type { AtLeastOne, Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
6
7
  import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
7
8
  /** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
8
9
  export declare const idKey: unique symbol;
@@ -254,8 +255,12 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
254
255
  * subquery a `$count` reads. Both resolve to SQL at registration, so everything downstream sees one.
255
256
  */
256
257
  readonly computed?: ComputedSql<E>;
257
- /** Whether {@link FieldOptions.computed} is a generated column rather than spliced into each read; no query changes either way. */
258
- readonly stored?: boolean;
258
+ /**
259
+ * Where {@link FieldOptions.computed} lives instead of being spliced into each read. `true` makes it a
260
+ * generated column, which takes only an immutable expression; a list of events makes it a stamp a trigger
261
+ * writes on each, whoever writes the row - how `CURRENT_TIMESTAMP` is kept, where `onUpdate` sees only uql's writes.
262
+ */
263
+ readonly stored?: boolean | readonly StampEvent[];
259
264
  readonly updatable?: boolean;
260
265
  readonly eager?: boolean;
261
266
  readonly onInsert?: OnFieldCallback<V>;
@@ -691,11 +696,97 @@ export type EntityMeta<E> = {
691
696
  checks?: EntityCheckMeta<E>[];
692
697
  /** Lifecycle hooks registered via @BeforeInsert, @AfterUpdate, etc. */
693
698
  hooks?: Partial<Record<HookEvent, HookRegistration[]>>;
699
+ /** Triggers the database runs, compiled when the schema is built. */
700
+ triggers?: EntityTriggerMeta<E>[];
694
701
  /** Bumped by every `define*` call, so what is derived from the metadata can tell it changed. */
695
702
  revision: number;
696
703
  /** The revision `getMeta` last finalized, which is what makes finalizing idempotent and re-entrant. */
697
704
  processedAt?: number;
698
705
  };
706
+ /** When the database writes a stamp: as the row is inserted, as it is updated, or both. */
707
+ export type StampEvent = 'insert' | 'update';
708
+ /**
709
+ * The events a trigger fires on: the lifecycle names, minus the upsert pair, which names no event of its
710
+ * own because `ON CONFLICT` fires the insert or the update triggers, and minus `afterLoad`, which is a
711
+ * read. Derived from {@link HookEvent} so the two vocabularies cannot drift.
712
+ */
713
+ export type TriggerEvent = Exclude<HookEvent, 'beforeUpsert' | 'afterUpsert' | 'afterLoad'>;
714
+ /** Both events for one operation, since a trigger's timing never changes which rows it has. */
715
+ type TriggerEventOn<Op extends string> = Extract<TriggerEvent, `before${Op}` | `after${Op}`>;
716
+ /** The events with a row on both sides, the only ones that can say which columns moved. */
717
+ type TriggerUpdateEvent = TriggerEventOn<'Update'>;
718
+ /**
719
+ * The rows an event has, as `where` keys them: the incoming one on an insert, the outgoing one on a
720
+ * delete, both on an update.
721
+ */
722
+ type TriggerRow<Ev extends TriggerEvent> = Ev extends TriggerEventOn<'Insert'> ? '$new' : Ev extends TriggerEventOn<'Delete'> ? '$old' : '$new' | '$old';
723
+ /** One row's refs, rendering `NEW."col"` or `OLD."col"`, or `never` where the event has no such row. */
724
+ type TriggerRowRefs<E, Ev extends TriggerEvent, R extends '$new' | '$old'> = R extends TriggerRow<Ev> ? RefMap<E> : never;
725
+ /**
726
+ * What a trigger runs, over its rows: the incoming row first and the outgoing one second, on every event.
727
+ * The one an event lacks is `never`, so reading it does not compile, and a body reading only the outgoing
728
+ * row - `(_newRow, oldRow)` - serves an update and a delete alike.
729
+ */
730
+ type TriggerBody<E, Ev extends TriggerEvent> = (newRow: TriggerRowRefs<E, Ev, '$new'>, oldRow: TriggerRowRefs<E, Ev, '$old'>) => QueryRaw;
731
+ /**
732
+ * A condition as data: a predicate on each row it names, of the rows the event has, all of which hold.
733
+ * The row is `$`-marked, as the operators inside it are, so it never reads as a field of the entity.
734
+ */
735
+ type TriggerPredicate<E, Ev extends TriggerEvent> = {
736
+ readonly [R in TriggerRow<Ev>]?: EntityPredicate<E>;
737
+ };
738
+ /**
739
+ * The body, the engine's own SQL: one for every engine it reads alike, or a map naming one per engine
740
+ * where they differ, as SQL Server's set-based `inserted`/`deleted` does. A missing entry for the engine
741
+ * in use is refused at `sync`, since an entity is declared without knowing which pool will render it.
742
+ */
743
+ type TriggerRun<E, Ev extends TriggerEvent> = TriggerBody<E, Ev> | Readonly<AtLeastOne<Record<SqlDialectName, TriggerBody<E, Ev>>>>;
744
+ /**
745
+ * A trigger, `{ on: 'beforeUpdate', of: (post) => [post.body], run: (newRow) => raw`...` }`.
746
+ *
747
+ * A list rather than a map keyed by the event, as `checks` and `indexes` are lists: several triggers may
748
+ * share an event, they fire in the order written, and each is named, diffed and dropped by that name.
749
+ */
750
+ export type TriggerOptions<E = unknown> = {
751
+ [Ev in TriggerEvent]: {
752
+ readonly on: Ev;
753
+ /**
754
+ * What to call this trigger within the entity, for a clearer identifier than its event and position.
755
+ * A label, not the identifier: uql prefixes and qualifies what it installs, so it can tell its own
756
+ * objects from hand-written ones and two entities may share a label.
757
+ */
758
+ readonly name?: string;
759
+ /**
760
+ * The columns whose change the trigger waits for, reading as the `UPDATE OF` it renders. Beside it
761
+ * goes a `WHEN` comparing each with `IS DISTINCT FROM`, which is the point of the pair: `UPDATE OF`
762
+ * fires on a column that was merely assigned, and the comparison narrows that to one that moved.
763
+ */
764
+ readonly of?: Ev extends TriggerUpdateEvent ? (refs: RefMap<E>) => readonly ColumnRef<string>[] : never;
765
+ /**
766
+ * A further condition, as the `WHEN` the engine evaluates before entering the body, over the rows the
767
+ * body reads: a predicate on each, `{ $old: { status: 'draft' }, $new: { status: 'published' } }`,
768
+ * rendered on every engine from the one declaration, or SQL off them for what no predicate states.
769
+ */
770
+ readonly where?: TriggerPredicate<E, Ev> | TriggerBody<E, Ev>;
771
+ readonly run: TriggerRun<E, Ev>;
772
+ };
773
+ }[TriggerEvent];
774
+ /**
775
+ * A trigger as entity metadata keeps it: the column callback resolved to keys, and a body widened to
776
+ * take both rows, which the renderer passes whatever the event, each body reading only its own.
777
+ */
778
+ export type EntityTriggerMeta<E = object> = Except<TriggerOptions<E>, 'of' | 'where' | 'run'> & {
779
+ readonly of?: readonly string[];
780
+ readonly where?: TriggerPredicate<E, TriggerUpdateEvent> | TriggerMetaBody<E>;
781
+ readonly run: TriggerMetaBody<E> | Readonly<Partial<Record<SqlDialectName, TriggerMetaBody<E>>>>;
782
+ };
783
+ /**
784
+ * A body as the renderer calls it, with both rows whatever the event. Bivariant, as {@link EntitySql} is,
785
+ * so a body typing the row its event lacks as `never` is still held here.
786
+ */
787
+ export type TriggerMetaBody<E> = {
788
+ run(newRow: RefMap<E>, oldRow: RefMap<E>): QueryRaw;
789
+ }['run'];
699
790
  /**
700
791
  * A table's `CHECK`, `{ where: { balance: { $gte: 0 } } }`, or SQL off the refs,
701
792
  * `{ where: (wallet) => raw`${wallet.spent} <= ${wallet.balance}` }`.
@@ -743,6 +834,8 @@ export type EntityOptions<E = unknown> = {
743
834
  readonly checks?: readonly CheckOptions<E>[];
744
835
  /** Each lifecycle event and the methods it runs, read off the key map: `{ beforeInsert: (post) => [post.stamp] }`. */
745
836
  readonly hooks?: Partial<Record<HookEvent, (keys: KeyMap<E>) => readonly MethodKey<E>[]>>;
837
+ /** Triggers the database runs, in the order written. See {@link TriggerOptions}. */
838
+ readonly triggers?: readonly TriggerOptions<E>[];
746
839
  };
747
840
  /**
748
841
  * Everything an index carries beyond its columns, as the migration builder's `table.index(...)` takes it,
@@ -118,14 +118,7 @@ export interface ColumnSchema extends Omit<ColumnNode, 'type' | 'table' | 'refer
118
118
  export interface TableSchema {
119
119
  readonly name: string;
120
120
  readonly columns: ColumnSchema[];
121
- /** The key's columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
122
- readonly primaryKey?: string[];
123
- /**
124
- * What the engine calls the key's constraint, where it names one at all - Postgres's `Member_pkey`,
125
- * MySQL's literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the
126
- * database actually reported will do: a derived one would name a constraint that is not there.
127
- */
128
- readonly primaryKeyName?: string;
121
+ readonly primaryKey?: PrimaryKeySchema;
129
122
  readonly indexes?: IndexSchema[];
130
123
  readonly foreignKeys?: ForeignKeySchema[];
131
124
  }
@@ -176,8 +169,26 @@ export interface ForeignKeySchema {
176
169
  readonly onUpdate?: ForeignKeyAction;
177
170
  }
178
171
  /**
179
- * Represents a difference between current and desired schema
172
+ * One object's change: added (`to` alone), dropped (`from` alone), or altered (both), each whole so the
173
+ * change is undone by swapping its ends. No engine alters an index, a key or a foreign key in place, so
174
+ * an alter of one is its drop and its add, which safe mode holds back together.
180
175
  */
176
+ export interface Change<T> {
177
+ readonly from?: T;
178
+ readonly to?: T;
179
+ }
180
+ /** A primary key, whichever side it is read from: the entities, the database, or a diff between them. */
181
+ export interface PrimaryKeySchema {
182
+ /** Its columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
183
+ readonly columns: readonly string[];
184
+ /**
185
+ * What the engine calls its constraint, where it names one at all - Postgres's `Member_pkey`, MySQL's
186
+ * literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the database
187
+ * reported will do: a derived one would name a constraint that is not there.
188
+ */
189
+ readonly name?: string;
190
+ }
191
+ /** A table's differences from what its entity declares, or the table to create or drop. */
181
192
  export interface SchemaDiff {
182
193
  /** Qualified where the table has a schema, since it is also the key the table is found under. */
183
194
  readonly tableName: string;
@@ -187,36 +198,10 @@ export interface SchemaDiff {
187
198
  */
188
199
  readonly schema?: string;
189
200
  readonly type: 'create' | 'alter' | 'drop';
190
- /**
191
- * The table's key against the entity's, where their columns differ; `fromName` is the name the
192
- * database reported, which is what a `DROP` needs.
193
- */
194
- readonly primaryKey?: {
195
- readonly from: string[];
196
- readonly to: string[];
197
- readonly fromName?: string;
198
- };
199
- readonly columnsToAdd?: ColumnSchema[];
200
- readonly columnsToAlter?: {
201
- from: ColumnSchema;
202
- to: ColumnSchema;
203
- }[];
204
- readonly columnsToDrop?: string[];
205
- readonly indexesToAdd?: IndexSchema[];
206
- /** Whole rather than by name, so the rollback can create each again. */
207
- readonly indexesToDrop?: IndexSchema[];
208
- readonly foreignKeysToAdd?: ForeignKeySchema[];
209
- /** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
210
- readonly foreignKeysToDrop?: string[];
211
- /**
212
- * A constraint whose referential actions changed. Its own field rather than a pair of entries in
213
- * the two above, because no engine alters an action in place: it is a drop and an add that have to
214
- * travel together, and safe mode has to hold back both or neither.
215
- */
216
- readonly foreignKeysToAlter?: {
217
- readonly from: ForeignKeySchema;
218
- readonly to: ForeignKeySchema;
219
- }[];
201
+ readonly primaryKey?: Change<PrimaryKeySchema>;
202
+ readonly columns?: readonly Change<ColumnSchema>[];
203
+ readonly indexes?: readonly Change<IndexSchema>[];
204
+ readonly foreignKeys?: readonly Change<ForeignKeySchema>[];
220
205
  }
221
206
  /**
222
207
  * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
@@ -248,10 +233,14 @@ export interface DropSchemaOptions {
248
233
  readonly ifExists?: boolean;
249
234
  readonly cascade?: boolean;
250
235
  }
236
+ /** The triggers uql installed on one table, by name, each with the statements that recreate it as it stands. */
237
+ export type InstalledTriggers = ReadonlyMap<string, readonly string[]>;
251
238
  /**
252
239
  * Interface for generating DDL statements from entity metadata
253
240
  */
254
241
  export interface SchemaGenerator {
242
+ /** Whether a column's stored default is the one the entity declares, as the engine reprints it. Absent where columns are not compared. */
243
+ readonly defaultsEqual?: (expected: unknown, actual: unknown) => boolean;
255
244
  /** The whole schema for `entities`, tables then the foreign keys between them, which need every entity at once. */
256
245
  generateCreateSchema(entities: readonly Type<object>[], options?: CreateSchemaOptions): string[];
257
246
  /**
@@ -263,13 +252,16 @@ export interface SchemaGenerator {
263
252
  /** Generate DROP TABLE statement. */
264
253
  generateDropTable(tableName: string, options?: DropSchemaOptions): string;
265
254
  /**
266
- * Generate ALTER TABLE statements based on schema diff
255
+ * What takes the triggers on `entity`'s table from `installed` - each by name, with the statements that
256
+ * recreate it - to what it declares: nothing where the two agree, which is always on MongoDB.
267
257
  */
258
+ generateTriggers(entity: Type<object>, installed?: InstalledTriggers): string[];
259
+ /** The inverse of {@link generateTriggers} from the same `installed`: its triggers dropped, and the ones it dropped restored. */
260
+ generateTriggersDown(entity: Type<object>, installed?: InstalledTriggers): string[];
261
+ /** A `DROP` for each trigger uql owns among `names` on `entity`'s table, whatever the entity declares. */
262
+ generateTriggerDrops(entity: Type<object>, names: readonly string[]): string[];
263
+ /** The statements taking a table through `diff`; its rollback is the diff reversed, see `reverseDiff`. */
268
264
  generateAlterTable(diff: SchemaDiff): string[];
269
- /**
270
- * Generate rollback (down) statements for ALTER TABLE based on schema diff
271
- */
272
- generateAlterTableDown(diff: SchemaDiff): string[];
273
265
  /**
274
266
  * Generate CREATE INDEX statement
275
267
  */
@@ -321,6 +313,13 @@ export interface SchemaGenerator {
321
313
  * Interface for introspecting the current database schema
322
314
  */
323
315
  export interface SchemaIntrospector {
316
+ /**
317
+ * Every trigger uql installed on `table`, by name, each with the statements that recreate it as it
318
+ * stands. The names say which to drop once an entity no longer declares them; the statements are what
319
+ * a rollback puts back, read off the engine rather than recorded anywhere by uql. One table's alone,
320
+ * so reading it never meets a trigger another writer is dropping from some other table.
321
+ */
322
+ ownedTriggers(table: string): Promise<InstalledTriggers>;
324
323
  /**
325
324
  * What this introspector can read back about an index, and so all that diffing may compare.
326
325
  * Comparing a feature it cannot read reports the same drift forever: the entity side declares it,
@@ -22,13 +22,22 @@ export type QueryOptions = {
22
22
  * table look alike. The entity's own filters never count as naming one.
23
23
  */
24
24
  unfiltered?: boolean;
25
- /**
26
- * prefix the query with this.
27
- */
25
+ };
26
+ /**
27
+ * What a statement is rendered with, on top of the options its caller passed. Kept apart from
28
+ * {@link QueryOptions} because that one is public - it is the third argument of every querier method -
29
+ * and none of this is a caller's to set: an alias is the dialect's to choose and to spell.
30
+ */
31
+ export type QueryRenderOptions = QueryOptions & {
32
+ /** The alias columns are read off, escaped by the dialect unless {@link escapedPrefix} spells it. */
28
33
  prefix?: string;
29
34
  /**
30
- * automatically infer the prefix for the query.
35
+ * The prefix already written out, for the one caller whose row is not an identifier: a trigger reads
36
+ * `NEW."col"`, where `NEW` is a record the engine declares, and quoting it names a table that is not
37
+ * in scope. Defaults to {@link prefix} escaped.
31
38
  */
39
+ escapedPrefix?: string;
40
+ /** Whether to infer the alias where none is given. */
32
41
  autoPrefix?: boolean;
33
42
  };
34
43
  /**
@@ -223,6 +223,8 @@ export type QueryWhereElemMatch<U, Raw = QueryRaw> = unknown extends U ? {
223
223
  } : NonNullable<U> extends Scalar ? QueryWhereFieldOperators<NonNullable<U>, Raw> : {
224
224
  [K in keyof NonNullable<U>]?: QueryWhereFieldValue<NonNullable<U>[K], Raw>;
225
225
  };
226
+ /** Every operator a field condition takes, which is what a key of one is once checked. */
227
+ export type QueryWhereFieldOp = keyof QueryWhereFieldOperatorMap<unknown>;
226
228
  /**
227
229
  * Simple relational comparison operators. `Pick`'s constraint ties this back to
228
230
  * {@link QueryWhereFieldOperatorMap} so a rename there breaks this union at compile time.
@@ -251,7 +253,7 @@ type QueryArrayOp = keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$all' | '$s
251
253
  /**
252
254
  * Ordering operators: {@link QueryCompareOp} plus `$between`.
253
255
  */
254
- type QueryOrderedOp = QueryCompareOp | keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$between'>;
256
+ export type QueryOrderedOp = QueryCompareOp | keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$between'>;
255
257
  /**
256
258
  * Vector-only operators. `Pick`'s constraint ties this back to {@link QueryWhereFieldOperatorMap}
257
259
  * so a rename there breaks this union at compile time.
@@ -261,7 +263,7 @@ type QueryVectorOp = keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$near'>;
261
263
  * The operators every field takes. A subtraction, so an operator added to the map without being
262
264
  * classified above is offered on every field: classify it first.
263
265
  */
264
- type QueryCommonOp = Exclude<keyof QueryWhereFieldOperatorMap<unknown>, QueryStringOp | QueryArrayOp | QueryOrderedOp | QueryVectorOp>;
266
+ type QueryCommonOp = Exclude<QueryWhereFieldOp, QueryStringOp | QueryArrayOp | QueryOrderedOp | QueryVectorOp>;
265
267
  /**
266
268
  * Operator keys applicable to a field of type `T`. Brackets prevent union distribution so an
267
269
  * optional field (`string | undefined`) or a literal union (`'a' | 'b'`) gates as one type.