@an5/orm 1.0.7 → 1.0.9

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