@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.
Files changed (38) hide show
  1. package/README.md +167 -175
  2. package/dist/an5Metadata.js +64 -0
  3. package/dist/an5Orm.d.ts +92 -3
  4. package/dist/an5Orm.js +697 -206
  5. package/dist/errors.d.ts +1 -1
  6. package/dist/errors.js +1 -1
  7. package/dist/generator/src/code-generator.d.ts +14 -13
  8. package/dist/generator/src/code-generator.js +249 -225
  9. package/dist/generator/src/dotnet-generator.d.ts +13 -11
  10. package/dist/generator/src/dotnet-generator.js +872 -546
  11. package/dist/generator/src/index.d.ts +1 -1
  12. package/dist/generator/src/index.js +104 -88
  13. package/dist/generator/src/metadata-generator.d.ts +11 -8
  14. package/dist/generator/src/metadata-generator.js +104 -90
  15. package/dist/generator/src/parser.d.ts +11 -11
  16. package/dist/generator/src/parser.js +206 -206
  17. package/dist/generator/src/python-generator.d.ts +19 -9
  18. package/dist/generator/src/python-generator.js +345 -105
  19. package/dist/generator/src/types.d.ts +27 -27
  20. package/dist/generator/src/types.js +2 -2
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.js +19 -8
  23. package/dist/logger.js +1 -1
  24. package/dist/migration-core.d.ts +87 -0
  25. package/dist/migration-core.js +692 -0
  26. package/dist/scripts/release-cli.d.ts +2 -2
  27. package/dist/scripts/release-cli.js +115 -115
  28. package/dist/sql-utils.d.ts +3 -1
  29. package/dist/sql-utils.js +94 -24
  30. package/generator/src/code-generator.ts +259 -230
  31. package/generator/src/dotnet-generator.ts +882 -544
  32. package/generator/src/golang-generator.ts +1242 -0
  33. package/generator/src/index.ts +111 -116
  34. package/generator/src/metadata-generator.ts +116 -101
  35. package/generator/src/parser.ts +202 -202
  36. package/generator/src/python-generator.ts +424 -114
  37. package/generator/src/types.ts +29 -29
  38. package/package.json +12 -6
@@ -1,546 +1,872 @@
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.DotnetGenerator = void 0;
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
9
- class DotnetGenerator {
10
- constructor(outputDir) {
11
- this.outputDir = outputDir;
12
- if (!fs_1.default.existsSync(this.outputDir)) {
13
- fs_1.default.mkdirSync(this.outputDir, { recursive: true });
14
- }
15
- }
16
- generate(models) {
17
- // 1. Generate individual C# entity class files
18
- for (const model of models) {
19
- this.generateEntityClass(model);
20
- }
21
- // 2. Generate DbContext / Client metadata file
22
- this.generateDbContext(models);
23
- // 3. Generate configuration helper class
24
- this.generateConfigClass();
25
- }
26
- generateConfigClass() {
27
- const content = `// This file is auto-generated. Do not edit directly.
28
- using System;
29
-
30
- namespace An5Orm
31
- {
32
- public static class An5Config
33
- {
34
- public static string ConnectionString { get; set; } = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;";
35
- }
36
- }
37
- `;
38
- fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'An5Config.cs'), content);
39
- }
40
- mapType(fieldType, isOptional) {
41
- let csType = 'string';
42
- const lowerType = fieldType.toLowerCase();
43
- if (['int', 'integer', 'smallint', 'tinyint', 'number'].includes(lowerType)) {
44
- csType = 'int';
45
- }
46
- else if (['bigint', 'long'].includes(lowerType)) {
47
- csType = 'long';
48
- }
49
- else if (['float', 'real', 'double'].includes(lowerType)) {
50
- csType = 'double';
51
- }
52
- else if (['decimal', 'numeric', 'money', 'smallmoney'].includes(lowerType)) {
53
- csType = 'decimal';
54
- }
55
- else if (['bool', 'boolean', 'bit'].includes(lowerType)) {
56
- csType = 'bool';
57
- }
58
- else if (['datetime', 'datetime2', 'smalldatetime', 'date'].includes(lowerType)) {
59
- csType = 'DateTime';
60
- }
61
- else if (lowerType === 'datetimeoffset') {
62
- csType = 'DateTimeOffset';
63
- }
64
- else if (lowerType === 'time') {
65
- csType = 'TimeSpan';
66
- }
67
- else if (['guid', 'uuid', 'uniqueidentifier'].includes(lowerType)) {
68
- csType = 'Guid';
69
- }
70
- else if (['bytes', 'binary', 'varbinary', 'image'].includes(lowerType)) {
71
- csType = 'byte[]';
72
- }
73
- else if (['xml', 'hierarchyid', 'geography', 'geometry', 'sysname', 'sql_variant', 'sqlvariant', 'rowversion', 'variant'].includes(lowerType)) {
74
- csType = 'string';
75
- }
76
- else if (fieldType.endsWith('[]')) {
77
- csType = 'string';
78
- }
79
- if (isOptional && csType !== 'string' && !csType.endsWith('[]')) {
80
- csType += '?';
81
- }
82
- return csType;
83
- }
84
- generateEntityClass(model) {
85
- let content = `// This file is auto-generated. Do not edit directly.
86
- using System;
87
- using System.Collections.Generic;
88
-
89
- namespace An5Orm.Entities
90
- {
91
- public class ${model.name}
92
- {
93
- `;
94
- // Generate fields
95
- for (const field of model.fields) {
96
- const csType = this.mapType(field.type, field.isOptional);
97
- content += ` public ${csType} ${this.capitalize(field.name)} { get; set; }\n`;
98
- }
99
- // Generate relations
100
- if (model.relations.length > 0) {
101
- content += '\n // ── Relations ────────────────────────────────────────────────────────\n';
102
- for (const rel of model.relations) {
103
- if (rel.isArray) {
104
- content += ` public List<${rel.type}> ${this.capitalize(rel.name)} { get; set; } = new List<${rel.type}>();\n`;
105
- }
106
- else {
107
- content += ` public ${rel.type} ${this.capitalize(rel.name)} { get; set; }\n`;
108
- }
109
- }
110
- }
111
- content += ` }
112
- }
113
- `;
114
- fs_1.default.writeFileSync(path_1.default.join(this.outputDir, `${model.name}.cs`), content);
115
- }
116
- generateDbContext(models) {
117
- let content = `// This file is auto-generated. Do not edit directly.
118
- using System;
119
- using System.Collections.Generic;
120
- using System.Data;
121
- using System.Reflection;
122
- using Microsoft.Data.SqlClient;
123
- using An5Orm.Entities;
124
-
125
- namespace An5Orm
126
- {
127
- public class An5DbContext
128
- {
129
- public string ConnectionString { get; }
130
-
131
- [ThreadStatic]
132
- private static SqlConnection _txConn;
133
- [ThreadStatic]
134
- private static SqlTransaction _tx;
135
-
136
- public An5DbContext(string connectionString = null)
137
- {
138
- ConnectionString = connectionString ?? An5Config.ConnectionString;
139
- }
140
-
141
- public An5Transaction BeginTransaction()
142
- {
143
- var conn = new SqlConnection(ConnectionString);
144
- conn.Open();
145
- var tx = conn.BeginTransaction();
146
- _txConn = conn;
147
- _tx = tx;
148
- return new An5Transaction(conn, tx, () => {
149
- _txConn = null;
150
- _tx = null;
151
- });
152
- }
153
-
154
- public static SqlConnection GetActiveConnection(string connectionString, out bool isTx)
155
- {
156
- if (_txConn != null)
157
- {
158
- isTx = true;
159
- return _txConn;
160
- }
161
- isTx = false;
162
- var conn = new SqlConnection(connectionString);
163
- conn.Open();
164
- return conn;
165
- }
166
-
167
- public static SqlTransaction GetActiveTransaction() => _tx;
168
-
169
- // ── Tables / Repositories ──────────────────────────────────────────────
170
- `;
171
- for (const model of models) {
172
- content += ` public TableClient<${model.name}> ${this.capitalize(model.name)}s => new TableClient<${model.name}>(ConnectionString, "${model.schemaName}.${model.tableName}");\n`;
173
- }
174
- content += ` }
175
-
176
- public class An5Transaction : IDisposable
177
- {
178
- private readonly SqlConnection _conn;
179
- private readonly SqlTransaction _tx;
180
- private readonly Action _cleanup;
181
- private bool _completed;
182
-
183
- public An5Transaction(SqlConnection conn, SqlTransaction tx, Action cleanup)
184
- {
185
- _conn = conn;
186
- _tx = tx;
187
- _cleanup = cleanup;
188
- }
189
-
190
- public void Commit()
191
- {
192
- _tx.Commit();
193
- _completed = true;
194
- }
195
-
196
- public void Rollback()
197
- {
198
- _tx.Rollback();
199
- _completed = true;
200
- }
201
-
202
- public void Dispose()
203
- {
204
- if (!_completed)
205
- {
206
- try { _tx.Rollback(); } catch { }
207
- }
208
- _tx.Dispose();
209
- _conn.Dispose();
210
- _cleanup();
211
- }
212
- }
213
-
214
- public class TableClient<T> where T : new()
215
- {
216
- public string ConnectionString { get; }
217
- public string TableName { get; }
218
-
219
- public TableClient(string connectionString, string tableName)
220
- {
221
- ConnectionString = connectionString;
222
- TableName = tableName;
223
- }
224
-
225
- private SqlCommand CreateCommand(SqlConnection conn, string query)
226
- {
227
- var cmd = new SqlCommand(query, conn);
228
- var activeTx = An5DbContext.GetActiveTransaction();
229
- if (activeTx != null)
230
- {
231
- cmd.Transaction = activeTx;
232
- }
233
- return cmd;
234
- }
235
-
236
- public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null)
237
- {
238
- var list = new List<T>();
239
- string query = $"SELECT * FROM {TableName}";
240
- if (!string.IsNullOrEmpty(whereClause))
241
- {
242
- query += $" WHERE {whereClause}";
243
- }
244
-
245
- var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
246
- try
247
- {
248
- using (var cmd = CreateCommand(conn, query))
249
- {
250
- if (parameters != null)
251
- {
252
- foreach (var kvp in parameters)
253
- {
254
- cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
255
- }
256
- }
257
-
258
- using (var reader = cmd.ExecuteReader())
259
- {
260
- var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
261
- while (reader.Read())
262
- {
263
- var item = new T();
264
- foreach (var prop in properties)
265
- {
266
- if (HasColumn(reader, prop.Name))
267
- {
268
- var val = reader[prop.Name];
269
- if (val != DBNull.Value)
270
- {
271
- prop.SetValue(item, val);
272
- }
273
- }
274
- }
275
- list.Add(item);
276
- }
277
- }
278
- }
279
- }
280
- finally
281
- {
282
- if (!isTx) conn.Dispose();
283
- }
284
- return list;
285
- }
286
-
287
- public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
288
- {
289
- string query = $"SELECT TOP 1 * FROM {TableName}";
290
- if (!string.IsNullOrEmpty(whereClause))
291
- {
292
- query += $" WHERE {whereClause}";
293
- }
294
-
295
- var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
296
- try
297
- {
298
- using (var cmd = CreateCommand(conn, query))
299
- {
300
- if (parameters != null)
301
- {
302
- foreach (var kvp in parameters)
303
- {
304
- cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
305
- }
306
- }
307
-
308
- using (var reader = cmd.ExecuteReader())
309
- {
310
- if (reader.Read())
311
- {
312
- var item = new T();
313
- var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
314
- foreach (var prop in properties)
315
- {
316
- if (HasColumn(reader, prop.Name))
317
- {
318
- var val = reader[prop.Name];
319
- if (val != DBNull.Value)
320
- {
321
- prop.SetValue(item, val);
322
- }
323
- }
324
- }
325
- return item;
326
- }
327
- }
328
- }
329
- }
330
- finally
331
- {
332
- if (!isTx) conn.Dispose();
333
- }
334
- return default;
335
- }
336
-
337
- public T FindUnique(object id)
338
- {
339
- return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
340
- }
341
-
342
- public T Create(T entity)
343
- {
344
- var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
345
- var columns = new List<string>();
346
- var values = new List<string>();
347
- var sqlParams = new List<SqlParameter>();
348
-
349
- foreach (var prop in properties)
350
- {
351
- var val = prop.GetValue(entity);
352
- if (val != null)
353
- {
354
- columns.Add(prop.Name);
355
- values.Add("@" + prop.Name);
356
- sqlParams.Add(new SqlParameter("@" + prop.Name, val));
357
- }
358
- }
359
-
360
- string query = $"INSERT INTO {TableName} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
361
- var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
362
- try
363
- {
364
- using (var cmd = CreateCommand(conn, query))
365
- {
366
- cmd.Parameters.AddRange(sqlParams.ToArray());
367
- cmd.ExecuteNonQuery();
368
- if (!isTx)
369
- {
370
- // Commit standard queries if not in transaction
371
- }
372
- }
373
- }
374
- finally
375
- {
376
- if (!isTx) conn.Dispose();
377
- }
378
- return entity;
379
- }
380
-
381
- public T Update(T entity)
382
- {
383
- var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
384
- var sets = new List<string>();
385
- var sqlParams = new List<SqlParameter>();
386
- object idVal = null;
387
-
388
- foreach (var prop in properties)
389
- {
390
- var val = prop.GetValue(entity);
391
- if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
392
- {
393
- idVal = val;
394
- }
395
- else if (val != null)
396
- {
397
- sets.Add($"{prop.Name} = @{prop.Name}");
398
- sqlParams.Add(new SqlParameter("@" + prop.Name, val));
399
- }
400
- }
401
-
402
- if (idVal == null)
403
- {
404
- throw new InvalidOperationException("Cannot update entity without Id");
405
- }
406
-
407
- sqlParams.Add(new SqlParameter("@id", idVal));
408
- string query = $"UPDATE {TableName} SET {string.Join(", ", sets)} WHERE Id = @id";
409
- var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
410
- try
411
- {
412
- using (var cmd = CreateCommand(conn, query))
413
- {
414
- cmd.Parameters.AddRange(sqlParams.ToArray());
415
- cmd.ExecuteNonQuery();
416
- }
417
- }
418
- finally
419
- {
420
- if (!isTx) conn.Dispose();
421
- }
422
- return entity;
423
- }
424
-
425
- public bool Delete(object id)
426
- {
427
- string query = $"DELETE FROM {TableName} WHERE Id = @id";
428
- var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
429
- try
430
- {
431
- using (var cmd = CreateCommand(conn, query))
432
- {
433
- cmd.Parameters.AddWithValue("@id", id);
434
- int affected = cmd.ExecuteNonQuery();
435
- return affected > 0;
436
- }
437
- }
438
- finally
439
- {
440
- if (!isTx) conn.Dispose();
441
- }
442
- }
443
-
444
- public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
445
- {
446
- var rows = FindMany(whereClause, parameters);
447
- var results = new List<Tuple<T, double>>();
448
-
449
- var propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance);
450
- if (propInfo == null)
451
- {
452
- // Fallback case-insensitive
453
- propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
454
- }
455
- if (propInfo == null) return rows;
456
-
457
- foreach (var row in rows)
458
- {
459
- List<double> rowVector = null;
460
- var rawVal = propInfo.GetValue(row);
461
- if (rawVal != null)
462
- {
463
- try
464
- {
465
- if (rawVal is string jsonStr)
466
- {
467
- rowVector = ParseDoubleArray(jsonStr);
468
- }
469
- }
470
- catch { }
471
- }
472
-
473
- if (rowVector != null && rowVector.Count == vector.Count)
474
- {
475
- double sim = CosineSimilarity(vector, rowVector);
476
- double distance = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? (1.0 - sim) : sim;
477
- results.Add(Tuple.Create(row, distance));
478
- }
479
- }
480
-
481
- results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
482
-
483
- var output = new List<T>();
484
- int limit = Math.Min(take, results.Count);
485
- for (int i = 0; i < limit; i++)
486
- {
487
- // Set the distance property if the model has a public property named Distance
488
- var distanceProp = typeof(T).GetProperty("Distance", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
489
- if (distanceProp != null && distanceProp.PropertyType == typeof(double))
490
- {
491
- distanceProp.SetValue(results[i].Item1, results[i].Item2);
492
- }
493
- output.Add(results[i].Item1);
494
- }
495
- return output;
496
- }
497
-
498
- private static List<double> ParseDoubleArray(string json)
499
- {
500
- var clean = json.Trim('[', ']');
501
- if (string.IsNullOrWhiteSpace(clean)) return new List<double>();
502
-
503
- var parts = clean.Split(',');
504
- var list = new List<double>();
505
- foreach (var p in parts)
506
- {
507
- if (double.TryParse(p.Trim(), out double d))
508
- list.Add(d);
509
- }
510
- return list;
511
- }
512
-
513
- private static double CosineSimilarity(List<double> v1, List<double> v2)
514
- {
515
- double dot = 0.0, m1 = 0.0, m2 = 0.0;
516
- for (int i = 0; i < v1.Count; i++)
517
- {
518
- dot += v1[i] * v2[i];
519
- m1 += v1[i] * v1[i];
520
- m2 += v2[i] * v2[i];
521
- }
522
- if (m1 == 0 || m2 == 0) return 0.0;
523
- return dot / (Math.Sqrt(m1) * Math.Sqrt(m2));
524
- }
525
-
526
- private bool HasColumn(SqlDataReader reader, string columnName)
527
- {
528
- for (int i = 0; i < reader.FieldCount; i++)
529
- {
530
- if (reader.GetName(i).Equals(columnName, StringComparison.OrdinalIgnoreCase))
531
- return true;
532
- }
533
- return false;
534
- }
535
- }
536
- }
537
- `;
538
- fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'An5DbContext.cs'), content);
539
- }
540
- capitalize(str) {
541
- if (!str)
542
- return '';
543
- return str[0].toUpperCase() + str.slice(1);
544
- }
545
- }
546
- exports.DotnetGenerator = DotnetGenerator;
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.DotnetGenerator = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ class DotnetGenerator {
10
+ constructor(outputDir) {
11
+ this.outputDir = outputDir;
12
+ if (!fs_1.default.existsSync(this.outputDir)) {
13
+ fs_1.default.mkdirSync(this.outputDir, { recursive: true });
14
+ }
15
+ }
16
+ generate(models) {
17
+ // 1. Generate individual C# entity class files
18
+ for (const model of models) {
19
+ this.generateEntityClass(model);
20
+ }
21
+ // 2. Generate per-model ORM types (WhereInput, OrderByInput, FindManyArgs, etc.)
22
+ this.generateOrmTypes(models);
23
+ // 3. Generate DbContext / Client metadata file
24
+ this.generateDbContext(models);
25
+ // 4. Generate configuration helper class
26
+ this.generateConfigClass();
27
+ }
28
+ getCsFilterType(fieldType, isOptional) {
29
+ const lower = fieldType.toLowerCase();
30
+ if (['datetime', 'datetime2', 'smalldatetime', 'date', 'datetimeoffset'].includes(lower))
31
+ return 'DateTimeFilter';
32
+ if (['bool', 'boolean', 'bit'].includes(lower))
33
+ return 'BoolFilter';
34
+ if (['int', 'integer', 'smallint', 'tinyint', 'number'].includes(lower))
35
+ return 'IntFilter';
36
+ if (['bigint', 'long', 'float', 'real', 'double', 'decimal', 'numeric', 'money', 'smallmoney'].includes(lower))
37
+ return 'NumberFilter';
38
+ return 'StringFilter';
39
+ }
40
+ generateOrmTypes(models) {
41
+ let content = `// This file is auto-generated. Do not edit directly.
42
+ using System;
43
+ using System.Collections.Generic;
44
+
45
+ namespace An5Orm
46
+ {
47
+ // ── Base Filter Types ──────────────────────────────────────────────────────
48
+
49
+ /// <summary>Type-safe filter for string fields.</summary>
50
+ public class StringFilter
51
+ {
52
+ public new string Equals { get; set; }
53
+ public string Not { get; set; }
54
+ public List<string> In { get; set; }
55
+ public List<string> NotIn { get; set; }
56
+ public string Contains { get; set; }
57
+ public string StartsWith { get; set; }
58
+ public string EndsWith { get; set; }
59
+ public string Gt { get; set; }
60
+ public string Gte { get; set; }
61
+ public string Lt { get; set; }
62
+ public string Lte { get; set; }
63
+ }
64
+
65
+ /// <summary>Type-safe filter for integer fields.</summary>
66
+ public class IntFilter
67
+ {
68
+ public new int? Equals { get; set; }
69
+ public int? Not { get; set; }
70
+ public List<int> In { get; set; }
71
+ public List<int> NotIn { get; set; }
72
+ public int? Gt { get; set; }
73
+ public int? Gte { get; set; }
74
+ public int? Lt { get; set; }
75
+ public int? Lte { get; set; }
76
+ }
77
+
78
+ /// <summary>Type-safe filter for numeric/float fields.</summary>
79
+ public class NumberFilter
80
+ {
81
+ public new double? Equals { get; set; }
82
+ public double? Not { get; set; }
83
+ public List<double> In { get; set; }
84
+ public List<double> NotIn { get; set; }
85
+ public double? Gt { get; set; }
86
+ public double? Gte { get; set; }
87
+ public double? Lt { get; set; }
88
+ public double? Lte { get; set; }
89
+ }
90
+
91
+ /// <summary>Type-safe filter for boolean fields.</summary>
92
+ public class BoolFilter
93
+ {
94
+ public new bool? Equals { get; set; }
95
+ }
96
+
97
+ /// <summary>Type-safe filter for DateTime fields.</summary>
98
+ public class DateTimeFilter
99
+ {
100
+ public new DateTime? Equals { get; set; }
101
+ public DateTime? Not { get; set; }
102
+ public List<DateTime> In { get; set; }
103
+ public List<DateTime> NotIn { get; set; }
104
+ public DateTime? Gt { get; set; }
105
+ public DateTime? Gte { get; set; }
106
+ public DateTime? Lt { get; set; }
107
+ public DateTime? Lte { get; set; }
108
+ }
109
+
110
+ `;
111
+ for (const model of models) {
112
+ const name = this.capitalize(model.name);
113
+ const csType = (f) => this.mapType(f.type, f.isOptional);
114
+ content += ` // ── ${name} ORM Types ──────────────────────────────────────────────────────\n\n`;
115
+ // WhereInput
116
+ content += ` /// <summary>Type-safe WHERE filter for ${name} queries.</summary>\n`;
117
+ content += ` public class ${name}WhereInput\n {\n`;
118
+ content += ` public List<${name}WhereInput> AND { get; set; }\n`;
119
+ content += ` public List<${name}WhereInput> OR { get; set; }\n`;
120
+ content += ` public ${name}WhereInput NOT { get; set; }\n`;
121
+ for (const f of model.fields) {
122
+ const ft = this.getCsFilterType(f.type, f.isOptional);
123
+ content += ` public ${ft} ${this.capitalize(f.name)} { get; set; }\n`;
124
+ }
125
+ content += ` }\n\n`;
126
+ // OrderByInput
127
+ content += ` /// <summary>Type-safe ORDER BY for ${name} queries.</summary>\n`;
128
+ content += ` public class ${name}OrderByInput\n {\n`;
129
+ for (const f of model.fields) {
130
+ content += ` public string ${this.capitalize(f.name)} { get; set; } // "asc" or "desc"\n`;
131
+ }
132
+ content += ` }\n\n`;
133
+ // CreateInput
134
+ content += ` /// <summary>Typed data for creating a new ${name} record.</summary>\n`;
135
+ content += ` public class ${name}CreateInput\n {\n`;
136
+ for (const f of model.fields) {
137
+ if (!f.isId) {
138
+ const ct = this.mapType(f.type, f.isOptional || f.hasDefault);
139
+ content += ` public ${ct} ${this.capitalize(f.name)} { get; set; }\n`;
140
+ }
141
+ }
142
+ content += ` }\n\n`;
143
+ // UpdateInput
144
+ content += ` /// <summary>Typed data for updating an existing ${name} record.</summary>\n`;
145
+ content += ` public class ${name}UpdateInput\n {\n`;
146
+ for (const f of model.fields) {
147
+ if (!f.isId) {
148
+ const ut = this.mapType(f.type, true); // always nullable for update
149
+ content += ` public ${ut} ${this.capitalize(f.name)} { get; set; }\n`;
150
+ }
151
+ }
152
+ content += ` }\n\n`;
153
+ // FindManyArgs
154
+ content += ` /// <summary>ORM-style args for ${name}.FindMany().</summary>\n`;
155
+ content += ` public class ${name}FindManyArgs\n {\n`;
156
+ content += ` public ${name}WhereInput Where { get; set; }\n`;
157
+ content += ` public ${name}OrderByInput OrderBy { get; set; }\n`;
158
+ content += ` public int? Take { get; set; }\n`;
159
+ content += ` public int? Skip { get; set; }\n`;
160
+ content += ` public List<string> Select { get; set; }\n`;
161
+ content += ` }\n\n`;
162
+ // FindFirstArgs
163
+ content += ` /// <summary>ORM-style args for ${name}.FindFirst().</summary>\n`;
164
+ content += ` public class ${name}FindFirstArgs\n {\n`;
165
+ content += ` public ${name}WhereInput Where { get; set; }\n`;
166
+ content += ` public ${name}OrderByInput OrderBy { get; set; }\n`;
167
+ content += ` public List<string> Select { get; set; }\n`;
168
+ content += ` }\n\n`;
169
+ // FindUniqueArgs
170
+ content += ` /// <summary>ORM-style args for ${name}.FindUnique().</summary>\n`;
171
+ content += ` public class ${name}FindUniqueArgs\n {\n`;
172
+ content += ` public ${name}WhereInput Where { get; set; }\n`;
173
+ content += ` }\n\n`;
174
+ // DeleteManyArgs
175
+ content += ` /// <summary>ORM-style args for ${name}.DeleteMany().</summary>\n`;
176
+ content += ` public class ${name}DeleteManyArgs\n {\n`;
177
+ content += ` public ${name}WhereInput Where { get; set; }\n`;
178
+ content += ` }\n\n`;
179
+ // CountArgs
180
+ content += ` /// <summary>ORM-style args for ${name}.Count().</summary>\n`;
181
+ content += ` public class ${name}CountArgs\n {\n`;
182
+ content += ` public ${name}WhereInput Where { get; set; }\n`;
183
+ content += ` }\n\n`;
184
+ }
185
+ content += `}\n`;
186
+ fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'An5OrmTypes.cs'), content);
187
+ }
188
+ generateConfigClass() {
189
+ const content = `// This file is auto-generated. Do not edit directly.
190
+ using System;
191
+
192
+ namespace An5Orm
193
+ {
194
+ public static class An5Config
195
+ {
196
+ public static string ConnectionString { get; set; } = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;";
197
+ }
198
+ }
199
+ `;
200
+ fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'An5Config.cs'), content);
201
+ }
202
+ mapType(fieldType, isOptional) {
203
+ let csType = 'string';
204
+ const lowerType = fieldType.toLowerCase();
205
+ if (['int', 'integer', 'smallint', 'tinyint', 'number'].includes(lowerType)) {
206
+ csType = 'int';
207
+ }
208
+ else if (['bigint', 'long'].includes(lowerType)) {
209
+ csType = 'long';
210
+ }
211
+ else if (['float', 'real', 'double'].includes(lowerType)) {
212
+ csType = 'double';
213
+ }
214
+ else if (['decimal', 'numeric', 'money', 'smallmoney'].includes(lowerType)) {
215
+ csType = 'decimal';
216
+ }
217
+ else if (['bool', 'boolean', 'bit'].includes(lowerType)) {
218
+ csType = 'bool';
219
+ }
220
+ else if (['datetime', 'datetime2', 'smalldatetime', 'date'].includes(lowerType)) {
221
+ csType = 'DateTime';
222
+ }
223
+ else if (lowerType === 'datetimeoffset') {
224
+ csType = 'DateTimeOffset';
225
+ }
226
+ else if (lowerType === 'time') {
227
+ csType = 'TimeSpan';
228
+ }
229
+ else if (['guid', 'uuid', 'uniqueidentifier'].includes(lowerType)) {
230
+ csType = 'Guid';
231
+ }
232
+ else if (['bytes', 'binary', 'varbinary', 'image'].includes(lowerType)) {
233
+ csType = 'byte[]';
234
+ }
235
+ else if (['xml', 'hierarchyid', 'geography', 'geometry', 'sysname', 'sql_variant', 'sqlvariant', 'rowversion', 'variant'].includes(lowerType)) {
236
+ csType = 'string';
237
+ }
238
+ else if (fieldType.endsWith('[]')) {
239
+ csType = 'string';
240
+ }
241
+ if (isOptional && csType !== 'string' && !csType.endsWith('[]')) {
242
+ csType += '?';
243
+ }
244
+ return csType;
245
+ }
246
+ generateEntityClass(model) {
247
+ let content = `// This file is auto-generated. Do not edit directly.
248
+ using System;
249
+ using System.Collections.Generic;
250
+
251
+ namespace An5Orm.Entities
252
+ {
253
+ public class ${model.name}
254
+ {
255
+ `;
256
+ // Generate fields
257
+ for (const field of model.fields) {
258
+ const csType = this.mapType(field.type, field.isOptional);
259
+ content += ` public ${csType} ${this.capitalize(field.name)} { get; set; }\n`;
260
+ }
261
+ // Generate relations
262
+ if (model.relations.length > 0) {
263
+ content += '\n // ── Relations ────────────────────────────────────────────────────────\n';
264
+ for (const rel of model.relations) {
265
+ if (rel.isArray) {
266
+ content += ` public List<${rel.type}> ${this.capitalize(rel.name)} { get; set; } = new List<${rel.type}>();\n`;
267
+ }
268
+ else {
269
+ content += ` public ${rel.type} ${this.capitalize(rel.name)} { get; set; }\n`;
270
+ }
271
+ }
272
+ }
273
+ content += ` }
274
+ }
275
+ `;
276
+ fs_1.default.writeFileSync(path_1.default.join(this.outputDir, `${model.name}.cs`), content);
277
+ }
278
+ generateDbContext(models) {
279
+ let content = `// This file is auto-generated. Do not edit directly.
280
+ using System;
281
+ using System.Collections.Generic;
282
+ using System.Data;
283
+ using System.Reflection;
284
+ using System.Text.Json;
285
+ using Microsoft.Data.SqlClient;
286
+ using An5Orm.Entities;
287
+
288
+ namespace An5Orm
289
+ {
290
+ public class An5DbContext
291
+ {
292
+ public string ConnectionString { get; }
293
+
294
+ [ThreadStatic]
295
+ private static SqlConnection _txConn;
296
+ [ThreadStatic]
297
+ private static SqlTransaction _tx;
298
+
299
+ public An5DbContext(string connectionString = null)
300
+ {
301
+ ConnectionString = connectionString ?? An5Config.ConnectionString;
302
+ }
303
+
304
+ public An5Transaction BeginTransaction()
305
+ {
306
+ var conn = new SqlConnection(ConnectionString);
307
+ conn.Open();
308
+ var tx = conn.BeginTransaction();
309
+ _txConn = conn;
310
+ _tx = tx;
311
+ return new An5Transaction(conn, tx, () => {
312
+ _txConn = null;
313
+ _tx = null;
314
+ });
315
+ }
316
+
317
+ public static SqlConnection GetActiveConnection(string connectionString, out bool isTx)
318
+ {
319
+ if (_txConn != null)
320
+ {
321
+ isTx = true;
322
+ return _txConn;
323
+ }
324
+ isTx = false;
325
+ var conn = new SqlConnection(connectionString);
326
+ conn.Open();
327
+ return conn;
328
+ }
329
+
330
+ public static SqlTransaction GetActiveTransaction() => _tx;
331
+
332
+ // ── Tables / Repositories ──────────────────────────────────────────────
333
+ `;
334
+ for (const model of models) {
335
+ const name = this.capitalize(model.name);
336
+ content += ` public TableClient<${model.name}> ${name}s => new TableClient<${model.name}>(ConnectionString, "${model.schemaName}.${model.tableName}");\n`;
337
+ content += ` public TableClient<${model.name}> ${name} => ${name}s;\n`;
338
+ }
339
+ content += ` }
340
+
341
+ public class An5Transaction : IDisposable
342
+ {
343
+ private readonly SqlConnection _conn;
344
+ private readonly SqlTransaction _tx;
345
+ private readonly Action _cleanup;
346
+ private bool _completed;
347
+
348
+ public An5Transaction(SqlConnection conn, SqlTransaction tx, Action cleanup)
349
+ {
350
+ _conn = conn;
351
+ _tx = tx;
352
+ _cleanup = cleanup;
353
+ }
354
+
355
+ public void Commit()
356
+ {
357
+ _tx.Commit();
358
+ _completed = true;
359
+ }
360
+
361
+ public void Rollback()
362
+ {
363
+ _tx.Rollback();
364
+ _completed = true;
365
+ }
366
+
367
+ public void Dispose()
368
+ {
369
+ if (!_completed)
370
+ {
371
+ try { _tx.Rollback(); } catch { }
372
+ }
373
+ _tx.Dispose();
374
+ _conn.Dispose();
375
+ _cleanup();
376
+ }
377
+ }
378
+
379
+ public class TableClient<T> where T : new()
380
+ {
381
+ public string ConnectionString { get; }
382
+ public string TableName { get; }
383
+
384
+ public TableClient(string connectionString, string tableName)
385
+ {
386
+ ConnectionString = connectionString;
387
+ TableName = tableName;
388
+ }
389
+
390
+ private SqlCommand CreateCommand(SqlConnection conn, string query)
391
+ {
392
+ var cmd = new SqlCommand(query, conn);
393
+ var activeTx = An5DbContext.GetActiveTransaction();
394
+ if (activeTx != null)
395
+ {
396
+ cmd.Transaction = activeTx;
397
+ }
398
+ return cmd;
399
+ }
400
+
401
+ public List<T> QueryRaw(string query, Dictionary<string, object> parameters = null)
402
+ {
403
+ var list = new List<T>();
404
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
405
+ try
406
+ {
407
+ using (var cmd = CreateCommand(conn, query))
408
+ {
409
+ if (parameters != null)
410
+ {
411
+ foreach (var kvp in parameters)
412
+ {
413
+ cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
414
+ }
415
+ }
416
+
417
+ using (var reader = cmd.ExecuteReader())
418
+ {
419
+ var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
420
+ while (reader.Read())
421
+ {
422
+ var item = new T();
423
+ foreach (var prop in properties)
424
+ {
425
+ if (HasColumn(reader, prop.Name))
426
+ {
427
+ var val = reader[prop.Name];
428
+ if (val != DBNull.Value)
429
+ {
430
+ prop.SetValue(item, val);
431
+ }
432
+ }
433
+ }
434
+ list.Add(item);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ finally
440
+ {
441
+ if (!isTx) conn.Dispose();
442
+ }
443
+ return list;
444
+ }
445
+
446
+ public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null)
447
+ {
448
+ var list = new List<T>();
449
+ string query = $"SELECT * FROM {TableName}";
450
+ if (!string.IsNullOrEmpty(whereClause))
451
+ {
452
+ query += $" WHERE {whereClause}";
453
+ }
454
+
455
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
456
+ try
457
+ {
458
+ using (var cmd = CreateCommand(conn, query))
459
+ {
460
+ if (parameters != null)
461
+ {
462
+ foreach (var kvp in parameters)
463
+ {
464
+ cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
465
+ }
466
+ }
467
+
468
+ using (var reader = cmd.ExecuteReader())
469
+ {
470
+ var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
471
+ while (reader.Read())
472
+ {
473
+ var item = new T();
474
+ foreach (var prop in properties)
475
+ {
476
+ if (HasColumn(reader, prop.Name))
477
+ {
478
+ var val = reader[prop.Name];
479
+ if (val != DBNull.Value)
480
+ {
481
+ prop.SetValue(item, val);
482
+ }
483
+ }
484
+ }
485
+ list.Add(item);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ finally
491
+ {
492
+ if (!isTx) conn.Dispose();
493
+ }
494
+ return list;
495
+ }
496
+
497
+ public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
498
+ {
499
+ string query = $"SELECT TOP 1 * FROM {TableName}";
500
+ if (!string.IsNullOrEmpty(whereClause))
501
+ {
502
+ query += $" WHERE {whereClause}";
503
+ }
504
+
505
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
506
+ try
507
+ {
508
+ using (var cmd = CreateCommand(conn, query))
509
+ {
510
+ if (parameters != null)
511
+ {
512
+ foreach (var kvp in parameters)
513
+ {
514
+ cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
515
+ }
516
+ }
517
+
518
+ using (var reader = cmd.ExecuteReader())
519
+ {
520
+ if (reader.Read())
521
+ {
522
+ var item = new T();
523
+ var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
524
+ foreach (var prop in properties)
525
+ {
526
+ if (HasColumn(reader, prop.Name))
527
+ {
528
+ var val = reader[prop.Name];
529
+ if (val != DBNull.Value)
530
+ {
531
+ prop.SetValue(item, val);
532
+ }
533
+ }
534
+ }
535
+ return item;
536
+ }
537
+ }
538
+ }
539
+ }
540
+ finally
541
+ {
542
+ if (!isTx) conn.Dispose();
543
+ }
544
+ return default;
545
+ }
546
+
547
+ public T FindUnique(object id)
548
+ {
549
+ return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
550
+ }
551
+
552
+ public T Create(T entity)
553
+ {
554
+ var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
555
+ var columns = new List<string>();
556
+ var values = new List<string>();
557
+ var sqlParams = new List<SqlParameter>();
558
+
559
+ foreach (var prop in properties)
560
+ {
561
+ var val = prop.GetValue(entity);
562
+ if (val != null)
563
+ {
564
+ columns.Add(prop.Name);
565
+ values.Add("@" + prop.Name);
566
+ sqlParams.Add(new SqlParameter("@" + prop.Name, val));
567
+ }
568
+ }
569
+
570
+ string query = $"INSERT INTO {TableName} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
571
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
572
+ try
573
+ {
574
+ using (var cmd = CreateCommand(conn, query))
575
+ {
576
+ cmd.Parameters.AddRange(sqlParams.ToArray());
577
+ cmd.ExecuteNonQuery();
578
+ if (!isTx)
579
+ {
580
+ // Commit standard queries if not in transaction
581
+ }
582
+ }
583
+ }
584
+ finally
585
+ {
586
+ if (!isTx) conn.Dispose();
587
+ }
588
+ return entity;
589
+ }
590
+
591
+ public T Update(T entity)
592
+ {
593
+ var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
594
+ var sets = new List<string>();
595
+ var sqlParams = new List<SqlParameter>();
596
+ object idVal = null;
597
+
598
+ foreach (var prop in properties)
599
+ {
600
+ var val = prop.GetValue(entity);
601
+ if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
602
+ {
603
+ idVal = val;
604
+ }
605
+ else if (val != null)
606
+ {
607
+ sets.Add($"{prop.Name} = @{prop.Name}");
608
+ sqlParams.Add(new SqlParameter("@" + prop.Name, val));
609
+ }
610
+ }
611
+
612
+ if (idVal == null)
613
+ {
614
+ throw new InvalidOperationException("Cannot update entity without Id");
615
+ }
616
+
617
+ sqlParams.Add(new SqlParameter("@id", idVal));
618
+ string query = $"UPDATE {TableName} SET {string.Join(", ", sets)} WHERE Id = @id";
619
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
620
+ try
621
+ {
622
+ using (var cmd = CreateCommand(conn, query))
623
+ {
624
+ cmd.Parameters.AddRange(sqlParams.ToArray());
625
+ cmd.ExecuteNonQuery();
626
+ }
627
+ }
628
+ finally
629
+ {
630
+ if (!isTx) conn.Dispose();
631
+ }
632
+ return entity;
633
+ }
634
+
635
+ public bool Delete(object id)
636
+ {
637
+ string query = $"DELETE FROM {TableName} WHERE Id = @id";
638
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
639
+ try
640
+ {
641
+ using (var cmd = CreateCommand(conn, query))
642
+ {
643
+ cmd.Parameters.AddWithValue("@id", id);
644
+ int affected = cmd.ExecuteNonQuery();
645
+ return affected > 0;
646
+ }
647
+ }
648
+ finally
649
+ {
650
+ if (!isTx) conn.Dispose();
651
+ }
652
+ }
653
+
654
+ public int Count(string whereClause = null, Dictionary<string, object> parameters = null)
655
+ {
656
+ string query = $"SELECT COUNT(*) FROM {TableName}";
657
+ if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
658
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
659
+ try
660
+ {
661
+ using (var cmd = CreateCommand(conn, query))
662
+ {
663
+ if (parameters != null)
664
+ {
665
+ foreach (var kvp in parameters)
666
+ cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
667
+ }
668
+ var res = cmd.ExecuteScalar();
669
+ return res != null && res != DBNull.Value ? Convert.ToInt32(res) : 0;
670
+ }
671
+ }
672
+ finally { if (!isTx) conn.Dispose(); }
673
+ }
674
+
675
+ public int CreateMany(IEnumerable<T> entities)
676
+ {
677
+ int count = 0;
678
+ foreach (var entity in entities)
679
+ {
680
+ Create(entity);
681
+ count++;
682
+ }
683
+ return count;
684
+ }
685
+
686
+ public int UpdateMany(string whereClause, Dictionary<string, object> updateData, Dictionary<string, object> parameters = null)
687
+ {
688
+ if (updateData == null || updateData.Count == 0) return 0;
689
+ var sets = new List<string>();
690
+ var sqlParams = new List<SqlParameter>();
691
+ int pIndex = 0;
692
+ foreach (var kvp in updateData)
693
+ {
694
+ string paramName = "@u_" + pIndex++;
695
+ sets.Add($"{kvp.Key} = {paramName}");
696
+ sqlParams.Add(new SqlParameter(paramName, kvp.Value ?? DBNull.Value));
697
+ }
698
+ string query = $"UPDATE {TableName} SET {string.Join(", ", sets)}";
699
+ if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
700
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
701
+ try
702
+ {
703
+ using (var cmd = CreateCommand(conn, query))
704
+ {
705
+ cmd.Parameters.AddRange(sqlParams.ToArray());
706
+ if (parameters != null)
707
+ {
708
+ foreach (var kvp in parameters)
709
+ cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
710
+ }
711
+ return cmd.ExecuteNonQuery();
712
+ }
713
+ }
714
+ finally { if (!isTx) conn.Dispose(); }
715
+ }
716
+
717
+ public int DeleteMany(string whereClause = null, Dictionary<string, object> parameters = null)
718
+ {
719
+ string query = $"DELETE FROM {TableName}";
720
+ if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
721
+ var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
722
+ try
723
+ {
724
+ using (var cmd = CreateCommand(conn, query))
725
+ {
726
+ if (parameters != null)
727
+ {
728
+ foreach (var kvp in parameters)
729
+ cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
730
+ }
731
+ return cmd.ExecuteNonQuery();
732
+ }
733
+ }
734
+ finally { if (!isTx) conn.Dispose(); }
735
+ }
736
+
737
+ public T Upsert(T entity, string idColumnName = "Id")
738
+ {
739
+ var prop = typeof(T).GetProperty(idColumnName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
740
+ if (prop == null) throw new InvalidOperationException($"Property '{idColumnName}' not found on entity.");
741
+ var idVal = prop.GetValue(entity);
742
+ var existing = idVal != null ? FindUnique(idVal) : default;
743
+ if (existing != null) return Update(entity);
744
+ return Create(entity);
745
+ }
746
+
747
+ public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
748
+ {
749
+ // 1. Primary path: Native database SQL vector query execution (VECTOR_DISTANCE)
750
+ try
751
+ {
752
+ var dim = vector.Count;
753
+ var vecJson = JsonSerializer.Serialize(vector);
754
+ var sql = $"SELECT TOP ({take}) *, VECTOR_DISTANCE('{distanceMetric}', CAST([{vectorField}] AS VECTOR({dim}, float32)), CAST(@query_vector AS VECTOR({dim}, float32))) AS distance FROM {TableName} WITH (NOLOCK)";
755
+
756
+ var p = parameters != null ? new Dictionary<string, object>(parameters) : new Dictionary<string, object>();
757
+ p["query_vector"] = vecJson;
758
+
759
+ if (!string.IsNullOrWhiteSpace(whereClause))
760
+ sql += $" WHERE [{vectorField}] IS NOT NULL AND ({whereClause})";
761
+ else
762
+ sql += $" WHERE [{vectorField}] IS NOT NULL";
763
+ sql += " ORDER BY distance ASC";
764
+
765
+ var nativeRows = QueryRaw(sql, p);
766
+ if (nativeRows != null) return nativeRows;
767
+ }
768
+ catch
769
+ {
770
+ // Fallback to in-memory similarity computation if DB instance lacks native VECTOR_DISTANCE
771
+ }
772
+
773
+ // 2. Secondary fallback: In-memory similarity computation
774
+ var rows = FindMany(whereClause, parameters);
775
+ var results = new List<Tuple<T, double>>();
776
+
777
+ var propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance);
778
+ if (propInfo == null)
779
+ {
780
+ propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
781
+ }
782
+ if (propInfo == null) return rows;
783
+
784
+ foreach (var row in rows)
785
+ {
786
+ List<double> rowVector = null;
787
+ var rawVal = propInfo.GetValue(row);
788
+ if (rawVal != null)
789
+ {
790
+ try
791
+ {
792
+ if (rawVal is string jsonStr)
793
+ {
794
+ rowVector = ParseDoubleArray(jsonStr);
795
+ }
796
+ }
797
+ catch { }
798
+ }
799
+
800
+ if (rowVector != null && rowVector.Count == vector.Count)
801
+ {
802
+ double sim = CosineSimilarity(vector, rowVector);
803
+ double distance = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? (1.0 - sim) : sim;
804
+ results.Add(Tuple.Create(row, distance));
805
+ }
806
+ }
807
+
808
+ results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
809
+
810
+ var output = new List<T>();
811
+ int limit = Math.Min(take, results.Count);
812
+ for (int i = 0; i < limit; i++)
813
+ {
814
+ var distanceProp = typeof(T).GetProperty("Distance", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
815
+ if (distanceProp != null && distanceProp.PropertyType == typeof(double))
816
+ {
817
+ distanceProp.SetValue(results[i].Item1, results[i].Item2);
818
+ }
819
+ output.Add(results[i].Item1);
820
+ }
821
+ return output;
822
+ }
823
+
824
+ private static List<double> ParseDoubleArray(string json)
825
+ {
826
+ var clean = json.Trim('[', ']');
827
+ if (string.IsNullOrWhiteSpace(clean)) return new List<double>();
828
+
829
+ var parts = clean.Split(',');
830
+ var list = new List<double>();
831
+ foreach (var p in parts)
832
+ {
833
+ if (double.TryParse(p.Trim(), out double d))
834
+ list.Add(d);
835
+ }
836
+ return list;
837
+ }
838
+
839
+ private static double CosineSimilarity(List<double> v1, List<double> v2)
840
+ {
841
+ double dot = 0.0, m1 = 0.0, m2 = 0.0;
842
+ for (int i = 0; i < v1.Count; i++)
843
+ {
844
+ dot += v1[i] * v2[i];
845
+ m1 += v1[i] * v1[i];
846
+ m2 += v2[i] * v2[i];
847
+ }
848
+ if (m1 == 0 || m2 == 0) return 0.0;
849
+ return dot / (Math.Sqrt(m1) * Math.Sqrt(m2));
850
+ }
851
+
852
+ private bool HasColumn(SqlDataReader reader, string columnName)
853
+ {
854
+ for (int i = 0; i < reader.FieldCount; i++)
855
+ {
856
+ if (reader.GetName(i).Equals(columnName, StringComparison.OrdinalIgnoreCase))
857
+ return true;
858
+ }
859
+ return false;
860
+ }
861
+ }
862
+ }
863
+ `;
864
+ fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'An5DbContext.cs'), content);
865
+ }
866
+ capitalize(str) {
867
+ if (!str)
868
+ return '';
869
+ return str[0].toUpperCase() + str.slice(1);
870
+ }
871
+ }
872
+ exports.DotnetGenerator = DotnetGenerator;