@an5/orm 1.3.2 → 1.3.4

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.
Files changed (62) hide show
  1. package/README.md +74 -11
  2. package/dist/cleanup.d.ts +1 -0
  3. package/dist/cleanup.js +177 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +147 -0
  6. package/dist/generator/src/attributes.d.ts +57 -0
  7. package/dist/generator/src/attributes.js +225 -0
  8. package/dist/generator/src/code-generator.js +49 -16
  9. package/dist/generator/src/config.d.ts +2 -2
  10. package/dist/generator/src/config.js +4 -3
  11. package/dist/generator/src/dialect.d.ts +52 -1
  12. package/dist/generator/src/dialect.js +136 -3
  13. package/dist/generator/src/dotnet-generator.js +67 -23
  14. package/dist/generator/src/golang-generator.d.ts +8 -0
  15. package/dist/generator/src/golang-generator.js +86 -21
  16. package/dist/generator/src/java-generator.d.ts +8 -0
  17. package/dist/generator/src/java-generator.js +108 -3
  18. package/dist/generator/src/kotlin-generator.js +51 -0
  19. package/dist/generator/src/parser.d.ts +1 -0
  20. package/dist/generator/src/parser.js +90 -35
  21. package/dist/generator/src/pull-schema.d.ts +54 -0
  22. package/dist/generator/src/pull-schema.js +116 -0
  23. package/dist/generator/src/push-apply.d.ts +4 -2
  24. package/dist/generator/src/push-apply.js +53 -4
  25. package/dist/generator/src/push-schema.d.ts +21 -9
  26. package/dist/generator/src/push-schema.js +132 -32
  27. package/dist/generator/src/python-generator.js +35 -22
  28. package/dist/generator/src/rust-generator.js +79 -74
  29. package/dist/generator/src/swift-generator.js +54 -3
  30. package/dist/generator/src/types.d.ts +10 -0
  31. package/dist/generator/src/view-order.d.ts +33 -0
  32. package/dist/generator/src/view-order.js +123 -0
  33. package/dist/migrate.d.ts +12 -0
  34. package/dist/migrate.js +442 -0
  35. package/dist/migration-core.d.ts +32 -3
  36. package/dist/migration-core.js +213 -43
  37. package/dist/provider-support.d.ts +0 -1
  38. package/dist/provider-support.js +0 -1
  39. package/dist/pull.d.ts +1 -0
  40. package/dist/pull.js +425 -0
  41. package/dist/push.d.ts +1 -0
  42. package/dist/push.js +93 -0
  43. package/dist/seed.d.ts +1 -0
  44. package/dist/seed.js +33 -0
  45. package/generator/src/attributes.ts +231 -0
  46. package/generator/src/code-generator.ts +47 -16
  47. package/generator/src/config.ts +5 -3
  48. package/generator/src/dialect.ts +188 -5
  49. package/generator/src/dotnet-generator.ts +68 -24
  50. package/generator/src/golang-generator.ts +89 -22
  51. package/generator/src/java-generator.ts +109 -3
  52. package/generator/src/kotlin-generator.ts +51 -0
  53. package/generator/src/parser.ts +102 -30
  54. package/generator/src/pull-schema.ts +127 -0
  55. package/generator/src/push-apply.ts +62 -5
  56. package/generator/src/push-schema.ts +160 -32
  57. package/generator/src/python-generator.ts +33 -20
  58. package/generator/src/rust-generator.ts +79 -73
  59. package/generator/src/swift-generator.ts +55 -4
  60. package/generator/src/types.ts +10 -0
  61. package/generator/src/view-order.ts +133 -0
  62. package/package.json +19 -14
@@ -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 match = line.match(new RegExp(`${directive}\\(\\[([\\w,\\s]+)\\]([^)]*)\\)`));
84
- if (!match)
111
+ const parsed = (0, attributes_1.parseIndexDirective)((0, attributes_1.attributeArgs)(line, directive));
112
+ if (!parsed)
85
113
  return null;
86
- const fields = (match[1] ?? '').split(',').map(field => field.trim()).filter(Boolean);
87
- const nameMatch = match[2]?.match(/\bmap\s*:\s*"([^"]+)"/);
88
- const includeMatch = match[2]?.match(/\binclude\s*:\s*\[([\w,\s]+)\]/);
89
- const filterMatch = match[2]?.match(/\bfilter\s*:\s*"([^"]+)"/);
90
- const optionsMatch = match[2]?.match(/\boptions\s*:\s*"([^"]+)"/);
91
- const definition = { fields };
92
- if (nameMatch?.[1])
93
- definition.name = nameMatch[1];
94
- if (includeMatch?.[1])
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 = text.split('\n');
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 m = line.match(/@@map\("(.+)"\)/);
305
- if (m?.[1])
306
- current.tableName = m[1];
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 m = line.match(/@@schema\("(.*)"\)/);
315
- if (m !== null)
316
- schemas.set(current, m[1]?.trim() ?? '');
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
- modelRefs.push({ model: current.name, field: head.name, type: head.type });
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: line.includes('@id'),
345
- isUnique: line.includes('@unique'),
346
- uniqueName: line.match(/@unique\([^)]*\bmap\s*:\s*"([^"]+)"/)?.[1],
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: line.match(/@default\((.*)\)/)?.[1]
351
- ?? (line.includes('@updatedAt') ? 'now()' : undefined),
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 (!modelNames.includes(ref.type)) {
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, modelNames),
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
- if (!f.isOptional && !f.defaultValue && !f.isId)
382
- def += ' NOT NULL';
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 += f.uniqueName ? ` CONSTRAINT [${f.uniqueName}] UNIQUE` : ' UNIQUE';
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 += f.uniqueName ? ` CONSTRAINT [${f.uniqueName}] UNIQUE` : ' UNIQUE';
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(schemaModels.map(m => tableIdentityName(m.tableName)));
553
- for (const model of schemaModels) {
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 schemaModels) {
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
- buildIndexDiff(model, await introspectArtifacts(model.tableName), ops);
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
@@ -24,4 +24,3 @@ export declare const SQL_SERVER_ONLY_COMMANDS: Record<string, string>;
24
24
  * thing it exists to say.
25
25
  */
26
26
  export declare function sqlServerOnlyError(provider: Provider, command: string): string | null;
27
- //# sourceMappingURL=provider-support.d.ts.map
@@ -47,4 +47,3 @@ function sqlServerOnlyError(provider, command) {
47
47
  ' Use a SQL Server connection string, or generate and apply the schema with ' +
48
48
  `${field_types_1.PROVIDER_LABELS[provider]}'s own tooling.`);
49
49
  }
50
- //# sourceMappingURL=provider-support.js.map
package/dist/pull.d.ts ADDED
@@ -0,0 +1 @@
1
+ import "dotenv/config";