@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.
- 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 +67 -23
- package/dist/generator/src/golang-generator.d.ts +8 -0
- package/dist/generator/src/golang-generator.js +86 -21
- 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 +68 -24
- package/generator/src/golang-generator.ts +89 -22
- 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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading `@attribute(...)` arguments from a `.an5` field or model line.
|
|
3
|
+
*
|
|
4
|
+
* The schema used to be read with regexes such as `/@default\((.*)\)/`, which
|
|
5
|
+
* ran to the *last* `)` on the line. A field with `@default(now())` followed by
|
|
6
|
+
* `@description("...")` therefore had `now()) @description("..."` as its default
|
|
7
|
+
* expression, and `db:push` emitted SQL no database accepts. The closing
|
|
8
|
+
* parenthesis has to be the one that matches the opening one — parentheses nest
|
|
9
|
+
* (`now()`, `uuid()`, `lower(...)`) and quoted strings may contain both.
|
|
10
|
+
*
|
|
11
|
+
* These helpers only read attributes; they do not validate the schema language.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Finds a matching `)` for the `(` at `open`, ignoring parentheses inside quotes. */
|
|
15
|
+
function closingParenthesis(line: string, open: number): number {
|
|
16
|
+
let depth = 0;
|
|
17
|
+
let quote: string | null = null;
|
|
18
|
+
|
|
19
|
+
for (let index = open; index < line.length; index += 1) {
|
|
20
|
+
const character = line[index];
|
|
21
|
+
|
|
22
|
+
if (quote !== null) {
|
|
23
|
+
if (character === '\\') {
|
|
24
|
+
index += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (character === quote) quote = null;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A triple-quoted string may span lines and may contain either quote, so it
|
|
32
|
+
// has to be skipped as a unit before the single-quote rule below sees it.
|
|
33
|
+
if (character === '"' && line[index + 1] === '"' && line[index + 2] === '"') {
|
|
34
|
+
const end = line.indexOf('"""', index + 3);
|
|
35
|
+
if (end < 0) return -1;
|
|
36
|
+
index = end + 2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (character === '"' || character === "'") {
|
|
40
|
+
quote = character;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (character === '(') {
|
|
44
|
+
depth += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (character === ')') {
|
|
48
|
+
depth -= 1;
|
|
49
|
+
if (depth === 0) return index;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether `line` has the attribute, with or without arguments. */
|
|
57
|
+
export function hasAttribute(line: string, name: string): boolean {
|
|
58
|
+
const pattern = new RegExp(`(^|\\s)${escapeRegExp(name)}(?![\\w])`);
|
|
59
|
+
return pattern.test(line);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The raw argument text of the first `name(...)` on `line`.
|
|
64
|
+
*
|
|
65
|
+
* Returns `undefined` when the attribute is absent, or when it is written
|
|
66
|
+
* without parentheses. The returned text keeps its quotes, so callers decide
|
|
67
|
+
* whether they are reading a string, a number or an expression.
|
|
68
|
+
*/
|
|
69
|
+
export function attributeArgs(line: string, name: string): string | undefined {
|
|
70
|
+
const pattern = new RegExp(`(^|\\s)${escapeRegExp(name)}\\s*\\(`, 'g');
|
|
71
|
+
const match = pattern.exec(line);
|
|
72
|
+
if (!match) return undefined;
|
|
73
|
+
|
|
74
|
+
const open = line.indexOf('(', match.index + match[0].length - 1);
|
|
75
|
+
if (open < 0) return undefined;
|
|
76
|
+
const close = closingParenthesis(line, open);
|
|
77
|
+
if (close < 0) return undefined;
|
|
78
|
+
return line.slice(open + 1, close);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Strips one pair of surrounding double quotes, if present. */
|
|
82
|
+
export function unquote(value: string | undefined): string | undefined {
|
|
83
|
+
if (value === undefined) return undefined;
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
// Triple quotes allow a view's SQL to be written over several lines without
|
|
86
|
+
// escaping: `@@sql("""
|
|
87
|
+
// SELECT ...
|
|
88
|
+
// """)`. The common indentation is removed so the generated DDL is not
|
|
89
|
+
// needlessly indented, while meaningful inner indentation is kept.
|
|
90
|
+
//
|
|
91
|
+
// The closing `"""` ends the string, not the argument list: `)` may follow on
|
|
92
|
+
// the same line or the next one, so the body is found by searching for the
|
|
93
|
+
// closing quotes rather than assuming they sit at the very end.
|
|
94
|
+
if (trimmed.startsWith('"""')) {
|
|
95
|
+
const end = trimmed.indexOf('"""', 3);
|
|
96
|
+
if (end >= 0) {
|
|
97
|
+
const body = trimmed.slice(3, end).replace(/^\r?\n/, '').replace(/\r?\n$/, '');
|
|
98
|
+
const lines = body.split(/\r?\n/);
|
|
99
|
+
const indents = lines
|
|
100
|
+
.filter((line) => line.trim() !== '')
|
|
101
|
+
.map((line) => line.match(/^[ \t]*/)?.[0].length ?? 0);
|
|
102
|
+
const dedent = indents.length > 0 ? Math.min(...indents) : 0;
|
|
103
|
+
return lines.map((line) => line.slice(dedent)).join('\n');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
107
|
+
return trimmed.slice(1, -1);
|
|
108
|
+
}
|
|
109
|
+
return trimmed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether `line` holds a complete `@@sql("""…""")` directive.
|
|
114
|
+
*
|
|
115
|
+
* The closing `"""` and the `)` that closes the attribute need not share a line,
|
|
116
|
+
* so this asks whether the argument list is balanced *and* the body it contains
|
|
117
|
+
* has been closed. An incomplete directive is kept as a pending line instead of
|
|
118
|
+
* being handed to the parser as if the SQL ended at the newline.
|
|
119
|
+
*/
|
|
120
|
+
function isCompleteViewDirective(line: string): boolean {
|
|
121
|
+
const args = attributeArgs(line, '@@sql') ?? attributeArgs(line, '@@definition');
|
|
122
|
+
if (args === undefined) return false;
|
|
123
|
+
const trimmed = args.trim();
|
|
124
|
+
if (!trimmed.startsWith('"""')) return false;
|
|
125
|
+
return trimmed.slice(3).includes('"""');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Splits schema text into logical lines, keeping a triple-quoted `@@sql` body on
|
|
130
|
+
* one line.
|
|
131
|
+
*
|
|
132
|
+
* The parsers read a schema line by line. Without this, a multi-line view body
|
|
133
|
+
* would end the directive at its first newline and the rest of the `SELECT`
|
|
134
|
+
* would be read as fields of the view. Newlines inside the quotes are preserved;
|
|
135
|
+
* only the split points change.
|
|
136
|
+
*/
|
|
137
|
+
export function splitSchemaLines(text: string): string[] {
|
|
138
|
+
const lines: string[] = [];
|
|
139
|
+
let pending: string | null = null;
|
|
140
|
+
|
|
141
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
142
|
+
if (pending !== null) {
|
|
143
|
+
pending += `\n${rawLine}`;
|
|
144
|
+
if (isCompleteViewDirective(pending)) {
|
|
145
|
+
lines.push(pending);
|
|
146
|
+
pending = null;
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const start = /@@(?:sql|definition)\s*\(\s*"""/.exec(rawLine);
|
|
152
|
+
if (start) {
|
|
153
|
+
if (isCompleteViewDirective(rawLine)) {
|
|
154
|
+
lines.push(rawLine);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
pending = rawLine;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lines.push(rawLine);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (pending !== null) lines.push(pending);
|
|
165
|
+
return lines;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Reads a named string option from an attribute argument list, e.g.
|
|
170
|
+
* `map: "UQ_users_email"` in `@unique(map: "UQ_users_email")`.
|
|
171
|
+
*/
|
|
172
|
+
export function stringOption(args: string | undefined, key: string): string | undefined {
|
|
173
|
+
if (!args) return undefined;
|
|
174
|
+
const pattern = new RegExp(`\\b${escapeRegExp(key)}\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`);
|
|
175
|
+
return pattern.exec(args)?.[1];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Reads a named field-list option, e.g. `include: [email, tenantId]`. */
|
|
179
|
+
export function fieldListOption(args: string | undefined, key: string): string[] | undefined {
|
|
180
|
+
if (!args) return undefined;
|
|
181
|
+
const pattern = new RegExp(`\\b${escapeRegExp(key)}\\s*:\\s*\\[([^\\]]*)\\]`);
|
|
182
|
+
const value = pattern.exec(args)?.[1];
|
|
183
|
+
if (value === undefined) return undefined;
|
|
184
|
+
return value.split(',').map((field) => field.trim()).filter(Boolean);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One `@@unique` or `@@index` directive, with the options it accepts. */
|
|
188
|
+
export interface IndexDirective {
|
|
189
|
+
fields: string[];
|
|
190
|
+
name?: string;
|
|
191
|
+
includeFields?: string[];
|
|
192
|
+
filter?: string;
|
|
193
|
+
options?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parses the inside of `@@unique([a, b], map: "…", include: […], …)`.
|
|
198
|
+
*
|
|
199
|
+
* The directive is the one place a schema can carry raw SQL (`filter`,
|
|
200
|
+
* `options`), so its argument list is balanced rather than cut at the first
|
|
201
|
+
* `)`. A filter such as `(deletedAt IS NULL)` used to truncate the directive.
|
|
202
|
+
*/
|
|
203
|
+
export function parseIndexDirective(args: string | undefined): IndexDirective | null {
|
|
204
|
+
if (!args) return null;
|
|
205
|
+
|
|
206
|
+
const fieldsMatch = args.match(/^\s*\[([\w\s,]*)\]/);
|
|
207
|
+
if (!fieldsMatch) return null;
|
|
208
|
+
const fields = (fieldsMatch[1] ?? '').split(',').map((field) => field.trim()).filter(Boolean);
|
|
209
|
+
if (fields.length === 0) return null;
|
|
210
|
+
|
|
211
|
+
const rest = args.slice(fieldsMatch[0].length);
|
|
212
|
+
const directive: IndexDirective = { fields };
|
|
213
|
+
|
|
214
|
+
const name = stringOption(rest, 'map');
|
|
215
|
+
if (name) directive.name = name;
|
|
216
|
+
|
|
217
|
+
const includeFields = fieldListOption(rest, 'include');
|
|
218
|
+
if (includeFields && includeFields.length > 0) directive.includeFields = includeFields;
|
|
219
|
+
|
|
220
|
+
const filter = stringOption(rest, 'filter');
|
|
221
|
+
if (filter !== undefined) directive.filter = filter;
|
|
222
|
+
|
|
223
|
+
const options = stringOption(rest, 'options');
|
|
224
|
+
if (options !== undefined) directive.options = options;
|
|
225
|
+
|
|
226
|
+
return directive;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function escapeRegExp(value: string): string {
|
|
230
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
231
|
+
}
|
|
@@ -60,12 +60,28 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
|
|
|
60
60
|
groupBy(args: GroupByArgs): Promise<any[]>;
|
|
61
61
|
upsert(args: UpsertArgs): Promise<T>;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A read-only client for a view block.
|
|
66
|
+
*
|
|
67
|
+
* The same query surface as TableClient without any write method, matching the
|
|
68
|
+
* runtime ViewClient in @an5/adapters.
|
|
69
|
+
*/
|
|
70
|
+
export interface ViewClient<T, WhereInput = any, _Select = any, Include = any, FindManyArgs = any, FindFirstArgs = any, FindUniqueArgs = any, AggregateArgs = any, GroupByArgs = any> {
|
|
71
|
+
findMany(args?: FindManyArgs): Promise<T[]>;
|
|
72
|
+
vectorSearch(args: { vector: number[]; take?: number; where?: WhereInput; include?: Include; vectorField?: string; distanceMetric?: 'cosine' | 'euclidean' | 'dot'; }): Promise<(T & { distance: number })[]>;
|
|
73
|
+
findFirst(args?: FindFirstArgs): Promise<T | null>;
|
|
74
|
+
findUnique(args?: FindUniqueArgs): Promise<T | null>;
|
|
75
|
+
count(args?: { where?: WhereInput; }): Promise<number>;
|
|
76
|
+
aggregate(args: AggregateArgs): Promise<any>;
|
|
77
|
+
groupBy(args: GroupByArgs): Promise<any[]>;
|
|
78
|
+
}
|
|
63
79
|
`;
|
|
64
80
|
fs.writeFileSync(path.join(this.outputDir, 'base.ts'), baseTs);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
private generateModelFile(model: Model) {
|
|
68
|
-
let content = `import { An5, TableClient } from './base';\n`;
|
|
84
|
+
let content = `import { An5, ${model.isView ? 'ViewClient' : 'TableClient'} } from './base';\n`;
|
|
69
85
|
const relModels = [...new Set(model.relations.map(r => r.type))];
|
|
70
86
|
for (const relModel of relModels) {
|
|
71
87
|
if (relModel !== model.name) {
|
|
@@ -121,17 +137,21 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
|
|
|
121
137
|
}
|
|
122
138
|
content += `export type ${model.name}Include = { ${includeRels.join('; ')}${includeRels.length > 0 ? ';' : ''} };\n`;
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
if (!model.isView) {
|
|
141
|
+
content += `export type ${model.name}CreateInput = { ${model.fields.map(f => `${f.name}${f.isOptional || f.hasDefault ? '?' : ''}: ${this.inputType(f.type)}${f.isOptional ? ' | null' : ''}`).join('; ')}; ${model.relations.map(r => `${r.name}?: { create?: ${r.type}CreateInput | ${r.type}CreateInput[]; connect?: ${r.type}WhereInput | ${r.type}WhereInput[]; }`).join('; ')} };\n`;
|
|
142
|
+
content += `export type ${model.name}UpdateInput = { ${model.fields.filter(f => !f.isId).map(f => `${f.name}?: ${f.type === 'number' ? 'number | An5.IntFieldUpdateOperationsInput' : this.inputType(f.type)}${f.isOptional ? ' | null' : ''}`).join('; ')}; ${model.relations.map(r => `${r.name}?: { create?: ${r.type}CreateInput | ${r.type}CreateInput[]; connect?: ${r.type}WhereInput | ${r.type}WhereInput[]; set?: ${r.type}WhereInput | ${r.type}WhereInput[]; disconnect?: ${r.type}WhereInput | ${r.type}WhereInput[]; delete?: ${r.type}WhereInput | ${r.type}WhereInput[]; update?: { where: ${r.type}WhereInput; data: ${r.type}UpdateInput; } | { where: ${r.type}WhereInput; data: ${r.type}UpdateInput; }[]; upsert?: { where: ${r.type}WhereInput; create: ${r.type}CreateInput; update: ${r.type}UpdateInput; } | { where: ${r.type}WhereInput; create: ${r.type}CreateInput; update: ${r.type}UpdateInput; }[]; }`).join('; ')} };\n`;
|
|
143
|
+
}
|
|
126
144
|
|
|
127
145
|
content += `export type ${model.name}OrderByInput = { ${model.fields.map(f => `${f.name}?: An5.SortOrder`).join('; ')} };\n`;
|
|
128
146
|
content += `export type ${model.name}FindManyArgs = { where?: ${model.name}WhereInput; orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[]; take?: number; skip?: number; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
|
|
129
147
|
content += `export type ${model.name}FindFirstArgs = { where?: ${model.name}WhereInput; orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[]; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
|
|
130
148
|
content += `export type ${model.name}FindUniqueArgs = { where?: ${model.name}WhereInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
if (!model.isView) {
|
|
150
|
+
content += `export type ${model.name}CreateArgs = { data: ${model.name}CreateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
|
|
151
|
+
content += `export type ${model.name}UpdateArgs = { where: ${model.name}WhereInput; data: ${model.name}UpdateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
|
|
152
|
+
content += `export type ${model.name}UpsertArgs = { where: ${model.name}WhereInput; create: ${model.name}CreateInput; update: ${model.name}UpdateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
|
|
153
|
+
content += `export type ${model.name}DeleteArgs = { where: ${model.name}WhereInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
|
|
154
|
+
}
|
|
135
155
|
content += `export type ${model.name}ScalarFieldEnum = ${model.fields.map(f => `'${f.name}'`).join(' | ') || 'never'};\n`;
|
|
136
156
|
content += `export type ${model.name}AggregateArgs = { where?: ${model.name}WhereInput; _count?: true | { _all?: true; ${model.fields.map(f => `${f.name}?: true`).join('; ')} }; _sum?: { ${model.fields.filter(f => this.normalizeType(f.type) === 'number').map(f => `${f.name}?: true`).join('; ')} }; _avg?: { ${model.fields.filter(f => this.normalizeType(f.type) === 'number').map(f => `${f.name}?: true`).join('; ')} }; _min?: { ${model.fields.map(f => `${f.name}?: true`).join('; ')} }; _max?: { ${model.fields.map(f => `${f.name}?: true`).join('; ')} }; };\n`;
|
|
137
157
|
const numericHavingFields = model.fields.filter(f => this.normalizeType(f.type) === 'number').map(f => `${f.name}?: An5.NumberFilter | number`).join('; ');
|
|
@@ -139,7 +159,11 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
|
|
|
139
159
|
content += `export type ${model.name}AggregateHavingInput = { _count?: { _all?: An5.NumberFilter | number; ${model.fields.map(f => `${f.name}?: An5.NumberFilter | number`).join('; ')} }; _sum?: { ${numericHavingFields} }; _avg?: { ${numericHavingFields} }; _min?: { ${scalarHavingFields} }; _max?: { ${scalarHavingFields} }; };\n`;
|
|
140
160
|
content += `export type ${model.name}GroupByArgs = { by: ${model.name}ScalarFieldEnum | ${model.name}ScalarFieldEnum[]; where?: ${model.name}WhereInput; having?: ${model.name}AggregateHavingInput; orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[]; skip?: number; take?: number; _count?: true | { _all?: true; ${model.fields.map(f => `${f.name}?: true`).join('; ')} }; _sum?: { ${model.fields.filter(f => this.normalizeType(f.type) === 'number').map(f => `${f.name}?: true`).join('; ')} }; _avg?: { ${model.fields.filter(f => this.normalizeType(f.type) === 'number').map(f => `${f.name}?: true`).join('; ')} }; _min?: { ${model.fields.map(f => `${f.name}?: true`).join('; ')} }; _max?: { ${model.fields.map(f => `${f.name}?: true`).join('; ')} }; };\n`;
|
|
141
161
|
|
|
142
|
-
|
|
162
|
+
if (model.isView) {
|
|
163
|
+
content += `export type ${model.name}ViewClient = ViewClient<\n ${model.name},\n ${model.name}WhereInput,\n ${model.name}Select,\n ${model.name}Include,\n ${model.name}FindManyArgs,\n ${model.name}FindFirstArgs,\n ${model.name}FindUniqueArgs,\n ${model.name}AggregateArgs,\n ${model.name}GroupByArgs\n>;\n`;
|
|
164
|
+
} else {
|
|
165
|
+
content += `export type ${model.name}TableClient = TableClient<\n ${model.name},\n ${model.name}WhereInput,\n ${model.name}Select,\n ${model.name}Include,\n ${model.name}CreateInput,\n ${model.name}UpdateInput,\n ${model.name}FindManyArgs,\n ${model.name}FindFirstArgs,\n ${model.name}FindUniqueArgs,\n ${model.name}CreateArgs,\n ${model.name}UpdateArgs,\n ${model.name}UpsertArgs,\n ${model.name}DeleteArgs,\n ${model.name}AggregateArgs,\n ${model.name}GroupByArgs\n>;\n`;
|
|
166
|
+
}
|
|
143
167
|
|
|
144
168
|
fs.writeFileSync(path.join(this.outputDir, `${model.name}.ts`), content);
|
|
145
169
|
}
|
|
@@ -173,17 +197,23 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
|
|
|
173
197
|
content += ` export type ${model.name}WhereInput = ${model.name}Types.${model.name}WhereInput;\n`;
|
|
174
198
|
content += ` export type ${model.name}Select = ${model.name}Types.${model.name}Select;\n`;
|
|
175
199
|
content += ` export type ${model.name}Include = ${model.name}Types.${model.name}Include;\n`;
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
if (!model.isView) {
|
|
201
|
+
content += ` export type ${model.name}CreateInput = ${model.name}Types.${model.name}CreateInput;\n`;
|
|
202
|
+
content += ` export type ${model.name}UpdateInput = ${model.name}Types.${model.name}UpdateInput;\n`;
|
|
203
|
+
}
|
|
178
204
|
content += ` export type ${model.name}OrderByInput = ${model.name}Types.${model.name}OrderByInput;\n`;
|
|
179
205
|
content += ` export type ${model.name}FindManyArgs = ${model.name}Types.${model.name}FindManyArgs;\n`;
|
|
180
206
|
content += ` export type ${model.name}FindFirstArgs = ${model.name}Types.${model.name}FindFirstArgs;\n`;
|
|
181
207
|
content += ` export type ${model.name}FindUniqueArgs = ${model.name}Types.${model.name}FindUniqueArgs;\n`;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
if (!model.isView) {
|
|
209
|
+
content += ` export type ${model.name}CreateArgs = ${model.name}Types.${model.name}CreateArgs;\n`;
|
|
210
|
+
content += ` export type ${model.name}UpdateArgs = ${model.name}Types.${model.name}UpdateArgs;\n`;
|
|
211
|
+
content += ` export type ${model.name}UpsertArgs = ${model.name}Types.${model.name}UpsertArgs;\n`;
|
|
212
|
+
content += ` export type ${model.name}DeleteArgs = ${model.name}Types.${model.name}DeleteArgs;\n`;
|
|
213
|
+
content += ` export type ${model.name}TableClient = ${model.name}Types.${model.name}TableClient;\n`;
|
|
214
|
+
} else {
|
|
215
|
+
content += ` export type ${model.name}ViewClient = ${model.name}Types.${model.name}ViewClient;\n`;
|
|
216
|
+
}
|
|
187
217
|
content += ` export type ${model.name}ScalarFieldEnum = ${model.name}Types.${model.name}ScalarFieldEnum;\n`;
|
|
188
218
|
content += ` export type ${model.name}AggregateArgs = ${model.name}Types.${model.name}AggregateArgs;\n`;
|
|
189
219
|
content += ` export type ${model.name}GroupByArgs = ${model.name}Types.${model.name}GroupByArgs;\n`;
|
|
@@ -195,9 +225,10 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
|
|
|
195
225
|
const addedProps = new Set<string>();
|
|
196
226
|
for (const model of models) {
|
|
197
227
|
const props = this.getAllPropertyVariations(model.name);
|
|
228
|
+
const clientType = `${model.name}Types.${model.name}${model.isView ? 'ViewClient' : 'TableClient'}`;
|
|
198
229
|
for (const prop of props) {
|
|
199
230
|
if (!addedProps.has(prop)) {
|
|
200
|
-
content += ` ${prop}!: ${
|
|
231
|
+
content += ` ${prop}!: ${clientType};\n`;
|
|
201
232
|
addedProps.add(prop);
|
|
202
233
|
}
|
|
203
234
|
}
|
package/generator/src/config.ts
CHANGED
|
@@ -417,8 +417,8 @@ export function resolveConnectionString(
|
|
|
417
417
|
*
|
|
418
418
|
* Falls back to SQL Server when the scheme is unknown — it is the default
|
|
419
419
|
* provider, and ADO-style strings (`Server=...;Database=...`) have no scheme to
|
|
420
|
-
* read. `sqlite
|
|
421
|
-
* list `An5Adapter` uses.
|
|
420
|
+
* read. `:memory:`, any `sqlite:` URI (`sqlite://`, `sqlite::memory:`) and a bare
|
|
421
|
+
* path ending in `.sqlite`, `.sqlite3` or `.db` — the same list `An5Adapter` uses.
|
|
422
422
|
*/
|
|
423
423
|
export function detectProvider(connectionString: string | undefined | null): Provider {
|
|
424
424
|
// Lower-cased: a URI scheme is case-insensitive, so `MySQL://` is the same
|
|
@@ -430,7 +430,9 @@ export function detectProvider(connectionString: string | undefined | null): Pro
|
|
|
430
430
|
if (cs.startsWith('googlesheets://')) return 'googlesheets';
|
|
431
431
|
if (cs.startsWith('postgres://') || cs.startsWith('postgresql://')) return 'postgres';
|
|
432
432
|
if (cs.startsWith('mysql://') || cs.startsWith('mariadb://')) return 'mysql';
|
|
433
|
-
if (SQLITE_FILE_SUFFIXES.some((suffix) => cs.endsWith(suffix)))
|
|
433
|
+
if (cs === ':memory:' || cs.startsWith('sqlite:') || SQLITE_FILE_SUFFIXES.some((suffix) => cs.endsWith(suffix))) {
|
|
434
|
+
return 'sqlite';
|
|
435
|
+
}
|
|
434
436
|
return DEFAULT_PROVIDER;
|
|
435
437
|
}
|
|
436
438
|
|
package/generator/src/dialect.ts
CHANGED
|
@@ -24,7 +24,7 @@ function objectName(raw: string): string {
|
|
|
24
24
|
function splitQualified(raw: string): { schema: string | undefined; name: string } {
|
|
25
25
|
const parts = raw.replace(/[\[\]"`]/g, '').split('.').filter(Boolean);
|
|
26
26
|
if (parts.length >= 2) {
|
|
27
|
-
return { schema: parts[parts.length - 2], name: parts[parts.length - 1] };
|
|
27
|
+
return { schema: parts[parts.length - 2], name: parts[parts.length - 1]! };
|
|
28
28
|
}
|
|
29
29
|
return { schema: undefined, name: parts[0] ?? raw };
|
|
30
30
|
}
|
|
@@ -50,10 +50,25 @@ export interface ColumnSpec {
|
|
|
50
50
|
isId: boolean;
|
|
51
51
|
isUnique: boolean;
|
|
52
52
|
isOptional: boolean;
|
|
53
|
+
/** `@unique(map: "…")`: the constraint name both push and migrate must use. */
|
|
54
|
+
uniqueName?: string | undefined;
|
|
53
55
|
/** The raw `@default(...)` expression, e.g. `now()` or `"draft"`. */
|
|
54
56
|
defaultExpr?: string | undefined;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The extra clauses `@@index(..., include:, filter:, options:)` asks for.
|
|
61
|
+
*
|
|
62
|
+
* Field names are already quoted by the caller. `options` is SQL Server syntax
|
|
63
|
+
* (`WITH (FILLFACTOR = 90, ONLINE = ON)`); other providers either support the
|
|
64
|
+
* standard subset (`include`, `filter`) or ignore what they cannot express.
|
|
65
|
+
*/
|
|
66
|
+
export interface IndexOptions {
|
|
67
|
+
includeFields?: string[] | undefined;
|
|
68
|
+
filter?: string | undefined;
|
|
69
|
+
options?: string | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
export interface PushDialect {
|
|
58
73
|
readonly provider: Provider;
|
|
59
74
|
/** Whether this database can be written to with DDL at all. */
|
|
@@ -80,6 +95,16 @@ export interface PushDialect {
|
|
|
80
95
|
readonly supportsUniqueInlineOnAddColumn: boolean;
|
|
81
96
|
/** Query returning one row when the table exists. */
|
|
82
97
|
tableExists(raw: string): string;
|
|
98
|
+
/** Query returning one row when a view exists. */
|
|
99
|
+
viewExists(raw: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Query returning the stored view definition in a single `definition` column.
|
|
102
|
+
*
|
|
103
|
+
* Used to skip a push that would recreate an unchanged view. The value may be
|
|
104
|
+
* the body alone (PostgreSQL/MySQL) or the full `CREATE VIEW` text (SQL
|
|
105
|
+
* Server/SQLite); `normalizeViewSql` compares either form.
|
|
106
|
+
*/
|
|
107
|
+
viewDefinition(raw: string): string;
|
|
83
108
|
/** Query returning one row when the column exists. */
|
|
84
109
|
columnExists(raw: string, column: string): string;
|
|
85
110
|
/** Query returning one row when the unique constraint exists. */
|
|
@@ -89,7 +114,17 @@ export interface PushDialect {
|
|
|
89
114
|
/** One column definition, as it appears inside `CREATE TABLE`. */
|
|
90
115
|
columnDefinition(column: ColumnSpec): string;
|
|
91
116
|
createTable(quotedTable: string, columnDefs: string[]): string;
|
|
92
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Whether `CREATE OR REPLACE VIEW` (or `CREATE OR ALTER VIEW`) is available.
|
|
119
|
+
*
|
|
120
|
+
* SQLite has neither, so a changed view needs `DROP VIEW` first; see `dropView`.
|
|
121
|
+
*/
|
|
122
|
+
readonly supportsCreateOrReplaceView: boolean;
|
|
123
|
+
/** `CREATE [OR REPLACE|OR ALTER] VIEW ... AS <select>`. */
|
|
124
|
+
createView(quotedView: string, selectSql: string): string;
|
|
125
|
+
/** `DROP VIEW IF EXISTS`, for providers without create-or-replace. */
|
|
126
|
+
dropView(quotedView: string): string;
|
|
127
|
+
createIndex(quotedName: string, quotedTable: string, quotedFields: string[], options?: IndexOptions): string;
|
|
93
128
|
createUniqueIndex(quotedName: string, quotedTable: string, quotedFields: string[]): string;
|
|
94
129
|
/** `ALTER TABLE ... ADD <column>`, nullability included. */
|
|
95
130
|
addColumn(quotedTable: string, column: ColumnSpec): string;
|
|
@@ -154,6 +189,86 @@ function buildTableExists(provider: Provider): (raw: string) => string {
|
|
|
154
189
|
};
|
|
155
190
|
}
|
|
156
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Where a view's existence and definition are recorded.
|
|
194
|
+
*
|
|
195
|
+
* SQL Server keeps both in `sys.views`/`OBJECT_DEFINITION`; PostgreSQL and MySQL
|
|
196
|
+
* expose the body through their catalogs; SQLite stores the original statement in
|
|
197
|
+
* `sqlite_master`. The returned definition is compared with `normalizeViewSql`,
|
|
198
|
+
* which accepts either a full `CREATE VIEW` statement or a bare `SELECT`.
|
|
199
|
+
*/
|
|
200
|
+
function buildViewExists(provider: Provider): (raw: string) => string {
|
|
201
|
+
return (raw) => {
|
|
202
|
+
const { schema, name } = splitQualified(raw);
|
|
203
|
+
switch (provider) {
|
|
204
|
+
case 'mssql':
|
|
205
|
+
return `SELECT name FROM sys.views WHERE object_id = OBJECT_ID(${sqlLiteral(objectName(raw))})`;
|
|
206
|
+
case 'postgres':
|
|
207
|
+
return `SELECT viewname FROM pg_views WHERE schemaname = ${schema ? sqlLiteral(schema) : defaultSchema(provider, undefined)} AND viewname = ${sqlLiteral(name)}`;
|
|
208
|
+
case 'mysql':
|
|
209
|
+
return (
|
|
210
|
+
`SELECT table_name FROM information_schema.views WHERE table_schema = ${schema ? sqlLiteral(schema) : 'DATABASE()'} ` +
|
|
211
|
+
`AND table_name = ${sqlLiteral(name)}`
|
|
212
|
+
);
|
|
213
|
+
case 'sqlite':
|
|
214
|
+
return `SELECT name FROM sqlite_master WHERE type = 'view' AND name = ${sqlLiteral(name)}`;
|
|
215
|
+
default:
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildViewDefinition(provider: Provider): (raw: string) => string {
|
|
222
|
+
return (raw) => {
|
|
223
|
+
const { schema, name } = splitQualified(raw);
|
|
224
|
+
switch (provider) {
|
|
225
|
+
case 'mssql':
|
|
226
|
+
return `SELECT OBJECT_DEFINITION(OBJECT_ID(${sqlLiteral(objectName(raw))})) AS definition FROM sys.views WHERE object_id = OBJECT_ID(${sqlLiteral(objectName(raw))})`;
|
|
227
|
+
case 'postgres':
|
|
228
|
+
return (
|
|
229
|
+
`SELECT definition FROM pg_views WHERE schemaname = ${schema ? sqlLiteral(schema) : defaultSchema(provider, undefined)} ` +
|
|
230
|
+
`AND viewname = ${sqlLiteral(name)}`
|
|
231
|
+
);
|
|
232
|
+
case 'mysql':
|
|
233
|
+
return (
|
|
234
|
+
`SELECT view_definition AS definition FROM information_schema.views WHERE table_schema = ` +
|
|
235
|
+
`${schema ? sqlLiteral(schema) : 'DATABASE()'} AND table_name = ${sqlLiteral(name)}`
|
|
236
|
+
);
|
|
237
|
+
case 'sqlite':
|
|
238
|
+
return `SELECT sql AS definition FROM sqlite_master WHERE type = 'view' AND name = ${sqlLiteral(name)}`;
|
|
239
|
+
default:
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The `SELECT` body of a view definition.
|
|
247
|
+
*
|
|
248
|
+
* Catalogs return a full `CREATE VIEW ... AS ...` statement or the bare body,
|
|
249
|
+
* depending on the provider (and on whether `OBJECT_DEFINITION` is available).
|
|
250
|
+
* `@@sql` wants the body alone, so both forms reduce to the same text.
|
|
251
|
+
*/
|
|
252
|
+
export function viewSqlBody(sql: string | null | undefined): string {
|
|
253
|
+
if (!sql) return '';
|
|
254
|
+
let body = sql.trim();
|
|
255
|
+
const create = body.match(/^\s*CREATE\s+(?:OR\s+(?:ALTER|REPLACE)\s+)?VIEW\b[\s\S]*?\sAS\b\s*([\s\S]*)$/i);
|
|
256
|
+
if (create?.[1] !== undefined) body = create[1];
|
|
257
|
+
return body.replace(/;\s*$/, '').trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Normalizes a view definition for comparison.
|
|
262
|
+
*
|
|
263
|
+
* A catalog may return `CREATE VIEW [x] AS SELECT ...` while the schema holds
|
|
264
|
+
* only `SELECT ...`. Whitespace and case are not meaningful here either, so both
|
|
265
|
+
* forms reduce to the same string; comparing the raw text would make every push
|
|
266
|
+
* think the view changed.
|
|
267
|
+
*/
|
|
268
|
+
export function normalizeViewSql(sql: string | null | undefined): string {
|
|
269
|
+
return viewSqlBody(sql).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
270
|
+
}
|
|
271
|
+
|
|
157
272
|
function buildColumnExists(provider: Provider): (raw: string, column: string) => string {
|
|
158
273
|
return (raw, column) => {
|
|
159
274
|
const { schema, name } = splitQualified(raw);
|
|
@@ -281,6 +396,47 @@ function identityKeyword(column: ColumnSpec, provider: Provider): string {
|
|
|
281
396
|
}
|
|
282
397
|
}
|
|
283
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Renders `CREATE [UNIQUE] INDEX`, carrying the `@@index` options where the
|
|
401
|
+
* provider understands them.
|
|
402
|
+
*
|
|
403
|
+
* SQL Server writes `INCLUDE (...) WHERE <filter> WITH (<options>)`; PostgreSQL
|
|
404
|
+
* shares the standard `INCLUDE`/`WHERE` subset; SQLite has partial indexes but no
|
|
405
|
+
* covering indexes; MySQL has neither, so the base index is the whole statement.
|
|
406
|
+
* Dropping an unsupported clause keeps the index valid — it only means the engine
|
|
407
|
+
* cannot use it for a narrower scan.
|
|
408
|
+
*/
|
|
409
|
+
function createIndexSql(
|
|
410
|
+
kind: 'INDEX' | 'UNIQUE INDEX',
|
|
411
|
+
quotedName: string,
|
|
412
|
+
quotedTable: string,
|
|
413
|
+
quotedFields: string[],
|
|
414
|
+
options: IndexOptions | undefined,
|
|
415
|
+
provider: Provider,
|
|
416
|
+
): string {
|
|
417
|
+
const statement = kind === 'INDEX' ? 'CREATE INDEX' : 'CREATE UNIQUE INDEX';
|
|
418
|
+
const base = `${statement} ${quotedName} ON ${quotedTable} (${quotedFields.join(', ')})`;
|
|
419
|
+
if (!options) return base;
|
|
420
|
+
|
|
421
|
+
const include = options.includeFields && options.includeFields.length > 0
|
|
422
|
+
? ` INCLUDE (${options.includeFields.join(', ')})`
|
|
423
|
+
: '';
|
|
424
|
+
const filter = options.filter ? ` WHERE ${options.filter}` : '';
|
|
425
|
+
const withClause = options.options ? ` WITH (${options.options})` : '';
|
|
426
|
+
|
|
427
|
+
switch (provider) {
|
|
428
|
+
case 'mssql':
|
|
429
|
+
return `${base}${include}${filter}${withClause}`;
|
|
430
|
+
case 'postgres':
|
|
431
|
+
return `${base}${include}${filter}`;
|
|
432
|
+
case 'sqlite':
|
|
433
|
+
return `${base}${filter}`;
|
|
434
|
+
default:
|
|
435
|
+
// MySQL alone: no INCLUDE, no partial indexes and no WITH options.
|
|
436
|
+
return base;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
284
440
|
function createDialect(provider: Provider, quote: (name: string) => string): PushDialect {
|
|
285
441
|
const supportsUniqueInlineOnAddColumn = provider !== 'sqlite';
|
|
286
442
|
const quoteTable = (raw: string): string => {
|
|
@@ -303,7 +459,14 @@ function createDialect(provider: Provider, quote: (name: string) => string): Pus
|
|
|
303
459
|
? `${quote(column.name)} ${sqlType} ${identity}`
|
|
304
460
|
: `${quote(column.name)} ${sqlType}`;
|
|
305
461
|
const nullable = column.isOptional ? '' : ' NOT NULL';
|
|
306
|
-
|
|
462
|
+
// Without `map:` the database names the constraint; with it, push has to use
|
|
463
|
+
// the schema's name so the next migration recognises the artifact instead of
|
|
464
|
+
// trying to add a second unique for the same column.
|
|
465
|
+
const unique = column.isUnique && !column.isId
|
|
466
|
+
? column.uniqueName
|
|
467
|
+
? ` CONSTRAINT ${quote(column.uniqueName)} UNIQUE`
|
|
468
|
+
: ' UNIQUE'
|
|
469
|
+
: '';
|
|
307
470
|
const defaultSql = defaultClause(column.defaultExpr, provider);
|
|
308
471
|
const defaultPart = defaultSql ? ` ${defaultSql}` : '';
|
|
309
472
|
|
|
@@ -321,14 +484,29 @@ function createDialect(provider: Provider, quote: (name: string) => string): Pus
|
|
|
321
484
|
quoteTable,
|
|
322
485
|
supportsNamedUniqueConstraint: provider !== 'sqlite',
|
|
323
486
|
supportsUniqueInlineOnAddColumn,
|
|
487
|
+
supportsCreateOrReplaceView: provider !== 'sqlite',
|
|
324
488
|
tableExists: buildTableExists(provider),
|
|
489
|
+
viewExists: buildViewExists(provider),
|
|
490
|
+
viewDefinition: buildViewDefinition(provider),
|
|
325
491
|
columnExists: buildColumnExists(provider),
|
|
326
492
|
uniqueConstraintExists: buildUniqueConstraintExists(provider),
|
|
327
493
|
indexExists: buildIndexExists(provider),
|
|
328
494
|
columnDefinition,
|
|
329
495
|
createTable: (table, columnDefs) => `CREATE TABLE ${table} (\n ${columnDefs.join(',\n ')}\n)`,
|
|
330
|
-
|
|
331
|
-
|
|
496
|
+
createView: (view, selectSql) => {
|
|
497
|
+
// `@@sql` is the SELECT body, but a definition pasted from a catalog may
|
|
498
|
+
// still carry its `CREATE VIEW ... AS` header; dropping it here means such a
|
|
499
|
+
// schema produces one statement instead of `CREATE VIEW ... AS CREATE VIEW`.
|
|
500
|
+
const body = viewSqlBody(selectSql).trim();
|
|
501
|
+
if (provider === 'mssql') return `CREATE OR ALTER VIEW ${view} AS\n${body}`;
|
|
502
|
+
if (provider === 'sqlite') return `CREATE VIEW ${view} AS\n${body}`;
|
|
503
|
+
return `CREATE OR REPLACE VIEW ${view} AS\n${body}`;
|
|
504
|
+
},
|
|
505
|
+
dropView: (view) => `DROP VIEW IF EXISTS ${view}`,
|
|
506
|
+
createIndex: (name, table, fields, options) =>
|
|
507
|
+
createIndexSql('INDEX', name, table, fields, options, provider),
|
|
508
|
+
createUniqueIndex: (name, table, fields) =>
|
|
509
|
+
createIndexSql('UNIQUE INDEX', name, table, fields, undefined, provider),
|
|
332
510
|
addColumn: (table, column) => {
|
|
333
511
|
// SQLite refuses "Cannot add a UNIQUE column", so the unique is left out here
|
|
334
512
|
// and added as a separate index by the caller — silently dropping it instead
|
|
@@ -354,12 +532,17 @@ const SHEETS_DIALECT: PushDialect = {
|
|
|
354
532
|
quoteTable: (raw) => objectName(raw),
|
|
355
533
|
supportsNamedUniqueConstraint: false,
|
|
356
534
|
supportsUniqueInlineOnAddColumn: false,
|
|
535
|
+
supportsCreateOrReplaceView: false,
|
|
357
536
|
tableExists: () => '',
|
|
537
|
+
viewExists: () => '',
|
|
538
|
+
viewDefinition: () => '',
|
|
358
539
|
columnExists: () => '',
|
|
359
540
|
uniqueConstraintExists: () => '',
|
|
360
541
|
indexExists: () => '',
|
|
361
542
|
columnDefinition: () => '',
|
|
362
543
|
createTable: () => '',
|
|
544
|
+
createView: () => '',
|
|
545
|
+
dropView: () => '',
|
|
363
546
|
createIndex: () => '',
|
|
364
547
|
createUniqueIndex: () => '',
|
|
365
548
|
addColumn: () => '',
|