@an5/orm 1.0.8 → 1.1.0

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 (73) hide show
  1. package/README.md +174 -175
  2. package/dist/an5Metadata.js +64 -0
  3. package/dist/an5Orm.d.ts +92 -3
  4. package/dist/an5Orm.js +697 -206
  5. package/dist/config.d.ts +9 -1
  6. package/dist/config.js +13 -18
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.js +1 -1
  9. package/dist/generator/src/api.d.ts +35 -0
  10. package/dist/generator/src/api.js +58 -0
  11. package/dist/generator/src/code-generator.d.ts +14 -13
  12. package/dist/generator/src/code-generator.js +249 -225
  13. package/dist/generator/src/config.d.ts +162 -0
  14. package/dist/generator/src/config.js +354 -0
  15. package/dist/generator/src/dialect.d.ts +88 -0
  16. package/dist/generator/src/dialect.js +318 -0
  17. package/dist/generator/src/dotnet-generator.d.ts +21 -11
  18. package/dist/generator/src/dotnet-generator.js +1053 -546
  19. package/dist/generator/src/field-types.d.ts +96 -0
  20. package/dist/generator/src/field-types.js +433 -0
  21. package/dist/generator/src/golang-generator.d.ts +36 -0
  22. package/dist/generator/src/golang-generator.js +1229 -0
  23. package/dist/generator/src/index.d.ts +1 -1
  24. package/dist/generator/src/index.js +137 -88
  25. package/dist/generator/src/metadata-generator.d.ts +11 -8
  26. package/dist/generator/src/metadata-generator.js +105 -90
  27. package/dist/generator/src/parser.d.ts +37 -11
  28. package/dist/generator/src/parser.js +208 -206
  29. package/dist/generator/src/push-apply.d.ts +37 -0
  30. package/dist/generator/src/push-apply.js +109 -0
  31. package/dist/generator/src/push-schema.d.ts +93 -0
  32. package/dist/generator/src/push-schema.js +189 -0
  33. package/dist/generator/src/python-generator.d.ts +37 -9
  34. package/dist/generator/src/python-generator.js +428 -105
  35. package/dist/generator/src/rust-generator.d.ts +38 -0
  36. package/dist/generator/src/rust-generator.js +971 -0
  37. package/dist/generator/src/suggest.d.ts +17 -0
  38. package/dist/generator/src/suggest.js +45 -0
  39. package/dist/generator/src/type-kinds.d.ts +27 -0
  40. package/dist/generator/src/type-kinds.js +72 -0
  41. package/dist/generator/src/types.d.ts +60 -27
  42. package/dist/generator/src/types.js +41 -2
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +19 -8
  45. package/dist/logger.js +1 -1
  46. package/dist/migration-core.d.ts +102 -0
  47. package/dist/migration-core.js +731 -0
  48. package/dist/orm.d.ts +147 -0
  49. package/dist/orm.js +1654 -0
  50. package/dist/provider-support.d.ts +27 -0
  51. package/dist/provider-support.js +50 -0
  52. package/dist/scripts/release-cli.d.ts +2 -2
  53. package/dist/scripts/release-cli.js +115 -115
  54. package/dist/sql-utils.d.ts +3 -1
  55. package/dist/sql-utils.js +94 -24
  56. package/generator/src/api.ts +45 -0
  57. package/generator/src/code-generator.ts +259 -230
  58. package/generator/src/config.ts +496 -0
  59. package/generator/src/dialect.ts +415 -0
  60. package/generator/src/dotnet-generator.ts +1071 -544
  61. package/generator/src/field-types.ts +473 -0
  62. package/generator/src/golang-generator.ts +1267 -0
  63. package/generator/src/index.ts +146 -116
  64. package/generator/src/metadata-generator.ts +116 -101
  65. package/generator/src/parser.ts +234 -202
  66. package/generator/src/push-apply.ts +148 -0
  67. package/generator/src/push-schema.ts +250 -0
  68. package/generator/src/python-generator.ts +480 -114
  69. package/generator/src/rust-generator.ts +974 -0
  70. package/generator/src/suggest.ts +41 -0
  71. package/generator/src/type-kinds.ts +78 -0
  72. package/generator/src/types.ts +77 -29
  73. package/package.json +32 -6
@@ -0,0 +1,1267 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { Model, Field, Relation, bracketedTableName, dottedTableName } from './types';
4
+ import { fieldKind } from './type-kinds';
5
+ import { Provider } from './field-types';
6
+
7
+ export class GolangGenerator {
8
+ /** The database being generated for; decides types like `TIMESTAMP`. */
9
+ private provider: Provider | undefined;
10
+
11
+ constructor(private outputDir: string) {
12
+ if (!fs.existsSync(this.outputDir)) {
13
+ fs.mkdirSync(this.outputDir, { recursive: true });
14
+ }
15
+ }
16
+
17
+ public generate(models: Model[]) {
18
+ // Remembered for the type mapping: `TIMESTAMP` is a rowversion on SQL Server and
19
+ // a point in time on every other provider.
20
+ this.provider = models[0]?.provider;
21
+
22
+ // 1. Generate per-model files like dotnet (<Model>.go) -
23
+ // entity struct + that model's WhereInput/OrderByInput/Args types.
24
+ // Same Go package, so no re-export shim is needed.
25
+ for (const model of models) {
26
+ this.generateModelFile(model);
27
+ }
28
+
29
+ // 2. Generate client.go - ORM DbContext + TableClient with full type-safe API
30
+ this.generateClientGo(models);
31
+
32
+ // 3. Generate config.go - connection string helpers
33
+ this.generateConfigGo();
34
+
35
+ // 4. Generate metadata.go - model-to-table mapping
36
+ this.generateMetadataGo(models);
37
+ }
38
+
39
+ // ─── Type mapping ──────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * The Go type for a field.
43
+ *
44
+ * Driven by the declared type, not the generated TypeScript one: that collapses
45
+ * `INT`, `FLOAT` and `DECIMAL` into `number`, which matched none of the branches
46
+ * below and made every numeric column a `string`.
47
+ */
48
+ private mapGoType(field: Field, isOptional: boolean): string {
49
+ const kind = fieldKind(field, this.provider);
50
+ let goType: string;
51
+ switch (kind) {
52
+ case 'int': goType = 'int'; break;
53
+ case 'bigint': goType = 'int64'; break;
54
+ case 'float': goType = 'float64'; break;
55
+ case 'bool': goType = 'bool'; break;
56
+ case 'date': goType = 'time.Time'; break;
57
+ case 'bytes': goType = '[]byte'; break;
58
+ case 'vector': goType = '[]float32'; break;
59
+ // A JSON column holds whatever the row put in it; string keeps any value
60
+ // representable, and json.RawMessage would need the import everywhere.
61
+ case 'json': goType = 'string'; break;
62
+ default: goType = 'string'; break;
63
+ }
64
+
65
+ if (isOptional && goType !== '[]byte') {
66
+ goType = '*' + goType;
67
+ }
68
+ return goType;
69
+ }
70
+
71
+ // ─── <Model>.go (per-model file, like dotnet `<Model>.cs`) ────────────────
72
+
73
+ private generateModelFile(model: Model) {
74
+ // Single-model file: entity struct + its WhereInput/OrderByInput/Args
75
+ // types. All files share `package an5client`, so callers keep using
76
+ // `an5client.User` etc. regardless of file layout.
77
+ // NOTE: Go rejects unused imports, so `time` is only imported when a
78
+ // field actually maps to `time.Time`.
79
+ const needsTime = model.fields.some(f => this.mapGoType(f, false).includes('time.Time'));
80
+ let content = `// Code generated by an5ORM. DO NOT EDIT.
81
+ package an5client
82
+
83
+ `;
84
+ if (needsTime) {
85
+ content += `import (
86
+ \t"time"
87
+ )
88
+
89
+ `;
90
+ }
91
+ content += this.generateModelStruct(model);
92
+ content += this.generateModelOrmTypes(model);
93
+
94
+ fs.writeFileSync(path.join(this.outputDir, `${model.name}.go`), content);
95
+ }
96
+
97
+ private generateModelStruct(model: Model): string {
98
+ let content = '';
99
+ if (model.description) {
100
+ content += `// ${model.name} - ${model.description}\n`;
101
+ }
102
+ content += `type ${model.name} struct {\n`;
103
+
104
+ for (const field of model.fields) {
105
+ const goType = this.mapGoType(field, field.isOptional);
106
+ const jsonTag = field.isOptional ? `${this.toCamelCase(field.name)},omitempty` : this.toCamelCase(field.name);
107
+ const dbTag = this.toSnakeCase(field.name);
108
+ content += `\t${this.capitalize(field.name)} ${goType} \`json:"${jsonTag}" db:"${dbTag}"\`\n`;
109
+ }
110
+
111
+ if (model.relations.length > 0) {
112
+ content += `\n\t// Relations\n`;
113
+ for (const rel of model.relations) {
114
+ const relType = rel.isArray ? `[]${rel.type}` : `*${rel.type}`;
115
+ content += `\t${this.capitalize(rel.name)} ${relType} \`json:"${this.toCamelCase(rel.name)},omitempty"\`\n`;
116
+ }
117
+ }
118
+
119
+ content += `}\n\n`;
120
+ return content;
121
+ }
122
+
123
+ // ─── metadata.go ───────────────────────────────────────────────────────────
124
+
125
+ private generateMetadataGo(models: Model[]) {
126
+ let content = `// Code generated by an5ORM. DO NOT EDIT.
127
+ package an5client
128
+
129
+ // ModelToTable maps model name variations to fully-qualified SQL table names.
130
+ var ModelToTable = map[string]string{
131
+ `;
132
+ for (const model of models) {
133
+ const fullTable = bracketedTableName(model);
134
+ for (const prop of this.getAllPropertyVariations(model.name)) {
135
+ content += `\t"${prop}": "${fullTable}",\n`;
136
+ }
137
+ }
138
+ content += `}\n\n`;
139
+
140
+ content += `// ModelPrimaryKeys maps model names to their primary key column name.\nvar ModelPrimaryKeys = map[string]string{\n`;
141
+ for (const model of models) {
142
+ const idField = model.fields.find(f => f.isId) ?? model.fields[0];
143
+ const pkCol = idField ? this.toSnakeCase(idField.name) : 'id';
144
+ for (const prop of this.getAllPropertyVariations(model.name)) {
145
+ content += `\t"${prop}": "${pkCol}",\n`;
146
+ }
147
+ }
148
+ content += `}\n`;
149
+
150
+ fs.writeFileSync(path.join(this.outputDir, 'metadata.go'), content);
151
+ }
152
+
153
+ // ─── config.go ─────────────────────────────────────────────────────────────
154
+
155
+ private generateConfigGo() {
156
+ const content = `// Code generated by an5ORM. DO NOT EDIT.
157
+ package an5client
158
+
159
+ import (
160
+ \t"os"
161
+ )
162
+
163
+ // An5Config holds database configuration options for Golang client.
164
+ type An5Config struct {
165
+ \tConnectionString string
166
+ \tDriverName string
167
+ }
168
+
169
+ // GetDefaultConnectionString retrieves DATABASE_URL or fallback string.
170
+ func GetDefaultConnectionString() string {
171
+ \tconnStr := os.Getenv("DATABASE_URL")
172
+ \tif connStr == "" {
173
+ \t\tconnStr = "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;"
174
+ \t}
175
+ \treturn connStr
176
+ }
177
+ `;
178
+ fs.writeFileSync(path.join(this.outputDir, 'config.go'), content);
179
+ }
180
+
181
+ // ─── client.go ─────────────────────────────────────────────────────────────
182
+
183
+ private generateClientGo(models: Model[]) {
184
+ let content = this.generateClientHeader(models);
185
+ content += this.generateBaseOrmTypes();
186
+ content += this.generateDbContext(models);
187
+ content += this.generateTableClientStruct();
188
+ content += this.generateQueryAndScan();
189
+ content += this.generateWhereBuilder();
190
+ content += this.generateCrudMethods();
191
+ content += this.generateVectorSearch();
192
+ content += this.generateHelpers();
193
+
194
+ // NOTE: per-model ORM types live in each `<Model>.go` file.
195
+ fs.writeFileSync(path.join(this.outputDir, 'client.go'), content);
196
+ }
197
+
198
+ private generateClientHeader(models: Model[]): string {
199
+ return `// Code generated by an5ORM. DO NOT EDIT.
200
+ package an5client
201
+
202
+ import (
203
+ \t"context"
204
+ \t"database/sql"
205
+ \t"encoding/json"
206
+ \t"fmt"
207
+ \t"math"
208
+ \t"reflect"
209
+ \t"sort"
210
+ \t"strings"
211
+ \t"time"
212
+ )
213
+
214
+ `;
215
+ }
216
+
217
+ // ─── Base ORM types (shared filters, sort order) ───────────────────────────
218
+
219
+ private generateBaseOrmTypes(): string {
220
+ return `// SortOrder represents ascending or descending sort direction.
221
+ type SortOrder string
222
+
223
+ const (
224
+ \tSortOrderAsc SortOrder = "ASC"
225
+ \tSortOrderDesc SortOrder = "DESC"
226
+ )
227
+
228
+ // StringFilter provides type-safe string field filtering options.
229
+ type StringFilter struct {
230
+ \tEquals *string
231
+ \tNot *string
232
+ \tIn []string
233
+ \tNotIn []string
234
+ \tContains *string
235
+ \tStartsWith *string
236
+ \tEndsWith *string
237
+ \tGt *string
238
+ \tGte *string
239
+ \tLt *string
240
+ \tLte *string
241
+ }
242
+
243
+ // NumberFilter provides type-safe numeric field filtering options.
244
+ type NumberFilter struct {
245
+ \tEquals *float64
246
+ \tNot *float64
247
+ \tIn []float64
248
+ \tNotIn []float64
249
+ \tGt *float64
250
+ \tGte *float64
251
+ \tLt *float64
252
+ \tLte *float64
253
+ }
254
+
255
+ // IntFilter provides type-safe integer field filtering options.
256
+ type IntFilter struct {
257
+ \tEquals *int
258
+ \tNot *int
259
+ \tIn []int
260
+ \tNotIn []int
261
+ \tGt *int
262
+ \tGte *int
263
+ \tLt *int
264
+ \tLte *int
265
+ }
266
+
267
+ // BoolFilter provides type-safe boolean field filtering options.
268
+ type BoolFilter struct {
269
+ \tEquals *bool
270
+ }
271
+
272
+ // DateTimeFilter provides type-safe datetime field filtering options.
273
+ type DateTimeFilter struct {
274
+ \tEquals *time.Time
275
+ \tNot *time.Time
276
+ \tIn []time.Time
277
+ \tNotIn []time.Time
278
+ \tGt *time.Time
279
+ \tGte *time.Time
280
+ \tLt *time.Time
281
+ \tLte *time.Time
282
+ }
283
+
284
+ // Dialect identifies the database engine in use.
285
+ type Dialect string
286
+
287
+ const (
288
+ \tDialectMssql Dialect = "mssql"
289
+ \tDialectPostgres Dialect = "postgres"
290
+ \tDialectSqlite Dialect = "sqlite"
291
+ )
292
+
293
+ // detectDialect infers dialect from a connection string.
294
+ func detectDialect(connStr string) Dialect {
295
+ \tl := strings.ToLower(strings.TrimSpace(connStr))
296
+ \tif strings.HasPrefix(l, "postgres://") || strings.HasPrefix(l, "postgresql://") || strings.Contains(l, "port=5432") {
297
+ \t\treturn DialectPostgres
298
+ \t}
299
+ \tif strings.HasPrefix(l, "sqlite://") || strings.HasPrefix(l, "sqlite:") || strings.HasPrefix(l, "file:") || strings.HasSuffix(l, ".db") || strings.HasSuffix(l, ".sqlite") || strings.HasSuffix(l, ".sqlite3") || l == ":memory:" {
300
+ \t\treturn DialectSqlite
301
+ \t}
302
+ \treturn DialectMssql
303
+ }
304
+
305
+ // Pointer helpers
306
+ func StringPtr(s string) *string { return &s }
307
+ func IntPtr(i int) *int { return &i }
308
+ func Int64Ptr(i int64) *int64 { return &i }
309
+ func Float64Ptr(f float64) *float64 { return &f }
310
+ func BoolPtr(b bool) *bool { return &b }
311
+ func SortOrderPtr(s SortOrder) *SortOrder { return &s }
312
+
313
+ `;
314
+ }
315
+
316
+ // ─── An5DbContext ──────────────────────────────────────────────────────────
317
+
318
+ private generateDbContext(models: Model[]): string {
319
+ let content = `// An5DbContext represents the main ORM database context.\ntype An5DbContext struct {\n\tDB *sql.DB\n\tDialect Dialect\n`;
320
+
321
+ for (const model of models) {
322
+ const name = this.capitalize(model.name);
323
+ content += `\t${name}s *TableClient[${model.name}]\n`;
324
+ content += `\t${name} *TableClient[${model.name}]\n`;
325
+ }
326
+
327
+ content += `}
328
+
329
+ // NewAn5DbContext initializes the ORM context (defaults to MSSQL dialect).
330
+ func NewAn5DbContext(db *sql.DB) *An5DbContext {
331
+ \treturn NewAn5DbContextWithConnStr(db, "")
332
+ }
333
+
334
+ // NewAn5DbContextWithConnStr initializes the ORM context with dialect detection.
335
+ func NewAn5DbContextWithConnStr(db *sql.DB, connStr string) *An5DbContext {
336
+ \td := detectDialect(connStr)
337
+ \tctx := &An5DbContext{DB: db, Dialect: d}
338
+ `;
339
+ for (const model of models) {
340
+ const name = this.capitalize(model.name);
341
+ content += `\tclient${name} := NewTableClient[${model.name}](db, "${dottedTableName(model)}", d)\n`;
342
+ content += `\tctx.${name}s = client${name}\n`;
343
+ content += `\tctx.${name} = client${name}\n`;
344
+ }
345
+ content += `\treturn ctx\n}\n\n`;
346
+
347
+ return content;
348
+ }
349
+
350
+ // ─── TableClient struct ────────────────────────────────────────────────────
351
+
352
+ private generateTableClientStruct(): string {
353
+ return `// TableClient provides generic type-safe ORM operations for model T.
354
+ type TableClient[T any] struct {
355
+ \tDB *sql.DB
356
+ \tDialect Dialect
357
+ \tTableName string
358
+ }
359
+
360
+ // NewTableClient creates a typed table client.
361
+ func NewTableClient[T any](db *sql.DB, tableName string, dialect Dialect) *TableClient[T] {
362
+ \treturn &TableClient[T]{
363
+ \t\tDB: db,
364
+ \t\tDialect: dialect,
365
+ \t\tTableName: tableName,
366
+ \t}
367
+ }
368
+
369
+ // quoteName quotes a column/identifier for the dialect.
370
+ func (c *TableClient[T]) quoteName(name string) string {
371
+ \tif c.Dialect == DialectPostgres || c.Dialect == DialectSqlite {
372
+ \t\treturn \`"\` + strings.ReplaceAll(name, \`"\`, \`""\`) + \`"\`
373
+ \t}
374
+ \treturn "[" + strings.ReplaceAll(name, "]", "]]") + "]"
375
+ }
376
+
377
+ // quoteTable quotes a schema.table name for the dialect.
378
+ func (c *TableClient[T]) quoteTable() string {
379
+ \tparts := strings.Split(c.TableName, ".")
380
+ \tquoted := make([]string, len(parts))
381
+ \tfor i, p := range parts {
382
+ \t\tquoted[i] = c.quoteName(p)
383
+ \t}
384
+ \treturn strings.Join(quoted, ".")
385
+ }
386
+
387
+ // ph returns a positional placeholder - ? for MSSQL, $N for Postgres.
388
+ func (c *TableClient[T]) ph(n int) string {
389
+ \tif c.Dialect == DialectPostgres {
390
+ \t\treturn fmt.Sprintf("$%d", n)
391
+ \t}
392
+ \treturn "?"
393
+ }
394
+
395
+ `;
396
+ }
397
+
398
+ // ─── queryAndScan ──────────────────────────────────────────────────────────
399
+
400
+ private generateQueryAndScan(): string {
401
+ return `// queryAndScan executes a raw SELECT and scans results into []T via db struct tags.
402
+ func (c *TableClient[T]) queryAndScan(ctx context.Context, query string, args ...interface{}) ([]T, error) {
403
+ \trows, err := c.DB.QueryContext(ctx, query, args...)
404
+ \tif err != nil {
405
+ \t\treturn nil, err
406
+ \t}
407
+ \tdefer rows.Close()
408
+
409
+ \tvar results []T
410
+ \tcols, err := rows.Columns()
411
+ \tif err != nil {
412
+ \t\treturn nil, err
413
+ \t}
414
+
415
+ \tfor rows.Next() {
416
+ \t\tvar item T
417
+ \t\tval := reflect.ValueOf(&item).Elem()
418
+ \t\tscanArgs := make([]interface{}, len(cols))
419
+ \t\tfor i, colName := range cols {
420
+ \t\t\tfieldVal := findFieldByDbTag(val, colName)
421
+ \t\t\tif fieldVal.IsValid() && fieldVal.CanSet() {
422
+ \t\t\t\tscanArgs[i] = fieldVal.Addr().Interface()
423
+ \t\t\t} else {
424
+ \t\t\t\tvar unused interface{}
425
+ \t\t\t\tscanArgs[i] = &unused
426
+ \t\t\t}
427
+ \t\t}
428
+ \t\tif err := rows.Scan(scanArgs...); err != nil {
429
+ \t\t\treturn nil, err
430
+ \t\t}
431
+ \t\tresults = append(results, item)
432
+ \t}
433
+ \tif err := rows.Err(); err != nil {
434
+ \t\treturn nil, err
435
+ \t}
436
+ \treturn results, nil
437
+ }
438
+
439
+ `;
440
+ }
441
+
442
+ // ─── WHERE builder ─────────────────────────────────────────────────────────
443
+
444
+ private generateWhereBuilder(): string {
445
+ return `// whereResult holds the SQL WHERE clause and its bound arguments.
446
+ type whereResult struct {
447
+ \tClause string
448
+ \tArgs []interface{}
449
+ }
450
+
451
+ // buildStringFilter converts a StringFilter to SQL fragments.
452
+ func buildStringFilter(col string, f *StringFilter, args []interface{}, ph func(int) string) (string, []interface{}) {
453
+ \tif f == nil {
454
+ \t\treturn "", args
455
+ \t}
456
+ \tvar parts []string
457
+ \tif f.Equals != nil {
458
+ \t\targs = append(args, *f.Equals)
459
+ \t\tparts = append(parts, col+" = "+ph(len(args)))
460
+ \t}
461
+ \tif f.Not != nil {
462
+ \t\targs = append(args, *f.Not)
463
+ \t\tparts = append(parts, col+" <> "+ph(len(args)))
464
+ \t}
465
+ \tif f.Contains != nil {
466
+ \t\targs = append(args, "%"+*f.Contains+"%")
467
+ \t\tparts = append(parts, col+" LIKE "+ph(len(args)))
468
+ \t}
469
+ \tif f.StartsWith != nil {
470
+ \t\targs = append(args, *f.StartsWith+"%")
471
+ \t\tparts = append(parts, col+" LIKE "+ph(len(args)))
472
+ \t}
473
+ \tif f.EndsWith != nil {
474
+ \t\targs = append(args, "%"+*f.EndsWith)
475
+ \t\tparts = append(parts, col+" LIKE "+ph(len(args)))
476
+ \t}
477
+ \tif f.Gt != nil {
478
+ \t\targs = append(args, *f.Gt)
479
+ \t\tparts = append(parts, col+" > "+ph(len(args)))
480
+ \t}
481
+ \tif f.Gte != nil {
482
+ \t\targs = append(args, *f.Gte)
483
+ \t\tparts = append(parts, col+" >= "+ph(len(args)))
484
+ \t}
485
+ \tif f.Lt != nil {
486
+ \t\targs = append(args, *f.Lt)
487
+ \t\tparts = append(parts, col+" < "+ph(len(args)))
488
+ \t}
489
+ \tif f.Lte != nil {
490
+ \t\targs = append(args, *f.Lte)
491
+ \t\tparts = append(parts, col+" <= "+ph(len(args)))
492
+ \t}
493
+ \tif len(f.In) > 0 {
494
+ \t\tphs := make([]string, len(f.In))
495
+ \t\tfor i, v := range f.In {
496
+ \t\t\targs = append(args, v)
497
+ \t\t\tphs[i] = ph(len(args))
498
+ \t\t}
499
+ \t\tparts = append(parts, col+" IN ("+strings.Join(phs, ", ")+")")
500
+ \t}
501
+ \treturn strings.Join(parts, " AND "), args
502
+ }
503
+
504
+ // buildNumberFilter converts a NumberFilter to SQL fragments.
505
+ func buildNumberFilter(col string, f *NumberFilter, args []interface{}, ph func(int) string) (string, []interface{}) {
506
+ \tif f == nil {
507
+ \t\treturn "", args
508
+ \t}
509
+ \tvar parts []string
510
+ \tif f.Equals != nil { args = append(args, *f.Equals); parts = append(parts, col+" = "+ph(len(args))) }
511
+ \tif f.Not != nil { args = append(args, *f.Not); parts = append(parts, col+" <> "+ph(len(args))) }
512
+ \tif f.Gt != nil { args = append(args, *f.Gt); parts = append(parts, col+" > "+ph(len(args))) }
513
+ \tif f.Gte != nil { args = append(args, *f.Gte); parts = append(parts, col+" >= "+ph(len(args))) }
514
+ \tif f.Lt != nil { args = append(args, *f.Lt); parts = append(parts, col+" < "+ph(len(args))) }
515
+ \tif f.Lte != nil { args = append(args, *f.Lte); parts = append(parts, col+" <= "+ph(len(args))) }
516
+ \tif len(f.In) > 0 {
517
+ \t\tphs := make([]string, len(f.In))
518
+ \t\tfor i, v := range f.In { args = append(args, v); phs[i] = ph(len(args)) }
519
+ \t\tparts = append(parts, col+" IN ("+strings.Join(phs, ", ")+")")
520
+ \t}
521
+ \treturn strings.Join(parts, " AND "), args
522
+ }
523
+
524
+ // buildIntFilter converts an IntFilter to SQL fragments.
525
+ func buildIntFilter(col string, f *IntFilter, args []interface{}, ph func(int) string) (string, []interface{}) {
526
+ \tif f == nil {
527
+ \t\treturn "", args
528
+ \t}
529
+ \tvar parts []string
530
+ \tif f.Equals != nil { args = append(args, *f.Equals); parts = append(parts, col+" = "+ph(len(args))) }
531
+ \tif f.Not != nil { args = append(args, *f.Not); parts = append(parts, col+" <> "+ph(len(args))) }
532
+ \tif f.Gt != nil { args = append(args, *f.Gt); parts = append(parts, col+" > "+ph(len(args))) }
533
+ \tif f.Gte != nil { args = append(args, *f.Gte); parts = append(parts, col+" >= "+ph(len(args))) }
534
+ \tif f.Lt != nil { args = append(args, *f.Lt); parts = append(parts, col+" < "+ph(len(args))) }
535
+ \tif f.Lte != nil { args = append(args, *f.Lte); parts = append(parts, col+" <= "+ph(len(args))) }
536
+ \tif len(f.In) > 0 {
537
+ \t\tphs := make([]string, len(f.In))
538
+ \t\tfor i, v := range f.In { args = append(args, v); phs[i] = ph(len(args)) }
539
+ \t\tparts = append(parts, col+" IN ("+strings.Join(phs, ", ")+")")
540
+ \t}
541
+ \treturn strings.Join(parts, " AND "), args
542
+ }
543
+
544
+ // buildDateTimeFilter converts a DateTimeFilter to SQL fragments.
545
+ func buildDateTimeFilter(col string, f *DateTimeFilter, args []interface{}, ph func(int) string) (string, []interface{}) {
546
+ \tif f == nil {
547
+ \t\treturn "", args
548
+ \t}
549
+ \tvar parts []string
550
+ \tif f.Equals != nil { args = append(args, *f.Equals); parts = append(parts, col+" = "+ph(len(args))) }
551
+ \tif f.Not != nil { args = append(args, *f.Not); parts = append(parts, col+" <> "+ph(len(args))) }
552
+ \tif f.Gt != nil { args = append(args, *f.Gt); parts = append(parts, col+" > "+ph(len(args))) }
553
+ \tif f.Gte != nil { args = append(args, *f.Gte); parts = append(parts, col+" >= "+ph(len(args))) }
554
+ \tif f.Lt != nil { args = append(args, *f.Lt); parts = append(parts, col+" < "+ph(len(args))) }
555
+ \tif f.Lte != nil { args = append(args, *f.Lte); parts = append(parts, col+" <= "+ph(len(args))) }
556
+ \treturn strings.Join(parts, " AND "), args
557
+ }
558
+
559
+ `;
560
+ }
561
+
562
+ // ─── CRUD methods ──────────────────────────────────────────────────────────
563
+
564
+ private generateCrudMethods(): string {
565
+ return `// FindMany returns multiple records. Use model-specific FindManyArgs for type-safe filtering.
566
+ // Example: db.User.FindMany(ctx, &UserFindManyArgs{ Where: &UserWhereInput{ Name: &StringFilter{Contains: "Jo"} } })
567
+ func (c *TableClient[T]) FindMany(ctx context.Context, args interface{}) ([]T, error) {
568
+ clause, orderBy, take, skip, selectCols, queryArgs := c.parseArgs(args)
569
+ table := c.quoteTable()
570
+ var query string
571
+ if len(selectCols) > 0 {
572
+ quoted := make([]string, len(selectCols))
573
+ for i, col := range selectCols {
574
+ quoted[i] = c.quoteName(col)
575
+ }
576
+ query = "SELECT " + strings.Join(quoted, ", ") + " FROM " + table
577
+ } else {
578
+ query = "SELECT * FROM " + table
579
+ }
580
+ if clause != "" {
581
+ query += " WHERE " + clause
582
+ }
583
+ if orderBy != "" {
584
+ query += " " + orderBy
585
+ }
586
+ if c.Dialect == DialectPostgres || c.Dialect == DialectSqlite {
587
+ if take > 0 {
588
+ query += fmt.Sprintf(" LIMIT %d", take)
589
+ }
590
+ if skip > 0 {
591
+ query += fmt.Sprintf(" OFFSET %d", skip)
592
+ }
593
+ } else {
594
+ if skip > 0 {
595
+ query += fmt.Sprintf(" OFFSET %d ROWS", skip)
596
+ if take > 0 {
597
+ query += fmt.Sprintf(" FETCH NEXT %d ROWS ONLY", take)
598
+ }
599
+ } else if take > 0 {
600
+ // rewrite SELECT to SELECT TOP
601
+ query = strings.Replace(query, "SELECT *", fmt.Sprintf("SELECT TOP (%d) *", take), 1)
602
+ query = strings.Replace(query, "SELECT "+strings.Join(func() []string {
603
+ if len(selectCols) == 0 { return []string{"*"} }
604
+ return selectCols
605
+ }(), ", "), fmt.Sprintf("SELECT TOP (%d) *", take), 1)
606
+ }
607
+ }
608
+ return c.queryAndScan(ctx, query, queryArgs...)
609
+ }
610
+
611
+ // FindFirst returns first record matching criteria.
612
+ func (c *TableClient[T]) FindFirst(ctx context.Context, args interface{}) (*T, error) {
613
+ rows, err := c.FindMany(ctx, args)
614
+ if err != nil {
615
+ return nil, err
616
+ }
617
+ if len(rows) == 0 {
618
+ return nil, nil
619
+ }
620
+ return &rows[0], nil
621
+ }
622
+
623
+ // FindUnique fetches a single record by primary key.
624
+ // The where input should uniquely identify one record.
625
+ func (c *TableClient[T]) FindUnique(ctx context.Context, args interface{}) (*T, error) {
626
+ \treturn c.FindFirst(ctx, args)
627
+ }
628
+
629
+ // Count returns total records matching criteria.
630
+ func (c *TableClient[T]) Count(ctx context.Context, args interface{}) (int64, error) {
631
+ \tclause, _, _, _, _, queryArgs := c.parseArgs(args)
632
+ \tquery := fmt.Sprintf("SELECT COUNT(*) FROM %s", c.quoteTable())
633
+ \tif clause != "" {
634
+ \t\tquery += " WHERE " + clause
635
+ \t}
636
+ \tvar cnt int64
637
+ \terr := c.DB.QueryRowContext(ctx, query, queryArgs...).Scan(&cnt)
638
+ \treturn cnt, err
639
+ }
640
+
641
+ // Create inserts a new record and returns it.
642
+ func (c *TableClient[T]) Create(ctx context.Context, entity *T) (*T, error) {
643
+ \tval := reflect.ValueOf(entity).Elem()
644
+ \ttyp := val.Type()
645
+
646
+ \tvar cols []string
647
+ \tvar phs []string
648
+ \tvar args []interface{}
649
+
650
+ \tfor i := 0; i < typ.NumField(); i++ {
651
+ \t\tfield := typ.Field(i)
652
+ \t\tdbTag := field.Tag.Get("db")
653
+ \t\tif dbTag == "" || dbTag == "-" {
654
+ \t\t\tcontinue
655
+ \t\t}
656
+ \t\tfieldVal := val.Field(i)
657
+ \t\tif !fieldVal.IsZero() {
658
+ \t\t\tcols = append(cols, c.quoteName(dbTag))
659
+ \t\t\targs = append(args, fieldVal.Interface())
660
+ \t\t\tphs = append(phs, c.ph(len(args)))
661
+ \t\t}
662
+ \t}
663
+
664
+ \tquery := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", c.quoteTable(), strings.Join(cols, ", "), strings.Join(phs, ", "))
665
+ \t_, err := c.DB.ExecContext(ctx, query, args...)
666
+ \tif err != nil {
667
+ \t\treturn nil, err
668
+ \t}
669
+ \treturn entity, nil
670
+ }
671
+
672
+ // CreateMany inserts multiple records and returns count.
673
+ func (c *TableClient[T]) CreateMany(ctx context.Context, entities []*T) (int64, error) {
674
+ \tvar count int64
675
+ \tfor _, e := range entities {
676
+ \t\t_, err := c.Create(ctx, e)
677
+ \t\tif err != nil {
678
+ \t\t\treturn count, err
679
+ \t\t}
680
+ \t\tcount++
681
+ \t}
682
+ \treturn count, nil
683
+ }
684
+
685
+ // Update updates an existing record identified by its primary key field (isId tag or "id" column).
686
+ func (c *TableClient[T]) Update(ctx context.Context, entity *T) (*T, error) {
687
+ \tval := reflect.ValueOf(entity).Elem()
688
+ \ttyp := val.Type()
689
+
690
+ \tvar sets []string
691
+ \tvar args []interface{}
692
+ \tvar idVal interface{}
693
+ \tvar idCol string
694
+
695
+ \tfor i := 0; i < typ.NumField(); i++ {
696
+ \t\tfield := typ.Field(i)
697
+ \t\tdbTag := field.Tag.Get("db")
698
+ \t\tif dbTag == "" || dbTag == "-" {
699
+ \t\t\tcontinue
700
+ \t\t}
701
+ \t\tfieldVal := val.Field(i)
702
+ \t\tif strings.EqualFold(dbTag, "id") || field.Tag.Get("pk") == "true" {
703
+ \t\t\tidVal = fieldVal.Interface()
704
+ \t\t\tidCol = dbTag
705
+ \t\t} else if !fieldVal.IsZero() {
706
+ \t\t\targs = append(args, fieldVal.Interface())
707
+ \t\t\tsets = append(sets, c.quoteName(dbTag)+" = "+c.ph(len(args)))
708
+ \t\t}
709
+ \t}
710
+
711
+ \tif idVal == nil {
712
+ \t\treturn nil, fmt.Errorf("cannot update entity without primary key")
713
+ \t}
714
+
715
+ \targs = append(args, idVal)
716
+ \tquery := fmt.Sprintf("UPDATE %s SET %s WHERE %s = %s",
717
+ \t\tc.quoteTable(), strings.Join(sets, ", "), c.quoteName(idCol), c.ph(len(args)))
718
+ \t_, err := c.DB.ExecContext(ctx, query, args...)
719
+ \tif err != nil {
720
+ \t\treturn nil, err
721
+ \t}
722
+ \treturn entity, nil
723
+ }
724
+
725
+ // UpdateMany updates multiple records matching where criteria with given data fields.
726
+ func (c *TableClient[T]) UpdateMany(ctx context.Context, whereArgs interface{}, data map[string]interface{}) (int64, error) {
727
+ \tvar sets []string
728
+ \tvar execArgs []interface{}
729
+
730
+ \tfor col, v := range data {
731
+ \t\texecArgs = append(execArgs, v)
732
+ \t\tsets = append(sets, c.quoteName(col)+" = "+c.ph(len(execArgs)))
733
+ \t}
734
+
735
+ \tclause, _, _, _, _, whereQueryArgs := c.parseArgs(whereArgs)
736
+ \texecArgs = append(execArgs, whereQueryArgs...)
737
+ \tquery := fmt.Sprintf("UPDATE %s SET %s", c.quoteTable(), strings.Join(sets, ", "))
738
+ \tif clause != "" {
739
+ \t\tquery += " WHERE " + clause
740
+ \t}
741
+
742
+ \tres, err := c.DB.ExecContext(ctx, query, execArgs...)
743
+ \tif err != nil {
744
+ \t\treturn 0, err
745
+ \t}
746
+ \treturn res.RowsAffected()
747
+ }
748
+
749
+ // Delete removes a record by primary key value.
750
+ func (c *TableClient[T]) Delete(ctx context.Context, id interface{}) (bool, error) {
751
+ \tquery := fmt.Sprintf("DELETE FROM %s WHERE %s = %s", c.quoteTable(), c.quoteName("id"), c.ph(1))
752
+ \tres, err := c.DB.ExecContext(ctx, query, id)
753
+ \tif err != nil {
754
+ \t\treturn false, err
755
+ \t}
756
+ \tn, _ := res.RowsAffected()
757
+ \treturn n > 0, nil
758
+ }
759
+
760
+ // DeleteMany removes records matching where criteria.
761
+ func (c *TableClient[T]) DeleteMany(ctx context.Context, args interface{}) (int64, error) {
762
+ \tclause, _, _, _, _, queryArgs := c.parseArgs(args)
763
+ \tquery := fmt.Sprintf("DELETE FROM %s", c.quoteTable())
764
+ \tif clause != "" {
765
+ \t\tquery += " WHERE " + clause
766
+ \t}
767
+ \tres, err := c.DB.ExecContext(ctx, query, queryArgs...)
768
+ \tif err != nil {
769
+ \t\treturn 0, err
770
+ \t}
771
+ \treturn res.RowsAffected()
772
+ }
773
+
774
+ // Upsert creates or updates entity based on its primary key.
775
+ func (c *TableClient[T]) Upsert(ctx context.Context, entity *T) (*T, error) {
776
+ \tval := reflect.ValueOf(entity).Elem()
777
+ \tidField := findFieldByDbTag(val, "id")
778
+ \tif idField.IsValid() && !idField.IsZero() {
779
+ \t\texisting, err := c.FindUnique(ctx, nil)
780
+ \t\tif err == nil && existing != nil {
781
+ \t\t\treturn c.Update(ctx, entity)
782
+ \t\t}
783
+ \t}
784
+ \treturn c.Create(ctx, entity)
785
+ }
786
+
787
+ // QueryRaw executes a raw SQL SELECT and returns rows as typed slice.
788
+ func (c *TableClient[T]) QueryRaw(ctx context.Context, query string, args ...interface{}) ([]T, error) {
789
+ \treturn c.queryAndScan(ctx, query, args...)
790
+ }
791
+
792
+ // ExecuteRaw executes a non-SELECT raw SQL and returns affected rows count.
793
+ func (c *TableClient[T]) ExecuteRaw(ctx context.Context, query string, args ...interface{}) (int64, error) {
794
+ \tres, err := c.DB.ExecContext(ctx, query, args...)
795
+ \tif err != nil {
796
+ \t\treturn 0, err
797
+ \t}
798
+ \treturn res.RowsAffected()
799
+ }
800
+
801
+ // Transaction executes fn within a database transaction, rolling back on error.
802
+ func (c *TableClient[T]) Transaction(ctx context.Context, fn func(tx *sql.Tx) error) error {
803
+ \ttx, err := c.DB.BeginTx(ctx, nil)
804
+ \tif err != nil {
805
+ \t\treturn err
806
+ \t}
807
+ \tdefer func() {
808
+ \t\tif p := recover(); p != nil {
809
+ \t\t\t_ = tx.Rollback()
810
+ \t\t\tpanic(p)
811
+ \t\t}
812
+ \t}()
813
+ \tif err := fn(tx); err != nil {
814
+ \t\t_ = tx.Rollback()
815
+ \t\treturn err
816
+ \t}
817
+ \treturn tx.Commit()
818
+ }
819
+
820
+ // parseArgs is the generic where/orderBy/take/skip extractor.
821
+ // It uses reflection to read WhereClause, OrderBy, Take, Skip, Select from any args struct.
822
+ func (c *TableClient[T]) parseArgs(args interface{}) (whereClause string, orderBy string, take int, skip int, selectCols []string, queryArgs []interface{}) {
823
+ \tif args == nil {
824
+ \t\treturn
825
+ \t}
826
+ \tv := reflect.ValueOf(args)
827
+ \tif v.Kind() == reflect.Ptr {
828
+ \t\tif v.IsNil() {
829
+ \t\t\treturn
830
+ \t\t}
831
+ \t\tv = v.Elem()
832
+ \t}
833
+ \tif v.Kind() != reflect.Struct {
834
+ \t\treturn
835
+ \t}
836
+
837
+ \t// Where
838
+ \twhereField := v.FieldByName("Where")
839
+ \tif whereField.IsValid() && !whereField.IsNil() {
840
+ \t\t// where is a pointer to a model-specific WhereInput struct
841
+ \t\t// Delegate to buildWhereFromStruct
842
+ \t\twc, wa := c.buildWhereFromStruct(whereField.Elem(), 1)
843
+ \t\twhereClause = wc
844
+ \t\tqueryArgs = wa
845
+ \t}
846
+
847
+ \t// OrderBy
848
+ \torderByField := v.FieldByName("OrderBy")
849
+ \tif orderByField.IsValid() && !orderByField.IsNil() {
850
+ \t\tobv := orderByField.Elem()
851
+ \t\ttyp := obv.Type()
852
+ \t\tvar parts []string
853
+ \t\tfor i := 0; i < obv.NumField(); i++ {
854
+ \t\t\tfv := obv.Field(i)
855
+ \t\t\tif fv.IsNil() {
856
+ \t\t\t\tcontinue
857
+ \t\t\t}
858
+ \t\t\tcol := c.quoteName(c.toSnakeCase(typ.Field(i).Name))
859
+ \t\t\tdirection := strings.ToUpper(fv.Elem().String())
860
+ \t\t\tif direction != "DESC" {
861
+ \t\t\t\tdirection = "ASC"
862
+ \t\t\t}
863
+ \t\t\tparts = append(parts, col+" "+direction)
864
+ \t\t}
865
+ \t\tif len(parts) > 0 {
866
+ \t\t\torderBy = "ORDER BY " + strings.Join(parts, ", ")
867
+ \t\t}
868
+ \t}
869
+
870
+ \t// Take
871
+ \ttakeField := v.FieldByName("Take")
872
+ \tif takeField.IsValid() && !takeField.IsNil() {
873
+ \t\ttake = int(takeField.Elem().Int())
874
+ \t}
875
+
876
+ \t// Skip
877
+ \tskipField := v.FieldByName("Skip")
878
+ \tif skipField.IsValid() && !skipField.IsNil() {
879
+ \t\tskip = int(skipField.Elem().Int())
880
+ \t}
881
+
882
+ \t// Select
883
+ \tselectField := v.FieldByName("Select")
884
+ \tif selectField.IsValid() && selectField.Kind() == reflect.Slice && !selectField.IsNil() {
885
+ \t\tfor i := 0; i < selectField.Len(); i++ {
886
+ \t\t\tselectCols = append(selectCols, selectField.Index(i).String())
887
+ \t\t}
888
+ \t}
889
+
890
+ \treturn
891
+ }
892
+
893
+ // buildWhereFromStruct converts a WhereInput struct to SQL clause + args.
894
+ // It processes exported pointer fields using their type to detect StringFilter, NumberFilter, etc.
895
+ func (c *TableClient[T]) buildWhereFromStruct(v reflect.Value, argOffset int) (string, []interface{}) {
896
+ \ttyp := v.Type()
897
+ \tvar parts []string
898
+ \tvar args []interface{}
899
+
900
+ \tpHFunc := func(n int) string { return c.ph(n) }
901
+
902
+ \tfor i := 0; i < v.NumField(); i++ {
903
+ \t\tfield := typ.Field(i)
904
+ \t\tfv := v.Field(i)
905
+ \t\tname := field.Name
906
+
907
+ \t\t// AND / OR / NOT handling
908
+ \t\tif name == "AND" && fv.Kind() == reflect.Slice && !fv.IsNil() {
909
+ \t\t\tvar subParts []string
910
+ \t\t\tfor j := 0; j < fv.Len(); j++ {
911
+ \t\t\t\tsub, subArgs := c.buildWhereFromStruct(fv.Index(j), argOffset+len(args))
912
+ \t\t\t\tif sub != "" { subParts = append(subParts, sub); args = append(args, subArgs...) }
913
+ \t\t\t}
914
+ \t\t\tif len(subParts) > 0 { parts = append(parts, "("+strings.Join(subParts, " AND ")+")") }
915
+ \t\t\tcontinue
916
+ \t\t}
917
+ \t\tif name == "OR" && fv.Kind() == reflect.Slice && !fv.IsNil() {
918
+ \t\t\tvar subParts []string
919
+ \t\t\tfor j := 0; j < fv.Len(); j++ {
920
+ \t\t\t\tsub, subArgs := c.buildWhereFromStruct(fv.Index(j), argOffset+len(args))
921
+ \t\t\t\tif sub != "" { subParts = append(subParts, sub); args = append(args, subArgs...) }
922
+ \t\t\t}
923
+ \t\t\tif len(subParts) > 0 { parts = append(parts, "("+strings.Join(subParts, " OR ")+")") }
924
+ \t\t\tcontinue
925
+ \t\t}
926
+ \t\tif name == "NOT" && fv.Kind() == reflect.Ptr && !fv.IsNil() {
927
+ \t\t\tsub, subArgs := c.buildWhereFromStruct(fv.Elem(), argOffset+len(args))
928
+ \t\t\tif sub != "" { parts = append(parts, "NOT ("+sub+")"); args = append(args, subArgs...) }
929
+ \t\t\tcontinue
930
+ \t\t}
931
+
932
+ \t\tif fv.Kind() != reflect.Ptr || fv.IsNil() {
933
+ \t\t\tcontinue
934
+ \t\t}
935
+
936
+ \t\tcol := c.quoteName(c.toSnakeCase(name))
937
+ \t\telem := fv.Elem()
938
+ \t\ttypeName := field.Type.Elem().Name()
939
+
940
+ \t\tswitch typeName {
941
+ \t\tcase "StringFilter":
942
+ \t\t\tfilter := elem.Interface().(StringFilter)\n\t\t\tclause, newArgs := buildStringFilter(col, &filter, args, pHFunc)\n\t\t\tif clause != "" { parts = append(parts, clause) }\n\t\t\targs = newArgs
943
+ \t\tcase "NumberFilter":
944
+ \t\t\tfilter := elem.Interface().(NumberFilter)\n\t\t\tclause, newArgs := buildNumberFilter(col, &filter, args, pHFunc)\n\t\t\tif clause != "" { parts = append(parts, clause) }\n\t\t\targs = newArgs
945
+ \t\tcase "IntFilter":
946
+ \t\t\tfilter := elem.Interface().(IntFilter)\n\t\t\tclause, newArgs := buildIntFilter(col, &filter, args, pHFunc)\n\t\t\tif clause != "" { parts = append(parts, clause) }\n\t\t\targs = newArgs
947
+ \t\tcase "DateTimeFilter":
948
+ \t\t\tfilter := elem.Interface().(DateTimeFilter)\n\t\t\tclause, newArgs := buildDateTimeFilter(col, &filter, args, pHFunc)\n\t\t\tif clause != "" { parts = append(parts, clause) }\n\t\t\targs = newArgs
949
+ \t\tcase "bool":
950
+ \t\t\targs = append(args, elem.Bool())
951
+ \t\t\tparts = append(parts, col+" = "+c.ph(argOffset+len(args)))
952
+ \t\tcase "string":
953
+ \t\t\targs = append(args, elem.String())
954
+ \t\t\tparts = append(parts, col+" = "+c.ph(argOffset+len(args)))
955
+ \t\tcase "int", "int64":
956
+ \t\t\targs = append(args, elem.Int())
957
+ \t\t\tparts = append(parts, col+" = "+c.ph(argOffset+len(args)))
958
+ \t\tcase "float64":
959
+ \t\t\targs = append(args, elem.Float())
960
+ \t\t\tparts = append(parts, col+" = "+c.ph(argOffset+len(args)))
961
+ \t\t}
962
+ \t}
963
+
964
+ \treturn strings.Join(parts, " AND "), args
965
+ }
966
+
967
+ `;
968
+ }
969
+
970
+ // ─── VectorSearch ──────────────────────────────────────────────────────────
971
+
972
+ private generateVectorSearch(): string {
973
+ return `// VectorSearchOptions contains options for vector similarity queries.
974
+ type VectorSearchOptions struct {
975
+ \tTake int
976
+ \tWhere interface{}
977
+ \tVectorField string
978
+ \tDistanceMetric string // "cosine" | "euclidean" | "dot"
979
+ }
980
+
981
+ // VectorSearch performs native SQL vector similarity search with in-memory fallback.
982
+ // Primary: SQL Server VECTOR_DISTANCE (MSSQL) or pgvector operators (Postgres).
983
+ // Fallback: in-memory cosine/euclidean/dot similarity if DB lacks native extensions.
984
+ func (c *TableClient[T]) VectorSearch(ctx context.Context, vector []float64, opts ...VectorSearchOptions) ([]T, error) {
985
+ \ttake := 10
986
+ \tvectorField := "embedding"
987
+ \tdistanceMetric := "cosine"
988
+ \tvar whereArgs interface{}
989
+
990
+ \tif len(opts) > 0 {
991
+ \t\tif opts[0].Take > 0 { take = opts[0].Take }
992
+ \t\tif opts[0].VectorField != "" { vectorField = opts[0].VectorField }
993
+ \t\tif opts[0].DistanceMetric != "" { distanceMetric = opts[0].DistanceMetric }
994
+ \t\twhereArgs = opts[0].Where
995
+ \t}
996
+
997
+ \twhereCl, _, _, _, _, whereQueryArgs := c.parseArgs(whereArgs)
998
+ \tdim := len(vector)
999
+ \tvecBytes, _ := json.Marshal(vector)
1000
+ \tvecStr := string(vecBytes)
1001
+
1002
+ \t// 1. Primary path: dialect-aware native SQL vector query
1003
+ \tvar sqlQuery string
1004
+ \tqueryArgs := append([]interface{}{vecStr}, whereQueryArgs...)
1005
+ \tif c.Dialect == DialectPostgres {
1006
+ \t\top := "<=>"
1007
+ \t\tif strings.EqualFold(distanceMetric, "euclidean") { op = "<->" }
1008
+ \t\tif strings.EqualFold(distanceMetric, "dot") { op = "<#>" }
1009
+ \t\tvcol := c.quoteName(vectorField)
1010
+ \t\tsqlQuery = fmt.Sprintf("SELECT *, (%s %s $1::vector) AS distance FROM %s", vcol, op, c.quoteTable())
1011
+ \t\tif whereCl != "" {
1012
+ \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL AND (%s)", vcol, whereCl)
1013
+ \t\t} else {
1014
+ \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL", vcol)
1015
+ \t\t}
1016
+ \t\tsqlQuery += fmt.Sprintf(" ORDER BY distance ASC LIMIT %d", take)
1017
+ \t} else {
1018
+ \t\tvcol := c.quoteName(vectorField)
1019
+ \t\tsqlQuery = fmt.Sprintf(
1020
+ \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)",
1021
+ \t\t\ttake, distanceMetric, vcol, dim, dim, c.quoteTable(),
1022
+ \t\t)
1023
+ \t\tif whereCl != "" {
1024
+ \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL AND (%s)", vcol, whereCl)
1025
+ \t\t} else {
1026
+ \t\t\tsqlQuery += fmt.Sprintf(" WHERE %s IS NOT NULL", vcol)
1027
+ \t\t}
1028
+ \t\tsqlQuery += " ORDER BY distance ASC"
1029
+ \t}
1030
+
1031
+ \tnativeRows, nativeErr := c.queryAndScan(ctx, sqlQuery, queryArgs...)
1032
+ \tif nativeErr == nil {
1033
+ \t\treturn nativeRows, nil
1034
+ \t}
1035
+
1036
+ \t// 2. Fallback: in-memory similarity
1037
+ \trows, err := c.FindMany(ctx, whereArgs)
1038
+ \tif err != nil {
1039
+ \t\treturn nil, err
1040
+ \t}
1041
+
1042
+ \ttype scoredItem struct {
1043
+ \t\titem T
1044
+ \t\tdistance float64
1045
+ \t}
1046
+ \tvar scored []scoredItem
1047
+
1048
+ \tfor _, item := range rows {
1049
+ \t\tval := reflect.ValueOf(item)
1050
+ \t\tfieldVal := findFieldByDbTag(val, vectorField)
1051
+ \t\tif !fieldVal.IsValid() || fieldVal.IsZero() {
1052
+ \t\t\tcontinue
1053
+ \t\t}
1054
+
1055
+ \t\tvar rowVec []float64
1056
+ \t\tif fieldVal.Kind() == reflect.String {
1057
+ \t\t\t_ = json.Unmarshal([]byte(fieldVal.String()), &rowVec)
1058
+ \t\t} else if fieldVal.Kind() == reflect.Slice {
1059
+ \t\t\tfor i := 0; i < fieldVal.Len(); i++ {
1060
+ \t\t\t\trowVec = append(rowVec, fieldVal.Index(i).Float())
1061
+ \t\t\t}
1062
+ \t\t}
1063
+ \t\tif len(rowVec) != len(vector) {
1064
+ \t\t\tcontinue
1065
+ \t\t}
1066
+
1067
+ \t\tvar dist float64
1068
+ \t\tswitch strings.ToLower(distanceMetric) {
1069
+ \t\tcase "euclidean":
1070
+ \t\t\tvar sum float64
1071
+ \t\t\tfor i := range vector { d := vector[i] - rowVec[i]; sum += d * d }
1072
+ \t\t\tdist = math.Sqrt(sum)
1073
+ \t\tcase "dot":
1074
+ \t\t\tvar dot float64
1075
+ \t\t\tfor i := range vector { dot += vector[i] * rowVec[i] }
1076
+ \t\t\tdist = -dot
1077
+ \t\tdefault: // cosine
1078
+ \t\t\tvar dot, m1, m2 float64
1079
+ \t\t\tfor i := range vector { dot += vector[i] * rowVec[i]; m1 += vector[i] * vector[i]; m2 += rowVec[i] * rowVec[i] }
1080
+ \t\t\tif m1 > 0 && m2 > 0 { dist = 1.0 - dot/(math.Sqrt(m1)*math.Sqrt(m2)) } else { dist = 1.0 }
1081
+ \t\t}
1082
+
1083
+ \t\tscored = append(scored, scoredItem{item: item, distance: dist})
1084
+ \t}
1085
+
1086
+ \tsort.Slice(scored, func(i, j int) bool { return scored[i].distance < scored[j].distance })
1087
+
1088
+ \tvar result []T
1089
+ \tlimit := take
1090
+ \tif limit > len(scored) { limit = len(scored) }
1091
+ \tfor i := 0; i < limit; i++ {
1092
+ \t\tresult = append(result, scored[i].item)
1093
+ \t}
1094
+ \treturn result, nil
1095
+ }
1096
+
1097
+ `;
1098
+ }
1099
+
1100
+ // ─── Helpers ───────────────────────────────────────────────────────────────
1101
+
1102
+ private generateHelpers(): string {
1103
+ return `// findFieldByDbTag looks up a struct field by its "db" tag (case-insensitive).
1104
+ func findFieldByDbTag(val reflect.Value, dbTag string) reflect.Value {
1105
+ \tif val.Kind() == reflect.Ptr {
1106
+ \t\tval = val.Elem()
1107
+ \t}
1108
+ \tif val.Kind() != reflect.Struct {
1109
+ \t\treturn reflect.Value{}
1110
+ \t}
1111
+ \ttyp := val.Type()
1112
+ \tfor i := 0; i < typ.NumField(); i++ {
1113
+ \t\ttag := typ.Field(i).Tag.Get("db")
1114
+ \t\tif strings.EqualFold(tag, dbTag) || strings.EqualFold(typ.Field(i).Name, dbTag) {
1115
+ \t\t\treturn val.Field(i)
1116
+ \t\t}
1117
+ \t}
1118
+ \treturn reflect.Value{}
1119
+ }
1120
+
1121
+ // toSnakeCase converts CamelCase to snake_case for column name resolution.
1122
+ func (c *TableClient[T]) toSnakeCase(s string) string {
1123
+ \tvar res []rune
1124
+ \tfor i, r := range s {
1125
+ \t\tif i > 0 && r >= 'A' && r <= 'Z' {
1126
+ \t\t\tres = append(res, '_')
1127
+ \t\t}
1128
+ \t\tres = append(res, []rune(strings.ToLower(string(r)))...)
1129
+ \t}
1130
+ \treturn string(res)
1131
+ }
1132
+
1133
+ // Ensure unused imports are referenced
1134
+ var _ = sort.Slice
1135
+ var _ = time.Time{}
1136
+
1137
+ `;
1138
+ }
1139
+
1140
+ // ─── Per-model ORM types ───────────────────────────────────────────────────
1141
+
1142
+ private generateModelOrmTypes(model: Model): string {
1143
+ const name = this.capitalize(model.name);
1144
+ let content = `// ─── ${name} ORM Types ──────────────────────────────────────────────────────────\n\n`;
1145
+
1146
+ // WhereInput
1147
+ content += `// ${name}WhereInput provides type-safe filtering for ${name} queries.\ntype ${name}WhereInput struct {\n`;
1148
+ content += `\tAND []${name}WhereInput\n`;
1149
+ content += `\tOR []${name}WhereInput\n`;
1150
+ content += `\tNOT *${name}WhereInput\n`;
1151
+ for (const field of model.fields) {
1152
+ const filterType = this.getGoFilterType(field);
1153
+ content += `\t${this.capitalize(field.name)} *${filterType}\n`;
1154
+ }
1155
+ content += `}\n\n`;
1156
+
1157
+ // OrderByInput
1158
+ content += `// ${name}OrderByInput provides type-safe ordering for ${name} queries.\ntype ${name}OrderByInput struct {\n`;
1159
+ for (const field of model.fields) {
1160
+ content += `\t${this.capitalize(field.name)} *SortOrder\n`;
1161
+ }
1162
+ content += `}\n\n`;
1163
+
1164
+ // CreateInput
1165
+ content += `// ${name}CreateInput holds data for creating a new ${name} record.\ntype ${name}CreateInput struct {\n`;
1166
+ for (const field of model.fields) {
1167
+ if (!field.isId) {
1168
+ const goType = this.mapGoType(field, field.isOptional || field.hasDefault);
1169
+ content += `\t${this.capitalize(field.name)} ${goType}\n`;
1170
+ }
1171
+ }
1172
+ content += `}\n\n`;
1173
+
1174
+ // UpdateInput
1175
+ content += `// ${name}UpdateInput holds data for updating an existing ${name} record.\ntype ${name}UpdateInput struct {\n`;
1176
+ for (const field of model.fields) {
1177
+ if (!field.isId) {
1178
+ const goType = this.mapGoType(field, true); // always pointer so we can detect zero
1179
+ content += `\t${this.capitalize(field.name)} ${goType}\n`;
1180
+ }
1181
+ }
1182
+ content += `}\n\n`;
1183
+
1184
+ // FindManyArgs
1185
+ content += `// ${name}FindManyArgs provides ORM-style args for FindMany queries on ${name}.\ntype ${name}FindManyArgs struct {\n`;
1186
+ content += `\tWhere *${name}WhereInput\n`;
1187
+ content += `\tOrderBy *${name}OrderByInput\n`;
1188
+ content += `\tTake *int\n`;
1189
+ content += `\tSkip *int\n`;
1190
+ content += `\tSelect []string\n`;
1191
+ content += `}\n\n`;
1192
+
1193
+ // FindFirstArgs
1194
+ content += `// ${name}FindFirstArgs provides ORM-style args for FindFirst queries on ${name}.\ntype ${name}FindFirstArgs struct {\n`;
1195
+ content += `\tWhere *${name}WhereInput\n`;
1196
+ content += `\tOrderBy *${name}OrderByInput\n`;
1197
+ content += `\tSelect []string\n`;
1198
+ content += `}\n\n`;
1199
+
1200
+ // FindUniqueArgs
1201
+ content += `// ${name}FindUniqueArgs provides ORM-style args for FindUnique queries on ${name}.\ntype ${name}FindUniqueArgs struct {\n`;
1202
+ content += `\tWhere *${name}WhereInput\n`;
1203
+ content += `}\n\n`;
1204
+
1205
+ // DeleteManyArgs
1206
+ content += `// ${name}DeleteManyArgs provides ORM-style args for DeleteMany queries on ${name}.\ntype ${name}DeleteManyArgs struct {\n`;
1207
+ content += `\tWhere *${name}WhereInput\n`;
1208
+ content += `}\n\n`;
1209
+
1210
+ // CountArgs
1211
+ content += `// ${name}CountArgs provides ORM-style args for Count queries on ${name}.\ntype ${name}CountArgs struct {\n`;
1212
+ content += `\tWhere *${name}WhereInput\n`;
1213
+ content += `}\n\n`;
1214
+
1215
+ return content;
1216
+ }
1217
+
1218
+ private getGoFilterType(field: Field): string {
1219
+ // From the declared type, so a DECIMAL column gets the number filter and an INT
1220
+ // column the integer one; both used to arrive as `number` and pick the first.
1221
+ switch (fieldKind(field, this.provider)) {
1222
+ case 'date': return 'DateTimeFilter';
1223
+ case 'bool': return 'BoolFilter';
1224
+ case 'int':
1225
+ case 'bigint': return 'IntFilter';
1226
+ case 'float': return 'NumberFilter';
1227
+ default: return 'StringFilter';
1228
+ }
1229
+ }
1230
+
1231
+ // ─── Utilities ─────────────────────────────────────────────────────────────
1232
+
1233
+ private getAllPropertyVariations(modelName: string): string[] {
1234
+ const variations = new Set<string>();
1235
+ variations.add(modelName);
1236
+ variations.add(this.toCamelCase(modelName));
1237
+ variations.add(modelName + 's');
1238
+ variations.add(this.toCamelCase(modelName) + 's');
1239
+ const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
1240
+ for (const acronym of acronyms) {
1241
+ if (modelName.startsWith(acronym)) {
1242
+ variations.add(acronym.toLowerCase() + modelName.slice(acronym.length));
1243
+ variations.add(acronym[0].toLowerCase() + acronym.slice(1) + modelName.slice(acronym.length));
1244
+ variations.add(acronym.toLowerCase() + modelName.slice(acronym.length) + 's');
1245
+ }
1246
+ }
1247
+ return Array.from(variations);
1248
+ }
1249
+
1250
+ private capitalize(str: string): string {
1251
+ if (!str) return '';
1252
+ return str[0].toUpperCase() + str.slice(1);
1253
+ }
1254
+
1255
+ private toCamelCase(str: string): string {
1256
+ if (!str) return '';
1257
+ return str[0].toLowerCase() + str.slice(1);
1258
+ }
1259
+
1260
+ private toSnakeCase(str: string): string {
1261
+ if (!str) return '';
1262
+ return str
1263
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
1264
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
1265
+ .toLowerCase();
1266
+ }
1267
+ }