uql-orm 0.25.0 → 0.26.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 +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/bin.js +0 -0
- 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 +32 -27
- 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 -233
- 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 +12 -10
- package/dist/type/migration.d.ts +14 -1
- package/dist/util/string.util.js +6 -1
- package/package.json +3 -4
- package/LICENSE.md +0 -22
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isVectorCategory } from '../../schema/canonicalType.js';
|
|
2
|
+
import { indexColumns } from '../../schema/indexColumns.js';
|
|
2
3
|
/**
|
|
3
4
|
* An AST index as the generators and dialects want it. Spread rather than copied field by field:
|
|
4
5
|
* rebuilding it by hand is how the partial-index `where` once vanished without a trace. The node-only
|
|
@@ -8,7 +9,8 @@ import { isVectorCategory } from '../../schema/canonicalType.js';
|
|
|
8
9
|
export function indexNodeToSchema(index) {
|
|
9
10
|
return {
|
|
10
11
|
...index,
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
vectorType: indexColumns(index)
|
|
13
|
+
.map((col) => col.type?.category)
|
|
14
|
+
.find(isVectorCategory),
|
|
13
15
|
};
|
|
14
16
|
}
|
|
@@ -42,7 +42,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
42
42
|
const indexName = typeof field.index === 'string' ? field.index : `idx_${collectionName}_${columnName}`;
|
|
43
43
|
indexes.push({
|
|
44
44
|
name: indexName,
|
|
45
|
-
|
|
45
|
+
entries: [{ column: columnName }],
|
|
46
46
|
unique: !!field.unique,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
@@ -85,7 +85,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
85
85
|
*/
|
|
86
86
|
generateCreateIndex(tableName, index) {
|
|
87
87
|
const key = {};
|
|
88
|
-
for (const entry of index.
|
|
88
|
+
for (const entry of index.entries) {
|
|
89
89
|
if (entry.expression || entry.length !== undefined || entry.nulls || entry.opsClass) {
|
|
90
90
|
throw new TypeError(`mongodb does not support that index column option (index "${index.name}")`);
|
|
91
91
|
}
|
|
@@ -146,7 +146,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
146
146
|
if (!existingIndexes.has(indexName)) {
|
|
147
147
|
indexesToAdd.push({
|
|
148
148
|
name: indexName,
|
|
149
|
-
|
|
149
|
+
entries: [{ column: columnName }],
|
|
150
150
|
unique: !!field.unique,
|
|
151
151
|
});
|
|
152
152
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../../dialect/index.js';
|
|
2
|
+
import type { IndexFacet } from '../../schema/indexDifferences.js';
|
|
2
3
|
import { SchemaAST } from '../../schema/schemaAST.js';
|
|
3
4
|
import type { TableSchema } from '../../type/migration.js';
|
|
4
5
|
/**
|
|
@@ -6,6 +7,8 @@ import type { TableSchema } from '../../type/migration.js';
|
|
|
6
7
|
*/
|
|
7
8
|
export declare abstract class BaseSqlIntrospector {
|
|
8
9
|
protected readonly dialect: AbstractSqlDialect;
|
|
10
|
+
/** Columns and uniqueness only; each introspector opts in to what its catalogue queries report. */
|
|
11
|
+
readonly indexFacets: ReadonlySet<IndexFacet>;
|
|
9
12
|
constructor(dialect: AbstractSqlDialect);
|
|
10
13
|
protected escapeId(identifier: string): string;
|
|
11
14
|
/**
|
|
@@ -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.
|