@an5/orm 1.0.8 → 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 +174 -175
- package/dist/an5Metadata.js +64 -0
- package/dist/an5Orm.d.ts +92 -3
- package/dist/an5Orm.js +697 -206
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- package/dist/generator/src/code-generator.d.ts +14 -13
- package/dist/generator/src/code-generator.js +249 -225
- 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 +21 -11
- package/dist/generator/src/dotnet-generator.js +1053 -546
- 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.d.ts +1 -1
- package/dist/generator/src/index.js +137 -88
- package/dist/generator/src/metadata-generator.d.ts +11 -8
- package/dist/generator/src/metadata-generator.js +105 -90
- package/dist/generator/src/parser.d.ts +37 -11
- package/dist/generator/src/parser.js +208 -206
- 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 +37 -9
- package/dist/generator/src/python-generator.js +428 -105
- 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 +60 -27
- package/dist/generator/src/types.js +41 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +19 -8
- package/dist/logger.js +1 -1
- package/dist/migration-core.d.ts +102 -0
- package/dist/migration-core.js +731 -0
- 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/dist/scripts/release-cli.d.ts +2 -2
- package/dist/scripts/release-cli.js +115 -115
- package/dist/sql-utils.d.ts +3 -1
- package/dist/sql-utils.js +94 -24
- package/generator/src/api.ts +45 -0
- package/generator/src/code-generator.ts +259 -230
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +1071 -544
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +1267 -0
- package/generator/src/index.ts +146 -116
- package/generator/src/metadata-generator.ts +116 -101
- package/generator/src/parser.ts +234 -202
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +480 -114
- 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 +77 -29
- package/package.json +32 -6
|
@@ -1,544 +1,1071 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { Model, Field, Relation } from './types';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
public
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
content += `
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
public
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
{
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
public
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
{
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Model, Field, Relation, dottedTableName } from './types';
|
|
4
|
+
import { fieldKind } from './type-kinds';
|
|
5
|
+
import { Provider } from './field-types';
|
|
6
|
+
|
|
7
|
+
export class DotnetGenerator {
|
|
8
|
+
/** The database being generated for; decides types like `TIMESTAMP`. */
|
|
9
|
+
private provider: Provider | undefined;
|
|
10
|
+
|
|
11
|
+
constructor(private outputDir: string) {
|
|
12
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
13
|
+
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public generate(models: Model[]) {
|
|
18
|
+
this.provider = models[0]?.provider;
|
|
19
|
+
|
|
20
|
+
// 1. Generate individual C# entity class files
|
|
21
|
+
for (const model of models) {
|
|
22
|
+
this.generateEntityClass(model);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Generate per-model ORM types (WhereInput, OrderByInput, FindManyArgs, etc.)
|
|
26
|
+
this.generateOrmTypes(models);
|
|
27
|
+
|
|
28
|
+
// 3. Generate DbContext / Client metadata file
|
|
29
|
+
this.generateDbContext(models);
|
|
30
|
+
|
|
31
|
+
// 4. Generate configuration helper class
|
|
32
|
+
this.generateConfigClass();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private getCsFilterType(field: Field, isOptional: boolean): string {
|
|
36
|
+
// From the declared type: a DECIMAL column took the integer filter because it
|
|
37
|
+
// arrived as `number`.
|
|
38
|
+
switch (fieldKind(field, this.provider)) {
|
|
39
|
+
case 'date': return 'DateTimeFilter';
|
|
40
|
+
case 'bool': return 'BoolFilter';
|
|
41
|
+
case 'int':
|
|
42
|
+
case 'bigint': return 'IntFilter';
|
|
43
|
+
case 'float': return 'NumberFilter';
|
|
44
|
+
default: return 'StringFilter';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private generateOrmTypes(models: Model[]) {
|
|
49
|
+
let content = `// This file is auto-generated. Do not edit directly.
|
|
50
|
+
using System;
|
|
51
|
+
using System.Collections.Generic;
|
|
52
|
+
|
|
53
|
+
namespace An5Orm
|
|
54
|
+
{
|
|
55
|
+
// ── Base Filter Types ──────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/// <summary>Type-safe filter for string fields.</summary>
|
|
58
|
+
public class StringFilter
|
|
59
|
+
{
|
|
60
|
+
public new string Equals { get; set; }
|
|
61
|
+
public string Not { get; set; }
|
|
62
|
+
public List<string> In { get; set; }
|
|
63
|
+
public List<string> NotIn { get; set; }
|
|
64
|
+
public string Contains { get; set; }
|
|
65
|
+
public string StartsWith { get; set; }
|
|
66
|
+
public string EndsWith { get; set; }
|
|
67
|
+
public string Gt { get; set; }
|
|
68
|
+
public string Gte { get; set; }
|
|
69
|
+
public string Lt { get; set; }
|
|
70
|
+
public string Lte { get; set; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// <summary>Type-safe filter for integer fields.</summary>
|
|
74
|
+
public class IntFilter
|
|
75
|
+
{
|
|
76
|
+
public new int? Equals { get; set; }
|
|
77
|
+
public int? Not { get; set; }
|
|
78
|
+
public List<int> In { get; set; }
|
|
79
|
+
public List<int> NotIn { get; set; }
|
|
80
|
+
public int? Gt { get; set; }
|
|
81
|
+
public int? Gte { get; set; }
|
|
82
|
+
public int? Lt { get; set; }
|
|
83
|
+
public int? Lte { get; set; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// <summary>Type-safe filter for numeric/float fields.</summary>
|
|
87
|
+
public class NumberFilter
|
|
88
|
+
{
|
|
89
|
+
public new double? Equals { get; set; }
|
|
90
|
+
public double? Not { get; set; }
|
|
91
|
+
public List<double> In { get; set; }
|
|
92
|
+
public List<double> NotIn { get; set; }
|
|
93
|
+
public double? Gt { get; set; }
|
|
94
|
+
public double? Gte { get; set; }
|
|
95
|
+
public double? Lt { get; set; }
|
|
96
|
+
public double? Lte { get; set; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// <summary>Type-safe filter for boolean fields.</summary>
|
|
100
|
+
public class BoolFilter
|
|
101
|
+
{
|
|
102
|
+
public new bool? Equals { get; set; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// <summary>Type-safe filter for DateTime fields.</summary>
|
|
106
|
+
public class DateTimeFilter
|
|
107
|
+
{
|
|
108
|
+
public new DateTime? Equals { get; set; }
|
|
109
|
+
public DateTime? Not { get; set; }
|
|
110
|
+
public List<DateTime> In { get; set; }
|
|
111
|
+
public List<DateTime> NotIn { get; set; }
|
|
112
|
+
public DateTime? Gt { get; set; }
|
|
113
|
+
public DateTime? Gte { get; set; }
|
|
114
|
+
public DateTime? Lt { get; set; }
|
|
115
|
+
public DateTime? Lte { get; set; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
for (const model of models) {
|
|
121
|
+
const name = this.capitalize(model.name);
|
|
122
|
+
const csType = (f: Field) => this.mapType(f, f.isOptional);
|
|
123
|
+
|
|
124
|
+
content += ` // ── ${name} ORM Types ──────────────────────────────────────────────────────\n\n`;
|
|
125
|
+
|
|
126
|
+
// WhereInput
|
|
127
|
+
content += ` /// <summary>Type-safe WHERE filter for ${name} queries.</summary>\n`;
|
|
128
|
+
content += ` public class ${name}WhereInput\n {\n`;
|
|
129
|
+
content += ` public List<${name}WhereInput> AND { get; set; }\n`;
|
|
130
|
+
content += ` public List<${name}WhereInput> OR { get; set; }\n`;
|
|
131
|
+
content += ` public ${name}WhereInput NOT { get; set; }\n`;
|
|
132
|
+
for (const f of model.fields) {
|
|
133
|
+
const ft = this.getCsFilterType(f, f.isOptional);
|
|
134
|
+
content += ` public ${ft} ${this.capitalize(f.name)} { get; set; }\n`;
|
|
135
|
+
}
|
|
136
|
+
content += ` }\n\n`;
|
|
137
|
+
|
|
138
|
+
// OrderByInput
|
|
139
|
+
content += ` /// <summary>Type-safe ORDER BY for ${name} queries.</summary>\n`;
|
|
140
|
+
content += ` public class ${name}OrderByInput\n {\n`;
|
|
141
|
+
for (const f of model.fields) {
|
|
142
|
+
content += ` public string ${this.capitalize(f.name)} { get; set; } // "asc" or "desc"\n`;
|
|
143
|
+
}
|
|
144
|
+
content += ` }\n\n`;
|
|
145
|
+
|
|
146
|
+
// CreateInput
|
|
147
|
+
content += ` /// <summary>Typed data for creating a new ${name} record.</summary>\n`;
|
|
148
|
+
content += ` public class ${name}CreateInput\n {\n`;
|
|
149
|
+
for (const f of model.fields) {
|
|
150
|
+
if (!f.isId) {
|
|
151
|
+
const ct = this.mapType(f, f.isOptional || f.hasDefault);
|
|
152
|
+
content += ` public ${ct} ${this.capitalize(f.name)} { get; set; }\n`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
content += ` }\n\n`;
|
|
156
|
+
|
|
157
|
+
// UpdateInput
|
|
158
|
+
content += ` /// <summary>Typed data for updating an existing ${name} record.</summary>\n`;
|
|
159
|
+
content += ` public class ${name}UpdateInput\n {\n`;
|
|
160
|
+
for (const f of model.fields) {
|
|
161
|
+
if (!f.isId) {
|
|
162
|
+
const ut = this.mapType(f, true); // always nullable for update
|
|
163
|
+
content += ` public ${ut} ${this.capitalize(f.name)} { get; set; }\n`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
content += ` }\n\n`;
|
|
167
|
+
|
|
168
|
+
// FindManyArgs
|
|
169
|
+
content += ` /// <summary>ORM-style args for ${name}.FindMany().</summary>\n`;
|
|
170
|
+
content += ` public class ${name}FindManyArgs\n {\n`;
|
|
171
|
+
content += ` public ${name}WhereInput Where { get; set; }\n`;
|
|
172
|
+
content += ` public ${name}OrderByInput OrderBy { get; set; }\n`;
|
|
173
|
+
content += ` public int? Take { get; set; }\n`;
|
|
174
|
+
content += ` public int? Skip { get; set; }\n`;
|
|
175
|
+
content += ` public List<string> Select { get; set; }\n`;
|
|
176
|
+
content += ` }\n\n`;
|
|
177
|
+
|
|
178
|
+
// FindFirstArgs
|
|
179
|
+
content += ` /// <summary>ORM-style args for ${name}.FindFirst().</summary>\n`;
|
|
180
|
+
content += ` public class ${name}FindFirstArgs\n {\n`;
|
|
181
|
+
content += ` public ${name}WhereInput Where { get; set; }\n`;
|
|
182
|
+
content += ` public ${name}OrderByInput OrderBy { get; set; }\n`;
|
|
183
|
+
content += ` public List<string> Select { get; set; }\n`;
|
|
184
|
+
content += ` }\n\n`;
|
|
185
|
+
|
|
186
|
+
// FindUniqueArgs
|
|
187
|
+
content += ` /// <summary>ORM-style args for ${name}.FindUnique().</summary>\n`;
|
|
188
|
+
content += ` public class ${name}FindUniqueArgs\n {\n`;
|
|
189
|
+
content += ` public ${name}WhereInput Where { get; set; }\n`;
|
|
190
|
+
content += ` }\n\n`;
|
|
191
|
+
|
|
192
|
+
// DeleteManyArgs
|
|
193
|
+
content += ` /// <summary>ORM-style args for ${name}.DeleteMany().</summary>\n`;
|
|
194
|
+
content += ` public class ${name}DeleteManyArgs\n {\n`;
|
|
195
|
+
content += ` public ${name}WhereInput Where { get; set; }\n`;
|
|
196
|
+
content += ` }\n\n`;
|
|
197
|
+
|
|
198
|
+
// CountArgs
|
|
199
|
+
content += ` /// <summary>ORM-style args for ${name}.Count().</summary>\n`;
|
|
200
|
+
content += ` public class ${name}CountArgs\n {\n`;
|
|
201
|
+
content += ` public ${name}WhereInput Where { get; set; }\n`;
|
|
202
|
+
content += ` }\n\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
content += `}\n`;
|
|
206
|
+
|
|
207
|
+
fs.writeFileSync(path.join(this.outputDir, 'An5OrmTypes.cs'), content);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private generateConfigClass() {
|
|
211
|
+
const content = `// This file is auto-generated. Do not edit directly.
|
|
212
|
+
using System;
|
|
213
|
+
|
|
214
|
+
namespace An5Orm
|
|
215
|
+
{
|
|
216
|
+
public static class An5Config
|
|
217
|
+
{
|
|
218
|
+
public static string ConnectionString { get; set; } = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
fs.writeFileSync(path.join(this.outputDir, 'An5Config.cs'), content);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The C# type for a field.
|
|
227
|
+
*
|
|
228
|
+
* Driven by the declared type, so a `DECIMAL` column is a `decimal` and a `FLOAT`
|
|
229
|
+
* one a `double`: both arrived as `number` and took the `int` branch.
|
|
230
|
+
*/
|
|
231
|
+
private mapType(field: Field, isOptional: boolean): string {
|
|
232
|
+
let csType: string;
|
|
233
|
+
switch (fieldKind(field, this.provider)) {
|
|
234
|
+
case 'int': csType = 'int'; break;
|
|
235
|
+
case 'bigint': csType = 'long'; break;
|
|
236
|
+
case 'float': {
|
|
237
|
+
// `DECIMAL`/`NUMERIC`/`MONEY` are exact and stay `decimal`; a floating
|
|
238
|
+
// column is a `double`. Both used to arrive as `number` and became `int`.
|
|
239
|
+
const base = (field.sqlType ?? field.type).replace(/\([^)]*\)/g, '').trim().toUpperCase();
|
|
240
|
+
csType = ['DECIMAL', 'NUMERIC', 'MONEY', 'SMALLMONEY'].includes(base) ? 'decimal' : 'double';
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'bool': csType = 'bool'; break;
|
|
244
|
+
case 'date': {
|
|
245
|
+
const base = (field.sqlType ?? field.type).replace(/\([^)]*\)/g, '').trim().toUpperCase();
|
|
246
|
+
csType = base === 'DATETIMEOFFSET' ? 'DateTimeOffset' : base === 'TIME' ? 'TimeSpan' : 'DateTime';
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case 'bytes': csType = 'byte[]'; break;
|
|
250
|
+
case 'vector': csType = 'float[]'; break;
|
|
251
|
+
// JSON, XML, geometry and the rest stay strings: they are read as text and a
|
|
252
|
+
// Guid column is covered by the string case below via its declared name.
|
|
253
|
+
default: {
|
|
254
|
+
const base = (field.sqlType ?? field.type).replace(/\([^)]*\)/g, '').trim().toUpperCase();
|
|
255
|
+
csType = ['GUID', 'UUID', 'UNIQUEIDENTIFIER'].includes(base) ? 'Guid' : 'string';
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (isOptional && csType !== 'string' && !csType.endsWith('[]')) {
|
|
261
|
+
csType += '?';
|
|
262
|
+
}
|
|
263
|
+
return csType;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private generateEntityClass(model: Model) {
|
|
267
|
+
let content = `// This file is auto-generated. Do not edit directly.
|
|
268
|
+
using System;
|
|
269
|
+
using System.Collections.Generic;
|
|
270
|
+
|
|
271
|
+
namespace An5Orm.Entities
|
|
272
|
+
{
|
|
273
|
+
public class ${model.name}
|
|
274
|
+
{
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
// Generate fields
|
|
278
|
+
for (const field of model.fields) {
|
|
279
|
+
const csType = this.mapType(field, field.isOptional);
|
|
280
|
+
content += ` public ${csType} ${this.capitalize(field.name)} { get; set; }\n`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Generate relations
|
|
284
|
+
if (model.relations.length > 0) {
|
|
285
|
+
content += '\n // ── Relations ────────────────────────────────────────────────────────\n';
|
|
286
|
+
for (const rel of model.relations) {
|
|
287
|
+
if (rel.isArray) {
|
|
288
|
+
content += ` public List<${rel.type}> ${this.capitalize(rel.name)} { get; set; } = new List<${rel.type}>();\n`;
|
|
289
|
+
} else {
|
|
290
|
+
content += ` public ${rel.type} ${this.capitalize(rel.name)} { get; set; }\n`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
content += ` }
|
|
296
|
+
}
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
fs.writeFileSync(path.join(this.outputDir, `${model.name}.cs`), content);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private generateDbContext(models: Model[]) {
|
|
303
|
+
let content = `// This file is auto-generated. Do not edit directly.
|
|
304
|
+
using System;
|
|
305
|
+
using System.Collections.Generic;
|
|
306
|
+
using System.Data;
|
|
307
|
+
using System.Data.Common;
|
|
308
|
+
using System.Reflection;
|
|
309
|
+
using System.Text.Json;
|
|
310
|
+
using An5Orm.Entities;
|
|
311
|
+
|
|
312
|
+
namespace An5Orm
|
|
313
|
+
{
|
|
314
|
+
/// <summary>SQL dialects this client can talk to.</summary>
|
|
315
|
+
public enum An5Dialect
|
|
316
|
+
{
|
|
317
|
+
Mssql,
|
|
318
|
+
Postgres,
|
|
319
|
+
Sqlite
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// <summary>
|
|
323
|
+
/// Picks the ADO.NET provider and rewrites the connection string.
|
|
324
|
+
///
|
|
325
|
+
/// Everything below works against DbConnection/DbCommand rather than the
|
|
326
|
+
/// SQL Server types, because SqlClient, Npgsql and Sqlite each derive from
|
|
327
|
+
/// those but share no other members. Only the SQL that differs per dialect
|
|
328
|
+
/// is branched on.
|
|
329
|
+
/// </summary>
|
|
330
|
+
internal static class An5Provider
|
|
331
|
+
{
|
|
332
|
+
public static An5Dialect Detect(string connectionString)
|
|
333
|
+
{
|
|
334
|
+
var cs = (connectionString ?? "").Trim().ToLowerInvariant();
|
|
335
|
+
// SQLite first: a bare path or Data Source= would otherwise fall
|
|
336
|
+
// through to SQL Server.
|
|
337
|
+
if (cs.StartsWith("sqlite:") || cs.StartsWith("file:") || cs == ":memory:"
|
|
338
|
+
|| cs.Contains("data source=") || cs.Contains("datasource=")
|
|
339
|
+
|| cs.EndsWith(".db") || cs.EndsWith(".sqlite") || cs.EndsWith(".sqlite3"))
|
|
340
|
+
return An5Dialect.Sqlite;
|
|
341
|
+
if (cs.StartsWith("postgres://") || cs.StartsWith("postgresql://") || cs.Contains("host="))
|
|
342
|
+
return An5Dialect.Postgres;
|
|
343
|
+
return An5Dialect.Mssql;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// <summary>
|
|
347
|
+
/// Rewrites the accepted SQLite spellings into the one
|
|
348
|
+
/// Microsoft.Data.Sqlite reads, which is Data Source=.
|
|
349
|
+
/// </summary>
|
|
350
|
+
public static string NormalizeSqlite(string connectionString)
|
|
351
|
+
{
|
|
352
|
+
var cs = (connectionString ?? "").Trim();
|
|
353
|
+
if (cs.Length == 0) return "Data Source=:memory:";
|
|
354
|
+
if (cs.IndexOf("Data Source", StringComparison.OrdinalIgnoreCase) >= 0
|
|
355
|
+
|| cs.IndexOf("DataSource", StringComparison.OrdinalIgnoreCase) >= 0
|
|
356
|
+
|| cs.IndexOf("Mode", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
357
|
+
return cs;
|
|
358
|
+
if (cs == ":memory:") return "Data Source=:memory:";
|
|
359
|
+
if (cs.StartsWith("sqlite://", StringComparison.OrdinalIgnoreCase))
|
|
360
|
+
return "Data Source=" + cs.Substring("sqlite://".Length);
|
|
361
|
+
if (cs.StartsWith("sqlite:", StringComparison.OrdinalIgnoreCase))
|
|
362
|
+
return "Data Source=" + cs.Substring("sqlite:".Length);
|
|
363
|
+
if (cs.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) return cs;
|
|
364
|
+
return "Data Source=" + cs;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public static DbConnection Open(string connectionString, An5Dialect dialect)
|
|
368
|
+
{
|
|
369
|
+
DbConnection conn = dialect switch
|
|
370
|
+
{
|
|
371
|
+
An5Dialect.Postgres => new Npgsql.NpgsqlConnection(connectionString),
|
|
372
|
+
An5Dialect.Sqlite => new Microsoft.Data.Sqlite.SqliteConnection(NormalizeSqlite(connectionString)),
|
|
373
|
+
_ => new Microsoft.Data.SqlClient.SqlConnection(connectionString)
|
|
374
|
+
};
|
|
375
|
+
conn.Open();
|
|
376
|
+
if (dialect == An5Dialect.Sqlite)
|
|
377
|
+
{
|
|
378
|
+
// WAL keeps readers off the writer's back, and SQLite leaves
|
|
379
|
+
// foreign keys off even when the schema declares them.
|
|
380
|
+
using var pragma = conn.CreateCommand();
|
|
381
|
+
pragma.CommandText = "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;";
|
|
382
|
+
pragma.ExecuteNonQuery();
|
|
383
|
+
}
|
|
384
|
+
return conn;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// <summary>
|
|
388
|
+
/// Adds a parameter. The name is stored without the @ prefix: Npgsql and
|
|
389
|
+
/// Sqlite treat a leading @ as part of the name, while SqlClient adds it
|
|
390
|
+
/// back, so stripping it is the only spelling all three agree on.
|
|
391
|
+
/// </summary>
|
|
392
|
+
public static DbParameter Bind(DbCommand cmd, string name, object value)
|
|
393
|
+
{
|
|
394
|
+
var p = cmd.CreateParameter();
|
|
395
|
+
p.ParameterName = (name ?? "").TrimStart('@');
|
|
396
|
+
p.Value = value ?? DBNull.Value;
|
|
397
|
+
cmd.Parameters.Add(p);
|
|
398
|
+
return p;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// <summary>Quotes an identifier for the dialect.</summary>
|
|
402
|
+
public static string Quote(string name, An5Dialect dialect)
|
|
403
|
+
{
|
|
404
|
+
if (name.StartsWith("[") || name.StartsWith("\\"")) return name;
|
|
405
|
+
if (dialect == An5Dialect.Mssql)
|
|
406
|
+
return "[" + name.Replace("]", "]]") + "]";
|
|
407
|
+
return "\\"" + name.Replace("\\"", "\\"\\"") + "\\"";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// <summary>Schema-less engines have no dbo prefix on the generated table names.</summary>
|
|
411
|
+
public static string TableName(string tableName, An5Dialect dialect)
|
|
412
|
+
{
|
|
413
|
+
if (dialect != An5Dialect.Sqlite) return tableName;
|
|
414
|
+
if (tableName.StartsWith("dbo.", StringComparison.OrdinalIgnoreCase))
|
|
415
|
+
return tableName.Substring(4);
|
|
416
|
+
return tableName;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public class An5DbContext
|
|
421
|
+
{
|
|
422
|
+
public string ConnectionString { get; }
|
|
423
|
+
public An5Dialect Dialect { get; }
|
|
424
|
+
|
|
425
|
+
[ThreadStatic]
|
|
426
|
+
private static DbConnection _txConn;
|
|
427
|
+
[ThreadStatic]
|
|
428
|
+
private static DbTransaction _tx;
|
|
429
|
+
|
|
430
|
+
public An5DbContext(string connectionString = null)
|
|
431
|
+
{
|
|
432
|
+
ConnectionString = connectionString ?? An5Config.ConnectionString;
|
|
433
|
+
Dialect = An5Provider.Detect(ConnectionString);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
public An5Transaction BeginTransaction()
|
|
437
|
+
{
|
|
438
|
+
var conn = An5Provider.Open(ConnectionString, Dialect);
|
|
439
|
+
var tx = conn.BeginTransaction();
|
|
440
|
+
_txConn = conn;
|
|
441
|
+
_tx = tx;
|
|
442
|
+
return new An5Transaction(conn, tx, () => {
|
|
443
|
+
_txConn = null;
|
|
444
|
+
_tx = null;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
public static DbConnection GetActiveConnection(string connectionString, out bool isTx)
|
|
449
|
+
{
|
|
450
|
+
if (_txConn != null)
|
|
451
|
+
{
|
|
452
|
+
isTx = true;
|
|
453
|
+
return _txConn;
|
|
454
|
+
}
|
|
455
|
+
isTx = false;
|
|
456
|
+
return An5Provider.Open(connectionString, An5Provider.Detect(connectionString));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public static DbTransaction GetActiveTransaction() => _tx;
|
|
460
|
+
|
|
461
|
+
// ── Tables / Repositories ──────────────────────────────────────────────
|
|
462
|
+
`;
|
|
463
|
+
|
|
464
|
+
for (const model of models) {
|
|
465
|
+
const name = this.capitalize(model.name);
|
|
466
|
+
content += ` public TableClient<${model.name}> ${name}s => new TableClient<${model.name}>(ConnectionString, "${dottedTableName(model)}");\n`;
|
|
467
|
+
content += ` public TableClient<${model.name}> ${name} => ${name}s;\n`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
content += ` }
|
|
471
|
+
|
|
472
|
+
public class An5Transaction : IDisposable
|
|
473
|
+
{
|
|
474
|
+
private readonly DbConnection _conn;
|
|
475
|
+
private readonly DbTransaction _tx;
|
|
476
|
+
private readonly Action _cleanup;
|
|
477
|
+
private bool _completed;
|
|
478
|
+
|
|
479
|
+
public An5Transaction(DbConnection conn, DbTransaction tx, Action cleanup)
|
|
480
|
+
{
|
|
481
|
+
_conn = conn;
|
|
482
|
+
_tx = tx;
|
|
483
|
+
_cleanup = cleanup;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
public void Commit()
|
|
487
|
+
{
|
|
488
|
+
_tx.Commit();
|
|
489
|
+
_completed = true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public void Rollback()
|
|
493
|
+
{
|
|
494
|
+
_tx.Rollback();
|
|
495
|
+
_completed = true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
public void Dispose()
|
|
499
|
+
{
|
|
500
|
+
if (!_completed)
|
|
501
|
+
{
|
|
502
|
+
try { _tx.Rollback(); } catch { }
|
|
503
|
+
}
|
|
504
|
+
_tx.Dispose();
|
|
505
|
+
_conn.Dispose();
|
|
506
|
+
_cleanup();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
public class TableClient<T> where T : new()
|
|
511
|
+
{
|
|
512
|
+
public string ConnectionString { get; }
|
|
513
|
+
public string TableName { get; }
|
|
514
|
+
public An5Dialect Dialect { get; }
|
|
515
|
+
|
|
516
|
+
public TableClient(string connectionString, string tableName)
|
|
517
|
+
{
|
|
518
|
+
ConnectionString = connectionString;
|
|
519
|
+
Dialect = An5Provider.Detect(connectionString);
|
|
520
|
+
// SQLite has no schemas, so the generated dbo. prefix would be part
|
|
521
|
+
// of the table name and match nothing.
|
|
522
|
+
TableName = An5Provider.TableName(tableName, Dialect);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private DbCommand CreateCommand(DbConnection conn, string query)
|
|
526
|
+
{
|
|
527
|
+
var cmd = conn.CreateCommand();
|
|
528
|
+
cmd.CommandText = query;
|
|
529
|
+
var activeTx = An5DbContext.GetActiveTransaction();
|
|
530
|
+
if (activeTx != null)
|
|
531
|
+
{
|
|
532
|
+
cmd.Transaction = activeTx;
|
|
533
|
+
}
|
|
534
|
+
return cmd;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
public List<T> QueryRaw(string query, Dictionary<string, object> parameters = null)
|
|
538
|
+
{
|
|
539
|
+
var list = new List<T>();
|
|
540
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
541
|
+
try
|
|
542
|
+
{
|
|
543
|
+
using (var cmd = CreateCommand(conn, query))
|
|
544
|
+
{
|
|
545
|
+
if (parameters != null)
|
|
546
|
+
{
|
|
547
|
+
foreach (var kvp in parameters)
|
|
548
|
+
{
|
|
549
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
using (var reader = cmd.ExecuteReader())
|
|
554
|
+
{
|
|
555
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
556
|
+
while (reader.Read())
|
|
557
|
+
{
|
|
558
|
+
var item = new T();
|
|
559
|
+
foreach (var prop in properties)
|
|
560
|
+
{
|
|
561
|
+
if (HasColumn(reader, prop.Name))
|
|
562
|
+
{
|
|
563
|
+
var val = reader[prop.Name];
|
|
564
|
+
if (val != DBNull.Value)
|
|
565
|
+
{
|
|
566
|
+
SetValue(prop, item, val);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
list.Add(item);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
finally
|
|
576
|
+
{
|
|
577
|
+
if (!isTx) conn.Dispose();
|
|
578
|
+
}
|
|
579
|
+
return list;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
583
|
+
{
|
|
584
|
+
var list = new List<T>();
|
|
585
|
+
string query = $"SELECT * FROM {TableName}";
|
|
586
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
587
|
+
{
|
|
588
|
+
query += $" WHERE {whereClause}";
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
592
|
+
try
|
|
593
|
+
{
|
|
594
|
+
using (var cmd = CreateCommand(conn, query))
|
|
595
|
+
{
|
|
596
|
+
if (parameters != null)
|
|
597
|
+
{
|
|
598
|
+
foreach (var kvp in parameters)
|
|
599
|
+
{
|
|
600
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
using (var reader = cmd.ExecuteReader())
|
|
605
|
+
{
|
|
606
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
607
|
+
while (reader.Read())
|
|
608
|
+
{
|
|
609
|
+
var item = new T();
|
|
610
|
+
foreach (var prop in properties)
|
|
611
|
+
{
|
|
612
|
+
if (HasColumn(reader, prop.Name))
|
|
613
|
+
{
|
|
614
|
+
var val = reader[prop.Name];
|
|
615
|
+
if (val != DBNull.Value)
|
|
616
|
+
{
|
|
617
|
+
SetValue(prop, item, val);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
list.Add(item);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
finally
|
|
627
|
+
{
|
|
628
|
+
if (!isTx) conn.Dispose();
|
|
629
|
+
}
|
|
630
|
+
return list;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
634
|
+
{
|
|
635
|
+
// TOP goes before the table, LIMIT after the WHERE — appending LIMIT
|
|
636
|
+
// to the SELECT would put it ahead of the predicate.
|
|
637
|
+
string query = Dialect == An5Dialect.Mssql
|
|
638
|
+
? $"SELECT TOP 1 * FROM {TableName}"
|
|
639
|
+
: $"SELECT * FROM {TableName}";
|
|
640
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
641
|
+
{
|
|
642
|
+
query += $" WHERE {whereClause}";
|
|
643
|
+
}
|
|
644
|
+
if (Dialect != An5Dialect.Mssql)
|
|
645
|
+
{
|
|
646
|
+
query += " LIMIT 1";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
650
|
+
try
|
|
651
|
+
{
|
|
652
|
+
using (var cmd = CreateCommand(conn, query))
|
|
653
|
+
{
|
|
654
|
+
if (parameters != null)
|
|
655
|
+
{
|
|
656
|
+
foreach (var kvp in parameters)
|
|
657
|
+
{
|
|
658
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
using (var reader = cmd.ExecuteReader())
|
|
663
|
+
{
|
|
664
|
+
if (reader.Read())
|
|
665
|
+
{
|
|
666
|
+
var item = new T();
|
|
667
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
668
|
+
foreach (var prop in properties)
|
|
669
|
+
{
|
|
670
|
+
if (HasColumn(reader, prop.Name))
|
|
671
|
+
{
|
|
672
|
+
var val = reader[prop.Name];
|
|
673
|
+
if (val != DBNull.Value)
|
|
674
|
+
{
|
|
675
|
+
SetValue(prop, item, val);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return item;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
finally
|
|
685
|
+
{
|
|
686
|
+
if (!isTx) conn.Dispose();
|
|
687
|
+
}
|
|
688
|
+
return default;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
public T FindUnique(object id)
|
|
692
|
+
{
|
|
693
|
+
return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
public T Create(T entity)
|
|
697
|
+
{
|
|
698
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
699
|
+
var columns = new List<string>();
|
|
700
|
+
var values = new List<string>();
|
|
701
|
+
// Collected before the command exists; the provider creates the
|
|
702
|
+
// concrete parameter type, so binding has to wait for the command.
|
|
703
|
+
var sqlParams = new List<(string Name, object Value)>();
|
|
704
|
+
|
|
705
|
+
foreach (var prop in properties)
|
|
706
|
+
{
|
|
707
|
+
var val = prop.GetValue(entity);
|
|
708
|
+
if (val != null)
|
|
709
|
+
{
|
|
710
|
+
columns.Add(prop.Name);
|
|
711
|
+
values.Add("@" + prop.Name);
|
|
712
|
+
sqlParams.Add((prop.Name, val));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
string query = $"INSERT INTO {TableName} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
|
|
717
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
718
|
+
try
|
|
719
|
+
{
|
|
720
|
+
using (var cmd = CreateCommand(conn, query))
|
|
721
|
+
{
|
|
722
|
+
foreach (var (name, value) in sqlParams) An5Provider.Bind(cmd, name, value);
|
|
723
|
+
cmd.ExecuteNonQuery();
|
|
724
|
+
if (!isTx)
|
|
725
|
+
{
|
|
726
|
+
// Commit standard queries if not in transaction
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
finally
|
|
731
|
+
{
|
|
732
|
+
if (!isTx) conn.Dispose();
|
|
733
|
+
}
|
|
734
|
+
return entity;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
public T Update(T entity)
|
|
738
|
+
{
|
|
739
|
+
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
740
|
+
var sets = new List<string>();
|
|
741
|
+
// Collected before the command exists; the provider creates the
|
|
742
|
+
// concrete parameter type, so binding has to wait for the command.
|
|
743
|
+
var sqlParams = new List<(string Name, object Value)>();
|
|
744
|
+
object idVal = null;
|
|
745
|
+
|
|
746
|
+
foreach (var prop in properties)
|
|
747
|
+
{
|
|
748
|
+
var val = prop.GetValue(entity);
|
|
749
|
+
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
750
|
+
{
|
|
751
|
+
idVal = val;
|
|
752
|
+
}
|
|
753
|
+
else if (val != null)
|
|
754
|
+
{
|
|
755
|
+
sets.Add($"{prop.Name} = @{prop.Name}");
|
|
756
|
+
sqlParams.Add((prop.Name, val));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (idVal == null)
|
|
761
|
+
{
|
|
762
|
+
throw new InvalidOperationException("Cannot update entity without Id");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
sqlParams.Add(("id", idVal));
|
|
766
|
+
string query = $"UPDATE {TableName} SET {string.Join(", ", sets)} WHERE Id = @id";
|
|
767
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
768
|
+
try
|
|
769
|
+
{
|
|
770
|
+
using (var cmd = CreateCommand(conn, query))
|
|
771
|
+
{
|
|
772
|
+
foreach (var (name, value) in sqlParams) An5Provider.Bind(cmd, name, value);
|
|
773
|
+
cmd.ExecuteNonQuery();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
finally
|
|
777
|
+
{
|
|
778
|
+
if (!isTx) conn.Dispose();
|
|
779
|
+
}
|
|
780
|
+
return entity;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
public bool Delete(object id)
|
|
784
|
+
{
|
|
785
|
+
string query = $"DELETE FROM {TableName} WHERE Id = @id";
|
|
786
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
787
|
+
try
|
|
788
|
+
{
|
|
789
|
+
using (var cmd = CreateCommand(conn, query))
|
|
790
|
+
{
|
|
791
|
+
An5Provider.Bind(cmd, "id", id);
|
|
792
|
+
int affected = cmd.ExecuteNonQuery();
|
|
793
|
+
return affected > 0;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
finally
|
|
797
|
+
{
|
|
798
|
+
if (!isTx) conn.Dispose();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
public int Count(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
803
|
+
{
|
|
804
|
+
string query = $"SELECT COUNT(*) FROM {TableName}";
|
|
805
|
+
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
806
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
807
|
+
try
|
|
808
|
+
{
|
|
809
|
+
using (var cmd = CreateCommand(conn, query))
|
|
810
|
+
{
|
|
811
|
+
if (parameters != null)
|
|
812
|
+
{
|
|
813
|
+
foreach (var kvp in parameters)
|
|
814
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
815
|
+
}
|
|
816
|
+
var res = cmd.ExecuteScalar();
|
|
817
|
+
return res != null && res != DBNull.Value ? Convert.ToInt32(res) : 0;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
public int CreateMany(IEnumerable<T> entities)
|
|
824
|
+
{
|
|
825
|
+
int count = 0;
|
|
826
|
+
foreach (var entity in entities)
|
|
827
|
+
{
|
|
828
|
+
Create(entity);
|
|
829
|
+
count++;
|
|
830
|
+
}
|
|
831
|
+
return count;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
public int UpdateMany(string whereClause, Dictionary<string, object> updateData, Dictionary<string, object> parameters = null)
|
|
835
|
+
{
|
|
836
|
+
if (updateData == null || updateData.Count == 0) return 0;
|
|
837
|
+
var sets = new List<string>();
|
|
838
|
+
// Bound after the command exists, so the provider can create the
|
|
839
|
+
// right parameter type; a value tuple carries (name, value) until then.
|
|
840
|
+
var sqlParams = new List<(string Name, object Value)>();
|
|
841
|
+
int pIndex = 0;
|
|
842
|
+
foreach (var kvp in updateData)
|
|
843
|
+
{
|
|
844
|
+
string paramName = "@u_" + pIndex++;
|
|
845
|
+
sets.Add($"{kvp.Key} = {paramName}");
|
|
846
|
+
sqlParams.Add((paramName, kvp.Value));
|
|
847
|
+
}
|
|
848
|
+
string query = $"UPDATE {TableName} SET {string.Join(", ", sets)}";
|
|
849
|
+
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
850
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
851
|
+
try
|
|
852
|
+
{
|
|
853
|
+
using (var cmd = CreateCommand(conn, query))
|
|
854
|
+
{
|
|
855
|
+
foreach (var (name, value) in sqlParams) An5Provider.Bind(cmd, name, value);
|
|
856
|
+
if (parameters != null)
|
|
857
|
+
{
|
|
858
|
+
foreach (var kvp in parameters)
|
|
859
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
860
|
+
}
|
|
861
|
+
return cmd.ExecuteNonQuery();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
public int DeleteMany(string whereClause = null, Dictionary<string, object> parameters = null)
|
|
868
|
+
{
|
|
869
|
+
string query = $"DELETE FROM {TableName}";
|
|
870
|
+
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
871
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
872
|
+
try
|
|
873
|
+
{
|
|
874
|
+
using (var cmd = CreateCommand(conn, query))
|
|
875
|
+
{
|
|
876
|
+
if (parameters != null)
|
|
877
|
+
{
|
|
878
|
+
foreach (var kvp in parameters)
|
|
879
|
+
An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
880
|
+
}
|
|
881
|
+
return cmd.ExecuteNonQuery();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
public T Upsert(T entity, string idColumnName = "Id")
|
|
888
|
+
{
|
|
889
|
+
var prop = typeof(T).GetProperty(idColumnName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
890
|
+
if (prop == null) throw new InvalidOperationException($"Property '{idColumnName}' not found on entity.");
|
|
891
|
+
var idVal = prop.GetValue(entity);
|
|
892
|
+
var existing = idVal != null ? FindUnique(idVal) : default;
|
|
893
|
+
if (existing != null) return Update(entity);
|
|
894
|
+
return Create(entity);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
|
|
898
|
+
{
|
|
899
|
+
// 1. Primary path: native vector query. SQL Server has VECTOR_DISTANCE
|
|
900
|
+
// and Postgres has pgvector; SQLite has no vector operator, so it is
|
|
901
|
+
// skipped outright rather than issuing SQL that can only fail and
|
|
902
|
+
// cost a round trip before falling through.
|
|
903
|
+
if (Dialect != An5Dialect.Sqlite)
|
|
904
|
+
try
|
|
905
|
+
{
|
|
906
|
+
var dim = vector.Count;
|
|
907
|
+
var vecJson = JsonSerializer.Serialize(vector);
|
|
908
|
+
var field = An5Provider.Quote(vectorField, Dialect);
|
|
909
|
+
string sql;
|
|
910
|
+
if (Dialect == An5Dialect.Postgres)
|
|
911
|
+
{
|
|
912
|
+
var op = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? "<=>"
|
|
913
|
+
: (distanceMetric.Equals("euclidean", StringComparison.OrdinalIgnoreCase) ? "<->" : "<#>");
|
|
914
|
+
sql = $"SELECT *, ({field} {op} @query_vector::vector) AS distance FROM {TableName}";
|
|
915
|
+
}
|
|
916
|
+
else
|
|
917
|
+
{
|
|
918
|
+
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)";
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
var p = parameters != null ? new Dictionary<string, object>(parameters) : new Dictionary<string, object>();
|
|
922
|
+
p["query_vector"] = vecJson;
|
|
923
|
+
|
|
924
|
+
if (!string.IsNullOrWhiteSpace(whereClause))
|
|
925
|
+
sql += $" WHERE {field} IS NOT NULL AND ({whereClause})";
|
|
926
|
+
else
|
|
927
|
+
sql += $" WHERE {field} IS NOT NULL";
|
|
928
|
+
sql += " ORDER BY distance ASC";
|
|
929
|
+
if (Dialect == An5Dialect.Postgres) sql += $" LIMIT {take}";
|
|
930
|
+
|
|
931
|
+
var nativeRows = QueryRaw(sql, p);
|
|
932
|
+
if (nativeRows != null) return nativeRows;
|
|
933
|
+
}
|
|
934
|
+
catch
|
|
935
|
+
{
|
|
936
|
+
// Fallback to in-memory similarity computation if the database has no native vector support
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// 2. Secondary fallback: In-memory similarity computation
|
|
940
|
+
var rows = FindMany(whereClause, parameters);
|
|
941
|
+
var results = new List<Tuple<T, double>>();
|
|
942
|
+
|
|
943
|
+
var propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance);
|
|
944
|
+
if (propInfo == null)
|
|
945
|
+
{
|
|
946
|
+
propInfo = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
947
|
+
}
|
|
948
|
+
if (propInfo == null) return rows;
|
|
949
|
+
|
|
950
|
+
foreach (var row in rows)
|
|
951
|
+
{
|
|
952
|
+
List<double> rowVector = null;
|
|
953
|
+
var rawVal = propInfo.GetValue(row);
|
|
954
|
+
if (rawVal != null)
|
|
955
|
+
{
|
|
956
|
+
try
|
|
957
|
+
{
|
|
958
|
+
if (rawVal is string jsonStr)
|
|
959
|
+
{
|
|
960
|
+
rowVector = ParseDoubleArray(jsonStr);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
catch { }
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (rowVector != null && rowVector.Count == vector.Count)
|
|
967
|
+
{
|
|
968
|
+
double sim = CosineSimilarity(vector, rowVector);
|
|
969
|
+
double distance = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? (1.0 - sim) : sim;
|
|
970
|
+
results.Add(Tuple.Create(row, distance));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
|
|
975
|
+
|
|
976
|
+
var output = new List<T>();
|
|
977
|
+
int limit = Math.Min(take, results.Count);
|
|
978
|
+
for (int i = 0; i < limit; i++)
|
|
979
|
+
{
|
|
980
|
+
var distanceProp = typeof(T).GetProperty("Distance", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
|
981
|
+
if (distanceProp != null && distanceProp.PropertyType == typeof(double))
|
|
982
|
+
{
|
|
983
|
+
distanceProp.SetValue(results[i].Item1, results[i].Item2);
|
|
984
|
+
}
|
|
985
|
+
output.Add(results[i].Item1);
|
|
986
|
+
}
|
|
987
|
+
return output;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private static List<double> ParseDoubleArray(string json)
|
|
991
|
+
{
|
|
992
|
+
var clean = json.Trim('[', ']');
|
|
993
|
+
if (string.IsNullOrWhiteSpace(clean)) return new List<double>();
|
|
994
|
+
|
|
995
|
+
var parts = clean.Split(',');
|
|
996
|
+
var list = new List<double>();
|
|
997
|
+
foreach (var p in parts)
|
|
998
|
+
{
|
|
999
|
+
if (double.TryParse(p.Trim(), out double d))
|
|
1000
|
+
list.Add(d);
|
|
1001
|
+
}
|
|
1002
|
+
return list;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private static double CosineSimilarity(List<double> v1, List<double> v2)
|
|
1006
|
+
{
|
|
1007
|
+
double dot = 0.0, m1 = 0.0, m2 = 0.0;
|
|
1008
|
+
for (int i = 0; i < v1.Count; i++)
|
|
1009
|
+
{
|
|
1010
|
+
dot += v1[i] * v2[i];
|
|
1011
|
+
m1 += v1[i] * v1[i];
|
|
1012
|
+
m2 += v2[i] * v2[i];
|
|
1013
|
+
}
|
|
1014
|
+
if (m1 == 0 || m2 == 0) return 0.0;
|
|
1015
|
+
return dot / (Math.Sqrt(m1) * Math.Sqrt(m2));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/// <summary>
|
|
1019
|
+
/// Assigns a column to a property, converting when the provider's CLR
|
|
1020
|
+
/// type does not match the property.
|
|
1021
|
+
///
|
|
1022
|
+
/// Required across dialects, not just for SQLite: SQLite hands back
|
|
1023
|
+
/// dates as TEXT and every integer as Int64, so a DateTime or int
|
|
1024
|
+
/// property would otherwise fail to set. A value that cannot be
|
|
1025
|
+
/// converted is skipped rather than aborting the whole read.
|
|
1026
|
+
/// </summary>
|
|
1027
|
+
private static void SetValue(System.Reflection.PropertyInfo prop, object target, object value)
|
|
1028
|
+
{
|
|
1029
|
+
var wanted = prop.PropertyType;
|
|
1030
|
+
if (wanted.IsInstanceOfType(value))
|
|
1031
|
+
{
|
|
1032
|
+
prop.SetValue(target, value);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
try
|
|
1036
|
+
{
|
|
1037
|
+
var underlying = Nullable.GetUnderlyingType(wanted);
|
|
1038
|
+
if (underlying != null && value == null) { prop.SetValue(target, null); return; }
|
|
1039
|
+
if (underlying != null)
|
|
1040
|
+
{
|
|
1041
|
+
prop.SetValue(target, Convert.ChangeType(value, underlying));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (value is string text && wanted == typeof(Guid)) { prop.SetValue(target, Guid.Parse(text)); return; }
|
|
1045
|
+
if (value is string when && wanted == typeof(DateTime)) { prop.SetValue(target, DateTime.Parse(when, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind)); return; }
|
|
1046
|
+
prop.SetValue(target, Convert.ChangeType(value, wanted));
|
|
1047
|
+
}
|
|
1048
|
+
catch { }
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private bool HasColumn(DbDataReader reader, string columnName)
|
|
1052
|
+
{
|
|
1053
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
1054
|
+
{
|
|
1055
|
+
if (reader.GetName(i).Equals(columnName, StringComparison.OrdinalIgnoreCase))
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
`;
|
|
1063
|
+
|
|
1064
|
+
fs.writeFileSync(path.join(this.outputDir, 'An5DbContext.cs'), content);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
private capitalize(str: string): string {
|
|
1068
|
+
if (!str) return '';
|
|
1069
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
1070
|
+
}
|
|
1071
|
+
}
|