@an5/orm 1.0.9 → 1.1.1

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