uql-orm 0.79.0 → 0.80.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/browser/uql-browser.min.js.map +2 -2
- package/dist/cockroachdb/cockroachDialect.js +5 -1
- package/dist/dialect/abstractDialect.d.ts +1 -31
- package/dist/dialect/abstractDialect.js +3 -27
- package/dist/dialect/abstractSqlDialect.d.ts +25 -56
- package/dist/dialect/abstractSqlDialect.js +78 -145
- package/dist/dialect/aliases.d.ts +5 -0
- package/dist/dialect/aliases.js +5 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
- package/dist/dialect/operators.d.ts +66 -0
- package/dist/dialect/operators.js +129 -0
- package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
- package/dist/dialect/pgLikeSqlDialect.js +16 -3
- package/dist/entity/decorator/entity.d.ts +6 -1
- package/dist/entity/decorator/entity.js +12 -1
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +6 -1
- package/dist/entity/metadata/definition.js +19 -0
- package/dist/migrate/codegen/entityTypes.js +1 -2
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +2 -0
- package/dist/migrate/ddl/mssqlTableDdl.js +5 -0
- package/dist/migrate/ddl/tableDdl.d.ts +2 -0
- package/dist/migrate/ddl/tableDdl.js +4 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +4 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +10 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +13 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +21 -0
- package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
- package/dist/migrate/introspection/mongoIntrospector.js +4 -0
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +8 -0
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mysqlIntrospector.js +9 -0
- package/dist/migrate/introspection/postgresIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/postgresIntrospector.js +10 -0
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +3 -0
- package/dist/migrate/migrator.d.ts +28 -1
- package/dist/migrate/migrator.js +88 -9
- package/dist/migrate/schemaGenerator.d.ts +12 -1
- package/dist/migrate/schemaGenerator.js +47 -5
- package/dist/migrate/storage/databaseStorage.d.ts +4 -0
- package/dist/migrate/storage/databaseStorage.js +14 -8
- package/dist/migrate/triggerSql.d.ts +24 -0
- package/dist/migrate/triggerSql.js +229 -0
- package/dist/mongo/mongoDialect.d.ts +0 -21
- package/dist/mongo/mongoDialect.js +105 -100
- package/dist/mongo/mongodbQuerier.js +17 -1
- package/dist/mssql/mssqlDialect.d.ts +18 -7
- package/dist/mssql/mssqlDialect.js +77 -33
- package/dist/mssql/mssqlQuerier.js +2 -2
- package/dist/schema/canonicalType.d.ts +6 -1
- package/dist/schema/canonicalType.js +14 -0
- package/dist/schema/schemaASTBuilder.d.ts +2 -8
- package/dist/schema/schemaASTBuilder.js +6 -20
- package/dist/sqlite/sqliteDialect.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.js +12 -3
- package/dist/type/dialect.d.ts +69 -9
- package/dist/type/entity.d.ts +96 -3
- package/dist/type/migration.d.ts +17 -0
- package/dist/type/query.d.ts +13 -4
- package/dist/type/queryWhere.d.ts +4 -2
- package/dist/util/field.util.d.ts +9 -1
- package/dist/util/field.util.js +14 -2
- package/dist/util/fieldOption.util.d.ts +2 -2
- package/dist/util/fieldOption.util.js +2 -2
- package/dist/util/raw.d.ts +9 -1
- package/dist/util/raw.js +36 -11
- package/dist/util/sql.util.d.ts +12 -0
- package/dist/util/sql.util.js +24 -3
- package/dist/util/uqlError.d.ts +2 -0
- package/dist/util/uqlError.js +4 -0
- package/package.json +4 -4
- package/skills/uql-orm/SKILL.md +11 -6
|
@@ -4,7 +4,7 @@ import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
|
|
|
4
4
|
import { jsonArraySlotArgs, jsonPath, jsonSlotArgs } from '../dialect/jsonSql.js';
|
|
5
5
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
6
6
|
import { getMeta } from '../entity/index.js';
|
|
7
|
-
import { fieldOptionsToCanonical } from '../schema/canonicalType.js';
|
|
7
|
+
import { canonicalToSql, fieldOptionsToCanonical, resolveColumnCanonicalType } from '../schema/canonicalType.js';
|
|
8
8
|
import { QueryRaw } from '../type/index.js';
|
|
9
9
|
import { parseQueryLock } from '../type/index.js';
|
|
10
10
|
import { isAutoIncrement } from '../util/field.util.js';
|
|
@@ -13,10 +13,8 @@ import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
|
13
13
|
import { UqlUsageError } from '../util/uqlError.js';
|
|
14
14
|
/** What SQL Server has. */
|
|
15
15
|
const MSSQL_FEATURES = {
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
ifNotExists: false,
|
|
19
|
-
indexIfNotExists: false,
|
|
16
|
+
// No `IF NOT EXISTS` on an index, so `MsSqlIndexDdl` guards the create with a `sys.indexes` lookup.
|
|
17
|
+
indexIfNotExists: true,
|
|
20
18
|
schemas: true,
|
|
21
19
|
dropTableCascade: false,
|
|
22
20
|
foreignKeyAlter: true,
|
|
@@ -40,6 +38,16 @@ const MSSQL_FEATURES = {
|
|
|
40
38
|
narrowVectorTypes: false,
|
|
41
39
|
vectorTuningNeedsTransaction: false,
|
|
42
40
|
serialDeclaresPrimaryKey: false,
|
|
41
|
+
triggers: {
|
|
42
|
+
preamble: 'SET NOCOUNT ON;',
|
|
43
|
+
assignsRow: false,
|
|
44
|
+
body: 'inline',
|
|
45
|
+
guards: 'beginEnd',
|
|
46
|
+
layout: 'tableFirst',
|
|
47
|
+
rows: 'set',
|
|
48
|
+
scope: 'schema',
|
|
49
|
+
before: false,
|
|
50
|
+
},
|
|
43
51
|
};
|
|
44
52
|
/** The `type` `OPENJSON` reports for the JSON scalar an element is compared with; anything else binds as a string. */
|
|
45
53
|
function openJsonType(value) {
|
|
@@ -48,6 +56,14 @@ function openJsonType(value) {
|
|
|
48
56
|
}
|
|
49
57
|
return typeof value === 'boolean' ? 3 : 1;
|
|
50
58
|
}
|
|
59
|
+
/** The table variable an `OUTPUT` fills, `_uql`-prefixed as every name uql invents is. */
|
|
60
|
+
const OUTPUT_TABLE = '@_uql_output';
|
|
61
|
+
/** The one key column and its field, or nothing on a composite key. */
|
|
62
|
+
function soleId(meta) {
|
|
63
|
+
const [key] = meta.ids;
|
|
64
|
+
const field = meta.ids.length === 1 ? meta.fields[key] : undefined;
|
|
65
|
+
return field && { key, field };
|
|
66
|
+
}
|
|
51
67
|
/** Microsoft SQL Server 2017 and up. Identifiers are `"`-quoted, the ANSI spelling `tedious` enables. */
|
|
52
68
|
export class MsSqlDialect extends MergeSqlDialect {
|
|
53
69
|
features = MSSQL_FEATURES;
|
|
@@ -74,6 +90,8 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
74
90
|
maxBindValues = 2100;
|
|
75
91
|
/** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
|
|
76
92
|
returningPosition = 'after-target';
|
|
93
|
+
/** `OUTPUT` reads the written row off the `INSERTED` pseudo-table. */
|
|
94
|
+
returnedRowPrefix = 'INSERTED.';
|
|
77
95
|
insertIdSource = 'returning';
|
|
78
96
|
/** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
|
|
79
97
|
mergeTargetHint = ' WITH (HOLDLOCK)';
|
|
@@ -96,30 +114,20 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
96
114
|
}
|
|
97
115
|
super.pager(ctx, opts, sorted);
|
|
98
116
|
}
|
|
99
|
-
/**
|
|
100
|
-
* `SET IDENTITY_INSERT` around an insert that states a key the engine would generate, which it otherwise
|
|
101
|
-
* refuses; turned off in the same batch, since one table per session may hold it.
|
|
102
|
-
*/
|
|
103
117
|
insert(ctx, entity, payload, opts) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
ctx.append(
|
|
107
|
-
}
|
|
108
|
-
super.insert(ctx, entity, payload, opts);
|
|
109
|
-
if (table) {
|
|
110
|
-
ctx.append(`; SET IDENTITY_INSERT ${table} OFF`);
|
|
111
|
-
}
|
|
118
|
+
this.writeRows(ctx, getMeta(entity), payload, () => {
|
|
119
|
+
super.insert(ctx, entity, payload, opts);
|
|
120
|
+
ctx.append(';');
|
|
121
|
+
});
|
|
112
122
|
}
|
|
113
123
|
/** The table to toggle, or nothing when no record writes a key the engine would have generated. */
|
|
114
|
-
identityInsertTarget(
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const field = meta.ids.length === 1 ? meta.fields[idKey] : undefined;
|
|
118
|
-
if (!field || !isAutoIncrement(field, true)) {
|
|
124
|
+
identityInsertTarget(meta, payload) {
|
|
125
|
+
const id = soleId(meta);
|
|
126
|
+
if (!id || !isAutoIncrement(id.field, true)) {
|
|
119
127
|
return undefined;
|
|
120
128
|
}
|
|
121
129
|
const records = Array.isArray(payload) ? payload : [payload];
|
|
122
|
-
const stated = records.some((record) => record[
|
|
130
|
+
const stated = records.some((record) => record[id.key] !== undefined);
|
|
123
131
|
return stated ? this.escapedTableName(meta) : undefined;
|
|
124
132
|
}
|
|
125
133
|
/** A `DECIMAL` declared `String`, converted before it crosses the wire, where `tedious` would round it. */
|
|
@@ -151,17 +159,44 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
151
159
|
placeholder(index) {
|
|
152
160
|
return `@p${index}`;
|
|
153
161
|
}
|
|
154
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it, and
|
|
164
|
+
* always `INTO` a table variable: SQL Server refuses an `OUTPUT` straight back to the client on a table
|
|
165
|
+
* carrying a trigger, and through a table it works whether one is there or not.
|
|
166
|
+
*/
|
|
155
167
|
returningId(meta) {
|
|
156
168
|
const expression = this.returningIdExpression(meta);
|
|
157
|
-
return expression ? `OUTPUT ${expression}` : '';
|
|
169
|
+
return expression ? `OUTPUT ${expression} INTO ${OUTPUT_TABLE}` : '';
|
|
170
|
+
}
|
|
171
|
+
upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
|
|
172
|
+
this.writeRows(ctx, getMeta(entity), payload, () => super.upsert(ctx, entity, conflictPaths, payload, extraReturning));
|
|
158
173
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
/**
|
|
175
|
+
* A statement writing rows, `write`, wrapped in what SQL Server needs around one. The table variable its
|
|
176
|
+
* `OUTPUT` fills, typed as the id column is, and the read handing that back, uncounted so the batch
|
|
177
|
+
* reports the rows written alone; none on a composite key, which has no id to hand back. And `SET
|
|
178
|
+
* IDENTITY_INSERT` where a row states a key the engine would generate, turned off in the same batch.
|
|
179
|
+
*/
|
|
180
|
+
writeRows(ctx, meta, payload, write) {
|
|
181
|
+
const id = soleId(meta);
|
|
182
|
+
const column = this.escapeId('id');
|
|
183
|
+
const identity = this.identityInsertTarget(meta, payload);
|
|
184
|
+
if (id) {
|
|
185
|
+
ctx.append(`DECLARE ${OUTPUT_TABLE} TABLE (${column} ${canonicalToSql(resolveColumnCanonicalType(id.field), this)}); `);
|
|
186
|
+
}
|
|
187
|
+
if (identity) {
|
|
188
|
+
ctx.append(`SET IDENTITY_INSERT ${identity} ON; `);
|
|
189
|
+
}
|
|
190
|
+
write();
|
|
191
|
+
if (identity) {
|
|
192
|
+
ctx.append(` SET IDENTITY_INSERT ${identity} OFF;`);
|
|
193
|
+
}
|
|
194
|
+
if (id) {
|
|
195
|
+
ctx.append(` SET NOCOUNT ON; SELECT ${column} FROM ${OUTPUT_TABLE}; SET NOCOUNT OFF;`);
|
|
196
|
+
}
|
|
162
197
|
}
|
|
163
198
|
mergeReturning(expression) {
|
|
164
|
-
return `OUTPUT ${expression}`;
|
|
199
|
+
return `OUTPUT ${expression} INTO ${OUTPUT_TABLE}`;
|
|
165
200
|
}
|
|
166
201
|
/** A row lock is a hint on the table here, which `rowLocks.placement` says instead of a trailing clause. */
|
|
167
202
|
lockHint(q) {
|
|
@@ -217,9 +252,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
217
252
|
}
|
|
218
253
|
/** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
|
|
219
254
|
createSchemaSql(schema) {
|
|
220
|
-
|
|
221
|
-
const quoted = escapeSingleQuotes(this.escapeId(schema, true));
|
|
222
|
-
return `IF SCHEMA_ID(N'${literal}') IS NULL EXEC(N'CREATE SCHEMA ${quoted}')`;
|
|
255
|
+
return `IF SCHEMA_ID(${this.escape(schema)}) IS NULL EXEC(${this.escape(`CREATE SCHEMA ${this.escapeId(schema, true)}`)})`;
|
|
223
256
|
}
|
|
224
257
|
/** The estimate the engine already keeps per partition, live without a stats refresh. */
|
|
225
258
|
estimatedCount(ctx, entity) {
|
|
@@ -229,8 +262,19 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
229
262
|
` JOIN sys.schemas s ON s.schema_id = o.schema_id` +
|
|
230
263
|
` WHERE p.index_id IN (0, 1) AND o.name = `);
|
|
231
264
|
ctx.addValue(this.resolveTableAlias(meta));
|
|
265
|
+
// The connection's own default schema where the entity names none, as the introspector reads it.
|
|
266
|
+
const schema = this.resolveSchema(meta);
|
|
232
267
|
ctx.append(' AND s.name = ');
|
|
233
|
-
|
|
268
|
+
if (schema === undefined) {
|
|
269
|
+
ctx.append('SCHEMA_NAME()');
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
ctx.addValue(schema);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Null-safe on every version, `EXCEPT` comparing NULLs as equal: `IS DISTINCT FROM` needs 2022. */
|
|
276
|
+
neExpr(field, ph) {
|
|
277
|
+
return `EXISTS (SELECT ${field} EXCEPT SELECT ${ph})`;
|
|
234
278
|
}
|
|
235
279
|
numericCast(expr) {
|
|
236
280
|
return `TRY_CAST(${expr} AS FLOAT)`;
|
|
@@ -25,8 +25,8 @@ export class MsSqlQuerier extends AbstractPoolQuerier {
|
|
|
25
25
|
async internalRun(query, values) {
|
|
26
26
|
const res = await this.#request(values).query(query);
|
|
27
27
|
return this.buildUpdateResult({
|
|
28
|
-
// `rowsAffected` carries one entry per statement, and a
|
|
29
|
-
//
|
|
28
|
+
// `rowsAffected` carries one entry per statement, and a write runs in a batch of several - the table
|
|
29
|
+
// variable its ids go through, the read handing them back uncounted - so the counts are summed.
|
|
30
30
|
changes: res.rowsAffected.reduce((total, count) => total + count, 0),
|
|
31
31
|
rows: decodeWireTypes(res.recordset, res.recordset?.columns),
|
|
32
32
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
2
2
|
import type { VectorCast } from '../dialect/vectorCast.js';
|
|
3
|
-
import type { ColumnType, FieldOptions } from '../type/entity.js';
|
|
3
|
+
import type { ColumnType, EntityGetter, FieldMeta, FieldOptions } from '../type/entity.js';
|
|
4
4
|
import type { CanonicalType, TypeCategory } from './types.js';
|
|
5
5
|
/** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
|
|
6
6
|
export declare function isVectorCategory(category: TypeCategory | undefined): category is VectorCast;
|
|
@@ -51,3 +51,8 @@ export declare function isBreakingTypeChange(from: CanonicalType, to: CanonicalT
|
|
|
51
51
|
* Get the UQL ColumnType that best matches a canonical type.
|
|
52
52
|
*/
|
|
53
53
|
export declare function canonicalToColumnType(type: CanonicalType): ColumnType;
|
|
54
|
+
/**
|
|
55
|
+
* A field's canonical type, taken from the referenced key where the field gave `references` and no
|
|
56
|
+
* `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveColumnCanonicalType(field: FieldMeta, seen?: Set<EntityGetter>): CanonicalType;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Canonical types, between an engine's SQL types and TypeScript's.
|
|
2
|
+
import { fieldOf, getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
2
3
|
import { QueryRaw } from '../type/index.js';
|
|
3
4
|
import { columnFamily, isIntegerColumn } from '../util/field.util.js';
|
|
4
5
|
import { constantSql } from '../util/raw.js';
|
|
@@ -456,3 +457,16 @@ export function canonicalToColumnType(type) {
|
|
|
456
457
|
return 'sparsevec';
|
|
457
458
|
}
|
|
458
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* A field's canonical type, taken from the referenced key where the field gave `references` and no
|
|
462
|
+
* `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
|
|
463
|
+
*/
|
|
464
|
+
export function resolveColumnCanonicalType(field, seen = new Set()) {
|
|
465
|
+
const hasExplicitType = !!field.columnType || !field.typeFromReference;
|
|
466
|
+
if (!hasExplicitType && field.references && !seen.has(field.references)) {
|
|
467
|
+
seen.add(field.references);
|
|
468
|
+
const referencedMeta = getMeta(field.references());
|
|
469
|
+
return resolveColumnCanonicalType(fieldOf(referencedMeta, soleIdOf(referencedMeta, 'a foreign key')), seen);
|
|
470
|
+
}
|
|
471
|
+
return fieldOptionsToCanonical(field);
|
|
472
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { EntityMeta, EntityWhereMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, EntityWhereMeta, FieldOptions, Type } from '../type/index.js';
|
|
3
2
|
import type { NamingStrategy } from '../type/namingStrategy.js';
|
|
4
3
|
import { SchemaAST } from './schemaAST.js';
|
|
5
|
-
import { type
|
|
4
|
+
import { type ForeignKeyAction } from './types.js';
|
|
6
5
|
/**
|
|
7
6
|
* Options for building SchemaAST from entities.
|
|
8
7
|
*/
|
|
@@ -34,8 +33,3 @@ export interface BuildSchemaASTOptions {
|
|
|
34
33
|
* resolves against a table another entity declares, and an index against the columns of its own.
|
|
35
34
|
*/
|
|
36
35
|
export declare function buildSchemaAST(entities: readonly Type<object>[], options?: BuildSchemaASTOptions): SchemaAST;
|
|
37
|
-
/**
|
|
38
|
-
* A field's canonical type, taken from the referenced key where the field gave `references` and no
|
|
39
|
-
* `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
|
|
40
|
-
*/
|
|
41
|
-
export declare function resolveColumnCanonicalType(field: FieldMeta, seen?: Set<EntityGetter>): CanonicalType;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { fieldOf, foreignKeysOf, getMeta
|
|
1
|
+
import { fieldOf, foreignKeysOf, getMeta } from '../entity/metadata/definition.js';
|
|
2
2
|
import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../util/ddlExpression.util.js';
|
|
3
3
|
import { fulltextWeights, textWeightSteps } from '../util/dialect.util.js';
|
|
4
|
-
import { isInlinedExpression } from '../util/field.util.js';
|
|
5
|
-
import { isSoleIdField } from '../util/field.util.js';
|
|
6
|
-
import { isAutoIncrement } from '../util/field.util.js';
|
|
4
|
+
import { isAutoIncrement, isInlinedExpression, isSoleIdField } from '../util/field.util.js';
|
|
7
5
|
import { definedEntries } from '../util/object.util.js';
|
|
8
6
|
import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
|
|
9
|
-
import {
|
|
7
|
+
import { resolveColumnCanonicalType } from './canonicalType.js';
|
|
10
8
|
import { createTableNode, SchemaAST } from './schemaAST.js';
|
|
11
|
-
import { DEFAULT_FOREIGN_KEY_ACTION
|
|
9
|
+
import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
|
|
12
10
|
/**
|
|
13
11
|
* Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
|
|
14
12
|
*
|
|
@@ -40,19 +38,6 @@ export function buildSchemaAST(entities, options = {}) {
|
|
|
40
38
|
function refuseDdl() {
|
|
41
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');
|
|
42
40
|
}
|
|
43
|
-
/**
|
|
44
|
-
* A field's canonical type, taken from the referenced key where the field gave `references` and no
|
|
45
|
-
* `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
|
|
46
|
-
*/
|
|
47
|
-
export function resolveColumnCanonicalType(field, seen = new Set()) {
|
|
48
|
-
const hasExplicitType = !!field.columnType || !field.typeFromReference;
|
|
49
|
-
if (!hasExplicitType && field.references && !seen.has(field.references)) {
|
|
50
|
-
seen.add(field.references);
|
|
51
|
-
const referencedMeta = getMeta(field.references());
|
|
52
|
-
return resolveColumnCanonicalType(fieldOf(referencedMeta, soleIdOf(referencedMeta, 'a foreign key')), seen);
|
|
53
|
-
}
|
|
54
|
-
return fieldOptionsToCanonical(field);
|
|
55
|
-
}
|
|
56
41
|
/**
|
|
57
42
|
* Add a table from entity metadata.
|
|
58
43
|
*/
|
|
@@ -80,7 +65,8 @@ function addTableFromEntity(ctx, meta) {
|
|
|
80
65
|
isPrimaryKey,
|
|
81
66
|
isAutoIncrement: isAutoIncrement(field, isSoleKey),
|
|
82
67
|
isUnique: field.unique ?? false,
|
|
83
|
-
|
|
68
|
+
// A stamp is filled by a trigger, so it is a column like any other; only `stored: true` generates.
|
|
69
|
+
generatedAs: field.stored === true && field.computed ? ctx.compileDdl(field.computed, meta.entity) : undefined,
|
|
84
70
|
comment: field.comment,
|
|
85
71
|
enum: field.enum,
|
|
86
72
|
table,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
118
|
-
return
|
|
126
|
+
neExpr(field, ph) {
|
|
127
|
+
return `${field} IS NOT ${ph}`;
|
|
119
128
|
}
|
|
120
129
|
normalizeValue(value) {
|
|
121
130
|
if (value instanceof Date)
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityMeta, UpdatePayload } from './entity.js';
|
|
2
|
-
import type { Query, QueryConflictPaths,
|
|
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 =
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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.
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
258
|
-
|
|
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,
|