@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
@@ -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
- 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`;
125
- 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`;
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
- content += `export type ${model.name}CreateArgs = { data: ${model.name}CreateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
132
- content += `export type ${model.name}UpdateArgs = { where: ${model.name}WhereInput; data: ${model.name}UpdateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
133
- 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`;
134
- content += `export type ${model.name}DeleteArgs = { where: ${model.name}WhereInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
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
- 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`;
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
- content += ` export type ${model.name}CreateInput = ${model.name}Types.${model.name}CreateInput;\n`;
177
- content += ` export type ${model.name}UpdateInput = ${model.name}Types.${model.name}UpdateInput;\n`;
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
- content += ` export type ${model.name}CreateArgs = ${model.name}Types.${model.name}CreateArgs;\n`;
183
- content += ` export type ${model.name}UpdateArgs = ${model.name}Types.${model.name}UpdateArgs;\n`;
184
- content += ` export type ${model.name}UpsertArgs = ${model.name}Types.${model.name}UpsertArgs;\n`;
185
- content += ` export type ${model.name}DeleteArgs = ${model.name}Types.${model.name}DeleteArgs;\n`;
186
- content += ` export type ${model.name}TableClient = ${model.name}Types.${model.name}TableClient;\n`;
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}!: ${model.name}Types.${model.name}TableClient;\n`;
231
+ content += ` ${prop}!: ${clientType};\n`;
201
232
  addedProps.add(prop);
202
233
  }
203
234
  }
@@ -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://`, a bare path ending in `.sqlite`, `.sqlite3` or `.db` — the same
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))) return 'sqlite';
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
 
@@ -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
- createIndex(quotedName: string, quotedTable: string, quotedFields: string[]): string;
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
- const unique = column.isUnique && !column.isId ? ' UNIQUE' : '';
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
- createIndex: (name, table, fields) => `CREATE INDEX ${name} ON ${table} (${list(fields)})`,
331
- createUniqueIndex: (name, table, fields) => `CREATE UNIQUE INDEX ${name} ON ${table} (${list(fields)})`,
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: () => '',