uql-orm 0.80.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dialect/vectorSqlDialect.d.ts +2 -0
- package/dist/dialect/vectorSqlDialect.js +4 -0
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +24 -0
- package/dist/migrate/cli.js +1 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
- package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
- package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
- package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
- package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
- package/dist/migrate/ddl/pgIndexDdl.js +8 -6
- package/dist/migrate/ddl/tableDdl.d.ts +3 -2
- package/dist/migrate/ddl/tableDdl.js +9 -7
- package/dist/migrate/drift/driftDetector.d.ts +4 -5
- package/dist/migrate/drift/driftDetector.js +21 -21
- package/dist/migrate/generator/definitionToNode.d.ts +1 -1
- package/dist/migrate/generator/definitionToNode.js +9 -20
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +11 -19
- package/dist/migrate/index.d.ts +2 -1
- package/dist/migrate/index.js +1 -0
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -18
- package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +3 -3
- package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
- package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
- package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
- package/dist/migrate/introspection/postgresIntrospector.js +63 -46
- package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
- package/dist/migrate/migrator.d.ts +5 -0
- package/dist/migrate/migrator.js +33 -44
- package/dist/migrate/schemaChange.d.ts +18 -0
- package/dist/migrate/schemaChange.js +37 -0
- package/dist/migrate/schemaGenerator.d.ts +13 -14
- package/dist/migrate/schemaGenerator.js +83 -177
- package/dist/schema/indexDifferences.d.ts +22 -6
- package/dist/schema/indexDifferences.js +23 -8
- package/dist/schema/matchByKey.d.ts +10 -0
- package/dist/schema/matchByKey.js +18 -0
- package/dist/schema/schemaAST.d.ts +6 -2
- package/dist/schema/schemaAST.js +7 -3
- package/dist/schema/schemaASTBuilder.d.ts +2 -0
- package/dist/schema/schemaASTBuilder.js +15 -10
- package/dist/schema/schemaASTDiffer.d.ts +2 -3
- package/dist/schema/schemaASTDiffer.js +15 -36
- package/dist/schema/types.d.ts +14 -15
- package/dist/type/migration.d.ts +32 -50
- package/dist/util/ddlExpression.util.d.ts +5 -1
- package/dist/util/ddlExpression.util.js +6 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INDEX_TYPES } from '../../schema/types.js';
|
|
2
|
+
import { isVectorIndexType } from '../../type/vector.js';
|
|
2
3
|
import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
|
|
3
4
|
/**
|
|
4
5
|
* PostgreSQL schema introspector
|
|
@@ -15,15 +16,15 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
15
16
|
'opsClass',
|
|
16
17
|
'accessMethod',
|
|
17
18
|
'include',
|
|
19
|
+
'distance',
|
|
18
20
|
]);
|
|
19
21
|
triggersQuery() {
|
|
20
22
|
return /*sql*/ `
|
|
21
|
-
SELECT
|
|
22
|
-
pg_get_functiondef(t.tgfoid) AS requires
|
|
23
|
+
SELECT t.tgname AS name, pg_get_triggerdef(t.oid) AS definition, pg_get_functiondef(t.tgfoid) AS requires
|
|
23
24
|
FROM pg_trigger t
|
|
24
25
|
JOIN pg_class c ON c.oid = t.tgrelid
|
|
25
26
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
26
|
-
WHERE NOT t.tgisinternal AND n.nspname = ${this.schemaExpr}
|
|
27
|
+
WHERE NOT t.tgisinternal AND n.nspname = ${this.schemaExpr} AND c.relname = ${this.dialect.placeholder(1)}
|
|
27
28
|
`;
|
|
28
29
|
}
|
|
29
30
|
getTableNamesQuery() {
|
|
@@ -54,7 +55,11 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
54
55
|
* database scans meet that table every time something else is migrating.
|
|
55
56
|
*
|
|
56
57
|
* `attgenerated` rather than `is_generated`, which cannot part a stored generated column from the
|
|
57
|
-
* virtual one Postgres 18 added and uql never declares. CockroachDB states it too.
|
|
58
|
+
* virtual one Postgres 18 added and uql never declares. CockroachDB states it too. `format_type` for an
|
|
59
|
+
* extension type's modifier, which `information_schema` drops: a `vector(256)` read back as `vector`.
|
|
60
|
+
* CockroachDB names that type `vector` where Postgres says `USER-DEFINED`.
|
|
61
|
+
*
|
|
62
|
+
* A column is unique by a unique index over it alone, a constraint's or its own, as every engine reads it.
|
|
58
63
|
*/
|
|
59
64
|
getColumnsQuery(_tableName) {
|
|
60
65
|
return /*sql*/ `
|
|
@@ -69,11 +74,9 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
69
74
|
c.numeric_scale,
|
|
70
75
|
c.is_identity,
|
|
71
76
|
c.identity_generation,
|
|
72
|
-
CASE WHEN
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
AND a.attname = c.column_name
|
|
76
|
-
) = 's' THEN c.generation_expression END AS generated_as,
|
|
77
|
+
CASE WHEN a.attgenerated = 's' THEN c.generation_expression END AS generated_as,
|
|
78
|
+
CASE WHEN c.data_type IN ('USER-DEFINED', 'vector') AND a.atttypmod > -1
|
|
79
|
+
THEN format_type(a.atttypid, a.atttypmod) END AS formatted_type,
|
|
77
80
|
EXISTS (
|
|
78
81
|
SELECT 1 FROM information_schema.table_constraints tc
|
|
79
82
|
JOIN information_schema.key_column_usage kcu USING (constraint_schema, constraint_name)
|
|
@@ -83,19 +86,19 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
83
86
|
AND kcu.column_name = c.column_name
|
|
84
87
|
) AS is_primary_key,
|
|
85
88
|
EXISTS (
|
|
86
|
-
SELECT 1 FROM
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
AND
|
|
90
|
-
AND tc.constraint_type = 'UNIQUE'
|
|
91
|
-
GROUP BY tc.constraint_name
|
|
92
|
-
HAVING COUNT(*) = 1 AND MIN(kcu.column_name) = c.column_name
|
|
89
|
+
SELECT 1 FROM pg_catalog.pg_index ix
|
|
90
|
+
WHERE ix.indrelid = a.attrelid AND ix.indisunique AND NOT ix.indisprimary
|
|
91
|
+
AND ix.indnkeyatts = 1 AND ix.indkey[0] = a.attnum
|
|
92
|
+
AND ix.indpred IS NULL AND ix.indexprs IS NULL
|
|
93
93
|
) AS is_unique,
|
|
94
94
|
pg_catalog.col_description(
|
|
95
95
|
to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name)),
|
|
96
96
|
c.ordinal_position
|
|
97
97
|
) AS column_comment
|
|
98
98
|
FROM information_schema.columns c
|
|
99
|
+
LEFT JOIN pg_catalog.pg_attribute a
|
|
100
|
+
ON a.attrelid = to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))
|
|
101
|
+
AND a.attname = c.column_name
|
|
99
102
|
WHERE c.table_schema = ${this.schemaExpr}
|
|
100
103
|
AND c.table_name = $1
|
|
101
104
|
ORDER BY c.ordinal_position
|
|
@@ -108,23 +111,23 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
108
111
|
* while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
|
|
109
112
|
* `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
|
|
110
113
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
+
* The key's index and an `EXCLUDE`'s are left out. A `UNIQUE` constraint's index stays, as SQL Server
|
|
115
|
+
* and the MySQL family report theirs: the diff reads one over a single column as that column's
|
|
116
|
+
* uniqueness, and one over several as the unique `@Index` it is.
|
|
114
117
|
*/
|
|
115
118
|
getIndexesQuery(_tableName) {
|
|
116
119
|
return /*sql*/ `
|
|
117
120
|
SELECT
|
|
118
121
|
i.relname AS index_name,
|
|
119
122
|
ix.indisunique AS is_unique,
|
|
120
|
-
|
|
123
|
+
${this.indexMethodSql} AS method,
|
|
121
124
|
pg_get_expr(ix.indpred, ix.indrelid, true) AS predicate,
|
|
122
125
|
k.n <= ix.indnkeyatts AS is_key,
|
|
123
126
|
k.attnum = 0 AS is_expression,
|
|
124
127
|
COALESCE(a.attname::text, pg_get_indexdef(ix.indexrelid, k.n::int, true)) AS entry,
|
|
125
128
|
(ix.indoption[k.n - 1] & 1) <> 0 AS descending,
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
${this.nullsFirstSql} AS nulls_first,
|
|
130
|
+
${this.opsClassSql} AS ops_class
|
|
128
131
|
FROM pg_class t
|
|
129
132
|
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
130
133
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
@@ -137,22 +140,17 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
137
140
|
AND n.nspname = ${this.schemaExpr}
|
|
138
141
|
AND NOT ix.indisprimary
|
|
139
142
|
AND NOT EXISTS (
|
|
140
|
-
SELECT 1 FROM pg_constraint con
|
|
141
|
-
WHERE con.conindid = ix.indexrelid
|
|
142
|
-
AND con.contype IN (${this.constraintIndexTypes.map((type) => `'${type}'`).join(', ')})
|
|
143
|
+
SELECT 1 FROM pg_constraint con WHERE con.conindid = ix.indexrelid AND con.contype = 'x'
|
|
143
144
|
)
|
|
144
145
|
ORDER BY i.relname, k.n
|
|
145
146
|
`;
|
|
146
147
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
* compare. This one decides which indexes are reported at all.
|
|
154
|
-
*/
|
|
155
|
-
constraintIndexTypes = ['p', 'u', 'x'];
|
|
148
|
+
/** Whether an entry sorts nulls first, which Postgres states on every entry. */
|
|
149
|
+
nullsFirstSql = '(ix.indoption[k.n - 1] & 2) <> 0';
|
|
150
|
+
/** An index's access method, which is the type it declares. */
|
|
151
|
+
indexMethodSql = 'am.amname';
|
|
152
|
+
/** An entry's operator class, where it is not the default for its type. */
|
|
153
|
+
opsClassSql = 'CASE WHEN op.opcdefault THEN NULL ELSE op.opcname END';
|
|
156
154
|
/** From `pg_constraint`, whose key arrays keep each column paired with the one it references. */
|
|
157
155
|
getForeignKeysQuery(_tableName) {
|
|
158
156
|
const columnsOf = (keys, table) => /*sql*/ `ARRAY_TO_JSON(ARRAY(
|
|
@@ -194,7 +192,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
194
192
|
async mapColumnsResult(_read, _tableName, results) {
|
|
195
193
|
return results.map((row) => ({
|
|
196
194
|
name: row.column_name,
|
|
197
|
-
type: this.normalizeType(row.data_type, row.udt_name),
|
|
195
|
+
type: row.formatted_type?.toUpperCase() ?? this.normalizeType(row.data_type, row.udt_name),
|
|
198
196
|
nullable: row.is_nullable === 'YES',
|
|
199
197
|
defaultValue: this.parseDefaultValue(row.column_default),
|
|
200
198
|
isPrimaryKey: row.is_primary_key,
|
|
@@ -207,11 +205,25 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
207
205
|
generatedAs: row.generated_as ?? undefined,
|
|
208
206
|
}));
|
|
209
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* A vector index keeps its distance as its vector column's operator class, `{type}_{metric}_ops`, which
|
|
210
|
+
* is how `PgIndexDdl` writes the entity's `distance`; read back as that `distance`, so the two compare.
|
|
211
|
+
* No class named is the engine's default, L2.
|
|
212
|
+
*/
|
|
213
|
+
withVectorDistance(index) {
|
|
214
|
+
if (!isVectorIndexType(index.type)) {
|
|
215
|
+
return index;
|
|
216
|
+
}
|
|
217
|
+
const opsClass = index.entries.find((entry) => entry.opsClass)?.opsClass;
|
|
218
|
+
const distance = this.dialect.indexedDistance(opsClass ? /_([a-z0-9]+)_ops$/.exec(opsClass)?.[1] : 'l2');
|
|
219
|
+
const entries = index.entries.map(({ opsClass: _opsClass, ...entry }) => entry);
|
|
220
|
+
return distance ? { ...index, distance, entries } : index;
|
|
221
|
+
}
|
|
210
222
|
async mapIndexesResult(_read, _tableName, results) {
|
|
211
223
|
// One row per index entry, ordered by position, so the rows of an index are its entries in order.
|
|
212
224
|
return [...Map.groupBy(results, (row) => row.index_name)].map(([name, rows]) => {
|
|
213
225
|
const include = rows.filter((row) => !row.is_key).map((row) => row.entry);
|
|
214
|
-
return {
|
|
226
|
+
return this.withVectorDistance({
|
|
215
227
|
name,
|
|
216
228
|
entries: rows.filter((row) => row.is_key).map(mapIndexEntry),
|
|
217
229
|
unique: rows[0].is_unique,
|
|
@@ -219,7 +231,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
219
231
|
where: rows[0].predicate ?? undefined,
|
|
220
232
|
include: include.length > 0 ? include : undefined,
|
|
221
233
|
...fulltextIndex(rows),
|
|
222
|
-
};
|
|
234
|
+
});
|
|
223
235
|
});
|
|
224
236
|
}
|
|
225
237
|
async mapForeignKeysResult(_read, _tableName, results) {
|
|
@@ -315,7 +327,7 @@ function mapIndexEntry(row) {
|
|
|
315
327
|
column: row.entry,
|
|
316
328
|
...(row.is_expression && { expression: true }),
|
|
317
329
|
order: row.descending ? 'desc' : 'asc',
|
|
318
|
-
nulls: row.nulls_first ? 'first' : 'last',
|
|
330
|
+
...(row.nulls_first !== null && { nulls: row.nulls_first ? 'first' : 'last' }),
|
|
319
331
|
...(row.ops_class && { opsClass: row.ops_class }),
|
|
320
332
|
};
|
|
321
333
|
}
|
|
@@ -323,16 +335,21 @@ function mapIndexEntry(row) {
|
|
|
323
335
|
* CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
|
|
324
336
|
* still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
|
|
325
337
|
* on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
|
|
326
|
-
* every ascending index as drifted, against an entity that could not have asked for one.
|
|
327
|
-
* method, `prefix`, needs nothing: a method that is not a known index type is reported as no type.
|
|
338
|
+
* every ascending index as drifted, against an entity that could not have asked for one.
|
|
328
339
|
*/
|
|
329
340
|
export class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
|
|
330
|
-
indexFacets = new Set([
|
|
341
|
+
indexFacets = new Set([
|
|
342
|
+
'order',
|
|
343
|
+
'include',
|
|
344
|
+
'vector',
|
|
345
|
+
'distance',
|
|
346
|
+
]);
|
|
347
|
+
/** None: it rejects a stated nulls order, so reading one back gives an index it would refuse to rebuild. */
|
|
348
|
+
nullsFirstSql = 'NULL::BOOL';
|
|
331
349
|
/**
|
|
332
|
-
* `
|
|
333
|
-
*
|
|
334
|
-
* user asked for and report it missing forever. It leaves no way to tell the two apart, so the
|
|
335
|
-
* index a `@Field({ unique })` builds underneath itself stays visible there.
|
|
350
|
+
* Every index reports the access method `prefix` and no operator class, so a vector index is read off
|
|
351
|
+
* its definition, `USING cspann (vec vector_cosine_ops)`: its type, and its last key's class.
|
|
336
352
|
*/
|
|
337
|
-
|
|
353
|
+
indexMethodSql = `CASE WHEN pg_get_indexdef(ix.indexrelid) LIKE '% USING cspann %' THEN 'vector' ELSE am.amname END`;
|
|
354
|
+
opsClassSql = `CASE WHEN k.n = ix.indnkeyatts THEN substring(pg_get_indexdef(ix.indexrelid) from '(\\w+_ops)\\)') END`;
|
|
338
355
|
}
|
|
@@ -5,10 +5,10 @@ import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.j
|
|
|
5
5
|
*/
|
|
6
6
|
export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
7
7
|
/** Whether an index is libSQL's vector index, where the engine has one; elsewhere a declared one is built plain. */
|
|
8
|
-
indexFacets = new Set(this.dialect.hasVectorIndex() ? ['vector'] : []);
|
|
8
|
+
indexFacets = new Set(this.dialect.hasVectorIndex() ? ['vector', 'distance'] : []);
|
|
9
9
|
/** Not SQLite's own tables, nor the ones libSQL keeps a vector index in: its metadata and `<index>_shadow`. */
|
|
10
10
|
triggersQuery() {
|
|
11
|
-
return /*sql*/ `SELECT
|
|
11
|
+
return /*sql*/ `SELECT name, sql AS definition FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?`;
|
|
12
12
|
}
|
|
13
13
|
getTableNamesQuery() {
|
|
14
14
|
return /*sql*/ `
|
|
@@ -89,15 +89,12 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
89
89
|
const indexSchemas = [];
|
|
90
90
|
for (const index of results) {
|
|
91
91
|
const columns = await this.getIndexColumns(read, index.name);
|
|
92
|
-
//
|
|
93
|
-
// Skip primary key indexes ('pk') and single-column unique constraints
|
|
94
|
-
const isUserCreated = index.origin === 'c';
|
|
95
|
-
const isCompositeUnique = index.origin === 'u' && columns.length > 1;
|
|
92
|
+
// A unique constraint's index ('u') is reported as every engine reports it, and only the key's ('pk') left out.
|
|
96
93
|
// `PRAGMA index_info` names an expression entry `null` (its `cid` is -2), and the expression text
|
|
97
94
|
// lives only in `sqlite_master.sql`. Reporting `{ column: null }` put a column literally named
|
|
98
95
|
// `null` into the diff, so an index UQL cannot describe is left out, libSQL's vector index aside.
|
|
99
96
|
const named = columns.filter((column) => column.name !== null);
|
|
100
|
-
if (
|
|
97
|
+
if (index.origin === 'pk') {
|
|
101
98
|
continue;
|
|
102
99
|
}
|
|
103
100
|
if (named.length === columns.length) {
|
|
@@ -171,8 +168,13 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
171
168
|
return undefined;
|
|
172
169
|
}
|
|
173
170
|
const metric = row.sql?.match(/'metric=(\w+)'/i)?.[1]?.toLowerCase();
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
return {
|
|
172
|
+
name: indexName,
|
|
173
|
+
entries: [{ column }],
|
|
174
|
+
unique: false,
|
|
175
|
+
type: 'vector',
|
|
176
|
+
distance: this.dialect.indexedDistance(metric),
|
|
177
|
+
};
|
|
176
178
|
}
|
|
177
179
|
/** The statement that created the table, which is where SQLite keeps every expression it was given. */
|
|
178
180
|
async getTableDdl(read, tableName) {
|
|
@@ -128,6 +128,11 @@ export declare class Migrator {
|
|
|
128
128
|
* never asks for the rollback, which on SQLite cannot even be expressed (no `ALTER COLUMN`).
|
|
129
129
|
*/
|
|
130
130
|
private pendingChanges;
|
|
131
|
+
/**
|
|
132
|
+
* Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
|
|
133
|
+
* and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
|
|
134
|
+
* Without `drop`, a column's drop is held too.
|
|
135
|
+
*/
|
|
131
136
|
protected filterDiff(diff: SchemaDiff, options: {
|
|
132
137
|
safe?: boolean;
|
|
133
138
|
drop?: boolean;
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -8,6 +8,7 @@ import { LoggerWrapper } from '../util/index.js';
|
|
|
8
8
|
import { buildMigrationModule } from './codegen/migrationFile.js';
|
|
9
9
|
import { introspectorFor } from './introspection/registry.js';
|
|
10
10
|
import { migrationBuilderFor, migrationTargetFor } from './migrationTarget.js';
|
|
11
|
+
import { dropped, nonEmpty, reverseDiff, sides } from './schemaChange.js';
|
|
11
12
|
/**
|
|
12
13
|
* Main class for managing database migrations
|
|
13
14
|
*/
|
|
@@ -215,17 +216,10 @@ export class Migrator {
|
|
|
215
216
|
// Tables this plan creates are left out: their `CREATE` carries their triggers, and asking the
|
|
216
217
|
// catalogue about a table that is not there yet fails outright on some engines.
|
|
217
218
|
const wanted = entities.filter((entity) => !fresh.has(this.tableOf(entity)));
|
|
218
|
-
const bySchema = new Map();
|
|
219
219
|
const state = [];
|
|
220
220
|
for (const entity of wanted) {
|
|
221
221
|
const meta = getMeta(entity);
|
|
222
|
-
const
|
|
223
|
-
let owned = bySchema.get(schema);
|
|
224
|
-
if (!owned) {
|
|
225
|
-
owned = await this.schemaIntrospectorFor(schema).ownedTriggers();
|
|
226
|
-
bySchema.set(schema, owned);
|
|
227
|
-
}
|
|
228
|
-
const installed = owned.get(dialect.resolveTableAlias(meta)) ?? new Map();
|
|
222
|
+
const installed = await this.schemaIntrospectorFor(dialect.resolveSchema(meta)).ownedTriggers(dialect.resolveTableAlias(meta));
|
|
229
223
|
// An installed trigger alone keeps it: an entity that stopped declaring one has it to drop.
|
|
230
224
|
if (installed.size || hasTriggers(meta)) {
|
|
231
225
|
state.push({ entity, installed });
|
|
@@ -239,7 +233,7 @@ export class Migrator {
|
|
|
239
233
|
* its entity declares goes back on after. `down` is lazy: SQLite cannot express every alter's inverse.
|
|
240
234
|
*/
|
|
241
235
|
alterPlan(generator, altered, state) {
|
|
242
|
-
const changing = new Set(altered.filter((diff) => diff.
|
|
236
|
+
const changing = new Set(altered.filter((diff) => sides(diff.columns, 'from').length).map((diff) => diff.tableName));
|
|
243
237
|
const cleared = state.filter(({ entity }) => changing.has(this.tableOf(entity)));
|
|
244
238
|
const after = state.map((it) => (cleared.includes(it) ? { entity: it.entity, installed: new Map() } : it));
|
|
245
239
|
return {
|
|
@@ -250,7 +244,7 @@ export class Migrator {
|
|
|
250
244
|
],
|
|
251
245
|
down: () => [
|
|
252
246
|
...this.revertedTriggers(generator, after),
|
|
253
|
-
...altered.toReversed().flatMap((diff) => generator.
|
|
247
|
+
...altered.toReversed().flatMap((diff) => generator.generateAlterTable(reverseDiff(diff))),
|
|
254
248
|
...cleared.flatMap(({ installed }) => [...installed.values()].flat().map((sql) => `${sql};`)),
|
|
255
249
|
],
|
|
256
250
|
};
|
|
@@ -400,45 +394,40 @@ export class Migrator {
|
|
|
400
394
|
altered: diffs.filter((diff) => diff.type === 'alter'),
|
|
401
395
|
};
|
|
402
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
|
|
399
|
+
* and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
|
|
400
|
+
* Without `drop`, a column's drop is held too.
|
|
401
|
+
*/
|
|
403
402
|
filterDiff(diff, options) {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (filteredDiff.columnsToDrop?.length) {
|
|
409
|
-
this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${filteredDiff.columnsToDrop.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToDrop.join(', ')} (safe mode active)`);
|
|
410
|
-
delete filteredDiff.columnsToDrop;
|
|
411
|
-
}
|
|
412
|
-
if (filteredDiff.columnsToAlter?.length) {
|
|
413
|
-
this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.columnsToAlter.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToAlter.map((c) => c.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
|
|
414
|
-
delete filteredDiff.columnsToAlter;
|
|
415
|
-
}
|
|
416
|
-
if (filteredDiff.primaryKey) {
|
|
417
|
-
// Rewriting a key drops a constraint and rebuilds an index over the whole table, and fails
|
|
418
|
-
// outright where the new columns are null on rows that already exist. Firmly not additive.
|
|
419
|
-
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.`);
|
|
420
|
-
delete filteredDiff.primaryKey;
|
|
403
|
+
const safe = options.safe !== false;
|
|
404
|
+
const skip = (what, names, fix) => {
|
|
405
|
+
if (names.length) {
|
|
406
|
+
this.logger.logSkippedMigration(`[AutoSync] Skipped ${names.length} ${what} in table '${diff.tableName}': ${names.join(', ')} (${fix}).`);
|
|
421
407
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
if (filteredDiff.indexesToDrop?.length) {
|
|
429
|
-
// An index recreated under its old name is a drop and an add, held back together.
|
|
430
|
-
const dropped = new Set(filteredDiff.indexesToDrop.map((index) => index.name));
|
|
431
|
-
this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${dropped.size} indexes in table '${diff.tableName}': ${[...dropped].join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
|
|
432
|
-
filteredDiff.indexesToAdd = filteredDiff.indexesToAdd?.filter((index) => !dropped.has(index.name));
|
|
433
|
-
delete filteredDiff.indexesToDrop;
|
|
408
|
+
};
|
|
409
|
+
const safeFix = 'safe mode active. Use a migration or { safe: false } to apply';
|
|
410
|
+
const additive = (what, changes, nameOf) => {
|
|
411
|
+
if (!safe) {
|
|
412
|
+
return changes;
|
|
434
413
|
}
|
|
435
|
-
|
|
414
|
+
skip(`${what} changes`, sides(changes, 'from').map(nameOf), safeFix);
|
|
415
|
+
return nonEmpty((changes ?? []).filter((change) => change.from === undefined));
|
|
416
|
+
};
|
|
417
|
+
const columns = additive('column', diff.columns, (column) => column.name);
|
|
418
|
+
if (safe && diff.primaryKey) {
|
|
419
|
+
skip('primary key changes', [diff.tableName], safeFix);
|
|
436
420
|
}
|
|
437
|
-
if (!options.drop
|
|
438
|
-
|
|
439
|
-
delete filteredDiff.columnsToDrop;
|
|
421
|
+
if (!options.drop) {
|
|
422
|
+
skip('column drops', dropped(columns).map((column) => column.name), 'drop: false. Use { drop: true } to apply');
|
|
440
423
|
}
|
|
441
|
-
return
|
|
424
|
+
return {
|
|
425
|
+
...diff,
|
|
426
|
+
primaryKey: safe ? undefined : diff.primaryKey,
|
|
427
|
+
columns: options.drop ? columns : nonEmpty((columns ?? []).filter((change) => change.to !== undefined)),
|
|
428
|
+
indexes: additive('index', diff.indexes, (index) => index.name),
|
|
429
|
+
foreignKeys: additive('foreign key', diff.foreignKeys, (foreignKey) => foreignKey.name ?? foreignKey.columns.join(', ')),
|
|
430
|
+
};
|
|
442
431
|
}
|
|
443
432
|
/** Runs the statements a generator wrote, in one transaction where the engine takes DDL in one. */
|
|
444
433
|
async executeSyncStatements(statements, options) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Change, SchemaDiff } from '../type/index.js';
|
|
2
|
+
/** Each change's end on `side`, where it has one: what a drop half removes (`from`), or an add half creates (`to`). */
|
|
3
|
+
export declare function sides<T>(changes: readonly Change<T>[] | undefined, side: 'from' | 'to'): T[];
|
|
4
|
+
/** What the changes add: each `to` with no `from`. */
|
|
5
|
+
export declare function added<T>(changes?: readonly Change<T>[]): T[];
|
|
6
|
+
/** What the changes drop: each `from` with no `to`. */
|
|
7
|
+
export declare function dropped<T>(changes?: readonly Change<T>[]): T[];
|
|
8
|
+
/** The changes that alter an object in place, which only a column can. */
|
|
9
|
+
export declare function alterations<T>(changes?: readonly Change<T>[]): {
|
|
10
|
+
readonly from: T;
|
|
11
|
+
readonly to: T;
|
|
12
|
+
}[];
|
|
13
|
+
/** `items`, or nothing where it has none, so an empty change list is left off a diff. */
|
|
14
|
+
export declare function nonEmpty<T>(items: readonly T[]): readonly T[] | undefined;
|
|
15
|
+
/** `change` undone: an add becomes a drop, a drop an add, and an alter runs the other way. */
|
|
16
|
+
export declare function swap<T>({ from, to }: Change<T>): Change<T>;
|
|
17
|
+
/** `diff` undone: every change swapped, which is what a migration's `down` runs. */
|
|
18
|
+
export declare function reverseDiff(diff: SchemaDiff): SchemaDiff;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Each change's end on `side`, where it has one: what a drop half removes (`from`), or an add half creates (`to`). */
|
|
2
|
+
export function sides(changes, side) {
|
|
3
|
+
return (changes ?? []).flatMap((change) => {
|
|
4
|
+
const end = change[side];
|
|
5
|
+
return end === undefined ? [] : [end];
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
/** What the changes add: each `to` with no `from`. */
|
|
9
|
+
export function added(changes = []) {
|
|
10
|
+
return changes.flatMap(({ from, to }) => (from === undefined && to !== undefined ? [to] : []));
|
|
11
|
+
}
|
|
12
|
+
/** What the changes drop: each `from` with no `to`. */
|
|
13
|
+
export function dropped(changes = []) {
|
|
14
|
+
return changes.flatMap(({ from, to }) => (to === undefined && from !== undefined ? [from] : []));
|
|
15
|
+
}
|
|
16
|
+
/** The changes that alter an object in place, which only a column can. */
|
|
17
|
+
export function alterations(changes = []) {
|
|
18
|
+
return changes.flatMap(({ from, to }) => (from === undefined || to === undefined ? [] : [{ from, to }]));
|
|
19
|
+
}
|
|
20
|
+
/** `items`, or nothing where it has none, so an empty change list is left off a diff. */
|
|
21
|
+
export function nonEmpty(items) {
|
|
22
|
+
return items.length ? items : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** `change` undone: an add becomes a drop, a drop an add, and an alter runs the other way. */
|
|
25
|
+
export function swap({ from, to }) {
|
|
26
|
+
return { from: to, to: from };
|
|
27
|
+
}
|
|
28
|
+
/** `diff` undone: every change swapped, which is what a migration's `down` runs. */
|
|
29
|
+
export function reverseDiff(diff) {
|
|
30
|
+
return {
|
|
31
|
+
...diff,
|
|
32
|
+
primaryKey: diff.primaryKey && swap(diff.primaryKey),
|
|
33
|
+
columns: diff.columns?.map(swap),
|
|
34
|
+
indexes: diff.indexes?.map(swap),
|
|
35
|
+
foreignKeys: diff.foreignKeys?.map(swap),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -68,18 +68,19 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
68
68
|
*/
|
|
69
69
|
private orderedTables;
|
|
70
70
|
generateDropTable(tableName: string, options?: DropSchemaOptions): string;
|
|
71
|
+
/**
|
|
72
|
+
* The statements taking a table through `diff`, in the one order both directions need: whatever holds
|
|
73
|
+
* something down goes before it and comes back after it. A foreign key holds its columns and the key it
|
|
74
|
+
* points at, so it goes first and comes back last; the key holds its columns; and some engines drop an
|
|
75
|
+
* index along with its column, which would leave nothing to name. An alter is its drop, then its add.
|
|
76
|
+
*/
|
|
71
77
|
generateAlterTable(diff: SchemaDiff): string[];
|
|
72
78
|
/** `ADD CONSTRAINT` for each of `foreignKeys`. */
|
|
73
79
|
private addForeignKeyStatements;
|
|
74
|
-
/** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
|
|
75
|
-
private dropForeignKeyStatements;
|
|
76
|
-
/** The `ALTER COLUMN` restating each of `columns`. */
|
|
77
|
-
private alterColumnStatements;
|
|
78
80
|
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
79
81
|
private addIndexStatements;
|
|
80
|
-
/**
|
|
81
|
-
private
|
|
82
|
-
generateAlterTableDown(diff: SchemaDiff): string[];
|
|
82
|
+
/** A column added to a table that exists, and its comment where the engine keeps one apart. */
|
|
83
|
+
private addColumnStatements;
|
|
83
84
|
generateCreateIndex(tableName: string, index: IndexSchema, options?: {
|
|
84
85
|
ifNotExists?: boolean;
|
|
85
86
|
}): string;
|
|
@@ -96,8 +97,8 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
96
97
|
/**
|
|
97
98
|
* The one place a column definition is spelled, so the `ColumnSchema` and `ColumnNode` paths cannot
|
|
98
99
|
* drift. A key column states `NOT NULL` rather than leave it to the key: SQLite lets a key column hold
|
|
99
|
-
* NULL otherwise, and SQL Server adds no key over a nullable column. `UNIQUE
|
|
100
|
-
* enum's `CHECK` comes last, the only place MariaDB takes it.
|
|
100
|
+
* NULL otherwise, and SQL Server adds no key over a nullable column. Never `UNIQUE`: a unique column is
|
|
101
|
+
* a unique index, which the table creates beside it. An enum's `CHECK` comes last, the only place MariaDB takes it.
|
|
101
102
|
*/
|
|
102
103
|
private renderColumn;
|
|
103
104
|
/**
|
|
@@ -124,10 +125,8 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
124
125
|
protected diffOptions(): DiffOptions;
|
|
125
126
|
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
126
127
|
private columnNodeToSchema;
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
*/
|
|
130
|
-
protected isDefaultValueEqual(current: unknown, desired: unknown): boolean;
|
|
128
|
+
/** Whether a column's stored default is the one the entity declares, as this engine reprints it. */
|
|
129
|
+
readonly defaultsEqual: (desired: unknown, current: unknown) => boolean;
|
|
131
130
|
generateCreateTableFromNode(table: TableNode, options?: {
|
|
132
131
|
ifNotExists?: boolean;
|
|
133
132
|
}): string[];
|
|
@@ -186,4 +185,4 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
186
185
|
* The entities as an AST, named by `generator`'s resolvers rather than a naming strategy, which would
|
|
187
186
|
* also rename an explicit `@Entity({ name })` and so compare each table under another name.
|
|
188
187
|
*/
|
|
189
|
-
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName' | 'compileDdl' | 'compileIndexPredicate'>, entities: readonly Type<object>[], options?: Pick<BuildSchemaASTOptions, 'defaultForeignKeyAction' | 'textScoreIndexes'>): SchemaAST;
|
|
188
|
+
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName' | 'compileDdl' | 'compileIndexPredicate'>, entities: readonly Type<object>[], options?: Pick<BuildSchemaASTOptions, 'defaultForeignKeyAction' | 'textScoreIndexes' | 'vectorIndexRequiresNotNull'>): SchemaAST;
|