@an5/orm 1.3.1 → 1.3.3
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 +74 -11
- package/dist/cleanup.d.ts +1 -0
- package/dist/cleanup.js +177 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +147 -0
- package/dist/generator/src/attributes.d.ts +57 -0
- package/dist/generator/src/attributes.js +225 -0
- package/dist/generator/src/code-generator.js +49 -16
- package/dist/generator/src/config.d.ts +2 -2
- package/dist/generator/src/config.js +4 -3
- package/dist/generator/src/dialect.d.ts +52 -1
- package/dist/generator/src/dialect.js +136 -3
- package/dist/generator/src/dotnet-generator.js +107 -27
- package/dist/generator/src/golang-generator.d.ts +8 -0
- package/dist/generator/src/golang-generator.js +116 -49
- package/dist/generator/src/java-generator.d.ts +8 -0
- package/dist/generator/src/java-generator.js +108 -3
- package/dist/generator/src/kotlin-generator.js +51 -0
- package/dist/generator/src/parser.d.ts +1 -0
- package/dist/generator/src/parser.js +90 -35
- package/dist/generator/src/pull-schema.d.ts +54 -0
- package/dist/generator/src/pull-schema.js +116 -0
- package/dist/generator/src/push-apply.d.ts +4 -2
- package/dist/generator/src/push-apply.js +53 -4
- package/dist/generator/src/push-schema.d.ts +21 -9
- package/dist/generator/src/push-schema.js +132 -32
- package/dist/generator/src/python-generator.js +35 -22
- package/dist/generator/src/rust-generator.js +79 -74
- package/dist/generator/src/swift-generator.js +54 -3
- package/dist/generator/src/types.d.ts +10 -0
- package/dist/generator/src/view-order.d.ts +33 -0
- package/dist/generator/src/view-order.js +123 -0
- package/dist/migrate.d.ts +12 -0
- package/dist/migrate.js +442 -0
- package/dist/migration-core.d.ts +32 -3
- package/dist/migration-core.js +213 -43
- package/dist/provider-support.d.ts +0 -1
- package/dist/provider-support.js +0 -1
- package/dist/pull.d.ts +1 -0
- package/dist/pull.js +425 -0
- package/dist/push.d.ts +1 -0
- package/dist/push.js +93 -0
- package/dist/seed.d.ts +1 -0
- package/dist/seed.js +33 -0
- package/generator/src/attributes.ts +231 -0
- package/generator/src/code-generator.ts +47 -16
- package/generator/src/config.ts +5 -3
- package/generator/src/dialect.ts +188 -5
- package/generator/src/dotnet-generator.ts +108 -28
- package/generator/src/golang-generator.ts +119 -50
- package/generator/src/java-generator.ts +109 -3
- package/generator/src/kotlin-generator.ts +51 -0
- package/generator/src/parser.ts +102 -30
- package/generator/src/pull-schema.ts +127 -0
- package/generator/src/push-apply.ts +62 -5
- package/generator/src/push-schema.ts +160 -32
- package/generator/src/python-generator.ts +33 -20
- package/generator/src/rust-generator.ts +79 -73
- package/generator/src/swift-generator.ts +55 -4
- package/generator/src/types.ts +10 -0
- package/generator/src/view-order.ts +133 -0
- package/package.json +19 -14
package/dist/migration-core.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyAppliedMigrations = verifyAppliedMigrations;
|
|
3
4
|
exports.parseSqlType = parseSqlType;
|
|
4
5
|
exports.formatDbColumnSqlType = formatDbColumnSqlType;
|
|
5
6
|
exports.mapDefault = mapDefault;
|
|
@@ -12,6 +13,7 @@ exports.buildAddColumnPreflightSql = buildAddColumnPreflightSql;
|
|
|
12
13
|
exports.buildUniqueConstraintPreflightSql = buildUniqueConstraintPreflightSql;
|
|
13
14
|
exports.parseSchemaText = parseSchemaText;
|
|
14
15
|
exports.buildCreateTableSql = buildCreateTableSql;
|
|
16
|
+
exports.buildCreateViewSql = buildCreateViewSql;
|
|
15
17
|
exports.buildIndexDiff = buildIndexDiff;
|
|
16
18
|
exports.generateColumnDiff = generateColumnDiff;
|
|
17
19
|
exports.generateDiff = generateDiff;
|
|
@@ -21,7 +23,33 @@ exports.parseMigrationSections = parseMigrationSections;
|
|
|
21
23
|
exports.splitSqlBatches = splitSqlBatches;
|
|
22
24
|
exports.parseRollbackSelection = parseRollbackSelection;
|
|
23
25
|
exports.parseMigrationCommandOptions = parseMigrationCommandOptions;
|
|
26
|
+
const attributes_1 = require("./generator/src/attributes");
|
|
24
27
|
const field_types_1 = require("./generator/src/field-types");
|
|
28
|
+
const dialect_1 = require("./generator/src/dialect");
|
|
29
|
+
const view_order_1 = require("./generator/src/view-order");
|
|
30
|
+
/**
|
|
31
|
+
* Compares applied migrations with the files they were applied from.
|
|
32
|
+
*
|
|
33
|
+
* `apply` used to look only at the ids, so editing an applied file was invisible
|
|
34
|
+
* and a fresh database would get different SQL from the one already recorded.
|
|
35
|
+
* Rollback already refuses a changed checksum; this catches it before the next
|
|
36
|
+
* migration is applied, where it can still be fixed without a partial history.
|
|
37
|
+
*/
|
|
38
|
+
function verifyAppliedMigrations(applied, readMigration, checksum) {
|
|
39
|
+
const drift = { changed: [], missing: [] };
|
|
40
|
+
for (const migration of applied) {
|
|
41
|
+
if (!migration.checksum)
|
|
42
|
+
continue;
|
|
43
|
+
const content = readMigration(migration.id);
|
|
44
|
+
if (content === null) {
|
|
45
|
+
drift.missing.push(migration.id);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (checksum(content) !== migration.checksum)
|
|
49
|
+
drift.changed.push(migration.id);
|
|
50
|
+
}
|
|
51
|
+
return drift;
|
|
52
|
+
}
|
|
25
53
|
function parseSqlType(raw) {
|
|
26
54
|
const match = raw.match(/^(\w+)/);
|
|
27
55
|
return match?.[1]?.toUpperCase() ?? raw.toUpperCase();
|
|
@@ -80,23 +108,18 @@ function schemaIndexName(definition, fallback) {
|
|
|
80
108
|
return fallback(schemaIndexFields(definition));
|
|
81
109
|
}
|
|
82
110
|
function parseMappedIndex(line, directive) {
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
111
|
+
const parsed = (0, attributes_1.parseIndexDirective)((0, attributes_1.attributeArgs)(line, directive));
|
|
112
|
+
if (!parsed)
|
|
85
113
|
return null;
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
definition.includeFields = includeMatch[1].split(',').map(field => field.trim()).filter(Boolean);
|
|
96
|
-
if (filterMatch?.[1])
|
|
97
|
-
definition.filter = filterMatch[1];
|
|
98
|
-
if (optionsMatch?.[1])
|
|
99
|
-
definition.options = optionsMatch[1];
|
|
114
|
+
const definition = { fields: parsed.fields };
|
|
115
|
+
if (parsed.name)
|
|
116
|
+
definition.name = parsed.name;
|
|
117
|
+
if (parsed.includeFields && parsed.includeFields.length > 0)
|
|
118
|
+
definition.includeFields = parsed.includeFields;
|
|
119
|
+
if (parsed.filter !== undefined)
|
|
120
|
+
definition.filter = parsed.filter;
|
|
121
|
+
if (parsed.options !== undefined)
|
|
122
|
+
definition.options = parsed.options;
|
|
100
123
|
return definition;
|
|
101
124
|
}
|
|
102
125
|
function quoteTableName(raw) {
|
|
@@ -269,9 +292,10 @@ function parseSchemaText(text, provider = field_types_1.DEFAULT_PROVIDER) {
|
|
|
269
292
|
const models = [];
|
|
270
293
|
const typeIssues = [];
|
|
271
294
|
const modelRefs = [];
|
|
295
|
+
const viewRefs = [];
|
|
272
296
|
/** `@@schema` per model, applied after the model is read rather than mid-loop. */
|
|
273
297
|
const schemas = new Map();
|
|
274
|
-
const lines =
|
|
298
|
+
const lines = (0, attributes_1.splitSchemaLines)(text);
|
|
275
299
|
let current = null;
|
|
276
300
|
for (let line of lines) {
|
|
277
301
|
line = line.trim();
|
|
@@ -290,20 +314,43 @@ function parseSchemaText(text, provider = field_types_1.DEFAULT_PROVIDER) {
|
|
|
290
314
|
models.push(current);
|
|
291
315
|
continue;
|
|
292
316
|
}
|
|
317
|
+
const viewMatch = line.match(/^view\s+(\w+)\s*\{/);
|
|
318
|
+
if (viewMatch) {
|
|
319
|
+
const viewName = viewMatch[1] ?? 'View';
|
|
320
|
+
current = {
|
|
321
|
+
name: viewName,
|
|
322
|
+
tableName: viewName.toLowerCase() + 's',
|
|
323
|
+
fields: [],
|
|
324
|
+
compoundUniques: [],
|
|
325
|
+
indexes: [],
|
|
326
|
+
isView: true,
|
|
327
|
+
};
|
|
328
|
+
models.push(current);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
293
331
|
if (line === '}') {
|
|
294
332
|
current = null;
|
|
295
333
|
continue;
|
|
296
334
|
}
|
|
297
335
|
if (!current)
|
|
298
336
|
continue;
|
|
337
|
+
if (line.startsWith('@@sql') || line.startsWith('@@definition')) {
|
|
338
|
+
if (current.isView) {
|
|
339
|
+
const attribute = line.startsWith('@@sql') ? '@@sql' : '@@definition';
|
|
340
|
+
const sql = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, attribute));
|
|
341
|
+
if (sql !== undefined)
|
|
342
|
+
current.viewSql = sql.trim();
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
299
346
|
if (line.startsWith('@@map')) {
|
|
300
347
|
// Just the table name. A schema comes from `@@schema` alone, because a mapped
|
|
301
348
|
// name may legitimately contain a dot: `@@map("reports.daily")` is one table
|
|
302
349
|
// whose name has a dot in it, not a table called `daily` in a schema called
|
|
303
350
|
// `reports`.
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
current.tableName =
|
|
351
|
+
const mapped = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@map'));
|
|
352
|
+
if (mapped)
|
|
353
|
+
current.tableName = mapped;
|
|
307
354
|
continue;
|
|
308
355
|
}
|
|
309
356
|
if (line.startsWith('@@schema')) {
|
|
@@ -311,18 +358,22 @@ function parseSchemaText(text, provider = field_types_1.DEFAULT_PROVIDER) {
|
|
|
311
358
|
// honoured it, so a migration created the table in the connection's default
|
|
312
359
|
// schema and the client then read the one the schema file named. Composed in
|
|
313
360
|
// once the whole model has been read, below.
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
schemas.set(current,
|
|
361
|
+
const declared = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@schema'));
|
|
362
|
+
if (declared !== undefined)
|
|
363
|
+
schemas.set(current, declared.trim());
|
|
317
364
|
continue;
|
|
318
365
|
}
|
|
319
366
|
if (line.startsWith('@@unique')) {
|
|
367
|
+
if (current.isView)
|
|
368
|
+
continue;
|
|
320
369
|
const parsed = parseMappedIndex(line, '@@unique');
|
|
321
370
|
if (parsed)
|
|
322
371
|
current.compoundUniques.push(parsed);
|
|
323
372
|
continue;
|
|
324
373
|
}
|
|
325
374
|
if (line.startsWith('@@index')) {
|
|
375
|
+
if (current.isView)
|
|
376
|
+
continue;
|
|
326
377
|
const parsed = parseMappedIndex(line, '@@index');
|
|
327
378
|
if (parsed)
|
|
328
379
|
current.indexes.push(parsed);
|
|
@@ -334,21 +385,27 @@ function parseSchemaText(text, provider = field_types_1.DEFAULT_PROVIDER) {
|
|
|
334
385
|
if (!head)
|
|
335
386
|
continue;
|
|
336
387
|
if (!(0, field_types_1.resolveFieldType)(head.type, provider)) {
|
|
337
|
-
|
|
388
|
+
if (current.isView) {
|
|
389
|
+
viewRefs.push({ view: current.name, field: head.name, type: head.type });
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
modelRefs.push({ model: current.name, field: head.name, type: head.type });
|
|
393
|
+
}
|
|
338
394
|
continue;
|
|
339
395
|
}
|
|
396
|
+
const uniqueArgs = (0, attributes_1.attributeArgs)(line, '@unique');
|
|
340
397
|
current.fields.push({
|
|
341
398
|
name: head.name,
|
|
342
399
|
sqlType: head.type.toUpperCase(),
|
|
343
400
|
isOptional: head.isOptional,
|
|
344
|
-
isId:
|
|
345
|
-
isUnique:
|
|
346
|
-
uniqueName:
|
|
401
|
+
isId: (0, attributes_1.hasAttribute)(line, '@id'),
|
|
402
|
+
isUnique: (0, attributes_1.hasAttribute)(line, '@unique'),
|
|
403
|
+
uniqueName: uniqueArgs ? (0, attributes_1.stringOption)(uniqueArgs, 'map') : undefined,
|
|
347
404
|
// `@updatedAt` is `now()` under another name, and db:push treats it as such.
|
|
348
405
|
// Without this here a stamp column looked like it had no default and every
|
|
349
406
|
// diff wanted to add one.
|
|
350
|
-
defaultValue:
|
|
351
|
-
?? (
|
|
407
|
+
defaultValue: (0, attributes_1.attributeArgs)(line, '@default')
|
|
408
|
+
?? ((0, attributes_1.hasAttribute)(line, '@updatedAt') ? 'now()' : undefined),
|
|
352
409
|
});
|
|
353
410
|
}
|
|
354
411
|
// `dbo` stays implicit: an unqualified model produces the SQL it always has, and a
|
|
@@ -359,14 +416,34 @@ function parseSchemaText(text, provider = field_types_1.DEFAULT_PROVIDER) {
|
|
|
359
416
|
model.tableName = `${schema}.${model.tableName}`;
|
|
360
417
|
}
|
|
361
418
|
const modelNames = models.map((model) => model.name);
|
|
419
|
+
const tableNames = models.filter((model) => !model.isView).map((model) => model.name);
|
|
420
|
+
const viewNames = new Set(models.filter((model) => model.isView).map((model) => model.name));
|
|
362
421
|
for (const ref of modelRefs) {
|
|
363
|
-
if (
|
|
422
|
+
if (viewNames.has(ref.type)) {
|
|
423
|
+
typeIssues.push({
|
|
424
|
+
path: `${ref.model}.${ref.field}`,
|
|
425
|
+
message: `"${ref.type}" is a view and cannot be the target of a relation; select from it directly instead`,
|
|
426
|
+
});
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (!tableNames.includes(ref.type)) {
|
|
364
430
|
typeIssues.push({
|
|
365
431
|
path: `${ref.model}.${ref.field}`,
|
|
366
|
-
message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, provider,
|
|
432
|
+
message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, provider, tableNames),
|
|
367
433
|
});
|
|
368
434
|
}
|
|
369
435
|
}
|
|
436
|
+
for (const ref of viewRefs) {
|
|
437
|
+
typeIssues.push({
|
|
438
|
+
path: `${ref.view}.${ref.field}`,
|
|
439
|
+
message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, provider, modelNames),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
for (const model of models) {
|
|
443
|
+
if (model.isView && !model.viewSql?.trim()) {
|
|
444
|
+
typeIssues.push({ path: model.name, message: 'a view needs @@sql("SELECT ...")' });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
370
447
|
if (typeIssues.length > 0)
|
|
371
448
|
throw new field_types_1.FieldTypeError(provider, typeIssues);
|
|
372
449
|
return models;
|
|
@@ -376,12 +453,16 @@ function buildCreateTableSql(model) {
|
|
|
376
453
|
let def = `[${f.name}] ${f.sqlType}`;
|
|
377
454
|
if (f.isId)
|
|
378
455
|
def += ' PRIMARY KEY';
|
|
456
|
+
// A default does not make a column nullable. Skipping NOT NULL here created a
|
|
457
|
+
// nullable column that the next diff then wanted to alter back.
|
|
458
|
+
if (!f.isOptional && !f.isId)
|
|
459
|
+
def += ' NOT NULL';
|
|
379
460
|
if (f.defaultValue)
|
|
380
461
|
def += ` ${mapDefault(f.defaultValue)}`;
|
|
381
|
-
|
|
382
|
-
|
|
462
|
+
// Always named: an unnamed UNIQUE gets a database-generated name, which the
|
|
463
|
+
// next diff cannot match against `UQ_<table>_<field>` and tries to add again.
|
|
383
464
|
if (f.isUnique && !f.isId)
|
|
384
|
-
def +=
|
|
465
|
+
def += ` CONSTRAINT [${f.uniqueName ?? fieldUniqueConstraintName(model, f)}] UNIQUE`;
|
|
385
466
|
return def;
|
|
386
467
|
});
|
|
387
468
|
for (let idx = 0; idx < model.compoundUniques.length; idx++) {
|
|
@@ -395,6 +476,22 @@ function buildCreateTableSql(model) {
|
|
|
395
476
|
}
|
|
396
477
|
return `CREATE TABLE ${quoteTableName(model.tableName)} (\n ${colDefs.join(',\n ')}\n)`;
|
|
397
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* The migration DDL for a view.
|
|
481
|
+
*
|
|
482
|
+
* `CREATE OR ALTER` is SQL Server's form; `db:migrate` is SQL Server-only today.
|
|
483
|
+
* The body is expected to be a single `SELECT` (`@@sql`), so no batch splitting
|
|
484
|
+
* is needed here — the caller isolates the statement with `GO`.
|
|
485
|
+
*/
|
|
486
|
+
function buildCreateViewSql(model) {
|
|
487
|
+
if (!model.viewSql?.trim()) {
|
|
488
|
+
throw new Error(`View "${model.name}" has no @@sql("SELECT ...") definition.`);
|
|
489
|
+
}
|
|
490
|
+
// A pasted catalog definition keeps its `CREATE VIEW` header; the header this
|
|
491
|
+
// function writes is the one that names the migrated object.
|
|
492
|
+
const body = (0, dialect_1.viewSqlBody)(model.viewSql);
|
|
493
|
+
return `CREATE OR ALTER VIEW ${quoteTableName(model.tableName)} AS\n${body}`;
|
|
494
|
+
}
|
|
398
495
|
function buildIndexDiff(model, artifacts, ops) {
|
|
399
496
|
const existingIndexes = new Set((artifacts.indexes || []).map(name => name.toLowerCase()));
|
|
400
497
|
const existingUniques = new Set((artifacts.uniqueConstraints || []).map(name => name.toLowerCase()));
|
|
@@ -493,12 +590,12 @@ function generateColumnDiff(model, dbColumns, ops) {
|
|
|
493
590
|
let def = `[${f.name}] ${f.sqlType}`;
|
|
494
591
|
if (f.isId)
|
|
495
592
|
def += ' PRIMARY KEY';
|
|
593
|
+
if (!f.isOptional && !f.isId)
|
|
594
|
+
def += ' NOT NULL';
|
|
496
595
|
if (f.defaultValue)
|
|
497
596
|
def += ` ${mapDefault(f.defaultValue)}`;
|
|
498
|
-
if (!f.isOptional && !f.defaultValue && !f.isId)
|
|
499
|
-
def += ' NOT NULL';
|
|
500
597
|
if (f.isUnique && !f.isId)
|
|
501
|
-
def +=
|
|
598
|
+
def += ` CONSTRAINT [${f.uniqueName ?? fieldUniqueConstraintName(model, f)}] UNIQUE`;
|
|
502
599
|
ops.push({
|
|
503
600
|
type: 'ADD_COLUMN',
|
|
504
601
|
table: model.tableName,
|
|
@@ -546,11 +643,19 @@ function generateColumnDiff(model, dbColumns, ops) {
|
|
|
546
643
|
}
|
|
547
644
|
}
|
|
548
645
|
}
|
|
549
|
-
async function generateDiff(schemaModels, dbTables, introspectTable, introspectArtifacts) {
|
|
646
|
+
async function generateDiff(schemaModels, dbTables, introspectTable, introspectArtifacts, dbViews = [], introspectViewDefinition) {
|
|
550
647
|
const ops = [];
|
|
648
|
+
const tableModels = schemaModels.filter(model => !model.isView);
|
|
649
|
+
// A view may read another view. The migration applies its operations in
|
|
650
|
+
// order, so the referenced view has to be created first.
|
|
651
|
+
const viewModels = (0, view_order_1.orderViewsByDependency)(schemaModels.filter(model => model.isView).map(model => ({
|
|
652
|
+
model,
|
|
653
|
+
names: [model.tableName, model.name],
|
|
654
|
+
sql: model.viewSql ?? '',
|
|
655
|
+
}))).ordered.map(entry => entry.model);
|
|
551
656
|
const dbTableIdentities = new Set(dbTables.map(tableIdentityName));
|
|
552
|
-
const schemaTableNames = new Set(
|
|
553
|
-
for (const model of
|
|
657
|
+
const schemaTableNames = new Set(tableModels.map(m => tableIdentityName(m.tableName)));
|
|
658
|
+
for (const model of tableModels) {
|
|
554
659
|
if (!dbTableIdentities.has(tableIdentityName(model.tableName))) {
|
|
555
660
|
ops.push({ type: 'CREATE_TABLE', table: model.tableName, sql: buildCreateTableSql(model) });
|
|
556
661
|
buildIndexDiff(model, {
|
|
@@ -562,12 +667,23 @@ async function generateDiff(schemaModels, dbTables, introspectTable, introspectA
|
|
|
562
667
|
}, ops);
|
|
563
668
|
}
|
|
564
669
|
}
|
|
565
|
-
for (const model of
|
|
670
|
+
for (const model of tableModels) {
|
|
566
671
|
if (dbTableIdentities.has(tableIdentityName(model.tableName))) {
|
|
567
672
|
const dbColumns = await introspectTable(model.tableName);
|
|
568
673
|
generateColumnDiff(model, dbColumns, ops);
|
|
569
674
|
if (introspectArtifacts) {
|
|
570
|
-
|
|
675
|
+
const artifacts = await introspectArtifacts(model.tableName);
|
|
676
|
+
// A new `@unique` column already carries its UNIQUE in the ADD COLUMN
|
|
677
|
+
// statement. Without this, buildIndexDiff saw the constraint missing from
|
|
678
|
+
// the catalog and emitted a second, identically named one, failing on apply.
|
|
679
|
+
const existingColumns = new Set(dbColumns.map(column => column.columnName.toLowerCase()));
|
|
680
|
+
const addedUniqueNames = model.fields
|
|
681
|
+
.filter(field => field.isUnique && !field.isId && !existingColumns.has(field.name.toLowerCase()))
|
|
682
|
+
.map(field => field.uniqueName ?? fieldUniqueConstraintName(model, field));
|
|
683
|
+
buildIndexDiff(model, {
|
|
684
|
+
indexes: artifacts.indexes ?? [],
|
|
685
|
+
uniqueConstraints: [...(artifacts.uniqueConstraints ?? []), ...addedUniqueNames],
|
|
686
|
+
}, ops);
|
|
571
687
|
}
|
|
572
688
|
}
|
|
573
689
|
}
|
|
@@ -581,6 +697,42 @@ async function generateDiff(schemaModels, dbTables, introspectTable, introspectA
|
|
|
581
697
|
});
|
|
582
698
|
}
|
|
583
699
|
}
|
|
700
|
+
// Views are compared by their SQL body rather than by name alone: a view whose
|
|
701
|
+
// query changed has to be replaced, and a catalog that reformats whitespace must
|
|
702
|
+
// not make every diff think it did.
|
|
703
|
+
const dbViewIdentities = new Map(dbViews.map(name => [tableIdentityName(name), name]));
|
|
704
|
+
const schemaViewNames = new Set(viewModels.map(view => tableIdentityName(view.tableName)));
|
|
705
|
+
for (const view of viewModels) {
|
|
706
|
+
const existingName = dbViewIdentities.get(tableIdentityName(view.tableName));
|
|
707
|
+
const existingDefinition = existingName !== undefined && introspectViewDefinition
|
|
708
|
+
? await introspectViewDefinition(existingName)
|
|
709
|
+
: null;
|
|
710
|
+
if (existingName !== undefined && (0, dialect_1.normalizeViewSql)(existingDefinition) === (0, dialect_1.normalizeViewSql)(view.viewSql)) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const previousSql = existingDefinition
|
|
714
|
+
? /^\s*CREATE\s/i.test(existingDefinition)
|
|
715
|
+
? existingDefinition.trim()
|
|
716
|
+
: `CREATE OR ALTER VIEW ${quoteTableName(view.tableName)} AS\n${existingDefinition.trim().replace(/;+\s*$/, '')}`
|
|
717
|
+
: undefined;
|
|
718
|
+
ops.push({
|
|
719
|
+
type: 'CREATE_VIEW',
|
|
720
|
+
table: view.tableName,
|
|
721
|
+
details: existingName !== undefined ? 'View definition changed' : 'View not in database',
|
|
722
|
+
sql: buildCreateViewSql(view),
|
|
723
|
+
...(previousSql !== undefined ? { previousSql } : {}),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
for (const viewName of dbViews) {
|
|
727
|
+
if (!schemaViewNames.has(tableIdentityName(viewName))) {
|
|
728
|
+
ops.push({
|
|
729
|
+
type: 'DROP_VIEW',
|
|
730
|
+
table: viewName,
|
|
731
|
+
details: 'View not in schema',
|
|
732
|
+
sql: `-- DROP VIEW ${quoteTableName(viewName)}`,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
584
736
|
return ops;
|
|
585
737
|
}
|
|
586
738
|
function buildMigrationFile(timestamp, ops) {
|
|
@@ -602,12 +754,20 @@ function buildMigrationFile(timestamp, ops) {
|
|
|
602
754
|
lines.push('-- migrate:up');
|
|
603
755
|
lines.push('');
|
|
604
756
|
for (const op of ops) {
|
|
757
|
+
// `CREATE VIEW` has to be the first statement in a T-SQL batch, so it is fenced
|
|
758
|
+
// with GO. Without this, a migration that creates a table and then a view fails
|
|
759
|
+
// with "'CREATE VIEW' must be the first statement in a query batch".
|
|
760
|
+
const lastMeaningfulLine = [...lines].reverse().find(line => line.trim() !== '');
|
|
761
|
+
if (op.type === 'CREATE_VIEW' && lastMeaningfulLine !== 'GO')
|
|
762
|
+
lines.push('GO');
|
|
605
763
|
lines.push(`-- ${op.type}: ${op.table}`);
|
|
606
764
|
for (const warning of op.riskWarnings || []) {
|
|
607
765
|
lines.push(`-- WARNING: ${warning}`);
|
|
608
766
|
}
|
|
609
767
|
if (op.sql)
|
|
610
768
|
lines.push(op.sql);
|
|
769
|
+
if (op.type === 'CREATE_VIEW')
|
|
770
|
+
lines.push('GO');
|
|
611
771
|
lines.push('');
|
|
612
772
|
}
|
|
613
773
|
lines.push('-- migrate:down');
|
|
@@ -629,6 +789,17 @@ function buildDownMigrationSql(ops) {
|
|
|
629
789
|
lines.push(`DROP TABLE ${table}`);
|
|
630
790
|
continue;
|
|
631
791
|
}
|
|
792
|
+
if (op.type === 'CREATE_VIEW') {
|
|
793
|
+
// A replaced view is restored to its old definition; a newly created one is
|
|
794
|
+
// simply dropped. `CREATE OR ALTER` is fenced by the GO this function joins.
|
|
795
|
+
lines.push(op.previousSql ? op.previousSql.trim() : `DROP VIEW ${table}`);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (op.type === 'DROP_VIEW') {
|
|
799
|
+
// Its up SQL is a commented `DROP VIEW`; dropping a view is never automatic,
|
|
800
|
+
// so there is nothing to undo here.
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
632
803
|
if (op.type === 'ADD_COLUMN' && op.column) {
|
|
633
804
|
lines.push(`ALTER TABLE ${table} DROP COLUMN [${op.column}]`);
|
|
634
805
|
continue;
|
|
@@ -730,4 +901,3 @@ function parseMigrationCommandOptions(args) {
|
|
|
730
901
|
}
|
|
731
902
|
return { preview, rest };
|
|
732
903
|
}
|
|
733
|
-
//# sourceMappingURL=migration-core.js.map
|
package/dist/provider-support.js
CHANGED
package/dist/pull.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|