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