@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,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { Model, Field, Relation } from './types';
4
+ import { attributeArgs, hasAttribute, parseIndexDirective, splitSchemaLines, unquote } from './attributes';
4
5
  import {
5
6
  DEFAULT_PROVIDER,
6
7
  FieldTypeError,
@@ -37,10 +38,25 @@ interface PendingModelRef {
37
38
  line: string;
38
39
  }
39
40
 
41
+ /**
42
+ * A field inside a `view` whose type matched nothing.
43
+ *
44
+ * A view's fields describe the shape of a result set, never a relation, so the
45
+ * decision made for models — "a name used by another block is a relation" — does
46
+ * not apply. The reference is kept until every block has been read so the error
47
+ * can still suggest a close model name.
48
+ */
49
+ interface PendingViewRef {
50
+ view: string;
51
+ field: string;
52
+ type: string;
53
+ }
54
+
40
55
  export class SchemaParser {
41
56
  private schemaText: string = '';
42
57
  private issues: FieldTypeIssue[] = [];
43
58
  private modelRefs: PendingModelRef[] = [];
59
+ private viewRefs: PendingViewRef[] = [];
44
60
  private models: Model[] = [];
45
61
 
46
62
  /**
@@ -55,9 +71,10 @@ export class SchemaParser {
55
71
  public async parse(): Promise<Model[]> {
56
72
  this.issues = [];
57
73
  this.modelRefs = [];
74
+ this.viewRefs = [];
58
75
  this.models = [];
59
76
  this.loadSchema();
60
- const lines = this.schemaText.split('\n');
77
+ const lines = splitSchemaLines(this.schemaText);
61
78
  const models: Model[] = [];
62
79
  let currentModel: Model | null = null;
63
80
 
@@ -84,6 +101,22 @@ export class SchemaParser {
84
101
  continue;
85
102
  }
86
103
 
104
+ const viewHeaderMatch = line.match(/^view\s+(\w+)\s*\{/);
105
+ if (viewHeaderMatch) {
106
+ const viewName = viewHeaderMatch[1];
107
+ currentModel = {
108
+ name: viewName,
109
+ tableName: viewName.toLowerCase() + 's',
110
+ schemaName: this.provider === 'mssql' ? 'dbo' : '',
111
+ provider: this.provider,
112
+ fields: [],
113
+ relations: [],
114
+ isView: true,
115
+ };
116
+ models.push(currentModel);
117
+ continue;
118
+ }
119
+
87
120
  if (line === '}') {
88
121
  currentModel = null;
89
122
  continue;
@@ -97,6 +130,12 @@ export class SchemaParser {
97
130
  this.models = models;
98
131
  this.resolveModelRefs();
99
132
 
133
+ const missingSql = models.filter((model) => model.isView && !model.viewSql?.trim());
134
+ if (missingSql.length > 0) {
135
+ const names = missingSql.map((model) => `"${model.name}"`).join(', ');
136
+ throw new Error(`View ${names} must declare @@sql("SELECT ...").`);
137
+ }
138
+
100
139
  // Every bad type is reported at once, so one pass over the schema is enough
101
140
  // instead of re-running the generator after every line.
102
141
  if (this.issues.length > 0) {
@@ -119,32 +158,42 @@ export class SchemaParser {
119
158
  }
120
159
 
121
160
  private parseModelLine(line: string, model: Model) {
161
+ if (line.startsWith('@@sql') || line.startsWith('@@definition')) {
162
+ // `@@definition` is accepted as an alias: the directive names a SQL body,
163
+ // and both spellings are unambiguous.
164
+ const attribute = line.startsWith('@@sql') ? '@@sql' : '@@definition';
165
+ if (model.isView) {
166
+ const sql = unquote(attributeArgs(line, attribute));
167
+ if (sql !== undefined) model.viewSql = sql.trim();
168
+ }
169
+ return;
170
+ }
122
171
  if (line.startsWith('@@map')) {
123
- const mapMatch = line.match(/@@map\("(.+)"\)/);
124
- if (mapMatch) model.tableName = mapMatch[1];
172
+ const mapped = unquote(attributeArgs(line, '@@map'));
173
+ if (mapped) model.tableName = mapped;
125
174
  return;
126
175
  }
127
176
  if (line.startsWith('@@schema')) {
128
- // `(.*)` rather than `(.+)`: `@@schema("")` is how a model says "no
129
- // schema", needed for databases that have no schema concept. With `(.+)`
130
- // the empty string does not match, the directive is silently ignored and
131
- // the model keeps `dbo`.
132
- const schemaMatch = line.match(/@@schema\("(.*)"\)/);
133
- if (schemaMatch) model.schemaName = schemaMatch[1].trim();
177
+ // An empty string is meaningful: `@@schema("")` says "no schema", needed
178
+ // for databases that have no schema concept. It must not be dropped as if
179
+ // the directive were absent.
180
+ const declared = unquote(attributeArgs(line, '@@schema'));
181
+ if (declared !== undefined) model.schemaName = declared.trim();
134
182
  return;
135
183
  }
136
184
  if (line.startsWith('@@unique')) {
137
- const uniqueMatch = line.match(/@@unique\(\[([\w,\s]+)\]\)/);
138
- if (uniqueMatch) {
139
- const fields = uniqueMatch[1].split(',').map(f => f.trim());
185
+ // Constraints describe stored tables; a view's shape comes from its SQL.
186
+ if (model.isView) return;
187
+ const parsed = parseIndexDirective(attributeArgs(line, '@@unique'));
188
+ if (parsed) {
140
189
  model.compoundUniques = model.compoundUniques || [];
141
- model.compoundUniques.push(fields);
190
+ model.compoundUniques.push(parsed.fields);
142
191
  }
143
192
  return;
144
193
  }
145
194
  if (line.startsWith('@@description')) {
146
- const descMatch = line.match(/@@description\("(.+)"\)/);
147
- if (descMatch) model.description = descMatch[1];
195
+ const description = unquote(attributeArgs(line, '@@description'));
196
+ if (description !== undefined) model.description = description;
148
197
  return;
149
198
  }
150
199
  if (line.startsWith('@@')) return;
@@ -158,16 +207,19 @@ export class SchemaParser {
158
207
  // later at the database layer, in a message unrelated to the schema.
159
208
  const resolved = resolveFieldType(head.type, this.provider);
160
209
  if (!resolved) {
210
+ if (model.isView) {
211
+ // A view's fields are columns of a result set, never relations.
212
+ this.viewRefs.push({ view: model.name, field: head.name, type: head.type });
213
+ return;
214
+ }
161
215
  this.modelRefs.push({ model, name: head.name, type: head.type, isArray: head.isArray, isOptional: head.isOptional, line });
162
216
  return;
163
217
  }
164
218
 
165
- const hasDefault = line.includes('@default') || line.includes('@updatedAt') || line.includes('@id');
166
- const isId = line.includes('@id');
167
- let description: string | undefined;
168
- const descMatch = line.match(/@description\("(.+)"\)/);
169
- if (descMatch) description = descMatch[1];
170
- model.fields.push({ name: head.name, type: resolved.ts, sqlType: head.type, isOptional: head.isOptional, hasDefault, isId, description });
219
+ const hasDefault = hasAttribute(line, '@default') || hasAttribute(line, '@updatedAt') || hasAttribute(line, '@id');
220
+ const isId = hasAttribute(line, '@id');
221
+ const description = unquote(attributeArgs(line, '@description'));
222
+ model.fields.push({ name: head.name, type: resolved.ts, sqlType: head.type, isOptional: head.isOptional, hasDefault, isId, ...(description !== undefined ? { description } : {}) });
171
223
  }
172
224
 
173
225
  /**
@@ -182,25 +234,45 @@ export class SchemaParser {
182
234
  */
183
235
  private resolveModelRefs() {
184
236
  const modelNames = this.models.map((model) => model.name);
237
+ const tableNames = this.models.filter((model) => !model.isView).map((model) => model.name);
238
+ const viewNames = new Set(this.models.filter((model) => model.isView).map((model) => model.name));
239
+
240
+ for (const ref of this.viewRefs) {
241
+ this.issues.push({
242
+ path: `${ref.view}.${ref.field}`,
243
+ message: unknownFieldTypeMessage(ref.type, this.provider, modelNames),
244
+ });
245
+ }
246
+ this.viewRefs = [];
185
247
 
186
248
  for (const ref of this.modelRefs) {
187
- if (!modelNames.includes(ref.type)) {
249
+ if (viewNames.has(ref.type)) {
250
+ // A relation carries a foreign key and generated write inputs. A view has
251
+ // neither, and an `include` of it would have no CreateInput to reference.
252
+ this.issues.push({
253
+ path: `${ref.model.name}.${ref.name}`,
254
+ message: `"${ref.type}" is a view and cannot be the target of a relation; select from it directly instead`,
255
+ });
256
+ continue;
257
+ }
258
+ if (!tableNames.includes(ref.type)) {
188
259
  this.issues.push({
189
260
  path: `${ref.model.name}.${ref.name}`,
190
- message: unknownFieldTypeMessage(ref.type, this.provider, modelNames),
261
+ message: unknownFieldTypeMessage(ref.type, this.provider, tableNames),
191
262
  });
192
263
  continue;
193
264
  }
194
265
 
195
266
  let foreignKey = '', localKey = '', relationName = '';
196
- const nameMatch = ref.line.match(/@relation\("(\w+)"/);
197
- if (nameMatch) relationName = nameMatch[1];
198
- const relationMatch = ref.line.match(/@relation\((?:.*fields:\s*\[(\w+)\],)?\s*(?:.*references:\s*\[(\w+)\],?)?.*\)/);
199
- if (relationMatch) {
200
- foreignKey = relationMatch[1] || '';
201
- localKey = relationMatch[2] || '';
267
+ const relationArgs = attributeArgs(ref.line, '@relation');
268
+ if (relationArgs) {
269
+ // `@relation("name", fields: [userId], references: [id])`; the name is
270
+ // either positional or spelled `name:`.
271
+ relationName = relationArgs.match(/^\s*(?:\w+\s*:\s*)?"([^"]+)"/)?.[1] ?? '';
272
+ foreignKey = (relationArgs.match(/\bfields\s*:\s*\[([^\]]*)\]/)?.[1] ?? '').split(',')[0]?.trim() ?? '';
273
+ localKey = (relationArgs.match(/\breferences\s*:\s*\[([^\]]*)\]/)?.[1] ?? '').split(',')[0]?.trim() ?? '';
202
274
  }
203
- const description = ref.line.match(/@description\("(.+)"\)/)?.[1];
275
+ const description = unquote(attributeArgs(ref.line, '@description'));
204
276
  ref.model.relations.push({
205
277
  name: ref.name,
206
278
  type: ref.type,
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Pure helpers for `db:pull`.
3
+ *
4
+ * Reading SQL Server catalogs and writing schema files lives in `pull.ts`, which
5
+ * starts a connection as soon as it is imported. The decisions it makes about the
6
+ * text — which block to replace, how a view becomes a `view` block — are here so
7
+ * they can be tested without a database.
8
+ */
9
+ import { viewSqlBody } from './dialect';
10
+
11
+ /** One column as `sys.columns`/`sys.types` describe it. */
12
+ export interface PulledColumn {
13
+ columnName: string;
14
+ dataType: string;
15
+ maxLength: number;
16
+ precision: number;
17
+ scale: number;
18
+ isNullable: boolean | number;
19
+ }
20
+
21
+ /**
22
+ * The schema type of an introspected SQL Server column.
23
+ *
24
+ * `max_length` is bytes, so the character types divide it by two; `-1` is `MAX`.
25
+ * Tables and views describe their columns the same way, so they share this.
26
+ */
27
+ export function sqlServerTypeForColumn(col: PulledColumn): string {
28
+ let sqlType = col.dataType.toUpperCase();
29
+ const dt = col.dataType.toLowerCase();
30
+
31
+ if (dt === 'nvarchar' || dt === 'varchar') {
32
+ sqlType = col.maxLength === -1 ? `${sqlType.toUpperCase()}(MAX)` : `${sqlType.toUpperCase()}(${col.maxLength / 2})`;
33
+ } else if (dt === 'char' || dt === 'nchar') {
34
+ sqlType = `${sqlType.toUpperCase()}(${col.maxLength / 2})`;
35
+ } else if (dt === 'varbinary' || dt === 'binary') {
36
+ sqlType = col.maxLength === -1 ? `${sqlType.toUpperCase()}(MAX)` : `${sqlType.toUpperCase()}(${col.maxLength})`;
37
+ } else if (dt === 'decimal' || dt === 'numeric') {
38
+ sqlType = `${sqlType.toUpperCase()}(${col.precision}, ${col.scale})`;
39
+ }
40
+
41
+ return sqlType;
42
+ }
43
+
44
+ /**
45
+ * Where `keyword Name { ... }` starts and ends in a schema file.
46
+ *
47
+ * The closing brace is the one on its own line, not the first `}` anywhere: a
48
+ * view's SQL may contain one (`WHERE name <> '}'`) and a regex that stops there
49
+ * would truncate the block.
50
+ */
51
+ export function blockRange(text: string, keyword: 'model' | 'view', name: string): { start: number; end: number } | null {
52
+ const header = new RegExp(`(^|\\n)[ \\t]*${keyword}\\s+${name}\\s*\\{`);
53
+ const match = header.exec(text);
54
+ if (!match) return null;
55
+
56
+ const start = match.index + (match[1]?.length ?? 0);
57
+ const bodyStart = match.index + match[0].length;
58
+ const close = /(^|\n)[ \t]*\}/.exec(text.slice(bodyStart));
59
+ if (!close) return null;
60
+
61
+ return { start, end: bodyStart + close.index + close[0].length };
62
+ }
63
+
64
+ /** The additional key a schema block is known by, e.g. `reporting.active_users`. */
65
+ export function qualifiedBlockKey(name: string, schemaName: string | undefined): string {
66
+ return !schemaName || schemaName.toLowerCase() === 'dbo' ? name : `${schemaName}.${name}`;
67
+ }
68
+
69
+ /**
70
+ * A likely block name for an introspected object.
71
+ *
72
+ * `db:pull` names a new block after the table or view, dropping the plural `s`
73
+ * so `users` becomes `User`. A word that already ends in `s` but is singular
74
+ * (`status`, `address`, `analysis`) must keep it: stripping the character turns
75
+ * it into `Statu`, which reads like a typo in every generated client. Existing
76
+ * blocks keep their own name, so this only decides what a newly pulled object is
77
+ * called.
78
+ */
79
+ export function blockNameForObject(objectName: string): string {
80
+ // `active_users` becomes `ActiveUser`, not `Active_user`: the block name is a
81
+ // generated type in every client, so it follows the schema's own casing.
82
+ const words = objectName.split(/[^A-Za-z0-9]+/).filter(Boolean);
83
+ const capitalized = (words.length > 0 ? words : [objectName])
84
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
85
+ .join('');
86
+ // `addresses`/`classes` lose `es`; `status`/`address`/`analysis` are singular
87
+ // and keep the `s`.
88
+ if (/sses$/i.test(capitalized)) return capitalized.slice(0, -2);
89
+ if (/(?:ss|us|is)$/i.test(capitalized)) return capitalized;
90
+ if (/s$/i.test(capitalized)) return capitalized.slice(0, -1);
91
+ return capitalized;
92
+ }
93
+
94
+ /**
95
+ * A `view` block for a catalog view.
96
+ *
97
+ * `@@sql` gets the body, not the `CREATE VIEW` statement the catalog returns:
98
+ * push and migrate add the `CREATE VIEW` themselves, so storing the header would
99
+ * produce `CREATE OR REPLACE VIEW x AS CREATE VIEW ...`.
100
+ */
101
+ export function buildViewBlock(input: {
102
+ modelName: string;
103
+ viewName: string;
104
+ schemaName: string;
105
+ columns: readonly PulledColumn[];
106
+ definition: string | null | undefined;
107
+ }): string | null {
108
+ const body = viewSqlBody(input.definition);
109
+ if (!body) return null;
110
+
111
+ const lines = [`view ${input.modelName} {`];
112
+ for (const col of input.columns) {
113
+ lines.push(` ${col.columnName} ${sqlServerTypeForColumn(col)}${col.isNullable ? '?' : ''}`);
114
+ }
115
+
116
+ if (input.viewName !== input.modelName.toLowerCase() + 's') {
117
+ lines.push('', ` @@map("${input.viewName}")`);
118
+ }
119
+ if (input.schemaName && input.schemaName.toLowerCase() !== 'dbo') {
120
+ lines.push(` @@schema("${input.schemaName}")`);
121
+ }
122
+
123
+ lines.push('', ' @@sql("""');
124
+ for (const line of body.split(/\r?\n/)) lines.push(` ${line}`);
125
+ lines.push(' """)', '}');
126
+ return lines.join('\n');
127
+ }
@@ -8,8 +8,17 @@
8
8
  * The database is two functions rather than the adapter, which keeps this testable
9
9
  * and keeps the driver out of the decision making.
10
10
  */
11
- import { PushDialect } from './dialect';
12
- import { PushIndex, PushModel, directiveName, qualifiedTableName, safeIdentifierName } from './push-schema';
11
+ import { PushDialect, normalizeViewSql } from './dialect';
12
+ import {
13
+ PushIndex,
14
+ PushModel,
15
+ PushView,
16
+ directiveName,
17
+ qualifiedTableName,
18
+ qualifiedViewName,
19
+ safeIdentifierName,
20
+ } from './push-schema';
21
+ import { orderViewsByDependency } from './view-order';
13
22
 
14
23
  /** The two things this needs from a database connection. */
15
24
  export interface PushDatabase {
@@ -28,6 +37,8 @@ export interface ApplyResult {
28
37
  columnsAdded: number;
29
38
  indexesCreated: number;
30
39
  uniqueConstraintsAdded: number;
40
+ /** How many views were created or replaced. */
41
+ viewsApplied: number;
31
42
  }
32
43
 
33
44
  /**
@@ -41,6 +52,7 @@ export async function applySchema(
41
52
  models: readonly PushModel[],
42
53
  dialect: PushDialect,
43
54
  db: PushDatabase,
55
+ views: readonly PushView[] = [],
44
56
  ): Promise<ApplyResult> {
45
57
  const result: ApplyResult = {
46
58
  tablesCreated: 0,
@@ -48,6 +60,7 @@ export async function applySchema(
48
60
  columnsAdded: 0,
49
61
  indexesCreated: 0,
50
62
  uniqueConstraintsAdded: 0,
63
+ viewsApplied: 0,
51
64
  };
52
65
 
53
66
  for (const model of models) {
@@ -89,7 +102,9 @@ export async function applySchema(
89
102
  // A new `@unique` column needs its unique too, and on SQLite that cannot
90
103
  // ride along with the ADD COLUMN — SQLite refuses to add a UNIQUE column.
91
104
  if (field.isUnique && !field.isId && !dialect.supportsUniqueInlineOnAddColumn) {
92
- const indexName = `UQ_${safeName}_${field.name}`;
105
+ // The same name `db:migrate` expects for this column's unique, so the
106
+ // next diff sees push's artifact instead of adding another one.
107
+ const indexName = field.uniqueName ?? `UQ_${safeName}_${field.name}`;
93
108
  const quoted = dialect.quote(indexName);
94
109
  if ((await db.query(dialect.uniqueConstraintExists(tableName, indexName))).length === 0) {
95
110
  db.log(`Creating unique index ${quoted} on table ${sqlTableName}...`);
@@ -112,7 +127,6 @@ export async function applySchema(
112
127
  db.log(`Creating unique index ${quoted} on table ${sqlTableName}...`);
113
128
  await db.execute(dialect.createUniqueIndex(quoted, sqlTableName, quoteFields(entry.fields)));
114
129
  result.indexesCreated += 1;
115
- result.uniqueConstraintsAdded += 1;
116
130
  }
117
131
  result.uniqueConstraintsAdded += 1;
118
132
  }
@@ -122,11 +136,54 @@ export async function applySchema(
122
136
  if ((await db.query(dialect.indexExists(tableName, name))).length > 0) continue;
123
137
  const quoted = dialect.quote(name);
124
138
  db.log(`Creating index ${quoted} on table ${sqlTableName}...`);
125
- await db.execute(dialect.createIndex(quoted, sqlTableName, quoteFields(directive.fields)));
139
+ await db.execute(
140
+ dialect.createIndex(quoted, sqlTableName, quoteFields(directive.fields), {
141
+ includeFields: directive.includeFields ? quoteFields(directive.includeFields) : undefined,
142
+ filter: directive.filter,
143
+ options: directive.options,
144
+ }),
145
+ );
126
146
  result.indexesCreated += 1;
127
147
  }
128
148
  }
129
149
 
150
+ // Views are applied after every table: a view may select from a table this same
151
+ // push created, and SQL Server validates the reference while compiling it.
152
+ // Within the views, a dependent view has to come after the one it reads.
153
+ const orderedViews = orderViewsByDependency(
154
+ views.map((view) => ({
155
+ view,
156
+ names: [qualifiedViewName(view), view.viewName],
157
+ sql: view.sql,
158
+ })),
159
+ );
160
+ if (orderedViews.cyclic) {
161
+ // The definitions cannot all exist at once; the database reports which one
162
+ // is missing, which is more useful than a generic ordering error here.
163
+ db.log('Warning: view definitions reference each other in a cycle; keeping schema order.');
164
+ }
165
+
166
+ for (const { view } of orderedViews.ordered) {
167
+ const viewName = qualifiedViewName(view);
168
+ const sqlViewName = dialect.quoteTable(viewName);
169
+ db.log(`Processing view ${sqlViewName}...`);
170
+
171
+ const rows = await db.query(dialect.viewDefinition(viewName));
172
+ const existing = rows[0] as { definition?: string | null } | undefined;
173
+ if (existing && normalizeViewSql(existing.definition) === normalizeViewSql(view.sql)) {
174
+ // The catalog and the schema agree; recreating would just churn dependencies.
175
+ continue;
176
+ }
177
+
178
+ if (existing && !dialect.supportsCreateOrReplaceView) {
179
+ db.log(`Dropping view ${sqlViewName} before recreating it...`);
180
+ await db.execute(dialect.dropView(sqlViewName));
181
+ }
182
+ db.log(`Creating view ${sqlViewName}...`);
183
+ await db.execute(dialect.createView(sqlViewName, view.sql));
184
+ result.viewsApplied += 1;
185
+ }
186
+
130
187
  return result;
131
188
  }
132
189