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
|
@@ -10,256 +10,231 @@ import { fieldOptionsToCanonical } from './canonicalType.js';
|
|
|
10
10
|
import { SchemaAST } from './schemaAST.js';
|
|
11
11
|
import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
|
|
14
|
+
*
|
|
15
|
+
* Three passes, because each needs the one before it to have finished for *every* entity: a relation
|
|
16
|
+
* resolves against a table another entity declares, and an index against the columns of its own.
|
|
14
17
|
*/
|
|
15
|
-
export
|
|
16
|
-
namingStrategy;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Reset the builder for a new schema.
|
|
26
|
-
*/
|
|
27
|
-
reset() {
|
|
28
|
-
this.ast = new SchemaAST();
|
|
29
|
-
return this;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Get the built AST.
|
|
33
|
-
*/
|
|
34
|
-
getAST() {
|
|
35
|
-
return this.ast;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Build AST from entity classes (decorated with @Entity, @Field, etc.)
|
|
39
|
-
*/
|
|
40
|
-
fromEntities(entities, options = {}) {
|
|
41
|
-
this.reset();
|
|
42
|
-
const namingStrategy = options.namingStrategy ?? this.namingStrategy;
|
|
43
|
-
const resolveTableName = options.resolveTableName ?? ((e, m) => namingStrategy?.tableName(m.name ?? e.name) ?? m.name ?? e.name);
|
|
44
|
-
const resolveColumnName = options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k);
|
|
45
|
-
// First pass: create all tables and columns
|
|
46
|
-
for (const entity of entities) {
|
|
47
|
-
const meta = getMeta(entity);
|
|
48
|
-
this.addTableFromEntity(entity, meta, resolveTableName, resolveColumnName);
|
|
49
|
-
}
|
|
50
|
-
// Second pass: create relationships from relation decorators
|
|
51
|
-
for (const entity of entities) {
|
|
52
|
-
const meta = getMeta(entity);
|
|
53
|
-
this.addRelationshipsFromEntity(entity, meta, resolveTableName, resolveColumnName, options);
|
|
54
|
-
}
|
|
55
|
-
// Third pass: create indexes from field options
|
|
18
|
+
export function buildSchemaAST(entities, options = {}) {
|
|
19
|
+
const { namingStrategy } = options;
|
|
20
|
+
const ctx = {
|
|
21
|
+
ast: new SchemaAST(),
|
|
22
|
+
resolveTableName: options.resolveTableName ?? ((e, m) => namingStrategy?.tableName(m.name ?? e.name) ?? m.name ?? e.name),
|
|
23
|
+
resolveColumnName: options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k),
|
|
24
|
+
defaultForeignKeyAction: options.defaultForeignKeyAction ?? DEFAULT_FOREIGN_KEY_ACTION,
|
|
25
|
+
};
|
|
26
|
+
for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
|
|
56
27
|
for (const entity of entities) {
|
|
57
|
-
|
|
58
|
-
this.addIndexesFromEntity(entity, meta, resolveTableName, resolveColumnName);
|
|
28
|
+
pass(ctx, entity, getMeta(entity));
|
|
59
29
|
}
|
|
60
|
-
return this.ast;
|
|
61
30
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
31
|
+
return ctx.ast;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the canonical type for a field, inheriting from the referenced
|
|
35
|
+
* entity's primary key when the field is a foreign-key reference
|
|
36
|
+
* (`@Field({ references: () => SomeEntity })`) with no explicit type of its
|
|
37
|
+
* own.
|
|
38
|
+
*
|
|
39
|
+
* Without this, a field like `creatorId?: UUID` (a bare TypeScript alias for
|
|
40
|
+
* `string`, erased at runtime) falls back to the generic string inference in
|
|
41
|
+
* {@link fieldOptionsToCanonical} and gets typed as TEXT/VARCHAR - producing a
|
|
42
|
+
* foreign key column whose type doesn't match the UUID primary key it
|
|
43
|
+
* references, which Postgres (and most databases) reject outright.
|
|
44
|
+
*
|
|
45
|
+
* `field.typeFromReference` (set by `defineField`, see entity/metadata/definition.ts)
|
|
46
|
+
* is what distinguishes "no type was given" from "the decorator explicitly set
|
|
47
|
+
* a type" - including explicit constructor overrides like `type: BigInt`, which
|
|
48
|
+
* a value-based check (e.g. `typeof field.type === 'string'`) would miss since
|
|
49
|
+
* reflection also produces constructor values like `String`/`Number`.
|
|
50
|
+
* `columnType` remains the unambiguous, always-respected explicit override.
|
|
51
|
+
*/
|
|
52
|
+
function resolveColumnCanonicalType(field, seen = new Set()) {
|
|
53
|
+
const hasExplicitType = !!field.columnType || !field.typeFromReference;
|
|
54
|
+
if (!hasExplicitType && field.references && !seen.has(field.references)) {
|
|
55
|
+
seen.add(field.references);
|
|
56
|
+
const referencedMeta = getMeta(field.references());
|
|
57
|
+
const referencedIdField = referencedMeta.fields[referencedMeta.id];
|
|
58
|
+
if (referencedIdField) {
|
|
59
|
+
return resolveColumnCanonicalType(referencedIdField, seen);
|
|
90
60
|
}
|
|
91
|
-
return fieldOptionsToCanonical(field, field.type);
|
|
92
61
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
62
|
+
return fieldOptionsToCanonical(field, field.type);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Add a table from entity metadata.
|
|
66
|
+
*/
|
|
67
|
+
function addTableFromEntity(ctx, entity, meta) {
|
|
68
|
+
const tableName = ctx.resolveTableName(entity, meta);
|
|
69
|
+
const columns = new Map();
|
|
70
|
+
const primaryKey = [];
|
|
71
|
+
// Create placeholder table (will be fully initialized below)
|
|
72
|
+
const table = {
|
|
73
|
+
name: tableName,
|
|
74
|
+
columns,
|
|
75
|
+
primaryKey,
|
|
76
|
+
indexes: [],
|
|
77
|
+
schema: ctx.ast,
|
|
78
|
+
incomingRelations: [],
|
|
79
|
+
outgoingRelations: [],
|
|
80
|
+
};
|
|
81
|
+
// Add columns from fields
|
|
82
|
+
const fields = meta.fields;
|
|
83
|
+
for (const key of Object.keys(fields)) {
|
|
84
|
+
const field = fields[key];
|
|
85
|
+
if (!field)
|
|
86
|
+
continue;
|
|
87
|
+
// Skip virtual fields
|
|
88
|
+
if (field.virtual)
|
|
89
|
+
continue;
|
|
90
|
+
const columnName = ctx.resolveColumnName(key, field);
|
|
91
|
+
const type = resolveColumnCanonicalType(field);
|
|
92
|
+
const isPrimaryKey = key === meta.id;
|
|
93
|
+
const column = {
|
|
94
|
+
name: columnName,
|
|
95
|
+
type,
|
|
96
|
+
// A primary key is NOT NULL in every engine, whatever the entity's property says: `id?: number`
|
|
97
|
+
// is optional because the database assigns it, not because the column accepts a null.
|
|
98
|
+
nullable: isPrimaryKey ? false : (field.nullable ?? true),
|
|
99
|
+
defaultValue: field.defaultValue,
|
|
100
|
+
isPrimaryKey,
|
|
101
|
+
isAutoIncrement: field.autoIncrement ?? (isPrimaryKey && type.category === 'integer'),
|
|
102
|
+
isUnique: field.unique ?? false,
|
|
103
|
+
comment: field.comment,
|
|
104
|
+
table,
|
|
105
|
+
referencedBy: [],
|
|
106
|
+
references: undefined,
|
|
109
107
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const field = fields[key];
|
|
114
|
-
if (!field)
|
|
115
|
-
continue;
|
|
116
|
-
// Skip virtual fields
|
|
117
|
-
if (field.virtual)
|
|
118
|
-
continue;
|
|
119
|
-
const columnName = resolveColumnName(key, field);
|
|
120
|
-
const type = this.resolveColumnCanonicalType(field);
|
|
121
|
-
const column = {
|
|
122
|
-
name: columnName,
|
|
123
|
-
type,
|
|
124
|
-
nullable: field.nullable ?? true,
|
|
125
|
-
defaultValue: field.defaultValue,
|
|
126
|
-
isPrimaryKey: key === meta.id,
|
|
127
|
-
isAutoIncrement: field.autoIncrement ?? (key === meta.id && type.category === 'integer'),
|
|
128
|
-
isUnique: field.unique ?? false,
|
|
129
|
-
comment: field.comment,
|
|
130
|
-
table,
|
|
131
|
-
referencedBy: [],
|
|
132
|
-
references: undefined,
|
|
133
|
-
};
|
|
134
|
-
columns.set(columnName, column);
|
|
135
|
-
if (key === meta.id) {
|
|
136
|
-
primaryKey.push(column);
|
|
137
|
-
}
|
|
108
|
+
columns.set(columnName, column);
|
|
109
|
+
if (key === meta.id) {
|
|
110
|
+
primaryKey.push(column);
|
|
138
111
|
}
|
|
139
|
-
this.ast.addTable(table);
|
|
140
112
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
113
|
+
ctx.ast.addTable(table);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Add relationships from entity relation decorators.
|
|
117
|
+
*/
|
|
118
|
+
function addRelationshipsFromEntity(ctx, entity, meta) {
|
|
119
|
+
const tableName = ctx.resolveTableName(entity, meta);
|
|
120
|
+
const table = ctx.ast.getTable(tableName);
|
|
121
|
+
if (!table)
|
|
122
|
+
return;
|
|
123
|
+
const relations = meta.relations;
|
|
124
|
+
for (const key of Object.keys(relations)) {
|
|
125
|
+
const relation = relations[key];
|
|
126
|
+
if (!relation)
|
|
127
|
+
continue;
|
|
128
|
+
const relatedEntity = relation.entity();
|
|
129
|
+
const relatedMeta = getMeta(relatedEntity);
|
|
130
|
+
const relatedTableName = ctx.resolveTableName(relatedEntity, relatedMeta);
|
|
131
|
+
const relatedTable = ctx.ast.getTable(relatedTableName);
|
|
132
|
+
if (!relatedTable)
|
|
133
|
+
continue;
|
|
134
|
+
// Only the owning side gets the FK. `mappedBy` marks the inverse side of a one-to-one, whose
|
|
135
|
+
// `references` describe how to join back (its own primary key against the owner's FK column) -
|
|
136
|
+
// reading those as a foreign key emitted a reversed constraint (`User(id) REFERENCES
|
|
137
|
+
// user_profile(creatorId)`), which SQLite rejects outright as a foreign key mismatch.
|
|
138
|
+
const ownsForeignKey = relation.cardinality === 'm1' || (relation.cardinality === '11' && !relation.mappedBy);
|
|
139
|
+
if (ownsForeignKey) {
|
|
140
|
+
const localPropName = relation.references[0].local;
|
|
141
|
+
const foreignPropName = relation.references[0].foreign;
|
|
142
|
+
const localField = meta.fields[localPropName];
|
|
143
|
+
if (!localField)
|
|
153
144
|
continue;
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const relatedTable = this.ast.getTable(relatedTableName);
|
|
158
|
-
if (!relatedTable)
|
|
145
|
+
const localColName = ctx.resolveColumnName(localPropName, localField);
|
|
146
|
+
const foreignField = relatedMeta.fields[foreignPropName];
|
|
147
|
+
if (!foreignField)
|
|
159
148
|
continue;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const foreignColumn = relatedTable.columns.get(foreignColName);
|
|
178
|
-
if (localColumn && foreignColumn) {
|
|
179
|
-
const relNode = {
|
|
180
|
-
name: `fk_${tableName}_${localColName}`,
|
|
181
|
-
type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
|
|
182
|
-
from: { table, columns: [localColumn] },
|
|
183
|
-
to: { table: relatedTable, columns: [foreignColumn] },
|
|
184
|
-
// The relation's own action wins over the global default, so one relation can cascade in the
|
|
185
|
-
// database while the rest stay on `NO ACTION`.
|
|
186
|
-
onDelete: relation.onDelete ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
|
|
187
|
-
onUpdate: relation.onUpdate ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
|
|
188
|
-
confidence: 1.0,
|
|
189
|
-
inferredFrom: 'entity_decorator',
|
|
190
|
-
};
|
|
191
|
-
this.ast.addRelationship(relNode);
|
|
192
|
-
}
|
|
149
|
+
const foreignColName = ctx.resolveColumnName(foreignPropName, foreignField);
|
|
150
|
+
const localColumn = table.columns.get(localColName);
|
|
151
|
+
const foreignColumn = relatedTable.columns.get(foreignColName);
|
|
152
|
+
if (localColumn && foreignColumn) {
|
|
153
|
+
const relNode = {
|
|
154
|
+
name: `fk_${tableName}_${localColName}`,
|
|
155
|
+
type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
|
|
156
|
+
from: { table, columns: [localColumn] },
|
|
157
|
+
to: { table: relatedTable, columns: [foreignColumn] },
|
|
158
|
+
// Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
|
|
159
|
+
// references, onDelete })` work with no relation declared at all.
|
|
160
|
+
onDelete: relation.onDelete ?? localField.onDelete ?? ctx.defaultForeignKeyAction,
|
|
161
|
+
onUpdate: relation.onUpdate ?? ctx.defaultForeignKeyAction,
|
|
162
|
+
confidence: 1.0,
|
|
163
|
+
inferredFrom: 'entity_decorator',
|
|
164
|
+
};
|
|
165
|
+
ctx.ast.addRelationship(relNode);
|
|
193
166
|
}
|
|
194
167
|
}
|
|
195
168
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
columns: [column],
|
|
216
|
-
unique: field.unique ?? false,
|
|
217
|
-
source: 'entity',
|
|
218
|
-
syncStatus: 'entity_only',
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
for (const idxMeta of meta.indexes ?? []) {
|
|
222
|
-
this.addCompositeIndex(table, meta, idxMeta, resolveColumnName);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
|
|
227
|
-
* names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
|
|
228
|
-
* subset, which is what diffing and introspection compare.
|
|
229
|
-
*/
|
|
230
|
-
addCompositeIndex(table, meta, idxMeta, resolveColumnName) {
|
|
231
|
-
// An entry survives if it is an expression (nothing to resolve) or names a column that exists;
|
|
232
|
-
// an index left with none is dropped, the same as one naming only unknown columns always was.
|
|
233
|
-
const entries = idxMeta.columns
|
|
234
|
-
.map((entry) => {
|
|
235
|
-
if (entry.expression)
|
|
236
|
-
return entry;
|
|
237
|
-
const field = meta.fields[entry.column];
|
|
238
|
-
const column = field && resolveColumnName(entry.column, field);
|
|
239
|
-
return column && table.columns.has(column) ? { ...entry, column } : undefined;
|
|
240
|
-
})
|
|
241
|
-
.filter((entry) => entry !== undefined);
|
|
242
|
-
if (!entries.length)
|
|
243
|
-
return;
|
|
244
|
-
const columns = entries
|
|
245
|
-
.map((entry) => (entry.expression ? undefined : table.columns.get(entry.column)))
|
|
246
|
-
.filter((column) => column !== undefined);
|
|
247
|
-
const named = columns.length ? columns.map((column) => column.name) : entries.map((_, at) => `expr${at}`);
|
|
248
|
-
this.ast.addIndex({
|
|
249
|
-
name: idxMeta.name ?? `idx_${table.name}_${named.join('_')}`,
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Add indexes from field options (`@Field({ index })`) and from `@Index([...])`, which have nothing
|
|
172
|
+
* in common beyond their target table.
|
|
173
|
+
*/
|
|
174
|
+
function addIndexesFromEntity(ctx, entity, meta) {
|
|
175
|
+
const tableName = ctx.resolveTableName(entity, meta);
|
|
176
|
+
const table = ctx.ast.getTable(tableName);
|
|
177
|
+
if (!table)
|
|
178
|
+
return;
|
|
179
|
+
for (const key of Object.keys(meta.fields)) {
|
|
180
|
+
const field = meta.fields[key];
|
|
181
|
+
if (!field?.index)
|
|
182
|
+
continue;
|
|
183
|
+
const column = table.columns.get(ctx.resolveColumnName(key, field));
|
|
184
|
+
if (!column)
|
|
185
|
+
continue;
|
|
186
|
+
ctx.ast.addIndex({
|
|
187
|
+
name: typeof field.index === 'string' ? field.index : `idx_${tableName}_${column.name}`,
|
|
250
188
|
table,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
include: idxMeta.include,
|
|
254
|
-
unique: idxMeta.unique ?? false,
|
|
255
|
-
type: idxMeta.type,
|
|
256
|
-
where: idxMeta.where,
|
|
257
|
-
distance: idxMeta.distance,
|
|
258
|
-
m: idxMeta.m,
|
|
259
|
-
efConstruction: idxMeta.efConstruction,
|
|
260
|
-
lists: idxMeta.lists,
|
|
189
|
+
entries: [{ column: column.name }],
|
|
190
|
+
unique: field.unique ?? false,
|
|
261
191
|
source: 'entity',
|
|
262
192
|
syncStatus: 'entity_only',
|
|
263
193
|
});
|
|
264
194
|
}
|
|
195
|
+
for (const idxMeta of meta.indexes ?? []) {
|
|
196
|
+
addCompositeIndex(ctx, table, meta, idxMeta);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
|
|
201
|
+
* names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
|
|
202
|
+
* subset, which is what diffing and introspection compare.
|
|
203
|
+
*/
|
|
204
|
+
/** An `include` column is named like any other, so a naming strategy has to reach it too. */
|
|
205
|
+
function resolveIncludeColumn(ctx, meta, column) {
|
|
206
|
+
const field = meta.fields[column];
|
|
207
|
+
return field ? ctx.resolveColumnName(column, field) : column;
|
|
208
|
+
}
|
|
209
|
+
function addCompositeIndex(ctx, table, meta, idxMeta) {
|
|
210
|
+
// An entry survives if it is an expression (nothing to resolve) or names a column that exists;
|
|
211
|
+
// an index left with none is dropped, the same as one naming only unknown columns always was.
|
|
212
|
+
const entries = idxMeta.columns
|
|
213
|
+
.map((entry) => {
|
|
214
|
+
if (entry.expression)
|
|
215
|
+
return entry;
|
|
216
|
+
const field = meta.fields[entry.column];
|
|
217
|
+
const column = field && ctx.resolveColumnName(entry.column, field);
|
|
218
|
+
return column && table.columns.has(column) ? { ...entry, column } : undefined;
|
|
219
|
+
})
|
|
220
|
+
.filter((entry) => entry !== undefined);
|
|
221
|
+
if (!entries.length)
|
|
222
|
+
return;
|
|
223
|
+
// An index over expressions alone has no column names to build a default name from.
|
|
224
|
+
const named = entries.map((entry, at) => (entry.expression ? `expr${at}` : entry.column));
|
|
225
|
+
ctx.ast.addIndex({
|
|
226
|
+
name: idxMeta.name ?? `idx_${table.name}_${named.join('_')}`,
|
|
227
|
+
table,
|
|
228
|
+
entries,
|
|
229
|
+
include: idxMeta.include?.map((column) => resolveIncludeColumn(ctx, meta, column)),
|
|
230
|
+
unique: idxMeta.unique ?? false,
|
|
231
|
+
type: idxMeta.type,
|
|
232
|
+
where: idxMeta.where,
|
|
233
|
+
distance: idxMeta.distance,
|
|
234
|
+
m: idxMeta.m,
|
|
235
|
+
efConstruction: idxMeta.efConstruction,
|
|
236
|
+
lists: idxMeta.lists,
|
|
237
|
+
source: 'entity',
|
|
238
|
+
syncStatus: 'entity_only',
|
|
239
|
+
});
|
|
265
240
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Drift detection (expected vs actual)
|
|
8
8
|
* - Schema synchronization
|
|
9
9
|
*/
|
|
10
|
+
import { type IndexFacet } from './indexDifferences.js';
|
|
10
11
|
import type { SchemaAST } from './schemaAST.js';
|
|
11
12
|
import type { SchemaDiffResult } from './types.js';
|
|
12
13
|
/**
|
|
@@ -15,6 +16,8 @@ import type { SchemaDiffResult } from './types.js';
|
|
|
15
16
|
export interface DiffOptions {
|
|
16
17
|
/** Compare indexes */
|
|
17
18
|
compareIndexes?: boolean;
|
|
19
|
+
/** What the target side can report about an index, normally an introspector's `indexFacets`. Anything left out is not compared. */
|
|
20
|
+
indexFacets?: ReadonlySet<IndexFacet>;
|
|
18
21
|
/** Compare foreign keys/relationships */
|
|
19
22
|
compareRelationships?: boolean;
|
|
20
23
|
/** Ignore case differences in names */
|
|
@@ -23,61 +26,11 @@ export interface DiffOptions {
|
|
|
23
26
|
excludeTables?: string[];
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* @param source - The "expected" or "desired" schema (e.g., from entities)
|
|
33
|
-
* @param target - The "actual" or "current" schema (e.g., from database)
|
|
34
|
-
* @param options - Diff options
|
|
35
|
-
* @returns Detailed diff result
|
|
36
|
-
*/
|
|
37
|
-
diff(source: SchemaAST, target: SchemaAST, options?: DiffOptions): SchemaDiffResult;
|
|
38
|
-
/**
|
|
39
|
-
* Compare two tables and return the differences.
|
|
40
|
-
*/
|
|
41
|
-
private diffTable;
|
|
42
|
-
/**
|
|
43
|
-
* Compare columns between two tables.
|
|
44
|
-
*/
|
|
45
|
-
private diffTableColumns;
|
|
46
|
-
/**
|
|
47
|
-
* Compare indexes between two tables.
|
|
48
|
-
*/
|
|
49
|
-
private diffTableIndexes;
|
|
50
|
-
/**
|
|
51
|
-
* Compare two columns and return the difference.
|
|
52
|
-
*/
|
|
53
|
-
private diffColumn;
|
|
54
|
-
/**
|
|
55
|
-
* Compare two indexes and return the difference.
|
|
56
|
-
*/
|
|
57
|
-
private diffIndex;
|
|
58
|
-
/**
|
|
59
|
-
* Compare relationships at the schema level.
|
|
60
|
-
*/
|
|
61
|
-
private diffRelationships;
|
|
62
|
-
/**
|
|
63
|
-
* Compare two relationships.
|
|
64
|
-
*/
|
|
65
|
-
private diffRelationship;
|
|
66
|
-
/**
|
|
67
|
-
* Generate a unique key for a relationship based on its structure.
|
|
68
|
-
*/
|
|
69
|
-
private getRelationshipKey;
|
|
70
|
-
/**
|
|
71
|
-
* Format a canonical type for display.
|
|
72
|
-
*/
|
|
73
|
-
private formatType;
|
|
74
|
-
/**
|
|
75
|
-
* Normalize default values for comparison.
|
|
76
|
-
*/
|
|
77
|
-
private normalizeDefault;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Create a differ and run a comparison.
|
|
81
|
-
* Convenience function for one-off comparisons.
|
|
29
|
+
* Compare two schemas and return the differences.
|
|
30
|
+
*
|
|
31
|
+
* @param source - The "expected" or "desired" schema (e.g., from entities)
|
|
32
|
+
* @param target - The "actual" or "current" schema (e.g., from database)
|
|
33
|
+
* @param options - Diff options
|
|
34
|
+
* @returns Detailed diff result
|
|
82
35
|
*/
|
|
83
36
|
export declare function diffSchemas(source: SchemaAST, target: SchemaAST, options?: DiffOptions): SchemaDiffResult;
|