@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.
Files changed (99) hide show
  1. package/README.md +283 -305
  2. package/dist/an5Adapter.d.ts +142 -7
  3. package/dist/an5Adapter.js +998 -68
  4. package/dist/base/metadata.d.ts +10 -0
  5. package/dist/base/metadata.js +107 -0
  6. package/dist/base/sql.d.ts +19 -1
  7. package/dist/base/sql.js +152 -24
  8. package/dist/base/types.d.ts +10 -1
  9. package/dist/browser.d.ts +4 -1
  10. package/dist/browser.js +9 -2
  11. package/dist/googlesheets/adapter.d.ts +1 -0
  12. package/dist/googlesheets/adapter.js +9 -2
  13. package/dist/googlesheets/config.d.ts +4 -4
  14. package/dist/googlesheets/helpers.js +2 -0
  15. package/dist/googlesheets/parseConnectionString.js +5 -2
  16. package/dist/googlesheets/sqlExecutor.js +29 -24
  17. package/dist/googlesheets/tableClient.js +14 -5
  18. package/dist/index.d.ts +5 -1
  19. package/dist/index.js +15 -1
  20. package/dist/mssql/engine.js +8 -4
  21. package/dist/mysql/engine.js +1 -1
  22. package/dist/nbase/client.d.ts +92 -0
  23. package/dist/nbase/client.js +147 -0
  24. package/dist/nbase/index.d.ts +47 -0
  25. package/dist/nbase/index.js +80 -0
  26. package/dist/postgres/engine.js +1 -1
  27. package/dist/sqlite/browserEngine.d.ts +27 -0
  28. package/dist/sqlite/browserEngine.js +161 -0
  29. package/dist/sqlite/engine.js +15 -3
  30. package/dist/sqlite/index.d.ts +1 -0
  31. package/dist/sqlite/index.js +1 -0
  32. package/dist/typed.d.ts +5 -0
  33. package/dist/typed.js +2 -0
  34. package/dotnet/An5Adapter.cs +1112 -275
  35. package/dotnet/Base/Core.cs +380 -82
  36. package/dotnet/Mssql/MssqlEngine.cs +145 -144
  37. package/dotnet/Postgres/PostgresEngine.cs +149 -121
  38. package/dotnet/Sqlite/SqliteEngine.cs +214 -0
  39. package/golang/an5_adapter.go +314 -0
  40. package/golang/base/base.go +100 -0
  41. package/golang/base/metadata.go +116 -0
  42. package/golang/base/where.go +311 -0
  43. package/golang/go.mod +3 -0
  44. package/golang/mssql/mssql.go +32 -0
  45. package/golang/postgres/postgres.go +37 -0
  46. package/golang/sqlite/sqlite.go +14 -0
  47. package/golang/sqlite_test.go +42 -0
  48. package/golang/table_client.go +1329 -0
  49. package/package.json +26 -9
  50. package/python/an5_adapter.py +184 -97
  51. package/python/base/__init__.py +3 -3
  52. package/python/base/dialects.py +11 -8
  53. package/python/base/metadata.py +59 -10
  54. package/python/base/sql.py +202 -121
  55. package/python/mssql/__init__.py +1 -1
  56. package/python/mssql/provider.py +62 -62
  57. package/python/postgres/__init__.py +1 -1
  58. package/python/postgres/provider.py +15 -15
  59. package/python/sqlite/__init__.py +3 -0
  60. package/python/sqlite/provider.py +83 -0
  61. package/python/table_client.py +636 -216
  62. package/rust/Cargo.toml +31 -0
  63. package/rust/README.md +71 -0
  64. package/rust/src/adapter.rs +1312 -0
  65. package/rust/src/base/dialect.rs +172 -0
  66. package/rust/src/base/metadata.rs +134 -0
  67. package/rust/src/base/mod.rs +17 -0
  68. package/rust/src/base/where_builder.rs +405 -0
  69. package/rust/src/lib.rs +75 -0
  70. package/typescript/src/an5Adapter.ts +1515 -505
  71. package/typescript/src/base/index.ts +3 -3
  72. package/typescript/src/base/metadata.ts +148 -28
  73. package/typescript/src/base/sql.ts +264 -98
  74. package/typescript/src/base/types.ts +33 -24
  75. package/typescript/src/browser.ts +10 -6
  76. package/typescript/src/config.ts +95 -95
  77. package/typescript/src/googlesheets/adapter.ts +269 -259
  78. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  79. package/typescript/src/googlesheets/config.ts +47 -47
  80. package/typescript/src/googlesheets/helpers.ts +144 -143
  81. package/typescript/src/googlesheets/index.ts +3 -3
  82. package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
  83. package/typescript/src/googlesheets/retry.ts +126 -126
  84. package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
  85. package/typescript/src/googlesheets/tableClient.ts +461 -455
  86. package/typescript/src/googlesheets/types.ts +6 -6
  87. package/typescript/src/index.ts +48 -27
  88. package/typescript/src/mssql/engine.ts +134 -132
  89. package/typescript/src/mssql/index.ts +1 -1
  90. package/typescript/src/mysql/engine.ts +80 -80
  91. package/typescript/src/mysql/index.ts +1 -1
  92. package/typescript/src/nbase/client.ts +214 -0
  93. package/typescript/src/nbase/index.ts +104 -0
  94. package/typescript/src/postgres/engine.ts +87 -87
  95. package/typescript/src/postgres/index.ts +1 -1
  96. package/typescript/src/sqlite/browserEngine.ts +192 -0
  97. package/typescript/src/sqlite/engine.ts +66 -55
  98. package/typescript/src/sqlite/index.ts +3 -1
  99. 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
- import { buildOrderBy, parseWhere, quote } from './base/sql';
11
- import { getFieldsForModel, getModelToTable, setAdapterMetadata, type AdapterMetadata } from './base/metadata';
12
- export type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
13
- export { setAdapterMetadata, type AdapterMetadata } from './base/metadata';
14
-
15
- export type AnyAdapter = An5Adapter | An5SheetsAdapter;
16
- export type AnyAdapterConfig = An5AdapterConfig | An5SheetsAdapterConfig | { connectionString: string };
17
-
18
- function isSheetsConfig(config: AnyAdapterConfig): config is An5SheetsAdapterConfig {
19
- return (config as any).spreadsheetId !== undefined;
20
- }
21
-
22
- // ─── An5Adapter ────────────────────────────────────────────────────────────────────
23
-
24
- export class An5Adapter {
25
- private _engine: QueryEngine | null = null;
26
- private _engineType: 'postgres' | 'mysql' | 'sqlite' | 'mssql' | null = null;
27
- private _engineConfig: An5AdapterConfig | null = null;
28
- sheetsAdapter: An5SheetsAdapter | null = null;
29
-
30
- get dialect(): Dialect {
31
- if (this.sheetsAdapter) return 'googlesheets';
32
- if (this._engine) return this._engine.dialect;
33
- if (this._engineType === 'postgres') return 'postgres';
34
- if (this._engineType === 'mysql') return 'mysql';
35
- if (this._engineType === 'sqlite') return 'sqlite';
36
- return 'mssql';
37
- }
38
-
39
- constructor(adapterConfig: An5AdapterConfig | An5SheetsAdapterConfig) {
40
- if (isSheetsConfig(adapterConfig)) {
41
- this.sheetsAdapter = new An5SheetsAdapter(adapterConfig);
42
- return;
43
- }
44
-
45
- const cs = adapterConfig.connectionString.trim();
46
- if (cs.startsWith('googlesheets://')) {
47
- this.sheetsAdapter = new An5SheetsAdapter(parseSheetsConnectionString(cs));
48
- return;
49
- }
50
-
51
- this._engineConfig = adapterConfig;
52
- if (cs.startsWith('postgres://') || cs.startsWith('postgresql://')) {
53
- this._engineType = 'postgres';
54
- } else if (cs.startsWith('mysql://') || cs.startsWith('mariadb://')) {
55
- this._engineType = 'mysql';
56
- } else if (cs.startsWith('sqlite://') || cs.endsWith('.sqlite') || cs.endsWith('.db')) {
57
- this._engineType = 'sqlite';
58
- } else {
59
- this._engineType = 'mssql';
60
- }
61
- }
62
-
63
- private async requireEngine(): Promise<QueryEngine> {
64
- if (!this._engine && this._engineType) {
65
- switch (this._engineType) {
66
- case 'postgres': {
67
- const { PostgresEngine } = require('./postgres.js');
68
- this._engine = new PostgresEngine(this._engineConfig!);
69
- break;
70
- }
71
- case 'mysql': {
72
- const { MysqlEngine } = require('./mysql.js');
73
- this._engine = new MysqlEngine(this._engineConfig!);
74
- break;
75
- }
76
- case 'sqlite': {
77
- const { SqliteEngine } = require('./sqlite.js');
78
- this._engine = new SqliteEngine(this._engineConfig!);
79
- break;
80
- }
81
- case 'mssql': {
82
- const { MssqlEngine } = require('./mssql.js');
83
- this._engine = new MssqlEngine(this._engineConfig!);
84
- break;
85
- }
86
- }
87
- }
88
- if (!this._engine) throw new Error('SQL engine is not available for Google Sheets adapter');
89
- return this._engine;
90
- }
91
-
92
- private requireSheetsAdapter(): An5SheetsAdapter {
93
- if (!this.sheetsAdapter) throw new Error('Google Sheets methods require a googlesheets:// connection or Sheets config');
94
- return this.sheetsAdapter;
95
- }
96
-
97
- async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
98
- if (this.sheetsAdapter) return this.sheetsAdapter.exec<T>(query, params);
99
- return (await this.requireEngine()).exec<T>(query, params);
100
- }
101
-
102
- /** INTERNAL: used by AdapterTableClient for DML statements needing row count */
103
- async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
104
- return (await this.requireEngine()).executeRaw(query, params);
105
- }
106
-
107
- /** Execute a raw query with positional values → @p_0, @p_1, ... per dialect */
108
- async $queryRawUnsafe<T = any>(query: string, ...values: any[]): Promise<T[]> {
109
- if (this.sheetsAdapter) return this.sheetsAdapter.$queryRawUnsafe<T>(query, ...values);
110
- const engine = await this.requireEngine();
111
- const params: Record<string, any> = {};
112
- values.forEach((v, i) => { params[`p_${i}`] = v; });
113
- let q = query;
114
- if (this.dialect === 'postgres') {
115
- let idx = 0;
116
- q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
117
- } else if (this.dialect === 'mysql') {
118
- let idx = 0;
119
- q = q.replace(/\?/g, () => `@p_${idx++}`);
120
- }
121
- return engine.exec<T>(q, params);
122
- }
123
-
124
- async $executeRaw(query: string, ...values: any[]): Promise<number> {
125
- if (this.sheetsAdapter) return this.sheetsAdapter.$executeRaw(query, ...values);
126
- const engine = await this.requireEngine();
127
- const params: Record<string, any> = {};
128
- values.forEach((v, i) => { params[`p_${i}`] = v; });
129
- let q = query;
130
- if (this.dialect === 'postgres') {
131
- let idx = 0;
132
- q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
133
- } else if (this.dialect === 'mysql') {
134
- let idx = 0;
135
- q = q.replace(/\?/g, () => `@p_${idx++}`);
136
- }
137
- return engine.executeRaw(q, params);
138
- }
139
-
140
- async $executeRawUnsafe(query: string, ...values: any[]): Promise<number> {
141
- return this.$executeRaw(query, ...values);
142
- }
143
-
144
- async $connect(): Promise<void> {
145
- if (this.sheetsAdapter) return this.sheetsAdapter.$connect();
146
- await (await this.requireEngine()).connect();
147
- }
148
-
149
- async $disconnect(): Promise<void> {
150
- if (this.sheetsAdapter) return this.sheetsAdapter.$disconnect();
151
- await (await this.requireEngine()).disconnect();
152
- }
153
-
154
- /** Real transaction with BEGIN / COMMIT / ROLLBACK */
155
- async $transaction<R>(fn: (tx: An5AdapterTx) => Promise<R>, options?: { timeout?: number }): Promise<R>;
156
- async $transaction<R>(list: Promise<R>[]): Promise<R[]>;
157
- async $transaction(fn: any, _options?: any): Promise<any> {
158
- if (this.sheetsAdapter) return this.sheetsAdapter.$transaction(fn, _options);
159
- if (Array.isArray(fn)) return Promise.all(fn);
160
- const engine = await this.requireEngine();
161
- const handle = await engine.beginTransaction();
162
- const txAdapter = new An5AdapterTx(handle, this.dialect);
163
- try {
164
- const result = await fn(txAdapter);
165
- await handle.commit();
166
- return result;
167
- } catch (err) {
168
- await handle.rollback();
169
- throw err;
170
- }
171
- }
172
-
173
- table<T = any>(modelName: string): AdapterTableClient<T> | SheetsTableClient<T> {
174
- if (this.sheetsAdapter) return this.sheetsAdapter.table<T>(modelName);
175
- return new AdapterTableClient<T>(this, modelName);
176
- }
177
-
178
- async readRange<T = any>(range: string): Promise<T[][]> {
179
- return this.requireSheetsAdapter().readRange<T>(range);
180
- }
181
-
182
- async writeRange(range: string, values: any[][]): Promise<void> {
183
- await this.requireSheetsAdapter().writeRange(range, values);
184
- }
185
-
186
- async appendRange(range: string, values: any[][]): Promise<void> {
187
- await this.requireSheetsAdapter().appendRange(range, values);
188
- }
189
-
190
- async listSheets(): Promise<string[]> {
191
- return this.requireSheetsAdapter().listSheets();
192
- }
193
-
194
- async deleteSheet(name: string): Promise<void> {
195
- await this.requireSheetsAdapter().deleteSheet(name);
196
- }
197
- }
198
-
199
- // ─── Transaction-scoped adapter ────────────────────────────────────────────────────
200
-
201
- export class An5AdapterTx {
202
- constructor(
203
- private readonly handle: TransactionHandle,
204
- public readonly dialect: Dialect,
205
- ) { }
206
-
207
- async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
208
- return this.handle.exec<T>(query, params);
209
- }
210
-
211
- async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
212
- return this.handle.executeRaw(query, params);
213
- }
214
-
215
- table<T = any>(modelName: string): AdapterTableClient<T> {
216
- return new AdapterTableClient<T>(this, modelName);
217
- }
218
- }
219
-
220
- // ─── Table Client ──────────────────────────────────────────────────────────────────
221
-
222
- export class AdapterTableClient<T = any> {
223
- constructor(
224
- private readonly adapter: An5Adapter | An5AdapterTx,
225
- private readonly modelName: string,
226
- ) { }
227
-
228
- private get dialect(): Dialect { return this.adapter.dialect; }
229
-
230
- private get tableName(): string {
231
- const name = this.modelName;
232
- let t = name;
233
- const modelToTable = getModelToTable();
234
- if (modelToTable[name]) t = modelToTable[name];
235
- else {
236
- const camel = name.charAt(0).toLowerCase() + name.slice(1);
237
- if (modelToTable[camel]) t = modelToTable[camel];
238
- else {
239
- const lower = name.toLowerCase();
240
- if (modelToTable[lower]) t = modelToTable[lower];
241
- }
242
- }
243
- if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
244
- if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
245
- return quote(t, this.dialect);
246
- }
247
-
248
- private get nolock(): string {
249
- return this.dialect === 'mssql' ? ' WITH (NOLOCK)' : '';
250
- }
251
-
252
- private async doExec<U = any>(query: string, params?: Record<string, any>): Promise<U[]> {
253
- return this.adapter.exec<U>(query, params);
254
- }
255
-
256
- private async doExecuteRaw(query: string, params?: Record<string, any>): Promise<number> {
257
- return this.adapter._executeRaw(query, params);
258
- }
259
-
260
- async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any }): Promise<T[]> {
261
- const params: Record<string, any> = {};
262
- const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
263
- const orderSql = buildOrderBy(args?.orderBy, this.dialect);
264
- const take = args?.take;
265
- const skip = args?.skip ?? 0;
266
-
267
- let query: string;
268
- if (take !== undefined) {
269
- if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
270
- query = `SELECT * FROM ${this.tableName}`;
271
- if (whereSql) query += ` WHERE ${whereSql}`;
272
- if (orderSql) query += ` ${orderSql}`;
273
- query += ` LIMIT ${take} OFFSET ${skip}`;
274
- } else {
275
- // mssql requires ORDER BY before OFFSET/FETCH
276
- query = `SELECT * FROM ${this.tableName}${this.nolock}`;
277
- if (whereSql) query += ` WHERE ${whereSql}`;
278
- query += ` ${orderSql || 'ORDER BY (SELECT NULL)'}`;
279
- query += ` OFFSET ${skip} ROWS FETCH NEXT ${take} ROWS ONLY`;
280
- }
281
- } else {
282
- query = `SELECT * FROM ${this.tableName}${this.nolock}`;
283
- if (whereSql) query += ` WHERE ${whereSql}`;
284
- if (orderSql) query += ` ${orderSql}`;
285
- }
286
- return this.doExec<T>(query, params);
287
- }
288
-
289
- async findFirst(args?: { where?: any; orderBy?: any; select?: any }): Promise<T | null> {
290
- const rows = await this.findMany({ ...args, take: 1 });
291
- return rows[0] ?? null;
292
- }
293
-
294
- async findUnique(args: { where: any }): Promise<T | null> {
295
- return this.findFirst({ where: args.where });
296
- }
297
-
298
- async count(args?: { where?: any }): Promise<number> {
299
- const params: Record<string, any> = {};
300
- const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
301
- // Add NOLOCK for mssql count too
302
- let query = `SELECT COUNT(*) AS cnt FROM ${this.tableName}${this.nolock}`;
303
- if (whereSql) query += ` WHERE ${whereSql}`;
304
- const rows = await this.doExec<any>(query, params);
305
- return Number(rows[0]?.cnt ?? rows[0]?.CNT ?? 0);
306
- }
307
-
308
- async create(args: { data: Partial<T> }): Promise<T> {
309
- const fields = getFieldsForModel(this.modelName);
310
- const idFieldName = Object.prototype.hasOwnProperty.call(fields, 'id')
311
- ? 'id'
312
- : Object.keys(fields).find((name) => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
313
-
314
- const data: any = { ...args.data };
315
- if (idFieldName) {
316
- const fieldDef: any = fields[idFieldName];
317
- const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || fieldDef?.type || '');
318
- const normalizedType = String(rawType).toLowerCase();
319
- const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(normalizedType);
320
- if (isStringType && !data[idFieldName]) data[idFieldName] = generateUUID();
321
- }
322
-
323
- const cols = Object.keys(data).filter(k => data[k] !== undefined);
324
- const params: Record<string, any> = {};
325
- const vals: string[] = [];
326
-
327
- for (const col of cols) {
328
- const p = `c_${col}`;
329
- params[p] = data[col];
330
- vals.push(`@${p}`);
331
- }
332
-
333
- const query = `INSERT INTO ${this.tableName} (${cols.map(c => quote(c, this.dialect)).join(', ')}) VALUES (${vals.join(', ')})`;
334
- await this.doExec(query, params);
335
- return (await this.findFirst({ where: idFieldName ? { [idFieldName]: data[idFieldName] } : data })) as T;
336
- }
337
-
338
- async createMany(args: { data: Partial<T>[]; skipDuplicates?: boolean }): Promise<{ count: number }> {
339
- if (args.data.length === 0) return { count: 0 };
340
-
341
- const firstCols = Object.keys(args.data[0]).filter(k => (args.data[0] as any)[k] !== undefined);
342
-
343
- // Bulk INSERT for the common case (same columns, no skipDuplicates)
344
- if (firstCols.length > 0 && !args.skipDuplicates) {
345
- const params: Record<string, any> = {};
346
- const rowPlaceholders: string[] = [];
347
-
348
- for (let r = 0; r < args.data.length; r++) {
349
- const row = args.data[r] as any;
350
- const vals = firstCols.map(col => {
351
- const p = `r${r}_${col}`;
352
- params[p] = row[col] ?? null;
353
- return `@${p}`;
354
- });
355
- rowPlaceholders.push(`(${vals.join(', ')})`);
356
- }
357
-
358
- const query = `INSERT INTO ${this.tableName} (${firstCols.map(c => quote(c, this.dialect)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
359
- try {
360
- await this.doExecuteRaw(query, params);
361
- return { count: args.data.length };
362
- } catch {
363
- // fallback below if bulk fails (e.g. mixed column sets)
364
- }
365
- }
366
-
367
- // Row-by-row fallback
368
- let count = 0;
369
- for (const row of args.data) {
370
- try { await this.create({ data: row }); count++; }
371
- catch (e) { if (!args.skipDuplicates) throw e; }
372
- }
373
- return { count };
374
- }
375
-
376
- async update(args: { where: any; data: Partial<T> }): Promise<T> {
377
- const params: Record<string, any> = {};
378
- const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_');
379
- const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
380
- const sets: string[] = [];
381
-
382
- for (const col of setCols) {
383
- const p = `s_${col}`;
384
- params[p] = (args.data as any)[col];
385
- sets.push(`${quote(col, this.dialect)} = @${p}`);
386
- }
387
-
388
- const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
389
- await this.doExecuteRaw(query, params);
390
- return (await this.findFirst({ where: args.where })) as T;
391
- }
392
-
393
- async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
394
- const params: Record<string, any> = {};
395
- const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_');
396
- const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
397
- const sets: string[] = [];
398
-
399
- for (const col of setCols) {
400
- const p = `s_${col}`;
401
- params[p] = (args.data as any)[col];
402
- sets.push(`${quote(col, this.dialect)} = @${p}`);
403
- }
404
-
405
- const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
406
- const count = await this.doExecuteRaw(query, params);
407
- return { count };
408
- }
409
-
410
- async delete(args: { where: any }): Promise<T> {
411
- const existing = await this.findFirst({ where: args.where });
412
- const params: Record<string, any> = {};
413
- const whereSql = parseWhere(this.modelName, args.where, params, this.dialect);
414
- await this.doExecuteRaw(`DELETE FROM ${this.tableName} WHERE ${whereSql}`, params);
415
- return existing as T;
416
- }
417
-
418
- async deleteMany(args?: { where?: any }): Promise<{ count: number }> {
419
- const params: Record<string, any> = {};
420
- const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
421
- const query = `DELETE FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''}`;
422
- const count = await this.doExecuteRaw(query, params);
423
- return { count };
424
- }
425
-
426
- async upsert(args: { where: any; create: Partial<T>; update: Partial<T> }): Promise<T> {
427
- const existing = await this.findFirst({ where: args.where });
428
- return existing
429
- ? this.update({ where: args.where, data: args.update })
430
- : this.create({ data: args.create });
431
- }
432
-
433
- async aggregate(args: any): Promise<any> {
434
- const params: Record<string, any> = {};
435
- const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
436
- const aggs: string[] = [];
437
-
438
- if (args._count) aggs.push('COUNT(*) AS _count');
439
- if (args._sum) for (const f of Object.keys(args._sum)) aggs.push(`SUM(${quote(f, this.dialect)}) AS _sum_${f}`);
440
- if (args._avg) for (const f of Object.keys(args._avg)) aggs.push(`AVG(${quote(f, this.dialect)}) AS _avg_${f}`);
441
- if (args._min) for (const f of Object.keys(args._min)) aggs.push(`MIN(${quote(f, this.dialect)}) AS _min_${f}`);
442
- if (args._max) for (const f of Object.keys(args._max)) aggs.push(`MAX(${quote(f, this.dialect)}) AS _max_${f}`);
443
-
444
- const query = `SELECT ${aggs.join(', ')} FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''}`;
445
- const rows = await this.doExec(query, params);
446
- return rows[0] ?? {};
447
- }
448
-
449
- async groupBy(args: any): Promise<any[]> {
450
- const params: Record<string, any> = {};
451
- const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
452
- const byCols = (args.by || []).map((b: string) => quote(b, this.dialect)).join(', ');
453
- const query = `SELECT ${byCols}, COUNT(*) AS _count FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''} GROUP BY ${byCols}`;
454
- return this.doExec(query, params);
455
- }
456
-
457
- async vectorSearch(args: {
458
- vector: number[];
459
- take?: number;
460
- where?: any;
461
- vectorField?: string;
462
- distanceMetric?: 'cosine' | 'euclidean' | 'dot';
463
- }): Promise<(T & { distance: number })[]> {
464
- const rows = await this.findMany({ where: args.where });
465
- const vectorField = args.vectorField || 'embedding';
466
- const metric = args.distanceMetric || 'cosine';
467
-
468
- const scored: { row: T; dist: number }[] = [];
469
- for (const row of rows) {
470
- const raw = (row as any)[vectorField];
471
- if (!raw) continue;
472
- let vec: number[] = [];
473
- try { vec = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { continue; }
474
- if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
475
-
476
- let dot = 0, m1 = 0, m2 = 0;
477
- for (let i = 0; i < args.vector.length; i++) {
478
- dot += args.vector[i] * vec[i];
479
- m1 += args.vector[i] ** 2;
480
- m2 += vec[i] ** 2;
481
- }
482
- const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
483
- const dist = metric === 'cosine'
484
- ? 1 - cosine
485
- : metric === 'dot'
486
- ? -dot
487
- : Math.sqrt(args.vector.reduce((s, v, i) => s + (v - vec[i]) ** 2, 0));
488
- scored.push({ row, dist });
489
- }
490
- scored.sort((a, b) => a.dist - b.dist);
491
- return scored.slice(0, args.take ?? 10).map(s => ({ ...s.row as any, distance: s.dist }));
492
- }
493
- }
494
-
495
- // ─── Factory ──────────────────────────────────────────────────────────────────────
496
-
497
- export function createAn5Adapter(config: AnyAdapterConfig): AnyAdapter {
498
- if (isSheetsConfig(config)) return new An5SheetsAdapter(config);
499
- if (config.connectionString.trim().startsWith('googlesheets://')) {
500
- return new An5SheetsAdapter(parseSheetsConnectionString(config.connectionString));
501
- }
502
- return new An5Adapter(config);
503
- }
504
-
505
- export const createAdapter = createAn5Adapter;
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
+ }