@an5/orm 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +74 -11
  2. package/dist/cleanup.d.ts +1 -0
  3. package/dist/cleanup.js +177 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +147 -0
  6. package/dist/generator/src/attributes.d.ts +57 -0
  7. package/dist/generator/src/attributes.js +225 -0
  8. package/dist/generator/src/code-generator.js +49 -16
  9. package/dist/generator/src/config.d.ts +2 -2
  10. package/dist/generator/src/config.js +4 -3
  11. package/dist/generator/src/dialect.d.ts +52 -1
  12. package/dist/generator/src/dialect.js +136 -3
  13. package/dist/generator/src/dotnet-generator.js +67 -23
  14. package/dist/generator/src/golang-generator.d.ts +8 -0
  15. package/dist/generator/src/golang-generator.js +86 -21
  16. package/dist/generator/src/java-generator.d.ts +8 -0
  17. package/dist/generator/src/java-generator.js +108 -3
  18. package/dist/generator/src/kotlin-generator.js +51 -0
  19. package/dist/generator/src/parser.d.ts +1 -0
  20. package/dist/generator/src/parser.js +90 -35
  21. package/dist/generator/src/pull-schema.d.ts +54 -0
  22. package/dist/generator/src/pull-schema.js +116 -0
  23. package/dist/generator/src/push-apply.d.ts +4 -2
  24. package/dist/generator/src/push-apply.js +53 -4
  25. package/dist/generator/src/push-schema.d.ts +21 -9
  26. package/dist/generator/src/push-schema.js +132 -32
  27. package/dist/generator/src/python-generator.js +35 -22
  28. package/dist/generator/src/rust-generator.js +79 -74
  29. package/dist/generator/src/swift-generator.js +54 -3
  30. package/dist/generator/src/types.d.ts +10 -0
  31. package/dist/generator/src/view-order.d.ts +33 -0
  32. package/dist/generator/src/view-order.js +123 -0
  33. package/dist/migrate.d.ts +12 -0
  34. package/dist/migrate.js +442 -0
  35. package/dist/migration-core.d.ts +32 -3
  36. package/dist/migration-core.js +213 -43
  37. package/dist/provider-support.d.ts +0 -1
  38. package/dist/provider-support.js +0 -1
  39. package/dist/pull.d.ts +1 -0
  40. package/dist/pull.js +425 -0
  41. package/dist/push.d.ts +1 -0
  42. package/dist/push.js +93 -0
  43. package/dist/seed.d.ts +1 -0
  44. package/dist/seed.js +33 -0
  45. package/generator/src/attributes.ts +231 -0
  46. package/generator/src/code-generator.ts +47 -16
  47. package/generator/src/config.ts +5 -3
  48. package/generator/src/dialect.ts +188 -5
  49. package/generator/src/dotnet-generator.ts +68 -24
  50. package/generator/src/golang-generator.ts +89 -22
  51. package/generator/src/java-generator.ts +109 -3
  52. package/generator/src/kotlin-generator.ts +51 -0
  53. package/generator/src/parser.ts +102 -30
  54. package/generator/src/pull-schema.ts +127 -0
  55. package/generator/src/push-apply.ts +62 -5
  56. package/generator/src/push-schema.ts +160 -32
  57. package/generator/src/python-generator.ts +33 -20
  58. package/generator/src/rust-generator.ts +79 -73
  59. package/generator/src/swift-generator.ts +55 -4
  60. package/generator/src/types.ts +10 -0
  61. package/generator/src/view-order.ts +133 -0
  62. package/package.json +19 -14
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ /**
3
+ * Reading `@attribute(...)` arguments from a `.an5` field or model line.
4
+ *
5
+ * The schema used to be read with regexes such as `/@default\((.*)\)/`, which
6
+ * ran to the *last* `)` on the line. A field with `@default(now())` followed by
7
+ * `@description("...")` therefore had `now()) @description("..."` as its default
8
+ * expression, and `db:push` emitted SQL no database accepts. The closing
9
+ * parenthesis has to be the one that matches the opening one — parentheses nest
10
+ * (`now()`, `uuid()`, `lower(...)`) and quoted strings may contain both.
11
+ *
12
+ * These helpers only read attributes; they do not validate the schema language.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.hasAttribute = hasAttribute;
16
+ exports.attributeArgs = attributeArgs;
17
+ exports.unquote = unquote;
18
+ exports.splitSchemaLines = splitSchemaLines;
19
+ exports.stringOption = stringOption;
20
+ exports.fieldListOption = fieldListOption;
21
+ exports.parseIndexDirective = parseIndexDirective;
22
+ /** Finds a matching `)` for the `(` at `open`, ignoring parentheses inside quotes. */
23
+ function closingParenthesis(line, open) {
24
+ let depth = 0;
25
+ let quote = null;
26
+ for (let index = open; index < line.length; index += 1) {
27
+ const character = line[index];
28
+ if (quote !== null) {
29
+ if (character === '\\') {
30
+ index += 1;
31
+ continue;
32
+ }
33
+ if (character === quote)
34
+ quote = null;
35
+ continue;
36
+ }
37
+ // A triple-quoted string may span lines and may contain either quote, so it
38
+ // has to be skipped as a unit before the single-quote rule below sees it.
39
+ if (character === '"' && line[index + 1] === '"' && line[index + 2] === '"') {
40
+ const end = line.indexOf('"""', index + 3);
41
+ if (end < 0)
42
+ return -1;
43
+ index = end + 2;
44
+ continue;
45
+ }
46
+ if (character === '"' || character === "'") {
47
+ quote = character;
48
+ continue;
49
+ }
50
+ if (character === '(') {
51
+ depth += 1;
52
+ continue;
53
+ }
54
+ if (character === ')') {
55
+ depth -= 1;
56
+ if (depth === 0)
57
+ return index;
58
+ }
59
+ }
60
+ return -1;
61
+ }
62
+ /** Whether `line` has the attribute, with or without arguments. */
63
+ function hasAttribute(line, name) {
64
+ const pattern = new RegExp(`(^|\\s)${escapeRegExp(name)}(?![\\w])`);
65
+ return pattern.test(line);
66
+ }
67
+ /**
68
+ * The raw argument text of the first `name(...)` on `line`.
69
+ *
70
+ * Returns `undefined` when the attribute is absent, or when it is written
71
+ * without parentheses. The returned text keeps its quotes, so callers decide
72
+ * whether they are reading a string, a number or an expression.
73
+ */
74
+ function attributeArgs(line, name) {
75
+ const pattern = new RegExp(`(^|\\s)${escapeRegExp(name)}\\s*\\(`, 'g');
76
+ const match = pattern.exec(line);
77
+ if (!match)
78
+ return undefined;
79
+ const open = line.indexOf('(', match.index + match[0].length - 1);
80
+ if (open < 0)
81
+ return undefined;
82
+ const close = closingParenthesis(line, open);
83
+ if (close < 0)
84
+ return undefined;
85
+ return line.slice(open + 1, close);
86
+ }
87
+ /** Strips one pair of surrounding double quotes, if present. */
88
+ function unquote(value) {
89
+ if (value === undefined)
90
+ return undefined;
91
+ const trimmed = value.trim();
92
+ // Triple quotes allow a view's SQL to be written over several lines without
93
+ // escaping: `@@sql("""
94
+ // SELECT ...
95
+ // """)`. The common indentation is removed so the generated DDL is not
96
+ // needlessly indented, while meaningful inner indentation is kept.
97
+ //
98
+ // The closing `"""` ends the string, not the argument list: `)` may follow on
99
+ // the same line or the next one, so the body is found by searching for the
100
+ // closing quotes rather than assuming they sit at the very end.
101
+ if (trimmed.startsWith('"""')) {
102
+ const end = trimmed.indexOf('"""', 3);
103
+ if (end >= 0) {
104
+ const body = trimmed.slice(3, end).replace(/^\r?\n/, '').replace(/\r?\n$/, '');
105
+ const lines = body.split(/\r?\n/);
106
+ const indents = lines
107
+ .filter((line) => line.trim() !== '')
108
+ .map((line) => line.match(/^[ \t]*/)?.[0].length ?? 0);
109
+ const dedent = indents.length > 0 ? Math.min(...indents) : 0;
110
+ return lines.map((line) => line.slice(dedent)).join('\n');
111
+ }
112
+ }
113
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
114
+ return trimmed.slice(1, -1);
115
+ }
116
+ return trimmed;
117
+ }
118
+ /**
119
+ * Whether `line` holds a complete `@@sql("""…""")` directive.
120
+ *
121
+ * The closing `"""` and the `)` that closes the attribute need not share a line,
122
+ * so this asks whether the argument list is balanced *and* the body it contains
123
+ * has been closed. An incomplete directive is kept as a pending line instead of
124
+ * being handed to the parser as if the SQL ended at the newline.
125
+ */
126
+ function isCompleteViewDirective(line) {
127
+ const args = attributeArgs(line, '@@sql') ?? attributeArgs(line, '@@definition');
128
+ if (args === undefined)
129
+ return false;
130
+ const trimmed = args.trim();
131
+ if (!trimmed.startsWith('"""'))
132
+ return false;
133
+ return trimmed.slice(3).includes('"""');
134
+ }
135
+ /**
136
+ * Splits schema text into logical lines, keeping a triple-quoted `@@sql` body on
137
+ * one line.
138
+ *
139
+ * The parsers read a schema line by line. Without this, a multi-line view body
140
+ * would end the directive at its first newline and the rest of the `SELECT`
141
+ * would be read as fields of the view. Newlines inside the quotes are preserved;
142
+ * only the split points change.
143
+ */
144
+ function splitSchemaLines(text) {
145
+ const lines = [];
146
+ let pending = null;
147
+ for (const rawLine of text.split(/\r?\n/)) {
148
+ if (pending !== null) {
149
+ pending += `\n${rawLine}`;
150
+ if (isCompleteViewDirective(pending)) {
151
+ lines.push(pending);
152
+ pending = null;
153
+ }
154
+ continue;
155
+ }
156
+ const start = /@@(?:sql|definition)\s*\(\s*"""/.exec(rawLine);
157
+ if (start) {
158
+ if (isCompleteViewDirective(rawLine)) {
159
+ lines.push(rawLine);
160
+ continue;
161
+ }
162
+ pending = rawLine;
163
+ continue;
164
+ }
165
+ lines.push(rawLine);
166
+ }
167
+ if (pending !== null)
168
+ lines.push(pending);
169
+ return lines;
170
+ }
171
+ /**
172
+ * Reads a named string option from an attribute argument list, e.g.
173
+ * `map: "UQ_users_email"` in `@unique(map: "UQ_users_email")`.
174
+ */
175
+ function stringOption(args, key) {
176
+ if (!args)
177
+ return undefined;
178
+ const pattern = new RegExp(`\\b${escapeRegExp(key)}\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`);
179
+ return pattern.exec(args)?.[1];
180
+ }
181
+ /** Reads a named field-list option, e.g. `include: [email, tenantId]`. */
182
+ function fieldListOption(args, key) {
183
+ if (!args)
184
+ return undefined;
185
+ const pattern = new RegExp(`\\b${escapeRegExp(key)}\\s*:\\s*\\[([^\\]]*)\\]`);
186
+ const value = pattern.exec(args)?.[1];
187
+ if (value === undefined)
188
+ return undefined;
189
+ return value.split(',').map((field) => field.trim()).filter(Boolean);
190
+ }
191
+ /**
192
+ * Parses the inside of `@@unique([a, b], map: "…", include: […], …)`.
193
+ *
194
+ * The directive is the one place a schema can carry raw SQL (`filter`,
195
+ * `options`), so its argument list is balanced rather than cut at the first
196
+ * `)`. A filter such as `(deletedAt IS NULL)` used to truncate the directive.
197
+ */
198
+ function parseIndexDirective(args) {
199
+ if (!args)
200
+ return null;
201
+ const fieldsMatch = args.match(/^\s*\[([\w\s,]*)\]/);
202
+ if (!fieldsMatch)
203
+ return null;
204
+ const fields = (fieldsMatch[1] ?? '').split(',').map((field) => field.trim()).filter(Boolean);
205
+ if (fields.length === 0)
206
+ return null;
207
+ const rest = args.slice(fieldsMatch[0].length);
208
+ const directive = { fields };
209
+ const name = stringOption(rest, 'map');
210
+ if (name)
211
+ directive.name = name;
212
+ const includeFields = fieldListOption(rest, 'include');
213
+ if (includeFields && includeFields.length > 0)
214
+ directive.includeFields = includeFields;
215
+ const filter = stringOption(rest, 'filter');
216
+ if (filter !== undefined)
217
+ directive.filter = filter;
218
+ const options = stringOption(rest, 'options');
219
+ if (options !== undefined)
220
+ directive.options = options;
221
+ return directive;
222
+ }
223
+ function escapeRegExp(value) {
224
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
225
+ }
@@ -62,11 +62,27 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
62
62
  groupBy(args: GroupByArgs): Promise<any[]>;
63
63
  upsert(args: UpsertArgs): Promise<T>;
64
64
  }
65
+
66
+ /**
67
+ * A read-only client for a view block.
68
+ *
69
+ * The same query surface as TableClient without any write method, matching the
70
+ * runtime ViewClient in @an5/adapters.
71
+ */
72
+ export interface ViewClient<T, WhereInput = any, _Select = any, Include = any, FindManyArgs = any, FindFirstArgs = any, FindUniqueArgs = any, AggregateArgs = any, GroupByArgs = any> {
73
+ findMany(args?: FindManyArgs): Promise<T[]>;
74
+ vectorSearch(args: { vector: number[]; take?: number; where?: WhereInput; include?: Include; vectorField?: string; distanceMetric?: 'cosine' | 'euclidean' | 'dot'; }): Promise<(T & { distance: number })[]>;
75
+ findFirst(args?: FindFirstArgs): Promise<T | null>;
76
+ findUnique(args?: FindUniqueArgs): Promise<T | null>;
77
+ count(args?: { where?: WhereInput; }): Promise<number>;
78
+ aggregate(args: AggregateArgs): Promise<any>;
79
+ groupBy(args: GroupByArgs): Promise<any[]>;
80
+ }
65
81
  `;
66
82
  fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'base.ts'), baseTs);
67
83
  }
68
84
  generateModelFile(model) {
69
- let content = `import { An5, TableClient } from './base';\n`;
85
+ let content = `import { An5, ${model.isView ? 'ViewClient' : 'TableClient'} } from './base';\n`;
70
86
  const relModels = [...new Set(model.relations.map(r => r.type))];
71
87
  for (const relModel of relModels) {
72
88
  if (relModel !== model.name) {
@@ -118,23 +134,32 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
118
134
  includeRels.push(`_count?: boolean | { select?: { ${model.relations.map(r => `${r.name}?: boolean`).join('; ')} } }`);
119
135
  }
120
136
  content += `export type ${model.name}Include = { ${includeRels.join('; ')}${includeRels.length > 0 ? ';' : ''} };\n`;
121
- 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`;
122
- 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`;
137
+ if (!model.isView) {
138
+ 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`;
139
+ 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
+ }
123
141
  content += `export type ${model.name}OrderByInput = { ${model.fields.map(f => `${f.name}?: An5.SortOrder`).join('; ')} };\n`;
124
142
  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`;
125
143
  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`;
126
144
  content += `export type ${model.name}FindUniqueArgs = { where?: ${model.name}WhereInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
127
- content += `export type ${model.name}CreateArgs = { data: ${model.name}CreateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
128
- content += `export type ${model.name}UpdateArgs = { where: ${model.name}WhereInput; data: ${model.name}UpdateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
129
- 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`;
130
- content += `export type ${model.name}DeleteArgs = { where: ${model.name}WhereInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
145
+ if (!model.isView) {
146
+ content += `export type ${model.name}CreateArgs = { data: ${model.name}CreateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
147
+ content += `export type ${model.name}UpdateArgs = { where: ${model.name}WhereInput; data: ${model.name}UpdateInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
148
+ 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`;
149
+ content += `export type ${model.name}DeleteArgs = { where: ${model.name}WhereInput; include?: ${model.name}Include; select?: ${model.name}Select; };\n`;
150
+ }
131
151
  content += `export type ${model.name}ScalarFieldEnum = ${model.fields.map(f => `'${f.name}'`).join(' | ') || 'never'};\n`;
132
152
  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`;
133
153
  const numericHavingFields = model.fields.filter(f => this.normalizeType(f.type) === 'number').map(f => `${f.name}?: An5.NumberFilter | number`).join('; ');
134
154
  const scalarHavingFields = model.fields.map(f => `${f.name}?: any`).join('; ');
135
155
  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`;
136
156
  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`;
137
- 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`;
157
+ if (model.isView) {
158
+ 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`;
159
+ }
160
+ else {
161
+ 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
+ }
138
163
  fs_1.default.writeFileSync(path_1.default.join(this.outputDir, `${model.name}.ts`), content);
139
164
  }
140
165
  generateIndexTs(models) {
@@ -164,17 +189,24 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
164
189
  content += ` export type ${model.name}WhereInput = ${model.name}Types.${model.name}WhereInput;\n`;
165
190
  content += ` export type ${model.name}Select = ${model.name}Types.${model.name}Select;\n`;
166
191
  content += ` export type ${model.name}Include = ${model.name}Types.${model.name}Include;\n`;
167
- content += ` export type ${model.name}CreateInput = ${model.name}Types.${model.name}CreateInput;\n`;
168
- content += ` export type ${model.name}UpdateInput = ${model.name}Types.${model.name}UpdateInput;\n`;
192
+ if (!model.isView) {
193
+ content += ` export type ${model.name}CreateInput = ${model.name}Types.${model.name}CreateInput;\n`;
194
+ content += ` export type ${model.name}UpdateInput = ${model.name}Types.${model.name}UpdateInput;\n`;
195
+ }
169
196
  content += ` export type ${model.name}OrderByInput = ${model.name}Types.${model.name}OrderByInput;\n`;
170
197
  content += ` export type ${model.name}FindManyArgs = ${model.name}Types.${model.name}FindManyArgs;\n`;
171
198
  content += ` export type ${model.name}FindFirstArgs = ${model.name}Types.${model.name}FindFirstArgs;\n`;
172
199
  content += ` export type ${model.name}FindUniqueArgs = ${model.name}Types.${model.name}FindUniqueArgs;\n`;
173
- content += ` export type ${model.name}CreateArgs = ${model.name}Types.${model.name}CreateArgs;\n`;
174
- content += ` export type ${model.name}UpdateArgs = ${model.name}Types.${model.name}UpdateArgs;\n`;
175
- content += ` export type ${model.name}UpsertArgs = ${model.name}Types.${model.name}UpsertArgs;\n`;
176
- content += ` export type ${model.name}DeleteArgs = ${model.name}Types.${model.name}DeleteArgs;\n`;
177
- content += ` export type ${model.name}TableClient = ${model.name}Types.${model.name}TableClient;\n`;
200
+ if (!model.isView) {
201
+ content += ` export type ${model.name}CreateArgs = ${model.name}Types.${model.name}CreateArgs;\n`;
202
+ content += ` export type ${model.name}UpdateArgs = ${model.name}Types.${model.name}UpdateArgs;\n`;
203
+ content += ` export type ${model.name}UpsertArgs = ${model.name}Types.${model.name}UpsertArgs;\n`;
204
+ content += ` export type ${model.name}DeleteArgs = ${model.name}Types.${model.name}DeleteArgs;\n`;
205
+ content += ` export type ${model.name}TableClient = ${model.name}Types.${model.name}TableClient;\n`;
206
+ }
207
+ else {
208
+ content += ` export type ${model.name}ViewClient = ${model.name}Types.${model.name}ViewClient;\n`;
209
+ }
178
210
  content += ` export type ${model.name}ScalarFieldEnum = ${model.name}Types.${model.name}ScalarFieldEnum;\n`;
179
211
  content += ` export type ${model.name}AggregateArgs = ${model.name}Types.${model.name}AggregateArgs;\n`;
180
212
  content += ` export type ${model.name}GroupByArgs = ${model.name}Types.${model.name}GroupByArgs;\n`;
@@ -184,9 +216,10 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
184
216
  const addedProps = new Set();
185
217
  for (const model of models) {
186
218
  const props = this.getAllPropertyVariations(model.name);
219
+ const clientType = `${model.name}Types.${model.name}${model.isView ? 'ViewClient' : 'TableClient'}`;
187
220
  for (const prop of props) {
188
221
  if (!addedProps.has(prop)) {
189
- content += ` ${prop}!: ${model.name}Types.${model.name}TableClient;\n`;
222
+ content += ` ${prop}!: ${clientType};\n`;
190
223
  addedProps.add(prop);
191
224
  }
192
225
  }
@@ -125,8 +125,8 @@ export declare function resolveConnectionString(config: An5OrmConfig, env?: EnvL
125
125
  *
126
126
  * Falls back to SQL Server when the scheme is unknown — it is the default
127
127
  * provider, and ADO-style strings (`Server=...;Database=...`) have no scheme to
128
- * read. `sqlite://`, a bare path ending in `.sqlite`, `.sqlite3` or `.db` — the same
129
- * list `An5Adapter` uses.
128
+ * read. `:memory:`, any `sqlite:` URI (`sqlite://`, `sqlite::memory:`) and a bare
129
+ * path ending in `.sqlite`, `.sqlite3` or `.db` — the same list `An5Adapter` uses.
130
130
  */
131
131
  export declare function detectProvider(connectionString: string | undefined | null): Provider;
132
132
  /**
@@ -270,8 +270,8 @@ function resolveConnectionString(config, env = process.env, command = 'this comm
270
270
  *
271
271
  * Falls back to SQL Server when the scheme is unknown — it is the default
272
272
  * provider, and ADO-style strings (`Server=...;Database=...`) have no scheme to
273
- * read. `sqlite://`, a bare path ending in `.sqlite`, `.sqlite3` or `.db` — the same
274
- * list `An5Adapter` uses.
273
+ * read. `:memory:`, any `sqlite:` URI (`sqlite://`, `sqlite::memory:`) and a bare
274
+ * path ending in `.sqlite`, `.sqlite3` or `.db` — the same list `An5Adapter` uses.
275
275
  */
276
276
  function detectProvider(connectionString) {
277
277
  // Lower-cased: a URI scheme is case-insensitive, so `MySQL://` is the same
@@ -287,8 +287,9 @@ function detectProvider(connectionString) {
287
287
  return 'postgres';
288
288
  if (cs.startsWith('mysql://') || cs.startsWith('mariadb://'))
289
289
  return 'mysql';
290
- if (SQLITE_FILE_SUFFIXES.some((suffix) => cs.endsWith(suffix)))
290
+ if (cs === ':memory:' || cs.startsWith('sqlite:') || SQLITE_FILE_SUFFIXES.some((suffix) => cs.endsWith(suffix))) {
291
291
  return 'sqlite';
292
+ }
292
293
  return field_types_1.DEFAULT_PROVIDER;
293
294
  }
294
295
  /** File extensions that mean SQLite when there is no scheme to read. */
@@ -32,9 +32,23 @@ export interface ColumnSpec {
32
32
  isId: boolean;
33
33
  isUnique: boolean;
34
34
  isOptional: boolean;
35
+ /** `@unique(map: "…")`: the constraint name both push and migrate must use. */
36
+ uniqueName?: string | undefined;
35
37
  /** The raw `@default(...)` expression, e.g. `now()` or `"draft"`. */
36
38
  defaultExpr?: string | undefined;
37
39
  }
40
+ /**
41
+ * The extra clauses `@@index(..., include:, filter:, options:)` asks for.
42
+ *
43
+ * Field names are already quoted by the caller. `options` is SQL Server syntax
44
+ * (`WITH (FILLFACTOR = 90, ONLINE = ON)`); other providers either support the
45
+ * standard subset (`include`, `filter`) or ignore what they cannot express.
46
+ */
47
+ export interface IndexOptions {
48
+ includeFields?: string[] | undefined;
49
+ filter?: string | undefined;
50
+ options?: string | undefined;
51
+ }
38
52
  export interface PushDialect {
39
53
  readonly provider: Provider;
40
54
  /** Whether this database can be written to with DDL at all. */
@@ -61,6 +75,16 @@ export interface PushDialect {
61
75
  readonly supportsUniqueInlineOnAddColumn: boolean;
62
76
  /** Query returning one row when the table exists. */
63
77
  tableExists(raw: string): string;
78
+ /** Query returning one row when a view exists. */
79
+ viewExists(raw: string): string;
80
+ /**
81
+ * Query returning the stored view definition in a single `definition` column.
82
+ *
83
+ * Used to skip a push that would recreate an unchanged view. The value may be
84
+ * the body alone (PostgreSQL/MySQL) or the full `CREATE VIEW` text (SQL
85
+ * Server/SQLite); `normalizeViewSql` compares either form.
86
+ */
87
+ viewDefinition(raw: string): string;
64
88
  /** Query returning one row when the column exists. */
65
89
  columnExists(raw: string, column: string): string;
66
90
  /** Query returning one row when the unique constraint exists. */
@@ -70,12 +94,39 @@ export interface PushDialect {
70
94
  /** One column definition, as it appears inside `CREATE TABLE`. */
71
95
  columnDefinition(column: ColumnSpec): string;
72
96
  createTable(quotedTable: string, columnDefs: string[]): string;
73
- createIndex(quotedName: string, quotedTable: string, quotedFields: string[]): string;
97
+ /**
98
+ * Whether `CREATE OR REPLACE VIEW` (or `CREATE OR ALTER VIEW`) is available.
99
+ *
100
+ * SQLite has neither, so a changed view needs `DROP VIEW` first; see `dropView`.
101
+ */
102
+ readonly supportsCreateOrReplaceView: boolean;
103
+ /** `CREATE [OR REPLACE|OR ALTER] VIEW ... AS <select>`. */
104
+ createView(quotedView: string, selectSql: string): string;
105
+ /** `DROP VIEW IF EXISTS`, for providers without create-or-replace. */
106
+ dropView(quotedView: string): string;
107
+ createIndex(quotedName: string, quotedTable: string, quotedFields: string[], options?: IndexOptions): string;
74
108
  createUniqueIndex(quotedName: string, quotedTable: string, quotedFields: string[]): string;
75
109
  /** `ALTER TABLE ... ADD <column>`, nullability included. */
76
110
  addColumn(quotedTable: string, column: ColumnSpec): string;
77
111
  addUniqueConstraint(quotedName: string, quotedTable: string, quotedFields: string[]): string;
78
112
  }
113
+ /**
114
+ * The `SELECT` body of a view definition.
115
+ *
116
+ * Catalogs return a full `CREATE VIEW ... AS ...` statement or the bare body,
117
+ * depending on the provider (and on whether `OBJECT_DEFINITION` is available).
118
+ * `@@sql` wants the body alone, so both forms reduce to the same text.
119
+ */
120
+ export declare function viewSqlBody(sql: string | null | undefined): string;
121
+ /**
122
+ * Normalizes a view definition for comparison.
123
+ *
124
+ * A catalog may return `CREATE VIEW [x] AS SELECT ...` while the schema holds
125
+ * only `SELECT ...`. Whitespace and case are not meaningful here either, so both
126
+ * forms reduce to the same string; comparing the raw text would make every push
127
+ * think the view changed.
128
+ */
129
+ export declare function normalizeViewSql(sql: string | null | undefined): string;
79
130
  /** The dialect that writes DDL for a provider. */
80
131
  export declare function dialectFor(provider: Provider): PushDialect;
81
132
  /**
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sqlLiteral = sqlLiteral;
4
+ exports.viewSqlBody = viewSqlBody;
5
+ exports.normalizeViewSql = normalizeViewSql;
4
6
  exports.dialectFor = dialectFor;
5
7
  exports.defaultClause = defaultClause;
6
8
  /** `schema.table` reduced to the bare object name, e.g. `dbo.widgets`. */
@@ -78,6 +80,78 @@ function buildTableExists(provider) {
78
80
  }
79
81
  };
80
82
  }
83
+ /**
84
+ * Where a view's existence and definition are recorded.
85
+ *
86
+ * SQL Server keeps both in `sys.views`/`OBJECT_DEFINITION`; PostgreSQL and MySQL
87
+ * expose the body through their catalogs; SQLite stores the original statement in
88
+ * `sqlite_master`. The returned definition is compared with `normalizeViewSql`,
89
+ * which accepts either a full `CREATE VIEW` statement or a bare `SELECT`.
90
+ */
91
+ function buildViewExists(provider) {
92
+ return (raw) => {
93
+ const { schema, name } = splitQualified(raw);
94
+ switch (provider) {
95
+ case 'mssql':
96
+ return `SELECT name FROM sys.views WHERE object_id = OBJECT_ID(${sqlLiteral(objectName(raw))})`;
97
+ case 'postgres':
98
+ return `SELECT viewname FROM pg_views WHERE schemaname = ${schema ? sqlLiteral(schema) : defaultSchema(provider, undefined)} AND viewname = ${sqlLiteral(name)}`;
99
+ case 'mysql':
100
+ return (`SELECT table_name FROM information_schema.views WHERE table_schema = ${schema ? sqlLiteral(schema) : 'DATABASE()'} ` +
101
+ `AND table_name = ${sqlLiteral(name)}`);
102
+ case 'sqlite':
103
+ return `SELECT name FROM sqlite_master WHERE type = 'view' AND name = ${sqlLiteral(name)}`;
104
+ default:
105
+ return '';
106
+ }
107
+ };
108
+ }
109
+ function buildViewDefinition(provider) {
110
+ return (raw) => {
111
+ const { schema, name } = splitQualified(raw);
112
+ switch (provider) {
113
+ case 'mssql':
114
+ return `SELECT OBJECT_DEFINITION(OBJECT_ID(${sqlLiteral(objectName(raw))})) AS definition FROM sys.views WHERE object_id = OBJECT_ID(${sqlLiteral(objectName(raw))})`;
115
+ case 'postgres':
116
+ return (`SELECT definition FROM pg_views WHERE schemaname = ${schema ? sqlLiteral(schema) : defaultSchema(provider, undefined)} ` +
117
+ `AND viewname = ${sqlLiteral(name)}`);
118
+ case 'mysql':
119
+ return (`SELECT view_definition AS definition FROM information_schema.views WHERE table_schema = ` +
120
+ `${schema ? sqlLiteral(schema) : 'DATABASE()'} AND table_name = ${sqlLiteral(name)}`);
121
+ case 'sqlite':
122
+ return `SELECT sql AS definition FROM sqlite_master WHERE type = 'view' AND name = ${sqlLiteral(name)}`;
123
+ default:
124
+ return '';
125
+ }
126
+ };
127
+ }
128
+ /**
129
+ * The `SELECT` body of a view definition.
130
+ *
131
+ * Catalogs return a full `CREATE VIEW ... AS ...` statement or the bare body,
132
+ * depending on the provider (and on whether `OBJECT_DEFINITION` is available).
133
+ * `@@sql` wants the body alone, so both forms reduce to the same text.
134
+ */
135
+ function viewSqlBody(sql) {
136
+ if (!sql)
137
+ return '';
138
+ let body = sql.trim();
139
+ const create = body.match(/^\s*CREATE\s+(?:OR\s+(?:ALTER|REPLACE)\s+)?VIEW\b[\s\S]*?\sAS\b\s*([\s\S]*)$/i);
140
+ if (create?.[1] !== undefined)
141
+ body = create[1];
142
+ return body.replace(/;\s*$/, '').trim();
143
+ }
144
+ /**
145
+ * Normalizes a view definition for comparison.
146
+ *
147
+ * A catalog may return `CREATE VIEW [x] AS SELECT ...` while the schema holds
148
+ * only `SELECT ...`. Whitespace and case are not meaningful here either, so both
149
+ * forms reduce to the same string; comparing the raw text would make every push
150
+ * think the view changed.
151
+ */
152
+ function normalizeViewSql(sql) {
153
+ return viewSqlBody(sql).replace(/\s+/g, ' ').trim().toLowerCase();
154
+ }
81
155
  function buildColumnExists(provider) {
82
156
  return (raw, column) => {
83
157
  const { schema, name } = splitQualified(raw);
@@ -186,6 +260,38 @@ function identityKeyword(column, provider) {
186
260
  return '';
187
261
  }
188
262
  }
263
+ /**
264
+ * Renders `CREATE [UNIQUE] INDEX`, carrying the `@@index` options where the
265
+ * provider understands them.
266
+ *
267
+ * SQL Server writes `INCLUDE (...) WHERE <filter> WITH (<options>)`; PostgreSQL
268
+ * shares the standard `INCLUDE`/`WHERE` subset; SQLite has partial indexes but no
269
+ * covering indexes; MySQL has neither, so the base index is the whole statement.
270
+ * Dropping an unsupported clause keeps the index valid — it only means the engine
271
+ * cannot use it for a narrower scan.
272
+ */
273
+ function createIndexSql(kind, quotedName, quotedTable, quotedFields, options, provider) {
274
+ const statement = kind === 'INDEX' ? 'CREATE INDEX' : 'CREATE UNIQUE INDEX';
275
+ const base = `${statement} ${quotedName} ON ${quotedTable} (${quotedFields.join(', ')})`;
276
+ if (!options)
277
+ return base;
278
+ const include = options.includeFields && options.includeFields.length > 0
279
+ ? ` INCLUDE (${options.includeFields.join(', ')})`
280
+ : '';
281
+ const filter = options.filter ? ` WHERE ${options.filter}` : '';
282
+ const withClause = options.options ? ` WITH (${options.options})` : '';
283
+ switch (provider) {
284
+ case 'mssql':
285
+ return `${base}${include}${filter}${withClause}`;
286
+ case 'postgres':
287
+ return `${base}${include}${filter}`;
288
+ case 'sqlite':
289
+ return `${base}${filter}`;
290
+ default:
291
+ // MySQL alone: no INCLUDE, no partial indexes and no WITH options.
292
+ return base;
293
+ }
294
+ }
189
295
  function createDialect(provider, quote) {
190
296
  const supportsUniqueInlineOnAddColumn = provider !== 'sqlite';
191
297
  const quoteTable = (raw) => {
@@ -207,7 +313,14 @@ function createDialect(provider, quote) {
207
313
  ? `${quote(column.name)} ${sqlType} ${identity}`
208
314
  : `${quote(column.name)} ${sqlType}`;
209
315
  const nullable = column.isOptional ? '' : ' NOT NULL';
210
- const unique = column.isUnique && !column.isId ? ' UNIQUE' : '';
316
+ // Without `map:` the database names the constraint; with it, push has to use
317
+ // the schema's name so the next migration recognises the artifact instead of
318
+ // trying to add a second unique for the same column.
319
+ const unique = column.isUnique && !column.isId
320
+ ? column.uniqueName
321
+ ? ` CONSTRAINT ${quote(column.uniqueName)} UNIQUE`
322
+ : ' UNIQUE'
323
+ : '';
211
324
  const defaultSql = defaultClause(column.defaultExpr, provider);
212
325
  const defaultPart = defaultSql ? ` ${defaultSql}` : '';
213
326
  if (column.isId) {
@@ -223,14 +336,29 @@ function createDialect(provider, quote) {
223
336
  quoteTable,
224
337
  supportsNamedUniqueConstraint: provider !== 'sqlite',
225
338
  supportsUniqueInlineOnAddColumn,
339
+ supportsCreateOrReplaceView: provider !== 'sqlite',
226
340
  tableExists: buildTableExists(provider),
341
+ viewExists: buildViewExists(provider),
342
+ viewDefinition: buildViewDefinition(provider),
227
343
  columnExists: buildColumnExists(provider),
228
344
  uniqueConstraintExists: buildUniqueConstraintExists(provider),
229
345
  indexExists: buildIndexExists(provider),
230
346
  columnDefinition,
231
347
  createTable: (table, columnDefs) => `CREATE TABLE ${table} (\n ${columnDefs.join(',\n ')}\n)`,
232
- createIndex: (name, table, fields) => `CREATE INDEX ${name} ON ${table} (${list(fields)})`,
233
- createUniqueIndex: (name, table, fields) => `CREATE UNIQUE INDEX ${name} ON ${table} (${list(fields)})`,
348
+ createView: (view, selectSql) => {
349
+ // `@@sql` is the SELECT body, but a definition pasted from a catalog may
350
+ // still carry its `CREATE VIEW ... AS` header; dropping it here means such a
351
+ // schema produces one statement instead of `CREATE VIEW ... AS CREATE VIEW`.
352
+ const body = viewSqlBody(selectSql).trim();
353
+ if (provider === 'mssql')
354
+ return `CREATE OR ALTER VIEW ${view} AS\n${body}`;
355
+ if (provider === 'sqlite')
356
+ return `CREATE VIEW ${view} AS\n${body}`;
357
+ return `CREATE OR REPLACE VIEW ${view} AS\n${body}`;
358
+ },
359
+ dropView: (view) => `DROP VIEW IF EXISTS ${view}`,
360
+ createIndex: (name, table, fields, options) => createIndexSql('INDEX', name, table, fields, options, provider),
361
+ createUniqueIndex: (name, table, fields) => createIndexSql('UNIQUE INDEX', name, table, fields, undefined, provider),
234
362
  addColumn: (table, column) => {
235
363
  // SQLite refuses "Cannot add a UNIQUE column", so the unique is left out here
236
364
  // and added as a separate index by the caller — silently dropping it instead
@@ -255,12 +383,17 @@ const SHEETS_DIALECT = {
255
383
  quoteTable: (raw) => objectName(raw),
256
384
  supportsNamedUniqueConstraint: false,
257
385
  supportsUniqueInlineOnAddColumn: false,
386
+ supportsCreateOrReplaceView: false,
258
387
  tableExists: () => '',
388
+ viewExists: () => '',
389
+ viewDefinition: () => '',
259
390
  columnExists: () => '',
260
391
  uniqueConstraintExists: () => '',
261
392
  indexExists: () => '',
262
393
  columnDefinition: () => '',
263
394
  createTable: () => '',
395
+ createView: () => '',
396
+ dropView: () => '',
264
397
  createIndex: () => '',
265
398
  createUniqueIndex: () => '',
266
399
  addColumn: () => '',