@an5/orm 1.0.8 → 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.
- package/README.md +167 -175
- package/dist/an5Metadata.js +64 -0
- package/dist/an5Orm.d.ts +92 -3
- package/dist/an5Orm.js +697 -206
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/generator/src/code-generator.d.ts +14 -13
- package/dist/generator/src/code-generator.js +249 -225
- package/dist/generator/src/dotnet-generator.d.ts +13 -11
- package/dist/generator/src/dotnet-generator.js +872 -546
- package/dist/generator/src/index.d.ts +1 -1
- package/dist/generator/src/index.js +104 -88
- package/dist/generator/src/metadata-generator.d.ts +11 -8
- package/dist/generator/src/metadata-generator.js +104 -90
- package/dist/generator/src/parser.d.ts +11 -11
- package/dist/generator/src/parser.js +206 -206
- package/dist/generator/src/python-generator.d.ts +19 -9
- package/dist/generator/src/python-generator.js +345 -105
- package/dist/generator/src/types.d.ts +27 -27
- package/dist/generator/src/types.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +19 -8
- package/dist/logger.js +1 -1
- package/dist/migration-core.d.ts +87 -0
- package/dist/migration-core.js +692 -0
- package/dist/scripts/release-cli.d.ts +2 -2
- package/dist/scripts/release-cli.js +115 -115
- package/dist/sql-utils.d.ts +3 -1
- package/dist/sql-utils.js +94 -24
- package/generator/src/code-generator.ts +259 -230
- package/generator/src/dotnet-generator.ts +882 -544
- package/generator/src/golang-generator.ts +1242 -0
- package/generator/src/index.ts +111 -116
- package/generator/src/metadata-generator.ts +116 -101
- package/generator/src/parser.ts +202 -202
- package/generator/src/python-generator.ts +424 -114
- package/generator/src/types.ts +29 -29
- package/package.json +12 -6
|
@@ -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
|
+
}
|