uql-orm 0.25.1 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/browser/uql-browser.min.js.map +1 -1
- package/dist/cockroachdb/cockroachDialect.js +1 -1
- package/dist/dialect/indexSqlDialect.d.ts +3 -2
- package/dist/dialect/indexSqlDialect.js +10 -8
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +0 -7
- package/dist/dialect/pgLikeSqlDialect.js +1 -0
- package/dist/maria/mariaDialect.js +1 -1
- package/dist/migrate/builder/migrationBuilder.js +1 -1
- package/dist/migrate/builder/tableBuilder.js +2 -2
- package/dist/migrate/cli.js +4 -6
- package/dist/migrate/codegen/entityCodeGenerator.d.ts +5 -0
- package/dist/migrate/codegen/entityCodeGenerator.js +22 -25
- package/dist/migrate/codegen/fieldOptionsSource.d.ts +1 -1
- package/dist/migrate/codegen/fieldOptionsSource.js +6 -1
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +14 -0
- package/dist/migrate/codegen/indexDecoratorSource.js +105 -0
- package/dist/migrate/drift/driftDetector.d.ts +5 -50
- package/dist/migrate/drift/driftDetector.js +215 -224
- package/dist/migrate/drift/index.d.ts +1 -1
- package/dist/migrate/drift/index.js +1 -1
- package/dist/migrate/generator/definitionToNode.d.ts +9 -0
- package/dist/migrate/generator/definitionToNode.js +79 -0
- package/dist/migrate/generator/indexNodeToSchema.js +4 -2
- package/dist/migrate/generator/mongoSchemaGenerator.js +3 -3
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +3 -0
- package/dist/migrate/introspection/baseSqlIntrospector.js +13 -5
- package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -0
- package/dist/migrate/introspection/mongoIntrospector.js +5 -10
- package/dist/migrate/introspection/mysqlIntrospector.js +1 -1
- package/dist/migrate/introspection/postgresIntrospector.d.ts +57 -5
- package/dist/migrate/introspection/postgresIntrospector.js +93 -10
- package/dist/migrate/introspection/sqliteIntrospector.js +1 -1
- package/dist/migrate/migrator.js +3 -2
- package/dist/migrate/schemaGenerator.d.ts +26 -3
- package/dist/migrate/schemaGenerator.js +53 -92
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/index.js +2 -2
- package/dist/schema/indexColumns.d.ts +10 -0
- package/dist/schema/indexColumns.js +11 -0
- package/dist/schema/indexDifferences.d.ts +22 -0
- package/dist/schema/indexDifferences.js +49 -0
- package/dist/schema/schemaAST.js +2 -8
- package/dist/schema/schemaASTBuilder.d.ts +6 -59
- package/dist/schema/schemaASTBuilder.js +208 -236
- package/dist/schema/schemaASTDiffer.d.ts +9 -56
- package/dist/schema/schemaASTDiffer.js +229 -393
- package/dist/schema/types.d.ts +5 -12
- package/dist/schema/types.js +15 -0
- package/dist/type/dialect.d.ts +7 -2
- package/dist/type/dialect.js +1 -0
- package/dist/type/entity.d.ts +1 -1
- package/dist/type/migration.d.ts +14 -1
- package/dist/util/string.util.js +6 -1
- package/package.json +5 -5
|
@@ -6,6 +6,8 @@ import { escapeSqlId } from '../../util/index.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export class BaseSqlIntrospector {
|
|
8
8
|
dialect;
|
|
9
|
+
/** Columns and uniqueness only; each introspector opts in to what its catalogue queries report. */
|
|
10
|
+
indexFacets = new Set();
|
|
9
11
|
constructor(dialect) {
|
|
10
12
|
this.dialect = dialect;
|
|
11
13
|
}
|
|
@@ -82,8 +84,8 @@ export class BaseSqlIntrospector {
|
|
|
82
84
|
const toTable = tableNodes.get(fk.referencedTable);
|
|
83
85
|
if (!toTable)
|
|
84
86
|
continue;
|
|
85
|
-
const fromColumns = fk.columns.
|
|
86
|
-
const toColumns = fk.referencedColumns.
|
|
87
|
+
const fromColumns = fk.columns.flatMap((name) => fromTable.columns.get(name) ?? []);
|
|
88
|
+
const toColumns = fk.referencedColumns.flatMap((name) => toTable.columns.get(name) ?? []);
|
|
87
89
|
if (fromColumns.length > 0 && toColumns.length > 0) {
|
|
88
90
|
const rel = {
|
|
89
91
|
name: fk.name,
|
|
@@ -106,13 +108,19 @@ export class BaseSqlIntrospector {
|
|
|
106
108
|
if (!table)
|
|
107
109
|
continue;
|
|
108
110
|
for (const idx of schema.indexes) {
|
|
109
|
-
|
|
110
|
-
if
|
|
111
|
+
// An expression has no column to resolve. Dropping the entries that name a column this table
|
|
112
|
+
// does not have, and the index if that leaves none, is what the entity side does too.
|
|
113
|
+
const entries = idx.entries.filter((entry) => entry.expression || table.columns.has(entry.column));
|
|
114
|
+
if (entries.length > 0) {
|
|
111
115
|
const index = {
|
|
112
116
|
name: idx.name,
|
|
113
117
|
table,
|
|
114
|
-
|
|
118
|
+
entries,
|
|
115
119
|
unique: idx.unique,
|
|
120
|
+
type: idx.type,
|
|
121
|
+
where: idx.where,
|
|
122
|
+
include: idx.include,
|
|
123
|
+
source: 'database',
|
|
116
124
|
};
|
|
117
125
|
ast.addIndex(index);
|
|
118
126
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IndexFacet } from '../../schema/indexDifferences.js';
|
|
1
2
|
import { SchemaAST } from '../../schema/schemaAST.js';
|
|
2
3
|
import type { QuerierPool, SchemaIntrospector, TableSchema } from '../../type/index.js';
|
|
3
4
|
/**
|
|
@@ -6,6 +7,8 @@ import type { QuerierPool, SchemaIntrospector, TableSchema } from '../../type/in
|
|
|
6
7
|
*/
|
|
7
8
|
export declare class MongoSchemaIntrospector implements SchemaIntrospector {
|
|
8
9
|
private readonly pool;
|
|
10
|
+
/** `listIndexes` reports keys and uniqueness; a `partialFilterExpression` is no SQL predicate. */
|
|
11
|
+
readonly indexFacets: ReadonlySet<IndexFacet>;
|
|
9
12
|
constructor(pool: QuerierPool);
|
|
10
13
|
introspect(): Promise<SchemaAST>;
|
|
11
14
|
getTableSchema(tableName: string): Promise<TableSchema | undefined>;
|
|
@@ -5,6 +5,8 @@ import { SchemaAST } from '../../schema/schemaAST.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export class MongoSchemaIntrospector {
|
|
7
7
|
pool;
|
|
8
|
+
/** `listIndexes` reports keys and uniqueness; a `partialFilterExpression` is no SQL predicate. */
|
|
9
|
+
indexFacets = new Set();
|
|
8
10
|
constructor(pool) {
|
|
9
11
|
this.pool = pool;
|
|
10
12
|
}
|
|
@@ -26,8 +28,7 @@ export class MongoSchemaIntrospector {
|
|
|
26
28
|
};
|
|
27
29
|
if (schema.indexes) {
|
|
28
30
|
for (const idx of schema.indexes) {
|
|
29
|
-
const
|
|
30
|
-
for (const { column: colName } of idx.columns) {
|
|
31
|
+
for (const { column: colName } of idx.entries) {
|
|
31
32
|
let column = columns.get(colName);
|
|
32
33
|
if (!column) {
|
|
33
34
|
column = {
|
|
@@ -42,14 +43,8 @@ export class MongoSchemaIntrospector {
|
|
|
42
43
|
};
|
|
43
44
|
columns.set(colName, column);
|
|
44
45
|
}
|
|
45
|
-
indexColumns.push(column);
|
|
46
46
|
}
|
|
47
|
-
table.indexes.push({
|
|
48
|
-
name: idx.name,
|
|
49
|
-
table,
|
|
50
|
-
columns: indexColumns,
|
|
51
|
-
unique: idx.unique,
|
|
52
|
-
});
|
|
47
|
+
table.indexes.push({ name: idx.name, table, entries: idx.entries, unique: idx.unique });
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
50
|
ast.addTable(table);
|
|
@@ -73,7 +68,7 @@ export class MongoSchemaIntrospector {
|
|
|
73
68
|
columns: [], // We don't have columns in Mongo
|
|
74
69
|
indexes: indexes.map((idx) => ({
|
|
75
70
|
name: idx.name ?? Object.keys(idx.key).join('_'),
|
|
76
|
-
|
|
71
|
+
entries: Object.keys(idx.key).map((column) => ({ column })),
|
|
77
72
|
unique: !!idx.unique,
|
|
78
73
|
})),
|
|
79
74
|
};
|
|
@@ -110,7 +110,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
110
110
|
async mapIndexesResult(_read, _tableName, results) {
|
|
111
111
|
return results.map((row) => ({
|
|
112
112
|
name: row.index_name,
|
|
113
|
-
|
|
113
|
+
entries: (row.columns || '').split(',').map((column) => ({ column })),
|
|
114
114
|
unique: Boolean(row.is_unique),
|
|
115
115
|
}));
|
|
116
116
|
}
|
|
@@ -1,22 +1,44 @@
|
|
|
1
|
+
import type { IndexFacet } from '../../schema/indexDifferences.js';
|
|
1
2
|
import type { ColumnSchema, ForeignKeySchema, IndexSchema, RawRow } from '../../type/index.js';
|
|
2
3
|
import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
|
|
3
4
|
/**
|
|
4
5
|
* PostgreSQL schema introspector
|
|
5
6
|
*/
|
|
6
7
|
export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
8
|
+
/**
|
|
9
|
+
* Expressions and predicates are read back too, for `generate:from-db`, but they are text the
|
|
10
|
+
* database reprints in its own words, so they are not comparable and are not claimed here.
|
|
11
|
+
*/
|
|
12
|
+
readonly indexFacets: ReadonlySet<IndexFacet>;
|
|
7
13
|
protected getTableNamesQuery(): string;
|
|
8
14
|
protected tableExistsQuery(): string;
|
|
9
15
|
protected parseTableExistsResult(results: RawRow[]): boolean;
|
|
10
16
|
protected getColumnsQuery(_tableName: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* `attname` where the entry is a column, `pg_get_indexdef` for that one position where it is an
|
|
19
|
+
* expression. Neither alone will do: an expression entry has `attnum = 0`, so joining `pg_attribute`
|
|
20
|
+
* on it silently dropped the entry (a `lower(email)` index read back as having no columns at all),
|
|
21
|
+
* while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
|
|
22
|
+
* `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
|
|
23
|
+
*
|
|
24
|
+
* Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
|
|
25
|
+
* `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
|
|
26
|
+
* project it had an index its entities never asked for.
|
|
27
|
+
*/
|
|
11
28
|
protected getIndexesQuery(_tableName: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
|
|
31
|
+
* for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
|
|
32
|
+
* `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
|
|
33
|
+
*
|
|
34
|
+
* Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
|
|
35
|
+
* compare. This one decides which indexes are reported at all.
|
|
36
|
+
*/
|
|
37
|
+
protected readonly constraintIndexTypes: readonly string[];
|
|
12
38
|
protected getForeignKeysQuery(_tableName: string): string;
|
|
13
39
|
protected getPrimaryKeyQuery(_tableName: string): string;
|
|
14
40
|
protected mapColumnsResult(_read: TableRowReader, _tableName: string, results: PostgresColumnRow[]): Promise<ColumnSchema[]>;
|
|
15
|
-
protected mapIndexesResult(_read: TableRowReader, _tableName: string, results:
|
|
16
|
-
index_name: string;
|
|
17
|
-
columns: string[];
|
|
18
|
-
is_unique: boolean;
|
|
19
|
-
}[]): Promise<IndexSchema[]>;
|
|
41
|
+
protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: PostgresIndexRow[]): Promise<IndexSchema[]>;
|
|
20
42
|
protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: {
|
|
21
43
|
constraint_name: string;
|
|
22
44
|
columns: string[];
|
|
@@ -29,6 +51,36 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
|
|
|
29
51
|
protected parseDefaultValue(defaultValue: string | null): unknown;
|
|
30
52
|
protected isAutoIncrement(columnDefault: string | null, isIdentity: string): boolean;
|
|
31
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
|
|
56
|
+
* still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
|
|
57
|
+
* on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
|
|
58
|
+
* every ascending index as drifted, against an entity that could not have asked for one. Its access
|
|
59
|
+
* method, `prefix`, needs nothing: a method that is not a known index type is reported as no type.
|
|
60
|
+
*/
|
|
61
|
+
export declare class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
|
|
62
|
+
readonly indexFacets: ReadonlySet<IndexFacet>;
|
|
63
|
+
/**
|
|
64
|
+
* `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
|
|
65
|
+
* UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
|
|
66
|
+
* user asked for and report it missing forever. It leaves no way to tell the two apart, so the
|
|
67
|
+
* index a `@Field({ unique })` builds underneath itself stays visible there.
|
|
68
|
+
*/
|
|
69
|
+
protected readonly constraintIndexTypes: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
/** One entry of one index; what the index itself is repeats across its rows. */
|
|
72
|
+
type PostgresIndexRow = {
|
|
73
|
+
index_name: string;
|
|
74
|
+
is_unique: boolean;
|
|
75
|
+
method: string;
|
|
76
|
+
predicate: string | null;
|
|
77
|
+
is_key: boolean;
|
|
78
|
+
is_expression: boolean;
|
|
79
|
+
entry: string;
|
|
80
|
+
descending: boolean;
|
|
81
|
+
nulls_first: boolean;
|
|
82
|
+
ops_class: string | null;
|
|
83
|
+
};
|
|
32
84
|
type PostgresColumnRow = {
|
|
33
85
|
column_name: string;
|
|
34
86
|
data_type: string;
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
import { INDEX_TYPES } from '../../schema/types.js';
|
|
1
2
|
import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
|
|
2
3
|
/**
|
|
3
4
|
* PostgreSQL schema introspector
|
|
4
5
|
*/
|
|
5
6
|
export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
7
|
+
/**
|
|
8
|
+
* Expressions and predicates are read back too, for `generate:from-db`, but they are text the
|
|
9
|
+
* database reprints in its own words, so they are not comparable and are not claimed here.
|
|
10
|
+
*/
|
|
11
|
+
indexFacets = new Set([
|
|
12
|
+
'order',
|
|
13
|
+
'nulls',
|
|
14
|
+
'opsClass',
|
|
15
|
+
'accessMethod',
|
|
16
|
+
'include',
|
|
17
|
+
]);
|
|
6
18
|
getTableNamesQuery() {
|
|
7
19
|
return /*sql*/ `
|
|
8
20
|
SELECT table_name
|
|
@@ -67,25 +79,58 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
67
79
|
ORDER BY c.ordinal_position
|
|
68
80
|
`;
|
|
69
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* `attname` where the entry is a column, `pg_get_indexdef` for that one position where it is an
|
|
84
|
+
* expression. Neither alone will do: an expression entry has `attnum = 0`, so joining `pg_attribute`
|
|
85
|
+
* on it silently dropped the entry (a `lower(email)` index read back as having no columns at all),
|
|
86
|
+
* while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
|
|
87
|
+
* `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
|
|
88
|
+
*
|
|
89
|
+
* Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
|
|
90
|
+
* `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
|
|
91
|
+
* project it had an index its entities never asked for.
|
|
92
|
+
*/
|
|
70
93
|
getIndexesQuery(_tableName) {
|
|
71
94
|
return /*sql*/ `
|
|
72
95
|
SELECT
|
|
73
96
|
i.relname AS index_name,
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
ix.indisunique AS is_unique,
|
|
98
|
+
am.amname AS method,
|
|
99
|
+
pg_get_expr(ix.indpred, ix.indrelid, true) AS predicate,
|
|
100
|
+
k.n <= ix.indnkeyatts AS is_key,
|
|
101
|
+
k.attnum = 0 AS is_expression,
|
|
102
|
+
COALESCE(a.attname::text, pg_get_indexdef(ix.indexrelid, k.n::int, true)) AS entry,
|
|
103
|
+
(ix.indoption[k.n - 1] & 1) <> 0 AS descending,
|
|
104
|
+
(ix.indoption[k.n - 1] & 2) <> 0 AS nulls_first,
|
|
105
|
+
CASE WHEN op.opcdefault THEN NULL ELSE op.opcname END AS ops_class
|
|
76
106
|
FROM pg_class t
|
|
77
107
|
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
78
108
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
109
|
+
JOIN pg_am am ON am.oid = i.relam
|
|
79
110
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
80
111
|
CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)
|
|
81
|
-
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
112
|
+
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum AND k.attnum > 0
|
|
113
|
+
LEFT JOIN pg_opclass op ON op.oid = ix.indclass[k.n - 1]
|
|
82
114
|
WHERE t.relname = $1
|
|
83
115
|
AND n.nspname = 'public'
|
|
84
116
|
AND NOT ix.indisprimary
|
|
85
|
-
|
|
86
|
-
|
|
117
|
+
AND NOT EXISTS (
|
|
118
|
+
SELECT 1 FROM pg_constraint con
|
|
119
|
+
WHERE con.conindid = ix.indexrelid
|
|
120
|
+
AND con.contype IN (${this.constraintIndexTypes.map((type) => `'${type}'`).join(', ')})
|
|
121
|
+
)
|
|
122
|
+
ORDER BY i.relname, k.n
|
|
87
123
|
`;
|
|
88
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
|
|
127
|
+
* for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
|
|
128
|
+
* `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
|
|
129
|
+
*
|
|
130
|
+
* Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
|
|
131
|
+
* compare. This one decides which indexes are reported at all.
|
|
132
|
+
*/
|
|
133
|
+
constraintIndexTypes = ['p', 'u', 'x'];
|
|
89
134
|
getForeignKeysQuery(_tableName) {
|
|
90
135
|
return /*sql*/ `
|
|
91
136
|
SELECT
|
|
@@ -141,11 +186,18 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
141
186
|
}));
|
|
142
187
|
}
|
|
143
188
|
async mapIndexesResult(_read, _tableName, results) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
189
|
+
// One row per index entry, ordered by position, so the rows of an index are its entries in order.
|
|
190
|
+
return [...Map.groupBy(results, (row) => row.index_name)].map(([name, rows]) => {
|
|
191
|
+
const include = rows.filter((row) => !row.is_key).map((row) => row.entry);
|
|
192
|
+
return {
|
|
193
|
+
name,
|
|
194
|
+
entries: rows.filter((row) => row.is_key).map(mapIndexEntry),
|
|
195
|
+
unique: rows[0].is_unique,
|
|
196
|
+
type: INDEX_TYPES.find((type) => type === rows[0].method),
|
|
197
|
+
where: rows[0].predicate ?? undefined,
|
|
198
|
+
include: include.length > 0 ? include : undefined,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
149
201
|
}
|
|
150
202
|
async mapForeignKeysResult(_read, _tableName, results) {
|
|
151
203
|
return results.map((row) => ({
|
|
@@ -200,3 +252,34 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
200
252
|
return columnDefault?.includes('nextval(') ?? false;
|
|
201
253
|
}
|
|
202
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Postgres states every entry in full: a plain column still reports `order: 'asc'`, and only a
|
|
257
|
+
* non-default operator class is named. The diff defaults the entity side to match, so an option
|
|
258
|
+
* omitted there and one written out are not read as two different indexes.
|
|
259
|
+
*/
|
|
260
|
+
function mapIndexEntry(row) {
|
|
261
|
+
return {
|
|
262
|
+
column: row.entry,
|
|
263
|
+
...(row.is_expression && { expression: true }),
|
|
264
|
+
order: row.descending ? 'desc' : 'asc',
|
|
265
|
+
nulls: row.nulls_first ? 'first' : 'last',
|
|
266
|
+
...(row.ops_class && { opsClass: row.ops_class }),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
|
|
271
|
+
* still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
|
|
272
|
+
* on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
|
|
273
|
+
* every ascending index as drifted, against an entity that could not have asked for one. Its access
|
|
274
|
+
* method, `prefix`, needs nothing: a method that is not a known index type is reported as no type.
|
|
275
|
+
*/
|
|
276
|
+
export class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
|
|
277
|
+
indexFacets = new Set(['order', 'include']);
|
|
278
|
+
/**
|
|
279
|
+
* `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
|
|
280
|
+
* UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
|
|
281
|
+
* user asked for and report it missing forever. It leaves no way to tell the two apart, so the
|
|
282
|
+
* index a `@Field({ unique })` builds underneath itself stays visible there.
|
|
283
|
+
*/
|
|
284
|
+
constraintIndexTypes = ['p', 'x'];
|
|
285
|
+
}
|
|
@@ -88,7 +88,7 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
88
88
|
if (named.length === columns.length && (isUserCreated || isCompositeUnique)) {
|
|
89
89
|
indexSchemas.push({
|
|
90
90
|
name: index.name,
|
|
91
|
-
|
|
91
|
+
entries: named.map((column) => ({ column: column.name })),
|
|
92
92
|
unique: Boolean(index.unique),
|
|
93
93
|
});
|
|
94
94
|
}
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -8,7 +8,7 @@ import { LoggerWrapper } from '../util/index.js';
|
|
|
8
8
|
import { withQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
|
|
9
9
|
import { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCalls, } from './codegen/migrationFile.js';
|
|
10
10
|
import { runMongoCommand } from './generator/mongoCommand.js';
|
|
11
|
-
import { MongoSchemaIntrospector, MysqlSchemaIntrospector, PostgresSchemaIntrospector, SqliteSchemaIntrospector, } from './introspection/index.js';
|
|
11
|
+
import { CockroachSchemaIntrospector, MongoSchemaIntrospector, MysqlSchemaIntrospector, PostgresSchemaIntrospector, SqliteSchemaIntrospector, } from './introspection/index.js';
|
|
12
12
|
import { createSchemaGenerator } from './schemaGenerator.js';
|
|
13
13
|
import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
|
|
14
14
|
import { DatabaseMigrationStorage } from './storage/databaseStorage.js';
|
|
@@ -78,8 +78,9 @@ export class Migrator {
|
|
|
78
78
|
}
|
|
79
79
|
switch (d) {
|
|
80
80
|
case 'postgres':
|
|
81
|
-
case 'cockroachdb':
|
|
82
81
|
return new PostgresSchemaIntrospector(this.pool);
|
|
82
|
+
case 'cockroachdb':
|
|
83
|
+
return new CockroachSchemaIntrospector(this.pool);
|
|
83
84
|
case 'mysql':
|
|
84
85
|
case 'mariadb':
|
|
85
86
|
return new MysqlSchemaIntrospector(this.pool);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
|
|
2
|
+
import type { SchemaAST } from '../schema/schemaAST.js';
|
|
2
3
|
import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
|
|
3
|
-
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SqlDdlGenerator, Type } from '../type/index.js';
|
|
4
|
+
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
|
|
4
5
|
import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
|
|
5
6
|
/**
|
|
6
7
|
* Unified SQL schema generator.
|
|
@@ -82,6 +83,21 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
82
83
|
* Compare an entity with a database table node and return the differences.
|
|
83
84
|
*/
|
|
84
85
|
diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Indexes the entity declares that the table does not have, matched by name and built the same way
|
|
88
|
+
* `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
|
|
89
|
+
* up rather than waiting for the table to be created from scratch somewhere else.
|
|
90
|
+
*
|
|
91
|
+
* Only ever additive. An index the entity does not name is left alone: it may well have been
|
|
92
|
+
* created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
|
|
93
|
+
*/
|
|
94
|
+
private missingIndexes;
|
|
95
|
+
/**
|
|
96
|
+
* A vector index this dialect declares inside `CREATE TABLE` rather than as a statement of its own,
|
|
97
|
+
* which MariaDB is alone in doing. It has no `CREATE INDEX` form, so it can only ever be created
|
|
98
|
+
* with its table, never added to one.
|
|
99
|
+
*/
|
|
100
|
+
private isInlineVectorIndex;
|
|
85
101
|
private columnNodeToSchema;
|
|
86
102
|
/**
|
|
87
103
|
* Convert field options to ColumnSchema. Both sides of a diff are the engine's SQL spelling: what it
|
|
@@ -131,9 +147,16 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
131
147
|
generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
|
|
132
148
|
generateAddForeignKeySql(tableName: string, foreignKey: TableForeignKeyDefinition): string;
|
|
133
149
|
generateDropForeignKeySql(tableName: string, constraintName: string): string;
|
|
134
|
-
private tableDefinitionToNode;
|
|
135
|
-
private fullColumnDefinitionToNode;
|
|
136
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The entities as an AST, named the way `generator` names things.
|
|
153
|
+
*
|
|
154
|
+
* Its resolvers rather than a naming strategy, because the two disagree: a strategy renames whatever
|
|
155
|
+
* it is handed, while a generator leaves an explicit `@Entity({ name })` alone. Build the AST the
|
|
156
|
+
* other way and the table is created under one name and compared under another, which reports every
|
|
157
|
+
* table of a project using a naming strategy as both missing and unexpected.
|
|
158
|
+
*/
|
|
159
|
+
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableName' | 'resolveColumnName'>, entities: readonly Type<unknown>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
|
|
137
160
|
/**
|
|
138
161
|
* Synchronous factory for SQL schema generators only.
|
|
139
162
|
* For MongoDB, use `createSchemaGeneratorAsync` from `./schemaGeneratorAsync.js` so the optional `mongodb` peer is not loaded at import time.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import { getMeta } from '../entity/index.js';
|
|
3
3
|
import { areTypesEqual, canonicalToSql, fieldOptionsToCanonical, isVectorCategory, sqlToCanonical, } from '../schema/canonicalType.js';
|
|
4
|
-
import {
|
|
4
|
+
import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
|
|
5
5
|
import { escapeSqlId, getKeys, isAutoIncrement } from '../util/index.js';
|
|
6
6
|
import { formatDefaultValue } from './builder/expressions.js';
|
|
7
|
+
import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
|
|
7
8
|
import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
|
|
8
9
|
/**
|
|
9
10
|
* Unified SQL schema generator.
|
|
@@ -90,7 +91,7 @@ export class SqlSchemaGenerator {
|
|
|
90
91
|
* resolves instead of being silently dropped.
|
|
91
92
|
*/
|
|
92
93
|
orderedTables(entities, direction, only) {
|
|
93
|
-
const ast =
|
|
94
|
+
const ast = buildEntityAST(this, entities, this.defaultForeignKeyAction);
|
|
94
95
|
const tables = direction === 'create' ? ast.getCreateOrder() : ast.getDropOrder();
|
|
95
96
|
if (!only) {
|
|
96
97
|
return tables;
|
|
@@ -331,7 +332,11 @@ export class SqlSchemaGenerator {
|
|
|
331
332
|
for (const [name] of currentColumns) {
|
|
332
333
|
columnsToDrop.push(name);
|
|
333
334
|
}
|
|
334
|
-
|
|
335
|
+
const indexesToAdd = this.missingIndexes(entity, currentTable);
|
|
336
|
+
if (columnsToAdd.length === 0 &&
|
|
337
|
+
columnsToAlter.length === 0 &&
|
|
338
|
+
columnsToDrop.length === 0 &&
|
|
339
|
+
indexesToAdd.length === 0) {
|
|
335
340
|
return undefined;
|
|
336
341
|
}
|
|
337
342
|
return {
|
|
@@ -340,8 +345,32 @@ export class SqlSchemaGenerator {
|
|
|
340
345
|
columnsToAdd: columnsToAdd.length > 0 ? columnsToAdd : undefined,
|
|
341
346
|
columnsToAlter: columnsToAlter.length > 0 ? columnsToAlter : undefined,
|
|
342
347
|
columnsToDrop: columnsToDrop.length > 0 ? columnsToDrop : undefined,
|
|
348
|
+
indexesToAdd: indexesToAdd.length > 0 ? indexesToAdd : undefined,
|
|
343
349
|
};
|
|
344
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Indexes the entity declares that the table does not have, matched by name and built the same way
|
|
353
|
+
* `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
|
|
354
|
+
* up rather than waiting for the table to be created from scratch somewhere else.
|
|
355
|
+
*
|
|
356
|
+
* Only ever additive. An index the entity does not name is left alone: it may well have been
|
|
357
|
+
* created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
|
|
358
|
+
*/
|
|
359
|
+
missingIndexes(entity, currentTable) {
|
|
360
|
+
const desired = buildEntityAST(this, [entity]).getTable(currentTable.name)?.indexes ?? [];
|
|
361
|
+
const present = new Set(currentTable.indexes.map((index) => index.name));
|
|
362
|
+
return desired
|
|
363
|
+
.filter((index) => !present.has(index.name) && !this.isInlineVectorIndex(index))
|
|
364
|
+
.map(indexNodeToSchema);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* A vector index this dialect declares inside `CREATE TABLE` rather than as a statement of its own,
|
|
368
|
+
* which MariaDB is alone in doing. It has no `CREATE INDEX` form, so it can only ever be created
|
|
369
|
+
* with its table, never added to one.
|
|
370
|
+
*/
|
|
371
|
+
isInlineVectorIndex(index) {
|
|
372
|
+
return this.features.inlineVectorIndex && index.type === 'vector';
|
|
373
|
+
}
|
|
345
374
|
columnNodeToSchema(col) {
|
|
346
375
|
return {
|
|
347
376
|
name: col.name,
|
|
@@ -430,12 +459,11 @@ export class SqlSchemaGenerator {
|
|
|
430
459
|
generateCreateTableFromNode(table, options = {}) {
|
|
431
460
|
const columns = [];
|
|
432
461
|
const constraints = [];
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const regularIndexes = isInlineVectorIdx ? table.indexes.filter((idx) => idx.type !== 'vector') : table.indexes;
|
|
462
|
+
const vectorIndexes = table.indexes.filter((index) => this.isInlineVectorIndex(index));
|
|
463
|
+
const regularIndexes = table.indexes.filter((index) => !this.isInlineVectorIndex(index));
|
|
436
464
|
// MariaDB rejects a `VECTOR INDEX` whose column is nullable ("All parts of a VECTOR index must
|
|
437
465
|
// be NOT NULL"), so being indexed decides it rather than the entity's own nullability.
|
|
438
|
-
const indexedVectorColumns = new Set(vectorIndexes.flatMap((idx) => idx.
|
|
466
|
+
const indexedVectorColumns = new Set(vectorIndexes.flatMap((idx) => idx.entries.map((entry) => entry.column)));
|
|
439
467
|
for (const col of table.columns.values()) {
|
|
440
468
|
const colDef = this.generateColumnFromNode(indexedVectorColumns.has(col.name) ? { ...col, nullable: false } : col);
|
|
441
469
|
columns.push(colDef);
|
|
@@ -503,7 +531,7 @@ export class SqlSchemaGenerator {
|
|
|
503
531
|
return this.generateCreateIndex(index.table.name, indexNodeToSchema(index), options);
|
|
504
532
|
}
|
|
505
533
|
generateCreateTableFromDefinition(table, options = {}) {
|
|
506
|
-
const tableNode =
|
|
534
|
+
const tableNode = tableDefinitionToNode(table);
|
|
507
535
|
return this.generateCreateTableFromNode(tableNode, options);
|
|
508
536
|
}
|
|
509
537
|
generateRenameTableSql(oldName, newName) {
|
|
@@ -513,11 +541,11 @@ export class SqlSchemaGenerator {
|
|
|
513
541
|
return `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)};`;
|
|
514
542
|
}
|
|
515
543
|
generateAddColumnSql(tableName, column) {
|
|
516
|
-
const colSql = this.generateColumnFromNode(
|
|
544
|
+
const colSql = this.generateColumnFromNode(fullColumnDefinitionToNode(column, tableName));
|
|
517
545
|
return `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql};`;
|
|
518
546
|
}
|
|
519
547
|
generateAlterColumnSql(tableName, columnName, column) {
|
|
520
|
-
const node =
|
|
548
|
+
const node = fullColumnDefinitionToNode(column, tableName);
|
|
521
549
|
return this.generateAlterColumnStatements(tableName, { ...this.columnNodeToSchema(node), name: columnName }, this.generateColumnFromNode(node)).join('\n');
|
|
522
550
|
}
|
|
523
551
|
generateDropColumnSql(tableName, columnName) {
|
|
@@ -542,88 +570,21 @@ export class SqlSchemaGenerator {
|
|
|
542
570
|
generateDropForeignKeySql(tableName, constraintName) {
|
|
543
571
|
return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
|
|
544
572
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
node.table = table;
|
|
561
|
-
columns.set(node.name, node);
|
|
562
|
-
if (node.isPrimaryKey) {
|
|
563
|
-
pkNodes.push(node);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
const finalPrimaryKey = def.primaryKey
|
|
567
|
-
? def.primaryKey.map((name) => columns.get(name)).filter((c) => c !== undefined)
|
|
568
|
-
: pkNodes;
|
|
569
|
-
table.primaryKey = finalPrimaryKey;
|
|
570
|
-
for (const idxDef of def.indexes) {
|
|
571
|
-
const indexNode = {
|
|
572
|
-
...idxDef,
|
|
573
|
-
table,
|
|
574
|
-
columns: idxDef.columns
|
|
575
|
-
.map((entry) => (entry.expression ? undefined : columns.get(entry.column)))
|
|
576
|
-
.filter((c) => c !== undefined),
|
|
577
|
-
entries: idxDef.columns,
|
|
578
|
-
};
|
|
579
|
-
table.indexes.push(indexNode);
|
|
580
|
-
}
|
|
581
|
-
for (const fkDef of def.foreignKeys) {
|
|
582
|
-
const relNode = {
|
|
583
|
-
name: fkDef.name ?? `fk_${def.name}_${fkDef.columns.join('_')}`,
|
|
584
|
-
type: 'ManyToOne', // Builder default
|
|
585
|
-
from: {
|
|
586
|
-
table,
|
|
587
|
-
columns: fkDef.columns.map((name) => columns.get(name)).filter((c) => c !== undefined),
|
|
588
|
-
},
|
|
589
|
-
to: {
|
|
590
|
-
table: { name: fkDef.referencesTable },
|
|
591
|
-
columns: fkDef.referencesColumns.map((name) => ({ name })),
|
|
592
|
-
},
|
|
593
|
-
onDelete: fkDef.onDelete,
|
|
594
|
-
onUpdate: fkDef.onUpdate,
|
|
595
|
-
};
|
|
596
|
-
table.outgoingRelations.push(relNode);
|
|
597
|
-
}
|
|
598
|
-
return table;
|
|
599
|
-
}
|
|
600
|
-
fullColumnDefinitionToNode(col, tableName) {
|
|
601
|
-
return {
|
|
602
|
-
name: col.name,
|
|
603
|
-
type: col.type,
|
|
604
|
-
nullable: col.nullable,
|
|
605
|
-
defaultValue: col.defaultValue,
|
|
606
|
-
isPrimaryKey: col.primaryKey,
|
|
607
|
-
isAutoIncrement: col.autoIncrement,
|
|
608
|
-
isUnique: col.unique,
|
|
609
|
-
comment: col.comment,
|
|
610
|
-
table: { name: tableName },
|
|
611
|
-
referencedBy: [],
|
|
612
|
-
references: col.foreignKey
|
|
613
|
-
? {
|
|
614
|
-
name: `fk_${tableName}_${col.name}`,
|
|
615
|
-
type: 'ManyToOne',
|
|
616
|
-
from: { table: { name: tableName }, columns: [] },
|
|
617
|
-
to: {
|
|
618
|
-
table: { name: col.foreignKey.table },
|
|
619
|
-
columns: col.foreignKey.columns.map((name) => ({ name })),
|
|
620
|
-
},
|
|
621
|
-
onDelete: col.foreignKey.onDelete,
|
|
622
|
-
onUpdate: col.foreignKey.onUpdate,
|
|
623
|
-
}
|
|
624
|
-
: undefined,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* The entities as an AST, named the way `generator` names things.
|
|
576
|
+
*
|
|
577
|
+
* Its resolvers rather than a naming strategy, because the two disagree: a strategy renames whatever
|
|
578
|
+
* it is handed, while a generator leaves an explicit `@Entity({ name })` alone. Build the AST the
|
|
579
|
+
* other way and the table is created under one name and compared under another, which reports every
|
|
580
|
+
* table of a project using a naming strategy as both missing and unexpected.
|
|
581
|
+
*/
|
|
582
|
+
export function buildEntityAST(generator, entities, defaultForeignKeyAction) {
|
|
583
|
+
return buildSchemaAST(entities, {
|
|
584
|
+
resolveTableName: (entity, meta) => generator.resolveTableName(entity, meta),
|
|
585
|
+
resolveColumnName: (key, field) => generator.resolveColumnName(key, field),
|
|
586
|
+
defaultForeignKeyAction,
|
|
587
|
+
});
|
|
627
588
|
}
|
|
628
589
|
/**
|
|
629
590
|
* Synchronous factory for SQL schema generators only.
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export { areTypesEqual, canonicalToColumnType, canonicalToSql, canonicalToTypeSc
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function introspectSchema(introspector: SchemaIntrospector): Promise<SchemaAST>;
|
|
20
20
|
export { SchemaAST } from './schemaAST.js';
|
|
21
|
-
export type {
|
|
22
|
-
export {
|
|
21
|
+
export type { BuildSchemaASTOptions } from './schemaASTBuilder.js';
|
|
22
|
+
export { buildSchemaAST } from './schemaASTBuilder.js';
|
|
23
23
|
export type { DiffOptions } from './schemaASTDiffer.js';
|
|
24
|
-
export { diffSchemas
|
|
24
|
+
export { diffSchemas } from './schemaASTDiffer.js';
|
|
25
25
|
export type { CanonicalType, ColumnDiff, ColumnNode, Drift, DriftReport, DriftSeverity, DriftStatus, DriftType, ForeignKeyAction, IndexDiff, IndexNode, IndexSource, IndexSyncStatus, IndexType, RelationshipDiff, RelationshipNode, RelationshipSource, RelationshipType, SchemaAST as ISchemaAST, SchemaDiffResult, SizeVariant, TableDiff, TableNode, TypeCategory, ValidationError, ValidationErrorType, } from './types.js';
|