@an5/orm 1.3.1 → 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 +107 -27
  14. package/dist/generator/src/golang-generator.d.ts +8 -0
  15. package/dist/generator/src/golang-generator.js +116 -49
  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 +108 -28
  50. package/generator/src/golang-generator.ts +119 -50
  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
@@ -143,27 +143,28 @@ namespace An5Orm
143
143
  }
144
144
  content += ` }\n\n`;
145
145
 
146
- // CreateInput
147
- content += ` /// <summary>Typed data for creating a new ${name} record.</summary>\n`;
148
- content += ` public class ${name}CreateInput\n {\n`;
149
- for (const f of model.fields) {
150
- if (!f.isId) {
151
- const ct = this.mapType(f, f.isOptional || f.hasDefault);
152
- content += ` public ${ct} ${this.capitalize(f.name)} { get; set; }\n`;
146
+ // Create / update / delete inputs describe writes; a view has none.
147
+ if (!model.isView) {
148
+ content += ` /// <summary>Typed data for creating a new ${name} record.</summary>\n`;
149
+ content += ` public class ${name}CreateInput\n {\n`;
150
+ for (const f of model.fields) {
151
+ if (!f.isId) {
152
+ const ct = this.mapType(f, f.isOptional || f.hasDefault);
153
+ content += ` public ${ct} ${this.capitalize(f.name)} { get; set; }\n`;
154
+ }
153
155
  }
154
- }
155
- content += ` }\n\n`;
156
-
157
- // UpdateInput
158
- content += ` /// <summary>Typed data for updating an existing ${name} record.</summary>\n`;
159
- content += ` public class ${name}UpdateInput\n {\n`;
160
- for (const f of model.fields) {
161
- if (!f.isId) {
162
- const ut = this.mapType(f, true); // always nullable for update
163
- content += ` public ${ut} ${this.capitalize(f.name)} { get; set; }\n`;
156
+ content += ` }\n\n`;
157
+
158
+ content += ` /// <summary>Typed data for updating an existing ${name} record.</summary>\n`;
159
+ content += ` public class ${name}UpdateInput\n {\n`;
160
+ for (const f of model.fields) {
161
+ if (!f.isId) {
162
+ const ut = this.mapType(f, true); // always nullable for update
163
+ content += ` public ${ut} ${this.capitalize(f.name)} { get; set; }\n`;
164
+ }
164
165
  }
166
+ content += ` }\n\n`;
165
167
  }
166
- content += ` }\n\n`;
167
168
 
168
169
  // FindManyArgs
169
170
  content += ` /// <summary>ORM-style args for ${name}.FindMany().</summary>\n`;
@@ -190,10 +191,12 @@ namespace An5Orm
190
191
  content += ` }\n\n`;
191
192
 
192
193
  // DeleteManyArgs
193
- content += ` /// <summary>ORM-style args for ${name}.DeleteMany().</summary>\n`;
194
- content += ` public class ${name}DeleteManyArgs\n {\n`;
195
- content += ` public ${name}WhereInput Where { get; set; }\n`;
196
- content += ` }\n\n`;
194
+ if (!model.isView) {
195
+ content += ` /// <summary>ORM-style args for ${name}.DeleteMany().</summary>\n`;
196
+ content += ` public class ${name}DeleteManyArgs\n {\n`;
197
+ content += ` public ${name}WhereInput Where { get; set; }\n`;
198
+ content += ` }\n\n`;
199
+ }
197
200
 
198
201
  // CountArgs
199
202
  content += ` /// <summary>ORM-style args for ${name}.Count().</summary>\n`;
@@ -494,8 +497,9 @@ namespace An5Orm
494
497
 
495
498
  for (const model of models) {
496
499
  const name = this.capitalize(model.name);
497
- content += ` public TableClient<${model.name}> ${name}s => new TableClient<${model.name}>(ConnectionString, "${dottedTableName(model)}");\n`;
498
- content += ` public TableClient<${model.name}> ${name} => ${name}s;\n`;
500
+ const client = model.isView ? 'ViewClient' : 'TableClient';
501
+ content += ` public ${client}<${model.name}> ${name}s => new ${client}<${model.name}>(ConnectionString, "${dottedTableName(model)}");\n`;
502
+ content += ` public ${client}<${model.name}> ${name} => ${name}s;\n`;
499
503
  }
500
504
 
501
505
  content += ` }
@@ -986,9 +990,22 @@ namespace An5Orm
986
990
  {
987
991
  try
988
992
  {
989
- if (rawVal is string jsonStr)
993
+ if (rawVal is List<double> dblList)
990
994
  {
991
- rowVector = ParseDoubleArray(jsonStr);
995
+ rowVector = dblList;
996
+ }
997
+ else
998
+ {
999
+ var decoded = DecodeVectorColumn(rawVal);
1000
+ if (decoded != null)
1001
+ {
1002
+ rowVector = new List<double>(decoded.Length);
1003
+ for (int i = 0; i < decoded.Length; i++) rowVector.Add(decoded[i]);
1004
+ }
1005
+ else if (rawVal is string jsonStr)
1006
+ {
1007
+ rowVector = ParseDoubleArray(jsonStr);
1008
+ }
992
1009
  }
993
1010
  }
994
1011
  catch { }
@@ -996,8 +1013,31 @@ namespace An5Orm
996
1013
 
997
1014
  if (rowVector != null && rowVector.Count == vector.Count)
998
1015
  {
999
- double sim = CosineSimilarity(vector, rowVector);
1000
- double distance = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? (1.0 - sim) : sim;
1016
+ double distance;
1017
+ if (distanceMetric.Equals("euclidean", StringComparison.OrdinalIgnoreCase) || distanceMetric.Equals("l2", StringComparison.OrdinalIgnoreCase))
1018
+ {
1019
+ double sum = 0.0;
1020
+ for (int i = 0; i < vector.Count; i++)
1021
+ {
1022
+ double diff = vector[i] - rowVector[i];
1023
+ sum += diff * diff;
1024
+ }
1025
+ distance = Math.Sqrt(sum);
1026
+ }
1027
+ else if (distanceMetric.Equals("dot", StringComparison.OrdinalIgnoreCase) || distanceMetric.Equals("inner_product", StringComparison.OrdinalIgnoreCase) || distanceMetric.Equals("ip", StringComparison.OrdinalIgnoreCase))
1028
+ {
1029
+ double dot = 0.0;
1030
+ for (int i = 0; i < vector.Count; i++)
1031
+ {
1032
+ dot += vector[i] * rowVector[i];
1033
+ }
1034
+ distance = -dot;
1035
+ }
1036
+ else
1037
+ {
1038
+ double sim = CosineSimilarity(vector, rowVector);
1039
+ distance = 1.0 - sim;
1040
+ }
1001
1041
  results.Add(Tuple.Create(row, distance));
1002
1042
  }
1003
1043
  }
@@ -1154,6 +1194,46 @@ namespace An5Orm
1154
1194
  return false;
1155
1195
  }
1156
1196
  }
1197
+
1198
+ /// <summary>
1199
+ /// Read-only client for a database view.
1200
+ ///
1201
+ /// It exposes the same read methods as <see cref="TableClient{T}"/> and no
1202
+ /// write methods, so attempting to write a view fails at compile time rather
1203
+ /// than in the database. The view's rows still map onto the generated entity
1204
+ /// class, which is why the queries delegate instead of being duplicated.
1205
+ /// </summary>
1206
+ public class ViewClient<T> where T : new()
1207
+ {
1208
+ private readonly TableClient<T> _table;
1209
+
1210
+ public ViewClient(string connectionString, string viewName)
1211
+ {
1212
+ _table = new TableClient<T>(connectionString, viewName);
1213
+ }
1214
+
1215
+ public string ConnectionString => _table.ConnectionString;
1216
+ public string ViewName => _table.TableName;
1217
+ public An5Dialect Dialect => _table.Dialect;
1218
+
1219
+ public List<T> QueryRaw(string query, Dictionary<string, object> parameters = null)
1220
+ => _table.QueryRaw(query, parameters);
1221
+
1222
+ public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null)
1223
+ => _table.FindMany(whereClause, parameters);
1224
+
1225
+ public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
1226
+ => _table.FindFirst(whereClause, parameters);
1227
+
1228
+ public T FindUnique(object id)
1229
+ => _table.FindUnique(id);
1230
+
1231
+ public int Count(string whereClause = null, Dictionary<string, object> parameters = null)
1232
+ => _table.Count(whereClause, parameters);
1233
+
1234
+ public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
1235
+ => _table.VectorSearch(vector, take, whereClause, parameters, vectorField, distanceMetric);
1236
+ }
1157
1237
  }
1158
1238
  `;
1159
1239
 
@@ -185,6 +185,7 @@ func GetDefaultConnectionString() string {
185
185
  content += this.generateBaseOrmTypes();
186
186
  content += this.generateDbContext(models);
187
187
  content += this.generateTableClientStruct();
188
+ content += this.generateViewClientStruct();
188
189
  content += this.generateQueryAndScan();
189
190
  content += this.generateWhereBuilder();
190
191
  content += this.generateCrudMethods();
@@ -321,8 +322,9 @@ func SortOrderPtr(s SortOrder) *SortOrder { return &s }
321
322
 
322
323
  for (const model of models) {
323
324
  const name = this.capitalize(model.name);
324
- content += `\t${name}s *TableClient[${model.name}]\n`;
325
- content += `\t${name} *TableClient[${model.name}]\n`;
325
+ const client = model.isView ? 'ViewClient' : 'TableClient';
326
+ content += `\t${name}s *${client}[${model.name}]\n`;
327
+ content += `\t${name} *${client}[${model.name}]\n`;
326
328
  }
327
329
 
328
330
  content += `}
@@ -339,7 +341,8 @@ func NewAn5DbContextWithConnStr(db *sql.DB, connStr string) *An5DbContext {
339
341
  `;
340
342
  for (const model of models) {
341
343
  const name = this.capitalize(model.name);
342
- content += `\tclient${name} := NewTableClient[${model.name}](db, "${dottedTableName(model)}", d)\n`;
344
+ const client = model.isView ? 'NewViewClient' : 'NewTableClient';
345
+ content += `\tclient${name} := ${client}[${model.name}](db, "${dottedTableName(model)}", d)\n`;
343
346
  content += `\tctx.${name}s = client${name}\n`;
344
347
  content += `\tctx.${name} = client${name}\n`;
345
348
  }
@@ -394,6 +397,67 @@ func (c *TableClient[T]) ph(n int) string {
394
397
  \treturn "?"
395
398
  }
396
399
 
400
+ `;
401
+ }
402
+
403
+ // ─── ViewClient ────────────────────────────────────────────────────────────
404
+
405
+ /**
406
+ * The read-only client used for views.
407
+ *
408
+ * Go has no way to hide methods from an embedded value, so wrapping rather
409
+ * than embedding is what makes a write to a view a compile error. The read
410
+ * methods delegate, which keeps a view's rows scanning exactly like a table's.
411
+ */
412
+ private generateViewClientStruct(): string {
413
+ return `// ViewClient provides read-only access to a database view.
414
+ type ViewClient[T any] struct {
415
+ \tDB *sql.DB
416
+ \tDialect Dialect
417
+ \tViewName string
418
+ \ttable *TableClient[T]
419
+ }
420
+
421
+ // NewViewClient creates a typed read-only client for a view.
422
+ func NewViewClient[T any](db *sql.DB, viewName string, dialect Dialect) *ViewClient[T] {
423
+ \treturn &ViewClient[T]{
424
+ \t\tDB: db,
425
+ \t\tDialect: dialect,
426
+ \t\tViewName: viewName,
427
+ \t\ttable: NewTableClient[T](db, viewName, dialect),
428
+ \t}
429
+ }
430
+
431
+ // FindMany queries the view with ORM-style args.
432
+ func (c *ViewClient[T]) FindMany(ctx context.Context, args interface{}) ([]T, error) {
433
+ \treturn c.table.FindMany(ctx, args)
434
+ }
435
+
436
+ // FindFirst returns the first matching row of the view.
437
+ func (c *ViewClient[T]) FindFirst(ctx context.Context, args interface{}) (*T, error) {
438
+ \treturn c.table.FindFirst(ctx, args)
439
+ }
440
+
441
+ // FindUnique returns one row of the view by primary key.
442
+ func (c *ViewClient[T]) FindUnique(ctx context.Context, args interface{}) (*T, error) {
443
+ \treturn c.table.FindUnique(ctx, args)
444
+ }
445
+
446
+ // Count returns how many rows of the view match.
447
+ func (c *ViewClient[T]) Count(ctx context.Context, args interface{}) (int64, error) {
448
+ \treturn c.table.Count(ctx, args)
449
+ }
450
+
451
+ // QueryRaw runs a raw SELECT against the view.
452
+ func (c *ViewClient[T]) QueryRaw(ctx context.Context, query string, args ...interface{}) ([]T, error) {
453
+ \treturn c.table.QueryRaw(ctx, query, args...)
454
+ }
455
+
456
+ // VectorSearch searches a vector column of the view.
457
+ func (c *ViewClient[T]) VectorSearch(ctx context.Context, vector []float64, opts ...VectorSearchOptions) ([]T, error) {
458
+ \treturn c.table.VectorSearch(ctx, vector, opts...)
459
+ }
460
+
397
461
  `;
398
462
  }
399
463
 
@@ -1180,37 +1244,39 @@ func (c *TableClient[T]) VectorSearch(ctx context.Context, vector []float64, opt
1180
1244
  \tvecStr := string(vecBytes)
1181
1245
 
1182
1246
  \t// 1. Primary path: dialect-aware native SQL vector query
1183
- \tvar sqlQuery string
1184
- \tqueryArgs := append([]interface{}{vecStr}, whereQueryArgs...)
1185
- \tif c.Dialect == DialectPostgres {
1186
- \t\top := "<=>"
1187
- \t\tif strings.EqualFold(distanceMetric, "euclidean") { op = "<->" }
1188
- \t\tif strings.EqualFold(distanceMetric, "dot") { op = "<#>" }
1189
- \t\tvcol := c.quoteName(vectorField)
1190
- \t\tsqlQuery = fmt.Sprintf("SELECT *, (%s %s $1::vector) AS distance FROM %s", vcol, op, c.quoteTable())
1191
- \t\tif whereCl != "" {
1192
- \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL AND (%s)", vcol, whereCl)
1193
- \t\t} else {
1194
- \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL", vcol)
1195
- \t\t}
1196
- \t\tsqlQuery += fmt.Sprintf(" ORDER BY distance ASC LIMIT %d", take)
1197
- \t} else {
1198
- \t\tvcol := c.quoteName(vectorField)
1199
- \t\tsqlQuery = fmt.Sprintf(
1200
- \t\t\t"SELECT TOP (%d) *, VECTOR_DISTANCE('%s', CAST(%s AS VECTOR(%d, float32)), CAST(? AS VECTOR(%d, float32))) AS distance FROM %s WITH (NOLOCK)",
1201
- \t\t\ttake, distanceMetric, vcol, dim, dim, c.quoteTable(),
1202
- \t\t)
1203
- \t\tif whereCl != "" {
1204
- \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL AND (%s)", vcol, whereCl)
1247
+ \tif c.Dialect != DialectSqlite {
1248
+ \t\tvar sqlQuery string
1249
+ \t\tqueryArgs := append([]interface{}{vecStr}, whereQueryArgs...)
1250
+ \t\tif c.Dialect == DialectPostgres {
1251
+ \t\t\top := "<=>"
1252
+ \t\t\tif strings.EqualFold(distanceMetric, "euclidean") { op = "<->" }
1253
+ \t\t\tif strings.EqualFold(distanceMetric, "dot") { op = "<#>" }
1254
+ \t\t\tvcol := c.quoteName(vectorField)
1255
+ \t\t\tsqlQuery = fmt.Sprintf("SELECT *, (%s %s $1::vector) AS distance FROM %s", vcol, op, c.quoteTable())
1256
+ \t\t\tif whereCl != "" {
1257
+ \t\t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL AND (%s)", vcol, whereCl)
1258
+ \t\t\t} else {
1259
+ \t\t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL", vcol)
1260
+ \t\t\t}
1261
+ \t\t\tsqlQuery += fmt.Sprintf(" ORDER BY distance ASC LIMIT %d", take)
1205
1262
  \t\t} else {
1206
- \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL", vcol)
1263
+ \t\t\tvcol := c.quoteName(vectorField)
1264
+ \t\t\tsqlQuery = fmt.Sprintf(
1265
+ \t\t\t\t"SELECT TOP (%d) *, VECTOR_DISTANCE('%s', CAST(%s AS VECTOR(%d, float32)), CAST(? AS VECTOR(%d, float32))) AS distance FROM %s WITH (NOLOCK)",
1266
+ \t\t\t\ttake, distanceMetric, vcol, dim, dim, c.quoteTable(),
1267
+ \t\t\t)
1268
+ \t\t\tif whereCl != "" {
1269
+ \t\t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL AND (%s)", vcol, whereCl)
1270
+ \t\t\t} else {
1271
+ \t\t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL", vcol)
1272
+ \t\t\t}
1273
+ \t\t\tsqlQuery += " ORDER BY distance ASC"
1207
1274
  \t\t}
1208
- \t\tsqlQuery += " ORDER BY distance ASC"
1209
- \t}
1210
1275
 
1211
- \tnativeRows, nativeErr := c.queryAndScan(ctx, sqlQuery, queryArgs...)
1212
- \tif nativeErr == nil {
1213
- \t\treturn nativeRows, nil
1276
+ \t\tnativeRows, nativeErr := c.queryAndScan(ctx, sqlQuery, queryArgs...)
1277
+ \t\tif nativeErr == nil {
1278
+ \t\t\treturn nativeRows, nil
1279
+ \t\t}
1214
1280
  \t}
1215
1281
 
1216
1282
  \t// 2. Fallback: in-memory similarity
@@ -1337,25 +1403,26 @@ var _ = time.Time{}
1337
1403
  }
1338
1404
  content += `}\n\n`;
1339
1405
 
1340
- // CreateInput
1341
- content += `// ${name}CreateInput holds data for creating a new ${name} record.\ntype ${name}CreateInput struct {\n`;
1342
- for (const field of model.fields) {
1343
- if (!field.isId) {
1344
- const goType = this.mapGoType(field, field.isOptional || field.hasDefault);
1345
- content += `\t${this.capitalize(field.name)} ${goType}\n`;
1406
+ // CreateInput / UpdateInput / DeleteManyArgs describe writes; a view has none.
1407
+ if (!model.isView) {
1408
+ content += `// ${name}CreateInput holds data for creating a new ${name} record.\ntype ${name}CreateInput struct {\n`;
1409
+ for (const field of model.fields) {
1410
+ if (!field.isId) {
1411
+ const goType = this.mapGoType(field, field.isOptional || field.hasDefault);
1412
+ content += `\t${this.capitalize(field.name)} ${goType}\n`;
1413
+ }
1346
1414
  }
1347
- }
1348
- content += `}\n\n`;
1349
-
1350
- // UpdateInput
1351
- content += `// ${name}UpdateInput holds data for updating an existing ${name} record.\ntype ${name}UpdateInput struct {\n`;
1352
- for (const field of model.fields) {
1353
- if (!field.isId) {
1354
- const goType = this.mapGoType(field, true); // always pointer so we can detect zero
1355
- content += `\t${this.capitalize(field.name)} ${goType}\n`;
1415
+ content += `}\n\n`;
1416
+
1417
+ content += `// ${name}UpdateInput holds data for updating an existing ${name} record.\ntype ${name}UpdateInput struct {\n`;
1418
+ for (const field of model.fields) {
1419
+ if (!field.isId) {
1420
+ const goType = this.mapGoType(field, true); // always pointer so we can detect zero
1421
+ content += `\t${this.capitalize(field.name)} ${goType}\n`;
1422
+ }
1356
1423
  }
1424
+ content += `}\n\n`;
1357
1425
  }
1358
- content += `}\n\n`;
1359
1426
 
1360
1427
  // FindManyArgs
1361
1428
  content += `// ${name}FindManyArgs provides ORM-style args for FindMany queries on ${name}.\ntype ${name}FindManyArgs struct {\n`;
@@ -1379,9 +1446,11 @@ var _ = time.Time{}
1379
1446
  content += `}\n\n`;
1380
1447
 
1381
1448
  // DeleteManyArgs
1382
- content += `// ${name}DeleteManyArgs provides ORM-style args for DeleteMany queries on ${name}.\ntype ${name}DeleteManyArgs struct {\n`;
1383
- content += `\tWhere *${name}WhereInput\n`;
1384
- content += `}\n\n`;
1449
+ if (!model.isView) {
1450
+ content += `// ${name}DeleteManyArgs provides ORM-style args for DeleteMany queries on ${name}.\ntype ${name}DeleteManyArgs struct {\n`;
1451
+ content += `\tWhere *${name}WhereInput\n`;
1452
+ content += `}\n\n`;
1453
+ }
1385
1454
 
1386
1455
  // CountArgs
1387
1456
  content += `// ${name}CountArgs provides ORM-style args for Count queries on ${name}.\ntype ${name}CountArgs struct {\n`;
@@ -1266,8 +1266,104 @@ public final class ModelClient<T> {
1266
1266
  }
1267
1267
 
1268
1268
 
1269
+ /**
1270
+ * The typed client used for views.
1271
+ *
1272
+ * <p>It repeats {@link ModelClient}'s read signatures and stops there: with no
1273
+ * {@code create}/{@code update}/{@code delete} to call, writing a view is a
1274
+ * compile error rather than a runtime refusal.
1275
+ */
1276
+ private viewClientSource(): string {
1277
+ return `// This file is auto-generated. Do not edit directly.
1278
+ package an5.client;
1279
+
1280
+ import java.sql.SQLException;
1281
+ import java.util.ArrayList;
1282
+ import java.util.List;
1283
+ import java.util.Map;
1284
+ import java.util.function.Function;
1285
+
1286
+ import an5.adapters.An5Aggregate;
1287
+ import an5.adapters.An5GroupBy;
1288
+ import an5.adapters.An5Query;
1289
+ import an5.adapters.An5ViewClient;
1290
+
1291
+ /** A read-only, typed client for one database view. */
1292
+ public final class ViewClient<T> {
1293
+
1294
+ private final An5ViewClient view;
1295
+ private final Function<Map<String, Object>, T> fromRow;
1296
+
1297
+ ViewClient(An5ViewClient view, Function<Map<String, Object>, T> fromRow) {
1298
+ this.view = view;
1299
+ this.fromRow = fromRow;
1300
+ }
1301
+
1302
+ /** The view name this client reads. */
1303
+ public String view() {
1304
+ return view.viewName();
1305
+ }
1306
+
1307
+ public List<T> findMany() throws SQLException {
1308
+ return findMany(new An5Query());
1309
+ }
1310
+
1311
+ public List<T> findMany(An5Query query) throws SQLException {
1312
+ return map(view.findMany(query));
1313
+ }
1314
+
1315
+ public T findFirst(An5Query query) throws SQLException {
1316
+ return one(view.findFirst(query));
1317
+ }
1318
+
1319
+ public T findFirst(Map<String, Object> where) throws SQLException {
1320
+ return one(view.findFirst(where));
1321
+ }
1322
+
1323
+ public T findUnique(Map<String, Object> where) throws SQLException {
1324
+ return one(view.findUnique(where));
1325
+ }
1326
+
1327
+ public long count(Map<String, Object> where) throws SQLException {
1328
+ return view.count(where);
1329
+ }
1330
+
1331
+ public Map<String, Object> aggregate(An5Aggregate aggregate) throws SQLException {
1332
+ return view.aggregate(aggregate);
1333
+ }
1334
+
1335
+ public List<Map<String, Object>> groupBy(An5GroupBy group) throws SQLException {
1336
+ return view.groupBy(group);
1337
+ }
1338
+
1339
+ public List<T> vectorSearch(
1340
+ double[] vector, Integer take, Map<String, Object> where, String vectorField, String metric)
1341
+ throws SQLException {
1342
+ return map(view.vectorSearch(vector, take, where, vectorField, metric));
1343
+ }
1344
+
1345
+ public List<T> vectorSearch(double[] vector, Integer take) throws SQLException {
1346
+ return vectorSearch(vector, take, null, "embedding", "cosine");
1347
+ }
1348
+
1349
+ private List<T> map(List<Map<String, Object>> rows) {
1350
+ List<T> mapped = new ArrayList<T>();
1351
+ for (Map<String, Object> row : rows) {
1352
+ mapped.add(fromRow.apply(row));
1353
+ }
1354
+ return mapped;
1355
+ }
1356
+
1357
+ private T one(Map<String, Object> row) {
1358
+ return row == null ? null : fromRow.apply(row);
1359
+ }
1360
+ }
1361
+ `;
1362
+ }
1363
+
1269
1364
  private generateDbContext(models: Model[]): void {
1270
1365
  fs.writeFileSync(path.join(this.outputDir, 'ModelClient.java'), this.modelClientSource());
1366
+ fs.writeFileSync(path.join(this.outputDir, 'ViewClient.java'), this.viewClientSource());
1271
1367
 
1272
1368
  let content = `// This file is auto-generated. Do not edit directly.
1273
1369
  package an5.client;
@@ -1299,7 +1395,7 @@ public class An5DbContext implements AutoCloseable {
1299
1395
  const property = this.propertyNames(model.name)[0]!;
1300
1396
  if (declared.has(property)) continue;
1301
1397
  declared.add(property);
1302
- content += ` private final ModelClient<${model.name}> ${property}Client;\n`;
1398
+ content += ` private final ${model.isView ? 'ViewClient' : 'ModelClient'}<${model.name}> ${property}Client;\n`;
1303
1399
  }
1304
1400
 
1305
1401
  content += `
@@ -1314,6 +1410,11 @@ public class An5DbContext implements AutoCloseable {
1314
1410
  const property = this.propertyNames(model.name)[0]!;
1315
1411
  if (assigned.has(property)) continue;
1316
1412
  assigned.add(property);
1413
+ if (model.isView) {
1414
+ content += ` this.${property}Client = new ViewClient<${model.name}>(\n`;
1415
+ content += ` adapter.view("${model.name}"), ${model.name}::fromRow);\n`;
1416
+ continue;
1417
+ }
1317
1418
  content += ` this.${property}Client = new ModelClient<${model.name}>(\n`;
1318
1419
  content += ` adapter.table("${model.name}"), ${model.name}::fromRow, ${model.name}::toValues);\n`;
1319
1420
  }
@@ -1331,6 +1432,11 @@ public class An5DbContext implements AutoCloseable {
1331
1432
  const property = this.propertyNames(model.name)[0]!;
1332
1433
  if (wrapped.has(property)) continue;
1333
1434
  wrapped.add(property);
1435
+ if (model.isView) {
1436
+ content += ` this.${property}Client = new ViewClient<${model.name}>(\n`;
1437
+ content += ` adapter.view("${model.name}"), ${model.name}::fromRow);\n`;
1438
+ continue;
1439
+ }
1334
1440
  content += ` this.${property}Client = new ModelClient<${model.name}>(\n`;
1335
1441
  content += ` adapter.table("${model.name}"), ${model.name}::fromRow, ${model.name}::toValues);\n`;
1336
1442
  }
@@ -1355,8 +1461,8 @@ public class An5DbContext implements AutoCloseable {
1355
1461
  exposed.add(property);
1356
1462
  const accessor = `get${this.capitalize(property)}`;
1357
1463
  content += `
1358
- /** Queries for \`${model.name}\`. */
1359
- public ModelClient<${model.name}> ${accessor}() {
1464
+ /** ${model.isView ? 'Reads' : 'Queries'} for \`${model.name}\`. */
1465
+ public ${model.isView ? 'ViewClient' : 'ModelClient'}<${model.name}> ${accessor}() {
1360
1466
  return this.${property}Client;
1361
1467
  }
1362
1468
  `;
@@ -704,6 +704,47 @@ class ModelClient<T : Any>(
704
704
  ): List<T> = delegate.vectorSearch(vector, take, filter, vectorField, metric).map(read)
705
705
  }
706
706
 
707
+ /**
708
+ * A read-only, typed client for one view.
709
+ *
710
+ * It mirrors [ModelClient]'s read methods and stops there, so a write to a view does
711
+ * not compile. The runtime's read-only client does the actual work.
712
+ */
713
+ class ViewClient<T : Any>(
714
+ private val delegate: an5.adapters.ViewClient,
715
+ private val read: (Row) -> T,
716
+ ) {
717
+
718
+ /** The view this client reads. */
719
+ val name: String get() = delegate.name
720
+
721
+ fun findMany(): List<T> = delegate.findMany().map(read)
722
+
723
+ fun findMany(block: QueryBuilder.() -> Unit): List<T> = delegate.findMany(block).map(read)
724
+
725
+ fun findFirst(filter: Where): T? = delegate.findFirst(filter)?.let(read)
726
+
727
+ fun findUnique(filter: Where): T? = delegate.findUnique(filter)?.let(read)
728
+
729
+ fun count(filter: Where? = null): Long = delegate.count(filter)
730
+
731
+ /** One row of aggregate values, keyed \`_count\`, \`_sum_<field>\` and so on. */
732
+ fun aggregate(block: AggregateBuilder.() -> Unit): Row = delegate.aggregate(block)
733
+
734
+ /** One row per group, each carrying the group's count and aggregates. */
735
+ fun groupBy(vararg by: String, block: AggregateBuilder.() -> Unit = {}): List<Row> =
736
+ delegate.groupBy(*by, block = block)
737
+
738
+ /** The rows nearest [vector]. */
739
+ fun vectorSearch(
740
+ vector: DoubleArray,
741
+ take: Int = 10,
742
+ filter: Where? = null,
743
+ vectorField: String = "embedding",
744
+ metric: DistanceMetric = DistanceMetric.COSINE,
745
+ ): List<T> = delegate.vectorSearch(vector, take, filter, vectorField, metric).map(read)
746
+ }
747
+
707
748
  /**
708
749
  * The AN5 entry point: one typed client per model, over one connection.
709
750
  *
@@ -728,6 +769,16 @@ class An5Db internal constructor(private val an5: An5) : AutoCloseable {
728
769
  const property = KotlinGenerator.toLowerCamelCase(model.name);
729
770
  if (seen.has(property)) continue;
730
771
  seen.add(property);
772
+ if (model.isView) {
773
+ content += ` /** Reads for \`${model.name}\`. */
774
+ `;
775
+ content += ` val ${property}: ViewClient<${model.name}> =
776
+ `;
777
+ content += ` ViewClient(an5.view("${model.name}"), ${model.name}::fromRow)
778
+
779
+ `;
780
+ continue;
781
+ }
731
782
  content += ` /** Queries for \`${model.name}\`. */
732
783
  `;
733
784
  content += ` val ${property}: ModelClient<${model.name}> =