uql-orm 0.42.0 → 0.43.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/querier/httpQuerier.d.ts +3 -3
- package/dist/browser/type/clientQuerier.d.ts +3 -3
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +5 -5
- package/dist/dialect/abstractSqlDialect.d.ts +41 -14
- package/dist/dialect/abstractSqlDialect.js +75 -44
- package/dist/dialect/jsonSql.d.ts +3 -2
- package/dist/dialect/jsonSql.js +7 -5
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/dialect/vectorCast.d.ts +0 -6
- package/dist/dialect/vectorCast.js +0 -8
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/decorator/entity.js +1 -1
- package/dist/entity/decorator/members.d.ts +5 -2
- package/dist/entity/metadata/definition.js +29 -12
- package/dist/maria/mariaDialect.js +4 -3
- package/dist/migrate/builder/tableBuilder.js +5 -4
- package/dist/migrate/drift/driftDetector.js +21 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +8 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +25 -29
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
- package/dist/migrate/introspection/postgresIntrospector.js +1 -1
- package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
- package/dist/migrate/migrator.js +6 -0
- package/dist/migrate/schemaGenerator.d.ts +43 -37
- package/dist/migrate/schemaGenerator.js +163 -150
- package/dist/mongo/mongoDialect.js +22 -8
- package/dist/postgres/postgresDialect.js +1 -1
- package/dist/querier/abstractQuerier.js +18 -7
- package/dist/querier/relationCount.js +9 -7
- package/dist/schema/canonicalType.d.ts +19 -4
- package/dist/schema/canonicalType.js +114 -164
- package/dist/schema/indexDifferences.d.ts +28 -0
- package/dist/schema/indexDifferences.js +46 -0
- package/dist/schema/schemaASTBuilder.js +27 -33
- package/dist/schema/schemaASTDiffer.d.ts +27 -1
- package/dist/schema/schemaASTDiffer.js +59 -19
- package/dist/schema/types.d.ts +46 -7
- package/dist/sqlite/sqliteDialect.d.ts +2 -1
- package/dist/sqlite/sqliteDialect.js +4 -1
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +23 -6
- package/dist/type/migration.d.ts +20 -1
- package/dist/type/query.d.ts +2 -6
- package/dist/util/field.util.d.ts +28 -7
- package/dist/util/field.util.js +56 -48
- package/dist/util/fieldOption.util.d.ts +79 -0
- package/dist/util/fieldOption.util.js +84 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/object.util.js +11 -3
- package/dist/util/relationQuery.util.d.ts +10 -0
- package/dist/util/relationQuery.util.js +22 -2
- package/dist/util/sql.util.d.ts +28 -7
- package/dist/util/sql.util.js +79 -10
- package/package.json +2 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
|
-
import { areTypesEqual, canonicalToSql, fieldOptionsToCanonical, isVectorCategory,
|
|
3
|
+
import { areTypesEqual, canonicalToSql, engineType, fieldOptionsToCanonical, isVectorCategory, } from '../schema/canonicalType.js';
|
|
4
|
+
import { indexSignature } from '../schema/indexDifferences.js';
|
|
4
5
|
import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
6
|
+
import { diffTable } from '../schema/schemaASTDiffer.js';
|
|
7
|
+
import { getKeys, isAutoIncrement, isSoleIdField, qualifyName } from '../util/index.js';
|
|
8
|
+
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
|
|
7
9
|
import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
|
|
8
10
|
import { indexDdlFor } from './ddl/index.js';
|
|
9
11
|
import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
|
|
@@ -47,14 +49,8 @@ export class SqlSchemaGenerator {
|
|
|
47
49
|
/**
|
|
48
50
|
* Primary key type for auto-increment integer IDs
|
|
49
51
|
*/
|
|
50
|
-
get
|
|
51
|
-
return this.dialect.
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Convert FieldOptions to CanonicalType using the unified type system.
|
|
55
|
-
*/
|
|
56
|
-
getCanonicalType(field, fieldType) {
|
|
57
|
-
return fieldOptionsToCanonical(field, fieldType);
|
|
52
|
+
get serialType() {
|
|
53
|
+
return this.dialect.serialType;
|
|
58
54
|
}
|
|
59
55
|
canonicalTypeToSql(type) {
|
|
60
56
|
return canonicalToSql(type, this.dialect);
|
|
@@ -128,6 +124,11 @@ export class SqlSchemaGenerator {
|
|
|
128
124
|
generateAlterTable(diff) {
|
|
129
125
|
const statements = [];
|
|
130
126
|
const tableName = this.escapeId(diff.tableName);
|
|
127
|
+
// Before the columns, because a key column being added cannot be part of the old key, and after
|
|
128
|
+
// it is dropped the table is free to take the new one below.
|
|
129
|
+
if (diff.primaryKey?.from.length) {
|
|
130
|
+
statements.push(this.generateDropPrimaryKeySql(diff.tableName, diff.primaryKey.fromName));
|
|
131
|
+
}
|
|
131
132
|
// Add new columns
|
|
132
133
|
if (diff.columnsToAdd?.length) {
|
|
133
134
|
for (const column of diff.columnsToAdd) {
|
|
@@ -161,11 +162,21 @@ export class SqlSchemaGenerator {
|
|
|
161
162
|
statements.push(this.generateDropIndex(diff.tableName, indexName, diff.schema));
|
|
162
163
|
}
|
|
163
164
|
}
|
|
165
|
+
// Last, so every column it names exists by now.
|
|
166
|
+
if (diff.primaryKey?.to.length) {
|
|
167
|
+
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
|
|
168
|
+
}
|
|
164
169
|
return statements;
|
|
165
170
|
}
|
|
166
171
|
generateAlterTableDown(diff) {
|
|
167
172
|
const statements = [];
|
|
168
173
|
const tableName = this.escapeId(diff.tableName);
|
|
174
|
+
// The key first, mirroring the up direction: a column the up added cannot be dropped below while
|
|
175
|
+
// the new key still names it. Restored under the name the database gave it, which is what the
|
|
176
|
+
// table had before, rather than a derived one that was never on it.
|
|
177
|
+
if (diff.primaryKey?.to.length) {
|
|
178
|
+
statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
|
|
179
|
+
}
|
|
169
180
|
// Reverse column additions by dropping them
|
|
170
181
|
if (diff.columnsToAdd?.length) {
|
|
171
182
|
for (const column of diff.columnsToAdd) {
|
|
@@ -175,7 +186,7 @@ export class SqlSchemaGenerator {
|
|
|
175
186
|
// Reverse column alterations by restoring original schema
|
|
176
187
|
if (diff.columnsToAlter?.length) {
|
|
177
188
|
for (const { from } of diff.columnsToAlter) {
|
|
178
|
-
const colDef = this.generateColumnDefinitionFromSchema(from
|
|
189
|
+
const colDef = this.generateColumnDefinitionFromSchema(from);
|
|
179
190
|
const colStatements = this.generateAlterColumnStatements(diff.tableName, from, colDef);
|
|
180
191
|
statements.push(...colStatements);
|
|
181
192
|
}
|
|
@@ -186,6 +197,9 @@ export class SqlSchemaGenerator {
|
|
|
186
197
|
statements.push(this.generateDropIndex(diff.tableName, index.name, diff.schema));
|
|
187
198
|
}
|
|
188
199
|
}
|
|
200
|
+
if (diff.primaryKey?.from.length) {
|
|
201
|
+
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
|
|
202
|
+
}
|
|
189
203
|
if (diff.columnsToDrop?.length || diff.indexesToDrop?.length) {
|
|
190
204
|
statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes`);
|
|
191
205
|
}
|
|
@@ -205,11 +219,15 @@ export class SqlSchemaGenerator {
|
|
|
205
219
|
return `DROP INDEX IF EXISTS ${this.dialect.escapeQualifiedId(indexName, schema)};`;
|
|
206
220
|
}
|
|
207
221
|
/**
|
|
208
|
-
*
|
|
209
|
-
* and may
|
|
222
|
+
* A column definition from a {@link ColumnSchema}, whose type is already the engine's own spelling
|
|
223
|
+
* and may carry its own size.
|
|
224
|
+
*
|
|
225
|
+
* Kept apart from {@link generateColumnFromNode} rather than folded into it: a `ColumnSchema` has no
|
|
226
|
+
* `enum`, because introspection reads one back as a `CHECK` constraint and not as a property of the
|
|
227
|
+
* column, so only the node knows enough to emit that clause. Both spell the definition through
|
|
228
|
+
* {@link renderColumn}, which is the part that must not be written twice.
|
|
210
229
|
*/
|
|
211
|
-
generateColumnDefinitionFromSchema(column
|
|
212
|
-
const { includePrimaryKey = true, includeUnique = true } = options;
|
|
230
|
+
generateColumnDefinitionFromSchema(column) {
|
|
213
231
|
let type = column.type;
|
|
214
232
|
if (!type.includes('(')) {
|
|
215
233
|
if (column.precision !== undefined) {
|
|
@@ -219,17 +237,7 @@ export class SqlSchemaGenerator {
|
|
|
219
237
|
type += `(${column.length})`;
|
|
220
238
|
}
|
|
221
239
|
}
|
|
222
|
-
|
|
223
|
-
type = type.replace(/\s+PRIMARY\s+KEY/i, '');
|
|
224
|
-
}
|
|
225
|
-
// `includePrimaryKey: false` suppresses the keyword, not the fact: the column is still the primary
|
|
226
|
-
// key, so it must not pick up `NOT NULL` (implied) or `UNIQUE` (redundant) on the way out.
|
|
227
|
-
return this.renderColumn({
|
|
228
|
-
...column,
|
|
229
|
-
type,
|
|
230
|
-
isUnique: column.isUnique && includeUnique,
|
|
231
|
-
declaresPrimaryKey: includePrimaryKey && column.isPrimaryKey,
|
|
232
|
-
});
|
|
240
|
+
return this.renderColumn({ ...column, type });
|
|
233
241
|
}
|
|
234
242
|
/**
|
|
235
243
|
* The one place a column definition is spelled. Both callers reach it - the `ColumnSchema` path above
|
|
@@ -238,9 +246,6 @@ export class SqlSchemaGenerator {
|
|
|
238
246
|
*/
|
|
239
247
|
renderColumn(column) {
|
|
240
248
|
let def = `${this.escapeId(column.name)} ${column.type}`;
|
|
241
|
-
if (column.declaresPrimaryKey && !column.type.includes('PRIMARY KEY')) {
|
|
242
|
-
def += ' PRIMARY KEY';
|
|
243
|
-
}
|
|
244
249
|
if (!column.nullable && !column.isPrimaryKey) {
|
|
245
250
|
def += ' NOT NULL';
|
|
246
251
|
}
|
|
@@ -264,19 +269,21 @@ export class SqlSchemaGenerator {
|
|
|
264
269
|
? ''
|
|
265
270
|
: ` DEFAULT ${formatDefaultValue(column.defaultValue, this.dialect, column.type)}`;
|
|
266
271
|
}
|
|
267
|
-
getSqlType(field
|
|
268
|
-
//
|
|
272
|
+
getSqlType(field) {
|
|
273
|
+
// A foreign key takes the type of the key it points at. A `referencedKey` the target does not
|
|
274
|
+
// have falls through to this column's own options, as the AST builder does with the same case.
|
|
269
275
|
if (field.references) {
|
|
270
|
-
const
|
|
271
|
-
const refMeta = getMeta(refEntity);
|
|
276
|
+
const refMeta = getMeta(field.references());
|
|
272
277
|
const refIdField = refMeta.fields[field.referencedKey ?? soleIdOf(refMeta, 'a foreign key')];
|
|
273
|
-
|
|
278
|
+
if (refIdField) {
|
|
279
|
+
return this.getSqlType({ ...refIdField, references: undefined, isId: undefined, autoIncrement: false });
|
|
280
|
+
}
|
|
274
281
|
}
|
|
275
282
|
// Get canonical type and convert to SQL
|
|
276
|
-
const canonical =
|
|
283
|
+
const canonical = fieldOptionsToCanonical(field);
|
|
277
284
|
// Special case for serial primary keys
|
|
278
285
|
if (isAutoIncrement(field, field.isId === true)) {
|
|
279
|
-
return this.dialect.
|
|
286
|
+
return this.dialect.serialType;
|
|
280
287
|
}
|
|
281
288
|
return this.canonicalTypeToSql(canonical);
|
|
282
289
|
}
|
|
@@ -323,80 +330,90 @@ export class SqlSchemaGenerator {
|
|
|
323
330
|
/**
|
|
324
331
|
* Compare an entity with a database table node and return the differences.
|
|
325
332
|
*/
|
|
333
|
+
/**
|
|
334
|
+
* How this entity differs from the table the database reported, as the migrator's `SchemaDiff`.
|
|
335
|
+
*
|
|
336
|
+
* The comparison itself is {@link diffTable}, the same one drift detection runs, so the two can no
|
|
337
|
+
* longer disagree about what has changed. Only two things are this side's own: the entity becomes a
|
|
338
|
+
* table node first, and types are compared as the *engine* would store them - see `normalizeType`.
|
|
339
|
+
*/
|
|
326
340
|
diffSchema(entity, currentTable) {
|
|
327
341
|
const meta = getMeta(entity);
|
|
342
|
+
const tableName = this.resolveTableName(meta);
|
|
343
|
+
const schema = this.resolveSchema(meta);
|
|
328
344
|
if (!currentTable) {
|
|
329
|
-
return {
|
|
330
|
-
tableName: this.resolveTableName(meta),
|
|
331
|
-
schema: this.resolveSchema(meta),
|
|
332
|
-
type: 'create',
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
const columnsToAdd = [];
|
|
336
|
-
const columnsToAlter = [];
|
|
337
|
-
const columnsToDrop = [];
|
|
338
|
-
const currentColumns = new Map(currentTable.columns);
|
|
339
|
-
const fieldKeys = getKeys(meta.fields);
|
|
340
|
-
for (const key of fieldKeys) {
|
|
341
|
-
const field = meta.fields[key];
|
|
342
|
-
if (!field || field.virtual)
|
|
343
|
-
continue;
|
|
344
|
-
const columnName = this.dialect.resolveColumnName(key, field);
|
|
345
|
-
const currentColumn = currentColumns.get(columnName);
|
|
346
|
-
if (!currentColumn) {
|
|
347
|
-
columnsToAdd.push(this.fieldToColumnSchema(key, field, meta));
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
const desiredColumn = this.fieldToColumnSchema(key, field, meta);
|
|
351
|
-
const currentColumnSchema = this.columnNodeToSchema(currentColumn);
|
|
352
|
-
if (this.columnsNeedAlteration(currentColumnSchema, desiredColumn)) {
|
|
353
|
-
columnsToAlter.push({ from: currentColumnSchema, to: desiredColumn });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
currentColumns.delete(columnName);
|
|
345
|
+
return { tableName, schema, type: 'create' };
|
|
357
346
|
}
|
|
358
|
-
|
|
359
|
-
|
|
347
|
+
// Keyed by the qualified name this generator resolves, which is the key the AST it just built
|
|
348
|
+
// stores the table under.
|
|
349
|
+
const desired = buildEntityAST(this, [entity]).getTable(tableName);
|
|
350
|
+
if (!desired) {
|
|
351
|
+
return undefined;
|
|
360
352
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
353
|
+
// Indexes are matched here rather than by the differ, which pairs them by name so that a changed
|
|
354
|
+
// one reads as one index that altered. A migration needs the opposite: an index already in the
|
|
355
|
+
// table, under whatever name, must not be created again, and one whose shape differs is a
|
|
356
|
+
// separate index rather than a change - no engine alters an index's columns or uniqueness.
|
|
357
|
+
const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
|
|
358
|
+
const indexesToAdd = this.missingIndexes(desired, currentTable);
|
|
359
|
+
const columnDiffs = tableDiff?.columnDiffs ?? [];
|
|
360
|
+
const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
|
|
361
|
+
const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
|
|
362
|
+
const columnsToAlter = columnDiffs.flatMap((it) => it.type === 'alter'
|
|
363
|
+
? [{ from: this.columnNodeToSchema(it.actual), to: this.columnNodeToSchema(it.expected) }]
|
|
364
|
+
: []);
|
|
365
|
+
const primaryKey = tableDiff?.primaryKeyDiff && {
|
|
366
|
+
from: tableDiff.primaryKeyDiff.actual,
|
|
367
|
+
to: tableDiff.primaryKeyDiff.expected,
|
|
368
|
+
fromName: tableDiff.primaryKeyDiff.actualName,
|
|
369
|
+
};
|
|
370
|
+
if (!columnsToAdd.length &&
|
|
371
|
+
!columnsToAlter.length &&
|
|
372
|
+
!columnsToDrop.length &&
|
|
373
|
+
!indexesToAdd.length &&
|
|
374
|
+
!primaryKey) {
|
|
366
375
|
return undefined;
|
|
367
376
|
}
|
|
368
377
|
return {
|
|
369
|
-
tableName
|
|
370
|
-
schema
|
|
378
|
+
tableName,
|
|
379
|
+
schema,
|
|
371
380
|
type: 'alter',
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
381
|
+
primaryKey,
|
|
382
|
+
columnsToAdd: columnsToAdd.length ? columnsToAdd : undefined,
|
|
383
|
+
columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
|
|
384
|
+
columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
|
|
385
|
+
indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
|
|
376
386
|
};
|
|
377
387
|
}
|
|
378
388
|
/**
|
|
379
|
-
*
|
|
380
|
-
* `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
|
|
381
|
-
* up rather than waiting for the table to be created from scratch somewhere else.
|
|
389
|
+
* What the shared differ needs from a dialect: a type as this engine would actually store it.
|
|
382
390
|
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
391
|
+
* `boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite, so two canonical types that differ on
|
|
392
|
+
* paper can be one column in the database. Round-tripping through the engine's own spelling is what
|
|
393
|
+
* stops every such column reporting an alteration on every sync.
|
|
394
|
+
*/
|
|
395
|
+
/**
|
|
396
|
+
* Indexes the entity declares that the table does not already have, in any shape.
|
|
385
397
|
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
398
|
+
* Additive only: an index the entity does not name may well have been created deliberately outside
|
|
399
|
+
* the ORM, and dropping it is a decision for a reviewed migration.
|
|
388
400
|
*/
|
|
389
|
-
missingIndexes(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return
|
|
401
|
+
missingIndexes(desired, currentTable) {
|
|
402
|
+
const present = new Set(currentTable.indexes.map(indexSignature));
|
|
403
|
+
return desired.indexes.filter((index) => !present.has(indexSignature(index))).map(indexNodeToSchema);
|
|
404
|
+
}
|
|
405
|
+
diffOptions() {
|
|
406
|
+
return {
|
|
407
|
+
normalizeType: engineType(this.dialect),
|
|
408
|
+
defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
|
|
409
|
+
};
|
|
395
410
|
}
|
|
396
411
|
columnNodeToSchema(col) {
|
|
397
412
|
return {
|
|
398
413
|
name: col.name,
|
|
399
|
-
|
|
414
|
+
// The same rule `generateColumnFromNode` renders by, so a column added to an existing table
|
|
415
|
+
// gets the type it would have had if the table were created from scratch.
|
|
416
|
+
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
|
|
400
417
|
nullable: col.nullable,
|
|
401
418
|
defaultValue: col.defaultValue,
|
|
402
419
|
isPrimaryKey: col.isPrimaryKey,
|
|
@@ -405,65 +422,17 @@ export class SqlSchemaGenerator {
|
|
|
405
422
|
comment: col.comment,
|
|
406
423
|
};
|
|
407
424
|
}
|
|
408
|
-
/**
|
|
409
|
-
* Convert field options to ColumnSchema. Both sides of a diff are the engine's SQL spelling: what it
|
|
410
|
-
* would create for this field, against what it reported for the existing column.
|
|
411
|
-
*/
|
|
412
|
-
fieldToColumnSchema(fieldKey, field, meta) {
|
|
413
|
-
const isPrimaryKey = field.isId === true;
|
|
414
|
-
return {
|
|
415
|
-
name: this.dialect.resolveColumnName(fieldKey, field),
|
|
416
|
-
type: this.getSqlType(field, field.type),
|
|
417
|
-
nullable: field.nullable ?? !isPrimaryKey,
|
|
418
|
-
defaultValue: field.defaultValue,
|
|
419
|
-
isPrimaryKey,
|
|
420
|
-
isAutoIncrement: isAutoIncrement(field, isPrimaryKey),
|
|
421
|
-
isUnique: field.unique ?? false,
|
|
422
|
-
length: field.length,
|
|
423
|
-
precision: field.precision,
|
|
424
|
-
scale: field.scale,
|
|
425
|
-
comment: field.comment,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Check if two columns differ enough to require alteration
|
|
430
|
-
*/
|
|
431
|
-
columnsNeedAlteration(current, desired) {
|
|
432
|
-
if (current.isPrimaryKey && desired.isPrimaryKey) {
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
if (current.isPrimaryKey !== desired.isPrimaryKey)
|
|
436
|
-
return true;
|
|
437
|
-
if (current.nullable !== desired.nullable)
|
|
438
|
-
return true;
|
|
439
|
-
if (current.isUnique !== desired.isUnique)
|
|
440
|
-
return true;
|
|
441
|
-
if (!this.isTypeEqual(current, desired))
|
|
442
|
-
return true;
|
|
443
|
-
if (!this.isDefaultValueEqual(current.defaultValue, desired.defaultValue))
|
|
444
|
-
return true;
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* Whether two column types are the same *as this engine stores them*.
|
|
449
|
-
*
|
|
450
|
-
* Both sides are SQL spellings, parsed back to canonical so that `INT` and `INTEGER`, or `DATETIME`
|
|
451
|
-
* and `TIMESTAMP`, do not read as a change. Do not "simplify" this into comparing the entity's
|
|
452
|
-
* canonical type against the column's: several canonical types share one storage type per engine
|
|
453
|
-
* (`boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite), so that comparison reports an
|
|
454
|
-
* alteration for those columns on every single sync.
|
|
455
|
-
*/
|
|
456
|
-
isTypeEqual(current, desired) {
|
|
457
|
-
return areTypesEqual(sqlToCanonical(current.type), sqlToCanonical(desired.type));
|
|
458
|
-
}
|
|
459
425
|
/**
|
|
460
426
|
* Compare two default values for equality
|
|
461
427
|
*/
|
|
462
428
|
isDefaultValueEqual(current, desired) {
|
|
463
429
|
if (current === desired)
|
|
464
430
|
return true;
|
|
465
|
-
|
|
466
|
-
|
|
431
|
+
// Both spellings of "no default" are the same fact, and engines disagree on which they report:
|
|
432
|
+
// MariaDB says `null` where MySQL says nothing at all. Reading them as different values asked to
|
|
433
|
+
// `MODIFY` every nullable column, on every sync, forever.
|
|
434
|
+
if (current == null || desired == null)
|
|
435
|
+
return current == null && desired == null;
|
|
467
436
|
const normalize = (value) => {
|
|
468
437
|
if (value === null)
|
|
469
438
|
return 'null';
|
|
@@ -493,9 +462,16 @@ export class SqlSchemaGenerator {
|
|
|
493
462
|
const colDef = this.generateColumnFromNode(indexedVectorColumns.has(col.name) ? { ...col, nullable: false } : col);
|
|
494
463
|
columns.push(colDef);
|
|
495
464
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
465
|
+
// Every key, of any width, as one named constraint beside the checks and foreign keys - so a
|
|
466
|
+
// later `DROP` has something to name. The exception is a dialect whose serial type states the key
|
|
467
|
+
// itself (SQLite's `INTEGER PRIMARY KEY AUTOINCREMENT`, which cannot be split): there the column
|
|
468
|
+
// has already declared it, and saying it again is a second primary key.
|
|
469
|
+
const declaredByColumn = this.dialect.serialDeclaresPrimaryKey && table.primaryKey.length === 1 && table.primaryKey[0].isAutoIncrement;
|
|
470
|
+
if (table.primaryKey.length && !declaredByColumn) {
|
|
471
|
+
const pkColumns = table.primaryKey.map((c) => c.name);
|
|
472
|
+
const pkName = table.primaryKeyName ?? derivedPrimaryKeyName(table.name, pkColumns);
|
|
473
|
+
const pkCols = pkColumns.map((c) => this.escapeId(c)).join(', ');
|
|
474
|
+
constraints.push(`CONSTRAINT ${this.escapeId(pkName)} PRIMARY KEY (${pkCols})`);
|
|
499
475
|
}
|
|
500
476
|
(table.checks ?? []).forEach((check, i) => {
|
|
501
477
|
const name = check.name ?? derivedCheckName(table.name, i + 1);
|
|
@@ -542,8 +518,7 @@ export class SqlSchemaGenerator {
|
|
|
542
518
|
generateColumnFromNode(col) {
|
|
543
519
|
return this.renderColumn({
|
|
544
520
|
...col,
|
|
545
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.
|
|
546
|
-
declaresPrimaryKey: col.isPrimaryKey && col.table.primaryKey.length === 1,
|
|
521
|
+
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
|
|
547
522
|
});
|
|
548
523
|
}
|
|
549
524
|
/**
|
|
@@ -594,6 +569,44 @@ export class SqlSchemaGenerator {
|
|
|
594
569
|
generateDropForeignKeySql(tableName, constraintName) {
|
|
595
570
|
return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
|
|
596
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* `ALTER TABLE ... ADD CONSTRAINT <name> PRIMARY KEY (...)`, the other half of
|
|
574
|
+
* {@link generateDropPrimaryKeySql}. Refused where the engine cannot alter a key at all, by name,
|
|
575
|
+
* rather than emitting DDL it will reject.
|
|
576
|
+
*/
|
|
577
|
+
generateAddPrimaryKeySql(tableName, columns, name) {
|
|
578
|
+
this.assertPrimaryKeyAlterable(tableName);
|
|
579
|
+
const constraintName = this.escapeId(name ?? derivedPrimaryKeyName(tableName, columns));
|
|
580
|
+
const pkCols = columns.map((c) => this.escapeId(c)).join(', ');
|
|
581
|
+
return `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} PRIMARY KEY (${pkCols});`;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Drops whatever key the table has.
|
|
585
|
+
*
|
|
586
|
+
* `constraintName` has to be what the constraint is *actually* called: the name introspection
|
|
587
|
+
* reported for a key the database already had, or the derived one for a key this generator itself
|
|
588
|
+
* added, which is what reversing a migration drops. Guessing either way names nothing. MySQL takes
|
|
589
|
+
* no name at all - a table's key is always `PRIMARY` there.
|
|
590
|
+
*/
|
|
591
|
+
generateDropPrimaryKeySql(tableName, constraintName) {
|
|
592
|
+
this.assertPrimaryKeyAlterable(tableName);
|
|
593
|
+
const table = this.escapeId(tableName);
|
|
594
|
+
if (this.dialect.dropPrimaryKeySyntax === 'DROP PRIMARY KEY') {
|
|
595
|
+
return `ALTER TABLE ${table} DROP PRIMARY KEY;`;
|
|
596
|
+
}
|
|
597
|
+
if (!constraintName) {
|
|
598
|
+
throw new TypeError(`Cannot drop the primary key of "${tableName}": ${this.dialect} names the constraint, and ` +
|
|
599
|
+
'introspection did not report a name for it.');
|
|
600
|
+
}
|
|
601
|
+
return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
|
|
602
|
+
}
|
|
603
|
+
assertPrimaryKeyAlterable(tableName) {
|
|
604
|
+
if (this.features.primaryKeyAlter) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
throw new TypeError(`${this.dialect}: Cannot change the primary key of "${tableName}" - this database has no ALTER ` +
|
|
608
|
+
'for it. Recreate the table in a written migration.');
|
|
609
|
+
}
|
|
597
610
|
}
|
|
598
611
|
/**
|
|
599
612
|
* The entities as an AST, named the way `generator` names things.
|
|
@@ -4,7 +4,7 @@ import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '..
|
|
|
4
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
5
5
|
import { assertSoleId, getMeta, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
7
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, parseSortByCount, someKey, } from '../util/index.js';
|
|
7
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
8
8
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
9
9
|
export const mongoDialectFeatures = {
|
|
10
10
|
explicitJsonCast: false,
|
|
@@ -16,6 +16,7 @@ export const mongoDialectFeatures = {
|
|
|
16
16
|
dropTableCascade: false,
|
|
17
17
|
renameColumn: false,
|
|
18
18
|
foreignKeyAlter: false,
|
|
19
|
+
primaryKeyAlter: false,
|
|
19
20
|
columnComment: false,
|
|
20
21
|
vectorIndexRequiresNotNull: false,
|
|
21
22
|
vectorSupportsLength: false,
|
|
@@ -168,7 +169,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
168
169
|
*/
|
|
169
170
|
relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts) {
|
|
170
171
|
return relOpts.cardinality === 'mm' && relOpts.through
|
|
171
|
-
? this.junctionLookup(relOpts, relMeta,
|
|
172
|
+
? this.junctionLookup(meta, relOpts, relMeta, targetScope, temp, tail, opts)
|
|
172
173
|
: {
|
|
173
174
|
$lookup: {
|
|
174
175
|
from: this.resolveTableName(relMeta),
|
|
@@ -182,22 +183,29 @@ export class MongoDialect extends AbstractDialect {
|
|
|
182
183
|
* ManyToMany counts/tests junction rows, so the target is reached from inside the junction's own
|
|
183
184
|
* lookup - the junction's filters apply too, since a soft-deleted link is not a link.
|
|
184
185
|
*/
|
|
185
|
-
junctionLookup(relOpts, relMeta,
|
|
186
|
+
junctionLookup(meta, relOpts, relMeta, targetScope, temp, tail, opts) {
|
|
186
187
|
const throughEntity = relOpts.through();
|
|
187
188
|
const throughMeta = getMeta(throughEntity);
|
|
188
189
|
const junctionScope = this.renderFilter(throughEntity, this.scopedWhereMap(throughMeta, {}), opts);
|
|
189
190
|
const nested = REL_NESTED_KEY;
|
|
191
|
+
// Both ends are one column here - each `$lookup` matches one field against `_id` - so both sides
|
|
192
|
+
// must be sole-keyed. Sliced rather than indexed positionally: `references[1]` is the parent's
|
|
193
|
+
// *second* column on a composite, a real column of the wrong side.
|
|
194
|
+
assertSoleId(meta, 'MongoDB');
|
|
195
|
+
assertSoleId(relMeta, 'MongoDB');
|
|
196
|
+
const [parentJoin] = parentJoins(relOpts, meta.ids.length);
|
|
197
|
+
const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
|
|
190
198
|
return {
|
|
191
199
|
$lookup: {
|
|
192
200
|
from: this.resolveTableName(throughMeta),
|
|
193
201
|
localField: MongoDialect.ID_KEY,
|
|
194
|
-
foreignField: this.columnOf(throughMeta,
|
|
202
|
+
foreignField: this.columnOf(throughMeta, parentJoin.joined),
|
|
195
203
|
pipeline: [
|
|
196
204
|
...(hasKeys(junctionScope) ? [{ $match: junctionScope }] : []),
|
|
197
205
|
{
|
|
198
206
|
$lookup: {
|
|
199
207
|
from: this.resolveTableName(relMeta),
|
|
200
|
-
localField: this.columnOf(throughMeta,
|
|
208
|
+
localField: this.columnOf(throughMeta, targetColumn),
|
|
201
209
|
foreignField: MongoDialect.ID_KEY,
|
|
202
210
|
pipeline: [...(hasKeys(targetScope) ? [{ $match: targetScope }] : []), { $limit: 1 }],
|
|
203
211
|
as: nested,
|
|
@@ -659,9 +667,15 @@ export class MongoDialect extends AbstractDialect {
|
|
|
659
667
|
* direction joins the parent's `_id` to the target's FK.
|
|
660
668
|
*/
|
|
661
669
|
joinKeys(meta, relMeta, relOpts) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
670
|
+
if (relOpts.cardinality === 'm1') {
|
|
671
|
+
// The target's side, not this entity's: a lookup matches one `localField` against one
|
|
672
|
+
// `foreignField`, so a composite target would join on its first column alone and gather the
|
|
673
|
+
// rows of every key that agrees on it. The other branch is refused by `columnOf` below, whose
|
|
674
|
+
// key *is* an id column; this one names a plain foreign key, so it has to say so itself.
|
|
675
|
+
assertSoleId(relMeta, 'MongoDB');
|
|
676
|
+
return { localField: this.columnOf(meta, relOpts.references[0].local), foreignField: MongoDialect.ID_KEY };
|
|
677
|
+
}
|
|
678
|
+
return { localField: MongoDialect.ID_KEY, foreignField: this.columnOf(relMeta, relOpts.references[0].foreign) };
|
|
665
679
|
}
|
|
666
680
|
/** `[column, key]` for the fields whose stored name differs from their property name, memoized per entity. */
|
|
667
681
|
renamedColumns(meta) {
|
|
@@ -15,7 +15,7 @@ export class PostgresDialect extends PgLikeSqlDialect {
|
|
|
15
15
|
hasNarrowVectorTypes = true;
|
|
16
16
|
upsert(ctx, entity, conflictPaths, payload) {
|
|
17
17
|
// The xmax system column is 0 for a newly inserted row and non-zero for an updated one (MVCC).
|
|
18
|
-
super.upsert(ctx, entity, conflictPaths, payload,
|
|
18
|
+
super.upsert(ctx, entity, conflictPaths, payload, `(xmax = 0) AS ${this.escapeId('_created')}`);
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* `to_regclass` rather than a `::regclass` cast: it answers `NULL` for a table that does not exist
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, LoggerWrapper, parentJoins, parentsIn, parseRelationAtKey, parseRelationQueryValue,
|
|
1
|
+
import { assertSoleId, getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
2
|
+
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, joinedRowKey, LoggerWrapper, parentJoins, parentRowKey, parentsIn, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, targetKeyColumns, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
@@ -306,7 +306,7 @@ export class AbstractQuerier {
|
|
|
306
306
|
const { $select: _select, $exclude: _exclude, $where: _where, ...throughQuery } = relationQuery;
|
|
307
307
|
const throughFounds = await this.findMany(throughEntity, {
|
|
308
308
|
...throughQuery,
|
|
309
|
-
$select:
|
|
309
|
+
$select: joinedColumns(joins),
|
|
310
310
|
$populate: {
|
|
311
311
|
[targetRelKey]: {
|
|
312
312
|
...relationQuery,
|
|
@@ -315,6 +315,8 @@ export class AbstractQuerier {
|
|
|
315
315
|
},
|
|
316
316
|
$where: parentsIn(joins, payload),
|
|
317
317
|
});
|
|
318
|
+
// The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
|
|
319
|
+
// reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
|
|
318
320
|
const founds = throughFounds.map((it) => ({
|
|
319
321
|
...it[targetRelKey],
|
|
320
322
|
...Object.fromEntries(joins.map(({ joined }) => [joined, it[joined]])),
|
|
@@ -343,15 +345,13 @@ export class AbstractQuerier {
|
|
|
343
345
|
for (const child of children) {
|
|
344
346
|
// Every joined column, so two children agreeing on one column of a composite key are not
|
|
345
347
|
// gathered under the same parent.
|
|
346
|
-
|
|
347
|
-
(childrenByParentId[parentId] ??= []).push(child);
|
|
348
|
+
(childrenByParentId[joinedRowKey(joins, child)] ??= []).push(child);
|
|
348
349
|
}
|
|
349
350
|
for (const parent of parents) {
|
|
350
351
|
// `[]` rather than nothing for a parent with no children: a populated to-many is a list the
|
|
351
352
|
// caller asked for, so it maps and counts without a guard, and its type can say so. An
|
|
352
353
|
// unpopulated one stays absent, which is what tells the two apart.
|
|
353
|
-
|
|
354
|
-
parent[relKey] = (childrenByParentId[key] ?? []);
|
|
354
|
+
parent[relKey] = (childrenByParentId[parentRowKey(joins, parent)] ?? []);
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
357
|
async insertRelations(entity, payload) {
|
|
@@ -411,6 +411,11 @@ export class AbstractQuerier {
|
|
|
411
411
|
const relOpts = meta.relations[relKey];
|
|
412
412
|
if (!relOpts)
|
|
413
413
|
return;
|
|
414
|
+
// Here rather than only in the callers below: writing the parent's key into a child is one column
|
|
415
|
+
// per key, so a composite takes a statement per parent. `soleParentColumn` and the sole
|
|
416
|
+
// `targetKeyColumns` under it read the *first* pair, which is a real column of a wrong pairing
|
|
417
|
+
// unless this has run - and this method is `protected`, so a caller can arrive without them.
|
|
418
|
+
assertSoleId(meta, 'saving a relation');
|
|
414
419
|
const relEntity = relOpts.entity();
|
|
415
420
|
const relPayload = relValue;
|
|
416
421
|
switch (relOpts.cardinality) {
|
|
@@ -438,6 +443,12 @@ export class AbstractQuerier {
|
|
|
438
443
|
// would link every parent to a single shared row instead.
|
|
439
444
|
for (const id of ids) {
|
|
440
445
|
const savedIds = await this.saveMany(relEntity, relPayload);
|
|
446
|
+
// A link needs the target's id, and a driver that cannot report one (a MySQL batch mixing
|
|
447
|
+
// supplied and generated keys) would otherwise write a row pointing at `undefined`.
|
|
448
|
+
if (savedIds.some((relId) => relId === undefined)) {
|
|
449
|
+
throw new TypeError(`'${relEntity.name}' rows saved through '${throughEntity.name}' reported no id, so they cannot be linked. ` +
|
|
450
|
+
'Insert them with their own ids, or save the relation in its own statement.');
|
|
451
|
+
}
|
|
441
452
|
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [targetColumn]: relId })));
|
|
442
453
|
}
|
|
443
454
|
}
|