@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
|
@@ -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
|
|
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
|
|
95
|
-
if (
|
|
96
|
-
model.tableName =
|
|
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
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// the
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
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
|
|
120
|
-
if (
|
|
121
|
-
model.description =
|
|
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 =
|
|
139
|
-
const isId =
|
|
140
|
-
|
|
141
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
foreignKey =
|
|
173
|
-
localKey =
|
|
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
|
|
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
|
-
|
|
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;
|