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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* actual database schema.
|
|
6
6
|
*/
|
|
7
7
|
import type { AbstractDialect } from '../../dialect/abstractDialect.js';
|
|
8
|
+
import type { IndexFacet } from '../../schema/indexDifferences.js';
|
|
8
9
|
import type { SchemaAST } from '../../schema/schemaAST.js';
|
|
9
10
|
import type { DriftReport } from '../../schema/types.js';
|
|
10
11
|
/**
|
|
@@ -17,6 +18,8 @@ export interface DriftDetectorOptions {
|
|
|
17
18
|
checkNullable?: boolean;
|
|
18
19
|
/** Include index differences */
|
|
19
20
|
checkIndexes?: boolean;
|
|
21
|
+
/** `indexFacets` of the introspector that produced the actual schema; anything else goes uncompared. */
|
|
22
|
+
indexFacets?: ReadonlySet<IndexFacet>;
|
|
20
23
|
/** Include foreign key differences */
|
|
21
24
|
checkForeignKeys?: boolean;
|
|
22
25
|
/**
|
|
@@ -34,55 +37,7 @@ export interface DriftDetectorOptions {
|
|
|
34
37
|
dialect?: AbstractDialect;
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
export declare class DriftDetector {
|
|
40
|
-
private readonly expectedAST;
|
|
41
|
-
private readonly actualAST;
|
|
42
|
-
private readonly options;
|
|
43
|
-
constructor(expectedAST: SchemaAST, actualAST: SchemaAST, options?: DriftDetectorOptions);
|
|
44
|
-
/**
|
|
45
|
-
* Detect all schema drift.
|
|
46
|
-
*/
|
|
47
|
-
detect(): DriftReport;
|
|
48
|
-
/**
|
|
49
|
-
* Detect table-level drifts (missing/unexpected tables).
|
|
50
|
-
*/
|
|
51
|
-
private detectTableDrifts;
|
|
52
|
-
/**
|
|
53
|
-
* Detect column-level drifts.
|
|
54
|
-
*/
|
|
55
|
-
private detectColumnDrifts;
|
|
56
|
-
/**
|
|
57
|
-
* Add drifts for column alterations (type/nullable mismatches).
|
|
58
|
-
*/
|
|
59
|
-
private addAlterColumnDrifts;
|
|
60
|
-
/**
|
|
61
|
-
* Detect index drifts.
|
|
62
|
-
*/
|
|
63
|
-
private detectIndexDrifts;
|
|
64
|
-
/**
|
|
65
|
-
* Detect relationship/FK drifts.
|
|
66
|
-
*/
|
|
67
|
-
private detectRelationshipDrifts;
|
|
68
|
-
/**
|
|
69
|
-
* Calculate overall status based on drifts.
|
|
70
|
-
*/
|
|
71
|
-
private calculateStatus;
|
|
72
|
-
/**
|
|
73
|
-
* Create a summary of drifts by severity.
|
|
74
|
-
*/
|
|
75
|
-
private createSummary;
|
|
76
|
-
/**
|
|
77
|
-
* Format type for display.
|
|
78
|
-
*/
|
|
79
|
-
private formatType;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Create a DriftDetector for comparing expected vs actual schemas.
|
|
83
|
-
*/
|
|
84
|
-
export declare function createDriftDetector(expectedAST: SchemaAST, actualAST: SchemaAST, options?: DriftDetectorOptions): DriftDetector;
|
|
85
|
-
/**
|
|
86
|
-
* Quick check for schema drift.
|
|
40
|
+
* Compare an expected schema (from entities) with an actual one (from the database) and report every
|
|
41
|
+
* way they have drifted apart.
|
|
87
42
|
*/
|
|
88
43
|
export declare function detectDrift(expectedAST: SchemaAST, actualAST: SchemaAST, options?: DriftDetectorOptions): DriftReport;
|
|
@@ -5,261 +5,252 @@
|
|
|
5
5
|
* actual database schema.
|
|
6
6
|
*/
|
|
7
7
|
import { canonicalToSql } from '../../schema/canonicalType.js';
|
|
8
|
-
import {
|
|
8
|
+
import { diffSchemas } from '../../schema/schemaASTDiffer.js';
|
|
9
|
+
function resolveOptions(options) {
|
|
10
|
+
return {
|
|
11
|
+
checkTypes: options.checkTypes ?? true,
|
|
12
|
+
checkNullable: options.checkNullable ?? true,
|
|
13
|
+
checkIndexes: options.checkIndexes ?? true,
|
|
14
|
+
indexFacets: options.indexFacets ?? new Set(),
|
|
15
|
+
checkForeignKeys: options.checkForeignKeys ?? true,
|
|
16
|
+
checkDefaults: options.checkDefaults ?? false,
|
|
17
|
+
excludeTables: options.excludeTables ?? [],
|
|
18
|
+
dialect: options.dialect,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compare an expected schema (from entities) with an actual one (from the database) and report every
|
|
23
|
+
* way they have drifted apart.
|
|
24
|
+
*/
|
|
25
|
+
export function detectDrift(expectedAST, actualAST, options = {}) {
|
|
26
|
+
const opts = resolveOptions(options);
|
|
27
|
+
const diff = diffSchemas(expectedAST, actualAST, {
|
|
28
|
+
compareIndexes: opts.checkIndexes,
|
|
29
|
+
indexFacets: opts.indexFacets,
|
|
30
|
+
compareRelationships: opts.checkForeignKeys,
|
|
31
|
+
excludeTables: opts.excludeTables,
|
|
32
|
+
});
|
|
33
|
+
const drifts = [
|
|
34
|
+
...detectTableDrifts(diff),
|
|
35
|
+
...detectColumnDrifts(diff, opts),
|
|
36
|
+
...detectIndexDrifts(diff),
|
|
37
|
+
...detectRelationshipDrifts(diff),
|
|
38
|
+
];
|
|
39
|
+
return {
|
|
40
|
+
status: calculateStatus(drifts),
|
|
41
|
+
drifts,
|
|
42
|
+
summary: createSummary(drifts),
|
|
43
|
+
generatedAt: new Date(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
9
46
|
/**
|
|
10
|
-
*
|
|
47
|
+
* Detect table-level drifts (missing/unexpected tables).
|
|
11
48
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
checkIndexes: options.checkIndexes ?? true,
|
|
23
|
-
checkForeignKeys: options.checkForeignKeys ?? true,
|
|
24
|
-
checkDefaults: options.checkDefaults ?? false,
|
|
25
|
-
excludeTables: options.excludeTables ?? [],
|
|
26
|
-
dialect: options.dialect,
|
|
27
|
-
};
|
|
49
|
+
function detectTableDrifts(diff) {
|
|
50
|
+
const drifts = [];
|
|
51
|
+
for (const table of diff.tablesToCreate) {
|
|
52
|
+
drifts.push({
|
|
53
|
+
type: 'missing_table',
|
|
54
|
+
severity: 'critical',
|
|
55
|
+
table: table.name,
|
|
56
|
+
details: `Entity "${table.name}" exists but table not in database`,
|
|
57
|
+
suggestion: 'Run migrations to create table',
|
|
58
|
+
});
|
|
28
59
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
compareRelationships: this.options.checkForeignKeys,
|
|
37
|
-
excludeTables: this.options.excludeTables,
|
|
60
|
+
for (const table of diff.tablesToDrop) {
|
|
61
|
+
drifts.push({
|
|
62
|
+
type: 'unexpected_table',
|
|
63
|
+
severity: 'warning',
|
|
64
|
+
table: table.name,
|
|
65
|
+
details: `Table "${table.name}" exists in database but no matching entity`,
|
|
66
|
+
suggestion: 'Create entity or drop table',
|
|
38
67
|
});
|
|
39
|
-
const drifts = [
|
|
40
|
-
...this.detectTableDrifts(diff),
|
|
41
|
-
...this.detectColumnDrifts(diff),
|
|
42
|
-
...this.detectIndexDrifts(diff),
|
|
43
|
-
...this.detectRelationshipDrifts(diff),
|
|
44
|
-
];
|
|
45
|
-
return {
|
|
46
|
-
status: this.calculateStatus(drifts),
|
|
47
|
-
drifts,
|
|
48
|
-
summary: this.createSummary(drifts),
|
|
49
|
-
generatedAt: new Date(),
|
|
50
|
-
};
|
|
51
68
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
return drifts;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Detect column-level drifts.
|
|
73
|
+
*/
|
|
74
|
+
function detectColumnDrifts(diff, opts) {
|
|
75
|
+
const drifts = [];
|
|
76
|
+
for (const colDiff of diff.columnDiffs) {
|
|
77
|
+
if (colDiff.type === 'add') {
|
|
58
78
|
drifts.push({
|
|
59
|
-
type: '
|
|
79
|
+
type: 'missing_column',
|
|
60
80
|
severity: 'critical',
|
|
61
|
-
table: table
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
table: colDiff.table,
|
|
82
|
+
column: colDiff.column,
|
|
83
|
+
details: `Column "${colDiff.column}" expected but not found in database`,
|
|
84
|
+
suggestion: 'Run migration to add column',
|
|
64
85
|
});
|
|
65
86
|
}
|
|
66
|
-
|
|
87
|
+
else if (colDiff.type === 'drop') {
|
|
67
88
|
drifts.push({
|
|
68
|
-
type: '
|
|
89
|
+
type: 'unexpected_column',
|
|
69
90
|
severity: 'warning',
|
|
70
|
-
table: table
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
table: colDiff.table,
|
|
92
|
+
column: colDiff.column,
|
|
93
|
+
details: `Column "${colDiff.column}" exists in database but not in entity`,
|
|
94
|
+
suggestion: 'Add to entity or create migration to drop',
|
|
73
95
|
});
|
|
74
96
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Detect column-level drifts.
|
|
79
|
-
*/
|
|
80
|
-
detectColumnDrifts(diff) {
|
|
81
|
-
const drifts = [];
|
|
82
|
-
for (const colDiff of diff.columnDiffs) {
|
|
83
|
-
if (colDiff.type === 'add') {
|
|
84
|
-
drifts.push({
|
|
85
|
-
type: 'missing_column',
|
|
86
|
-
severity: 'critical',
|
|
87
|
-
table: colDiff.table,
|
|
88
|
-
column: colDiff.column,
|
|
89
|
-
details: `Column "${colDiff.column}" expected but not found in database`,
|
|
90
|
-
suggestion: 'Run migration to add column',
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
else if (colDiff.type === 'drop') {
|
|
94
|
-
drifts.push({
|
|
95
|
-
type: 'unexpected_column',
|
|
96
|
-
severity: 'warning',
|
|
97
|
-
table: colDiff.table,
|
|
98
|
-
column: colDiff.column,
|
|
99
|
-
details: `Column "${colDiff.column}" exists in database but not in entity`,
|
|
100
|
-
suggestion: 'Add to entity or create migration to drop',
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
else if (colDiff.type === 'alter') {
|
|
104
|
-
this.addAlterColumnDrifts(colDiff, drifts);
|
|
105
|
-
}
|
|
97
|
+
else if (colDiff.type === 'alter') {
|
|
98
|
+
addAlterColumnDrifts(colDiff, drifts, opts);
|
|
106
99
|
}
|
|
107
|
-
return drifts;
|
|
108
100
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
101
|
+
return drifts;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Add drifts for column alterations (type/nullable mismatches).
|
|
105
|
+
*/
|
|
106
|
+
function addAlterColumnDrifts(colDiff, drifts, opts) {
|
|
107
|
+
// Every check below compares the two sides, so there is nothing to report without both.
|
|
108
|
+
if (!colDiff.expected || !colDiff.actual) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (opts.checkTypes) {
|
|
112
|
+
const expectedType = formatType(colDiff.expected.type, opts.dialect);
|
|
113
|
+
const actualType = formatType(colDiff.actual.type, opts.dialect);
|
|
114
|
+
if (expectedType !== actualType) {
|
|
115
|
+
drifts.push({
|
|
116
|
+
type: 'type_mismatch',
|
|
117
|
+
severity: colDiff.isBreaking ? 'critical' : 'warning',
|
|
118
|
+
table: colDiff.table,
|
|
119
|
+
column: colDiff.column,
|
|
120
|
+
expected: expectedType,
|
|
121
|
+
actual: actualType,
|
|
122
|
+
details: `Type mismatch for "${colDiff.column}": expected ${expectedType}, got ${actualType}`,
|
|
123
|
+
suggestion: colDiff.isBreaking
|
|
124
|
+
? 'Data truncation risk! Create migration to fix.'
|
|
125
|
+
: 'Create migration to align types',
|
|
126
|
+
});
|
|
134
127
|
}
|
|
135
|
-
|
|
128
|
+
}
|
|
129
|
+
if (opts.checkNullable && colDiff.expected.nullable !== colDiff.actual.nullable) {
|
|
130
|
+
drifts.push({
|
|
131
|
+
type: 'constraint_mismatch',
|
|
132
|
+
severity: 'warning',
|
|
133
|
+
table: colDiff.table,
|
|
134
|
+
column: colDiff.column,
|
|
135
|
+
expected: colDiff.expected.nullable ? 'NULLABLE' : 'NOT NULL',
|
|
136
|
+
actual: colDiff.actual.nullable ? 'NULLABLE' : 'NOT NULL',
|
|
137
|
+
details: `Nullable mismatch for "${colDiff.column}"`,
|
|
138
|
+
suggestion: 'Align nullable setting in entity or database',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (opts.checkDefaults) {
|
|
142
|
+
const expected = String(colDiff.expected.defaultValue ?? 'NULL');
|
|
143
|
+
const actual = String(colDiff.actual.defaultValue ?? 'NULL');
|
|
144
|
+
if (expected !== actual) {
|
|
136
145
|
drifts.push({
|
|
137
146
|
type: 'constraint_mismatch',
|
|
138
|
-
severity: '
|
|
147
|
+
severity: 'info',
|
|
139
148
|
table: colDiff.table,
|
|
140
149
|
column: colDiff.column,
|
|
141
|
-
expected
|
|
142
|
-
actual
|
|
143
|
-
details: `
|
|
144
|
-
suggestion: 'Align
|
|
150
|
+
expected,
|
|
151
|
+
actual,
|
|
152
|
+
details: `Default mismatch for "${colDiff.column}"`,
|
|
153
|
+
suggestion: 'Align the default in the entity or the database',
|
|
145
154
|
});
|
|
146
155
|
}
|
|
147
|
-
if (this.options.checkDefaults) {
|
|
148
|
-
const expected = String(colDiff.expected.defaultValue ?? 'NULL');
|
|
149
|
-
const actual = String(colDiff.actual.defaultValue ?? 'NULL');
|
|
150
|
-
if (expected !== actual) {
|
|
151
|
-
drifts.push({
|
|
152
|
-
type: 'constraint_mismatch',
|
|
153
|
-
severity: 'info',
|
|
154
|
-
table: colDiff.table,
|
|
155
|
-
column: colDiff.column,
|
|
156
|
-
expected,
|
|
157
|
-
actual,
|
|
158
|
-
details: `Default mismatch for "${colDiff.column}"`,
|
|
159
|
-
suggestion: 'Align the default in the entity or the database',
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
156
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
else if (idxDiff.type === 'drop') {
|
|
181
|
-
drifts.push({
|
|
182
|
-
type: 'unexpected_index',
|
|
183
|
-
severity: 'info',
|
|
184
|
-
table: idxDiff.table,
|
|
185
|
-
index: idxDiff.name,
|
|
186
|
-
details: `Index "${idxDiff.name}" exists in database but not defined in entity`,
|
|
187
|
-
suggestion: 'Add @Field({ index }) or create migration to drop',
|
|
188
|
-
});
|
|
189
|
-
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Detect index drifts.
|
|
160
|
+
*/
|
|
161
|
+
function detectIndexDrifts(diff) {
|
|
162
|
+
const drifts = [];
|
|
163
|
+
for (const idxDiff of diff.indexDiffs) {
|
|
164
|
+
if (idxDiff.type === 'create') {
|
|
165
|
+
drifts.push({
|
|
166
|
+
type: 'missing_index',
|
|
167
|
+
severity: 'warning',
|
|
168
|
+
table: idxDiff.table,
|
|
169
|
+
index: idxDiff.name,
|
|
170
|
+
details: `Index "${idxDiff.name}" expected but not found in database`,
|
|
171
|
+
suggestion: 'Create index via migration',
|
|
172
|
+
});
|
|
190
173
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
severity: 'info',
|
|
213
|
-
table: relDiff.fromTable,
|
|
214
|
-
relationship: relDiff.name,
|
|
215
|
-
details: `FK "${relDiff.name}" exists in database but not in entity`,
|
|
216
|
-
suggestion: 'Add relation to entity or drop FK',
|
|
217
|
-
});
|
|
218
|
-
}
|
|
174
|
+
else if (idxDiff.type === 'drop') {
|
|
175
|
+
drifts.push({
|
|
176
|
+
type: 'unexpected_index',
|
|
177
|
+
severity: 'info',
|
|
178
|
+
table: idxDiff.table,
|
|
179
|
+
index: idxDiff.name,
|
|
180
|
+
details: `Index "${idxDiff.name}" exists in database but not defined in entity`,
|
|
181
|
+
suggestion: 'Add @Field({ index }) or create migration to drop',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else if (idxDiff.type === 'alter') {
|
|
185
|
+
// No `expected`/`actual` here: the CLI prints those by interpolation, where an `IndexNode`
|
|
186
|
+
// renders as `[object Object]`. What differs is already spelled out in `description`.
|
|
187
|
+
drifts.push({
|
|
188
|
+
type: 'index_mismatch',
|
|
189
|
+
severity: 'warning',
|
|
190
|
+
table: idxDiff.table,
|
|
191
|
+
index: idxDiff.name,
|
|
192
|
+
details: `Index "${idxDiff.name}" differs from the entity (${idxDiff.description})`,
|
|
193
|
+
suggestion: 'Drop and recreate the index via migration',
|
|
194
|
+
});
|
|
219
195
|
}
|
|
220
|
-
return drifts;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Calculate overall status based on drifts.
|
|
224
|
-
*/
|
|
225
|
-
calculateStatus(drifts) {
|
|
226
|
-
if (drifts.length === 0)
|
|
227
|
-
return 'in_sync';
|
|
228
|
-
const hasCritical = drifts.some((d) => d.severity === 'critical');
|
|
229
|
-
if (hasCritical)
|
|
230
|
-
return 'critical';
|
|
231
|
-
return 'drifted';
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Create a summary of drifts by severity.
|
|
235
|
-
*/
|
|
236
|
-
createSummary(drifts) {
|
|
237
|
-
return {
|
|
238
|
-
critical: drifts.filter((d) => d.severity === 'critical').length,
|
|
239
|
-
warning: drifts.filter((d) => d.severity === 'warning').length,
|
|
240
|
-
info: drifts.filter((d) => d.severity === 'info').length,
|
|
241
|
-
};
|
|
242
196
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
197
|
+
return drifts;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Detect relationship/FK drifts.
|
|
201
|
+
*/
|
|
202
|
+
function detectRelationshipDrifts(diff) {
|
|
203
|
+
const drifts = [];
|
|
204
|
+
for (const relDiff of diff.relationshipDiffs) {
|
|
205
|
+
if (relDiff.type === 'create') {
|
|
206
|
+
drifts.push({
|
|
207
|
+
type: 'missing_relationship',
|
|
208
|
+
severity: 'warning',
|
|
209
|
+
table: relDiff.fromTable,
|
|
210
|
+
relationship: relDiff.name,
|
|
211
|
+
details: `FK "${relDiff.name}" expected but not found in database`,
|
|
212
|
+
suggestion: 'Add FK constraint or remove relation from entity',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else if (relDiff.type === 'drop') {
|
|
216
|
+
drifts.push({
|
|
217
|
+
type: 'unexpected_relationship',
|
|
218
|
+
severity: 'info',
|
|
219
|
+
table: relDiff.fromTable,
|
|
220
|
+
relationship: relDiff.name,
|
|
221
|
+
details: `FK "${relDiff.name}" exists in database but not in entity`,
|
|
222
|
+
suggestion: 'Add relation to entity or drop FK',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
251
225
|
}
|
|
226
|
+
return drifts;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Calculate overall status based on drifts.
|
|
230
|
+
*/
|
|
231
|
+
function calculateStatus(drifts) {
|
|
232
|
+
if (drifts.length === 0)
|
|
233
|
+
return 'in_sync';
|
|
234
|
+
const hasCritical = drifts.some((d) => d.severity === 'critical');
|
|
235
|
+
if (hasCritical)
|
|
236
|
+
return 'critical';
|
|
237
|
+
return 'drifted';
|
|
252
238
|
}
|
|
253
239
|
/**
|
|
254
|
-
* Create a
|
|
240
|
+
* Create a summary of drifts by severity.
|
|
255
241
|
*/
|
|
256
|
-
|
|
257
|
-
return
|
|
242
|
+
function createSummary(drifts) {
|
|
243
|
+
return {
|
|
244
|
+
critical: drifts.filter((d) => d.severity === 'critical').length,
|
|
245
|
+
warning: drifts.filter((d) => d.severity === 'warning').length,
|
|
246
|
+
info: drifts.filter((d) => d.severity === 'info').length,
|
|
247
|
+
};
|
|
258
248
|
}
|
|
259
249
|
/**
|
|
260
|
-
*
|
|
250
|
+
* Format type for display.
|
|
261
251
|
*/
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
function formatType(type, dialect) {
|
|
253
|
+
if (!type || !dialect)
|
|
254
|
+
return 'unknown';
|
|
255
|
+
return canonicalToSql(type, dialect);
|
|
265
256
|
}
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Detects schema drift between expected and actual database schemas.
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
6
|
+
export { type DriftDetectorOptions, detectDrift, } from './driftDetector.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ColumnNode, TableNode } from '../../schema/types.js';
|
|
2
|
+
import type { FullColumnDefinition, TableDefinition } from '../builder/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A migration builder's table definition as the AST nodes the generators render from, so a hand-written
|
|
5
|
+
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape. Free functions and
|
|
6
|
+
* not generator methods: nothing here consults the dialect.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tableDefinitionToNode(def: TableDefinition): TableNode;
|
|
9
|
+
export declare function fullColumnDefinitionToNode(col: FullColumnDefinition, tableName: string): ColumnNode;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A migration builder's table definition as the AST nodes the generators render from, so a hand-written
|
|
3
|
+
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape. Free functions and
|
|
4
|
+
* not generator methods: nothing here consults the dialect.
|
|
5
|
+
*/
|
|
6
|
+
export function tableDefinitionToNode(def) {
|
|
7
|
+
const columns = new Map();
|
|
8
|
+
const pkNodes = [];
|
|
9
|
+
const table = {
|
|
10
|
+
name: def.name,
|
|
11
|
+
columns,
|
|
12
|
+
primaryKey: [], // placeholder
|
|
13
|
+
indexes: [],
|
|
14
|
+
schema: { tables: new Map(), relationships: [], indexes: [] },
|
|
15
|
+
incomingRelations: [],
|
|
16
|
+
outgoingRelations: [],
|
|
17
|
+
comment: def.comment,
|
|
18
|
+
};
|
|
19
|
+
for (const colDef of def.columns) {
|
|
20
|
+
const node = fullColumnDefinitionToNode(colDef, def.name);
|
|
21
|
+
node.table = table;
|
|
22
|
+
columns.set(node.name, node);
|
|
23
|
+
if (node.isPrimaryKey) {
|
|
24
|
+
pkNodes.push(node);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const finalPrimaryKey = def.primaryKey
|
|
28
|
+
? def.primaryKey.map((name) => columns.get(name)).filter((c) => c !== undefined)
|
|
29
|
+
: pkNodes;
|
|
30
|
+
table.primaryKey = finalPrimaryKey;
|
|
31
|
+
for (const idxDef of def.indexes) {
|
|
32
|
+
table.indexes.push({ ...idxDef, table });
|
|
33
|
+
}
|
|
34
|
+
for (const fkDef of def.foreignKeys) {
|
|
35
|
+
const relNode = {
|
|
36
|
+
name: fkDef.name ?? `fk_${def.name}_${fkDef.columns.join('_')}`,
|
|
37
|
+
type: 'ManyToOne', // Builder default
|
|
38
|
+
from: {
|
|
39
|
+
table,
|
|
40
|
+
columns: fkDef.columns.map((name) => columns.get(name)).filter((c) => c !== undefined),
|
|
41
|
+
},
|
|
42
|
+
to: {
|
|
43
|
+
table: { name: fkDef.referencesTable },
|
|
44
|
+
columns: fkDef.referencesColumns.map((name) => ({ name })),
|
|
45
|
+
},
|
|
46
|
+
onDelete: fkDef.onDelete,
|
|
47
|
+
onUpdate: fkDef.onUpdate,
|
|
48
|
+
};
|
|
49
|
+
table.outgoingRelations.push(relNode);
|
|
50
|
+
}
|
|
51
|
+
return table;
|
|
52
|
+
}
|
|
53
|
+
export function fullColumnDefinitionToNode(col, tableName) {
|
|
54
|
+
return {
|
|
55
|
+
name: col.name,
|
|
56
|
+
type: col.type,
|
|
57
|
+
nullable: col.nullable,
|
|
58
|
+
defaultValue: col.defaultValue,
|
|
59
|
+
isPrimaryKey: col.primaryKey,
|
|
60
|
+
isAutoIncrement: col.autoIncrement,
|
|
61
|
+
isUnique: col.unique,
|
|
62
|
+
comment: col.comment,
|
|
63
|
+
table: { name: tableName },
|
|
64
|
+
referencedBy: [],
|
|
65
|
+
references: col.foreignKey
|
|
66
|
+
? {
|
|
67
|
+
name: `fk_${tableName}_${col.name}`,
|
|
68
|
+
type: 'ManyToOne',
|
|
69
|
+
from: { table: { name: tableName }, columns: [] },
|
|
70
|
+
to: {
|
|
71
|
+
table: { name: col.foreignKey.table },
|
|
72
|
+
columns: col.foreignKey.columns.map((name) => ({ name })),
|
|
73
|
+
},
|
|
74
|
+
onDelete: col.foreignKey.onDelete,
|
|
75
|
+
onUpdate: col.foreignKey.onUpdate,
|
|
76
|
+
}
|
|
77
|
+
: undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|