@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
@@ -133,26 +133,27 @@ namespace An5Orm
133
133
  content += ` public string ${this.capitalize(f.name)} { get; set; } // "asc" or "desc"\n`;
134
134
  }
135
135
  content += ` }\n\n`;
136
- // CreateInput
137
- content += ` /// <summary>Typed data for creating a new ${name} record.</summary>\n`;
138
- content += ` public class ${name}CreateInput\n {\n`;
139
- for (const f of model.fields) {
140
- if (!f.isId) {
141
- const ct = this.mapType(f, f.isOptional || f.hasDefault);
142
- content += ` public ${ct} ${this.capitalize(f.name)} { get; set; }\n`;
136
+ // Create / update / delete inputs describe writes; a view has none.
137
+ if (!model.isView) {
138
+ content += ` /// <summary>Typed data for creating a new ${name} record.</summary>\n`;
139
+ content += ` public class ${name}CreateInput\n {\n`;
140
+ for (const f of model.fields) {
141
+ if (!f.isId) {
142
+ const ct = this.mapType(f, f.isOptional || f.hasDefault);
143
+ content += ` public ${ct} ${this.capitalize(f.name)} { get; set; }\n`;
144
+ }
143
145
  }
144
- }
145
- content += ` }\n\n`;
146
- // UpdateInput
147
- content += ` /// <summary>Typed data for updating an existing ${name} record.</summary>\n`;
148
- content += ` public class ${name}UpdateInput\n {\n`;
149
- for (const f of model.fields) {
150
- if (!f.isId) {
151
- const ut = this.mapType(f, true); // always nullable for update
152
- content += ` public ${ut} ${this.capitalize(f.name)} { get; set; }\n`;
146
+ content += ` }\n\n`;
147
+ content += ` /// <summary>Typed data for updating an existing ${name} record.</summary>\n`;
148
+ content += ` public class ${name}UpdateInput\n {\n`;
149
+ for (const f of model.fields) {
150
+ if (!f.isId) {
151
+ const ut = this.mapType(f, true); // always nullable for update
152
+ content += ` public ${ut} ${this.capitalize(f.name)} { get; set; }\n`;
153
+ }
153
154
  }
155
+ content += ` }\n\n`;
154
156
  }
155
- content += ` }\n\n`;
156
157
  // FindManyArgs
157
158
  content += ` /// <summary>ORM-style args for ${name}.FindMany().</summary>\n`;
158
159
  content += ` public class ${name}FindManyArgs\n {\n`;
@@ -175,10 +176,12 @@ namespace An5Orm
175
176
  content += ` public ${name}WhereInput Where { get; set; }\n`;
176
177
  content += ` }\n\n`;
177
178
  // DeleteManyArgs
178
- content += ` /// <summary>ORM-style args for ${name}.DeleteMany().</summary>\n`;
179
- content += ` public class ${name}DeleteManyArgs\n {\n`;
180
- content += ` public ${name}WhereInput Where { get; set; }\n`;
181
- content += ` }\n\n`;
179
+ if (!model.isView) {
180
+ content += ` /// <summary>ORM-style args for ${name}.DeleteMany().</summary>\n`;
181
+ content += ` public class ${name}DeleteManyArgs\n {\n`;
182
+ content += ` public ${name}WhereInput Where { get; set; }\n`;
183
+ content += ` }\n\n`;
184
+ }
182
185
  // CountArgs
183
186
  content += ` /// <summary>ORM-style args for ${name}.Count().</summary>\n`;
184
187
  content += ` public class ${name}CountArgs\n {\n`;
@@ -477,8 +480,9 @@ namespace An5Orm
477
480
  `;
478
481
  for (const model of models) {
479
482
  const name = this.capitalize(model.name);
480
- content += ` public TableClient<${model.name}> ${name}s => new TableClient<${model.name}>(ConnectionString, "${(0, types_1.dottedTableName)(model)}");\n`;
481
- content += ` public TableClient<${model.name}> ${name} => ${name}s;\n`;
483
+ const client = model.isView ? 'ViewClient' : 'TableClient';
484
+ content += ` public ${client}<${model.name}> ${name}s => new ${client}<${model.name}>(ConnectionString, "${(0, types_1.dottedTableName)(model)}");\n`;
485
+ content += ` public ${client}<${model.name}> ${name} => ${name}s;\n`;
482
486
  }
483
487
  content += ` }
484
488
 
@@ -1172,6 +1176,46 @@ namespace An5Orm
1172
1176
  return false;
1173
1177
  }
1174
1178
  }
1179
+
1180
+ /// <summary>
1181
+ /// Read-only client for a database view.
1182
+ ///
1183
+ /// It exposes the same read methods as <see cref="TableClient{T}"/> and no
1184
+ /// write methods, so attempting to write a view fails at compile time rather
1185
+ /// than in the database. The view's rows still map onto the generated entity
1186
+ /// class, which is why the queries delegate instead of being duplicated.
1187
+ /// </summary>
1188
+ public class ViewClient<T> where T : new()
1189
+ {
1190
+ private readonly TableClient<T> _table;
1191
+
1192
+ public ViewClient(string connectionString, string viewName)
1193
+ {
1194
+ _table = new TableClient<T>(connectionString, viewName);
1195
+ }
1196
+
1197
+ public string ConnectionString => _table.ConnectionString;
1198
+ public string ViewName => _table.TableName;
1199
+ public An5Dialect Dialect => _table.Dialect;
1200
+
1201
+ public List<T> QueryRaw(string query, Dictionary<string, object> parameters = null)
1202
+ => _table.QueryRaw(query, parameters);
1203
+
1204
+ public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null)
1205
+ => _table.FindMany(whereClause, parameters);
1206
+
1207
+ public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
1208
+ => _table.FindFirst(whereClause, parameters);
1209
+
1210
+ public T FindUnique(object id)
1211
+ => _table.FindUnique(id);
1212
+
1213
+ public int Count(string whereClause = null, Dictionary<string, object> parameters = null)
1214
+ => _table.Count(whereClause, parameters);
1215
+
1216
+ public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
1217
+ => _table.VectorSearch(vector, take, whereClause, parameters, vectorField, distanceMetric);
1218
+ }
1175
1219
  }
1176
1220
  `;
1177
1221
  fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'An5DbContext.cs'), content);
@@ -22,6 +22,14 @@ export declare class GolangGenerator {
22
22
  private generateBaseOrmTypes;
23
23
  private generateDbContext;
24
24
  private generateTableClientStruct;
25
+ /**
26
+ * The read-only client used for views.
27
+ *
28
+ * Go has no way to hide methods from an embedded value, so wrapping rather
29
+ * than embedding is what makes a write to a view a compile error. The read
30
+ * methods delegate, which keeps a view's rows scanning exactly like a table's.
31
+ */
32
+ private generateViewClientStruct;
25
33
  private generateQueryAndScan;
26
34
  /**
27
35
  * The vector codec the generated client needs on both sides of a query.
@@ -182,6 +182,7 @@ func GetDefaultConnectionString() string {
182
182
  content += this.generateBaseOrmTypes();
183
183
  content += this.generateDbContext(models);
184
184
  content += this.generateTableClientStruct();
185
+ content += this.generateViewClientStruct();
185
186
  content += this.generateQueryAndScan();
186
187
  content += this.generateWhereBuilder();
187
188
  content += this.generateCrudMethods();
@@ -311,8 +312,9 @@ func SortOrderPtr(s SortOrder) *SortOrder { return &s }
311
312
  let content = `// An5DbContext represents the main ORM database context.\ntype An5DbContext struct {\n\tDB *sql.DB\n\tDialect Dialect\n`;
312
313
  for (const model of models) {
313
314
  const name = this.capitalize(model.name);
314
- content += `\t${name}s *TableClient[${model.name}]\n`;
315
- content += `\t${name} *TableClient[${model.name}]\n`;
315
+ const client = model.isView ? 'ViewClient' : 'TableClient';
316
+ content += `\t${name}s *${client}[${model.name}]\n`;
317
+ content += `\t${name} *${client}[${model.name}]\n`;
316
318
  }
317
319
  content += `}
318
320
 
@@ -328,7 +330,8 @@ func NewAn5DbContextWithConnStr(db *sql.DB, connStr string) *An5DbContext {
328
330
  `;
329
331
  for (const model of models) {
330
332
  const name = this.capitalize(model.name);
331
- content += `\tclient${name} := NewTableClient[${model.name}](db, "${(0, types_1.dottedTableName)(model)}", d)\n`;
333
+ const client = model.isView ? 'NewViewClient' : 'NewTableClient';
334
+ content += `\tclient${name} := ${client}[${model.name}](db, "${(0, types_1.dottedTableName)(model)}", d)\n`;
332
335
  content += `\tctx.${name}s = client${name}\n`;
333
336
  content += `\tctx.${name} = client${name}\n`;
334
337
  }
@@ -380,6 +383,65 @@ func (c *TableClient[T]) ph(n int) string {
380
383
  \treturn "?"
381
384
  }
382
385
 
386
+ `;
387
+ }
388
+ // ─── ViewClient ────────────────────────────────────────────────────────────
389
+ /**
390
+ * The read-only client used for views.
391
+ *
392
+ * Go has no way to hide methods from an embedded value, so wrapping rather
393
+ * than embedding is what makes a write to a view a compile error. The read
394
+ * methods delegate, which keeps a view's rows scanning exactly like a table's.
395
+ */
396
+ generateViewClientStruct() {
397
+ return `// ViewClient provides read-only access to a database view.
398
+ type ViewClient[T any] struct {
399
+ \tDB *sql.DB
400
+ \tDialect Dialect
401
+ \tViewName string
402
+ \ttable *TableClient[T]
403
+ }
404
+
405
+ // NewViewClient creates a typed read-only client for a view.
406
+ func NewViewClient[T any](db *sql.DB, viewName string, dialect Dialect) *ViewClient[T] {
407
+ \treturn &ViewClient[T]{
408
+ \t\tDB: db,
409
+ \t\tDialect: dialect,
410
+ \t\tViewName: viewName,
411
+ \t\ttable: NewTableClient[T](db, viewName, dialect),
412
+ \t}
413
+ }
414
+
415
+ // FindMany queries the view with ORM-style args.
416
+ func (c *ViewClient[T]) FindMany(ctx context.Context, args interface{}) ([]T, error) {
417
+ \treturn c.table.FindMany(ctx, args)
418
+ }
419
+
420
+ // FindFirst returns the first matching row of the view.
421
+ func (c *ViewClient[T]) FindFirst(ctx context.Context, args interface{}) (*T, error) {
422
+ \treturn c.table.FindFirst(ctx, args)
423
+ }
424
+
425
+ // FindUnique returns one row of the view by primary key.
426
+ func (c *ViewClient[T]) FindUnique(ctx context.Context, args interface{}) (*T, error) {
427
+ \treturn c.table.FindUnique(ctx, args)
428
+ }
429
+
430
+ // Count returns how many rows of the view match.
431
+ func (c *ViewClient[T]) Count(ctx context.Context, args interface{}) (int64, error) {
432
+ \treturn c.table.Count(ctx, args)
433
+ }
434
+
435
+ // QueryRaw runs a raw SELECT against the view.
436
+ func (c *ViewClient[T]) QueryRaw(ctx context.Context, query string, args ...interface{}) ([]T, error) {
437
+ \treturn c.table.QueryRaw(ctx, query, args...)
438
+ }
439
+
440
+ // VectorSearch searches a vector column of the view.
441
+ func (c *ViewClient[T]) VectorSearch(ctx context.Context, vector []float64, opts ...VectorSearchOptions) ([]T, error) {
442
+ \treturn c.table.VectorSearch(ctx, vector, opts...)
443
+ }
444
+
383
445
  `;
384
446
  }
385
447
  // ─── queryAndScan ──────────────────────────────────────────────────────────
@@ -1309,24 +1371,25 @@ var _ = time.Time{}
1309
1371
  content += `\t${this.capitalize(field.name)} *SortOrder\n`;
1310
1372
  }
1311
1373
  content += `}\n\n`;
1312
- // CreateInput
1313
- content += `// ${name}CreateInput holds data for creating a new ${name} record.\ntype ${name}CreateInput struct {\n`;
1314
- for (const field of model.fields) {
1315
- if (!field.isId) {
1316
- const goType = this.mapGoType(field, field.isOptional || field.hasDefault);
1317
- content += `\t${this.capitalize(field.name)} ${goType}\n`;
1374
+ // CreateInput / UpdateInput / DeleteManyArgs describe writes; a view has none.
1375
+ if (!model.isView) {
1376
+ content += `// ${name}CreateInput holds data for creating a new ${name} record.\ntype ${name}CreateInput struct {\n`;
1377
+ for (const field of model.fields) {
1378
+ if (!field.isId) {
1379
+ const goType = this.mapGoType(field, field.isOptional || field.hasDefault);
1380
+ content += `\t${this.capitalize(field.name)} ${goType}\n`;
1381
+ }
1318
1382
  }
1319
- }
1320
- content += `}\n\n`;
1321
- // UpdateInput
1322
- content += `// ${name}UpdateInput holds data for updating an existing ${name} record.\ntype ${name}UpdateInput struct {\n`;
1323
- for (const field of model.fields) {
1324
- if (!field.isId) {
1325
- const goType = this.mapGoType(field, true); // always pointer so we can detect zero
1326
- content += `\t${this.capitalize(field.name)} ${goType}\n`;
1383
+ content += `}\n\n`;
1384
+ content += `// ${name}UpdateInput holds data for updating an existing ${name} record.\ntype ${name}UpdateInput struct {\n`;
1385
+ for (const field of model.fields) {
1386
+ if (!field.isId) {
1387
+ const goType = this.mapGoType(field, true); // always pointer so we can detect zero
1388
+ content += `\t${this.capitalize(field.name)} ${goType}\n`;
1389
+ }
1327
1390
  }
1391
+ content += `}\n\n`;
1328
1392
  }
1329
- content += `}\n\n`;
1330
1393
  // FindManyArgs
1331
1394
  content += `// ${name}FindManyArgs provides ORM-style args for FindMany queries on ${name}.\ntype ${name}FindManyArgs struct {\n`;
1332
1395
  content += `\tWhere *${name}WhereInput\n`;
@@ -1346,9 +1409,11 @@ var _ = time.Time{}
1346
1409
  content += `\tWhere *${name}WhereInput\n`;
1347
1410
  content += `}\n\n`;
1348
1411
  // DeleteManyArgs
1349
- content += `// ${name}DeleteManyArgs provides ORM-style args for DeleteMany queries on ${name}.\ntype ${name}DeleteManyArgs struct {\n`;
1350
- content += `\tWhere *${name}WhereInput\n`;
1351
- content += `}\n\n`;
1412
+ if (!model.isView) {
1413
+ content += `// ${name}DeleteManyArgs provides ORM-style args for DeleteMany queries on ${name}.\ntype ${name}DeleteManyArgs struct {\n`;
1414
+ content += `\tWhere *${name}WhereInput\n`;
1415
+ content += `}\n\n`;
1416
+ }
1352
1417
  // CountArgs
1353
1418
  content += `// ${name}CountArgs provides ORM-style args for Count queries on ${name}.\ntype ${name}CountArgs struct {\n`;
1354
1419
  content += `\tWhere *${name}WhereInput\n`;
@@ -51,6 +51,14 @@ export declare class JavaGenerator {
51
51
  private generateMetadata;
52
52
  private generateConfig;
53
53
  private modelClientSource;
54
+ /**
55
+ * The typed client used for views.
56
+ *
57
+ * <p>It repeats {@link ModelClient}'s read signatures and stops there: with no
58
+ * {@code create}/{@code update}/{@code delete} to call, writing a view is a
59
+ * compile error rather than a runtime refusal.
60
+ */
61
+ private viewClientSource;
54
62
  private generateDbContext;
55
63
  private tableName;
56
64
  }
@@ -1224,10 +1224,105 @@ public final class ModelClient<T> {
1224
1224
  return value instanceof Number ? ((Number) value).intValue() : 0;
1225
1225
  }
1226
1226
  }
1227
+ `;
1228
+ }
1229
+ /**
1230
+ * The typed client used for views.
1231
+ *
1232
+ * <p>It repeats {@link ModelClient}'s read signatures and stops there: with no
1233
+ * {@code create}/{@code update}/{@code delete} to call, writing a view is a
1234
+ * compile error rather than a runtime refusal.
1235
+ */
1236
+ viewClientSource() {
1237
+ return `// This file is auto-generated. Do not edit directly.
1238
+ package an5.client;
1239
+
1240
+ import java.sql.SQLException;
1241
+ import java.util.ArrayList;
1242
+ import java.util.List;
1243
+ import java.util.Map;
1244
+ import java.util.function.Function;
1245
+
1246
+ import an5.adapters.An5Aggregate;
1247
+ import an5.adapters.An5GroupBy;
1248
+ import an5.adapters.An5Query;
1249
+ import an5.adapters.An5ViewClient;
1250
+
1251
+ /** A read-only, typed client for one database view. */
1252
+ public final class ViewClient<T> {
1253
+
1254
+ private final An5ViewClient view;
1255
+ private final Function<Map<String, Object>, T> fromRow;
1256
+
1257
+ ViewClient(An5ViewClient view, Function<Map<String, Object>, T> fromRow) {
1258
+ this.view = view;
1259
+ this.fromRow = fromRow;
1260
+ }
1261
+
1262
+ /** The view name this client reads. */
1263
+ public String view() {
1264
+ return view.viewName();
1265
+ }
1266
+
1267
+ public List<T> findMany() throws SQLException {
1268
+ return findMany(new An5Query());
1269
+ }
1270
+
1271
+ public List<T> findMany(An5Query query) throws SQLException {
1272
+ return map(view.findMany(query));
1273
+ }
1274
+
1275
+ public T findFirst(An5Query query) throws SQLException {
1276
+ return one(view.findFirst(query));
1277
+ }
1278
+
1279
+ public T findFirst(Map<String, Object> where) throws SQLException {
1280
+ return one(view.findFirst(where));
1281
+ }
1282
+
1283
+ public T findUnique(Map<String, Object> where) throws SQLException {
1284
+ return one(view.findUnique(where));
1285
+ }
1286
+
1287
+ public long count(Map<String, Object> where) throws SQLException {
1288
+ return view.count(where);
1289
+ }
1290
+
1291
+ public Map<String, Object> aggregate(An5Aggregate aggregate) throws SQLException {
1292
+ return view.aggregate(aggregate);
1293
+ }
1294
+
1295
+ public List<Map<String, Object>> groupBy(An5GroupBy group) throws SQLException {
1296
+ return view.groupBy(group);
1297
+ }
1298
+
1299
+ public List<T> vectorSearch(
1300
+ double[] vector, Integer take, Map<String, Object> where, String vectorField, String metric)
1301
+ throws SQLException {
1302
+ return map(view.vectorSearch(vector, take, where, vectorField, metric));
1303
+ }
1304
+
1305
+ public List<T> vectorSearch(double[] vector, Integer take) throws SQLException {
1306
+ return vectorSearch(vector, take, null, "embedding", "cosine");
1307
+ }
1308
+
1309
+ private List<T> map(List<Map<String, Object>> rows) {
1310
+ List<T> mapped = new ArrayList<T>();
1311
+ for (Map<String, Object> row : rows) {
1312
+ mapped.add(fromRow.apply(row));
1313
+ }
1314
+ return mapped;
1315
+ }
1316
+
1317
+ private T one(Map<String, Object> row) {
1318
+ return row == null ? null : fromRow.apply(row);
1319
+ }
1320
+ }
1227
1321
  `;
1228
1322
  }
1229
1323
  generateDbContext(models) {
1230
1324
  fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'ModelClient.java'), this.modelClientSource());
1325
+ fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'ViewClient.java'), this.viewClientSource());
1231
1326
  let content = `// This file is auto-generated. Do not edit directly.
1232
1327
  package an5.client;
1233
1328
 
@@ -1259,7 +1354,7 @@ public class An5DbContext implements AutoCloseable {
1259
1354
  if (declared.has(property))
1260
1355
  continue;
1261
1356
  declared.add(property);
1262
- content += ` private final ModelClient<${model.name}> ${property}Client;\n`;
1357
+ content += ` private final ${model.isView ? 'ViewClient' : 'ModelClient'}<${model.name}> ${property}Client;\n`;
1263
1358
  }
1264
1359
  content += `
1265
1360
  /** Opens a context, registering this schema with the adapter. */
@@ -1273,6 +1368,11 @@ public class An5DbContext implements AutoCloseable {
1273
1368
  if (assigned.has(property))
1274
1369
  continue;
1275
1370
  assigned.add(property);
1371
+ if (model.isView) {
1372
+ content += ` this.${property}Client = new ViewClient<${model.name}>(\n`;
1373
+ content += ` adapter.view("${model.name}"), ${model.name}::fromRow);\n`;
1374
+ continue;
1375
+ }
1276
1376
  content += ` this.${property}Client = new ModelClient<${model.name}>(\n`;
1277
1377
  content += ` adapter.table("${model.name}"), ${model.name}::fromRow, ${model.name}::toValues);\n`;
1278
1378
  }
@@ -1289,6 +1389,11 @@ public class An5DbContext implements AutoCloseable {
1289
1389
  if (wrapped.has(property))
1290
1390
  continue;
1291
1391
  wrapped.add(property);
1392
+ if (model.isView) {
1393
+ content += ` this.${property}Client = new ViewClient<${model.name}>(\n`;
1394
+ content += ` adapter.view("${model.name}"), ${model.name}::fromRow);\n`;
1395
+ continue;
1396
+ }
1292
1397
  content += ` this.${property}Client = new ModelClient<${model.name}>(\n`;
1293
1398
  content += ` adapter.table("${model.name}"), ${model.name}::fromRow, ${model.name}::toValues);\n`;
1294
1399
  }
@@ -1312,8 +1417,8 @@ public class An5DbContext implements AutoCloseable {
1312
1417
  exposed.add(property);
1313
1418
  const accessor = `get${this.capitalize(property)}`;
1314
1419
  content += `
1315
- /** Queries for \`${model.name}\`. */
1316
- public ModelClient<${model.name}> ${accessor}() {
1420
+ /** ${model.isView ? 'Reads' : 'Queries'} for \`${model.name}\`. */
1421
+ public ${model.isView ? 'ViewClient' : 'ModelClient'}<${model.name}> ${accessor}() {
1317
1422
  return this.${property}Client;
1318
1423
  }
1319
1424
  `;
@@ -663,6 +663,47 @@ class ModelClient<T : Any>(
663
663
  ): List<T> = delegate.vectorSearch(vector, take, filter, vectorField, metric).map(read)
664
664
  }
665
665
 
666
+ /**
667
+ * A read-only, typed client for one view.
668
+ *
669
+ * It mirrors [ModelClient]'s read methods and stops there, so a write to a view does
670
+ * not compile. The runtime's read-only client does the actual work.
671
+ */
672
+ class ViewClient<T : Any>(
673
+ private val delegate: an5.adapters.ViewClient,
674
+ private val read: (Row) -> T,
675
+ ) {
676
+
677
+ /** The view this client reads. */
678
+ val name: String get() = delegate.name
679
+
680
+ fun findMany(): List<T> = delegate.findMany().map(read)
681
+
682
+ fun findMany(block: QueryBuilder.() -> Unit): List<T> = delegate.findMany(block).map(read)
683
+
684
+ fun findFirst(filter: Where): T? = delegate.findFirst(filter)?.let(read)
685
+
686
+ fun findUnique(filter: Where): T? = delegate.findUnique(filter)?.let(read)
687
+
688
+ fun count(filter: Where? = null): Long = delegate.count(filter)
689
+
690
+ /** One row of aggregate values, keyed \`_count\`, \`_sum_<field>\` and so on. */
691
+ fun aggregate(block: AggregateBuilder.() -> Unit): Row = delegate.aggregate(block)
692
+
693
+ /** One row per group, each carrying the group's count and aggregates. */
694
+ fun groupBy(vararg by: String, block: AggregateBuilder.() -> Unit = {}): List<Row> =
695
+ delegate.groupBy(*by, block = block)
696
+
697
+ /** The rows nearest [vector]. */
698
+ fun vectorSearch(
699
+ vector: DoubleArray,
700
+ take: Int = 10,
701
+ filter: Where? = null,
702
+ vectorField: String = "embedding",
703
+ metric: DistanceMetric = DistanceMetric.COSINE,
704
+ ): List<T> = delegate.vectorSearch(vector, take, filter, vectorField, metric).map(read)
705
+ }
706
+
666
707
  /**
667
708
  * The AN5 entry point: one typed client per model, over one connection.
668
709
  *
@@ -687,6 +728,16 @@ class An5Db internal constructor(private val an5: An5) : AutoCloseable {
687
728
  if (seen.has(property))
688
729
  continue;
689
730
  seen.add(property);
731
+ if (model.isView) {
732
+ content += ` /** Reads for \`${model.name}\`. */
733
+ `;
734
+ content += ` val ${property}: ViewClient<${model.name}> =
735
+ `;
736
+ content += ` ViewClient(an5.view("${model.name}"), ${model.name}::fromRow)
737
+
738
+ `;
739
+ continue;
740
+ }
690
741
  content += ` /** Queries for \`${model.name}\`. */
691
742
  `;
692
743
  content += ` val ${property}: ModelClient<${model.name}> =
@@ -13,6 +13,7 @@ export declare class SchemaParser {
13
13
  private schemaText;
14
14
  private issues;
15
15
  private modelRefs;
16
+ private viewRefs;
16
17
  private models;
17
18
  /**
18
19
  * @param schemaDir directory holding the `.an5` files