@an5/orm 1.0.5 → 1.0.6
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 +123 -123
- package/dist/an5Orm.js +14 -14
- package/dist/cleanup.js +10 -7
- package/dist/cli.js +0 -0
- package/dist/generator/src/code-generator.js +39 -39
- package/dist/generator/src/dotnet-generator.js +436 -436
- package/dist/generator/src/index.js +5 -2
- package/dist/migrate.js +25 -22
- package/dist/pull.js +44 -41
- package/dist/push.js +4 -1
- package/dist/seed.js +123 -123
- package/generator/src/code-generator.ts +234 -234
- package/generator/src/dotnet-generator.ts +544 -544
- package/generator/src/index.ts +94 -91
- package/generator/src/metadata-generator.ts +66 -66
- package/generator/src/parser.ts +202 -202
- package/generator/src/python-generator.ts +74 -74
- package/generator/src/types.ts +29 -29
- package/package.json +53 -53
|
@@ -1,544 +1,544 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { Model, Field, Relation } from './types';
|
|
4
|
-
|
|
5
|
-
export class DotnetGenerator {
|
|
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 individual C# entity class files
|
|
14
|
-
for (const model of models) {
|
|
15
|
-
this.generateEntityClass(model);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// 2. Generate DbContext / Client metadata file
|
|
19
|
-
this.generateDbContext(models);
|
|
20
|
-
|
|
21
|
-
// 3. Generate configuration helper class
|
|
22
|
-
this.generateConfigClass();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private generateConfigClass() {
|
|
26
|
-
const content = `// This file is auto-generated. Do not edit directly.
|
|
27
|
-
using System;
|
|
28
|
-
|
|
29
|
-
namespace An5Orm
|
|
30
|
-
{
|
|
31
|
-
public static class An5Config
|
|
32
|
-
{
|
|
33
|
-
public static string ConnectionString { get; set; } = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
`;
|
|
37
|
-
fs.writeFileSync(path.join(this.outputDir, 'An5Config.cs'), content);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private mapType(fieldType: string, isOptional: boolean): string {
|
|
41
|
-
let csType = 'string';
|
|
42
|
-
const lowerType = fieldType.toLowerCase();
|
|
43
|
-
|
|
44
|
-
if (['int', 'integer', 'smallint', 'tinyint', 'number'].includes(lowerType)) {
|
|
45
|
-
csType = 'int';
|
|
46
|
-
} else if (['bigint', 'long'].includes(lowerType)) {
|
|
47
|
-
csType = 'long';
|
|
48
|
-
} else if (['float', 'real', 'double'].includes(lowerType)) {
|
|
49
|
-
csType = 'double';
|
|
50
|
-
} else if (['decimal', 'numeric', 'money', 'smallmoney'].includes(lowerType)) {
|
|
51
|
-
csType = 'decimal';
|
|
52
|
-
} else if (['bool', 'boolean', 'bit'].includes(lowerType)) {
|
|
53
|
-
csType = 'bool';
|
|
54
|
-
} else if (['datetime', 'datetime2', 'smalldatetime', 'date'].includes(lowerType)) {
|
|
55
|
-
csType = 'DateTime';
|
|
56
|
-
} else if (lowerType === 'datetimeoffset') {
|
|
57
|
-
csType = 'DateTimeOffset';
|
|
58
|
-
} else if (lowerType === 'time') {
|
|
59
|
-
csType = 'TimeSpan';
|
|
60
|
-
} else if (['guid', 'uuid', 'uniqueidentifier'].includes(lowerType)) {
|
|
61
|
-
csType = 'Guid';
|
|
62
|
-
} else if (['bytes', 'binary', 'varbinary', 'image'].includes(lowerType)) {
|
|
63
|
-
csType = 'byte[]';
|
|
64
|
-
} else if (['xml', 'hierarchyid', 'geography', 'geometry', 'sysname', 'sql_variant', 'sqlvariant', 'rowversion', 'variant'].includes(lowerType)) {
|
|
65
|
-
csType = 'string';
|
|
66
|
-
} else if (fieldType.endsWith('[]')) {
|
|
67
|
-
csType = 'string';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (isOptional && csType !== 'string' && !csType.endsWith('[]')) {
|
|
71
|
-
csType += '?';
|
|
72
|
-
}
|
|
73
|
-
return csType;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private generateEntityClass(model: Model) {
|
|
77
|
-
let content = `// This file is auto-generated. Do not edit directly.
|
|
78
|
-
using System;
|
|
79
|
-
using System.Collections.Generic;
|
|
80
|
-
|
|
81
|
-
namespace An5Orm.Entities
|
|
82
|
-
{
|
|
83
|
-
public class ${model.name}
|
|
84
|
-
{
|
|
85
|
-
`;
|
|
86
|
-
|
|
87
|
-
// Generate fields
|
|
88
|
-
for (const field of model.fields) {
|
|
89
|
-
const csType = this.mapType(field.type, field.isOptional);
|
|
90
|
-
content += ` public ${csType} ${this.capitalize(field.name)} { get; set; }\n`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Generate relations
|
|
94
|
-
if (model.relations.length > 0) {
|
|
95
|
-
content += '\n // ── Relations ────────────────────────────────────────────────────────\n';
|
|
96
|
-
for (const rel of model.relations) {
|
|
97
|
-
if (rel.isArray) {
|
|
98
|
-
content += ` public List<${rel.type}> ${this.capitalize(rel.name)} { get; set; } = new List<${rel.type}>();\n`;
|
|
99
|
-
} else {
|
|
100
|
-
content += ` public ${rel.type} ${this.capitalize(rel.name)} { get; set; }\n`;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
content += ` }
|
|
106
|
-
}
|
|
107
|
-
`;
|
|
108
|
-
|
|
109
|
-
fs.writeFileSync(path.join(this.outputDir, `${model.name}.cs`), content);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private generateDbContext(models: Model[]) {
|
|
113
|
-
let content = `// This file is auto-generated. Do not edit directly.
|
|
114
|
-
using System;
|
|
115
|
-
using System.Collections.Generic;
|
|
116
|
-
using System.Data;
|
|
117
|
-
using System.Reflection;
|
|
118
|
-
using Microsoft.Data.SqlClient;
|
|
119
|
-
using An5Orm.Entities;
|
|
120
|
-
|
|
121
|
-
namespace An5Orm
|
|
122
|
-
{
|
|
123
|
-
public class An5DbContext
|
|
124
|
-
{
|
|
125
|
-
public string ConnectionString { get; }
|
|
126
|
-
|
|
127
|
-
[ThreadStatic]
|
|
128
|
-
private static SqlConnection _txConn;
|
|
129
|
-
[ThreadStatic]
|
|
130
|
-
private static SqlTransaction _tx;
|
|
131
|
-
|
|
132
|
-
public An5DbContext(string connectionString = null)
|
|
133
|
-
{
|
|
134
|
-
ConnectionString = connectionString ?? An5Config.ConnectionString;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
public An5Transaction BeginTransaction()
|
|
138
|
-
{
|
|
139
|
-
var conn = new SqlConnection(ConnectionString);
|
|
140
|
-
conn.Open();
|
|
141
|
-
var tx = conn.BeginTransaction();
|
|
142
|
-
_txConn = conn;
|
|
143
|
-
_tx = tx;
|
|
144
|
-
return new An5Transaction(conn, tx, () => {
|
|
145
|
-
_txConn = null;
|
|
146
|
-
_tx = null;
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
public static SqlConnection GetActiveConnection(string connectionString, out bool isTx)
|
|
151
|
-
{
|
|
152
|
-
if (_txConn != null)
|
|
153
|
-
{
|
|
154
|
-
isTx = true;
|
|
155
|
-
return _txConn;
|
|
156
|
-
}
|
|
157
|
-
isTx = false;
|
|
158
|
-
var conn = new SqlConnection(connectionString);
|
|
159
|
-
conn.Open();
|
|
160
|
-
return conn;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
public static SqlTransaction GetActiveTransaction() => _tx;
|
|
164
|
-
|
|
165
|
-
// ── Tables / Repositories ──────────────────────────────────────────────
|
|
166
|
-
`;
|
|
167
|
-
|
|
168
|
-
for (const model of models) {
|
|
169
|
-
content += ` public TableClient<${model.name}> ${this.capitalize(model.name)}s => new TableClient<${model.name}>(ConnectionString, "${model.schemaName}.${model.tableName}");\n`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
content += ` }
|
|
173
|
-
|
|
174
|
-
public class An5Transaction : IDisposable
|
|
175
|
-
{
|
|
176
|
-
private readonly SqlConnection _conn;
|
|
177
|
-
private readonly SqlTransaction _tx;
|
|
178
|
-
private readonly Action _cleanup;
|
|
179
|
-
private bool _completed;
|
|
180
|
-
|
|
181
|
-
public An5Transaction(SqlConnection conn, SqlTransaction tx, Action cleanup)
|
|
182
|
-
{
|
|
183
|
-
_conn = conn;
|
|
184
|
-
_tx = tx;
|
|
185
|
-
_cleanup = cleanup;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
public void Commit()
|
|
189
|
-
{
|
|
190
|
-
_tx.Commit();
|
|
191
|
-
_completed = true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
public void Rollback()
|
|
195
|
-
{
|
|
196
|
-
_tx.Rollback();
|
|
197
|
-
_completed = true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
public void Dispose()
|
|
201
|
-
{
|
|
202
|
-
if (!_completed)
|
|
203
|
-
{
|
|
204
|
-
try { _tx.Rollback(); } catch { }
|
|
205
|
-
}
|
|
206
|
-
_tx.Dispose();
|
|
207
|
-
_conn.Dispose();
|
|
208
|
-
_cleanup();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
public class TableClient<T> where T : new()
|
|
213
|
-
{
|
|
214
|
-
public string ConnectionString { get; }
|
|
215
|
-
public string TableName { get; }
|
|
216
|
-
|
|
217
|
-
public TableClient(string connectionString, string tableName)
|
|
218
|
-
{
|
|
219
|
-
ConnectionString = connectionString;
|
|
220
|
-
TableName = tableName;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private SqlCommand CreateCommand(SqlConnection conn, string query)
|
|
224
|
-
{
|
|
225
|
-
var cmd = new SqlCommand(query, conn);
|
|
226
|
-
var activeTx = An5DbContext.GetActiveTransaction();
|
|
227
|
-
if (activeTx != null)
|
|
228
|
-
{
|
|
229
|
-
cmd.Transaction = activeTx;
|
|
230
|
-
}
|
|
231
|
-
return cmd;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
235
|
-
{
|
|
236
|
-
var list = new List<T>();
|
|
237
|
-
string query = $"SELECT * FROM {TableName}";
|
|
238
|
-
if (!string.IsNullOrEmpty(whereClause))
|
|
239
|
-
{
|
|
240
|
-
query += $" WHERE {whereClause}";
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
244
|
-
try
|
|
245
|
-
{
|
|
246
|
-
using (var cmd = CreateCommand(conn, query))
|
|
247
|
-
{
|
|
248
|
-
if (parameters != null)
|
|
249
|
-
{
|
|
250
|
-
foreach (var kvp in parameters)
|
|
251
|
-
{
|
|
252
|
-
cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
using (var reader = cmd.ExecuteReader())
|
|
257
|
-
{
|
|
258
|
-
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
259
|
-
while (reader.Read())
|
|
260
|
-
{
|
|
261
|
-
var item = new T();
|
|
262
|
-
foreach (var prop in properties)
|
|
263
|
-
{
|
|
264
|
-
if (HasColumn(reader, prop.Name))
|
|
265
|
-
{
|
|
266
|
-
var val = reader[prop.Name];
|
|
267
|
-
if (val != DBNull.Value)
|
|
268
|
-
{
|
|
269
|
-
prop.SetValue(item, val);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
list.Add(item);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
finally
|
|
279
|
-
{
|
|
280
|
-
if (!isTx) conn.Dispose();
|
|
281
|
-
}
|
|
282
|
-
return list;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
286
|
-
{
|
|
287
|
-
string query = $"SELECT TOP 1 * FROM {TableName}";
|
|
288
|
-
if (!string.IsNullOrEmpty(whereClause))
|
|
289
|
-
{
|
|
290
|
-
query += $" WHERE {whereClause}";
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
294
|
-
try
|
|
295
|
-
{
|
|
296
|
-
using (var cmd = CreateCommand(conn, query))
|
|
297
|
-
{
|
|
298
|
-
if (parameters != null)
|
|
299
|
-
{
|
|
300
|
-
foreach (var kvp in parameters)
|
|
301
|
-
{
|
|
302
|
-
cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
using (var reader = cmd.ExecuteReader())
|
|
307
|
-
{
|
|
308
|
-
if (reader.Read())
|
|
309
|
-
{
|
|
310
|
-
var item = new T();
|
|
311
|
-
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
312
|
-
foreach (var prop in properties)
|
|
313
|
-
{
|
|
314
|
-
if (HasColumn(reader, prop.Name))
|
|
315
|
-
{
|
|
316
|
-
var val = reader[prop.Name];
|
|
317
|
-
if (val != DBNull.Value)
|
|
318
|
-
{
|
|
319
|
-
prop.SetValue(item, val);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return item;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
finally
|
|
329
|
-
{
|
|
330
|
-
if (!isTx) conn.Dispose();
|
|
331
|
-
}
|
|
332
|
-
return default;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
public T FindUnique(object id)
|
|
336
|
-
{
|
|
337
|
-
return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
public T Create(T entity)
|
|
341
|
-
{
|
|
342
|
-
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
343
|
-
var columns = new List<string>();
|
|
344
|
-
var values = new List<string>();
|
|
345
|
-
var sqlParams = new List<SqlParameter>();
|
|
346
|
-
|
|
347
|
-
foreach (var prop in properties)
|
|
348
|
-
{
|
|
349
|
-
var val = prop.GetValue(entity);
|
|
350
|
-
if (val != null)
|
|
351
|
-
{
|
|
352
|
-
columns.Add(prop.Name);
|
|
353
|
-
values.Add("@" + prop.Name);
|
|
354
|
-
sqlParams.Add(new SqlParameter("@" + prop.Name, val));
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
string query = $"INSERT INTO {TableName} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
|
|
359
|
-
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
360
|
-
try
|
|
361
|
-
{
|
|
362
|
-
using (var cmd = CreateCommand(conn, query))
|
|
363
|
-
{
|
|
364
|
-
cmd.Parameters.AddRange(sqlParams.ToArray());
|
|
365
|
-
cmd.ExecuteNonQuery();
|
|
366
|
-
if (!isTx)
|
|
367
|
-
{
|
|
368
|
-
// Commit standard queries if not in transaction
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
finally
|
|
373
|
-
{
|
|
374
|
-
if (!isTx) conn.Dispose();
|
|
375
|
-
}
|
|
376
|
-
return entity;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
public T Update(T entity)
|
|
380
|
-
{
|
|
381
|
-
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
382
|
-
var sets = new List<string>();
|
|
383
|
-
var sqlParams = new List<SqlParameter>();
|
|
384
|
-
object idVal = null;
|
|
385
|
-
|
|
386
|
-
foreach (var prop in properties)
|
|
387
|
-
{
|
|
388
|
-
var val = prop.GetValue(entity);
|
|
389
|
-
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
390
|
-
{
|
|
391
|
-
idVal = val;
|
|
392
|
-
}
|
|
393
|
-
else if (val != null)
|
|
394
|
-
{
|
|
395
|
-
sets.Add($"{prop.Name} = @{prop.Name}");
|
|
396
|
-
sqlParams.Add(new SqlParameter("@" + prop.Name, val));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (idVal == null)
|
|
401
|
-
{
|
|
402
|
-
throw new InvalidOperationException("Cannot update entity without Id");
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
sqlParams.Add(new SqlParameter("@id", idVal));
|
|
406
|
-
string query = $"UPDATE {TableName} SET {string.Join(", ", sets)} WHERE Id = @id";
|
|
407
|
-
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
408
|
-
try
|
|
409
|
-
{
|
|
410
|
-
using (var cmd = CreateCommand(conn, query))
|
|
411
|
-
{
|
|
412
|
-
cmd.Parameters.AddRange(sqlParams.ToArray());
|
|
413
|
-
cmd.ExecuteNonQuery();
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
finally
|
|
417
|
-
{
|
|
418
|
-
if (!isTx) conn.Dispose();
|
|
419
|
-
}
|
|
420
|
-
return entity;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
public bool Delete(object id)
|
|
424
|
-
{
|
|
425
|
-
string query = $"DELETE FROM {TableName} WHERE Id = @id";
|
|
426
|
-
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
427
|
-
try
|
|
428
|
-
{
|
|
429
|
-
using (var cmd = CreateCommand(conn, query))
|
|
430
|
-
{
|
|
431
|
-
cmd.Parameters.AddWithValue("@id", id);
|
|
432
|
-
int affected = cmd.ExecuteNonQuery();
|
|
433
|
-
return affected > 0;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
finally
|
|
437
|
-
{
|
|
438
|
-
if (!isTx) conn.Dispose();
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
|
|
443
|
-
{
|
|
444
|
-
var rows = FindMany(whereClause, parameters);
|
|
445
|
-
var results = new List<Tuple<T, double>>();
|
|
446
|
-
|
|
447
|
-
var propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance);
|
|
448
|
-
if (propInfo == null)
|
|
449
|
-
{
|
|
450
|
-
// Fallback case-insensitive
|
|
451
|
-
propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
452
|
-
}
|
|
453
|
-
if (propInfo == null) return rows;
|
|
454
|
-
|
|
455
|
-
foreach (var row in rows)
|
|
456
|
-
{
|
|
457
|
-
List<double> rowVector = null;
|
|
458
|
-
var rawVal = propInfo.GetValue(row);
|
|
459
|
-
if (rawVal != null)
|
|
460
|
-
{
|
|
461
|
-
try
|
|
462
|
-
{
|
|
463
|
-
if (rawVal is string jsonStr)
|
|
464
|
-
{
|
|
465
|
-
rowVector = ParseDoubleArray(jsonStr);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
catch { }
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (rowVector != null && rowVector.Count == vector.Count)
|
|
472
|
-
{
|
|
473
|
-
double sim = CosineSimilarity(vector, rowVector);
|
|
474
|
-
double distance = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? (1.0 - sim) : sim;
|
|
475
|
-
results.Add(Tuple.Create(row, distance));
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
|
|
480
|
-
|
|
481
|
-
var output = new List<T>();
|
|
482
|
-
int limit = Math.Min(take, results.Count);
|
|
483
|
-
for (int i = 0; i < limit; i++)
|
|
484
|
-
{
|
|
485
|
-
// Set the distance property if the model has a public property named Distance
|
|
486
|
-
var distanceProp = typeof(T).GetProperty("Distance", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
487
|
-
if (distanceProp != null && distanceProp.PropertyType == typeof(double))
|
|
488
|
-
{
|
|
489
|
-
distanceProp.SetValue(results[i].Item1, results[i].Item2);
|
|
490
|
-
}
|
|
491
|
-
output.Add(results[i].Item1);
|
|
492
|
-
}
|
|
493
|
-
return output;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
private static List<double> ParseDoubleArray(string json)
|
|
497
|
-
{
|
|
498
|
-
var clean = json.Trim('[', ']');
|
|
499
|
-
if (string.IsNullOrWhiteSpace(clean)) return new List<double>();
|
|
500
|
-
|
|
501
|
-
var parts = clean.Split(',');
|
|
502
|
-
var list = new List<double>();
|
|
503
|
-
foreach (var p in parts)
|
|
504
|
-
{
|
|
505
|
-
if (double.TryParse(p.Trim(), out double d))
|
|
506
|
-
list.Add(d);
|
|
507
|
-
}
|
|
508
|
-
return list;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private static double CosineSimilarity(List<double> v1, List<double> v2)
|
|
512
|
-
{
|
|
513
|
-
double dot = 0.0, m1 = 0.0, m2 = 0.0;
|
|
514
|
-
for (int i = 0; i < v1.Count; i++)
|
|
515
|
-
{
|
|
516
|
-
dot += v1[i] * v2[i];
|
|
517
|
-
m1 += v1[i] * v1[i];
|
|
518
|
-
m2 += v2[i] * v2[i];
|
|
519
|
-
}
|
|
520
|
-
if (m1 == 0 || m2 == 0) return 0.0;
|
|
521
|
-
return dot / (Math.Sqrt(m1) * Math.Sqrt(m2));
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
private bool HasColumn(SqlDataReader reader, string columnName)
|
|
525
|
-
{
|
|
526
|
-
for (int i = 0; i < reader.FieldCount; i++)
|
|
527
|
-
{
|
|
528
|
-
if (reader.GetName(i).Equals(columnName, StringComparison.OrdinalIgnoreCase))
|
|
529
|
-
return true;
|
|
530
|
-
}
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
`;
|
|
536
|
-
|
|
537
|
-
fs.writeFileSync(path.join(this.outputDir, 'An5DbContext.cs'), content);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
private capitalize(str: string): string {
|
|
541
|
-
if (!str) return '';
|
|
542
|
-
return str[0].toUpperCase() + str.slice(1);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Model, Field, Relation } from './types';
|
|
4
|
+
|
|
5
|
+
export class DotnetGenerator {
|
|
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 individual C# entity class files
|
|
14
|
+
for (const model of models) {
|
|
15
|
+
this.generateEntityClass(model);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Generate DbContext / Client metadata file
|
|
19
|
+
this.generateDbContext(models);
|
|
20
|
+
|
|
21
|
+
// 3. Generate configuration helper class
|
|
22
|
+
this.generateConfigClass();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private generateConfigClass() {
|
|
26
|
+
const content = `// This file is auto-generated. Do not edit directly.
|
|
27
|
+
using System;
|
|
28
|
+
|
|
29
|
+
namespace An5Orm
|
|
30
|
+
{
|
|
31
|
+
public static class An5Config
|
|
32
|
+
{
|
|
33
|
+
public static string ConnectionString { get; set; } = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
fs.writeFileSync(path.join(this.outputDir, 'An5Config.cs'), content);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private mapType(fieldType: string, isOptional: boolean): string {
|
|
41
|
+
let csType = 'string';
|
|
42
|
+
const lowerType = fieldType.toLowerCase();
|
|
43
|
+
|
|
44
|
+
if (['int', 'integer', 'smallint', 'tinyint', 'number'].includes(lowerType)) {
|
|
45
|
+
csType = 'int';
|
|
46
|
+
} else if (['bigint', 'long'].includes(lowerType)) {
|
|
47
|
+
csType = 'long';
|
|
48
|
+
} else if (['float', 'real', 'double'].includes(lowerType)) {
|
|
49
|
+
csType = 'double';
|
|
50
|
+
} else if (['decimal', 'numeric', 'money', 'smallmoney'].includes(lowerType)) {
|
|
51
|
+
csType = 'decimal';
|
|
52
|
+
} else if (['bool', 'boolean', 'bit'].includes(lowerType)) {
|
|
53
|
+
csType = 'bool';
|
|
54
|
+
} else if (['datetime', 'datetime2', 'smalldatetime', 'date'].includes(lowerType)) {
|
|
55
|
+
csType = 'DateTime';
|
|
56
|
+
} else if (lowerType === 'datetimeoffset') {
|
|
57
|
+
csType = 'DateTimeOffset';
|
|
58
|
+
} else if (lowerType === 'time') {
|
|
59
|
+
csType = 'TimeSpan';
|
|
60
|
+
} else if (['guid', 'uuid', 'uniqueidentifier'].includes(lowerType)) {
|
|
61
|
+
csType = 'Guid';
|
|
62
|
+
} else if (['bytes', 'binary', 'varbinary', 'image'].includes(lowerType)) {
|
|
63
|
+
csType = 'byte[]';
|
|
64
|
+
} else if (['xml', 'hierarchyid', 'geography', 'geometry', 'sysname', 'sql_variant', 'sqlvariant', 'rowversion', 'variant'].includes(lowerType)) {
|
|
65
|
+
csType = 'string';
|
|
66
|
+
} else if (fieldType.endsWith('[]')) {
|
|
67
|
+
csType = 'string';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isOptional && csType !== 'string' && !csType.endsWith('[]')) {
|
|
71
|
+
csType += '?';
|
|
72
|
+
}
|
|
73
|
+
return csType;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private generateEntityClass(model: Model) {
|
|
77
|
+
let content = `// This file is auto-generated. Do not edit directly.
|
|
78
|
+
using System;
|
|
79
|
+
using System.Collections.Generic;
|
|
80
|
+
|
|
81
|
+
namespace An5Orm.Entities
|
|
82
|
+
{
|
|
83
|
+
public class ${model.name}
|
|
84
|
+
{
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
// Generate fields
|
|
88
|
+
for (const field of model.fields) {
|
|
89
|
+
const csType = this.mapType(field.type, field.isOptional);
|
|
90
|
+
content += ` public ${csType} ${this.capitalize(field.name)} { get; set; }\n`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate relations
|
|
94
|
+
if (model.relations.length > 0) {
|
|
95
|
+
content += '\n // ── Relations ────────────────────────────────────────────────────────\n';
|
|
96
|
+
for (const rel of model.relations) {
|
|
97
|
+
if (rel.isArray) {
|
|
98
|
+
content += ` public List<${rel.type}> ${this.capitalize(rel.name)} { get; set; } = new List<${rel.type}>();\n`;
|
|
99
|
+
} else {
|
|
100
|
+
content += ` public ${rel.type} ${this.capitalize(rel.name)} { get; set; }\n`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
content += ` }
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
fs.writeFileSync(path.join(this.outputDir, `${model.name}.cs`), content);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private generateDbContext(models: Model[]) {
|
|
113
|
+
let content = `// This file is auto-generated. Do not edit directly.
|
|
114
|
+
using System;
|
|
115
|
+
using System.Collections.Generic;
|
|
116
|
+
using System.Data;
|
|
117
|
+
using System.Reflection;
|
|
118
|
+
using Microsoft.Data.SqlClient;
|
|
119
|
+
using An5Orm.Entities;
|
|
120
|
+
|
|
121
|
+
namespace An5Orm
|
|
122
|
+
{
|
|
123
|
+
public class An5DbContext
|
|
124
|
+
{
|
|
125
|
+
public string ConnectionString { get; }
|
|
126
|
+
|
|
127
|
+
[ThreadStatic]
|
|
128
|
+
private static SqlConnection _txConn;
|
|
129
|
+
[ThreadStatic]
|
|
130
|
+
private static SqlTransaction _tx;
|
|
131
|
+
|
|
132
|
+
public An5DbContext(string connectionString = null)
|
|
133
|
+
{
|
|
134
|
+
ConnectionString = connectionString ?? An5Config.ConnectionString;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public An5Transaction BeginTransaction()
|
|
138
|
+
{
|
|
139
|
+
var conn = new SqlConnection(ConnectionString);
|
|
140
|
+
conn.Open();
|
|
141
|
+
var tx = conn.BeginTransaction();
|
|
142
|
+
_txConn = conn;
|
|
143
|
+
_tx = tx;
|
|
144
|
+
return new An5Transaction(conn, tx, () => {
|
|
145
|
+
_txConn = null;
|
|
146
|
+
_tx = null;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public static SqlConnection GetActiveConnection(string connectionString, out bool isTx)
|
|
151
|
+
{
|
|
152
|
+
if (_txConn != null)
|
|
153
|
+
{
|
|
154
|
+
isTx = true;
|
|
155
|
+
return _txConn;
|
|
156
|
+
}
|
|
157
|
+
isTx = false;
|
|
158
|
+
var conn = new SqlConnection(connectionString);
|
|
159
|
+
conn.Open();
|
|
160
|
+
return conn;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public static SqlTransaction GetActiveTransaction() => _tx;
|
|
164
|
+
|
|
165
|
+
// ── Tables / Repositories ──────────────────────────────────────────────
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
for (const model of models) {
|
|
169
|
+
content += ` public TableClient<${model.name}> ${this.capitalize(model.name)}s => new TableClient<${model.name}>(ConnectionString, "${model.schemaName}.${model.tableName}");\n`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
content += ` }
|
|
173
|
+
|
|
174
|
+
public class An5Transaction : IDisposable
|
|
175
|
+
{
|
|
176
|
+
private readonly SqlConnection _conn;
|
|
177
|
+
private readonly SqlTransaction _tx;
|
|
178
|
+
private readonly Action _cleanup;
|
|
179
|
+
private bool _completed;
|
|
180
|
+
|
|
181
|
+
public An5Transaction(SqlConnection conn, SqlTransaction tx, Action cleanup)
|
|
182
|
+
{
|
|
183
|
+
_conn = conn;
|
|
184
|
+
_tx = tx;
|
|
185
|
+
_cleanup = cleanup;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public void Commit()
|
|
189
|
+
{
|
|
190
|
+
_tx.Commit();
|
|
191
|
+
_completed = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public void Rollback()
|
|
195
|
+
{
|
|
196
|
+
_tx.Rollback();
|
|
197
|
+
_completed = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public void Dispose()
|
|
201
|
+
{
|
|
202
|
+
if (!_completed)
|
|
203
|
+
{
|
|
204
|
+
try { _tx.Rollback(); } catch { }
|
|
205
|
+
}
|
|
206
|
+
_tx.Dispose();
|
|
207
|
+
_conn.Dispose();
|
|
208
|
+
_cleanup();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public class TableClient<T> where T : new()
|
|
213
|
+
{
|
|
214
|
+
public string ConnectionString { get; }
|
|
215
|
+
public string TableName { get; }
|
|
216
|
+
|
|
217
|
+
public TableClient(string connectionString, string tableName)
|
|
218
|
+
{
|
|
219
|
+
ConnectionString = connectionString;
|
|
220
|
+
TableName = tableName;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private SqlCommand CreateCommand(SqlConnection conn, string query)
|
|
224
|
+
{
|
|
225
|
+
var cmd = new SqlCommand(query, conn);
|
|
226
|
+
var activeTx = An5DbContext.GetActiveTransaction();
|
|
227
|
+
if (activeTx != null)
|
|
228
|
+
{
|
|
229
|
+
cmd.Transaction = activeTx;
|
|
230
|
+
}
|
|
231
|
+
return cmd;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
235
|
+
{
|
|
236
|
+
var list = new List<T>();
|
|
237
|
+
string query = $"SELECT * FROM {TableName}";
|
|
238
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
239
|
+
{
|
|
240
|
+
query += $" WHERE {whereClause}";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
244
|
+
try
|
|
245
|
+
{
|
|
246
|
+
using (var cmd = CreateCommand(conn, query))
|
|
247
|
+
{
|
|
248
|
+
if (parameters != null)
|
|
249
|
+
{
|
|
250
|
+
foreach (var kvp in parameters)
|
|
251
|
+
{
|
|
252
|
+
cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
using (var reader = cmd.ExecuteReader())
|
|
257
|
+
{
|
|
258
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
259
|
+
while (reader.Read())
|
|
260
|
+
{
|
|
261
|
+
var item = new T();
|
|
262
|
+
foreach (var prop in properties)
|
|
263
|
+
{
|
|
264
|
+
if (HasColumn(reader, prop.Name))
|
|
265
|
+
{
|
|
266
|
+
var val = reader[prop.Name];
|
|
267
|
+
if (val != DBNull.Value)
|
|
268
|
+
{
|
|
269
|
+
prop.SetValue(item, val);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
list.Add(item);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
finally
|
|
279
|
+
{
|
|
280
|
+
if (!isTx) conn.Dispose();
|
|
281
|
+
}
|
|
282
|
+
return list;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
286
|
+
{
|
|
287
|
+
string query = $"SELECT TOP 1 * FROM {TableName}";
|
|
288
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
289
|
+
{
|
|
290
|
+
query += $" WHERE {whereClause}";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
294
|
+
try
|
|
295
|
+
{
|
|
296
|
+
using (var cmd = CreateCommand(conn, query))
|
|
297
|
+
{
|
|
298
|
+
if (parameters != null)
|
|
299
|
+
{
|
|
300
|
+
foreach (var kvp in parameters)
|
|
301
|
+
{
|
|
302
|
+
cmd.Parameters.AddWithValue(kvp.Key.StartsWith("@") ? kvp.Key : "@" + kvp.Key, kvp.Value ?? DBNull.Value);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
using (var reader = cmd.ExecuteReader())
|
|
307
|
+
{
|
|
308
|
+
if (reader.Read())
|
|
309
|
+
{
|
|
310
|
+
var item = new T();
|
|
311
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
312
|
+
foreach (var prop in properties)
|
|
313
|
+
{
|
|
314
|
+
if (HasColumn(reader, prop.Name))
|
|
315
|
+
{
|
|
316
|
+
var val = reader[prop.Name];
|
|
317
|
+
if (val != DBNull.Value)
|
|
318
|
+
{
|
|
319
|
+
prop.SetValue(item, val);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return item;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
finally
|
|
329
|
+
{
|
|
330
|
+
if (!isTx) conn.Dispose();
|
|
331
|
+
}
|
|
332
|
+
return default;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public T FindUnique(object id)
|
|
336
|
+
{
|
|
337
|
+
return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
public T Create(T entity)
|
|
341
|
+
{
|
|
342
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
343
|
+
var columns = new List<string>();
|
|
344
|
+
var values = new List<string>();
|
|
345
|
+
var sqlParams = new List<SqlParameter>();
|
|
346
|
+
|
|
347
|
+
foreach (var prop in properties)
|
|
348
|
+
{
|
|
349
|
+
var val = prop.GetValue(entity);
|
|
350
|
+
if (val != null)
|
|
351
|
+
{
|
|
352
|
+
columns.Add(prop.Name);
|
|
353
|
+
values.Add("@" + prop.Name);
|
|
354
|
+
sqlParams.Add(new SqlParameter("@" + prop.Name, val));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
string query = $"INSERT INTO {TableName} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
|
|
359
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
360
|
+
try
|
|
361
|
+
{
|
|
362
|
+
using (var cmd = CreateCommand(conn, query))
|
|
363
|
+
{
|
|
364
|
+
cmd.Parameters.AddRange(sqlParams.ToArray());
|
|
365
|
+
cmd.ExecuteNonQuery();
|
|
366
|
+
if (!isTx)
|
|
367
|
+
{
|
|
368
|
+
// Commit standard queries if not in transaction
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
finally
|
|
373
|
+
{
|
|
374
|
+
if (!isTx) conn.Dispose();
|
|
375
|
+
}
|
|
376
|
+
return entity;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
public T Update(T entity)
|
|
380
|
+
{
|
|
381
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
382
|
+
var sets = new List<string>();
|
|
383
|
+
var sqlParams = new List<SqlParameter>();
|
|
384
|
+
object idVal = null;
|
|
385
|
+
|
|
386
|
+
foreach (var prop in properties)
|
|
387
|
+
{
|
|
388
|
+
var val = prop.GetValue(entity);
|
|
389
|
+
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
390
|
+
{
|
|
391
|
+
idVal = val;
|
|
392
|
+
}
|
|
393
|
+
else if (val != null)
|
|
394
|
+
{
|
|
395
|
+
sets.Add($"{prop.Name} = @{prop.Name}");
|
|
396
|
+
sqlParams.Add(new SqlParameter("@" + prop.Name, val));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (idVal == null)
|
|
401
|
+
{
|
|
402
|
+
throw new InvalidOperationException("Cannot update entity without Id");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
sqlParams.Add(new SqlParameter("@id", idVal));
|
|
406
|
+
string query = $"UPDATE {TableName} SET {string.Join(", ", sets)} WHERE Id = @id";
|
|
407
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
408
|
+
try
|
|
409
|
+
{
|
|
410
|
+
using (var cmd = CreateCommand(conn, query))
|
|
411
|
+
{
|
|
412
|
+
cmd.Parameters.AddRange(sqlParams.ToArray());
|
|
413
|
+
cmd.ExecuteNonQuery();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
finally
|
|
417
|
+
{
|
|
418
|
+
if (!isTx) conn.Dispose();
|
|
419
|
+
}
|
|
420
|
+
return entity;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
public bool Delete(object id)
|
|
424
|
+
{
|
|
425
|
+
string query = $"DELETE FROM {TableName} WHERE Id = @id";
|
|
426
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
427
|
+
try
|
|
428
|
+
{
|
|
429
|
+
using (var cmd = CreateCommand(conn, query))
|
|
430
|
+
{
|
|
431
|
+
cmd.Parameters.AddWithValue("@id", id);
|
|
432
|
+
int affected = cmd.ExecuteNonQuery();
|
|
433
|
+
return affected > 0;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
finally
|
|
437
|
+
{
|
|
438
|
+
if (!isTx) conn.Dispose();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
|
|
443
|
+
{
|
|
444
|
+
var rows = FindMany(whereClause, parameters);
|
|
445
|
+
var results = new List<Tuple<T, double>>();
|
|
446
|
+
|
|
447
|
+
var propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance);
|
|
448
|
+
if (propInfo == null)
|
|
449
|
+
{
|
|
450
|
+
// Fallback case-insensitive
|
|
451
|
+
propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
452
|
+
}
|
|
453
|
+
if (propInfo == null) return rows;
|
|
454
|
+
|
|
455
|
+
foreach (var row in rows)
|
|
456
|
+
{
|
|
457
|
+
List<double> rowVector = null;
|
|
458
|
+
var rawVal = propInfo.GetValue(row);
|
|
459
|
+
if (rawVal != null)
|
|
460
|
+
{
|
|
461
|
+
try
|
|
462
|
+
{
|
|
463
|
+
if (rawVal is string jsonStr)
|
|
464
|
+
{
|
|
465
|
+
rowVector = ParseDoubleArray(jsonStr);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch { }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (rowVector != null && rowVector.Count == vector.Count)
|
|
472
|
+
{
|
|
473
|
+
double sim = CosineSimilarity(vector, rowVector);
|
|
474
|
+
double distance = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? (1.0 - sim) : sim;
|
|
475
|
+
results.Add(Tuple.Create(row, distance));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
|
|
480
|
+
|
|
481
|
+
var output = new List<T>();
|
|
482
|
+
int limit = Math.Min(take, results.Count);
|
|
483
|
+
for (int i = 0; i < limit; i++)
|
|
484
|
+
{
|
|
485
|
+
// Set the distance property if the model has a public property named Distance
|
|
486
|
+
var distanceProp = typeof(T).GetProperty("Distance", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
487
|
+
if (distanceProp != null && distanceProp.PropertyType == typeof(double))
|
|
488
|
+
{
|
|
489
|
+
distanceProp.SetValue(results[i].Item1, results[i].Item2);
|
|
490
|
+
}
|
|
491
|
+
output.Add(results[i].Item1);
|
|
492
|
+
}
|
|
493
|
+
return output;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private static List<double> ParseDoubleArray(string json)
|
|
497
|
+
{
|
|
498
|
+
var clean = json.Trim('[', ']');
|
|
499
|
+
if (string.IsNullOrWhiteSpace(clean)) return new List<double>();
|
|
500
|
+
|
|
501
|
+
var parts = clean.Split(',');
|
|
502
|
+
var list = new List<double>();
|
|
503
|
+
foreach (var p in parts)
|
|
504
|
+
{
|
|
505
|
+
if (double.TryParse(p.Trim(), out double d))
|
|
506
|
+
list.Add(d);
|
|
507
|
+
}
|
|
508
|
+
return list;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private static double CosineSimilarity(List<double> v1, List<double> v2)
|
|
512
|
+
{
|
|
513
|
+
double dot = 0.0, m1 = 0.0, m2 = 0.0;
|
|
514
|
+
for (int i = 0; i < v1.Count; i++)
|
|
515
|
+
{
|
|
516
|
+
dot += v1[i] * v2[i];
|
|
517
|
+
m1 += v1[i] * v1[i];
|
|
518
|
+
m2 += v2[i] * v2[i];
|
|
519
|
+
}
|
|
520
|
+
if (m1 == 0 || m2 == 0) return 0.0;
|
|
521
|
+
return dot / (Math.Sqrt(m1) * Math.Sqrt(m2));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private bool HasColumn(SqlDataReader reader, string columnName)
|
|
525
|
+
{
|
|
526
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
527
|
+
{
|
|
528
|
+
if (reader.GetName(i).Equals(columnName, StringComparison.OrdinalIgnoreCase))
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
`;
|
|
536
|
+
|
|
537
|
+
fs.writeFileSync(path.join(this.outputDir, 'An5DbContext.cs'), content);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private capitalize(str: string): string {
|
|
541
|
+
if (!str) return '';
|
|
542
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
543
|
+
}
|
|
544
|
+
}
|