uql-orm 0.78.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 +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- 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 +7 -1
- package/dist/schema/canonicalType.js +35 -15
- 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 +102 -5
- package/dist/type/migration.d.ts +17 -0
- package/dist/type/query.d.ts +13 -4
- package/dist/type/queryRaw.d.ts +8 -1
- package/dist/type/queryRaw.js +10 -2
- 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 +10 -2
- package/dist/util/fieldOption.util.js +17 -2
- package/dist/util/raw.d.ts +14 -1
- package/dist/util/raw.js +45 -13
- 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 -5
|
@@ -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;
|
|
@@ -19,6 +19,7 @@ export declare function canonicalColumnType(sqlType: string, reported?: {
|
|
|
19
19
|
length?: number;
|
|
20
20
|
precision?: number;
|
|
21
21
|
scale?: number;
|
|
22
|
+
dimensions?: number;
|
|
22
23
|
}): CanonicalType;
|
|
23
24
|
/**
|
|
24
25
|
* Convert a canonical type to a SQL type string for a specific dialect instance.
|
|
@@ -50,3 +51,8 @@ export declare function isBreakingTypeChange(from: CanonicalType, to: CanonicalT
|
|
|
50
51
|
* Get the UQL ColumnType that best matches a canonical type.
|
|
51
52
|
*/
|
|
52
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,5 +1,8 @@
|
|
|
1
1
|
// Canonical types, between an engine's SQL types and TypeScript's.
|
|
2
|
+
import { fieldOf, getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
3
|
+
import { QueryRaw } from '../type/index.js';
|
|
2
4
|
import { columnFamily, isIntegerColumn } from '../util/field.util.js';
|
|
5
|
+
import { constantSql } from '../util/raw.js';
|
|
3
6
|
/** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
|
|
4
7
|
export function isVectorCategory(category) {
|
|
5
8
|
return category === 'vector' || category === 'halfvec' || category === 'sparsevec';
|
|
@@ -272,11 +275,23 @@ export function canonicalColumnType(sqlType, reported = {}) {
|
|
|
272
275
|
const base = sqlToCanonical(sqlType);
|
|
273
276
|
return {
|
|
274
277
|
...base,
|
|
275
|
-
length:
|
|
278
|
+
// A vector states its length as `dimensions`, and only a vector may: one bound, named from either side.
|
|
279
|
+
length: reported.dimensions ?? reported.length ?? base.length,
|
|
276
280
|
precision: reported.precision ?? base.precision,
|
|
277
281
|
scale: reported.scale ?? base.scale,
|
|
278
282
|
};
|
|
279
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* The SQL type a field declares, however it named one: `columnType`, the engine's own as a `raw`
|
|
286
|
+
* constant, or a `type` that is a SQL string rather than a constructor.
|
|
287
|
+
*/
|
|
288
|
+
function declaredSqlType(options) {
|
|
289
|
+
const { columnType, type } = options;
|
|
290
|
+
if (columnType instanceof QueryRaw) {
|
|
291
|
+
return constantSql(columnType);
|
|
292
|
+
}
|
|
293
|
+
return columnType ?? (typeof type === 'string' ? type : undefined);
|
|
294
|
+
}
|
|
280
295
|
/**
|
|
281
296
|
* Convert a canonical type to a SQL type string for a specific dialect instance.
|
|
282
297
|
*/
|
|
@@ -339,21 +354,13 @@ export function engineType(dialect) {
|
|
|
339
354
|
* Convert UQL FieldOptions to a canonical type.
|
|
340
355
|
*/
|
|
341
356
|
export function fieldOptionsToCanonical(options) {
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// Infer from type, which is a SQL type string or one of the constructors.
|
|
348
|
-
const { type } = options;
|
|
349
|
-
if (typeof type === 'string') {
|
|
350
|
-
const canonical = sqlToCanonical(type);
|
|
351
|
-
// Propagate explicit dimensions into CanonicalType.length for vector types
|
|
352
|
-
return options.dimensions && isVectorCategory(canonical.category)
|
|
353
|
-
? { ...canonical, length: options.dimensions }
|
|
354
|
-
: canonical;
|
|
357
|
+
// A SQL type is read exactly as an introspected one is, whichever option named it, so a bound stated
|
|
358
|
+
// beside it is read the same way either way.
|
|
359
|
+
const declared = declaredSqlType(options);
|
|
360
|
+
if (declared !== undefined) {
|
|
361
|
+
return canonicalColumnType(declared, options);
|
|
355
362
|
}
|
|
356
|
-
switch (columnFamily(type)) {
|
|
363
|
+
switch (columnFamily(options.type)) {
|
|
357
364
|
case 'numeric':
|
|
358
365
|
// BIGINT for every `Number` without a scale, key or not: a 32-bit column is a migration waiting
|
|
359
366
|
// to happen, and the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
|
|
@@ -450,3 +457,16 @@ export function canonicalToColumnType(type) {
|
|
|
450
457
|
return 'sparsevec';
|
|
451
458
|
}
|
|
452
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.
|