uql-orm 0.42.0 → 0.42.1
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 +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +31 -2
- package/dist/dialect/abstractSqlDialect.js +41 -10
- 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/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/decorator/entity.js +1 -1
- package/dist/maria/mariaDialect.js +2 -1
- package/dist/migrate/builder/tableBuilder.js +5 -4
- package/dist/migrate/drift/driftDetector.js +16 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +7 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +24 -28
- 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 -33
- package/dist/migrate/schemaGenerator.js +156 -139
- 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/indexDifferences.d.ts +28 -0
- package/dist/schema/indexDifferences.js +46 -0
- package/dist/schema/schemaASTBuilder.js +26 -32
- package/dist/schema/schemaASTDiffer.d.ts +27 -1
- package/dist/schema/schemaASTDiffer.js +54 -18
- 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/migration.d.ts +19 -0
- package/dist/util/field.util.d.ts +11 -1
- package/dist/util/field.util.js +12 -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 +24 -7
- package/dist/util/sql.util.js +75 -10
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { areTypesEqual, canonicalToSql, fieldOptionsToCanonical, isVectorCategory, sqlToCanonical, } 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,8 +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
|
+
get serialType() {
|
|
53
|
+
return this.dialect.serialType;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
56
|
* Convert FieldOptions to CanonicalType using the unified type system.
|
|
@@ -128,6 +130,11 @@ export class SqlSchemaGenerator {
|
|
|
128
130
|
generateAlterTable(diff) {
|
|
129
131
|
const statements = [];
|
|
130
132
|
const tableName = this.escapeId(diff.tableName);
|
|
133
|
+
// Before the columns, because a key column being added cannot be part of the old key, and after
|
|
134
|
+
// it is dropped the table is free to take the new one below.
|
|
135
|
+
if (diff.primaryKey?.from.length) {
|
|
136
|
+
statements.push(this.generateDropPrimaryKeySql(diff.tableName, diff.primaryKey.fromName));
|
|
137
|
+
}
|
|
131
138
|
// Add new columns
|
|
132
139
|
if (diff.columnsToAdd?.length) {
|
|
133
140
|
for (const column of diff.columnsToAdd) {
|
|
@@ -161,11 +168,21 @@ export class SqlSchemaGenerator {
|
|
|
161
168
|
statements.push(this.generateDropIndex(diff.tableName, indexName, diff.schema));
|
|
162
169
|
}
|
|
163
170
|
}
|
|
171
|
+
// Last, so every column it names exists by now.
|
|
172
|
+
if (diff.primaryKey?.to.length) {
|
|
173
|
+
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
|
|
174
|
+
}
|
|
164
175
|
return statements;
|
|
165
176
|
}
|
|
166
177
|
generateAlterTableDown(diff) {
|
|
167
178
|
const statements = [];
|
|
168
179
|
const tableName = this.escapeId(diff.tableName);
|
|
180
|
+
// The key first, mirroring the up direction: a column the up added cannot be dropped below while
|
|
181
|
+
// the new key still names it. Restored under the name the database gave it, which is what the
|
|
182
|
+
// table had before, rather than a derived one that was never on it.
|
|
183
|
+
if (diff.primaryKey?.to.length) {
|
|
184
|
+
statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
|
|
185
|
+
}
|
|
169
186
|
// Reverse column additions by dropping them
|
|
170
187
|
if (diff.columnsToAdd?.length) {
|
|
171
188
|
for (const column of diff.columnsToAdd) {
|
|
@@ -175,7 +192,7 @@ export class SqlSchemaGenerator {
|
|
|
175
192
|
// Reverse column alterations by restoring original schema
|
|
176
193
|
if (diff.columnsToAlter?.length) {
|
|
177
194
|
for (const { from } of diff.columnsToAlter) {
|
|
178
|
-
const colDef = this.generateColumnDefinitionFromSchema(from
|
|
195
|
+
const colDef = this.generateColumnDefinitionFromSchema(from);
|
|
179
196
|
const colStatements = this.generateAlterColumnStatements(diff.tableName, from, colDef);
|
|
180
197
|
statements.push(...colStatements);
|
|
181
198
|
}
|
|
@@ -186,6 +203,9 @@ export class SqlSchemaGenerator {
|
|
|
186
203
|
statements.push(this.generateDropIndex(diff.tableName, index.name, diff.schema));
|
|
187
204
|
}
|
|
188
205
|
}
|
|
206
|
+
if (diff.primaryKey?.from.length) {
|
|
207
|
+
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
|
|
208
|
+
}
|
|
189
209
|
if (diff.columnsToDrop?.length || diff.indexesToDrop?.length) {
|
|
190
210
|
statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes`);
|
|
191
211
|
}
|
|
@@ -205,11 +225,15 @@ export class SqlSchemaGenerator {
|
|
|
205
225
|
return `DROP INDEX IF EXISTS ${this.dialect.escapeQualifiedId(indexName, schema)};`;
|
|
206
226
|
}
|
|
207
227
|
/**
|
|
208
|
-
*
|
|
209
|
-
* and may
|
|
228
|
+
* A column definition from a {@link ColumnSchema}, whose type is already the engine's own spelling
|
|
229
|
+
* and may carry its own size.
|
|
230
|
+
*
|
|
231
|
+
* Kept apart from {@link generateColumnFromNode} rather than folded into it: a `ColumnSchema` has no
|
|
232
|
+
* `enum`, because introspection reads one back as a `CHECK` constraint and not as a property of the
|
|
233
|
+
* column, so only the node knows enough to emit that clause. Both spell the definition through
|
|
234
|
+
* {@link renderColumn}, which is the part that must not be written twice.
|
|
210
235
|
*/
|
|
211
|
-
generateColumnDefinitionFromSchema(column
|
|
212
|
-
const { includePrimaryKey = true, includeUnique = true } = options;
|
|
236
|
+
generateColumnDefinitionFromSchema(column) {
|
|
213
237
|
let type = column.type;
|
|
214
238
|
if (!type.includes('(')) {
|
|
215
239
|
if (column.precision !== undefined) {
|
|
@@ -219,17 +243,7 @@ export class SqlSchemaGenerator {
|
|
|
219
243
|
type += `(${column.length})`;
|
|
220
244
|
}
|
|
221
245
|
}
|
|
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
|
-
});
|
|
246
|
+
return this.renderColumn({ ...column, type });
|
|
233
247
|
}
|
|
234
248
|
/**
|
|
235
249
|
* The one place a column definition is spelled. Both callers reach it - the `ColumnSchema` path above
|
|
@@ -238,9 +252,6 @@ export class SqlSchemaGenerator {
|
|
|
238
252
|
*/
|
|
239
253
|
renderColumn(column) {
|
|
240
254
|
let def = `${this.escapeId(column.name)} ${column.type}`;
|
|
241
|
-
if (column.declaresPrimaryKey && !column.type.includes('PRIMARY KEY')) {
|
|
242
|
-
def += ' PRIMARY KEY';
|
|
243
|
-
}
|
|
244
255
|
if (!column.nullable && !column.isPrimaryKey) {
|
|
245
256
|
def += ' NOT NULL';
|
|
246
257
|
}
|
|
@@ -264,7 +275,7 @@ export class SqlSchemaGenerator {
|
|
|
264
275
|
? ''
|
|
265
276
|
: ` DEFAULT ${formatDefaultValue(column.defaultValue, this.dialect, column.type)}`;
|
|
266
277
|
}
|
|
267
|
-
getSqlType(field, fieldType) {
|
|
278
|
+
getSqlType(field, fieldType, isSoleKey = field.isId === true) {
|
|
268
279
|
// If field has a reference, inherit type from the target primary key
|
|
269
280
|
if (field.references) {
|
|
270
281
|
const refEntity = field.references();
|
|
@@ -275,8 +286,8 @@ export class SqlSchemaGenerator {
|
|
|
275
286
|
// Get canonical type and convert to SQL
|
|
276
287
|
const canonical = this.getCanonicalType(field, fieldType);
|
|
277
288
|
// Special case for serial primary keys
|
|
278
|
-
if (isAutoIncrement(field,
|
|
279
|
-
return this.dialect.
|
|
289
|
+
if (isAutoIncrement(field, isSoleKey)) {
|
|
290
|
+
return this.dialect.serialType;
|
|
280
291
|
}
|
|
281
292
|
return this.canonicalTypeToSql(canonical);
|
|
282
293
|
}
|
|
@@ -323,80 +334,90 @@ export class SqlSchemaGenerator {
|
|
|
323
334
|
/**
|
|
324
335
|
* Compare an entity with a database table node and return the differences.
|
|
325
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* How this entity differs from the table the database reported, as the migrator's `SchemaDiff`.
|
|
339
|
+
*
|
|
340
|
+
* The comparison itself is {@link diffTable}, the same one drift detection runs, so the two can no
|
|
341
|
+
* longer disagree about what has changed. Only two things are this side's own: the entity becomes a
|
|
342
|
+
* table node first, and types are compared as the *engine* would store them - see `normalizeType`.
|
|
343
|
+
*/
|
|
326
344
|
diffSchema(entity, currentTable) {
|
|
327
345
|
const meta = getMeta(entity);
|
|
346
|
+
const tableName = this.resolveTableName(meta);
|
|
347
|
+
const schema = this.resolveSchema(meta);
|
|
328
348
|
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);
|
|
349
|
+
return { tableName, schema, type: 'create' };
|
|
357
350
|
}
|
|
358
|
-
|
|
359
|
-
|
|
351
|
+
// Keyed by the qualified name this generator resolves, which is the key the AST it just built
|
|
352
|
+
// stores the table under.
|
|
353
|
+
const desired = buildEntityAST(this, [entity]).getTable(tableName);
|
|
354
|
+
if (!desired) {
|
|
355
|
+
return undefined;
|
|
360
356
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
357
|
+
// Indexes are matched here rather than by the differ, which pairs them by name so that a changed
|
|
358
|
+
// one reads as one index that altered. A migration needs the opposite: an index already in the
|
|
359
|
+
// table, under whatever name, must not be created again, and one whose shape differs is a
|
|
360
|
+
// separate index rather than a change - no engine alters an index's columns or uniqueness.
|
|
361
|
+
const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
|
|
362
|
+
const indexesToAdd = this.missingIndexes(desired, currentTable);
|
|
363
|
+
const columnDiffs = tableDiff?.columnDiffs ?? [];
|
|
364
|
+
const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
|
|
365
|
+
const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
|
|
366
|
+
const columnsToAlter = columnDiffs.flatMap((it) => it.type === 'alter'
|
|
367
|
+
? [{ from: this.columnNodeToSchema(it.actual), to: this.columnNodeToSchema(it.expected) }]
|
|
368
|
+
: []);
|
|
369
|
+
const primaryKey = tableDiff?.primaryKeyDiff && {
|
|
370
|
+
from: tableDiff.primaryKeyDiff.actual,
|
|
371
|
+
to: tableDiff.primaryKeyDiff.expected,
|
|
372
|
+
fromName: tableDiff.primaryKeyDiff.actualName,
|
|
373
|
+
};
|
|
374
|
+
if (!columnsToAdd.length &&
|
|
375
|
+
!columnsToAlter.length &&
|
|
376
|
+
!columnsToDrop.length &&
|
|
377
|
+
!indexesToAdd.length &&
|
|
378
|
+
!primaryKey) {
|
|
366
379
|
return undefined;
|
|
367
380
|
}
|
|
368
381
|
return {
|
|
369
|
-
tableName
|
|
370
|
-
schema
|
|
382
|
+
tableName,
|
|
383
|
+
schema,
|
|
371
384
|
type: 'alter',
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
385
|
+
primaryKey,
|
|
386
|
+
columnsToAdd: columnsToAdd.length ? columnsToAdd : undefined,
|
|
387
|
+
columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
|
|
388
|
+
columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
|
|
389
|
+
indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
|
|
376
390
|
};
|
|
377
391
|
}
|
|
378
392
|
/**
|
|
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.
|
|
393
|
+
* What the shared differ needs from a dialect: a type as this engine would actually store it.
|
|
382
394
|
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
395
|
+
* `boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite, so two canonical types that differ on
|
|
396
|
+
* paper can be one column in the database. Round-tripping through the engine's own spelling is what
|
|
397
|
+
* stops every such column reporting an alteration on every sync.
|
|
398
|
+
*/
|
|
399
|
+
/**
|
|
400
|
+
* Indexes the entity declares that the table does not already have, in any shape.
|
|
385
401
|
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
402
|
+
* Additive only: an index the entity does not name may well have been created deliberately outside
|
|
403
|
+
* the ORM, and dropping it is a decision for a reviewed migration.
|
|
388
404
|
*/
|
|
389
|
-
missingIndexes(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return
|
|
405
|
+
missingIndexes(desired, currentTable) {
|
|
406
|
+
const present = new Set(currentTable.indexes.map(indexSignature));
|
|
407
|
+
return desired.indexes.filter((index) => !present.has(indexSignature(index))).map(indexNodeToSchema);
|
|
408
|
+
}
|
|
409
|
+
diffOptions() {
|
|
410
|
+
return {
|
|
411
|
+
normalizeType: (type) => sqlToCanonical(this.canonicalTypeToSql(type)),
|
|
412
|
+
defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
|
|
413
|
+
};
|
|
395
414
|
}
|
|
396
415
|
columnNodeToSchema(col) {
|
|
397
416
|
return {
|
|
398
417
|
name: col.name,
|
|
399
|
-
|
|
418
|
+
// The same rule `generateColumnFromNode` renders by, so a column added to an existing table
|
|
419
|
+
// gets the type it would have had if the table were created from scratch.
|
|
420
|
+
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
|
|
400
421
|
nullable: col.nullable,
|
|
401
422
|
defaultValue: col.defaultValue,
|
|
402
423
|
isPrimaryKey: col.isPrimaryKey,
|
|
@@ -405,65 +426,17 @@ export class SqlSchemaGenerator {
|
|
|
405
426
|
comment: col.comment,
|
|
406
427
|
};
|
|
407
428
|
}
|
|
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
429
|
/**
|
|
460
430
|
* Compare two default values for equality
|
|
461
431
|
*/
|
|
462
432
|
isDefaultValueEqual(current, desired) {
|
|
463
433
|
if (current === desired)
|
|
464
434
|
return true;
|
|
465
|
-
|
|
466
|
-
|
|
435
|
+
// Both spellings of "no default" are the same fact, and engines disagree on which they report:
|
|
436
|
+
// MariaDB says `null` where MySQL says nothing at all. Reading them as different values asked to
|
|
437
|
+
// `MODIFY` every nullable column, on every sync, forever.
|
|
438
|
+
if (current == null || desired == null)
|
|
439
|
+
return current == null && desired == null;
|
|
467
440
|
const normalize = (value) => {
|
|
468
441
|
if (value === null)
|
|
469
442
|
return 'null';
|
|
@@ -493,9 +466,16 @@ export class SqlSchemaGenerator {
|
|
|
493
466
|
const colDef = this.generateColumnFromNode(indexedVectorColumns.has(col.name) ? { ...col, nullable: false } : col);
|
|
494
467
|
columns.push(colDef);
|
|
495
468
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
469
|
+
// Every key, of any width, as one named constraint beside the checks and foreign keys - so a
|
|
470
|
+
// later `DROP` has something to name. The exception is a dialect whose serial type states the key
|
|
471
|
+
// itself (SQLite's `INTEGER PRIMARY KEY AUTOINCREMENT`, which cannot be split): there the column
|
|
472
|
+
// has already declared it, and saying it again is a second primary key.
|
|
473
|
+
const declaredByColumn = this.dialect.serialDeclaresPrimaryKey && table.primaryKey.length === 1 && table.primaryKey[0].isAutoIncrement;
|
|
474
|
+
if (table.primaryKey.length && !declaredByColumn) {
|
|
475
|
+
const pkColumns = table.primaryKey.map((c) => c.name);
|
|
476
|
+
const pkName = table.primaryKeyName ?? derivedPrimaryKeyName(table.name, pkColumns);
|
|
477
|
+
const pkCols = pkColumns.map((c) => this.escapeId(c)).join(', ');
|
|
478
|
+
constraints.push(`CONSTRAINT ${this.escapeId(pkName)} PRIMARY KEY (${pkCols})`);
|
|
499
479
|
}
|
|
500
480
|
(table.checks ?? []).forEach((check, i) => {
|
|
501
481
|
const name = check.name ?? derivedCheckName(table.name, i + 1);
|
|
@@ -542,8 +522,7 @@ export class SqlSchemaGenerator {
|
|
|
542
522
|
generateColumnFromNode(col) {
|
|
543
523
|
return this.renderColumn({
|
|
544
524
|
...col,
|
|
545
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.
|
|
546
|
-
declaresPrimaryKey: col.isPrimaryKey && col.table.primaryKey.length === 1,
|
|
525
|
+
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
|
|
547
526
|
});
|
|
548
527
|
}
|
|
549
528
|
/**
|
|
@@ -594,6 +573,44 @@ export class SqlSchemaGenerator {
|
|
|
594
573
|
generateDropForeignKeySql(tableName, constraintName) {
|
|
595
574
|
return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
|
|
596
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* `ALTER TABLE ... ADD CONSTRAINT <name> PRIMARY KEY (...)`, the other half of
|
|
578
|
+
* {@link generateDropPrimaryKeySql}. Refused where the engine cannot alter a key at all, by name,
|
|
579
|
+
* rather than emitting DDL it will reject.
|
|
580
|
+
*/
|
|
581
|
+
generateAddPrimaryKeySql(tableName, columns, name) {
|
|
582
|
+
this.assertPrimaryKeyAlterable(tableName);
|
|
583
|
+
const constraintName = this.escapeId(name ?? derivedPrimaryKeyName(tableName, columns));
|
|
584
|
+
const pkCols = columns.map((c) => this.escapeId(c)).join(', ');
|
|
585
|
+
return `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} PRIMARY KEY (${pkCols});`;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Drops whatever key the table has.
|
|
589
|
+
*
|
|
590
|
+
* `constraintName` has to be what the constraint is *actually* called: the name introspection
|
|
591
|
+
* reported for a key the database already had, or the derived one for a key this generator itself
|
|
592
|
+
* added, which is what reversing a migration drops. Guessing either way names nothing. MySQL takes
|
|
593
|
+
* no name at all - a table's key is always `PRIMARY` there.
|
|
594
|
+
*/
|
|
595
|
+
generateDropPrimaryKeySql(tableName, constraintName) {
|
|
596
|
+
this.assertPrimaryKeyAlterable(tableName);
|
|
597
|
+
const table = this.escapeId(tableName);
|
|
598
|
+
if (this.dialect.dropPrimaryKeySyntax === 'DROP PRIMARY KEY') {
|
|
599
|
+
return `ALTER TABLE ${table} DROP PRIMARY KEY;`;
|
|
600
|
+
}
|
|
601
|
+
if (!constraintName) {
|
|
602
|
+
throw new TypeError(`Cannot drop the primary key of "${tableName}": ${this.dialect} names the constraint, and ` +
|
|
603
|
+
'introspection did not report a name for it.');
|
|
604
|
+
}
|
|
605
|
+
return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
|
|
606
|
+
}
|
|
607
|
+
assertPrimaryKeyAlterable(tableName) {
|
|
608
|
+
if (this.features.primaryKeyAlter) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
throw new TypeError(`${this.dialect}: Cannot change the primary key of "${tableName}" - this database has no ALTER ` +
|
|
612
|
+
'for it. Recreate the table in a written migration.');
|
|
613
|
+
}
|
|
597
614
|
}
|
|
598
615
|
/**
|
|
599
616
|
* 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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
4
|
-
import { asSelectMap, getKeys, parentJoins,
|
|
4
|
+
import { asSelectMap, getKeys, joinedColumns, joinedRowKey, parentJoins, parentRowKey, parentsIn, targetKeyColumns, } from '../util/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
7
7
|
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
@@ -50,6 +50,9 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
const meta = getMeta(entity);
|
|
53
|
+
// The tallies come back keyed by the columns *this relation* joins from, so its `joins` are kept
|
|
54
|
+
// beside them: reading the parent through `meta.ids` instead matches only where the two coincide,
|
|
55
|
+
// which is a to-many and nothing else.
|
|
53
56
|
const counted = new Map();
|
|
54
57
|
for (const relKey of getKeys(count)) {
|
|
55
58
|
const value = count[relKey];
|
|
@@ -59,15 +62,14 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
59
62
|
}
|
|
60
63
|
const where = typeof value === 'object' ? value.$where : undefined;
|
|
61
64
|
const joins = parentJoins(relOpts, meta.ids.length);
|
|
62
|
-
counted.set(relKey, await countPerParent(querier, relOpts, joins, payload, where));
|
|
65
|
+
counted.set(relKey, { joins, byParent: await countPerParent(querier, relOpts, joins, payload, where) });
|
|
63
66
|
}
|
|
64
67
|
for (const parent of payload) {
|
|
65
|
-
const id = rowKey(meta.ids.map((key) => parent[key]));
|
|
66
68
|
const row = {};
|
|
67
|
-
for (const [relKey, byParent] of counted) {
|
|
69
|
+
for (const [relKey, { joins, byParent }] of counted) {
|
|
68
70
|
// A parent the grouped result has no row for matched nothing, which is a zero rather than a
|
|
69
71
|
// gap: `_count` names what the caller asked to count, so every key it asked for is present.
|
|
70
|
-
row[relKey] = byParent[
|
|
72
|
+
row[relKey] = byParent[parentRowKey(joins, parent)] ?? 0;
|
|
71
73
|
}
|
|
72
74
|
parent[COUNT_RESULT_KEY] = row;
|
|
73
75
|
}
|
|
@@ -101,13 +103,13 @@ async function countThroughPerParent(querier, relOpts, throughEntity, joins, par
|
|
|
101
103
|
/** `SELECT <keys>, COUNT(*) ... GROUP BY <keys>`, as a lookup from parent key to tally. */
|
|
102
104
|
async function groupedCount(querier, entity, joins, where) {
|
|
103
105
|
const $agg = { [COUNT_ALIAS]: { $count: '*' } };
|
|
104
|
-
const $group =
|
|
106
|
+
const $group = joinedColumns(joins);
|
|
105
107
|
const rows = await querier.aggregate(entity, { $group, $agg, $where: where });
|
|
106
108
|
const byParent = {};
|
|
107
109
|
for (const row of rows) {
|
|
108
110
|
// Keyed by every joined column, which is how a tally finds the one parent whose whole key it
|
|
109
111
|
// matches - and how the rows an over-selecting `IN` brought back find no parent at all.
|
|
110
|
-
byParent[
|
|
112
|
+
byParent[joinedRowKey(joins, row)] = Number(row[COUNT_ALIAS]);
|
|
111
113
|
}
|
|
112
114
|
return byParent;
|
|
113
115
|
}
|