@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
@@ -5,6 +5,7 @@ exports.directiveName = directiveName;
5
5
  exports.parsePushSchema = parsePushSchema;
6
6
  exports.safeIdentifierName = safeIdentifierName;
7
7
  exports.qualifiedTableName = qualifiedTableName;
8
+ exports.qualifiedViewName = qualifiedViewName;
8
9
  /**
9
10
  * Reading `.an5` files for `db:push`.
10
11
  *
@@ -14,6 +15,7 @@ exports.qualifiedTableName = qualifiedTableName;
14
15
  * lands in. It is kept here rather than inline in `push.ts` so the mapping from
15
16
  * schema text to a column can be tested without a database.
16
17
  */
18
+ const attributes_1 = require("./attributes");
17
19
  const field_types_1 = require("./field-types");
18
20
  /**
19
21
  * The schema a model lands in when the schema file does not say: none, for every
@@ -27,20 +29,17 @@ const field_types_1 = require("./field-types");
27
29
  function defaultSchemaFor(_provider) {
28
30
  return '';
29
31
  }
30
- /** The field list of `@@unique([a, b], …)`, plus whatever options follow it. */
31
- const DIRECTIVE = /^@@(?:unique|index)\(\s*\[([\w\s,]+)\]([^)]*)\)/;
32
- function parseDirective(line, fields) {
33
- const options = line.match(DIRECTIVE)?.[2] ?? '';
34
- const name = options.match(/\bmap\s*:\s*"([^"]+)"/)?.[1];
35
- const include = options.match(/\binclude\s*:\s*\[([\w,\s]+)\]/)?.[1];
36
- const filter = options.match(/\bfilter\s*:\s*"([^"]+)"/)?.[1];
37
- const opts = options.match(/\boptions\s*:\s*"([^"]+)"/)?.[1];
32
+ /** Reads `@@unique(…)`/`@@index(…)` into the shape push and migrate share. */
33
+ function parseDirective(line, name) {
34
+ const parsed = (0, attributes_1.parseIndexDirective)((0, attributes_1.attributeArgs)(line, name));
35
+ if (!parsed)
36
+ return null;
38
37
  return {
39
- fields: fields.split(',').map((field) => field.trim()).filter(Boolean),
40
- ...(name ? { name } : {}),
41
- ...(include ? { includeFields: include.split(',').map((field) => field.trim()).filter(Boolean) } : {}),
42
- ...(filter ? { filter } : {}),
43
- ...(opts ? { options: opts } : {}),
38
+ fields: parsed.fields,
39
+ ...(parsed.name ? { name: parsed.name } : {}),
40
+ ...(parsed.includeFields ? { includeFields: parsed.includeFields } : {}),
41
+ ...(parsed.filter !== undefined ? { filter: parsed.filter } : {}),
42
+ ...(parsed.options !== undefined ? { options: parsed.options } : {}),
44
43
  };
45
44
  }
46
45
  /**
@@ -67,10 +66,13 @@ function directiveName(directive, table, kind, position) {
67
66
  */
68
67
  function parsePushSchema(text, provider) {
69
68
  const models = [];
69
+ const views = [];
70
70
  const issues = [];
71
71
  const modelRefs = [];
72
+ const viewRefs = [];
72
73
  let current = null;
73
- for (const raw of text.split('\n')) {
74
+ let currentView = null;
75
+ for (const raw of (0, attributes_1.splitSchemaLines)(text)) {
74
76
  const line = raw.trim();
75
77
  if (!line || line.startsWith('//'))
76
78
  continue;
@@ -84,37 +86,92 @@ function parsePushSchema(text, provider) {
84
86
  compoundUniques: [],
85
87
  indexes: [],
86
88
  };
89
+ currentView = null;
87
90
  models.push(current);
88
91
  continue;
89
92
  }
93
+ const viewHeader = line.match(/^view\s+(\w+)\s*\{/);
94
+ if (viewHeader) {
95
+ currentView = {
96
+ name: viewHeader[1],
97
+ viewName: viewHeader[1].toLowerCase() + 's',
98
+ schema: defaultSchemaFor(provider),
99
+ fields: [],
100
+ sql: '',
101
+ };
102
+ current = null;
103
+ views.push(currentView);
104
+ continue;
105
+ }
90
106
  if (line === '}') {
91
107
  current = null;
108
+ currentView = null;
109
+ continue;
110
+ }
111
+ if (currentView) {
112
+ if (line.startsWith('@@sql') || line.startsWith('@@definition')) {
113
+ const attribute = line.startsWith('@@sql') ? '@@sql' : '@@definition';
114
+ const sql = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, attribute));
115
+ if (sql !== undefined)
116
+ currentView.sql = sql.trim();
117
+ continue;
118
+ }
119
+ if (line.startsWith('@@map')) {
120
+ const mapped = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@map'));
121
+ if (mapped)
122
+ currentView.viewName = mapped;
123
+ continue;
124
+ }
125
+ if (line.startsWith('@@schema')) {
126
+ const declared = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@schema'));
127
+ if (declared !== undefined) {
128
+ currentView.schema = declared.trim();
129
+ currentView.declaredSchema = currentView.schema;
130
+ }
131
+ continue;
132
+ }
133
+ if (line.startsWith('@@'))
134
+ continue;
135
+ const head = (0, field_types_1.readFieldLineHead)(line);
136
+ if (!head)
137
+ continue;
138
+ if (!(0, field_types_1.resolveFieldType)(head.type, provider)) {
139
+ // A view's fields are columns of a result set, never relations.
140
+ viewRefs.push({ view: currentView.name, field: head.name, type: head.type });
141
+ continue;
142
+ }
143
+ currentView.fields.push({
144
+ name: head.name,
145
+ sqlType: head.type.toUpperCase(),
146
+ isOptional: head.isOptional,
147
+ isId: false,
148
+ isUnique: false,
149
+ });
92
150
  continue;
93
151
  }
94
152
  if (!current)
95
153
  continue;
96
154
  if (line.startsWith('@@map')) {
97
- const match = line.match(/@@map\("(.+)"\)/);
98
- if (match)
99
- current.tableName = match[1];
155
+ const mapped = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@map'));
156
+ if (mapped)
157
+ current.tableName = mapped;
100
158
  continue;
101
159
  }
102
160
  if (line.startsWith('@@schema')) {
103
- // `(.*)` rather than `(.+)`, because `@@schema("")` is how a model says "no
104
- // schema" and has to survive as an empty string rather than being dropped as
105
- // no match — which is how SQLite and MySQL schemas are written.
106
- const match = line.match(/@@schema\("(.*)"\)/);
107
- if (match) {
108
- current.schema = match[1].trim();
161
+ // An empty string is meaningful — `@@schema("")` says "no schema" for databases
162
+ // that have none — so it has to survive as `''` rather than being dropped.
163
+ const declared = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@schema'));
164
+ if (declared !== undefined) {
165
+ current.schema = declared.trim();
109
166
  current.declaredSchema = current.schema;
110
167
  }
111
168
  continue;
112
169
  }
113
170
  if (line.startsWith('@@unique') || line.startsWith('@@index')) {
114
- const match = line.match(DIRECTIVE);
115
- if (match) {
116
- const definition = parseDirective(line, match[1] ?? '');
117
- if (line.startsWith('@@unique'))
171
+ const isUnique = line.startsWith('@@unique');
172
+ const definition = parseDirective(line, isUnique ? '@@unique' : '@@index');
173
+ if (definition) {
174
+ if (isUnique)
118
175
  current.compoundUniques.push(definition);
119
176
  else
120
177
  current.indexes.push(definition);
@@ -132,17 +189,19 @@ function parsePushSchema(text, provider) {
132
189
  }
133
190
  // The type is kept as written (upper-cased) because that is what the database
134
191
  // is told; which types exist is what `resolveFieldType` just checked.
192
+ const uniqueArgs = (0, attributes_1.attributeArgs)(line, '@unique');
135
193
  current.fields.push({
136
194
  name: head.name,
137
195
  sqlType: head.type.toUpperCase(),
138
196
  isOptional: head.isOptional,
139
- isId: line.includes('@id'),
140
- isUnique: line.includes('@unique'),
197
+ isId: (0, attributes_1.hasAttribute)(line, '@id'),
198
+ isUnique: (0, attributes_1.hasAttribute)(line, '@unique'),
199
+ ...(uniqueArgs ? { uniqueName: (0, attributes_1.stringOption)(uniqueArgs, 'map') } : {}),
141
200
  // The expression rather than SQL: `uuid()` and `autoincrement()` are spelled
142
201
  // differently per provider, and only the dialect knows how. `@updatedAt` is
143
202
  // `now()` under another name.
144
- defaultExpr: line.match(/@default\((.*)\)/)?.[1]
145
- ?? (line.includes('@updatedAt') ? 'now()' : undefined),
203
+ defaultExpr: (0, attributes_1.attributeArgs)(line, '@default')
204
+ ?? ((0, attributes_1.hasAttribute)(line, '@updatedAt') ? 'now()' : undefined),
146
205
  });
147
206
  }
148
207
  // A declared `dbo` on a provider without schemas would push to a database that
@@ -158,8 +217,39 @@ function parsePushSchema(text, provider) {
158
217
  });
159
218
  }
160
219
  }
220
+ for (const view of views) {
221
+ if (view.declaredSchema === 'dbo' && provider !== 'mssql') {
222
+ issues.push({
223
+ path: view.name,
224
+ message: `schema "dbo" does not exist in ${provider}; remove the @@schema directive or set it ` +
225
+ 'to the schema this database uses',
226
+ });
227
+ }
228
+ if (!view.sql.trim()) {
229
+ issues.push({ path: view.name, message: 'a view needs @@sql("SELECT ...")' });
230
+ }
231
+ }
232
+ // A `@unique` without `map:` must still get the deterministic name `db:migrate`
233
+ // expects, or push leaves a database-named constraint that the next diff cannot
234
+ // recognise and adds a second one for the same column.
235
+ for (const model of models) {
236
+ const table = safeIdentifierName(model.tableName);
237
+ for (const field of model.fields) {
238
+ if (field.isUnique && !field.isId && !field.uniqueName) {
239
+ field.uniqueName = `UQ_${table}_${safeIdentifierName(field.name)}`;
240
+ }
241
+ }
242
+ }
161
243
  const modelNames = models.map((model) => model.name);
244
+ const viewNames = new Set(views.map((view) => view.name));
162
245
  for (const ref of modelRefs) {
246
+ if (viewNames.has(ref.type)) {
247
+ issues.push({
248
+ path: `${ref.model}.${ref.field}`,
249
+ message: `"${ref.type}" is a view and cannot be the target of a relation; select from it directly instead`,
250
+ });
251
+ continue;
252
+ }
163
253
  if (!modelNames.includes(ref.type)) {
164
254
  issues.push({
165
255
  path: `${ref.model}.${ref.field}`,
@@ -167,7 +257,13 @@ function parsePushSchema(text, provider) {
167
257
  });
168
258
  }
169
259
  }
170
- return { models, issues };
260
+ for (const ref of viewRefs) {
261
+ issues.push({
262
+ path: `${ref.view}.${ref.field}`,
263
+ message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, provider, modelNames),
264
+ });
265
+ }
266
+ return { models, views, issues };
171
267
  }
172
268
  /**
173
269
  * The table name to create, `schema.table` unless the model cleared the schema.
@@ -187,3 +283,7 @@ function safeIdentifierName(raw) {
187
283
  function qualifiedTableName(model) {
188
284
  return model.schema ? `${model.schema}.${model.tableName}` : model.tableName;
189
285
  }
286
+ /** The view name to create, qualified only when the view declared a schema. */
287
+ function qualifiedViewName(view) {
288
+ return view.schema ? `${view.schema}.${view.viewName}` : view.viewName;
289
+ }
@@ -88,28 +88,29 @@ class PythonGenerator {
88
88
  content += ` ${this.toSnakeCase(f.name)}: Optional[str] = None\n`;
89
89
  }
90
90
  content += '\n';
91
- // CreateInput
92
- const requiredFields = model.fields.filter(f => !f.isId && !f.isOptional && !f.hasDefault);
93
- const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
94
- content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
95
- for (const f of requiredFields) {
96
- content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
97
- }
98
- for (const f of optionalFields) {
99
- content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
100
- }
101
- if (requiredFields.length === 0 && optionalFields.length === 0)
102
- content += ' pass\n';
103
- content += '\n';
104
- // UpdateInput
105
- const updateFields = model.fields.filter(f => !f.isId);
106
- content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
107
- for (const f of updateFields) {
108
- content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
91
+ // CreateInput / UpdateInput describe writes; a view has none.
92
+ if (!model.isView) {
93
+ const requiredFields = model.fields.filter(f => !f.isId && !f.isOptional && !f.hasDefault);
94
+ const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
95
+ content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
96
+ for (const f of requiredFields) {
97
+ content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
98
+ }
99
+ for (const f of optionalFields) {
100
+ content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
101
+ }
102
+ if (requiredFields.length === 0 && optionalFields.length === 0)
103
+ content += ' pass\n';
104
+ content += '\n';
105
+ const updateFields = model.fields.filter(f => !f.isId);
106
+ content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
107
+ for (const f of updateFields) {
108
+ content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
109
+ }
110
+ if (updateFields.length === 0)
111
+ content += ' pass\n';
112
+ content += '\n';
109
113
  }
110
- if (updateFields.length === 0)
111
- content += ' pass\n';
112
- content += '\n';
113
114
  // FindManyArgs
114
115
  content += `@dataclass\nclass ${name}FindManyArgs:\n """ORM-style args for ${name}.find_many()."""\n`;
115
116
  content += ` where: Optional[${name}WhereInput] = None\n`;
@@ -292,6 +293,7 @@ class PythonGenerator {
292
293
  fs_1.default.writeFileSync(path_1.default.join(outputDir, 'an5_models.py'), content);
293
294
  }
294
295
  generateClient(models, outputDir) {
296
+ const hasViews = models.some((model) => model.isView);
295
297
  let content = '# This file is auto-generated. Do not edit directly.\n';
296
298
  content += 'import os\n';
297
299
  content += 'from typing import Dict, List, Optional, Any, Callable, TYPE_CHECKING\n\n';
@@ -300,7 +302,7 @@ class PythonGenerator {
300
302
  // generated output dir, so no relative-import fallback here — a
301
303
  // relative fallback would be unresolvable and trip Pylance
302
304
  // `reportMissingImports` on every generated client.
303
- content += 'from an5_adapter import An5Adapter, AdapterTableClient, create_an5_adapter, set_adapter_metadata\n\n';
305
+ content += `from an5_adapter import An5Adapter, AdapterTableClient${hasViews ? ', ViewClient' : ''}, create_an5_adapter, set_adapter_metadata\n\n`;
304
306
  const metadataModule = this.metadataModule();
305
307
  content += 'try:\n';
306
308
  content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
@@ -335,6 +337,17 @@ class PythonGenerator {
335
337
  for (const model of models) {
336
338
  const propName = this.toSnakeCase(model.name) + 's';
337
339
  const singleName = this.toSnakeCase(model.name);
340
+ if (model.isView) {
341
+ // The adapter's ViewClient refuses writes; assigning it here is what makes
342
+ // `db.activeUsers.update(...)` fail with the read-only message instead of
343
+ // reaching the database as a table write.
344
+ content += ` client = self.adapter.view("${model.name}")\n`;
345
+ content += ` self.${model.name}: ViewClient = client\n`;
346
+ content += ` self.${model.name}s: ViewClient = client\n`;
347
+ content += ` self.${singleName}: ViewClient = client\n`;
348
+ content += ` self.${propName}: ViewClient = client\n`;
349
+ continue;
350
+ }
338
351
  content += ` client = AdapterTableClient(self.adapter, "${model.name}")\n`;
339
352
  content += ` self.${model.name}: AdapterTableClient["${model.name}Row"] = client\n`;
340
353
  content += ` self.${model.name}s: AdapterTableClient["${model.name}Row"] = client\n`;
@@ -432,35 +432,38 @@ pub struct DateTimeFilter {
432
432
  s.push(` pub ${snake}: Option<SortOrder>,`);
433
433
  }
434
434
  s.push(`}\n`);
435
- // CreateInput (skip @id auto fields from required set via hasDefault)
436
- s.push(`/// Typed data for creating a new ${name} record.`);
437
- s.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`);
438
- s.push(`pub struct ${name}CreateInput {`);
439
- for (const f of model.fields) {
440
- if (f.isId && f.hasDefault)
441
- continue;
442
- const snake = this.toSnakeCase(f.name);
443
- const isOpt = f.isOptional || f.hasDefault;
444
- const rsType = this.mapRustType(f, isOpt);
445
- if (isOpt) {
435
+ // CreateInput / UpdateInput describe writes; a view has none.
436
+ if (!model.isView) {
437
+ // CreateInput (skip @id auto fields from required set via hasDefault)
438
+ s.push(`/// Typed data for creating a new ${name} record.`);
439
+ s.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`);
440
+ s.push(`pub struct ${name}CreateInput {`);
441
+ for (const f of model.fields) {
442
+ if (f.isId && f.hasDefault)
443
+ continue;
444
+ const snake = this.toSnakeCase(f.name);
445
+ const isOpt = f.isOptional || f.hasDefault;
446
+ const rsType = this.mapRustType(f, isOpt);
447
+ if (isOpt) {
448
+ s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
449
+ }
450
+ s.push(` pub ${snake}: ${rsType},`);
451
+ }
452
+ s.push(`}\n`);
453
+ // UpdateInput (all non-id optional)
454
+ s.push(`/// Typed data for updating an existing ${name} record.`);
455
+ s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
456
+ s.push(`pub struct ${name}UpdateInput {`);
457
+ for (const f of model.fields) {
458
+ if (f.isId)
459
+ continue;
460
+ const snake = this.toSnakeCase(f.name);
461
+ const rsType = this.mapRustType(f, true);
446
462
  s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
463
+ s.push(` pub ${snake}: ${rsType},`);
447
464
  }
448
- s.push(` pub ${snake}: ${rsType},`);
449
- }
450
- s.push(`}\n`);
451
- // UpdateInput (all non-id optional)
452
- s.push(`/// Typed data for updating an existing ${name} record.`);
453
- s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
454
- s.push(`pub struct ${name}UpdateInput {`);
455
- for (const f of model.fields) {
456
- if (f.isId)
457
- continue;
458
- const snake = this.toSnakeCase(f.name);
459
- const rsType = this.mapRustType(f, true);
460
- s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
461
- s.push(` pub ${snake}: ${rsType},`);
465
+ s.push(`}\n`);
462
466
  }
463
- s.push(`}\n`);
464
467
  // FindManyArgs / FindFirstArgs / FindUniqueArgs / CountArgs
465
468
  s.push(`/// ORM-style args for ${name}.find_many().`);
466
469
  s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
@@ -505,14 +508,16 @@ pub struct DateTimeFilter {
505
508
  s.push(` pub distance_metric: String,`);
506
509
  s.push(`}\n`);
507
510
  // UpdateArgs carries the typed input alongside the where clause.
508
- s.push(`/// ORM-style args for ${name}.update().`);
509
- s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
510
- s.push(`pub struct ${name}UpdateArgs {`);
511
- s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
512
- s.push(` pub where_: Option<${name}WhereInput>,`);
513
- s.push(` #[serde(default)]`);
514
- s.push(` pub data: ${name}UpdateInput,`);
515
- s.push(`}\n`);
511
+ if (!model.isView) {
512
+ s.push(`/// ORM-style args for ${name}.update().`);
513
+ s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
514
+ s.push(`pub struct ${name}UpdateArgs {`);
515
+ s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
516
+ s.push(` pub where_: Option<${name}WhereInput>,`);
517
+ s.push(` #[serde(default)]`);
518
+ s.push(` pub data: ${name}UpdateInput,`);
519
+ s.push(`}\n`);
520
+ }
516
521
  }
517
522
  return s.join('\n') + '\n';
518
523
  }
@@ -601,7 +606,7 @@ pub struct DateTimeFilter {
601
606
 
602
607
  use an5_adapters::{
603
608
  AdapterMetadata, An5Adapter, CountArgs, DeleteManyArgs, FindManyArgs, RowMap, TableClient,
604
- UpdateArgs, VectorSearchArgs,
609
+ UpdateArgs, VectorSearchArgs${models.some((model) => model.isView) ? ', ViewClient' : ''},
605
610
  };
606
611
  use serde::de::DeserializeOwned;
607
612
 
@@ -788,6 +793,11 @@ impl SqlBuilder {
788
793
  buildModelRuntimeHandle(model) {
789
794
  const name = this.capitalize(model.name);
790
795
  const snake = this.toSnakeCase(model.name);
796
+ // A view is read through the runtime's read-only ViewClient, so the handle
797
+ // for a view cannot expose write methods at all.
798
+ const reader = model.isView
799
+ ? `self.db.adapter().view("${model.name}")`
800
+ : `self.db.table("${model.name}")`;
791
801
  const lines = [];
792
802
  lines.push(`/// Typed queries for ${name}. Obtained via \`An5Client::${snake}()\`.`);
793
803
  lines.push(`#[derive(Clone)]`);
@@ -807,9 +817,7 @@ impl SqlBuilder {
807
817
  lines.push(` pub fn adapter(&self) -> &An5Adapter { self.db.adapter() }`);
808
818
  lines.push(``);
809
819
  lines.push(` pub async fn find_many(&self, args: &${name}FindManyArgs) -> Result<Vec<${name}>> {`);
810
- lines.push(` let rows = self`);
811
- lines.push(` .db`);
812
- lines.push(` .table("${model.name}")`);
820
+ lines.push(` let rows = ${reader}`);
813
821
  lines.push(` .find_many(&FindManyArgs {`);
814
822
  lines.push(` r#where: to_value(&args.where_)?,`);
815
823
  lines.push(` order_by: to_value(&args.order_by)?,`);
@@ -824,9 +832,7 @@ impl SqlBuilder {
824
832
  lines.push(``);
825
833
  lines.push(` /// The rows nearest \`vector\`, each carrying a \`distance\`.`);
826
834
  lines.push(` pub async fn vector_search(&self, args: &${name}VectorSearchArgs) -> Result<Vec<(${name}, f64)>> {`);
827
- lines.push(` let rows = self`);
828
- lines.push(` .db`);
829
- lines.push(` .table("${model.name}")`);
835
+ lines.push(` let rows = ${reader}`);
830
836
  lines.push(` .vector_search(&VectorSearchArgs {`);
831
837
  lines.push(` vector: args.vector.iter().map(|v| *v as f64).collect(),`);
832
838
  lines.push(` take: args.take.unwrap_or(0),`);
@@ -844,9 +850,7 @@ impl SqlBuilder {
844
850
  lines.push(` }`);
845
851
  lines.push(``);
846
852
  lines.push(` pub async fn find_first(&self, args: &${name}FindFirstArgs) -> Result<Option<${name}>> {`);
847
- lines.push(` let row = self`);
848
- lines.push(` .db`);
849
- lines.push(` .table("${model.name}")`);
853
+ lines.push(` let row = ${reader}`);
850
854
  lines.push(` .find_first(&FindManyArgs {`);
851
855
  lines.push(` r#where: to_value(&args.where_)?,`);
852
856
  lines.push(` order_by: to_value(&args.order_by)?,`);
@@ -860,9 +864,7 @@ impl SqlBuilder {
860
864
  lines.push(` }`);
861
865
  lines.push(``);
862
866
  lines.push(` pub async fn find_unique(&self, args: &${name}FindUniqueArgs) -> Result<Option<${name}>> {`);
863
- lines.push(` let row = self`);
864
- lines.push(` .db`);
865
- lines.push(` .table("${model.name}")`);
867
+ lines.push(` let row = ${reader}`);
866
868
  lines.push(` .find_unique(&FindManyArgs {`);
867
869
  lines.push(` r#where: to_value(&args.where_)?,`);
868
870
  lines.push(` ..Default::default()`);
@@ -875,37 +877,40 @@ impl SqlBuilder {
875
877
  lines.push(` }`);
876
878
  lines.push(``);
877
879
  lines.push(` pub async fn count(&self, args: &${name}FindUniqueArgs) -> Result<i64> {`);
878
- lines.push(` self.db`);
879
- lines.push(` .table("${model.name}")`);
880
+ lines.push(` ${reader}`);
880
881
  lines.push(` .count(&CountArgs { r#where: to_value(&args.where_)? })`);
881
882
  lines.push(` .await`);
882
883
  lines.push(` }`);
883
884
  lines.push(``);
884
- lines.push(` pub async fn create(&self, data: &${name}CreateInput) -> Result<${name}> {`);
885
- lines.push(` let row = self`);
886
- lines.push(` .db`);
887
- lines.push(` .table("${model.name}")`);
888
- lines.push(` .create(&to_map(data)?)`);
889
- lines.push(` .await?;`);
890
- lines.push(` deserialize_row(row)`);
891
- lines.push(` }`);
892
- lines.push(``);
893
- lines.push(` pub async fn update(&self, args: &${name}UpdateArgs) -> Result<u64> {`);
894
- lines.push(` self.db`);
895
- lines.push(` .table("${model.name}")`);
896
- lines.push(` .update(&UpdateArgs {`);
897
- lines.push(` r#where: to_value(&args.where_)?,`);
898
- lines.push(` data: serde_json::to_value(&args.data)?,`);
899
- lines.push(` })`);
900
- lines.push(` .await`);
901
- lines.push(` }`);
902
- lines.push(``);
903
- lines.push(` pub async fn delete(&self, args: &${name}FindUniqueArgs) -> Result<u64> {`);
904
- lines.push(` self.db`);
905
- lines.push(` .table("${model.name}")`);
906
- lines.push(` .delete_many(&DeleteManyArgs { r#where: to_value(&args.where_)? })`);
907
- lines.push(` .await`);
908
- lines.push(` }`);
885
+ // Writes are generated for tables only: a view has no CreateInput or
886
+ // UpdateInput, so these methods could not be called even if emitted.
887
+ if (!model.isView) {
888
+ lines.push(` pub async fn create(&self, data: &${name}CreateInput) -> Result<${name}> {`);
889
+ lines.push(` let row = self`);
890
+ lines.push(` .db`);
891
+ lines.push(` .table("${model.name}")`);
892
+ lines.push(` .create(&to_map(data)?)`);
893
+ lines.push(` .await?;`);
894
+ lines.push(` deserialize_row(row)`);
895
+ lines.push(` }`);
896
+ lines.push(``);
897
+ lines.push(` pub async fn update(&self, args: &${name}UpdateArgs) -> Result<u64> {`);
898
+ lines.push(` self.db`);
899
+ lines.push(` .table("${model.name}")`);
900
+ lines.push(` .update(&UpdateArgs {`);
901
+ lines.push(` r#where: to_value(&args.where_)?,`);
902
+ lines.push(` data: serde_json::to_value(&args.data)?,`);
903
+ lines.push(` })`);
904
+ lines.push(` .await`);
905
+ lines.push(` }`);
906
+ lines.push(``);
907
+ lines.push(` pub async fn delete(&self, args: &${name}FindUniqueArgs) -> Result<u64> {`);
908
+ lines.push(` self.db`);
909
+ lines.push(` .table("${model.name}")`);
910
+ lines.push(` .delete_many(&DeleteManyArgs { r#where: to_value(&args.where_)? })`);
911
+ lines.push(` .await`);
912
+ lines.push(` }`);
913
+ }
909
914
  lines.push(`}`);
910
915
  lines.push(``);
911
916
  return lines.join('\n') + '\n';