uql-orm 0.44.0 → 0.45.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/README.md +3 -2
- package/dist/dialect/abstractSqlDialect.d.ts +2 -2
- package/dist/dialect/abstractSqlDialect.js +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +8 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +8 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +1 -1
- package/dist/migrate/builder/migrationBuilder.js +1 -2
- package/dist/migrate/builder/tableBuilder.d.ts +1 -0
- package/dist/migrate/builder/tableBuilder.js +9 -10
- package/dist/migrate/builder/types.d.ts +3 -19
- package/dist/migrate/generator/definitionToNode.js +2 -2
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +2 -5
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -0
- package/dist/migrate/introspection/baseSqlIntrospector.js +4 -3
- package/dist/migrate/introspection/mysqlIntrospector.js +1 -2
- package/dist/migrate/introspection/postgresIntrospector.js +1 -2
- package/dist/migrate/introspection/sqliteIntrospector.js +1 -2
- package/dist/migrate/migrator.js +14 -2
- package/dist/migrate/schemaGenerator.d.ts +20 -5
- package/dist/migrate/schemaGenerator.js +105 -30
- package/dist/schema/schemaASTBuilder.js +2 -1
- package/dist/schema/schemaASTDiffer.d.ts +11 -1
- package/dist/schema/schemaASTDiffer.js +23 -7
- package/dist/schema/types.d.ts +19 -4
- package/dist/sqlite/sqliteDialect.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.js +1 -1
- package/dist/type/entity.d.ts +1 -1
- package/dist/type/migration.d.ts +42 -9
- package/dist/util/field.util.d.ts +7 -2
- package/dist/util/field.util.js +9 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
</picture>
|
|
8
8
|
</a>
|
|
9
9
|
|
|
10
|
-
<h3>The JSON-native ORM</h3>
|
|
10
|
+
<h3>The JSON-native TypeScript ORM</h3>
|
|
11
11
|
|
|
12
|
-
<p>
|
|
12
|
+
<p>UQL stands for Unified Query Language. With pure (type-safe) JSON queries, complex jobs can be done simply across SQL vendors + MongoDB. It got some inspiration from Mongo's best syntax.
|
|
13
|
+
</p>
|
|
13
14
|
|
|
14
15
|
<p>
|
|
15
16
|
<a href="https://uql-orm.dev"><b>Website</b></a> ·
|
|
@@ -16,9 +16,9 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
16
16
|
* declares over its columns as one named constraint. SQLite is the exception - see
|
|
17
17
|
* {@link serialDeclaresPrimaryKey}.
|
|
18
18
|
*/
|
|
19
|
-
abstract readonly
|
|
19
|
+
abstract readonly autoIncrementSuffix: string;
|
|
20
20
|
/**
|
|
21
|
-
* Whether {@link
|
|
21
|
+
* Whether {@link autoIncrementSuffix} states `PRIMARY KEY` itself, so the table must not state it again.
|
|
22
22
|
*
|
|
23
23
|
* True on SQLite alone, where `AUTOINCREMENT` is legal only in the exact phrase
|
|
24
24
|
* `INTEGER PRIMARY KEY AUTOINCREMENT` - the key cannot be lifted out of the column there.
|
|
@@ -11,7 +11,7 @@ import { resolveVectorCast } from './vectorCast.js';
|
|
|
11
11
|
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
12
12
|
export class AbstractSqlDialect extends VectorSqlDialect {
|
|
13
13
|
/**
|
|
14
|
-
* Whether {@link
|
|
14
|
+
* Whether {@link autoIncrementSuffix} states `PRIMARY KEY` itself, so the table must not state it again.
|
|
15
15
|
*
|
|
16
16
|
* True on SQLite alone, where `AUTOINCREMENT` is legal only in the exact phrase
|
|
17
17
|
* `INTEGER PRIMARY KEY AUTOINCREMENT` - the key cannot be lifted out of the column there.
|
|
@@ -24,7 +24,14 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
24
24
|
estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
|
|
25
25
|
/** `OFFSET` is only legal after a `LIMIT` here, so a bare `$skip` needs one. */
|
|
26
26
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Signed, though MySQL's own convention is `UNSIGNED`: a foreign key column takes its type from the
|
|
29
|
+
* key it points at, resolved through the *canonical* type, which has no way to know this string said
|
|
30
|
+
* `UNSIGNED`. The two then disagree and the engine refuses the constraint - the same trap knex hit
|
|
31
|
+
* (knex#6129) and MikroORM still carries (mikro-orm#5485). Signed is also the portable half: no
|
|
32
|
+
* other engine here has unsigned integers, so an `@Id` means one range everywhere.
|
|
33
|
+
*/
|
|
34
|
+
readonly autoIncrementSuffix = "AUTO_INCREMENT";
|
|
28
35
|
readonly escapeIdChar = "`";
|
|
29
36
|
readonly tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
|
30
37
|
readonly beginTransactionCommand = "START TRANSACTION";
|
|
@@ -63,7 +63,14 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
63
63
|
}
|
|
64
64
|
super.pager(ctx, opts);
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Signed, though MySQL's own convention is `UNSIGNED`: a foreign key column takes its type from the
|
|
68
|
+
* key it points at, resolved through the *canonical* type, which has no way to know this string said
|
|
69
|
+
* `UNSIGNED`. The two then disagree and the engine refuses the constraint - the same trap knex hit
|
|
70
|
+
* (knex#6129) and MikroORM still carries (mikro-orm#5485). Signed is also the portable half: no
|
|
71
|
+
* other engine here has unsigned integers, so an `@Id` means one range everywhere.
|
|
72
|
+
*/
|
|
73
|
+
autoIncrementSuffix = 'AUTO_INCREMENT';
|
|
67
74
|
escapeIdChar = '`';
|
|
68
75
|
tableOptions = 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
|
|
69
76
|
beginTransactionCommand = 'START TRANSACTION';
|
|
@@ -15,7 +15,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
15
15
|
/** Default {@link DialectFeatures} for Postgres-wire dialects. */
|
|
16
16
|
protected readonly featureDefaults: DialectFeatures;
|
|
17
17
|
readonly escapeIdChar = "\"";
|
|
18
|
-
readonly
|
|
18
|
+
readonly autoIncrementSuffix: string;
|
|
19
19
|
readonly tableOptions = "";
|
|
20
20
|
readonly beginTransactionCommand = "BEGIN";
|
|
21
21
|
readonly commitTransactionCommand = "COMMIT";
|
|
@@ -40,7 +40,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
40
40
|
// under heavy concurrent insert load (writes concentrate on one range); this default still beats
|
|
41
41
|
// `SERIAL` (CockroachDB's `unique_rowid()`, a ~64-bit value that overflows JS's safe-integer
|
|
42
42
|
// range). High-throughput CockroachDB users should override this per-entity with a UUID PK.
|
|
43
|
-
|
|
43
|
+
autoIncrementSuffix = 'GENERATED BY DEFAULT AS IDENTITY';
|
|
44
44
|
tableOptions = '';
|
|
45
45
|
beginTransactionCommand = 'BEGIN';
|
|
46
46
|
commitTransactionCommand = 'COMMIT';
|
|
@@ -49,8 +49,7 @@ function addForeignKeyOperation(tableName, columns, target, options = {}) {
|
|
|
49
49
|
foreignKey: {
|
|
50
50
|
name: options.name,
|
|
51
51
|
columns,
|
|
52
|
-
|
|
53
|
-
referencesColumns: target.columns,
|
|
52
|
+
references: { table: target.table, columns: target.columns },
|
|
54
53
|
onDelete: options.onDelete ?? 'NO ACTION',
|
|
55
54
|
onUpdate: options.onUpdate ?? 'NO ACTION',
|
|
56
55
|
},
|
|
@@ -16,6 +16,7 @@ export declare class TableBuilder implements ITableBuilder {
|
|
|
16
16
|
private _foreignKeyBuilders;
|
|
17
17
|
private _comment?;
|
|
18
18
|
constructor(name: string);
|
|
19
|
+
/** Big, matching an entity's `@Id`: a key is spelled from this type, so it has to state the real one. */
|
|
19
20
|
id(name?: string, options?: BaseColumnOptions): IColumnBuilder;
|
|
20
21
|
integer(name: string, options?: BaseColumnOptions): IColumnBuilder;
|
|
21
22
|
smallint(name: string, options?: BaseColumnOptions): IColumnBuilder;
|
|
@@ -12,8 +12,8 @@ import { expr } from './expressions.js';
|
|
|
12
12
|
*/
|
|
13
13
|
class TableForeignKeyBuilder {
|
|
14
14
|
_columns;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
_referencedTable;
|
|
16
|
+
_referencedColumns = [];
|
|
17
17
|
_onDelete = 'NO ACTION';
|
|
18
18
|
_onUpdate = 'NO ACTION';
|
|
19
19
|
_name;
|
|
@@ -21,8 +21,8 @@ class TableForeignKeyBuilder {
|
|
|
21
21
|
this._columns = columns;
|
|
22
22
|
}
|
|
23
23
|
references(table, columns) {
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
24
|
+
this._referencedTable = table;
|
|
25
|
+
this._referencedColumns = columns;
|
|
26
26
|
return this;
|
|
27
27
|
}
|
|
28
28
|
onDelete(action) {
|
|
@@ -41,13 +41,12 @@ class TableForeignKeyBuilder {
|
|
|
41
41
|
* Build the foreign key definition.
|
|
42
42
|
*/
|
|
43
43
|
build() {
|
|
44
|
-
if (!this.
|
|
44
|
+
if (!this._referencedTable)
|
|
45
45
|
return undefined;
|
|
46
46
|
return {
|
|
47
47
|
name: this._name,
|
|
48
48
|
columns: this._columns,
|
|
49
|
-
|
|
50
|
-
referencesColumns: this._referencesColumns,
|
|
49
|
+
references: { table: this._referencedTable, columns: this._referencedColumns },
|
|
51
50
|
onDelete: this._onDelete,
|
|
52
51
|
onUpdate: this._onUpdate,
|
|
53
52
|
};
|
|
@@ -66,8 +65,9 @@ export class TableBuilder {
|
|
|
66
65
|
constructor(name) {
|
|
67
66
|
this._name = name;
|
|
68
67
|
}
|
|
68
|
+
/** Big, matching an entity's `@Id`: a key is spelled from this type, so it has to state the real one. */
|
|
69
69
|
id(name = 'id', options = {}) {
|
|
70
|
-
return this.add(name, { category: 'integer' }, { ...options, primaryKey: true, autoIncrement: true });
|
|
70
|
+
return this.add(name, { category: 'integer', size: 'big' }, { ...options, primaryKey: true, autoIncrement: true });
|
|
71
71
|
}
|
|
72
72
|
integer(name, options) {
|
|
73
73
|
return this.add(name, { category: 'integer' }, options);
|
|
@@ -217,8 +217,7 @@ export class TableBuilder {
|
|
|
217
217
|
foreignKeys.push({
|
|
218
218
|
name: col.foreignKey.name,
|
|
219
219
|
columns: [col.name],
|
|
220
|
-
|
|
221
|
-
referencesColumns: col.foreignKey.columns,
|
|
220
|
+
references: { table: col.foreignKey.table, columns: col.foreignKey.columns },
|
|
222
221
|
onDelete: col.foreignKey.onDelete,
|
|
223
222
|
onUpdate: col.foreignKey.onUpdate,
|
|
224
223
|
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CanonicalType, ForeignKeyAction } from '../../schema/types.js';
|
|
8
8
|
import type { IndexColumnInput, IndexOptions, IndexSchema } from '../../type/index.js';
|
|
9
|
+
import type { ForeignKeySchema } from '../../type/migration.js';
|
|
9
10
|
/**
|
|
10
11
|
* Foreign key reference options.
|
|
11
12
|
*/
|
|
@@ -124,27 +125,10 @@ export interface TableDefinition {
|
|
|
124
125
|
/** Index definitions */
|
|
125
126
|
indexes: IndexSchema[];
|
|
126
127
|
/** Foreign key definitions at table level */
|
|
127
|
-
foreignKeys:
|
|
128
|
+
foreignKeys: ForeignKeySchema[];
|
|
128
129
|
/** Table comment */
|
|
129
130
|
comment?: string;
|
|
130
131
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Table-level foreign key (for composite FKs).
|
|
133
|
-
*/
|
|
134
|
-
export interface TableForeignKeyDefinition {
|
|
135
|
-
/** Constraint name */
|
|
136
|
-
name?: string;
|
|
137
|
-
/** Local columns */
|
|
138
|
-
columns: string[];
|
|
139
|
-
/** Referenced table */
|
|
140
|
-
referencesTable: string;
|
|
141
|
-
/** Referenced columns */
|
|
142
|
-
referencesColumns: string[];
|
|
143
|
-
/** Action on delete */
|
|
144
|
-
onDelete: ForeignKeyAction;
|
|
145
|
-
/** Action on update */
|
|
146
|
-
onUpdate: ForeignKeyAction;
|
|
147
|
-
}
|
|
148
132
|
/**
|
|
149
133
|
* Type of migration operation.
|
|
150
134
|
*/
|
|
@@ -238,7 +222,7 @@ export interface DropIndexOperation extends MigrationOperation {
|
|
|
238
222
|
export interface AddForeignKeyOperation extends MigrationOperation {
|
|
239
223
|
type: 'addForeignKey';
|
|
240
224
|
tableName: string;
|
|
241
|
-
foreignKey:
|
|
225
|
+
foreignKey: ForeignKeySchema;
|
|
242
226
|
}
|
|
243
227
|
/**
|
|
244
228
|
* Drop foreign key operation.
|
|
@@ -40,8 +40,8 @@ export function tableDefinitionToNode(def) {
|
|
|
40
40
|
columns: fkDef.columns.map((name) => columns.get(name)).filter((c) => c !== undefined),
|
|
41
41
|
},
|
|
42
42
|
to: {
|
|
43
|
-
table: { name: fkDef.
|
|
44
|
-
columns: fkDef.
|
|
43
|
+
table: { name: fkDef.references.table },
|
|
44
|
+
columns: fkDef.references.columns.map((name) => ({ name })),
|
|
45
45
|
},
|
|
46
46
|
onDelete: fkDef.onDelete,
|
|
47
47
|
onUpdate: fkDef.onUpdate,
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
+
import type { ForeignKeyAction } from '../../schema/types.js';
|
|
1
2
|
import type { ColumnSchema, ForeignKeySchema, IndexSchema, QuerierPool, RawRow, SchemaIntrospector, SqlQuerier, TableSchema } from '../../type/index.js';
|
|
2
3
|
import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
|
|
3
|
-
/**
|
|
4
|
-
* Referential action type for foreign key constraints.
|
|
5
|
-
*/
|
|
6
|
-
export type ReferentialAction = 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
|
|
7
4
|
/**
|
|
8
5
|
* Reads the rows of one statement while introspecting a table.
|
|
9
6
|
*
|
|
@@ -72,7 +69,7 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
|
|
|
72
69
|
/**
|
|
73
70
|
* Normalize referential action string to standard type.
|
|
74
71
|
*/
|
|
75
|
-
protected normalizeReferentialAction(action: string):
|
|
72
|
+
protected normalizeReferentialAction(action: string): ForeignKeyAction | undefined;
|
|
76
73
|
/**
|
|
77
74
|
* Convert bigint/null values to number safely.
|
|
78
75
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canonicalColumnType } from '../../schema/canonicalType.js';
|
|
2
2
|
import { createTableNode, SchemaAST } from '../../schema/schemaAST.js';
|
|
3
3
|
import { escapeSqlId } from '../../util/index.js';
|
|
4
|
+
import { derivedForeignKeyName } from '../../util/sql.util.js';
|
|
4
5
|
/**
|
|
5
6
|
* Base class for SQL introspectors with shared AST building logic.
|
|
6
7
|
*/
|
|
@@ -83,14 +84,14 @@ export class BaseSqlIntrospector {
|
|
|
83
84
|
if (!fromTable)
|
|
84
85
|
continue;
|
|
85
86
|
for (const fk of schema.foreignKeys) {
|
|
86
|
-
const toTable = tableNodes.get(fk.
|
|
87
|
+
const toTable = tableNodes.get(fk.references.table);
|
|
87
88
|
if (!toTable)
|
|
88
89
|
continue;
|
|
89
90
|
const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
|
|
90
|
-
const toColumns = fk.
|
|
91
|
+
const toColumns = fk.references.columns.flatMap((name) => toTable.columns.get(name) ?? []);
|
|
91
92
|
if (fromColumns.length > 0 && toColumns.length > 0) {
|
|
92
93
|
const rel = {
|
|
93
|
-
name: fk.name,
|
|
94
|
+
name: fk.name ?? derivedForeignKeyName(schema.name, fk.columns),
|
|
94
95
|
type: fromColumns[0].isUnique ? 'OneToOne' : 'ManyToOne',
|
|
95
96
|
from: { table: fromTable, columns: fromColumns },
|
|
96
97
|
to: { table: toTable, columns: toColumns },
|
|
@@ -123,8 +123,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
123
123
|
return results.map((row) => ({
|
|
124
124
|
name: row.constraint_name,
|
|
125
125
|
columns: (row.columns || '').split(','),
|
|
126
|
-
|
|
127
|
-
referencedColumns: (row.referenced_columns || '').split(','),
|
|
126
|
+
references: { table: row.referenced_table, columns: (row.referenced_columns || '').split(',') },
|
|
128
127
|
onDelete: this.normalizeReferentialAction(row.delete_rule),
|
|
129
128
|
onUpdate: this.normalizeReferentialAction(row.update_rule),
|
|
130
129
|
}));
|
|
@@ -204,8 +204,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
204
204
|
return results.map((row) => ({
|
|
205
205
|
name: row.constraint_name,
|
|
206
206
|
columns: row.columns,
|
|
207
|
-
|
|
208
|
-
referencedColumns: row.referenced_columns,
|
|
207
|
+
references: { table: row.referenced_table, columns: row.referenced_columns },
|
|
209
208
|
onDelete: this.normalizeReferentialAction(row.delete_rule),
|
|
210
209
|
onUpdate: this.normalizeReferentialAction(row.update_rule),
|
|
211
210
|
}));
|
|
@@ -113,8 +113,7 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
113
113
|
// derives it. Seeded from the columns, not the PRAGMA's row id, which nothing else knows.
|
|
114
114
|
name: derivedForeignKeyName(tableName, columns),
|
|
115
115
|
columns,
|
|
116
|
-
|
|
117
|
-
referencedColumns: rows.map((r) => r.to),
|
|
116
|
+
references: { table: first.table, columns: rows.map((r) => r.to) },
|
|
118
117
|
onDelete: this.normalizeReferentialAction(first.on_delete),
|
|
119
118
|
onUpdate: this.normalizeReferentialAction(first.on_update),
|
|
120
119
|
};
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -282,12 +282,16 @@ export class Migrator {
|
|
|
282
282
|
throw new TypeError('Schema generator and introspector must be set');
|
|
283
283
|
}
|
|
284
284
|
const ast = await this.introspectClaimedSchemas();
|
|
285
|
+
// Both sides built once: the database's above, the entities' here. Left to `diffSchema`, each
|
|
286
|
+
// entity would rebuild the whole AST, which is quadratic in the number of entities. Absent on a
|
|
287
|
+
// generator that compares no schema of its own - MongoDB, which reads only indexes.
|
|
288
|
+
const desiredAst = this.schemaGenerator.buildAST?.(this.entities);
|
|
285
289
|
const diffs = [];
|
|
286
290
|
for (const entity of this.entities) {
|
|
287
291
|
const meta = getMeta(entity);
|
|
288
292
|
const tableName = this.schemaGenerator.resolveTableName(meta);
|
|
289
293
|
const currentTable = ast.getTable(tableName);
|
|
290
|
-
const diff = this.schemaGenerator.diffSchema(entity, currentTable);
|
|
294
|
+
const diff = this.schemaGenerator.diffSchema(entity, currentTable, desiredAst);
|
|
291
295
|
if (diff) {
|
|
292
296
|
diffs.push(diff);
|
|
293
297
|
}
|
|
@@ -390,7 +394,9 @@ export class Migrator {
|
|
|
390
394
|
}
|
|
391
395
|
/** The same for one entity against the table it already has, and nothing where the two agree. */
|
|
392
396
|
alterFromEntity(entity, table, options) {
|
|
393
|
-
|
|
397
|
+
// Spanning the set for the reason `planEntity` spells out: a foreign key needs the table it
|
|
398
|
+
// points at, which a sync of one entity outside the configured list would not otherwise have.
|
|
399
|
+
const diff = this.generator.diffSchema(entity, table, this.generator.buildAST?.(this.entitiesWith(entity)));
|
|
394
400
|
return diff?.type === 'alter' ? this.alterFromDiff(diff, options) : [];
|
|
395
401
|
}
|
|
396
402
|
/** The configured entities, with `entity` among them however the migrator was built. */
|
|
@@ -480,6 +486,12 @@ export class Migrator {
|
|
|
480
486
|
this.logger.logSkippedMigration(`[AutoSync] Skipped changing the primary key of '${diff.tableName}' from (${filteredDiff.primaryKey.from.join(', ')}) to (${filteredDiff.primaryKey.to.join(', ')}) (safe mode active). Use a migration or { safe: false } to apply.`);
|
|
481
487
|
delete filteredDiff.primaryKey;
|
|
482
488
|
}
|
|
489
|
+
if (filteredDiff.foreignKeysToAlter?.length) {
|
|
490
|
+
// Altering one is dropping it and adding it back, so letting the add through while the drop
|
|
491
|
+
// is held would emit `ADD CONSTRAINT` for a constraint the table still has.
|
|
492
|
+
this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.foreignKeysToAlter.length} foreign keys in table '${diff.tableName}': ${filteredDiff.foreignKeysToAlter.map((fk) => fk.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
|
|
493
|
+
delete filteredDiff.foreignKeysToAlter;
|
|
494
|
+
}
|
|
483
495
|
delete filteredDiff.indexesToDrop;
|
|
484
496
|
delete filteredDiff.foreignKeysToDrop;
|
|
485
497
|
}
|
|
@@ -2,8 +2,8 @@ import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
|
|
|
2
2
|
import type { SchemaAST } from '../schema/schemaAST.js';
|
|
3
3
|
import { type DiffOptions } from '../schema/schemaASTDiffer.js';
|
|
4
4
|
import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
|
|
5
|
-
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
|
|
6
|
-
import type { FullColumnDefinition, TableDefinition
|
|
5
|
+
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
|
|
6
|
+
import type { FullColumnDefinition, TableDefinition } from './builder/types.js';
|
|
7
7
|
import { type IndexDdl } from './ddl/index.js';
|
|
8
8
|
/**
|
|
9
9
|
* Unified SQL schema generator.
|
|
@@ -26,8 +26,19 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
26
26
|
/**
|
|
27
27
|
* Primary key type for auto-increment integer IDs
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* How an auto-increment key of `type` is spelled: the type as any other column renders it, plus what
|
|
31
|
+
* the engine appends to make it generated.
|
|
32
|
+
*
|
|
33
|
+
* Derived rather than a fixed string per dialect, because a foreign key column takes its type from
|
|
34
|
+
* the key it points at, resolved through the same canonical type. A key whose spelling ignored that
|
|
35
|
+
* type could never be referenced: `@Id({ columnType: 'int' })` emitted `BIGINT` while the column
|
|
36
|
+
* pointing at it emitted `INT`, and every engine refuses that constraint.
|
|
37
|
+
*/
|
|
38
|
+
protected serialType(type: CanonicalType): string;
|
|
30
39
|
protected canonicalTypeToSql(type: CanonicalType): string;
|
|
40
|
+
/** The entity side as an AST, carrying this generator's default referential action. */
|
|
41
|
+
buildAST(entities: readonly Type<unknown>[]): SchemaAST;
|
|
31
42
|
/**
|
|
32
43
|
* Every `CREATE TABLE` for `entities`, then their foreign keys.
|
|
33
44
|
*
|
|
@@ -53,6 +64,10 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
53
64
|
private orderedTables;
|
|
54
65
|
generateDropTable(tableName: string, options?: DropSchemaOptions): string;
|
|
55
66
|
generateAlterTable(diff: SchemaDiff): string[];
|
|
67
|
+
/** `ADD CONSTRAINT` for each of `foreignKeys`. */
|
|
68
|
+
private addForeignKeyStatements;
|
|
69
|
+
/** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
|
|
70
|
+
private dropForeignKeyStatements;
|
|
56
71
|
generateAlterTableDown(diff: SchemaDiff): string[];
|
|
57
72
|
generateCreateIndex(tableName: string, index: IndexSchema, options?: {
|
|
58
73
|
ifNotExists?: boolean;
|
|
@@ -100,7 +115,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
100
115
|
* longer disagree about what has changed. Only two things are this side's own: the entity becomes a
|
|
101
116
|
* table node first, and types are compared as the *engine* would store them - see `normalizeType`.
|
|
102
117
|
*/
|
|
103
|
-
diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
|
|
118
|
+
diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
|
|
104
119
|
/**
|
|
105
120
|
* What the shared differ needs from a dialect: a type as this engine would actually store it.
|
|
106
121
|
*
|
|
@@ -144,7 +159,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
144
159
|
generateAlterColumnSql(tableName: string, columnName: string, column: FullColumnDefinition): string;
|
|
145
160
|
generateDropColumnSql(tableName: string, columnName: string): string;
|
|
146
161
|
generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
|
|
147
|
-
generateAddForeignKeySql(tableName: string, foreignKey:
|
|
162
|
+
generateAddForeignKeySql(tableName: string, foreignKey: ForeignKeySchema): string;
|
|
148
163
|
generateDropForeignKeySql(tableName: string, constraintName: string): string;
|
|
149
164
|
/**
|
|
150
165
|
* `ALTER TABLE ... ADD CONSTRAINT <name> PRIMARY KEY (...)`, the other half of
|
|
@@ -3,7 +3,7 @@ import { getMeta, soleIdOf } from '../entity/index.js';
|
|
|
3
3
|
import { areTypesEqual, canonicalToSql, engineType, fieldOptionsToCanonical, isVectorCategory, } from '../schema/canonicalType.js';
|
|
4
4
|
import { indexSignature } from '../schema/indexDifferences.js';
|
|
5
5
|
import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
|
|
6
|
-
import { diffTable } from '../schema/schemaASTDiffer.js';
|
|
6
|
+
import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
|
|
7
7
|
import { getKeys, isAutoIncrement, isSoleIdField, qualifyName } from '../util/index.js';
|
|
8
8
|
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
|
|
9
9
|
import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
|
|
@@ -49,12 +49,25 @@ export class SqlSchemaGenerator {
|
|
|
49
49
|
/**
|
|
50
50
|
* Primary key type for auto-increment integer IDs
|
|
51
51
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* How an auto-increment key of `type` is spelled: the type as any other column renders it, plus what
|
|
54
|
+
* the engine appends to make it generated.
|
|
55
|
+
*
|
|
56
|
+
* Derived rather than a fixed string per dialect, because a foreign key column takes its type from
|
|
57
|
+
* the key it points at, resolved through the same canonical type. A key whose spelling ignored that
|
|
58
|
+
* type could never be referenced: `@Id({ columnType: 'int' })` emitted `BIGINT` while the column
|
|
59
|
+
* pointing at it emitted `INT`, and every engine refuses that constraint.
|
|
60
|
+
*/
|
|
61
|
+
serialType(type) {
|
|
62
|
+
return `${this.canonicalTypeToSql(type)} ${this.dialect.autoIncrementSuffix}`;
|
|
54
63
|
}
|
|
55
64
|
canonicalTypeToSql(type) {
|
|
56
65
|
return canonicalToSql(type, this.dialect);
|
|
57
66
|
}
|
|
67
|
+
/** The entity side as an AST, carrying this generator's default referential action. */
|
|
68
|
+
buildAST(entities) {
|
|
69
|
+
return buildEntityAST(this, entities, this.defaultForeignKeyAction);
|
|
70
|
+
}
|
|
58
71
|
/**
|
|
59
72
|
* Every `CREATE TABLE` for `entities`, then their foreign keys.
|
|
60
73
|
*
|
|
@@ -76,16 +89,7 @@ export class SqlSchemaGenerator {
|
|
|
76
89
|
statements.push(...tables.flatMap((table) => this.generateCreateTableFromNode(inline ? table : { ...table, outgoingRelations: [] }, options)));
|
|
77
90
|
if (withForeignKeys && !inline) {
|
|
78
91
|
for (const table of tables) {
|
|
79
|
-
|
|
80
|
-
statements.push(this.generateAddForeignKeySql(qualifyName(table.name, table.schema), {
|
|
81
|
-
name: rel.name,
|
|
82
|
-
columns: rel.from.columns.map((c) => c.name),
|
|
83
|
-
referencesTable: qualifyName(rel.to.table.name, rel.to.table.schema),
|
|
84
|
-
referencesColumns: rel.to.columns.map((c) => c.name),
|
|
85
|
-
onDelete: rel.onDelete ?? this.defaultForeignKeyAction,
|
|
86
|
-
onUpdate: rel.onUpdate ?? this.defaultForeignKeyAction,
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
92
|
+
statements.push(...this.addForeignKeyStatements(qualifyName(table.name, table.schema), table.outgoingRelations.map(foreignKeyOf)));
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
return statements;
|
|
@@ -129,6 +133,12 @@ export class SqlSchemaGenerator {
|
|
|
129
133
|
if (diff.primaryKey?.from.length) {
|
|
130
134
|
statements.push(this.generateDropPrimaryKeySql(diff.tableName, diff.primaryKey.fromName));
|
|
131
135
|
}
|
|
136
|
+
// Before the columns: a constraint holds its columns down, so one the entity dropped cannot go
|
|
137
|
+
// while a foreign key still names it. An alter is a drop and an add, and this is its drop half.
|
|
138
|
+
statements.push(...this.dropForeignKeyStatements(diff.tableName, [
|
|
139
|
+
...(diff.foreignKeysToDrop ?? []),
|
|
140
|
+
...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.from)),
|
|
141
|
+
]));
|
|
132
142
|
// Add new columns
|
|
133
143
|
if (diff.columnsToAdd?.length) {
|
|
134
144
|
for (const column of diff.columnsToAdd) {
|
|
@@ -166,14 +176,34 @@ export class SqlSchemaGenerator {
|
|
|
166
176
|
if (diff.primaryKey?.to.length) {
|
|
167
177
|
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
|
|
168
178
|
}
|
|
179
|
+
// After the columns, for the same reason the key is: a constraint cannot name one that is not
|
|
180
|
+
// there yet. The add half of an alter rides along, its drop having gone out above.
|
|
181
|
+
statements.push(...this.addForeignKeyStatements(diff.tableName, [
|
|
182
|
+
...(diff.foreignKeysToAdd ?? []),
|
|
183
|
+
...(diff.foreignKeysToAlter ?? []).map((it) => it.to),
|
|
184
|
+
]));
|
|
169
185
|
return statements;
|
|
170
186
|
}
|
|
187
|
+
/** `ADD CONSTRAINT` for each of `foreignKeys`. */
|
|
188
|
+
addForeignKeyStatements(tableName, foreignKeys) {
|
|
189
|
+
return foreignKeys.map((foreignKey) => this.generateAddForeignKeySql(tableName, foreignKey));
|
|
190
|
+
}
|
|
191
|
+
/** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
|
|
192
|
+
dropForeignKeyStatements(tableName, constraintNames) {
|
|
193
|
+
return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
|
|
194
|
+
}
|
|
171
195
|
generateAlterTableDown(diff) {
|
|
172
196
|
const statements = [];
|
|
173
197
|
const tableName = this.escapeId(diff.tableName);
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
198
|
+
// Constraints first, mirroring the up direction: the up added them last, so the down drops them
|
|
199
|
+
// first, and a column it is about to drop is then free of anything naming it.
|
|
200
|
+
statements.push(...this.dropForeignKeyStatements(diff.tableName, [
|
|
201
|
+
...(diff.foreignKeysToAdd ?? []).map((it) => constraintNameOf(diff.tableName, it)),
|
|
202
|
+
...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.to)),
|
|
203
|
+
]));
|
|
204
|
+
// The key next, for the same reason: a column the up added cannot be dropped below while the new
|
|
205
|
+
// key still names it. Restored under the name the database gave it, which is what the table had
|
|
206
|
+
// before, rather than a derived one that was never on it.
|
|
177
207
|
if (diff.primaryKey?.to.length) {
|
|
178
208
|
statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
|
|
179
209
|
}
|
|
@@ -200,8 +230,11 @@ export class SqlSchemaGenerator {
|
|
|
200
230
|
if (diff.primaryKey?.from.length) {
|
|
201
231
|
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
|
|
202
232
|
}
|
|
203
|
-
|
|
204
|
-
|
|
233
|
+
// The constraint the up replaced, back under the name the database had for it. A foreign key the
|
|
234
|
+
// up *dropped* is not restored: only its name survived the diff, never what it pointed at.
|
|
235
|
+
statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
|
|
236
|
+
if (diff.columnsToDrop?.length || diff.indexesToDrop?.length || diff.foreignKeysToDrop?.length) {
|
|
237
|
+
statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes/foreign keys`);
|
|
205
238
|
}
|
|
206
239
|
return statements;
|
|
207
240
|
}
|
|
@@ -283,7 +316,7 @@ export class SqlSchemaGenerator {
|
|
|
283
316
|
const canonical = fieldOptionsToCanonical(field);
|
|
284
317
|
// Special case for serial primary keys
|
|
285
318
|
if (isAutoIncrement(field, field.isId === true)) {
|
|
286
|
-
return this.
|
|
319
|
+
return this.serialType(canonical);
|
|
287
320
|
}
|
|
288
321
|
return this.canonicalTypeToSql(canonical);
|
|
289
322
|
}
|
|
@@ -337,16 +370,16 @@ export class SqlSchemaGenerator {
|
|
|
337
370
|
* longer disagree about what has changed. Only two things are this side's own: the entity becomes a
|
|
338
371
|
* table node first, and types are compared as the *engine* would store them - see `normalizeType`.
|
|
339
372
|
*/
|
|
340
|
-
diffSchema(entity, currentTable) {
|
|
373
|
+
diffSchema(entity, currentTable, desiredAst) {
|
|
341
374
|
const meta = getMeta(entity);
|
|
342
375
|
const tableName = this.resolveTableName(meta);
|
|
343
376
|
const schema = this.resolveSchema(meta);
|
|
344
377
|
if (!currentTable) {
|
|
345
378
|
return { tableName, schema, type: 'create' };
|
|
346
379
|
}
|
|
347
|
-
// Keyed by the qualified name this generator resolves, which is the key the AST
|
|
348
|
-
//
|
|
349
|
-
const desired = buildEntityAST(this, [entity]).getTable(tableName);
|
|
380
|
+
// Keyed by the qualified name this generator resolves, which is the key the AST stores the table
|
|
381
|
+
// under.
|
|
382
|
+
const desired = (desiredAst ?? buildEntityAST(this, [entity], this.defaultForeignKeyAction)).getTable(tableName);
|
|
350
383
|
if (!desired) {
|
|
351
384
|
return undefined;
|
|
352
385
|
}
|
|
@@ -367,10 +400,26 @@ export class SqlSchemaGenerator {
|
|
|
367
400
|
to: tableDiff.primaryKeyDiff.expected,
|
|
368
401
|
fromName: tableDiff.primaryKeyDiff.actualName,
|
|
369
402
|
};
|
|
403
|
+
// This table's own constraints, which is what `outgoingRelations` holds on both sides: the
|
|
404
|
+
// entity's as the AST derived them, the database's as the introspector read them back.
|
|
405
|
+
//
|
|
406
|
+
// None at all where the engine cannot alter one: SQLite resolves foreign keys lazily and keeps
|
|
407
|
+
// them inline at CREATE time, and its only way to change one afterwards is the twelve-step table
|
|
408
|
+
// rebuild, which a sync does not do. Reporting a difference nothing can apply would throw on
|
|
409
|
+
// every sync of an entity that has a relation. `drift:check` still names it.
|
|
410
|
+
const relationDiffs = this.features.foreignKeyAlter
|
|
411
|
+
? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
|
|
412
|
+
: [];
|
|
413
|
+
const foreignKeysToAdd = relationDiffs.flatMap((it) => (it.type === 'create' ? [foreignKeyOf(it.expected)] : []));
|
|
414
|
+
const foreignKeysToDrop = relationDiffs.flatMap((it) => (it.type === 'drop' ? [it.name] : []));
|
|
415
|
+
const foreignKeysToAlter = relationDiffs.flatMap((it) => it.type === 'alter' ? [{ from: foreignKeyOf(it.actual), to: foreignKeyOf(it.expected) }] : []);
|
|
370
416
|
if (!columnsToAdd.length &&
|
|
371
417
|
!columnsToAlter.length &&
|
|
372
418
|
!columnsToDrop.length &&
|
|
373
419
|
!indexesToAdd.length &&
|
|
420
|
+
!foreignKeysToAdd.length &&
|
|
421
|
+
!foreignKeysToDrop.length &&
|
|
422
|
+
!foreignKeysToAlter.length &&
|
|
374
423
|
!primaryKey) {
|
|
375
424
|
return undefined;
|
|
376
425
|
}
|
|
@@ -383,6 +432,9 @@ export class SqlSchemaGenerator {
|
|
|
383
432
|
columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
|
|
384
433
|
columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
|
|
385
434
|
indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
|
|
435
|
+
foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
|
|
436
|
+
foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
|
|
437
|
+
foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
|
|
386
438
|
};
|
|
387
439
|
}
|
|
388
440
|
/**
|
|
@@ -413,7 +465,7 @@ export class SqlSchemaGenerator {
|
|
|
413
465
|
name: col.name,
|
|
414
466
|
// The same rule `generateColumnFromNode` renders by, so a column added to an existing table
|
|
415
467
|
// 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),
|
|
468
|
+
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
417
469
|
nullable: col.nullable,
|
|
418
470
|
defaultValue: col.defaultValue,
|
|
419
471
|
isPrimaryKey: col.isPrimaryKey,
|
|
@@ -518,7 +570,7 @@ export class SqlSchemaGenerator {
|
|
|
518
570
|
generateColumnFromNode(col) {
|
|
519
571
|
return this.renderColumn({
|
|
520
572
|
...col,
|
|
521
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType : this.canonicalTypeToSql(col.type),
|
|
573
|
+
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
522
574
|
});
|
|
523
575
|
}
|
|
524
576
|
/**
|
|
@@ -555,15 +607,13 @@ export class SqlSchemaGenerator {
|
|
|
555
607
|
}
|
|
556
608
|
generateAddForeignKeySql(tableName, foreignKey) {
|
|
557
609
|
const fkCols = foreignKey.columns.map((c) => this.escapeId(c)).join(', ');
|
|
558
|
-
const refCols = foreignKey.
|
|
559
|
-
const constraintName = foreignKey
|
|
560
|
-
? this.escapeId(foreignKey.name)
|
|
561
|
-
: this.escapeId(derivedForeignKeyName(tableName, foreignKey.columns));
|
|
610
|
+
const refCols = foreignKey.references.columns.map((c) => this.escapeId(c)).join(', ');
|
|
611
|
+
const constraintName = this.escapeId(constraintNameOf(tableName, foreignKey));
|
|
562
612
|
if (!this.features.foreignKeyAlter) {
|
|
563
613
|
throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
|
|
564
614
|
}
|
|
565
615
|
return (`ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} ` +
|
|
566
|
-
`FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.
|
|
616
|
+
`FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.references.table)} (${refCols}) ` +
|
|
567
617
|
`ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction};`);
|
|
568
618
|
}
|
|
569
619
|
generateDropForeignKeySql(tableName, constraintName) {
|
|
@@ -608,6 +658,31 @@ export class SqlSchemaGenerator {
|
|
|
608
658
|
'for it. Recreate the table in a written migration.');
|
|
609
659
|
}
|
|
610
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* What a constraint is called: its own name, or one derived from its columns where nothing named it.
|
|
663
|
+
* Shared by the add and the drop so a `DROP CONSTRAINT` names exactly what an `ADD CONSTRAINT` made.
|
|
664
|
+
*/
|
|
665
|
+
function constraintNameOf(tableName, foreignKey) {
|
|
666
|
+
return foreignKey.name ?? derivedForeignKeyName(tableName, foreignKey.columns);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* A relationship node as the migration's own `ForeignKeySchema`. The node's `name` is kept rather
|
|
670
|
+
* than derived: on the database's side it is the only name a `DROP` can use, and on the entity's it
|
|
671
|
+
* is already the derived one. An unset action stays unset - the default belongs to the one place
|
|
672
|
+
* that spends it, `addForeignKeyStatements`.
|
|
673
|
+
*/
|
|
674
|
+
function foreignKeyOf(relation) {
|
|
675
|
+
return {
|
|
676
|
+
name: relation.name,
|
|
677
|
+
columns: relation.from.columns.map((column) => column.name),
|
|
678
|
+
references: {
|
|
679
|
+
table: qualifyName(relation.to.table.name, relation.to.table.schema),
|
|
680
|
+
columns: relation.to.columns.map((column) => column.name),
|
|
681
|
+
},
|
|
682
|
+
onDelete: relation.onDelete,
|
|
683
|
+
onUpdate: relation.onUpdate,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
611
686
|
/**
|
|
612
687
|
* The entities as an AST, named the way `generator` names things.
|
|
613
688
|
*
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
9
9
|
import { isSoleIdField } from '../util/field.util.js';
|
|
10
|
+
import { isAutoIncrement } from '../util/field.util.js';
|
|
10
11
|
import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
|
|
11
12
|
import { fieldOptionsToCanonical } from './canonicalType.js';
|
|
12
13
|
import { createTableNode, SchemaAST } from './schemaAST.js';
|
|
@@ -96,7 +97,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
96
97
|
nullable: isPrimaryKey ? false : (field.nullable ?? true),
|
|
97
98
|
defaultValue: field.defaultValue,
|
|
98
99
|
isPrimaryKey,
|
|
99
|
-
isAutoIncrement: field
|
|
100
|
+
isAutoIncrement: isAutoIncrement(field, isSoleKey),
|
|
100
101
|
isUnique: field.unique ?? false,
|
|
101
102
|
comment: field.comment,
|
|
102
103
|
enum: field.enum,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { type IndexFacet } from './indexDifferences.js';
|
|
11
11
|
import type { SchemaAST } from './schemaAST.js';
|
|
12
12
|
import type { CanonicalType } from './types.js';
|
|
13
|
-
import type { SchemaDiffResult, TableDiff, TableNode } from './types.js';
|
|
13
|
+
import type { RelationshipDiff, RelationshipNode, SchemaDiffResult, TableDiff, TableNode } from './types.js';
|
|
14
14
|
/**
|
|
15
15
|
* Options for schema diffing.
|
|
16
16
|
*/
|
|
@@ -60,3 +60,13 @@ export declare function diffSchemas(source: SchemaAST, target: SchemaAST, option
|
|
|
60
60
|
* comparison serves both, so drift and migrations can no longer disagree about what has changed.
|
|
61
61
|
*/
|
|
62
62
|
export declare function diffTable(source: TableNode, target: TableNode, options?: DiffOptions): TableDiff | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Compare two lists of relationships.
|
|
65
|
+
*
|
|
66
|
+
* Lists rather than whole schemas, because a migration diffs one table: its two sides are that
|
|
67
|
+
* table's `outgoingRelations`, where `diffSchemas` passes the schema's every relationship.
|
|
68
|
+
*
|
|
69
|
+
* Matched by columns, never by name - the engine named every constraint that already exists, so
|
|
70
|
+
* pairing on names would report every hand-named one as a drop and an add.
|
|
71
|
+
*/
|
|
72
|
+
export declare function diffRelationshipNodes(source: readonly RelationshipNode[], target: readonly RelationshipNode[], opts?: DiffOptions): RelationshipDiff[];
|
|
@@ -65,7 +65,9 @@ export function diffSchemas(source, target, options = {}) {
|
|
|
65
65
|
const indexDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.indexDiffs ?? []);
|
|
66
66
|
const primaryKeyDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.primaryKeyDiff ?? []);
|
|
67
67
|
// Relationships span tables, so they are compared over the whole schema rather than per table.
|
|
68
|
-
const relationshipDiffs = opts.compareRelationships
|
|
68
|
+
const relationshipDiffs = opts.compareRelationships
|
|
69
|
+
? diffRelationshipNodes(source.relationships, target.relationships, opts)
|
|
70
|
+
: [];
|
|
69
71
|
const hasDifferences = tablesToCreate.length > 0 ||
|
|
70
72
|
tablesToDrop.length > 0 ||
|
|
71
73
|
tablesToAlter.length > 0 ||
|
|
@@ -167,7 +169,7 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
167
169
|
const differences = [];
|
|
168
170
|
// Two things a key column implies rather than states, and catalogues report inconsistently: its
|
|
169
171
|
// type, which is the dialect's serial spelling rather than one the entity chose and does not round
|
|
170
|
-
// trip (`BIGINT
|
|
172
|
+
// trip (`BIGINT AUTO_INCREMENT` reads back as `BIGINT(20)`), and its nullability,
|
|
171
173
|
// which is NOT NULL in every engine whatever is reported - SQLite's `PRAGMA table_info` says
|
|
172
174
|
// `notnull: 0` for the `INTEGER PRIMARY KEY` that is the table's own rowid. Comparing either asked
|
|
173
175
|
// to rewrite the column on every sync, and on SQLite, which cannot alter one at all, failed
|
|
@@ -179,6 +181,14 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
179
181
|
if (typeChanged) {
|
|
180
182
|
differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
|
|
181
183
|
}
|
|
184
|
+
// Signedness is the one thing compared on a generated key, because it is the one part of the serial
|
|
185
|
+
// spelling that does round trip - and a key left unsigned refuses every foreign key pointing at it,
|
|
186
|
+
// since the referencing column takes its type from the canonical one, which is signed. Without this
|
|
187
|
+
// a database created before the serial became signed could never gain a foreign key.
|
|
188
|
+
const signednessChanged = generatedType && !!source.type.unsigned !== !!target.type.unsigned;
|
|
189
|
+
if (signednessChanged) {
|
|
190
|
+
differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
|
|
191
|
+
}
|
|
182
192
|
if (!impliedNotNull && source.nullable !== target.nullable) {
|
|
183
193
|
differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
|
|
184
194
|
}
|
|
@@ -206,7 +216,7 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
206
216
|
// Only the type this diff actually reports: a column altered for its default carries no data loss,
|
|
207
217
|
// and a generated key's type - never compared above - reads as unsigned against an entity that
|
|
208
218
|
// cannot say so.
|
|
209
|
-
isBreaking: typeChanged && isBreakingTypeChange(target.type, source.type),
|
|
219
|
+
isBreaking: (typeChanged || signednessChanged) && isBreakingTypeChange(target.type, source.type),
|
|
210
220
|
description: differences.join(', '),
|
|
211
221
|
};
|
|
212
222
|
}
|
|
@@ -228,11 +238,17 @@ function diffIndex(tableName, source, target, facets) {
|
|
|
228
238
|
};
|
|
229
239
|
}
|
|
230
240
|
/**
|
|
231
|
-
* Compare
|
|
241
|
+
* Compare two lists of relationships.
|
|
242
|
+
*
|
|
243
|
+
* Lists rather than whole schemas, because a migration diffs one table: its two sides are that
|
|
244
|
+
* table's `outgoingRelations`, where `diffSchemas` passes the schema's every relationship.
|
|
245
|
+
*
|
|
246
|
+
* Matched by columns, never by name - the engine named every constraint that already exists, so
|
|
247
|
+
* pairing on names would report every hand-named one as a drop and an add.
|
|
232
248
|
*/
|
|
233
|
-
function
|
|
234
|
-
const normalizeName = nameNormalizer(opts);
|
|
235
|
-
const { created, dropped, matched } = matchByKey(source
|
|
249
|
+
export function diffRelationshipNodes(source, target, opts = {}) {
|
|
250
|
+
const normalizeName = nameNormalizer({ ...DEFAULT_OPTIONS, ...opts });
|
|
251
|
+
const { created, dropped, matched } = matchByKey(source, target, (relation) => getRelationshipKey(relation, normalizeName));
|
|
236
252
|
return [
|
|
237
253
|
...created.map((relation) => ({
|
|
238
254
|
...relationEnds(relation),
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -272,14 +272,29 @@ export interface IndexDiff {
|
|
|
272
272
|
/**
|
|
273
273
|
* Difference between two relationship definitions.
|
|
274
274
|
*/
|
|
275
|
-
|
|
275
|
+
interface RelationshipDiffBase {
|
|
276
276
|
readonly name: string;
|
|
277
277
|
readonly fromTable: string;
|
|
278
278
|
readonly toTable: string;
|
|
279
|
-
readonly type: 'create' | 'drop' | 'alter';
|
|
280
|
-
readonly expected?: RelationshipNode;
|
|
281
|
-
readonly actual?: RelationshipNode;
|
|
282
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Difference between two relationships, shaped like {@link ColumnDiff} and for the same reason: which
|
|
282
|
+
* node is present follows from the kind of difference, so a reader never asserts its way past an
|
|
283
|
+
* `undefined` the kind had already ruled out.
|
|
284
|
+
*/
|
|
285
|
+
export type RelationshipDiff = RelationshipDiffBase & ({
|
|
286
|
+
readonly type: 'create';
|
|
287
|
+
readonly expected: RelationshipNode;
|
|
288
|
+
readonly actual?: undefined;
|
|
289
|
+
} | {
|
|
290
|
+
readonly type: 'drop';
|
|
291
|
+
readonly expected?: undefined;
|
|
292
|
+
readonly actual: RelationshipNode;
|
|
293
|
+
} | {
|
|
294
|
+
readonly type: 'alter';
|
|
295
|
+
readonly expected: RelationshipNode;
|
|
296
|
+
readonly actual: RelationshipNode;
|
|
297
|
+
});
|
|
283
298
|
/**
|
|
284
299
|
* Complete diff between two schemas.
|
|
285
300
|
*/
|
|
@@ -5,7 +5,7 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
5
5
|
protected readonly featureDefaults: DialectFeatures;
|
|
6
6
|
readonly dialectName = "sqlite";
|
|
7
7
|
readonly escapeIdChar = "`";
|
|
8
|
-
readonly
|
|
8
|
+
readonly autoIncrementSuffix = "PRIMARY KEY AUTOINCREMENT";
|
|
9
9
|
readonly serialDeclaresPrimaryKey = true;
|
|
10
10
|
readonly tableOptions = "";
|
|
11
11
|
readonly beginTransactionCommand = "BEGIN TRANSACTION";
|
|
@@ -22,7 +22,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
22
22
|
};
|
|
23
23
|
dialectName = 'sqlite';
|
|
24
24
|
escapeIdChar = '`';
|
|
25
|
-
|
|
25
|
+
autoIncrementSuffix = 'PRIMARY KEY AUTOINCREMENT';
|
|
26
26
|
// `AUTOINCREMENT` is only legal in that exact phrase, so the key cannot be lifted to table level.
|
|
27
27
|
serialDeclaresPrimaryKey = true;
|
|
28
28
|
tableOptions = '';
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -232,7 +232,7 @@ export type RelationValue<E> = E[RelationKey<E>];
|
|
|
232
232
|
/**
|
|
233
233
|
* SQL numeric column types
|
|
234
234
|
*/
|
|
235
|
-
export type NumericColumnType = 'int' | 'integer' | 'tinyint' | 'smallint' | 'bigint' | 'float' | 'float4' | 'float8' | 'double' | 'double precision' | 'decimal' | 'numeric' | 'real'
|
|
235
|
+
export type NumericColumnType = 'int' | 'integer' | 'tinyint' | 'smallint' | 'bigint' | 'float' | 'float4' | 'float8' | 'double' | 'double precision' | 'decimal' | 'numeric' | 'real';
|
|
236
236
|
/**
|
|
237
237
|
* SQL string column types
|
|
238
238
|
*/
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VectorCast } from '../dialect/vectorCast.js';
|
|
2
|
-
import type { FullColumnDefinition, TableDefinition
|
|
2
|
+
import type { FullColumnDefinition, TableDefinition } from '../migrate/builder/types.js';
|
|
3
3
|
import type { IndexFacet } from '../schema/indexDifferences.js';
|
|
4
4
|
import type { SchemaAST } from '../schema/schemaAST.js';
|
|
5
5
|
import type { ForeignKeyAction, IndexNode, IndexType, TableNode } from '../schema/types.js';
|
|
@@ -161,15 +161,25 @@ export interface IndexSchema extends VectorIndexOptions {
|
|
|
161
161
|
readonly vectorType?: VectorCast;
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
164
|
-
*
|
|
164
|
+
* A foreign key constraint, wherever one is described: read back by introspection, planned into a
|
|
165
|
+
* {@link SchemaDiff}, or declared through the migration builder. One shape for all three - they
|
|
166
|
+
* differed only in spelling, and the translation between them was pure overhead.
|
|
165
167
|
*/
|
|
166
168
|
export interface ForeignKeySchema {
|
|
167
|
-
|
|
169
|
+
/** Absent when nothing named it, which the generator fills in with `derivedForeignKeyName`. */
|
|
170
|
+
readonly name?: string;
|
|
168
171
|
readonly columns: string[];
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
/**
|
|
173
|
+
* The far end, as one thing. Same shape and same name as everywhere else a relationship's target is
|
|
174
|
+
* described - `@Field({ references })`, `addForeignKey`'s `target`, a `RelationshipNode`'s `to` -
|
|
175
|
+
* so nothing has to be destructured on the way between them.
|
|
176
|
+
*/
|
|
177
|
+
readonly references: {
|
|
178
|
+
readonly table: string;
|
|
179
|
+
readonly columns: string[];
|
|
180
|
+
};
|
|
181
|
+
readonly onDelete?: ForeignKeyAction;
|
|
182
|
+
readonly onUpdate?: ForeignKeyAction;
|
|
173
183
|
}
|
|
174
184
|
/**
|
|
175
185
|
* Represents a difference between current and desired schema
|
|
@@ -204,7 +214,17 @@ export interface SchemaDiff {
|
|
|
204
214
|
readonly indexesToAdd?: IndexSchema[];
|
|
205
215
|
readonly indexesToDrop?: string[];
|
|
206
216
|
readonly foreignKeysToAdd?: ForeignKeySchema[];
|
|
217
|
+
/** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
|
|
207
218
|
readonly foreignKeysToDrop?: string[];
|
|
219
|
+
/**
|
|
220
|
+
* A constraint whose referential actions changed. Its own field rather than a pair of entries in
|
|
221
|
+
* the two above, because no engine alters an action in place: it is a drop and an add that have to
|
|
222
|
+
* travel together, and safe mode has to hold back both or neither.
|
|
223
|
+
*/
|
|
224
|
+
readonly foreignKeysToAlter?: {
|
|
225
|
+
readonly from: ForeignKeySchema;
|
|
226
|
+
readonly to: ForeignKeySchema;
|
|
227
|
+
}[];
|
|
208
228
|
}
|
|
209
229
|
/**
|
|
210
230
|
* What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
|
|
@@ -278,8 +298,21 @@ export interface SchemaGenerator {
|
|
|
278
298
|
getSqlType(fieldOptions: FieldOptions): string;
|
|
279
299
|
/**
|
|
280
300
|
* Compare an entity with a database table node and return the differences.
|
|
301
|
+
*
|
|
302
|
+
* `desiredAst` is the entity side, from {@link buildAST}, and must span every entity a foreign key
|
|
303
|
+
* on this table points at: a relation whose target is absent resolves to nothing, so the constraint
|
|
304
|
+
* reads as missing from both sides, which is a match and no statement. Defaults to this entity
|
|
305
|
+
* alone, which is right only where it has no relations.
|
|
306
|
+
*/
|
|
307
|
+
diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
|
|
308
|
+
/**
|
|
309
|
+
* The entity side as an AST, to hand to every {@link diffSchema} of one run - building it per
|
|
310
|
+
* entity instead is quadratic in the number of entities.
|
|
311
|
+
*
|
|
312
|
+
* Optional because not every generator compares one: MongoDB has no foreign keys and diffs only
|
|
313
|
+
* indexes, so it neither implements this nor reads the argument.
|
|
281
314
|
*/
|
|
282
|
-
|
|
315
|
+
buildAST?(entities: readonly Type<unknown>[]): SchemaAST;
|
|
283
316
|
/**
|
|
284
317
|
* The table's key: {@link resolveTableAlias} behind {@link resolveSchema}, which is how a
|
|
285
318
|
* `SchemaAST` stores it and how a diff finds it again.
|
|
@@ -326,7 +359,7 @@ export interface SqlDdlGenerator extends SchemaGenerator {
|
|
|
326
359
|
/** Generate RENAME COLUMN statement */
|
|
327
360
|
generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
|
|
328
361
|
/** Generate ADD FOREIGN KEY statement */
|
|
329
|
-
generateAddForeignKeySql(tableName: string, foreignKey:
|
|
362
|
+
generateAddForeignKeySql(tableName: string, foreignKey: ForeignKeySchema): string;
|
|
330
363
|
/** Generate DROP FOREIGN KEY statement */
|
|
331
364
|
generateDropForeignKeySql(tableName: string, constraintName: string): string;
|
|
332
365
|
}
|
|
@@ -12,7 +12,7 @@ export type ColumnFamily = 'string' | 'numeric' | 'boolean' | 'date' | 'json' |
|
|
|
12
12
|
* restates the unions: the compile-time side of the same question reads them directly.
|
|
13
13
|
*/
|
|
14
14
|
export declare const COLUMN_TYPES_BY_FAMILY: {
|
|
15
|
-
readonly numeric: readonly ["int", "integer", "tinyint", "smallint", "bigint", "float", "float4", "float8", "double", "double precision", "decimal", "numeric", "real"
|
|
15
|
+
readonly numeric: readonly ["int", "integer", "tinyint", "smallint", "bigint", "float", "float4", "float8", "double", "double precision", "decimal", "numeric", "real"];
|
|
16
16
|
readonly string: readonly ["char", "varchar", "text", "uuid"];
|
|
17
17
|
readonly date: readonly ["date", "time", "datetime", "timestamp", "timestamptz"];
|
|
18
18
|
readonly json: readonly ["json", "jsonb"];
|
|
@@ -33,6 +33,11 @@ export declare function columnFamily(type: unknown): ColumnFamily | undefined;
|
|
|
33
33
|
*/
|
|
34
34
|
export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOptions): boolean;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Whether the database generates this column's value.
|
|
37
|
+
*
|
|
38
|
+
* The only answer: the schema AST asked it separately and disagreed on three counts - it ignored
|
|
39
|
+
* `onInsert`, so a key the application generates was still emitted `AUTO_INCREMENT`, and it ignored
|
|
40
|
+
* `columnType`, where this one used to let *any* declared width suppress the whole inference. A key
|
|
41
|
+
* that states its width is still a generated key; one that states how it is filled is not.
|
|
37
42
|
*/
|
|
38
43
|
export declare function isAutoIncrement(field: FieldOptions, isPrimaryKey: boolean): boolean;
|
package/dist/util/field.util.js
CHANGED
|
@@ -20,9 +20,6 @@ export const COLUMN_TYPES_BY_FAMILY = {
|
|
|
20
20
|
'decimal',
|
|
21
21
|
'numeric',
|
|
22
22
|
'real',
|
|
23
|
-
'serial',
|
|
24
|
-
'smallserial',
|
|
25
|
-
'bigserial',
|
|
26
23
|
],
|
|
27
24
|
string: ['char', 'varchar', 'text', 'uuid'],
|
|
28
25
|
date: ['date', 'time', 'datetime', 'timestamp', 'timestamptz'],
|
|
@@ -62,15 +59,15 @@ export function isSoleIdField(meta, field) {
|
|
|
62
59
|
return field.isId === true && meta.ids.length === 1;
|
|
63
60
|
}
|
|
64
61
|
/**
|
|
65
|
-
*
|
|
62
|
+
* Whether the database generates this column's value.
|
|
63
|
+
*
|
|
64
|
+
* The only answer: the schema AST asked it separately and disagreed on three counts - it ignored
|
|
65
|
+
* `onInsert`, so a key the application generates was still emitted `AUTO_INCREMENT`, and it ignored
|
|
66
|
+
* `columnType`, where this one used to let *any* declared width suppress the whole inference. A key
|
|
67
|
+
* that states its width is still a generated key; one that states how it is filled is not.
|
|
66
68
|
*/
|
|
67
69
|
export function isAutoIncrement(field, isPrimaryKey) {
|
|
68
|
-
if (field.autoIncrement
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
return true;
|
|
72
|
-
const colType = field.columnType?.toLowerCase();
|
|
73
|
-
if (colType === 'serial' || colType === 'smallserial' || colType === 'bigserial')
|
|
74
|
-
return true;
|
|
75
|
-
return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert && !field.columnType;
|
|
70
|
+
if (field.autoIncrement !== undefined)
|
|
71
|
+
return field.autoIncrement;
|
|
72
|
+
return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert;
|
|
76
73
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uql-orm",
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
|
-
"description": "JSON-native TypeScript ORM for
|
|
4
|
+
"description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.45.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|