@an5/adapters 0.2.5 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -305
- package/dist/an5Adapter.d.ts +142 -7
- package/dist/an5Adapter.js +998 -68
- package/dist/base/metadata.d.ts +10 -0
- package/dist/base/metadata.js +107 -0
- package/dist/base/sql.d.ts +19 -1
- package/dist/base/sql.js +152 -24
- package/dist/base/types.d.ts +10 -1
- package/dist/browser.d.ts +4 -1
- package/dist/browser.js +9 -2
- package/dist/googlesheets/adapter.d.ts +1 -0
- package/dist/googlesheets/adapter.js +9 -2
- package/dist/googlesheets/config.d.ts +4 -4
- package/dist/googlesheets/helpers.js +2 -0
- package/dist/googlesheets/parseConnectionString.js +5 -2
- package/dist/googlesheets/sqlExecutor.js +29 -24
- package/dist/googlesheets/tableClient.js +14 -5
- package/dist/index.d.ts +5 -1
- package/dist/index.js +15 -1
- package/dist/mssql/engine.js +8 -4
- package/dist/mysql/engine.js +1 -1
- package/dist/nbase/client.d.ts +92 -0
- package/dist/nbase/client.js +147 -0
- package/dist/nbase/index.d.ts +47 -0
- package/dist/nbase/index.js +80 -0
- package/dist/postgres/engine.js +1 -1
- package/dist/sqlite/browserEngine.d.ts +27 -0
- package/dist/sqlite/browserEngine.js +161 -0
- package/dist/sqlite/engine.js +15 -3
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/typed.d.ts +5 -0
- package/dist/typed.js +2 -0
- package/dotnet/An5Adapter.cs +1112 -275
- package/dotnet/Base/Core.cs +380 -82
- package/dotnet/Mssql/MssqlEngine.cs +145 -144
- package/dotnet/Postgres/PostgresEngine.cs +149 -121
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/golang/an5_adapter.go +314 -0
- package/golang/base/base.go +100 -0
- package/golang/base/metadata.go +116 -0
- package/golang/base/where.go +311 -0
- package/golang/go.mod +3 -0
- package/golang/mssql/mssql.go +32 -0
- package/golang/postgres/postgres.go +37 -0
- package/golang/sqlite/sqlite.go +14 -0
- package/golang/sqlite_test.go +42 -0
- package/golang/table_client.go +1329 -0
- package/package.json +26 -9
- package/python/an5_adapter.py +184 -97
- package/python/base/__init__.py +3 -3
- package/python/base/dialects.py +11 -8
- package/python/base/metadata.py +59 -10
- package/python/base/sql.py +202 -121
- package/python/mssql/__init__.py +1 -1
- package/python/mssql/provider.py +62 -62
- package/python/postgres/__init__.py +1 -1
- package/python/postgres/provider.py +15 -15
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +636 -216
- package/rust/Cargo.toml +31 -0
- package/rust/README.md +71 -0
- package/rust/src/adapter.rs +1312 -0
- package/rust/src/base/dialect.rs +172 -0
- package/rust/src/base/metadata.rs +134 -0
- package/rust/src/base/mod.rs +17 -0
- package/rust/src/base/where_builder.rs +405 -0
- package/rust/src/lib.rs +75 -0
- package/typescript/src/an5Adapter.ts +1515 -505
- package/typescript/src/base/index.ts +3 -3
- package/typescript/src/base/metadata.ts +148 -28
- package/typescript/src/base/sql.ts +264 -98
- package/typescript/src/base/types.ts +33 -24
- package/typescript/src/browser.ts +10 -6
- package/typescript/src/config.ts +95 -95
- package/typescript/src/googlesheets/adapter.ts +269 -259
- package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
- package/typescript/src/googlesheets/config.ts +47 -47
- package/typescript/src/googlesheets/helpers.ts +144 -143
- package/typescript/src/googlesheets/index.ts +3 -3
- package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
- package/typescript/src/googlesheets/retry.ts +126 -126
- package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
- package/typescript/src/googlesheets/tableClient.ts +461 -455
- package/typescript/src/googlesheets/types.ts +6 -6
- package/typescript/src/index.ts +48 -27
- package/typescript/src/mssql/engine.ts +134 -132
- package/typescript/src/mssql/index.ts +1 -1
- package/typescript/src/mysql/engine.ts +80 -80
- package/typescript/src/mysql/index.ts +1 -1
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
- package/typescript/src/postgres/engine.ts +87 -87
- package/typescript/src/postgres/index.ts +1 -1
- package/typescript/src/sqlite/browserEngine.ts +192 -0
- package/typescript/src/sqlite/engine.ts +66 -55
- package/typescript/src/sqlite/index.ts +3 -1
- package/typescript/src/typed.ts +52 -0
|
@@ -1,505 +1,1515 @@
|
|
|
1
|
-
import { generateUUID } from './base/uuid';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
An5SheetsAdapter,
|
|
5
|
-
An5SheetsAdapterConfig,
|
|
6
|
-
SheetsTableClient,
|
|
7
|
-
} from './googlesheets';
|
|
8
|
-
import { parseSheetsConnectionString } from './googlesheets/parseConnectionString';
|
|
9
|
-
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
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
|
-
if (
|
|
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
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
private
|
|
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
|
-
return this.
|
|
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
|
-
if (
|
|
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
|
-
async
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
const
|
|
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
|
-
|
|
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
|
-
async
|
|
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
|
-
|
|
1
|
+
import { generateUUID } from './base/uuid';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
An5SheetsAdapter,
|
|
5
|
+
An5SheetsAdapterConfig,
|
|
6
|
+
SheetsTableClient,
|
|
7
|
+
} from './googlesheets';
|
|
8
|
+
import { parseSheetsConnectionString } from './googlesheets/parseConnectionString';
|
|
9
|
+
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* File extensions that mean SQLite when a connection string has no scheme.
|
|
13
|
+
*
|
|
14
|
+
* `.sqlite3` is here as well as in `@an5/orm`'s `detectProvider`, because the ORM
|
|
15
|
+
* validates field types and writes the DDL from the provider this file picks: if the
|
|
16
|
+
* two lists differed, a `.sqlite3` file would be pushed as SQL Server DDL.
|
|
17
|
+
*/
|
|
18
|
+
const SQLITE_FILE_SUFFIXES = ['.sqlite', '.sqlite3', '.db'];
|
|
19
|
+
import {
|
|
20
|
+
NBaseError,
|
|
21
|
+
buildVectorId,
|
|
22
|
+
parseVectorId,
|
|
23
|
+
isNBaseConnectionString,
|
|
24
|
+
parseNBaseConnectionString,
|
|
25
|
+
createNBaseVectorClient,
|
|
26
|
+
type NBaseAdapterConfig,
|
|
27
|
+
type NBaseVectorClient,
|
|
28
|
+
} from './nbase';
|
|
29
|
+
import { buildOrderBy, parseWhere, quote } from './base/sql';
|
|
30
|
+
import {
|
|
31
|
+
createAdapterProxy,
|
|
32
|
+
getFieldsForModel,
|
|
33
|
+
getModelToTable,
|
|
34
|
+
getRelationMap,
|
|
35
|
+
getRelationsForModel,
|
|
36
|
+
type RelationDef,
|
|
37
|
+
} from './base/metadata';
|
|
38
|
+
export type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
39
|
+
export { setAdapterMetadata, createAdapterProxy, type AdapterMetadata } from './base/metadata';
|
|
40
|
+
export type { TypedAn5Adapter, AdapterAPI } from './typed';
|
|
41
|
+
|
|
42
|
+
export type AnyAdapter = An5Adapter | An5SheetsAdapter;
|
|
43
|
+
export type AnyAdapterConfig = An5AdapterConfig | An5SheetsAdapterConfig | { connectionString: string };
|
|
44
|
+
|
|
45
|
+
function isSheetsConfig(config: AnyAdapterConfig): config is An5SheetsAdapterConfig {
|
|
46
|
+
return (config as any).spreadsheetId !== undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sanitizeParamName(name: string): string {
|
|
50
|
+
const cleaned = String(name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
51
|
+
return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appendUpdateSet(sets: string[], params: Record<string, any>, col: string, val: any, dialect: Dialect): void {
|
|
55
|
+
const quoted = quote(col, dialect);
|
|
56
|
+
const safeCol = sanitizeParamName(col);
|
|
57
|
+
if (val && typeof val === 'object' && !(val instanceof Date)) {
|
|
58
|
+
if (val.increment !== undefined) {
|
|
59
|
+
sets.push(`${quoted} = ${quoted} + @s_${safeCol}_inc`);
|
|
60
|
+
params[`s_${safeCol}_inc`] = val.increment;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (val.decrement !== undefined) {
|
|
64
|
+
sets.push(`${quoted} = ${quoted} - @s_${safeCol}_dec`);
|
|
65
|
+
params[`s_${safeCol}_dec`] = val.decrement;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (val.multiply !== undefined) {
|
|
69
|
+
sets.push(`${quoted} = ${quoted} * @s_${safeCol}_mul`);
|
|
70
|
+
params[`s_${safeCol}_mul`] = val.multiply;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (val.divide !== undefined) {
|
|
74
|
+
sets.push(`${quoted} = ${quoted} / @s_${safeCol}_div`);
|
|
75
|
+
params[`s_${safeCol}_div`] = val.divide;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (val.set !== undefined) {
|
|
79
|
+
sets.push(`${quoted} = @s_${safeCol}_set`);
|
|
80
|
+
params[`s_${safeCol}_set`] = val.set;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
sets.push(`${quoted} = @s_${safeCol}`);
|
|
86
|
+
params[`s_${safeCol}`] = val;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function selectedAggregateFields(fields: any): string[] {
|
|
90
|
+
if (!fields || typeof fields !== 'object') return [];
|
|
91
|
+
return Object.keys(fields).filter(key => fields[key]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeByFields(by: any): string[] {
|
|
95
|
+
if (typeof by === 'string') return [by];
|
|
96
|
+
return Array.isArray(by) ? by.filter(field => typeof field === 'string' && field.length > 0) : [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toNonNegativeInt(value: unknown, fallback = 0): number {
|
|
100
|
+
const n = Number.parseInt(String(value), 10);
|
|
101
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function projectFields(row: any, select: any): any {
|
|
105
|
+
if (!row || !select || typeof select !== 'object') return row;
|
|
106
|
+
const projected: any = {};
|
|
107
|
+
for (const [key, val] of Object.entries(select)) {
|
|
108
|
+
if (val) {
|
|
109
|
+
projected[key] = row[key];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (row._count) {
|
|
113
|
+
projected._count = row._count;
|
|
114
|
+
}
|
|
115
|
+
return projected;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function resolveIncludes(
|
|
119
|
+
modelName: string,
|
|
120
|
+
rows: any[],
|
|
121
|
+
include: any,
|
|
122
|
+
adapter: An5Adapter | An5AdapterTx
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
if (!rows || rows.length === 0 || !include || typeof include !== 'object') return;
|
|
125
|
+
const modelRelations = getRelationsForModel(modelName);
|
|
126
|
+
|
|
127
|
+
for (const [key, value] of Object.entries(include)) {
|
|
128
|
+
if (!value) continue;
|
|
129
|
+
|
|
130
|
+
if (key === '_count') {
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
if (!row._count) row._count = {};
|
|
133
|
+
}
|
|
134
|
+
for (const [relKey, relation] of Object.entries(modelRelations)) {
|
|
135
|
+
if (relation.relationType === 'many') {
|
|
136
|
+
const uniqueKeys = Array.from(new Set(rows.map(r => r[relation.localKey]).filter(k => k !== undefined && k !== null)));
|
|
137
|
+
if (uniqueKeys.length > 0) {
|
|
138
|
+
const relClient = adapter.table(relation.modelName);
|
|
139
|
+
const relatedRows = await relClient.findMany({
|
|
140
|
+
where: { [relation.foreignKey]: { in: uniqueKeys } },
|
|
141
|
+
});
|
|
142
|
+
const countMap = new Map<any, number>();
|
|
143
|
+
relatedRows.forEach((r: any) => {
|
|
144
|
+
const k = r[relation.foreignKey];
|
|
145
|
+
countMap.set(k, (countMap.get(k) || 0) + 1);
|
|
146
|
+
});
|
|
147
|
+
rows.forEach(r => {
|
|
148
|
+
const k = r[relation.localKey];
|
|
149
|
+
r._count[relKey] = countMap.get(k) || 0;
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
rows.forEach(r => { r._count[relKey] = 0; });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const relation = modelRelations[key];
|
|
160
|
+
if (!relation) continue;
|
|
161
|
+
|
|
162
|
+
const isMany = relation.relationType === 'many';
|
|
163
|
+
const localKey = relation.localKey;
|
|
164
|
+
const foreignKey = relation.foreignKey;
|
|
165
|
+
const joinKey = isMany ? localKey : foreignKey;
|
|
166
|
+
const matchKey = isMany ? foreignKey : localKey;
|
|
167
|
+
const uniqueKeys = Array.from(new Set(rows.map(r => r[joinKey]).filter(k => k !== undefined && k !== null)));
|
|
168
|
+
|
|
169
|
+
if (uniqueKeys.length === 0) {
|
|
170
|
+
rows.forEach(r => { r[key] = isMany ? [] : null; });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const subArgs: any = typeof value === 'object' ? { ...value } : {};
|
|
175
|
+
const nestedInclude = subArgs.include;
|
|
176
|
+
const nestedSelect = subArgs.select;
|
|
177
|
+
|
|
178
|
+
const subWhere: any = {
|
|
179
|
+
[matchKey]: { in: uniqueKeys },
|
|
180
|
+
...(subArgs.where || {}),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const relClient = adapter.table(relation.modelName);
|
|
184
|
+
const relatedRows = await relClient.findMany({
|
|
185
|
+
where: subWhere,
|
|
186
|
+
orderBy: subArgs.orderBy,
|
|
187
|
+
take: subArgs.take,
|
|
188
|
+
skip: subArgs.skip,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (nestedInclude) {
|
|
192
|
+
await resolveIncludes(relation.modelName, relatedRows, nestedInclude, adapter);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const outputRows = nestedSelect && typeof nestedSelect === 'object'
|
|
196
|
+
? relatedRows.map((r: any) => projectFields(r, nestedSelect))
|
|
197
|
+
: relatedRows;
|
|
198
|
+
|
|
199
|
+
const groupMap = new Map<any, any[]>();
|
|
200
|
+
relatedRows.forEach((r: any, idx: number) => {
|
|
201
|
+
const k = r[isMany ? foreignKey : localKey];
|
|
202
|
+
if (!groupMap.has(k)) groupMap.set(k, []);
|
|
203
|
+
groupMap.get(k)!.push(outputRows[idx]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
rows.forEach(r => {
|
|
207
|
+
const k = r[isMany ? localKey : foreignKey];
|
|
208
|
+
const matches = groupMap.get(k) || [];
|
|
209
|
+
if (isMany) {
|
|
210
|
+
r[key] = matches;
|
|
211
|
+
} else {
|
|
212
|
+
r[key] = matches[0] || null;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── An5Adapter ────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export class An5Adapter {
|
|
221
|
+
[key: string]: any;
|
|
222
|
+
private _engine: QueryEngine | null = null;
|
|
223
|
+
private _engineType: 'postgres' | 'mysql' | 'sqlite' | 'mssql' | null = null;
|
|
224
|
+
private _engineConfig: An5AdapterConfig | null = null;
|
|
225
|
+
sheetsAdapter: An5SheetsAdapter | null = null;
|
|
226
|
+
private _nbase: NBaseVectorClient | null = null;
|
|
227
|
+
private _nbaseConfig: NBaseAdapterConfig | null = null;
|
|
228
|
+
private _nbaseOnly = false;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Returns the NBase client for this adapter, if the project configured one.
|
|
232
|
+
* The connection string may also point straight at NBase, which keeps
|
|
233
|
+
* `DATABASE_URL=nbase://host:port` working on its own.
|
|
234
|
+
*/
|
|
235
|
+
nbaseClient(): { client: NBaseVectorClient; config: NBaseAdapterConfig } | null {
|
|
236
|
+
if (this._nbase && this._nbaseConfig) return { client: this._nbase, config: this._nbaseConfig };
|
|
237
|
+
|
|
238
|
+
if (!this._nbaseConfig) return null;
|
|
239
|
+
const config = this._nbaseConfig;
|
|
240
|
+
|
|
241
|
+
const client: NBaseVectorClient = createNBaseVectorClient(config);
|
|
242
|
+
this._nbase = client;
|
|
243
|
+
this._nbaseConfig = config;
|
|
244
|
+
return { client, config };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* True when the adapter only knows about NBase, so there is no relational
|
|
249
|
+
* database to read rows from.
|
|
250
|
+
*/
|
|
251
|
+
get nbaseOnly(): boolean {
|
|
252
|
+
return this._nbaseOnly === true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get dialect(): Dialect {
|
|
256
|
+
if (this.sheetsAdapter) return 'googlesheets';
|
|
257
|
+
if (this._engine) return this._engine.dialect;
|
|
258
|
+
if (this._engineType === 'postgres') return 'postgres';
|
|
259
|
+
if (this._engineType === 'mysql') return 'mysql';
|
|
260
|
+
if (this._engineType === 'sqlite') return 'sqlite';
|
|
261
|
+
return 'mssql';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
constructor(adapterConfig: An5AdapterConfig | An5SheetsAdapterConfig) {
|
|
265
|
+
if (isSheetsConfig(adapterConfig)) {
|
|
266
|
+
this.sheetsAdapter = new An5SheetsAdapter(adapterConfig);
|
|
267
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Keep the config even when the caller supplies its own engine: the NBase
|
|
271
|
+
// settings and anything else on the config are read from it later.
|
|
272
|
+
this._engineConfig = adapterConfig as An5AdapterConfig;
|
|
273
|
+
|
|
274
|
+
// `nbase://host:port` as the connection string configures the vector store
|
|
275
|
+
// on its own: there is no relational database, so the adapter is
|
|
276
|
+
// vector-only. The same string passed as `nbase` keeps the database from
|
|
277
|
+
// `connectionString` as the source of rows.
|
|
278
|
+
const rawConnectionString = String(this._engineConfig.connectionString ?? '').trim();
|
|
279
|
+
if (isNBaseConnectionString(rawConnectionString)) {
|
|
280
|
+
this._nbaseConfig = parseNBaseConnectionString(rawConnectionString);
|
|
281
|
+
this._nbaseOnly = true;
|
|
282
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
283
|
+
}
|
|
284
|
+
const attached = this._engineConfig.nbase;
|
|
285
|
+
if (attached && isNBaseConnectionString(attached)) {
|
|
286
|
+
this._nbaseConfig = parseNBaseConnectionString(attached);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if ((adapterConfig as any).engine) {
|
|
290
|
+
this._engine = (adapterConfig as any).engine;
|
|
291
|
+
this._engineType = (this._engine?.dialect as any) || 'sqlite';
|
|
292
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if ((adapterConfig as any).db || (adapterConfig as any).driver) {
|
|
296
|
+
const { SqliteBrowserEngine } = require('./sqlite/browserEngine.js');
|
|
297
|
+
this._engine = new SqliteBrowserEngine(adapterConfig);
|
|
298
|
+
this._engineType = 'sqlite';
|
|
299
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Lower-cased for the dialect test only; the connection string itself keeps its
|
|
303
|
+
// case for the driver. A URI scheme is case-insensitive, and `@an5/orm` reads the
|
|
304
|
+
// provider the same way to validate field types and write DDL — if the two
|
|
305
|
+
// disagreed, a schema would be checked against one database and run on another.
|
|
306
|
+
const cs = (adapterConfig.connectionString || '').trim();
|
|
307
|
+
const forDialect = cs.toLowerCase();
|
|
308
|
+
|
|
309
|
+
if (forDialect.startsWith('googlesheets://')) {
|
|
310
|
+
this.sheetsAdapter = new An5SheetsAdapter(parseSheetsConnectionString(cs));
|
|
311
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this._engineConfig = adapterConfig;
|
|
315
|
+
if (forDialect.startsWith('postgres://') || forDialect.startsWith('postgresql://')) {
|
|
316
|
+
this._engineType = 'postgres';
|
|
317
|
+
} else if (forDialect.startsWith('mysql://') || forDialect.startsWith('mariadb://')) {
|
|
318
|
+
this._engineType = 'mysql';
|
|
319
|
+
} else if (
|
|
320
|
+
forDialect.startsWith('sqlite://')
|
|
321
|
+
|| SQLITE_FILE_SUFFIXES.some((suffix) => forDialect.endsWith(suffix))
|
|
322
|
+
) {
|
|
323
|
+
this._engineType = 'sqlite';
|
|
324
|
+
} else {
|
|
325
|
+
this._engineType = 'mssql';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async requireEngine(): Promise<QueryEngine> {
|
|
332
|
+
if (!this._engine && this._engineType) {
|
|
333
|
+
switch (this._engineType) {
|
|
334
|
+
case 'postgres': {
|
|
335
|
+
const { PostgresEngine } = require('./postgres/index.js');
|
|
336
|
+
this._engine = new PostgresEngine(this._engineConfig!);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case 'mysql': {
|
|
340
|
+
const { MysqlEngine } = require('./mysql/index.js');
|
|
341
|
+
this._engine = new MysqlEngine(this._engineConfig!);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case 'sqlite': {
|
|
345
|
+
const { SqliteEngine } = require('./sqlite/index.js');
|
|
346
|
+
this._engine = new SqliteEngine(this._engineConfig!);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'mssql': {
|
|
350
|
+
const { MssqlEngine } = require('./mssql/index.js');
|
|
351
|
+
this._engine = new MssqlEngine(this._engineConfig!);
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!this._engine) throw new Error('SQL engine is not available for Google Sheets adapter');
|
|
357
|
+
return this._engine;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private requireSheetsAdapter(): An5SheetsAdapter {
|
|
361
|
+
if (!this.sheetsAdapter) throw new Error('Google Sheets methods require a googlesheets:// connection or Sheets config');
|
|
362
|
+
return this.sheetsAdapter;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
366
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.exec<T>(query, params);
|
|
367
|
+
return (await this.requireEngine()).exec<T>(query, params);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** INTERNAL: used by AdapterTableClient for DML statements needing row count */
|
|
371
|
+
async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
372
|
+
return (await this.requireEngine()).executeRaw(query, params);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Execute a raw query with positional values → @p_0, @p_1, ... per dialect */
|
|
376
|
+
async $queryRawUnsafe<T = any>(query: string, ...values: any[]): Promise<T[]> {
|
|
377
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$queryRawUnsafe<T>(query, ...values);
|
|
378
|
+
const engine = await this.requireEngine();
|
|
379
|
+
const params: Record<string, any> = {};
|
|
380
|
+
values.forEach((v, i) => { params[`p_${i}`] = v; });
|
|
381
|
+
let q = query;
|
|
382
|
+
if (this.dialect === 'postgres') {
|
|
383
|
+
let idx = 0;
|
|
384
|
+
q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
|
|
385
|
+
} else if (this.dialect === 'mysql') {
|
|
386
|
+
let idx = 0;
|
|
387
|
+
q = q.replace(/\?/g, () => `@p_${idx++}`);
|
|
388
|
+
}
|
|
389
|
+
return engine.exec<T>(q, params);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async $executeRaw(query: string, ...values: any[]): Promise<number> {
|
|
393
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$executeRaw(query, ...values);
|
|
394
|
+
const engine = await this.requireEngine();
|
|
395
|
+
const params: Record<string, any> = {};
|
|
396
|
+
values.forEach((v, i) => { params[`p_${i}`] = v; });
|
|
397
|
+
let q = query;
|
|
398
|
+
if (this.dialect === 'postgres') {
|
|
399
|
+
let idx = 0;
|
|
400
|
+
q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
|
|
401
|
+
} else if (this.dialect === 'mysql') {
|
|
402
|
+
let idx = 0;
|
|
403
|
+
q = q.replace(/\?/g, () => `@p_${idx++}`);
|
|
404
|
+
}
|
|
405
|
+
return engine.executeRaw(q, params);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async $executeRawUnsafe(query: string, ...values: any[]): Promise<number> {
|
|
409
|
+
return this.$executeRaw(query, ...values);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async $connect(): Promise<void> {
|
|
413
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$connect();
|
|
414
|
+
if (this._nbaseOnly) {
|
|
415
|
+
// NBase is stateless over HTTP, so there is nothing to open. Report the
|
|
416
|
+
// endpoint so a misconfiguration is obvious.
|
|
417
|
+
const config = this.nbaseClient()?.config;
|
|
418
|
+
if (config) await this.nbaseClient()!.client.health();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
await (await this.requireEngine()).connect();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Health check for the configured NBase instance, or the database when the
|
|
426
|
+
* adapter has none.
|
|
427
|
+
*/
|
|
428
|
+
async $nbaseHealth(): Promise<{ connected: boolean; endpoint?: string; error?: string }> {
|
|
429
|
+
const nbase = this.nbaseClient();
|
|
430
|
+
if (!nbase) return { connected: false, error: 'No NBase instance configured.' };
|
|
431
|
+
try {
|
|
432
|
+
const health = await nbase.client.health();
|
|
433
|
+
return { connected: health.status === 'ok', endpoint: nbase.config.url };
|
|
434
|
+
} catch (err) {
|
|
435
|
+
return {
|
|
436
|
+
connected: false,
|
|
437
|
+
endpoint: nbase.config.url,
|
|
438
|
+
error: err instanceof Error ? err.message : String(err),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async $disconnect(): Promise<void> {
|
|
444
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$disconnect();
|
|
445
|
+
await (await this.requireEngine()).disconnect();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Real transaction with BEGIN / COMMIT / ROLLBACK */
|
|
449
|
+
async $transaction<R>(fn: (tx: An5AdapterTx) => Promise<R>, options?: { timeout?: number }): Promise<R>;
|
|
450
|
+
async $transaction<R>(list: Promise<R>[]): Promise<R[]>;
|
|
451
|
+
async $transaction(fn: any, _options?: any): Promise<any> {
|
|
452
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$transaction(fn, _options);
|
|
453
|
+
if (Array.isArray(fn)) return Promise.all(fn);
|
|
454
|
+
const engine = await this.requireEngine();
|
|
455
|
+
const handle = await engine.beginTransaction();
|
|
456
|
+
const txAdapter = new An5AdapterTx(handle, this.dialect);
|
|
457
|
+
try {
|
|
458
|
+
const result = await fn(txAdapter);
|
|
459
|
+
await handle.commit();
|
|
460
|
+
return result;
|
|
461
|
+
} catch (err) {
|
|
462
|
+
await handle.rollback();
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async $begin(): Promise<An5AdapterTx> {
|
|
468
|
+
if (this.sheetsAdapter) {
|
|
469
|
+
throw new Error('Interactive transactions are not supported by the Google Sheets adapter');
|
|
470
|
+
}
|
|
471
|
+
const engine = await this.requireEngine();
|
|
472
|
+
return new An5AdapterTx(await engine.beginTransaction(), this.dialect);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
table<T = any>(modelName: string): AdapterTableClient<T> | SheetsTableClient<T> {
|
|
476
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.table<T>(modelName);
|
|
477
|
+
return new AdapterTableClient<T>(this, modelName);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private _listeners: Record<string, ((...args: any[]) => void)[]> = {};
|
|
481
|
+
private _middlewares: ((params: any) => Promise<any>)[] = [];
|
|
482
|
+
|
|
483
|
+
$on(event: 'query' | 'error' | 'warn' | 'info' | string, callback: (...args: any[]) => void): this {
|
|
484
|
+
if (!this._listeners[event]) this._listeners[event] = [];
|
|
485
|
+
this._listeners[event].push(callback);
|
|
486
|
+
return this;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
$off(event: string, callback: (...args: any[]) => void): this {
|
|
490
|
+
if (!this._listeners[event]) return this;
|
|
491
|
+
this._listeners[event] = this._listeners[event].filter(cb => cb !== callback);
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
$emit(event: string, ...args: any[]): void {
|
|
496
|
+
if (this._listeners[event]) {
|
|
497
|
+
for (const cb of this._listeners[event]) {
|
|
498
|
+
try { cb(...args); } catch {}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
$use(middleware: (params: { model?: string; action: string; args: any; run: (args: any) => Promise<any> }) => Promise<any>): this {
|
|
504
|
+
this._middlewares.push(middleware);
|
|
505
|
+
return this;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
view<T = any>(viewName: string): ViewClient<T> {
|
|
509
|
+
return new ViewClient<T>(this, viewName);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async $queryProc<T = any>(procName: string, params?: Record<string, any> | any[]): Promise<T[]> {
|
|
513
|
+
if (this.sheetsAdapter) throw new Error('Stored procedures are not supported by the Google Sheets adapter');
|
|
514
|
+
const paramObj: Record<string, any> = {};
|
|
515
|
+
let sql = '';
|
|
516
|
+
if (Array.isArray(params)) {
|
|
517
|
+
const placeholders = params.map((v, i) => {
|
|
518
|
+
paramObj[`p_${i}`] = v;
|
|
519
|
+
return `@p_${i}`;
|
|
520
|
+
});
|
|
521
|
+
sql = this.dialect === 'postgres' || this.dialect === 'mysql'
|
|
522
|
+
? `CALL ${procName}(${placeholders.join(', ')})`
|
|
523
|
+
: `EXEC ${procName} ${placeholders.join(', ')}`;
|
|
524
|
+
} else if (params && typeof params === 'object') {
|
|
525
|
+
const pairs = Object.entries(params).map(([k, v]) => {
|
|
526
|
+
paramObj[`p_${k}`] = v;
|
|
527
|
+
return `@${k} = @p_${k}`;
|
|
528
|
+
});
|
|
529
|
+
sql = this.dialect === 'postgres' || this.dialect === 'mysql'
|
|
530
|
+
? `CALL ${procName}(${Object.keys(params).map(k => `@p_${k}`).join(', ')})`
|
|
531
|
+
: `EXEC ${procName} ${pairs.join(', ')}`;
|
|
532
|
+
} else {
|
|
533
|
+
sql = this.dialect === 'postgres' || this.dialect === 'mysql'
|
|
534
|
+
? `CALL ${procName}()`
|
|
535
|
+
: `EXEC ${procName}`;
|
|
536
|
+
}
|
|
537
|
+
return this.exec<T>(sql, paramObj);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async $executeProc(procName: string, params?: Record<string, any> | any[]): Promise<number> {
|
|
541
|
+
if (this.sheetsAdapter) throw new Error('Stored procedures are not supported by the Google Sheets adapter');
|
|
542
|
+
const paramObj: Record<string, any> = {};
|
|
543
|
+
let sql = '';
|
|
544
|
+
if (Array.isArray(params)) {
|
|
545
|
+
const placeholders = params.map((v, i) => {
|
|
546
|
+
paramObj[`p_${i}`] = v;
|
|
547
|
+
return `@p_${i}`;
|
|
548
|
+
});
|
|
549
|
+
sql = this.dialect === 'postgres' || this.dialect === 'mysql'
|
|
550
|
+
? `CALL ${procName}(${placeholders.join(', ')})`
|
|
551
|
+
: `EXEC ${procName} ${placeholders.join(', ')}`;
|
|
552
|
+
} else if (params && typeof params === 'object') {
|
|
553
|
+
const pairs = Object.entries(params).map(([k, v]) => {
|
|
554
|
+
paramObj[`p_${k}`] = v;
|
|
555
|
+
return `@${k} = @p_${k}`;
|
|
556
|
+
});
|
|
557
|
+
sql = this.dialect === 'postgres' || this.dialect === 'mysql'
|
|
558
|
+
? `CALL ${procName}(${Object.keys(params).map(k => `@p_${k}`).join(', ')})`
|
|
559
|
+
: `EXEC ${procName} ${pairs.join(', ')}`;
|
|
560
|
+
} else {
|
|
561
|
+
sql = this.dialect === 'postgres' || this.dialect === 'mysql'
|
|
562
|
+
? `CALL ${procName}()`
|
|
563
|
+
: `EXEC ${procName}`;
|
|
564
|
+
}
|
|
565
|
+
return this._executeRaw(sql, paramObj);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async readRange<T = any>(range: string): Promise<T[][]> {
|
|
569
|
+
return this.requireSheetsAdapter().readRange<T>(range);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async writeRange(range: string, values: any[][]): Promise<void> {
|
|
573
|
+
await this.requireSheetsAdapter().writeRange(range, values);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async appendRange(range: string, values: any[][]): Promise<void> {
|
|
577
|
+
await this.requireSheetsAdapter().appendRange(range, values);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async listSheets(): Promise<string[]> {
|
|
581
|
+
return this.requireSheetsAdapter().listSheets();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async deleteSheet(name: string): Promise<void> {
|
|
585
|
+
await this.requireSheetsAdapter().deleteSheet(name);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ─── View Client (Read-Only) ────────────────────────────────────────────────────────
|
|
590
|
+
|
|
591
|
+
export class ViewClient<T = any> {
|
|
592
|
+
private readonly tableClient: AdapterTableClient<T>;
|
|
593
|
+
|
|
594
|
+
constructor(
|
|
595
|
+
public readonly adapter: An5Adapter | An5AdapterTx,
|
|
596
|
+
public readonly viewName: string,
|
|
597
|
+
) {
|
|
598
|
+
this.tableClient = new AdapterTableClient<T>(adapter, viewName);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
public get dialect(): Dialect { return this.tableClient.dialect; }
|
|
602
|
+
public get tableName(): string { return this.tableClient.tableName; }
|
|
603
|
+
|
|
604
|
+
async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any; include?: any }): Promise<T[]> {
|
|
605
|
+
return this.tableClient.findMany(args);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async findFirst(args?: { where?: any; orderBy?: any; select?: any; include?: any }): Promise<T | null> {
|
|
609
|
+
return this.tableClient.findFirst(args);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async findUnique(args: { where: any; select?: any; include?: any }): Promise<T | null> {
|
|
613
|
+
return this.tableClient.findUnique(args);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async count(args?: { where?: any }): Promise<number> {
|
|
617
|
+
return this.tableClient.count(args);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async aggregate(args: any): Promise<any> {
|
|
621
|
+
return this.tableClient.aggregate(args);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async groupBy(args: any): Promise<any[]> {
|
|
625
|
+
return this.tableClient.groupBy(args);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async vectorSearch(args: any): Promise<(T & { distance: number })[]> {
|
|
629
|
+
return this.tableClient.vectorSearch(args);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async create(): Promise<never> {
|
|
633
|
+
throw new Error(`View '${this.viewName}' is read-only. Mutation operations (create) are not allowed.`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async createMany(): Promise<never> {
|
|
637
|
+
throw new Error(`View '${this.viewName}' is read-only. Mutation operations (createMany) are not allowed.`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async update(): Promise<never> {
|
|
641
|
+
throw new Error(`View '${this.viewName}' is read-only. Mutation operations (update) are not allowed.`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async updateMany(): Promise<never> {
|
|
645
|
+
throw new Error(`View '${this.viewName}' is read-only. Mutation operations (updateMany) are not allowed.`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async delete(): Promise<never> {
|
|
649
|
+
throw new Error(`View '${this.viewName}' is read-only. Mutation operations (delete) are not allowed.`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async deleteMany(): Promise<never> {
|
|
653
|
+
throw new Error(`View '${this.viewName}' is read-only. Mutation operations (deleteMany) are not allowed.`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async upsert(): Promise<never> {
|
|
657
|
+
throw new Error(`View '${this.viewName}' is read-only. Mutation operations (upsert) are not allowed.`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ─── Transaction-scoped adapter ────────────────────────────────────────────────────
|
|
662
|
+
|
|
663
|
+
export class An5AdapterTx {
|
|
664
|
+
[key: string]: any;
|
|
665
|
+
private closed = false;
|
|
666
|
+
|
|
667
|
+
constructor(
|
|
668
|
+
private readonly handle: TransactionHandle,
|
|
669
|
+
public readonly dialect: Dialect,
|
|
670
|
+
) {
|
|
671
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
675
|
+
this.ensureOpen();
|
|
676
|
+
return this.handle.exec<T>(query, params);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
680
|
+
this.ensureOpen();
|
|
681
|
+
return this.handle.executeRaw(query, params);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async $commit(): Promise<void> {
|
|
685
|
+
this.ensureOpen();
|
|
686
|
+
this.closed = true;
|
|
687
|
+
await this.handle.commit();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async $rollback(): Promise<void> {
|
|
691
|
+
this.ensureOpen();
|
|
692
|
+
this.closed = true;
|
|
693
|
+
await this.handle.rollback();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
table<T = any>(modelName: string): AdapterTableClient<T> {
|
|
697
|
+
return new AdapterTableClient<T>(this, modelName);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private ensureOpen(): void {
|
|
701
|
+
if (this.closed) throw new Error('Transaction is already closed');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ─── Table Client ──────────────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
export class AdapterTableClient<T = any> {
|
|
708
|
+
constructor(
|
|
709
|
+
public readonly adapter: An5Adapter | An5AdapterTx,
|
|
710
|
+
public readonly modelName: string,
|
|
711
|
+
) { }
|
|
712
|
+
|
|
713
|
+
public get dialect(): Dialect { return this.adapter.dialect; }
|
|
714
|
+
|
|
715
|
+
public get tableName(): string {
|
|
716
|
+
const name = this.modelName;
|
|
717
|
+
let t = name;
|
|
718
|
+
const modelToTable = getModelToTable();
|
|
719
|
+
if (modelToTable[name]) t = modelToTable[name];
|
|
720
|
+
else {
|
|
721
|
+
const camel = name.charAt(0).toLowerCase() + name.slice(1);
|
|
722
|
+
if (modelToTable[camel]) t = modelToTable[camel];
|
|
723
|
+
else {
|
|
724
|
+
const lower = name.toLowerCase();
|
|
725
|
+
if (modelToTable[lower]) t = modelToTable[lower];
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
|
|
729
|
+
if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
|
|
730
|
+
return quote(t, this.dialect);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private get nolock(): string {
|
|
734
|
+
return this.dialect === 'mssql' ? ' WITH (NOLOCK)' : '';
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Build the shared context used by parseWhere to resolve relations and self references. */
|
|
738
|
+
private whereContext(): { relationMap: Record<string, Record<string, RelationDef>>; modelToTable: Record<string, string>; selfRef: string } {
|
|
739
|
+
return {
|
|
740
|
+
relationMap: getRelationMap(),
|
|
741
|
+
modelToTable: getModelToTable(),
|
|
742
|
+
selfRef: this.tableName,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private async doExec<U = any>(query: string, params?: Record<string, any>): Promise<U[]> {
|
|
747
|
+
return this.adapter.exec<U>(query, params);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private async doExecuteRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
751
|
+
return this.adapter._executeRaw(query, params);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any; include?: any }): Promise<T[]> {
|
|
755
|
+
const params: Record<string, any> = {};
|
|
756
|
+
const whereCtx = this.whereContext();
|
|
757
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', whereCtx);
|
|
758
|
+
const orderSql = buildOrderBy(args?.orderBy, this.dialect);
|
|
759
|
+
const take = args?.take;
|
|
760
|
+
const skip = args?.skip;
|
|
761
|
+
const hasSkip = skip !== undefined && skip !== null;
|
|
762
|
+
|
|
763
|
+
let cols = "*";
|
|
764
|
+
if (args?.select && typeof args.select === "object") {
|
|
765
|
+
const relationKeys = getRelationsForModel(this.modelName);
|
|
766
|
+
const hasRelationSelect = Object.keys(args.select).some(k => k === '_count' || relationKeys[k]);
|
|
767
|
+
const fields = getFieldsForModel(this.modelName);
|
|
768
|
+
const selectedKeys = Object.keys(args.select).filter(k => args.select[k] === true && (Object.keys(fields).length === 0 || fields[k]));
|
|
769
|
+
if (selectedKeys.length > 0 && !hasRelationSelect) {
|
|
770
|
+
cols = selectedKeys.map(k => quote(k, this.dialect)).join(", ");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
let query: string;
|
|
775
|
+
if (take !== undefined && !hasSkip) {
|
|
776
|
+
if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
|
|
777
|
+
query = `SELECT ${cols} FROM ${this.tableName}`;
|
|
778
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
779
|
+
if (orderSql) query += ` ${orderSql}`;
|
|
780
|
+
query += ` LIMIT ${take}`;
|
|
781
|
+
} else {
|
|
782
|
+
query = `SELECT TOP (${take}) ${cols} FROM ${this.tableName}${this.nolock}`;
|
|
783
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
784
|
+
if (orderSql) query += ` ${orderSql}`;
|
|
785
|
+
}
|
|
786
|
+
} else if (hasSkip) {
|
|
787
|
+
if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
|
|
788
|
+
query = `SELECT ${cols} FROM ${this.tableName}`;
|
|
789
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
790
|
+
if (orderSql) query += ` ${orderSql}`;
|
|
791
|
+
query += ` LIMIT ${take ?? 'ALL'} OFFSET ${skip}`;
|
|
792
|
+
} else {
|
|
793
|
+
// mssql requires ORDER BY before OFFSET/FETCH
|
|
794
|
+
query = `SELECT ${cols} FROM ${this.tableName}${this.nolock}`;
|
|
795
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
796
|
+
query += ` ${orderSql || 'ORDER BY (SELECT NULL)'}`;
|
|
797
|
+
query += ` OFFSET ${skip} ROWS`;
|
|
798
|
+
if (take !== undefined) {
|
|
799
|
+
query += ` FETCH NEXT ${take} ROWS ONLY`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
query = `SELECT ${cols} FROM ${this.tableName}${this.nolock}`;
|
|
804
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
805
|
+
if (orderSql) query += ` ${orderSql}`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const rows = await this.doExec<T>(query, params);
|
|
809
|
+
|
|
810
|
+
if (args?.include) {
|
|
811
|
+
await resolveIncludes(this.modelName, rows, args.include, this.adapter);
|
|
812
|
+
}
|
|
813
|
+
if (args?.select && typeof args.select === 'object') {
|
|
814
|
+
const relationKeys = getRelationsForModel(this.modelName);
|
|
815
|
+
const hasRelationSelect = Object.keys(args.select).some(k => k === '_count' || relationKeys[k]);
|
|
816
|
+
if (hasRelationSelect) {
|
|
817
|
+
await resolveIncludes(this.modelName, rows, args.select, this.adapter);
|
|
818
|
+
}
|
|
819
|
+
return rows.map((r: any) => projectFields(r, args.select));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return rows;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async findFirst(args?: { where?: any; orderBy?: any; select?: any; include?: any }): Promise<T | null> {
|
|
826
|
+
const rows = await this.findMany({ ...args, take: 1 });
|
|
827
|
+
return rows[0] ?? null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async findUnique(args: { where: any; select?: any; include?: any }): Promise<T | null> {
|
|
831
|
+
return this.findFirst({ where: args.where, select: args.select, include: args.include });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async count(args?: { where?: any }): Promise<number> {
|
|
835
|
+
const params: Record<string, any> = {};
|
|
836
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
|
|
837
|
+
let query = `SELECT COUNT(*) AS cnt FROM ${this.tableName}${this.nolock}`;
|
|
838
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
839
|
+
const rows = await this.doExec<any>(query, params);
|
|
840
|
+
return Number(rows[0]?.cnt ?? rows[0]?.CNT ?? 0);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async create(args: { data: Partial<T> | any; include?: any; select?: any }): Promise<T> {
|
|
844
|
+
const modelRelations = getRelationsForModel(this.modelName);
|
|
845
|
+
const rawData = { ...args.data };
|
|
846
|
+
const relationWrites: Record<string, any> = {};
|
|
847
|
+
|
|
848
|
+
for (const key of Object.keys(rawData)) {
|
|
849
|
+
if (modelRelations[key] && typeof rawData[key] === 'object' && rawData[key] !== null && !(rawData[key] instanceof Date)) {
|
|
850
|
+
relationWrites[key] = rawData[key];
|
|
851
|
+
delete rawData[key];
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const fields = getFieldsForModel(this.modelName);
|
|
856
|
+
const idFieldName = Object.prototype.hasOwnProperty.call(fields, 'id')
|
|
857
|
+
? 'id'
|
|
858
|
+
: Object.keys(fields).find((name) => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
|
|
859
|
+
|
|
860
|
+
const data: any = { ...rawData };
|
|
861
|
+
if (idFieldName) {
|
|
862
|
+
const fieldDef: any = fields[idFieldName];
|
|
863
|
+
const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || fieldDef?.type || '');
|
|
864
|
+
const normalizedType = String(rawType).toLowerCase();
|
|
865
|
+
const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(normalizedType);
|
|
866
|
+
if (isStringType && !data[idFieldName]) data[idFieldName] = generateUUID();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const cols = Object.keys(data).filter(k => data[k] !== undefined);
|
|
870
|
+
const params: Record<string, any> = {};
|
|
871
|
+
const vals: string[] = [];
|
|
872
|
+
|
|
873
|
+
for (const col of cols) {
|
|
874
|
+
const p = `c_${col}`;
|
|
875
|
+
params[p] = data[col];
|
|
876
|
+
vals.push(`@${p}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const query = `INSERT INTO ${this.tableName} (${cols.map(c => quote(c, this.dialect)).join(', ')}) VALUES (${vals.join(', ')})`;
|
|
880
|
+
await this.doExec(query, params);
|
|
881
|
+
|
|
882
|
+
const createdRecord = (await this.findFirst({ where: idFieldName ? { [idFieldName]: data[idFieldName] } : data })) as any;
|
|
883
|
+
|
|
884
|
+
// Handle nested relation writes
|
|
885
|
+
for (const [relKey, relWrite] of Object.entries(relationWrites)) {
|
|
886
|
+
const relation = modelRelations[relKey];
|
|
887
|
+
if (!relation) continue;
|
|
888
|
+
const childClient = this.adapter.table(relation.modelName);
|
|
889
|
+
if (relWrite.create) {
|
|
890
|
+
const createItems = Array.isArray(relWrite.create) ? relWrite.create : [relWrite.create];
|
|
891
|
+
for (const item of createItems) {
|
|
892
|
+
await childClient.create({
|
|
893
|
+
data: {
|
|
894
|
+
...item,
|
|
895
|
+
[relation.foreignKey]: createdRecord[relation.localKey],
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (args.include) {
|
|
903
|
+
await resolveIncludes(this.modelName, [createdRecord], args.include, this.adapter);
|
|
904
|
+
}
|
|
905
|
+
if (args.select) {
|
|
906
|
+
await resolveIncludes(this.modelName, [createdRecord], args.select, this.adapter);
|
|
907
|
+
return projectFields(createdRecord, args.select);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return createdRecord as T;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async createMany(args: { data: Partial<T>[]; skipDuplicates?: boolean }): Promise<{ count: number }> {
|
|
914
|
+
if (args.data.length === 0) return { count: 0 };
|
|
915
|
+
|
|
916
|
+
const firstRow = args.data[0];
|
|
917
|
+
if (firstRow === undefined) return { count: 0 };
|
|
918
|
+
const firstCols = Object.keys(firstRow).filter(k => (firstRow as any)[k] !== undefined);
|
|
919
|
+
|
|
920
|
+
// Bulk INSERT for the common case (same columns, no skipDuplicates)
|
|
921
|
+
if (firstCols.length > 0 && !args.skipDuplicates) {
|
|
922
|
+
const params: Record<string, any> = {};
|
|
923
|
+
const rowPlaceholders: string[] = [];
|
|
924
|
+
|
|
925
|
+
for (let r = 0; r < args.data.length; r++) {
|
|
926
|
+
const row = args.data[r] as any;
|
|
927
|
+
const vals = firstCols.map(col => {
|
|
928
|
+
const p = `r${r}_${col}`;
|
|
929
|
+
params[p] = row[col] ?? null;
|
|
930
|
+
return `@${p}`;
|
|
931
|
+
});
|
|
932
|
+
rowPlaceholders.push(`(${vals.join(', ')})`);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const query = `INSERT INTO ${this.tableName} (${firstCols.map(c => quote(c, this.dialect)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
|
|
936
|
+
try {
|
|
937
|
+
await this.doExecuteRaw(query, params);
|
|
938
|
+
return { count: args.data.length };
|
|
939
|
+
} catch {
|
|
940
|
+
// fallback below if bulk fails (e.g. mixed column sets)
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Row-by-row fallback
|
|
945
|
+
let count = 0;
|
|
946
|
+
for (const row of args.data) {
|
|
947
|
+
try { await this.create({ data: row }); count++; }
|
|
948
|
+
catch (e) { if (!args.skipDuplicates) throw e; }
|
|
949
|
+
}
|
|
950
|
+
return { count };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async update(args: { where: any; data: Partial<T> | any; include?: any; select?: any }): Promise<T> {
|
|
954
|
+
const modelRelations = getRelationsForModel(this.modelName);
|
|
955
|
+
const rawData = { ...args.data };
|
|
956
|
+
const relationWrites: Record<string, any> = {};
|
|
957
|
+
|
|
958
|
+
for (const key of Object.keys(rawData)) {
|
|
959
|
+
if (modelRelations[key] && typeof rawData[key] === 'object' && rawData[key] !== null && !(rawData[key] instanceof Date)) {
|
|
960
|
+
relationWrites[key] = rawData[key];
|
|
961
|
+
delete rawData[key];
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const setCols = Object.keys(rawData).filter(k => rawData[k] !== undefined);
|
|
966
|
+
if (setCols.length > 0) {
|
|
967
|
+
const params: Record<string, any> = {};
|
|
968
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
|
|
969
|
+
const sets: string[] = [];
|
|
970
|
+
for (const col of setCols) {
|
|
971
|
+
appendUpdateSet(sets, params, col, rawData[col], this.dialect);
|
|
972
|
+
}
|
|
973
|
+
if (sets.length > 0) {
|
|
974
|
+
const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
975
|
+
await this.doExecuteRaw(query, params);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const updatedRecord = (await this.findFirst({ where: args.where })) as any;
|
|
980
|
+
|
|
981
|
+
// Handle nested writes
|
|
982
|
+
for (const [relKey, relWrite] of Object.entries(relationWrites)) {
|
|
983
|
+
const relation = modelRelations[relKey];
|
|
984
|
+
if (!relation) continue;
|
|
985
|
+
const childClient = this.adapter.table(relation.modelName);
|
|
986
|
+
if (relWrite.create) {
|
|
987
|
+
const createItems = Array.isArray(relWrite.create) ? relWrite.create : [relWrite.create];
|
|
988
|
+
for (const item of createItems) {
|
|
989
|
+
await childClient.create({
|
|
990
|
+
data: {
|
|
991
|
+
...item,
|
|
992
|
+
[relation.foreignKey]: updatedRecord[relation.localKey],
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (relWrite.update) {
|
|
998
|
+
const updateObj = relWrite.update;
|
|
999
|
+
await childClient.update({
|
|
1000
|
+
where: updateObj.where,
|
|
1001
|
+
data: updateObj.data,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
if (relWrite.disconnect) {
|
|
1005
|
+
const disc = relWrite.disconnect;
|
|
1006
|
+
await childClient.updateMany({
|
|
1007
|
+
where: disc,
|
|
1008
|
+
data: { [relation.foreignKey]: null },
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (args.include) {
|
|
1014
|
+
await resolveIncludes(this.modelName, [updatedRecord], args.include, this.adapter);
|
|
1015
|
+
}
|
|
1016
|
+
if (args.select) {
|
|
1017
|
+
await resolveIncludes(this.modelName, [updatedRecord], args.select, this.adapter);
|
|
1018
|
+
return projectFields(updatedRecord, args.select);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return updatedRecord as T;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
|
|
1025
|
+
const params: Record<string, any> = {};
|
|
1026
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
|
|
1027
|
+
const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
|
|
1028
|
+
const sets: string[] = [];
|
|
1029
|
+
|
|
1030
|
+
for (const col of setCols) {
|
|
1031
|
+
appendUpdateSet(sets, params, col, (args.data as any)[col], this.dialect);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (sets.length === 0) return { count: 0 };
|
|
1035
|
+
|
|
1036
|
+
const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
1037
|
+
const count = await this.doExecuteRaw(query, params);
|
|
1038
|
+
return { count };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async delete(args: { where: any }): Promise<T> {
|
|
1042
|
+
const existing = await this.findFirst({ where: args.where });
|
|
1043
|
+
const params: Record<string, any> = {};
|
|
1044
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
|
|
1045
|
+
await this.doExecuteRaw(`DELETE FROM ${this.tableName} WHERE ${whereSql}`, params);
|
|
1046
|
+
return existing as T;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async deleteMany(args?: { where?: any }): Promise<{ count: number }> {
|
|
1050
|
+
const params: Record<string, any> = {};
|
|
1051
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
|
|
1052
|
+
const query = `DELETE FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
1053
|
+
const count = await this.doExecuteRaw(query, params);
|
|
1054
|
+
return { count };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async upsert(args: { where: any; create: Partial<T>; update: Partial<T> }): Promise<T> {
|
|
1058
|
+
const existing = await this.findFirst({ where: args.where });
|
|
1059
|
+
return existing
|
|
1060
|
+
? this.update({ where: args.where, data: args.update })
|
|
1061
|
+
: this.create({ data: args.create });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async aggregate(args: any): Promise<any> {
|
|
1065
|
+
const params: Record<string, any> = {};
|
|
1066
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
|
|
1067
|
+
const aggs: string[] = [];
|
|
1068
|
+
|
|
1069
|
+
if (args._count) {
|
|
1070
|
+
aggs.push('COUNT(*) AS cnt_all');
|
|
1071
|
+
if (typeof args._count === 'object') {
|
|
1072
|
+
for (const f of selectedAggregateFields(args._count)) {
|
|
1073
|
+
if (f !== '_all') aggs.push(`COUNT(${quote(f, this.dialect)}) AS cnt_${sanitizeParamName(f)}`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (args._sum) for (const f of selectedAggregateFields(args._sum)) aggs.push(`SUM(${quote(f, this.dialect)}) AS sum_${sanitizeParamName(f)}`);
|
|
1078
|
+
if (args._avg) for (const f of selectedAggregateFields(args._avg)) aggs.push(`AVG(${quote(f, this.dialect)}) AS avg_${sanitizeParamName(f)}`);
|
|
1079
|
+
if (args._min) for (const f of selectedAggregateFields(args._min)) aggs.push(`MIN(${quote(f, this.dialect)}) AS min_${sanitizeParamName(f)}`);
|
|
1080
|
+
if (args._max) for (const f of selectedAggregateFields(args._max)) aggs.push(`MAX(${quote(f, this.dialect)}) AS max_${sanitizeParamName(f)}`);
|
|
1081
|
+
|
|
1082
|
+
if (aggs.length === 0) throw new Error('Aggregate requires at least one aggregator field');
|
|
1083
|
+
|
|
1084
|
+
const query = `SELECT ${aggs.join(', ')} FROM ${this.tableName}${this.nolock}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
1085
|
+
const rows = await this.doExec<any>(query, params);
|
|
1086
|
+
const row = rows[0] || {};
|
|
1087
|
+
|
|
1088
|
+
const result: any = {};
|
|
1089
|
+
if (args._count) {
|
|
1090
|
+
const allCount = Number(row.cnt_all ?? row._count ?? 0);
|
|
1091
|
+
result._count = { _all: allCount };
|
|
1092
|
+
if (typeof args._count === 'object') {
|
|
1093
|
+
for (const f of selectedAggregateFields(args._count)) {
|
|
1094
|
+
if (f === '_all') continue;
|
|
1095
|
+
const k = `cnt_${sanitizeParamName(f)}`;
|
|
1096
|
+
result._count[f] = row[k] !== undefined ? Number(row[k]) : allCount;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (args._sum) {
|
|
1101
|
+
result._sum = {};
|
|
1102
|
+
for (const f of selectedAggregateFields(args._sum)) {
|
|
1103
|
+
const k = `sum_${sanitizeParamName(f)}`;
|
|
1104
|
+
result._sum[f] = row[k] !== null && row[k] !== undefined ? Number(row[k]) : null;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (args._avg) {
|
|
1108
|
+
result._avg = {};
|
|
1109
|
+
for (const f of selectedAggregateFields(args._avg)) {
|
|
1110
|
+
const k = `avg_${sanitizeParamName(f)}`;
|
|
1111
|
+
result._avg[f] = row[k] !== null && row[k] !== undefined ? Number(row[k]) : null;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (args._min) {
|
|
1115
|
+
result._min = {};
|
|
1116
|
+
for (const f of selectedAggregateFields(args._min)) {
|
|
1117
|
+
const k = `min_${sanitizeParamName(f)}`;
|
|
1118
|
+
result._min[f] = row[k] ?? null;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (args._max) {
|
|
1122
|
+
result._max = {};
|
|
1123
|
+
for (const f of selectedAggregateFields(args._max)) {
|
|
1124
|
+
const k = `max_${sanitizeParamName(f)}`;
|
|
1125
|
+
result._max[f] = row[k] ?? null;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return result;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async groupBy(args: any): Promise<any[]> {
|
|
1133
|
+
const params: Record<string, any> = {};
|
|
1134
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
1135
|
+
const byFields = normalizeByFields(args?.by);
|
|
1136
|
+
if (byFields.length === 0) throw new Error("groupBy requires 'by' fields");
|
|
1137
|
+
const byCols = byFields.map((b: string) => quote(b, this.dialect)).join(', ');
|
|
1138
|
+
const aggs: string[] = ['COUNT(*) AS _count'];
|
|
1139
|
+
if (args?._sum) for (const f of selectedAggregateFields(args._sum)) aggs.push(`SUM(${quote(f, this.dialect)}) AS _sum_${f}`);
|
|
1140
|
+
if (args?._avg) for (const f of selectedAggregateFields(args._avg)) aggs.push(`AVG(${quote(f, this.dialect)}) AS _avg_${f}`);
|
|
1141
|
+
if (args?._min) for (const f of selectedAggregateFields(args._min)) aggs.push(`MIN(${quote(f, this.dialect)}) AS _min_${f}`);
|
|
1142
|
+
if (args?._max) for (const f of selectedAggregateFields(args._max)) aggs.push(`MAX(${quote(f, this.dialect)}) AS _max_${f}`);
|
|
1143
|
+
let query = `SELECT ${byCols}, ${aggs.join(', ')} FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''} GROUP BY ${byCols}`;
|
|
1144
|
+
const orderSql = buildOrderBy(args?.orderBy, this.dialect);
|
|
1145
|
+
const hasSkip = args?.skip !== undefined && args?.skip !== null;
|
|
1146
|
+
const hasTake = args?.take !== undefined && args?.take !== null;
|
|
1147
|
+
if (orderSql) query += ` ${orderSql}`;
|
|
1148
|
+
else if (hasSkip || hasTake) query += ` ORDER BY ${byCols}`;
|
|
1149
|
+
if (hasSkip || hasTake) {
|
|
1150
|
+
const skip = toNonNegativeInt(args?.skip);
|
|
1151
|
+
const take = toNonNegativeInt(args?.take, 1);
|
|
1152
|
+
if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
|
|
1153
|
+
if (hasTake) query += ` LIMIT ${take}`;
|
|
1154
|
+
query += ` OFFSET ${skip}`;
|
|
1155
|
+
} else {
|
|
1156
|
+
query += ` OFFSET ${skip} ROWS`;
|
|
1157
|
+
if (hasTake) query += ` FETCH NEXT ${take} ROWS ONLY`;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return this.doExec(query, params);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Runs similarity search in NBase and hydrates the matching rows from the
|
|
1165
|
+
* relational table, keeping the `{ ...row, distance }` shape the other
|
|
1166
|
+
* engines return.
|
|
1167
|
+
*/
|
|
1168
|
+
private async searchViaNBase(
|
|
1169
|
+
nbase: { client: NBaseVectorClient; config: NBaseAdapterConfig },
|
|
1170
|
+
args: { vector: number[]; take?: number; where?: any },
|
|
1171
|
+
metric: 'cosine' | 'euclidean' | 'dot',
|
|
1172
|
+
take: number,
|
|
1173
|
+
): Promise<(T & { distance: number })[]> {
|
|
1174
|
+
const { client, config } = nbase;
|
|
1175
|
+
const idField = config.idField ?? 'an5Id';
|
|
1176
|
+
const modelField = config.modelField ?? 'an5Model';
|
|
1177
|
+
|
|
1178
|
+
const results = await client.search(args.vector, {
|
|
1179
|
+
k: take,
|
|
1180
|
+
// NBase implements cosine and euclidean; a dot-product request falls back
|
|
1181
|
+
// to cosine, which still orders the results sensibly.
|
|
1182
|
+
distanceMetric: metric === 'euclidean' ? 'euclidean' : 'cosine',
|
|
1183
|
+
includeMetadata: true,
|
|
1184
|
+
...(config.method ? { method: config.method } : {}),
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// Map each hit back to its row id. Ids are written as `Model:id`, and the
|
|
1188
|
+
// metadata key is honoured for projects that index vectors themselves.
|
|
1189
|
+
const ids: string[] = [];
|
|
1190
|
+
const distances = new Map<string, number>();
|
|
1191
|
+
for (const hit of results) {
|
|
1192
|
+
const fromMetadata = hit.metadata?.[idField];
|
|
1193
|
+
const rawId = fromMetadata !== undefined && fromMetadata !== null
|
|
1194
|
+
? String(fromMetadata)
|
|
1195
|
+
: parseVectorId(hit.id).id;
|
|
1196
|
+
if (!rawId) continue;
|
|
1197
|
+
const model = hit.metadata?.[modelField] ?? parseVectorId(hit.id).model;
|
|
1198
|
+
ids.push(model === this.modelName ? rawId : `${model}:${rawId}`);
|
|
1199
|
+
distances.set(ids[ids.length - 1] as string, hit.dist);
|
|
1200
|
+
}
|
|
1201
|
+
if (ids.length === 0) return [];
|
|
1202
|
+
|
|
1203
|
+
const pk = this.primaryKeyColumn();
|
|
1204
|
+
const inList = ids.map((_, i) => `@nbase_id_${i}`).join(', ');
|
|
1205
|
+
const idParams: Record<string, any> = {};
|
|
1206
|
+
ids.forEach((id, i) => { idParams[`nbase_id_${i}`] = id; });
|
|
1207
|
+
idParams.take = take;
|
|
1208
|
+
|
|
1209
|
+
const selectColumns = this.modelFields().map((f) => quote(f, this.dialect)).join(', ');
|
|
1210
|
+
const columns = selectColumns ? `${selectColumns}, ${quote(pk, this.dialect)} AS __an5_nbase_key` : `${quote(pk, this.dialect)} AS __an5_nbase_key`;
|
|
1211
|
+
const query = `SELECT ${columns} FROM ${this.tableName} WHERE ${quote(pk, this.dialect)} IN (${inList})`;
|
|
1212
|
+
|
|
1213
|
+
const rows = await this.adapter.exec<Record<string, any>>(query, idParams);
|
|
1214
|
+
const byKey = new Map<string, Record<string, any>>();
|
|
1215
|
+
for (const row of rows ?? []) {
|
|
1216
|
+
byKey.set(String(row.__an5_nbase_key), row);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const hydrated: (T & { distance: number })[] = [];
|
|
1220
|
+
for (const id of ids) {
|
|
1221
|
+
const row = byKey.get(id);
|
|
1222
|
+
if (!row) continue;
|
|
1223
|
+
const { __an5_nbase_key, ...rest } = row;
|
|
1224
|
+
hydrated.push({ ...(rest as T), distance: distances.get(id) ?? 0 } as T & { distance: number });
|
|
1225
|
+
if (hydrated.length >= take) break;
|
|
1226
|
+
}
|
|
1227
|
+
return hydrated;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Vector search for an adapter configured with only an NBase connection
|
|
1232
|
+
* string. The hit id and metadata are the result, since there is no
|
|
1233
|
+
* relational row to hydrate.
|
|
1234
|
+
*/
|
|
1235
|
+
private async searchViaNBaseOnly(
|
|
1236
|
+
nbase: { client: NBaseVectorClient; config: NBaseAdapterConfig },
|
|
1237
|
+
args: { vector: number[]; take?: number },
|
|
1238
|
+
metric: 'cosine' | 'euclidean' | 'dot',
|
|
1239
|
+
take: number,
|
|
1240
|
+
): Promise<Array<Record<string, unknown> & { distance: number }>> {
|
|
1241
|
+
const { client, config } = nbase;
|
|
1242
|
+
const idField = config.idField ?? 'an5Id';
|
|
1243
|
+
const results = await client.search(args.vector, {
|
|
1244
|
+
k: take,
|
|
1245
|
+
distanceMetric: metric === 'euclidean' ? 'euclidean' : 'cosine',
|
|
1246
|
+
includeMetadata: true,
|
|
1247
|
+
...(config.method ? { method: config.method } : {}),
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
return results.map((hit) => {
|
|
1251
|
+
const metadata = { ...(hit.metadata ?? {}) };
|
|
1252
|
+
if (metadata[idField] === undefined) {
|
|
1253
|
+
const parsed = parseVectorId(hit.id);
|
|
1254
|
+
if (parsed.id) metadata[idField] = parsed.id;
|
|
1255
|
+
if (parsed.model) metadata[config.modelField ?? 'an5Model'] = parsed.model;
|
|
1256
|
+
}
|
|
1257
|
+
return { id: hit.id, ...metadata, distance: hit.dist };
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/** The primary key column registered for this model, falling back to `id`. */
|
|
1262
|
+
private primaryKeyColumn(): string {
|
|
1263
|
+
const fields = this.modelFields();
|
|
1264
|
+
const marked = fields.find((f) => f === 'id');
|
|
1265
|
+
return marked ?? fields[0] ?? 'id';
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/** Scalar column names known for this model from the generated metadata. */
|
|
1269
|
+
private modelFields(): string[] {
|
|
1270
|
+
const fields = getFieldsForModel(this.modelName);
|
|
1271
|
+
if (fields && typeof fields === 'object' && !Array.isArray(fields)) {
|
|
1272
|
+
return Object.keys(fields as Record<string, unknown>);
|
|
1273
|
+
}
|
|
1274
|
+
return [];
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Pushes a table's embedding column into NBase so it can be searched there.
|
|
1279
|
+
*
|
|
1280
|
+
* Each vector keeps the row id in its metadata, which is what lets
|
|
1281
|
+
* `vectorSearch` return real rows instead of bare vector ids.
|
|
1282
|
+
*/
|
|
1283
|
+
async indexVectorsInNBase(args: {
|
|
1284
|
+
vectorField?: string;
|
|
1285
|
+
where?: any;
|
|
1286
|
+
take?: number;
|
|
1287
|
+
batchSize?: number;
|
|
1288
|
+
} = {}): Promise<{ indexed: number }> {
|
|
1289
|
+
const nbase = this.adapter.nbaseClient?.() ?? null;
|
|
1290
|
+
if (!nbase) {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
'No NBase instance configured. Use a nbase:// connection string, e.g. createAn5Adapter({ connectionString: "nbase://localhost:1307" }).',
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
if (this.adapter.nbaseOnly) {
|
|
1296
|
+
throw new Error(
|
|
1297
|
+
'indexVectorsInNBase reads the vectors from a table, but this adapter was created with an nbase:// connection string and has no database. ' +
|
|
1298
|
+
'Create the adapter with your database connection string to index, or add the vectors with the NBase client directly.',
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
const vectorField = args.vectorField || 'embedding';
|
|
1302
|
+
const batchSize = args.batchSize ?? 200;
|
|
1303
|
+
const pk = this.primaryKeyColumn();
|
|
1304
|
+
const rows = await this.findMany({ ...(args.where ? { where: args.where } : {}), ...(args.take ? { take: args.take } : {}) });
|
|
1305
|
+
|
|
1306
|
+
let indexed = 0;
|
|
1307
|
+
for (let i = 0; i < rows.length; i += batchSize) {
|
|
1308
|
+
const batch = rows.slice(i, i + batchSize);
|
|
1309
|
+
const vectors = batch
|
|
1310
|
+
.map((row) => {
|
|
1311
|
+
const raw = (row as Record<string, unknown>)[vectorField];
|
|
1312
|
+
if (raw === undefined || raw === null) return null;
|
|
1313
|
+
let vector: number[];
|
|
1314
|
+
try {
|
|
1315
|
+
vector = typeof raw === 'string' ? JSON.parse(raw) : (raw as number[]);
|
|
1316
|
+
} catch {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
if (!Array.isArray(vector) || vector.length === 0) return null;
|
|
1320
|
+
return {
|
|
1321
|
+
id: buildVectorId(this.modelName, String((row as Record<string, unknown>)[pk])),
|
|
1322
|
+
vector,
|
|
1323
|
+
metadata: {
|
|
1324
|
+
[nbase.config.idField ?? 'an5Id']: String((row as Record<string, unknown>)[pk]),
|
|
1325
|
+
[nbase.config.modelField ?? 'an5Model']: this.modelName,
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
1328
|
+
})
|
|
1329
|
+
.filter((v): v is NonNullable<typeof v> => v !== null);
|
|
1330
|
+
|
|
1331
|
+
if (vectors.length === 0) continue;
|
|
1332
|
+
const result = await nbase.client.addVectors(vectors);
|
|
1333
|
+
indexed += result.count;
|
|
1334
|
+
}
|
|
1335
|
+
return { indexed };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async vectorSearch(args: {
|
|
1339
|
+
vector: number[];
|
|
1340
|
+
take?: number;
|
|
1341
|
+
where?: any;
|
|
1342
|
+
vectorField?: string;
|
|
1343
|
+
distanceMetric?: 'cosine' | 'euclidean' | 'dot';
|
|
1344
|
+
vectorElementType?: 'float32' | 'float16' | 'uint8';
|
|
1345
|
+
}): Promise<(T & { distance: number })[]> {
|
|
1346
|
+
const vectorField = args.vectorField || 'embedding';
|
|
1347
|
+
const METRICS = ['cosine', 'euclidean', 'dot'];
|
|
1348
|
+
const ELEMENT_TYPES = ['float32', 'float16', 'uint8'];
|
|
1349
|
+
const metric = METRICS.includes(args.distanceMetric as string) ? args.distanceMetric! : 'cosine';
|
|
1350
|
+
const elementType = ELEMENT_TYPES.includes(args.vectorElementType as string) ? args.vectorElementType! : 'float32';
|
|
1351
|
+
const take = args.take ?? 10;
|
|
1352
|
+
const dim = Array.isArray(args.vector) ? args.vector.length : 0;
|
|
1353
|
+
const vectorJson = JSON.stringify(args.vector);
|
|
1354
|
+
const col = quote(vectorField, this.dialect);
|
|
1355
|
+
const params: Record<string, any> = { query_vector: vectorJson };
|
|
1356
|
+
|
|
1357
|
+
// 0. NBase, when the project configured one: the vectors live in the vector
|
|
1358
|
+
// database, so the search never loads the table into memory.
|
|
1359
|
+
const nbase = this.adapter.nbaseClient?.() ?? null;
|
|
1360
|
+
if (nbase) {
|
|
1361
|
+
try {
|
|
1362
|
+
// With an NBase-only connection string there is no table to read rows
|
|
1363
|
+
// from, so the hits are returned as the vector metadata itself.
|
|
1364
|
+
if (this.adapter.nbaseOnly) {
|
|
1365
|
+
return (await this.searchViaNBaseOnly(nbase, args, metric, take)) as (T & { distance: number })[];
|
|
1366
|
+
}
|
|
1367
|
+
return await this.searchViaNBase(nbase, args, metric, take);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
if (!(err instanceof NBaseError)) throw err;
|
|
1370
|
+
// A NBase outage must not take vector search down with it; fall through
|
|
1371
|
+
// to the database instead of failing the query.
|
|
1372
|
+
console.warn(`[an5] NBase vector search unavailable (${err.message}); falling back to ${this.dialect}.`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// 1. Try native dialect execution
|
|
1377
|
+
if (this.dialect === 'postgres') {
|
|
1378
|
+
const op = metric === 'cosine' ? '<=>' : metric === 'dot' ? '<#>' : '<->';
|
|
1379
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
|
|
1380
|
+
const query = `SELECT *, (${col} ${op} @query_vector::vector) AS distance FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''} ORDER BY distance ASC LIMIT ${take}`;
|
|
1381
|
+
try {
|
|
1382
|
+
return await this.doExec<(T & { distance: number })>(query, params);
|
|
1383
|
+
} catch (err: any) {
|
|
1384
|
+
const msg = String(err?.message || '').toLowerCase();
|
|
1385
|
+
if (!msg.includes('vector') && !msg.includes('operator does not exist')) throw err;
|
|
1386
|
+
}
|
|
1387
|
+
} else if (this.dialect === 'mssql') {
|
|
1388
|
+
const distFn = metric === 'cosine' ? 'cosine' : metric === 'euclidean' ? 'euclidean' : 'dot';
|
|
1389
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
|
|
1390
|
+
const query = `SELECT TOP (${take}) *, VECTOR_DISTANCE('${distFn}', ${col}, CAST(@query_vector AS VECTOR(${dim}, ${elementType}))) AS distance FROM ${this.tableName}${this.nolock}${whereSql ? ` WHERE ${whereSql}` : ''} ORDER BY distance ASC`;
|
|
1391
|
+
try {
|
|
1392
|
+
return await this.doExec<(T & { distance: number })>(query, params);
|
|
1393
|
+
} catch (err: any) {
|
|
1394
|
+
const msg = String(err?.message || '').toLowerCase();
|
|
1395
|
+
const isUnsupported =
|
|
1396
|
+
msg.includes('vector_distance') ||
|
|
1397
|
+
msg.includes('syntax near') ||
|
|
1398
|
+
msg.includes('not a recognized built-in function') ||
|
|
1399
|
+
msg.includes('not a defined system type') ||
|
|
1400
|
+
msg.includes('type "vector"') ||
|
|
1401
|
+
msg.includes('pgvector') ||
|
|
1402
|
+
msg.includes('operator does not exist') ||
|
|
1403
|
+
msg.includes('limit of 1998') ||
|
|
1404
|
+
err?.number === 195 ||
|
|
1405
|
+
err?.number === 102 ||
|
|
1406
|
+
err?.number === 319 ||
|
|
1407
|
+
err?.originalError?.number === 319;
|
|
1408
|
+
if (!isUnsupported) throw err;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// 2. Fallback: in-memory similarity computation when native vector support is unavailable
|
|
1413
|
+
const rows = await this.findMany({ where: args.where });
|
|
1414
|
+
|
|
1415
|
+
const scored: { row: T; dist: number }[] = [];
|
|
1416
|
+
for (const row of rows) {
|
|
1417
|
+
const raw = (row as any)[vectorField];
|
|
1418
|
+
if (!raw) continue;
|
|
1419
|
+
let vec: number[] = [];
|
|
1420
|
+
try { vec = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { continue; }
|
|
1421
|
+
if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
|
|
1422
|
+
|
|
1423
|
+
let dot = 0, m1 = 0, m2 = 0;
|
|
1424
|
+
for (let i = 0; i < args.vector.length; i++) {
|
|
1425
|
+
const av = args.vector[i] ?? 0;
|
|
1426
|
+
const bv = vec[i] ?? 0;
|
|
1427
|
+
dot += av * bv;
|
|
1428
|
+
m1 += av ** 2;
|
|
1429
|
+
m2 += bv ** 2;
|
|
1430
|
+
}
|
|
1431
|
+
const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
|
|
1432
|
+
const dist = metric === 'cosine'
|
|
1433
|
+
? 1 - cosine
|
|
1434
|
+
: metric === 'dot'
|
|
1435
|
+
? -dot
|
|
1436
|
+
: Math.sqrt(args.vector.reduce((s, v, i) => s + (v - (vec[i] ?? 0)) ** 2, 0));
|
|
1437
|
+
scored.push({ row, dist });
|
|
1438
|
+
}
|
|
1439
|
+
scored.sort((a, b) => a.dist - b.dist);
|
|
1440
|
+
return scored.slice(0, take).map(s => ({ ...s.row as any, distance: s.dist }));
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────────
|
|
1445
|
+
|
|
1446
|
+
export function createAn5Adapter(config: AnyAdapterConfig): AnyAdapter;
|
|
1447
|
+
export function createAn5Adapter<TModels extends object>(
|
|
1448
|
+
config: AnyAdapterConfig
|
|
1449
|
+
): import('./typed').TypedAn5Adapter<TModels>;
|
|
1450
|
+
export function createAn5Adapter(config: AnyAdapterConfig): AnyAdapter {
|
|
1451
|
+
if (isSheetsConfig(config)) return new An5SheetsAdapter(config);
|
|
1452
|
+
if (config.connectionString && config.connectionString.trim().startsWith('googlesheets://')) {
|
|
1453
|
+
return new An5SheetsAdapter(parseSheetsConnectionString(config.connectionString));
|
|
1454
|
+
}
|
|
1455
|
+
return new An5Adapter(config);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
export const createAdapter = createAn5Adapter;
|
|
1459
|
+
|
|
1460
|
+
export function createBrowserSqliteAdapter(config: any): An5Adapter {
|
|
1461
|
+
const { SqliteBrowserEngine } = require('./sqlite/browserEngine.js');
|
|
1462
|
+
return new An5Adapter({ engine: new SqliteBrowserEngine(config) });
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function normalizeAffectedCount(result: any): number {
|
|
1466
|
+
if (typeof result === 'number') return result;
|
|
1467
|
+
if (!result) return 0;
|
|
1468
|
+
if (Array.isArray(result.rowsAffected)) return Number(result.rowsAffected[0] ?? 0);
|
|
1469
|
+
if (typeof result.rowsAffected === 'number') return result.rowsAffected;
|
|
1470
|
+
if (typeof result.count === 'number') return result.count;
|
|
1471
|
+
return 0;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
export function executorFromAdapter(adapterLike: any): any {
|
|
1475
|
+
if (!adapterLike) return adapterLike;
|
|
1476
|
+
if (typeof adapterLike === 'function' && adapterLike.executeRaw) {
|
|
1477
|
+
return adapterLike;
|
|
1478
|
+
}
|
|
1479
|
+
return Object.assign(
|
|
1480
|
+
async (queryText: string, params?: Record<string, any>) => {
|
|
1481
|
+
return typeof adapterLike === 'function'
|
|
1482
|
+
? adapterLike(queryText, params)
|
|
1483
|
+
: adapterLike.exec(queryText, params);
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
executeRaw: async (queryText: string, params?: Record<string, any>) => {
|
|
1487
|
+
if (typeof adapterLike._executeRaw === 'function') {
|
|
1488
|
+
return adapterLike._executeRaw(queryText, params);
|
|
1489
|
+
}
|
|
1490
|
+
if (typeof adapterLike.executeRaw === 'function') {
|
|
1491
|
+
return adapterLike.executeRaw(queryText, params);
|
|
1492
|
+
}
|
|
1493
|
+
const res = typeof adapterLike === 'function'
|
|
1494
|
+
? await adapterLike(queryText, params)
|
|
1495
|
+
: await adapterLike.exec(queryText, params);
|
|
1496
|
+
return normalizeAffectedCount(res);
|
|
1497
|
+
},
|
|
1498
|
+
transaction: adapterLike.$transaction
|
|
1499
|
+
? async (fn: (txExecutor: any) => Promise<any>, options?: { timeout?: number }) => {
|
|
1500
|
+
return adapterLike.$transaction(async (tx: any) => fn(executorFromAdapter(tx)), options);
|
|
1501
|
+
}
|
|
1502
|
+
: undefined,
|
|
1503
|
+
beginTransaction: adapterLike.$begin
|
|
1504
|
+
? async () => {
|
|
1505
|
+
const tx = await adapterLike.$begin();
|
|
1506
|
+
return {
|
|
1507
|
+
executor: executorFromAdapter(tx),
|
|
1508
|
+
commit: () => tx.$commit(),
|
|
1509
|
+
rollback: () => tx.$rollback(),
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
: undefined,
|
|
1513
|
+
}
|
|
1514
|
+
);
|
|
1515
|
+
}
|