@an5/orm 1.0.9 → 1.1.0
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 +10 -3
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- package/dist/generator/src/config.d.ts +162 -0
- package/dist/generator/src/config.js +354 -0
- package/dist/generator/src/dialect.d.ts +88 -0
- package/dist/generator/src/dialect.js +318 -0
- package/dist/generator/src/dotnet-generator.d.ts +8 -0
- package/dist/generator/src/dotnet-generator.js +282 -101
- package/dist/generator/src/field-types.d.ts +96 -0
- package/dist/generator/src/field-types.js +433 -0
- package/dist/generator/src/golang-generator.d.ts +36 -0
- package/dist/generator/src/golang-generator.js +1229 -0
- package/dist/generator/src/index.js +69 -36
- package/dist/generator/src/metadata-generator.js +2 -1
- package/dist/generator/src/parser.d.ts +28 -2
- package/dist/generator/src/parser.js +92 -90
- package/dist/generator/src/push-apply.d.ts +37 -0
- package/dist/generator/src/push-apply.js +109 -0
- package/dist/generator/src/push-schema.d.ts +93 -0
- package/dist/generator/src/push-schema.js +189 -0
- package/dist/generator/src/python-generator.d.ts +19 -1
- package/dist/generator/src/python-generator.js +147 -64
- package/dist/generator/src/rust-generator.d.ts +38 -0
- package/dist/generator/src/rust-generator.js +971 -0
- package/dist/generator/src/suggest.d.ts +17 -0
- package/dist/generator/src/suggest.js +45 -0
- package/dist/generator/src/type-kinds.d.ts +27 -0
- package/dist/generator/src/type-kinds.js +72 -0
- package/dist/generator/src/types.d.ts +33 -0
- package/dist/generator/src/types.js +39 -0
- package/dist/migration-core.d.ts +16 -1
- package/dist/migration-core.js +60 -21
- package/dist/orm.d.ts +147 -0
- package/dist/orm.js +1654 -0
- package/dist/provider-support.d.ts +27 -0
- package/dist/provider-support.js +50 -0
- package/generator/src/api.ts +45 -0
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +277 -88
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +89 -64
- package/generator/src/index.ts +71 -36
- package/generator/src/metadata-generator.ts +2 -2
- package/generator/src/parser.ts +122 -90
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +118 -62
- package/generator/src/rust-generator.ts +974 -0
- package/generator/src/suggest.ts +41 -0
- package/generator/src/type-kinds.ts +78 -0
- package/generator/src/types.ts +48 -0
- package/package.json +26 -6
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.DotnetGenerator = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const type_kinds_1 = require("./type-kinds");
|
|
9
11
|
class DotnetGenerator {
|
|
10
12
|
constructor(outputDir) {
|
|
11
13
|
this.outputDir = outputDir;
|
|
@@ -14,6 +16,7 @@ class DotnetGenerator {
|
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
generate(models) {
|
|
19
|
+
this.provider = models[0]?.provider;
|
|
17
20
|
// 1. Generate individual C# entity class files
|
|
18
21
|
for (const model of models) {
|
|
19
22
|
this.generateEntityClass(model);
|
|
@@ -25,17 +28,17 @@ class DotnetGenerator {
|
|
|
25
28
|
// 4. Generate configuration helper class
|
|
26
29
|
this.generateConfigClass();
|
|
27
30
|
}
|
|
28
|
-
getCsFilterType(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return 'BoolFilter';
|
|
34
|
-
|
|
35
|
-
return 'IntFilter';
|
|
36
|
-
|
|
37
|
-
return '
|
|
38
|
-
|
|
31
|
+
getCsFilterType(field, isOptional) {
|
|
32
|
+
// From the declared type: a DECIMAL column took the integer filter because it
|
|
33
|
+
// arrived as `number`.
|
|
34
|
+
switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
|
|
35
|
+
case 'date': return 'DateTimeFilter';
|
|
36
|
+
case 'bool': return 'BoolFilter';
|
|
37
|
+
case 'int':
|
|
38
|
+
case 'bigint': return 'IntFilter';
|
|
39
|
+
case 'float': return 'NumberFilter';
|
|
40
|
+
default: return 'StringFilter';
|
|
41
|
+
}
|
|
39
42
|
}
|
|
40
43
|
generateOrmTypes(models) {
|
|
41
44
|
let content = `// This file is auto-generated. Do not edit directly.
|
|
@@ -110,7 +113,7 @@ namespace An5Orm
|
|
|
110
113
|
`;
|
|
111
114
|
for (const model of models) {
|
|
112
115
|
const name = this.capitalize(model.name);
|
|
113
|
-
const csType = (f) => this.mapType(f
|
|
116
|
+
const csType = (f) => this.mapType(f, f.isOptional);
|
|
114
117
|
content += ` // ── ${name} ORM Types ──────────────────────────────────────────────────────\n\n`;
|
|
115
118
|
// WhereInput
|
|
116
119
|
content += ` /// <summary>Type-safe WHERE filter for ${name} queries.</summary>\n`;
|
|
@@ -119,7 +122,7 @@ namespace An5Orm
|
|
|
119
122
|
content += ` public List<${name}WhereInput> OR { get; set; }\n`;
|
|
120
123
|
content += ` public ${name}WhereInput NOT { get; set; }\n`;
|
|
121
124
|
for (const f of model.fields) {
|
|
122
|
-
const ft = this.getCsFilterType(f
|
|
125
|
+
const ft = this.getCsFilterType(f, f.isOptional);
|
|
123
126
|
content += ` public ${ft} ${this.capitalize(f.name)} { get; set; }\n`;
|
|
124
127
|
}
|
|
125
128
|
content += ` }\n\n`;
|
|
@@ -135,7 +138,7 @@ namespace An5Orm
|
|
|
135
138
|
content += ` public class ${name}CreateInput\n {\n`;
|
|
136
139
|
for (const f of model.fields) {
|
|
137
140
|
if (!f.isId) {
|
|
138
|
-
const ct = this.mapType(f
|
|
141
|
+
const ct = this.mapType(f, f.isOptional || f.hasDefault);
|
|
139
142
|
content += ` public ${ct} ${this.capitalize(f.name)} { get; set; }\n`;
|
|
140
143
|
}
|
|
141
144
|
}
|
|
@@ -145,7 +148,7 @@ namespace An5Orm
|
|
|
145
148
|
content += ` public class ${name}UpdateInput\n {\n`;
|
|
146
149
|
for (const f of model.fields) {
|
|
147
150
|
if (!f.isId) {
|
|
148
|
-
const ut = this.mapType(f
|
|
151
|
+
const ut = this.mapType(f, true); // always nullable for update
|
|
149
152
|
content += ` public ${ut} ${this.capitalize(f.name)} { get; set; }\n`;
|
|
150
153
|
}
|
|
151
154
|
}
|
|
@@ -199,44 +202,49 @@ namespace An5Orm
|
|
|
199
202
|
`;
|
|
200
203
|
fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'An5Config.cs'), content);
|
|
201
204
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
205
|
+
/**
|
|
206
|
+
* The C# type for a field.
|
|
207
|
+
*
|
|
208
|
+
* Driven by the declared type, so a `DECIMAL` column is a `decimal` and a `FLOAT`
|
|
209
|
+
* one a `double`: both arrived as `number` and took the `int` branch.
|
|
210
|
+
*/
|
|
211
|
+
mapType(field, isOptional) {
|
|
212
|
+
let csType;
|
|
213
|
+
switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
|
|
214
|
+
case 'int':
|
|
215
|
+
csType = 'int';
|
|
216
|
+
break;
|
|
217
|
+
case 'bigint':
|
|
218
|
+
csType = 'long';
|
|
219
|
+
break;
|
|
220
|
+
case 'float': {
|
|
221
|
+
// `DECIMAL`/`NUMERIC`/`MONEY` are exact and stay `decimal`; a floating
|
|
222
|
+
// column is a `double`. Both used to arrive as `number` and became `int`.
|
|
223
|
+
const base = (field.sqlType ?? field.type).replace(/\([^)]*\)/g, '').trim().toUpperCase();
|
|
224
|
+
csType = ['DECIMAL', 'NUMERIC', 'MONEY', 'SMALLMONEY'].includes(base) ? 'decimal' : 'double';
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'bool':
|
|
228
|
+
csType = 'bool';
|
|
229
|
+
break;
|
|
230
|
+
case 'date': {
|
|
231
|
+
const base = (field.sqlType ?? field.type).replace(/\([^)]*\)/g, '').trim().toUpperCase();
|
|
232
|
+
csType = base === 'DATETIMEOFFSET' ? 'DateTimeOffset' : base === 'TIME' ? 'TimeSpan' : 'DateTime';
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'bytes':
|
|
236
|
+
csType = 'byte[]';
|
|
237
|
+
break;
|
|
238
|
+
case 'vector':
|
|
239
|
+
csType = 'float[]';
|
|
240
|
+
break;
|
|
241
|
+
// JSON, XML, geometry and the rest stay strings: they are read as text and a
|
|
242
|
+
// Guid column is covered by the string case below via its declared name.
|
|
243
|
+
default: {
|
|
244
|
+
const base = (field.sqlType ?? field.type).replace(/\([^)]*\)/g, '').trim().toUpperCase();
|
|
245
|
+
csType = ['GUID', 'UUID', 'UNIQUEIDENTIFIER'].includes(base) ? 'Guid' : 'string';
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
240
248
|
}
|
|
241
249
|
if (isOptional && csType !== 'string' && !csType.endsWith('[]')) {
|
|
242
250
|
csType += '?';
|
|
@@ -255,7 +263,7 @@ namespace An5Orm.Entities
|
|
|
255
263
|
`;
|
|
256
264
|
// Generate fields
|
|
257
265
|
for (const field of model.fields) {
|
|
258
|
-
const csType = this.mapType(field
|
|
266
|
+
const csType = this.mapType(field, field.isOptional);
|
|
259
267
|
content += ` public ${csType} ${this.capitalize(field.name)} { get; set; }\n`;
|
|
260
268
|
}
|
|
261
269
|
// Generate relations
|
|
@@ -280,31 +288,138 @@ namespace An5Orm.Entities
|
|
|
280
288
|
using System;
|
|
281
289
|
using System.Collections.Generic;
|
|
282
290
|
using System.Data;
|
|
291
|
+
using System.Data.Common;
|
|
283
292
|
using System.Reflection;
|
|
284
293
|
using System.Text.Json;
|
|
285
|
-
using Microsoft.Data.SqlClient;
|
|
286
294
|
using An5Orm.Entities;
|
|
287
295
|
|
|
288
296
|
namespace An5Orm
|
|
289
297
|
{
|
|
298
|
+
/// <summary>SQL dialects this client can talk to.</summary>
|
|
299
|
+
public enum An5Dialect
|
|
300
|
+
{
|
|
301
|
+
Mssql,
|
|
302
|
+
Postgres,
|
|
303
|
+
Sqlite
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// <summary>
|
|
307
|
+
/// Picks the ADO.NET provider and rewrites the connection string.
|
|
308
|
+
///
|
|
309
|
+
/// Everything below works against DbConnection/DbCommand rather than the
|
|
310
|
+
/// SQL Server types, because SqlClient, Npgsql and Sqlite each derive from
|
|
311
|
+
/// those but share no other members. Only the SQL that differs per dialect
|
|
312
|
+
/// is branched on.
|
|
313
|
+
/// </summary>
|
|
314
|
+
internal static class An5Provider
|
|
315
|
+
{
|
|
316
|
+
public static An5Dialect Detect(string connectionString)
|
|
317
|
+
{
|
|
318
|
+
var cs = (connectionString ?? "").Trim().ToLowerInvariant();
|
|
319
|
+
// SQLite first: a bare path or Data Source= would otherwise fall
|
|
320
|
+
// through to SQL Server.
|
|
321
|
+
if (cs.StartsWith("sqlite:") || cs.StartsWith("file:") || cs == ":memory:"
|
|
322
|
+
|| cs.Contains("data source=") || cs.Contains("datasource=")
|
|
323
|
+
|| cs.EndsWith(".db") || cs.EndsWith(".sqlite") || cs.EndsWith(".sqlite3"))
|
|
324
|
+
return An5Dialect.Sqlite;
|
|
325
|
+
if (cs.StartsWith("postgres://") || cs.StartsWith("postgresql://") || cs.Contains("host="))
|
|
326
|
+
return An5Dialect.Postgres;
|
|
327
|
+
return An5Dialect.Mssql;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// <summary>
|
|
331
|
+
/// Rewrites the accepted SQLite spellings into the one
|
|
332
|
+
/// Microsoft.Data.Sqlite reads, which is Data Source=.
|
|
333
|
+
/// </summary>
|
|
334
|
+
public static string NormalizeSqlite(string connectionString)
|
|
335
|
+
{
|
|
336
|
+
var cs = (connectionString ?? "").Trim();
|
|
337
|
+
if (cs.Length == 0) return "Data Source=:memory:";
|
|
338
|
+
if (cs.IndexOf("Data Source", StringComparison.OrdinalIgnoreCase) >= 0
|
|
339
|
+
|| cs.IndexOf("DataSource", StringComparison.OrdinalIgnoreCase) >= 0
|
|
340
|
+
|| cs.IndexOf("Mode", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
341
|
+
return cs;
|
|
342
|
+
if (cs == ":memory:") return "Data Source=:memory:";
|
|
343
|
+
if (cs.StartsWith("sqlite://", StringComparison.OrdinalIgnoreCase))
|
|
344
|
+
return "Data Source=" + cs.Substring("sqlite://".Length);
|
|
345
|
+
if (cs.StartsWith("sqlite:", StringComparison.OrdinalIgnoreCase))
|
|
346
|
+
return "Data Source=" + cs.Substring("sqlite:".Length);
|
|
347
|
+
if (cs.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) return cs;
|
|
348
|
+
return "Data Source=" + cs;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public static DbConnection Open(string connectionString, An5Dialect dialect)
|
|
352
|
+
{
|
|
353
|
+
DbConnection conn = dialect switch
|
|
354
|
+
{
|
|
355
|
+
An5Dialect.Postgres => new Npgsql.NpgsqlConnection(connectionString),
|
|
356
|
+
An5Dialect.Sqlite => new Microsoft.Data.Sqlite.SqliteConnection(NormalizeSqlite(connectionString)),
|
|
357
|
+
_ => new Microsoft.Data.SqlClient.SqlConnection(connectionString)
|
|
358
|
+
};
|
|
359
|
+
conn.Open();
|
|
360
|
+
if (dialect == An5Dialect.Sqlite)
|
|
361
|
+
{
|
|
362
|
+
// WAL keeps readers off the writer's back, and SQLite leaves
|
|
363
|
+
// foreign keys off even when the schema declares them.
|
|
364
|
+
using var pragma = conn.CreateCommand();
|
|
365
|
+
pragma.CommandText = "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;";
|
|
366
|
+
pragma.ExecuteNonQuery();
|
|
367
|
+
}
|
|
368
|
+
return conn;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// <summary>
|
|
372
|
+
/// Adds a parameter. The name is stored without the @ prefix: Npgsql and
|
|
373
|
+
/// Sqlite treat a leading @ as part of the name, while SqlClient adds it
|
|
374
|
+
/// back, so stripping it is the only spelling all three agree on.
|
|
375
|
+
/// </summary>
|
|
376
|
+
public static DbParameter Bind(DbCommand cmd, string name, object value)
|
|
377
|
+
{
|
|
378
|
+
var p = cmd.CreateParameter();
|
|
379
|
+
p.ParameterName = (name ?? "").TrimStart('@');
|
|
380
|
+
p.Value = value ?? DBNull.Value;
|
|
381
|
+
cmd.Parameters.Add(p);
|
|
382
|
+
return p;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// <summary>Quotes an identifier for the dialect.</summary>
|
|
386
|
+
public static string Quote(string name, An5Dialect dialect)
|
|
387
|
+
{
|
|
388
|
+
if (name.StartsWith("[") || name.StartsWith("\\"")) return name;
|
|
389
|
+
if (dialect == An5Dialect.Mssql)
|
|
390
|
+
return "[" + name.Replace("]", "]]") + "]";
|
|
391
|
+
return "\\"" + name.Replace("\\"", "\\"\\"") + "\\"";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// <summary>Schema-less engines have no dbo prefix on the generated table names.</summary>
|
|
395
|
+
public static string TableName(string tableName, An5Dialect dialect)
|
|
396
|
+
{
|
|
397
|
+
if (dialect != An5Dialect.Sqlite) return tableName;
|
|
398
|
+
if (tableName.StartsWith("dbo.", StringComparison.OrdinalIgnoreCase))
|
|
399
|
+
return tableName.Substring(4);
|
|
400
|
+
return tableName;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
290
404
|
public class An5DbContext
|
|
291
405
|
{
|
|
292
406
|
public string ConnectionString { get; }
|
|
293
|
-
|
|
407
|
+
public An5Dialect Dialect { get; }
|
|
408
|
+
|
|
294
409
|
[ThreadStatic]
|
|
295
|
-
private static
|
|
410
|
+
private static DbConnection _txConn;
|
|
296
411
|
[ThreadStatic]
|
|
297
|
-
private static
|
|
412
|
+
private static DbTransaction _tx;
|
|
298
413
|
|
|
299
414
|
public An5DbContext(string connectionString = null)
|
|
300
415
|
{
|
|
301
416
|
ConnectionString = connectionString ?? An5Config.ConnectionString;
|
|
417
|
+
Dialect = An5Provider.Detect(ConnectionString);
|
|
302
418
|
}
|
|
303
419
|
|
|
304
420
|
public An5Transaction BeginTransaction()
|
|
305
421
|
{
|
|
306
|
-
var conn =
|
|
307
|
-
conn.Open();
|
|
422
|
+
var conn = An5Provider.Open(ConnectionString, Dialect);
|
|
308
423
|
var tx = conn.BeginTransaction();
|
|
309
424
|
_txConn = conn;
|
|
310
425
|
_tx = tx;
|
|
@@ -313,8 +428,8 @@ namespace An5Orm
|
|
|
313
428
|
_tx = null;
|
|
314
429
|
});
|
|
315
430
|
}
|
|
316
|
-
|
|
317
|
-
public static
|
|
431
|
+
|
|
432
|
+
public static DbConnection GetActiveConnection(string connectionString, out bool isTx)
|
|
318
433
|
{
|
|
319
434
|
if (_txConn != null)
|
|
320
435
|
{
|
|
@@ -322,30 +437,28 @@ namespace An5Orm
|
|
|
322
437
|
return _txConn;
|
|
323
438
|
}
|
|
324
439
|
isTx = false;
|
|
325
|
-
|
|
326
|
-
conn.Open();
|
|
327
|
-
return conn;
|
|
440
|
+
return An5Provider.Open(connectionString, An5Provider.Detect(connectionString));
|
|
328
441
|
}
|
|
329
|
-
|
|
330
|
-
public static
|
|
442
|
+
|
|
443
|
+
public static DbTransaction GetActiveTransaction() => _tx;
|
|
331
444
|
|
|
332
445
|
// ── Tables / Repositories ──────────────────────────────────────────────
|
|
333
446
|
`;
|
|
334
447
|
for (const model of models) {
|
|
335
448
|
const name = this.capitalize(model.name);
|
|
336
|
-
content += ` public TableClient<${model.name}> ${name}s => new TableClient<${model.name}>(ConnectionString, "${
|
|
449
|
+
content += ` public TableClient<${model.name}> ${name}s => new TableClient<${model.name}>(ConnectionString, "${(0, types_1.dottedTableName)(model)}");\n`;
|
|
337
450
|
content += ` public TableClient<${model.name}> ${name} => ${name}s;\n`;
|
|
338
451
|
}
|
|
339
452
|
content += ` }
|
|
340
453
|
|
|
341
454
|
public class An5Transaction : IDisposable
|
|
342
455
|
{
|
|
343
|
-
private readonly
|
|
344
|
-
private readonly
|
|
456
|
+
private readonly DbConnection _conn;
|
|
457
|
+
private readonly DbTransaction _tx;
|
|
345
458
|
private readonly Action _cleanup;
|
|
346
459
|
private bool _completed;
|
|
347
460
|
|
|
348
|
-
public An5Transaction(
|
|
461
|
+
public An5Transaction(DbConnection conn, DbTransaction tx, Action cleanup)
|
|
349
462
|
{
|
|
350
463
|
_conn = conn;
|
|
351
464
|
_tx = tx;
|
|
@@ -380,16 +493,21 @@ namespace An5Orm
|
|
|
380
493
|
{
|
|
381
494
|
public string ConnectionString { get; }
|
|
382
495
|
public string TableName { get; }
|
|
496
|
+
public An5Dialect Dialect { get; }
|
|
383
497
|
|
|
384
498
|
public TableClient(string connectionString, string tableName)
|
|
385
499
|
{
|
|
386
500
|
ConnectionString = connectionString;
|
|
387
|
-
|
|
501
|
+
Dialect = An5Provider.Detect(connectionString);
|
|
502
|
+
// SQLite has no schemas, so the generated dbo. prefix would be part
|
|
503
|
+
// of the table name and match nothing.
|
|
504
|
+
TableName = An5Provider.TableName(tableName, Dialect);
|
|
388
505
|
}
|
|
389
506
|
|
|
390
|
-
private
|
|
507
|
+
private DbCommand CreateCommand(DbConnection conn, string query)
|
|
391
508
|
{
|
|
392
|
-
var cmd =
|
|
509
|
+
var cmd = conn.CreateCommand();
|
|
510
|
+
cmd.CommandText = query;
|
|
393
511
|
var activeTx = An5DbContext.GetActiveTransaction();
|
|
394
512
|
if (activeTx != null)
|
|
395
513
|
{
|
|
@@ -410,7 +528,7 @@ namespace An5Orm
|
|
|
410
528
|
{
|
|
411
529
|
foreach (var kvp in parameters)
|
|
412
530
|
{
|
|
413
|
-
|
|
531
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
414
532
|
}
|
|
415
533
|
}
|
|
416
534
|
|
|
@@ -427,7 +545,7 @@ namespace An5Orm
|
|
|
427
545
|
var val = reader[prop.Name];
|
|
428
546
|
if (val != DBNull.Value)
|
|
429
547
|
{
|
|
430
|
-
|
|
548
|
+
SetValue(prop, item, val);
|
|
431
549
|
}
|
|
432
550
|
}
|
|
433
551
|
}
|
|
@@ -461,7 +579,7 @@ namespace An5Orm
|
|
|
461
579
|
{
|
|
462
580
|
foreach (var kvp in parameters)
|
|
463
581
|
{
|
|
464
|
-
|
|
582
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
465
583
|
}
|
|
466
584
|
}
|
|
467
585
|
|
|
@@ -478,7 +596,7 @@ namespace An5Orm
|
|
|
478
596
|
var val = reader[prop.Name];
|
|
479
597
|
if (val != DBNull.Value)
|
|
480
598
|
{
|
|
481
|
-
|
|
599
|
+
SetValue(prop, item, val);
|
|
482
600
|
}
|
|
483
601
|
}
|
|
484
602
|
}
|
|
@@ -496,11 +614,19 @@ namespace An5Orm
|
|
|
496
614
|
|
|
497
615
|
public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
498
616
|
{
|
|
499
|
-
|
|
617
|
+
// TOP goes before the table, LIMIT after the WHERE — appending LIMIT
|
|
618
|
+
// to the SELECT would put it ahead of the predicate.
|
|
619
|
+
string query = Dialect == An5Dialect.Mssql
|
|
620
|
+
? $"SELECT TOP 1 * FROM {TableName}"
|
|
621
|
+
: $"SELECT * FROM {TableName}";
|
|
500
622
|
if (!string.IsNullOrEmpty(whereClause))
|
|
501
623
|
{
|
|
502
624
|
query += $" WHERE {whereClause}";
|
|
503
625
|
}
|
|
626
|
+
if (Dialect != An5Dialect.Mssql)
|
|
627
|
+
{
|
|
628
|
+
query += " LIMIT 1";
|
|
629
|
+
}
|
|
504
630
|
|
|
505
631
|
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
506
632
|
try
|
|
@@ -511,7 +637,7 @@ namespace An5Orm
|
|
|
511
637
|
{
|
|
512
638
|
foreach (var kvp in parameters)
|
|
513
639
|
{
|
|
514
|
-
|
|
640
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
515
641
|
}
|
|
516
642
|
}
|
|
517
643
|
|
|
@@ -528,7 +654,7 @@ namespace An5Orm
|
|
|
528
654
|
var val = reader[prop.Name];
|
|
529
655
|
if (val != DBNull.Value)
|
|
530
656
|
{
|
|
531
|
-
|
|
657
|
+
SetValue(prop, item, val);
|
|
532
658
|
}
|
|
533
659
|
}
|
|
534
660
|
}
|
|
@@ -554,7 +680,9 @@ namespace An5Orm
|
|
|
554
680
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
555
681
|
var columns = new List<string>();
|
|
556
682
|
var values = new List<string>();
|
|
557
|
-
|
|
683
|
+
// Collected before the command exists; the provider creates the
|
|
684
|
+
// concrete parameter type, so binding has to wait for the command.
|
|
685
|
+
var sqlParams = new List<(string Name, object Value)>();
|
|
558
686
|
|
|
559
687
|
foreach (var prop in properties)
|
|
560
688
|
{
|
|
@@ -563,7 +691,7 @@ namespace An5Orm
|
|
|
563
691
|
{
|
|
564
692
|
columns.Add(prop.Name);
|
|
565
693
|
values.Add("@" + prop.Name);
|
|
566
|
-
sqlParams.Add(
|
|
694
|
+
sqlParams.Add((prop.Name, val));
|
|
567
695
|
}
|
|
568
696
|
}
|
|
569
697
|
|
|
@@ -573,7 +701,7 @@ namespace An5Orm
|
|
|
573
701
|
{
|
|
574
702
|
using (var cmd = CreateCommand(conn, query))
|
|
575
703
|
{
|
|
576
|
-
|
|
704
|
+
foreach (var (name, value) in sqlParams) An5Provider.Bind(cmd, name, value);
|
|
577
705
|
cmd.ExecuteNonQuery();
|
|
578
706
|
if (!isTx)
|
|
579
707
|
{
|
|
@@ -592,7 +720,9 @@ namespace An5Orm
|
|
|
592
720
|
{
|
|
593
721
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
594
722
|
var sets = new List<string>();
|
|
595
|
-
|
|
723
|
+
// Collected before the command exists; the provider creates the
|
|
724
|
+
// concrete parameter type, so binding has to wait for the command.
|
|
725
|
+
var sqlParams = new List<(string Name, object Value)>();
|
|
596
726
|
object idVal = null;
|
|
597
727
|
|
|
598
728
|
foreach (var prop in properties)
|
|
@@ -605,7 +735,7 @@ namespace An5Orm
|
|
|
605
735
|
else if (val != null)
|
|
606
736
|
{
|
|
607
737
|
sets.Add($"{prop.Name} = @{prop.Name}");
|
|
608
|
-
sqlParams.Add(
|
|
738
|
+
sqlParams.Add((prop.Name, val));
|
|
609
739
|
}
|
|
610
740
|
}
|
|
611
741
|
|
|
@@ -614,14 +744,14 @@ namespace An5Orm
|
|
|
614
744
|
throw new InvalidOperationException("Cannot update entity without Id");
|
|
615
745
|
}
|
|
616
746
|
|
|
617
|
-
sqlParams.Add(
|
|
747
|
+
sqlParams.Add(("id", idVal));
|
|
618
748
|
string query = $"UPDATE {TableName} SET {string.Join(", ", sets)} WHERE Id = @id";
|
|
619
749
|
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
620
750
|
try
|
|
621
751
|
{
|
|
622
752
|
using (var cmd = CreateCommand(conn, query))
|
|
623
753
|
{
|
|
624
|
-
|
|
754
|
+
foreach (var (name, value) in sqlParams) An5Provider.Bind(cmd, name, value);
|
|
625
755
|
cmd.ExecuteNonQuery();
|
|
626
756
|
}
|
|
627
757
|
}
|
|
@@ -640,7 +770,7 @@ namespace An5Orm
|
|
|
640
770
|
{
|
|
641
771
|
using (var cmd = CreateCommand(conn, query))
|
|
642
772
|
{
|
|
643
|
-
|
|
773
|
+
An5Provider.Bind(cmd, "id", id);
|
|
644
774
|
int affected = cmd.ExecuteNonQuery();
|
|
645
775
|
return affected > 0;
|
|
646
776
|
}
|
|
@@ -663,7 +793,7 @@ namespace An5Orm
|
|
|
663
793
|
if (parameters != null)
|
|
664
794
|
{
|
|
665
795
|
foreach (var kvp in parameters)
|
|
666
|
-
|
|
796
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
667
797
|
}
|
|
668
798
|
var res = cmd.ExecuteScalar();
|
|
669
799
|
return res != null && res != DBNull.Value ? Convert.ToInt32(res) : 0;
|
|
@@ -687,13 +817,15 @@ namespace An5Orm
|
|
|
687
817
|
{
|
|
688
818
|
if (updateData == null || updateData.Count == 0) return 0;
|
|
689
819
|
var sets = new List<string>();
|
|
690
|
-
|
|
820
|
+
// Bound after the command exists, so the provider can create the
|
|
821
|
+
// right parameter type; a value tuple carries (name, value) until then.
|
|
822
|
+
var sqlParams = new List<(string Name, object Value)>();
|
|
691
823
|
int pIndex = 0;
|
|
692
824
|
foreach (var kvp in updateData)
|
|
693
825
|
{
|
|
694
826
|
string paramName = "@u_" + pIndex++;
|
|
695
827
|
sets.Add($"{kvp.Key} = {paramName}");
|
|
696
|
-
sqlParams.Add(
|
|
828
|
+
sqlParams.Add((paramName, kvp.Value));
|
|
697
829
|
}
|
|
698
830
|
string query = $"UPDATE {TableName} SET {string.Join(", ", sets)}";
|
|
699
831
|
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
@@ -702,11 +834,11 @@ namespace An5Orm
|
|
|
702
834
|
{
|
|
703
835
|
using (var cmd = CreateCommand(conn, query))
|
|
704
836
|
{
|
|
705
|
-
|
|
837
|
+
foreach (var (name, value) in sqlParams) An5Provider.Bind(cmd, name, value);
|
|
706
838
|
if (parameters != null)
|
|
707
839
|
{
|
|
708
840
|
foreach (var kvp in parameters)
|
|
709
|
-
|
|
841
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
710
842
|
}
|
|
711
843
|
return cmd.ExecuteNonQuery();
|
|
712
844
|
}
|
|
@@ -726,7 +858,7 @@ namespace An5Orm
|
|
|
726
858
|
if (parameters != null)
|
|
727
859
|
{
|
|
728
860
|
foreach (var kvp in parameters)
|
|
729
|
-
|
|
861
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
730
862
|
}
|
|
731
863
|
return cmd.ExecuteNonQuery();
|
|
732
864
|
}
|
|
@@ -746,28 +878,44 @@ namespace An5Orm
|
|
|
746
878
|
|
|
747
879
|
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
880
|
{
|
|
749
|
-
// 1. Primary path:
|
|
881
|
+
// 1. Primary path: native vector query. SQL Server has VECTOR_DISTANCE
|
|
882
|
+
// and Postgres has pgvector; SQLite has no vector operator, so it is
|
|
883
|
+
// skipped outright rather than issuing SQL that can only fail and
|
|
884
|
+
// cost a round trip before falling through.
|
|
885
|
+
if (Dialect != An5Dialect.Sqlite)
|
|
750
886
|
try
|
|
751
887
|
{
|
|
752
888
|
var dim = vector.Count;
|
|
753
889
|
var vecJson = JsonSerializer.Serialize(vector);
|
|
754
|
-
var
|
|
890
|
+
var field = An5Provider.Quote(vectorField, Dialect);
|
|
891
|
+
string sql;
|
|
892
|
+
if (Dialect == An5Dialect.Postgres)
|
|
893
|
+
{
|
|
894
|
+
var op = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? "<=>"
|
|
895
|
+
: (distanceMetric.Equals("euclidean", StringComparison.OrdinalIgnoreCase) ? "<->" : "<#>");
|
|
896
|
+
sql = $"SELECT *, ({field} {op} @query_vector::vector) AS distance FROM {TableName}";
|
|
897
|
+
}
|
|
898
|
+
else
|
|
899
|
+
{
|
|
900
|
+
sql = $"SELECT TOP ({take}) *, VECTOR_DISTANCE('{distanceMetric}', CAST({field} AS VECTOR({dim}, float32)), CAST(@query_vector AS VECTOR({dim}, float32))) AS distance FROM {TableName} WITH (NOLOCK)";
|
|
901
|
+
}
|
|
755
902
|
|
|
756
903
|
var p = parameters != null ? new Dictionary<string, object>(parameters) : new Dictionary<string, object>();
|
|
757
904
|
p["query_vector"] = vecJson;
|
|
758
905
|
|
|
759
906
|
if (!string.IsNullOrWhiteSpace(whereClause))
|
|
760
|
-
sql += $" WHERE
|
|
907
|
+
sql += $" WHERE {field} IS NOT NULL AND ({whereClause})";
|
|
761
908
|
else
|
|
762
|
-
sql += $" WHERE
|
|
909
|
+
sql += $" WHERE {field} IS NOT NULL";
|
|
763
910
|
sql += " ORDER BY distance ASC";
|
|
911
|
+
if (Dialect == An5Dialect.Postgres) sql += $" LIMIT {take}";
|
|
764
912
|
|
|
765
913
|
var nativeRows = QueryRaw(sql, p);
|
|
766
914
|
if (nativeRows != null) return nativeRows;
|
|
767
915
|
}
|
|
768
916
|
catch
|
|
769
917
|
{
|
|
770
|
-
// Fallback to in-memory similarity computation if
|
|
918
|
+
// Fallback to in-memory similarity computation if the database has no native vector support
|
|
771
919
|
}
|
|
772
920
|
|
|
773
921
|
// 2. Secondary fallback: In-memory similarity computation
|
|
@@ -849,7 +997,40 @@ namespace An5Orm
|
|
|
849
997
|
return dot / (Math.Sqrt(m1) * Math.Sqrt(m2));
|
|
850
998
|
}
|
|
851
999
|
|
|
852
|
-
|
|
1000
|
+
/// <summary>
|
|
1001
|
+
/// Assigns a column to a property, converting when the provider's CLR
|
|
1002
|
+
/// type does not match the property.
|
|
1003
|
+
///
|
|
1004
|
+
/// Required across dialects, not just for SQLite: SQLite hands back
|
|
1005
|
+
/// dates as TEXT and every integer as Int64, so a DateTime or int
|
|
1006
|
+
/// property would otherwise fail to set. A value that cannot be
|
|
1007
|
+
/// converted is skipped rather than aborting the whole read.
|
|
1008
|
+
/// </summary>
|
|
1009
|
+
private static void SetValue(System.Reflection.PropertyInfo prop, object target, object value)
|
|
1010
|
+
{
|
|
1011
|
+
var wanted = prop.PropertyType;
|
|
1012
|
+
if (wanted.IsInstanceOfType(value))
|
|
1013
|
+
{
|
|
1014
|
+
prop.SetValue(target, value);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
try
|
|
1018
|
+
{
|
|
1019
|
+
var underlying = Nullable.GetUnderlyingType(wanted);
|
|
1020
|
+
if (underlying != null && value == null) { prop.SetValue(target, null); return; }
|
|
1021
|
+
if (underlying != null)
|
|
1022
|
+
{
|
|
1023
|
+
prop.SetValue(target, Convert.ChangeType(value, underlying));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (value is string text && wanted == typeof(Guid)) { prop.SetValue(target, Guid.Parse(text)); return; }
|
|
1027
|
+
if (value is string when && wanted == typeof(DateTime)) { prop.SetValue(target, DateTime.Parse(when, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind)); return; }
|
|
1028
|
+
prop.SetValue(target, Convert.ChangeType(value, wanted));
|
|
1029
|
+
}
|
|
1030
|
+
catch { }
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private bool HasColumn(DbDataReader reader, string columnName)
|
|
853
1034
|
{
|
|
854
1035
|
for (int i = 0; i < reader.FieldCount; i++)
|
|
855
1036
|
{
|