@an5/adapters 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +189 -305
  2. package/dist/an5Adapter.d.ts +85 -7
  3. package/dist/an5Adapter.js +736 -64
  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 +4 -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 +3 -1
  19. package/dist/index.js +7 -1
  20. package/dist/mssql/engine.js +8 -4
  21. package/dist/mysql/engine.js +1 -1
  22. package/dist/postgres/engine.js +1 -1
  23. package/dist/sqlite/browserEngine.d.ts +27 -0
  24. package/dist/sqlite/browserEngine.js +161 -0
  25. package/dist/sqlite/engine.js +15 -3
  26. package/dist/sqlite/index.d.ts +1 -0
  27. package/dist/sqlite/index.js +1 -0
  28. package/dist/typed.d.ts +5 -0
  29. package/dist/typed.js +2 -0
  30. package/dotnet/An5Adapter.cs +1089 -275
  31. package/dotnet/Base/Core.cs +340 -82
  32. package/dotnet/Mssql/MssqlEngine.cs +145 -144
  33. package/dotnet/Postgres/PostgresEngine.cs +121 -121
  34. package/golang/an5_adapter.go +314 -0
  35. package/golang/base/base.go +100 -0
  36. package/golang/base/metadata.go +116 -0
  37. package/golang/base/where.go +311 -0
  38. package/golang/go.mod +3 -0
  39. package/golang/mssql/mssql.go +32 -0
  40. package/golang/postgres/postgres.go +37 -0
  41. package/golang/sqlite/sqlite.go +14 -0
  42. package/golang/sqlite_test.go +42 -0
  43. package/golang/table_client.go +1329 -0
  44. package/package.json +17 -7
  45. package/python/an5_adapter.py +119 -97
  46. package/python/base/__init__.py +3 -3
  47. package/python/base/dialects.py +11 -8
  48. package/python/base/metadata.py +35 -10
  49. package/python/base/sql.py +172 -121
  50. package/python/mssql/__init__.py +1 -1
  51. package/python/mssql/provider.py +62 -62
  52. package/python/postgres/__init__.py +1 -1
  53. package/python/postgres/provider.py +15 -15
  54. package/python/table_client.py +619 -216
  55. package/typescript/src/an5Adapter.ts +1220 -505
  56. package/typescript/src/base/index.ts +3 -3
  57. package/typescript/src/base/metadata.ts +148 -28
  58. package/typescript/src/base/sql.ts +264 -98
  59. package/typescript/src/base/types.ts +27 -24
  60. package/typescript/src/browser.ts +10 -6
  61. package/typescript/src/config.ts +95 -95
  62. package/typescript/src/googlesheets/adapter.ts +269 -259
  63. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  64. package/typescript/src/googlesheets/config.ts +47 -47
  65. package/typescript/src/googlesheets/helpers.ts +144 -143
  66. package/typescript/src/googlesheets/index.ts +3 -3
  67. package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
  68. package/typescript/src/googlesheets/retry.ts +126 -126
  69. package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
  70. package/typescript/src/googlesheets/tableClient.ts +461 -455
  71. package/typescript/src/googlesheets/types.ts +6 -6
  72. package/typescript/src/index.ts +31 -27
  73. package/typescript/src/mssql/engine.ts +134 -132
  74. package/typescript/src/mssql/index.ts +1 -1
  75. package/typescript/src/mysql/engine.ts +80 -80
  76. package/typescript/src/mysql/index.ts +1 -1
  77. package/typescript/src/postgres/engine.ts +87 -87
  78. package/typescript/src/postgres/index.ts +1 -1
  79. package/typescript/src/sqlite/browserEngine.ts +192 -0
  80. package/typescript/src/sqlite/engine.ts +66 -55
  81. package/typescript/src/sqlite/index.ts +3 -1
  82. package/typescript/src/typed.ts +52 -0
@@ -1,505 +1,1220 @@
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
+ import { buildOrderBy, parseWhere, quote } from './base/sql';
11
+ import {
12
+ createAdapterProxy,
13
+ getFieldsForModel,
14
+ getModelToTable,
15
+ getRelationMap,
16
+ getRelationsForModel,
17
+ type RelationDef,
18
+ } from './base/metadata';
19
+ export type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
20
+ export { setAdapterMetadata, createAdapterProxy, type AdapterMetadata } from './base/metadata';
21
+ export type { TypedAn5Adapter, AdapterAPI } from './typed';
22
+
23
+ export type AnyAdapter = An5Adapter | An5SheetsAdapter;
24
+ export type AnyAdapterConfig = An5AdapterConfig | An5SheetsAdapterConfig | { connectionString: string };
25
+
26
+ function isSheetsConfig(config: AnyAdapterConfig): config is An5SheetsAdapterConfig {
27
+ return (config as any).spreadsheetId !== undefined;
28
+ }
29
+
30
+ function sanitizeParamName(name: string): string {
31
+ const cleaned = String(name).replace(/[^A-Za-z0-9_]/g, '_');
32
+ return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
33
+ }
34
+
35
+ function appendUpdateSet(sets: string[], params: Record<string, any>, col: string, val: any, dialect: Dialect): void {
36
+ const quoted = quote(col, dialect);
37
+ const safeCol = sanitizeParamName(col);
38
+ if (val && typeof val === 'object' && !(val instanceof Date)) {
39
+ if (val.increment !== undefined) {
40
+ sets.push(`${quoted} = ${quoted} + @s_${safeCol}_inc`);
41
+ params[`s_${safeCol}_inc`] = val.increment;
42
+ return;
43
+ }
44
+ if (val.decrement !== undefined) {
45
+ sets.push(`${quoted} = ${quoted} - @s_${safeCol}_dec`);
46
+ params[`s_${safeCol}_dec`] = val.decrement;
47
+ return;
48
+ }
49
+ if (val.multiply !== undefined) {
50
+ sets.push(`${quoted} = ${quoted} * @s_${safeCol}_mul`);
51
+ params[`s_${safeCol}_mul`] = val.multiply;
52
+ return;
53
+ }
54
+ if (val.divide !== undefined) {
55
+ sets.push(`${quoted} = ${quoted} / @s_${safeCol}_div`);
56
+ params[`s_${safeCol}_div`] = val.divide;
57
+ return;
58
+ }
59
+ if (val.set !== undefined) {
60
+ sets.push(`${quoted} = @s_${safeCol}_set`);
61
+ params[`s_${safeCol}_set`] = val.set;
62
+ return;
63
+ }
64
+ }
65
+
66
+ sets.push(`${quoted} = @s_${safeCol}`);
67
+ params[`s_${safeCol}`] = val;
68
+ }
69
+
70
+ function selectedAggregateFields(fields: any): string[] {
71
+ if (!fields || typeof fields !== 'object') return [];
72
+ return Object.keys(fields).filter(key => fields[key]);
73
+ }
74
+
75
+ function normalizeByFields(by: any): string[] {
76
+ if (typeof by === 'string') return [by];
77
+ return Array.isArray(by) ? by.filter(field => typeof field === 'string' && field.length > 0) : [];
78
+ }
79
+
80
+ function toNonNegativeInt(value: unknown, fallback = 0): number {
81
+ const n = Number.parseInt(String(value), 10);
82
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
83
+ }
84
+
85
+ function projectFields(row: any, select: any): any {
86
+ if (!row || !select || typeof select !== 'object') return row;
87
+ const projected: any = {};
88
+ for (const [key, val] of Object.entries(select)) {
89
+ if (val) {
90
+ projected[key] = row[key];
91
+ }
92
+ }
93
+ if (row._count) {
94
+ projected._count = row._count;
95
+ }
96
+ return projected;
97
+ }
98
+
99
+ async function resolveIncludes(
100
+ modelName: string,
101
+ rows: any[],
102
+ include: any,
103
+ adapter: An5Adapter | An5AdapterTx
104
+ ): Promise<void> {
105
+ if (!rows || rows.length === 0 || !include || typeof include !== 'object') return;
106
+ const modelRelations = getRelationsForModel(modelName);
107
+
108
+ for (const [key, value] of Object.entries(include)) {
109
+ if (!value) continue;
110
+
111
+ if (key === '_count') {
112
+ for (const row of rows) {
113
+ if (!row._count) row._count = {};
114
+ }
115
+ for (const [relKey, relation] of Object.entries(modelRelations)) {
116
+ if (relation.relationType === 'many') {
117
+ const uniqueKeys = Array.from(new Set(rows.map(r => r[relation.localKey]).filter(k => k !== undefined && k !== null)));
118
+ if (uniqueKeys.length > 0) {
119
+ const relClient = adapter.table(relation.modelName);
120
+ const relatedRows = await relClient.findMany({
121
+ where: { [relation.foreignKey]: { in: uniqueKeys } },
122
+ });
123
+ const countMap = new Map<any, number>();
124
+ relatedRows.forEach((r: any) => {
125
+ const k = r[relation.foreignKey];
126
+ countMap.set(k, (countMap.get(k) || 0) + 1);
127
+ });
128
+ rows.forEach(r => {
129
+ const k = r[relation.localKey];
130
+ r._count[relKey] = countMap.get(k) || 0;
131
+ });
132
+ } else {
133
+ rows.forEach(r => { r._count[relKey] = 0; });
134
+ }
135
+ }
136
+ }
137
+ continue;
138
+ }
139
+
140
+ const relation = modelRelations[key];
141
+ if (!relation) continue;
142
+
143
+ const isMany = relation.relationType === 'many';
144
+ const localKey = relation.localKey;
145
+ const foreignKey = relation.foreignKey;
146
+ const joinKey = isMany ? localKey : foreignKey;
147
+ const matchKey = isMany ? foreignKey : localKey;
148
+ const uniqueKeys = Array.from(new Set(rows.map(r => r[joinKey]).filter(k => k !== undefined && k !== null)));
149
+
150
+ if (uniqueKeys.length === 0) {
151
+ rows.forEach(r => { r[key] = isMany ? [] : null; });
152
+ continue;
153
+ }
154
+
155
+ const subArgs: any = typeof value === 'object' ? { ...value } : {};
156
+ const nestedInclude = subArgs.include;
157
+ const nestedSelect = subArgs.select;
158
+
159
+ const subWhere: any = {
160
+ [matchKey]: { in: uniqueKeys },
161
+ ...(subArgs.where || {}),
162
+ };
163
+
164
+ const relClient = adapter.table(relation.modelName);
165
+ const relatedRows = await relClient.findMany({
166
+ where: subWhere,
167
+ orderBy: subArgs.orderBy,
168
+ take: subArgs.take,
169
+ skip: subArgs.skip,
170
+ });
171
+
172
+ if (nestedInclude) {
173
+ await resolveIncludes(relation.modelName, relatedRows, nestedInclude, adapter);
174
+ }
175
+
176
+ const outputRows = nestedSelect && typeof nestedSelect === 'object'
177
+ ? relatedRows.map((r: any) => projectFields(r, nestedSelect))
178
+ : relatedRows;
179
+
180
+ const groupMap = new Map<any, any[]>();
181
+ relatedRows.forEach((r: any, idx: number) => {
182
+ const k = r[isMany ? foreignKey : localKey];
183
+ if (!groupMap.has(k)) groupMap.set(k, []);
184
+ groupMap.get(k)!.push(outputRows[idx]);
185
+ });
186
+
187
+ rows.forEach(r => {
188
+ const k = r[isMany ? localKey : foreignKey];
189
+ const matches = groupMap.get(k) || [];
190
+ if (isMany) {
191
+ r[key] = matches;
192
+ } else {
193
+ r[key] = matches[0] || null;
194
+ }
195
+ });
196
+ }
197
+ }
198
+
199
+ // ─── An5Adapter ────────────────────────────────────────────────────────────────────
200
+
201
+ export class An5Adapter {
202
+ [key: string]: any;
203
+ private _engine: QueryEngine | null = null;
204
+ private _engineType: 'postgres' | 'mysql' | 'sqlite' | 'mssql' | null = null;
205
+ private _engineConfig: An5AdapterConfig | null = null;
206
+ sheetsAdapter: An5SheetsAdapter | null = null;
207
+
208
+ get dialect(): Dialect {
209
+ if (this.sheetsAdapter) return 'googlesheets';
210
+ if (this._engine) return this._engine.dialect;
211
+ if (this._engineType === 'postgres') return 'postgres';
212
+ if (this._engineType === 'mysql') return 'mysql';
213
+ if (this._engineType === 'sqlite') return 'sqlite';
214
+ return 'mssql';
215
+ }
216
+
217
+ constructor(adapterConfig: An5AdapterConfig | An5SheetsAdapterConfig) {
218
+ if (isSheetsConfig(adapterConfig)) {
219
+ this.sheetsAdapter = new An5SheetsAdapter(adapterConfig);
220
+ return createAdapterProxy(this, (name) => this.table(name));
221
+ }
222
+
223
+ if ((adapterConfig as any).engine) {
224
+ this._engine = (adapterConfig as any).engine;
225
+ this._engineType = (this._engine?.dialect as any) || 'sqlite';
226
+ return createAdapterProxy(this, (name) => this.table(name));
227
+ }
228
+
229
+ if ((adapterConfig as any).db || (adapterConfig as any).driver) {
230
+ const { SqliteBrowserEngine } = require('./sqlite/browserEngine.js');
231
+ this._engine = new SqliteBrowserEngine(adapterConfig);
232
+ this._engineType = 'sqlite';
233
+ return createAdapterProxy(this, (name) => this.table(name));
234
+ }
235
+
236
+ const cs = (adapterConfig.connectionString || '').trim();
237
+ if (cs.startsWith('googlesheets://')) {
238
+ this.sheetsAdapter = new An5SheetsAdapter(parseSheetsConnectionString(cs));
239
+ return createAdapterProxy(this, (name) => this.table(name));
240
+ }
241
+
242
+ this._engineConfig = adapterConfig;
243
+ if (cs.startsWith('postgres://') || cs.startsWith('postgresql://')) {
244
+ this._engineType = 'postgres';
245
+ } else if (cs.startsWith('mysql://') || cs.startsWith('mariadb://')) {
246
+ this._engineType = 'mysql';
247
+ } else if (cs.startsWith('sqlite://') || cs.endsWith('.sqlite') || cs.endsWith('.db')) {
248
+ this._engineType = 'sqlite';
249
+ } else {
250
+ this._engineType = 'mssql';
251
+ }
252
+
253
+ return createAdapterProxy(this, (name) => this.table(name));
254
+ }
255
+
256
+ private async requireEngine(): Promise<QueryEngine> {
257
+ if (!this._engine && this._engineType) {
258
+ switch (this._engineType) {
259
+ case 'postgres': {
260
+ const { PostgresEngine } = require('./postgres/index.js');
261
+ this._engine = new PostgresEngine(this._engineConfig!);
262
+ break;
263
+ }
264
+ case 'mysql': {
265
+ const { MysqlEngine } = require('./mysql/index.js');
266
+ this._engine = new MysqlEngine(this._engineConfig!);
267
+ break;
268
+ }
269
+ case 'sqlite': {
270
+ const { SqliteEngine } = require('./sqlite/index.js');
271
+ this._engine = new SqliteEngine(this._engineConfig!);
272
+ break;
273
+ }
274
+ case 'mssql': {
275
+ const { MssqlEngine } = require('./mssql/index.js');
276
+ this._engine = new MssqlEngine(this._engineConfig!);
277
+ break;
278
+ }
279
+ }
280
+ }
281
+ if (!this._engine) throw new Error('SQL engine is not available for Google Sheets adapter');
282
+ return this._engine;
283
+ }
284
+
285
+ private requireSheetsAdapter(): An5SheetsAdapter {
286
+ if (!this.sheetsAdapter) throw new Error('Google Sheets methods require a googlesheets:// connection or Sheets config');
287
+ return this.sheetsAdapter;
288
+ }
289
+
290
+ async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
291
+ if (this.sheetsAdapter) return this.sheetsAdapter.exec<T>(query, params);
292
+ return (await this.requireEngine()).exec<T>(query, params);
293
+ }
294
+
295
+ /** INTERNAL: used by AdapterTableClient for DML statements needing row count */
296
+ async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
297
+ return (await this.requireEngine()).executeRaw(query, params);
298
+ }
299
+
300
+ /** Execute a raw query with positional values → @p_0, @p_1, ... per dialect */
301
+ async $queryRawUnsafe<T = any>(query: string, ...values: any[]): Promise<T[]> {
302
+ if (this.sheetsAdapter) return this.sheetsAdapter.$queryRawUnsafe<T>(query, ...values);
303
+ const engine = await this.requireEngine();
304
+ const params: Record<string, any> = {};
305
+ values.forEach((v, i) => { params[`p_${i}`] = v; });
306
+ let q = query;
307
+ if (this.dialect === 'postgres') {
308
+ let idx = 0;
309
+ q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
310
+ } else if (this.dialect === 'mysql') {
311
+ let idx = 0;
312
+ q = q.replace(/\?/g, () => `@p_${idx++}`);
313
+ }
314
+ return engine.exec<T>(q, params);
315
+ }
316
+
317
+ async $executeRaw(query: string, ...values: any[]): Promise<number> {
318
+ if (this.sheetsAdapter) return this.sheetsAdapter.$executeRaw(query, ...values);
319
+ const engine = await this.requireEngine();
320
+ const params: Record<string, any> = {};
321
+ values.forEach((v, i) => { params[`p_${i}`] = v; });
322
+ let q = query;
323
+ if (this.dialect === 'postgres') {
324
+ let idx = 0;
325
+ q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
326
+ } else if (this.dialect === 'mysql') {
327
+ let idx = 0;
328
+ q = q.replace(/\?/g, () => `@p_${idx++}`);
329
+ }
330
+ return engine.executeRaw(q, params);
331
+ }
332
+
333
+ async $executeRawUnsafe(query: string, ...values: any[]): Promise<number> {
334
+ return this.$executeRaw(query, ...values);
335
+ }
336
+
337
+ async $connect(): Promise<void> {
338
+ if (this.sheetsAdapter) return this.sheetsAdapter.$connect();
339
+ await (await this.requireEngine()).connect();
340
+ }
341
+
342
+ async $disconnect(): Promise<void> {
343
+ if (this.sheetsAdapter) return this.sheetsAdapter.$disconnect();
344
+ await (await this.requireEngine()).disconnect();
345
+ }
346
+
347
+ /** Real transaction with BEGIN / COMMIT / ROLLBACK */
348
+ async $transaction<R>(fn: (tx: An5AdapterTx) => Promise<R>, options?: { timeout?: number }): Promise<R>;
349
+ async $transaction<R>(list: Promise<R>[]): Promise<R[]>;
350
+ async $transaction(fn: any, _options?: any): Promise<any> {
351
+ if (this.sheetsAdapter) return this.sheetsAdapter.$transaction(fn, _options);
352
+ if (Array.isArray(fn)) return Promise.all(fn);
353
+ const engine = await this.requireEngine();
354
+ const handle = await engine.beginTransaction();
355
+ const txAdapter = new An5AdapterTx(handle, this.dialect);
356
+ try {
357
+ const result = await fn(txAdapter);
358
+ await handle.commit();
359
+ return result;
360
+ } catch (err) {
361
+ await handle.rollback();
362
+ throw err;
363
+ }
364
+ }
365
+
366
+ async $begin(): Promise<An5AdapterTx> {
367
+ if (this.sheetsAdapter) {
368
+ throw new Error('Interactive transactions are not supported by the Google Sheets adapter');
369
+ }
370
+ const engine = await this.requireEngine();
371
+ return new An5AdapterTx(await engine.beginTransaction(), this.dialect);
372
+ }
373
+
374
+ table<T = any>(modelName: string): AdapterTableClient<T> | SheetsTableClient<T> {
375
+ if (this.sheetsAdapter) return this.sheetsAdapter.table<T>(modelName);
376
+ return new AdapterTableClient<T>(this, modelName);
377
+ }
378
+
379
+ private _listeners: Record<string, ((...args: any[]) => void)[]> = {};
380
+ private _middlewares: ((params: any) => Promise<any>)[] = [];
381
+
382
+ $on(event: 'query' | 'error' | 'warn' | 'info' | string, callback: (...args: any[]) => void): this {
383
+ if (!this._listeners[event]) this._listeners[event] = [];
384
+ this._listeners[event].push(callback);
385
+ return this;
386
+ }
387
+
388
+ $off(event: string, callback: (...args: any[]) => void): this {
389
+ if (!this._listeners[event]) return this;
390
+ this._listeners[event] = this._listeners[event].filter(cb => cb !== callback);
391
+ return this;
392
+ }
393
+
394
+ $emit(event: string, ...args: any[]): void {
395
+ if (this._listeners[event]) {
396
+ for (const cb of this._listeners[event]) {
397
+ try { cb(...args); } catch {}
398
+ }
399
+ }
400
+ }
401
+
402
+ $use(middleware: (params: { model?: string; action: string; args: any; run: (args: any) => Promise<any> }) => Promise<any>): this {
403
+ this._middlewares.push(middleware);
404
+ return this;
405
+ }
406
+
407
+ view<T = any>(viewName: string): ViewClient<T> {
408
+ return new ViewClient<T>(this, viewName);
409
+ }
410
+
411
+ async $queryProc<T = any>(procName: string, params?: Record<string, any> | any[]): Promise<T[]> {
412
+ if (this.sheetsAdapter) throw new Error('Stored procedures are not supported by the Google Sheets adapter');
413
+ const paramObj: Record<string, any> = {};
414
+ let sql = '';
415
+ if (Array.isArray(params)) {
416
+ const placeholders = params.map((v, i) => {
417
+ paramObj[`p_${i}`] = v;
418
+ return `@p_${i}`;
419
+ });
420
+ sql = this.dialect === 'postgres' || this.dialect === 'mysql'
421
+ ? `CALL ${procName}(${placeholders.join(', ')})`
422
+ : `EXEC ${procName} ${placeholders.join(', ')}`;
423
+ } else if (params && typeof params === 'object') {
424
+ const pairs = Object.entries(params).map(([k, v]) => {
425
+ paramObj[`p_${k}`] = v;
426
+ return `@${k} = @p_${k}`;
427
+ });
428
+ sql = this.dialect === 'postgres' || this.dialect === 'mysql'
429
+ ? `CALL ${procName}(${Object.keys(params).map(k => `@p_${k}`).join(', ')})`
430
+ : `EXEC ${procName} ${pairs.join(', ')}`;
431
+ } else {
432
+ sql = this.dialect === 'postgres' || this.dialect === 'mysql'
433
+ ? `CALL ${procName}()`
434
+ : `EXEC ${procName}`;
435
+ }
436
+ return this.exec<T>(sql, paramObj);
437
+ }
438
+
439
+ async $executeProc(procName: string, params?: Record<string, any> | any[]): Promise<number> {
440
+ if (this.sheetsAdapter) throw new Error('Stored procedures are not supported by the Google Sheets adapter');
441
+ const paramObj: Record<string, any> = {};
442
+ let sql = '';
443
+ if (Array.isArray(params)) {
444
+ const placeholders = params.map((v, i) => {
445
+ paramObj[`p_${i}`] = v;
446
+ return `@p_${i}`;
447
+ });
448
+ sql = this.dialect === 'postgres' || this.dialect === 'mysql'
449
+ ? `CALL ${procName}(${placeholders.join(', ')})`
450
+ : `EXEC ${procName} ${placeholders.join(', ')}`;
451
+ } else if (params && typeof params === 'object') {
452
+ const pairs = Object.entries(params).map(([k, v]) => {
453
+ paramObj[`p_${k}`] = v;
454
+ return `@${k} = @p_${k}`;
455
+ });
456
+ sql = this.dialect === 'postgres' || this.dialect === 'mysql'
457
+ ? `CALL ${procName}(${Object.keys(params).map(k => `@p_${k}`).join(', ')})`
458
+ : `EXEC ${procName} ${pairs.join(', ')}`;
459
+ } else {
460
+ sql = this.dialect === 'postgres' || this.dialect === 'mysql'
461
+ ? `CALL ${procName}()`
462
+ : `EXEC ${procName}`;
463
+ }
464
+ return this._executeRaw(sql, paramObj);
465
+ }
466
+
467
+ async readRange<T = any>(range: string): Promise<T[][]> {
468
+ return this.requireSheetsAdapter().readRange<T>(range);
469
+ }
470
+
471
+ async writeRange(range: string, values: any[][]): Promise<void> {
472
+ await this.requireSheetsAdapter().writeRange(range, values);
473
+ }
474
+
475
+ async appendRange(range: string, values: any[][]): Promise<void> {
476
+ await this.requireSheetsAdapter().appendRange(range, values);
477
+ }
478
+
479
+ async listSheets(): Promise<string[]> {
480
+ return this.requireSheetsAdapter().listSheets();
481
+ }
482
+
483
+ async deleteSheet(name: string): Promise<void> {
484
+ await this.requireSheetsAdapter().deleteSheet(name);
485
+ }
486
+ }
487
+
488
+ // ─── View Client (Read-Only) ────────────────────────────────────────────────────────
489
+
490
+ export class ViewClient<T = any> {
491
+ private readonly tableClient: AdapterTableClient<T>;
492
+
493
+ constructor(
494
+ public readonly adapter: An5Adapter | An5AdapterTx,
495
+ public readonly viewName: string,
496
+ ) {
497
+ this.tableClient = new AdapterTableClient<T>(adapter, viewName);
498
+ }
499
+
500
+ public get dialect(): Dialect { return this.tableClient.dialect; }
501
+ public get tableName(): string { return this.tableClient.tableName; }
502
+
503
+ async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any; include?: any }): Promise<T[]> {
504
+ return this.tableClient.findMany(args);
505
+ }
506
+
507
+ async findFirst(args?: { where?: any; orderBy?: any; select?: any; include?: any }): Promise<T | null> {
508
+ return this.tableClient.findFirst(args);
509
+ }
510
+
511
+ async findUnique(args: { where: any; select?: any; include?: any }): Promise<T | null> {
512
+ return this.tableClient.findUnique(args);
513
+ }
514
+
515
+ async count(args?: { where?: any }): Promise<number> {
516
+ return this.tableClient.count(args);
517
+ }
518
+
519
+ async aggregate(args: any): Promise<any> {
520
+ return this.tableClient.aggregate(args);
521
+ }
522
+
523
+ async groupBy(args: any): Promise<any[]> {
524
+ return this.tableClient.groupBy(args);
525
+ }
526
+
527
+ async vectorSearch(args: any): Promise<(T & { distance: number })[]> {
528
+ return this.tableClient.vectorSearch(args);
529
+ }
530
+
531
+ async create(): Promise<never> {
532
+ throw new Error(`View '${this.viewName}' is read-only. Mutation operations (create) are not allowed.`);
533
+ }
534
+
535
+ async createMany(): Promise<never> {
536
+ throw new Error(`View '${this.viewName}' is read-only. Mutation operations (createMany) are not allowed.`);
537
+ }
538
+
539
+ async update(): Promise<never> {
540
+ throw new Error(`View '${this.viewName}' is read-only. Mutation operations (update) are not allowed.`);
541
+ }
542
+
543
+ async updateMany(): Promise<never> {
544
+ throw new Error(`View '${this.viewName}' is read-only. Mutation operations (updateMany) are not allowed.`);
545
+ }
546
+
547
+ async delete(): Promise<never> {
548
+ throw new Error(`View '${this.viewName}' is read-only. Mutation operations (delete) are not allowed.`);
549
+ }
550
+
551
+ async deleteMany(): Promise<never> {
552
+ throw new Error(`View '${this.viewName}' is read-only. Mutation operations (deleteMany) are not allowed.`);
553
+ }
554
+
555
+ async upsert(): Promise<never> {
556
+ throw new Error(`View '${this.viewName}' is read-only. Mutation operations (upsert) are not allowed.`);
557
+ }
558
+ }
559
+
560
+ // ─── Transaction-scoped adapter ────────────────────────────────────────────────────
561
+
562
+ export class An5AdapterTx {
563
+ [key: string]: any;
564
+ private closed = false;
565
+
566
+ constructor(
567
+ private readonly handle: TransactionHandle,
568
+ public readonly dialect: Dialect,
569
+ ) {
570
+ return createAdapterProxy(this, (name) => this.table(name));
571
+ }
572
+
573
+ async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
574
+ this.ensureOpen();
575
+ return this.handle.exec<T>(query, params);
576
+ }
577
+
578
+ async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
579
+ this.ensureOpen();
580
+ return this.handle.executeRaw(query, params);
581
+ }
582
+
583
+ async $commit(): Promise<void> {
584
+ this.ensureOpen();
585
+ this.closed = true;
586
+ await this.handle.commit();
587
+ }
588
+
589
+ async $rollback(): Promise<void> {
590
+ this.ensureOpen();
591
+ this.closed = true;
592
+ await this.handle.rollback();
593
+ }
594
+
595
+ table<T = any>(modelName: string): AdapterTableClient<T> {
596
+ return new AdapterTableClient<T>(this, modelName);
597
+ }
598
+
599
+ private ensureOpen(): void {
600
+ if (this.closed) throw new Error('Transaction is already closed');
601
+ }
602
+ }
603
+
604
+ // ─── Table Client ──────────────────────────────────────────────────────────────────
605
+
606
+ export class AdapterTableClient<T = any> {
607
+ constructor(
608
+ public readonly adapter: An5Adapter | An5AdapterTx,
609
+ public readonly modelName: string,
610
+ ) { }
611
+
612
+ public get dialect(): Dialect { return this.adapter.dialect; }
613
+
614
+ public get tableName(): string {
615
+ const name = this.modelName;
616
+ let t = name;
617
+ const modelToTable = getModelToTable();
618
+ if (modelToTable[name]) t = modelToTable[name];
619
+ else {
620
+ const camel = name.charAt(0).toLowerCase() + name.slice(1);
621
+ if (modelToTable[camel]) t = modelToTable[camel];
622
+ else {
623
+ const lower = name.toLowerCase();
624
+ if (modelToTable[lower]) t = modelToTable[lower];
625
+ }
626
+ }
627
+ if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
628
+ if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
629
+ return quote(t, this.dialect);
630
+ }
631
+
632
+ private get nolock(): string {
633
+ return this.dialect === 'mssql' ? ' WITH (NOLOCK)' : '';
634
+ }
635
+
636
+ /** Build the shared context used by parseWhere to resolve relations and self references. */
637
+ private whereContext(): { relationMap: Record<string, Record<string, RelationDef>>; modelToTable: Record<string, string>; selfRef: string } {
638
+ return {
639
+ relationMap: getRelationMap(),
640
+ modelToTable: getModelToTable(),
641
+ selfRef: this.tableName,
642
+ };
643
+ }
644
+
645
+ private async doExec<U = any>(query: string, params?: Record<string, any>): Promise<U[]> {
646
+ return this.adapter.exec<U>(query, params);
647
+ }
648
+
649
+ private async doExecuteRaw(query: string, params?: Record<string, any>): Promise<number> {
650
+ return this.adapter._executeRaw(query, params);
651
+ }
652
+
653
+ async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any; include?: any }): Promise<T[]> {
654
+ const params: Record<string, any> = {};
655
+ const whereCtx = this.whereContext();
656
+ const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', whereCtx);
657
+ const orderSql = buildOrderBy(args?.orderBy, this.dialect);
658
+ const take = args?.take;
659
+ const skip = args?.skip;
660
+ const hasSkip = skip !== undefined && skip !== null;
661
+
662
+ let cols = "*";
663
+ if (args?.select && typeof args.select === "object") {
664
+ const relationKeys = getRelationsForModel(this.modelName);
665
+ const hasRelationSelect = Object.keys(args.select).some(k => k === '_count' || relationKeys[k]);
666
+ const fields = getFieldsForModel(this.modelName);
667
+ const selectedKeys = Object.keys(args.select).filter(k => args.select[k] === true && (Object.keys(fields).length === 0 || fields[k]));
668
+ if (selectedKeys.length > 0 && !hasRelationSelect) {
669
+ cols = selectedKeys.map(k => quote(k, this.dialect)).join(", ");
670
+ }
671
+ }
672
+
673
+ let query: string;
674
+ if (take !== undefined && !hasSkip) {
675
+ if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
676
+ query = `SELECT ${cols} FROM ${this.tableName}`;
677
+ if (whereSql) query += ` WHERE ${whereSql}`;
678
+ if (orderSql) query += ` ${orderSql}`;
679
+ query += ` LIMIT ${take}`;
680
+ } else {
681
+ query = `SELECT TOP (${take}) ${cols} FROM ${this.tableName}${this.nolock}`;
682
+ if (whereSql) query += ` WHERE ${whereSql}`;
683
+ if (orderSql) query += ` ${orderSql}`;
684
+ }
685
+ } else if (hasSkip) {
686
+ if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
687
+ query = `SELECT ${cols} FROM ${this.tableName}`;
688
+ if (whereSql) query += ` WHERE ${whereSql}`;
689
+ if (orderSql) query += ` ${orderSql}`;
690
+ query += ` LIMIT ${take ?? 'ALL'} OFFSET ${skip}`;
691
+ } else {
692
+ // mssql requires ORDER BY before OFFSET/FETCH
693
+ query = `SELECT ${cols} FROM ${this.tableName}${this.nolock}`;
694
+ if (whereSql) query += ` WHERE ${whereSql}`;
695
+ query += ` ${orderSql || 'ORDER BY (SELECT NULL)'}`;
696
+ query += ` OFFSET ${skip} ROWS`;
697
+ if (take !== undefined) {
698
+ query += ` FETCH NEXT ${take} ROWS ONLY`;
699
+ }
700
+ }
701
+ } else {
702
+ query = `SELECT ${cols} FROM ${this.tableName}${this.nolock}`;
703
+ if (whereSql) query += ` WHERE ${whereSql}`;
704
+ if (orderSql) query += ` ${orderSql}`;
705
+ }
706
+
707
+ const rows = await this.doExec<T>(query, params);
708
+
709
+ if (args?.include) {
710
+ await resolveIncludes(this.modelName, rows, args.include, this.adapter);
711
+ }
712
+ if (args?.select && typeof args.select === 'object') {
713
+ const relationKeys = getRelationsForModel(this.modelName);
714
+ const hasRelationSelect = Object.keys(args.select).some(k => k === '_count' || relationKeys[k]);
715
+ if (hasRelationSelect) {
716
+ await resolveIncludes(this.modelName, rows, args.select, this.adapter);
717
+ }
718
+ return rows.map((r: any) => projectFields(r, args.select));
719
+ }
720
+
721
+ return rows;
722
+ }
723
+
724
+ async findFirst(args?: { where?: any; orderBy?: any; select?: any; include?: any }): Promise<T | null> {
725
+ const rows = await this.findMany({ ...args, take: 1 });
726
+ return rows[0] ?? null;
727
+ }
728
+
729
+ async findUnique(args: { where: any; select?: any; include?: any }): Promise<T | null> {
730
+ return this.findFirst({ where: args.where, select: args.select, include: args.include });
731
+ }
732
+
733
+ async count(args?: { where?: any }): Promise<number> {
734
+ const params: Record<string, any> = {};
735
+ const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
736
+ let query = `SELECT COUNT(*) AS cnt FROM ${this.tableName}${this.nolock}`;
737
+ if (whereSql) query += ` WHERE ${whereSql}`;
738
+ const rows = await this.doExec<any>(query, params);
739
+ return Number(rows[0]?.cnt ?? rows[0]?.CNT ?? 0);
740
+ }
741
+
742
+ async create(args: { data: Partial<T> | any; include?: any; select?: any }): Promise<T> {
743
+ const modelRelations = getRelationsForModel(this.modelName);
744
+ const rawData = { ...args.data };
745
+ const relationWrites: Record<string, any> = {};
746
+
747
+ for (const key of Object.keys(rawData)) {
748
+ if (modelRelations[key] && typeof rawData[key] === 'object' && rawData[key] !== null && !(rawData[key] instanceof Date)) {
749
+ relationWrites[key] = rawData[key];
750
+ delete rawData[key];
751
+ }
752
+ }
753
+
754
+ const fields = getFieldsForModel(this.modelName);
755
+ const idFieldName = Object.prototype.hasOwnProperty.call(fields, 'id')
756
+ ? 'id'
757
+ : Object.keys(fields).find((name) => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
758
+
759
+ const data: any = { ...rawData };
760
+ if (idFieldName) {
761
+ const fieldDef: any = fields[idFieldName];
762
+ const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || fieldDef?.type || '');
763
+ const normalizedType = String(rawType).toLowerCase();
764
+ const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(normalizedType);
765
+ if (isStringType && !data[idFieldName]) data[idFieldName] = generateUUID();
766
+ }
767
+
768
+ const cols = Object.keys(data).filter(k => data[k] !== undefined);
769
+ const params: Record<string, any> = {};
770
+ const vals: string[] = [];
771
+
772
+ for (const col of cols) {
773
+ const p = `c_${col}`;
774
+ params[p] = data[col];
775
+ vals.push(`@${p}`);
776
+ }
777
+
778
+ const query = `INSERT INTO ${this.tableName} (${cols.map(c => quote(c, this.dialect)).join(', ')}) VALUES (${vals.join(', ')})`;
779
+ await this.doExec(query, params);
780
+
781
+ const createdRecord = (await this.findFirst({ where: idFieldName ? { [idFieldName]: data[idFieldName] } : data })) as any;
782
+
783
+ // Handle nested relation writes
784
+ for (const [relKey, relWrite] of Object.entries(relationWrites)) {
785
+ const relation = modelRelations[relKey];
786
+ if (!relation) continue;
787
+ const childClient = this.adapter.table(relation.modelName);
788
+ if (relWrite.create) {
789
+ const createItems = Array.isArray(relWrite.create) ? relWrite.create : [relWrite.create];
790
+ for (const item of createItems) {
791
+ await childClient.create({
792
+ data: {
793
+ ...item,
794
+ [relation.foreignKey]: createdRecord[relation.localKey],
795
+ },
796
+ });
797
+ }
798
+ }
799
+ }
800
+
801
+ if (args.include) {
802
+ await resolveIncludes(this.modelName, [createdRecord], args.include, this.adapter);
803
+ }
804
+ if (args.select) {
805
+ await resolveIncludes(this.modelName, [createdRecord], args.select, this.adapter);
806
+ return projectFields(createdRecord, args.select);
807
+ }
808
+
809
+ return createdRecord as T;
810
+ }
811
+
812
+ async createMany(args: { data: Partial<T>[]; skipDuplicates?: boolean }): Promise<{ count: number }> {
813
+ if (args.data.length === 0) return { count: 0 };
814
+
815
+ const firstRow = args.data[0];
816
+ if (firstRow === undefined) return { count: 0 };
817
+ const firstCols = Object.keys(firstRow).filter(k => (firstRow as any)[k] !== undefined);
818
+
819
+ // Bulk INSERT for the common case (same columns, no skipDuplicates)
820
+ if (firstCols.length > 0 && !args.skipDuplicates) {
821
+ const params: Record<string, any> = {};
822
+ const rowPlaceholders: string[] = [];
823
+
824
+ for (let r = 0; r < args.data.length; r++) {
825
+ const row = args.data[r] as any;
826
+ const vals = firstCols.map(col => {
827
+ const p = `r${r}_${col}`;
828
+ params[p] = row[col] ?? null;
829
+ return `@${p}`;
830
+ });
831
+ rowPlaceholders.push(`(${vals.join(', ')})`);
832
+ }
833
+
834
+ const query = `INSERT INTO ${this.tableName} (${firstCols.map(c => quote(c, this.dialect)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
835
+ try {
836
+ await this.doExecuteRaw(query, params);
837
+ return { count: args.data.length };
838
+ } catch {
839
+ // fallback below if bulk fails (e.g. mixed column sets)
840
+ }
841
+ }
842
+
843
+ // Row-by-row fallback
844
+ let count = 0;
845
+ for (const row of args.data) {
846
+ try { await this.create({ data: row }); count++; }
847
+ catch (e) { if (!args.skipDuplicates) throw e; }
848
+ }
849
+ return { count };
850
+ }
851
+
852
+ async update(args: { where: any; data: Partial<T> | any; include?: any; select?: any }): Promise<T> {
853
+ const modelRelations = getRelationsForModel(this.modelName);
854
+ const rawData = { ...args.data };
855
+ const relationWrites: Record<string, any> = {};
856
+
857
+ for (const key of Object.keys(rawData)) {
858
+ if (modelRelations[key] && typeof rawData[key] === 'object' && rawData[key] !== null && !(rawData[key] instanceof Date)) {
859
+ relationWrites[key] = rawData[key];
860
+ delete rawData[key];
861
+ }
862
+ }
863
+
864
+ const setCols = Object.keys(rawData).filter(k => rawData[k] !== undefined);
865
+ if (setCols.length > 0) {
866
+ const params: Record<string, any> = {};
867
+ const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
868
+ const sets: string[] = [];
869
+ for (const col of setCols) {
870
+ appendUpdateSet(sets, params, col, rawData[col], this.dialect);
871
+ }
872
+ if (sets.length > 0) {
873
+ const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
874
+ await this.doExecuteRaw(query, params);
875
+ }
876
+ }
877
+
878
+ const updatedRecord = (await this.findFirst({ where: args.where })) as any;
879
+
880
+ // Handle nested writes
881
+ for (const [relKey, relWrite] of Object.entries(relationWrites)) {
882
+ const relation = modelRelations[relKey];
883
+ if (!relation) continue;
884
+ const childClient = this.adapter.table(relation.modelName);
885
+ if (relWrite.create) {
886
+ const createItems = Array.isArray(relWrite.create) ? relWrite.create : [relWrite.create];
887
+ for (const item of createItems) {
888
+ await childClient.create({
889
+ data: {
890
+ ...item,
891
+ [relation.foreignKey]: updatedRecord[relation.localKey],
892
+ },
893
+ });
894
+ }
895
+ }
896
+ if (relWrite.update) {
897
+ const updateObj = relWrite.update;
898
+ await childClient.update({
899
+ where: updateObj.where,
900
+ data: updateObj.data,
901
+ });
902
+ }
903
+ if (relWrite.disconnect) {
904
+ const disc = relWrite.disconnect;
905
+ await childClient.updateMany({
906
+ where: disc,
907
+ data: { [relation.foreignKey]: null },
908
+ });
909
+ }
910
+ }
911
+
912
+ if (args.include) {
913
+ await resolveIncludes(this.modelName, [updatedRecord], args.include, this.adapter);
914
+ }
915
+ if (args.select) {
916
+ await resolveIncludes(this.modelName, [updatedRecord], args.select, this.adapter);
917
+ return projectFields(updatedRecord, args.select);
918
+ }
919
+
920
+ return updatedRecord as T;
921
+ }
922
+
923
+ async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
924
+ const params: Record<string, any> = {};
925
+ const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
926
+ const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
927
+ const sets: string[] = [];
928
+
929
+ for (const col of setCols) {
930
+ appendUpdateSet(sets, params, col, (args.data as any)[col], this.dialect);
931
+ }
932
+
933
+ if (sets.length === 0) return { count: 0 };
934
+
935
+ const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
936
+ const count = await this.doExecuteRaw(query, params);
937
+ return { count };
938
+ }
939
+
940
+ async delete(args: { where: any }): Promise<T> {
941
+ const existing = await this.findFirst({ where: args.where });
942
+ const params: Record<string, any> = {};
943
+ const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
944
+ await this.doExecuteRaw(`DELETE FROM ${this.tableName} WHERE ${whereSql}`, params);
945
+ return existing as T;
946
+ }
947
+
948
+ async deleteMany(args?: { where?: any }): Promise<{ count: number }> {
949
+ const params: Record<string, any> = {};
950
+ const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
951
+ const query = `DELETE FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''}`;
952
+ const count = await this.doExecuteRaw(query, params);
953
+ return { count };
954
+ }
955
+
956
+ async upsert(args: { where: any; create: Partial<T>; update: Partial<T> }): Promise<T> {
957
+ const existing = await this.findFirst({ where: args.where });
958
+ return existing
959
+ ? this.update({ where: args.where, data: args.update })
960
+ : this.create({ data: args.create });
961
+ }
962
+
963
+ async aggregate(args: any): Promise<any> {
964
+ const params: Record<string, any> = {};
965
+ const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
966
+ const aggs: string[] = [];
967
+
968
+ if (args._count) {
969
+ aggs.push('COUNT(*) AS cnt_all');
970
+ if (typeof args._count === 'object') {
971
+ for (const f of selectedAggregateFields(args._count)) {
972
+ if (f !== '_all') aggs.push(`COUNT(${quote(f, this.dialect)}) AS cnt_${sanitizeParamName(f)}`);
973
+ }
974
+ }
975
+ }
976
+ if (args._sum) for (const f of selectedAggregateFields(args._sum)) aggs.push(`SUM(${quote(f, this.dialect)}) AS sum_${sanitizeParamName(f)}`);
977
+ if (args._avg) for (const f of selectedAggregateFields(args._avg)) aggs.push(`AVG(${quote(f, this.dialect)}) AS avg_${sanitizeParamName(f)}`);
978
+ if (args._min) for (const f of selectedAggregateFields(args._min)) aggs.push(`MIN(${quote(f, this.dialect)}) AS min_${sanitizeParamName(f)}`);
979
+ if (args._max) for (const f of selectedAggregateFields(args._max)) aggs.push(`MAX(${quote(f, this.dialect)}) AS max_${sanitizeParamName(f)}`);
980
+
981
+ if (aggs.length === 0) throw new Error('Aggregate requires at least one aggregator field');
982
+
983
+ const query = `SELECT ${aggs.join(', ')} FROM ${this.tableName}${this.nolock}${whereSql ? ` WHERE ${whereSql}` : ''}`;
984
+ const rows = await this.doExec<any>(query, params);
985
+ const row = rows[0] || {};
986
+
987
+ const result: any = {};
988
+ if (args._count) {
989
+ const allCount = Number(row.cnt_all ?? row._count ?? 0);
990
+ result._count = { _all: allCount };
991
+ if (typeof args._count === 'object') {
992
+ for (const f of selectedAggregateFields(args._count)) {
993
+ if (f === '_all') continue;
994
+ const k = `cnt_${sanitizeParamName(f)}`;
995
+ result._count[f] = row[k] !== undefined ? Number(row[k]) : allCount;
996
+ }
997
+ }
998
+ }
999
+ if (args._sum) {
1000
+ result._sum = {};
1001
+ for (const f of selectedAggregateFields(args._sum)) {
1002
+ const k = `sum_${sanitizeParamName(f)}`;
1003
+ result._sum[f] = row[k] !== null && row[k] !== undefined ? Number(row[k]) : null;
1004
+ }
1005
+ }
1006
+ if (args._avg) {
1007
+ result._avg = {};
1008
+ for (const f of selectedAggregateFields(args._avg)) {
1009
+ const k = `avg_${sanitizeParamName(f)}`;
1010
+ result._avg[f] = row[k] !== null && row[k] !== undefined ? Number(row[k]) : null;
1011
+ }
1012
+ }
1013
+ if (args._min) {
1014
+ result._min = {};
1015
+ for (const f of selectedAggregateFields(args._min)) {
1016
+ const k = `min_${sanitizeParamName(f)}`;
1017
+ result._min[f] = row[k] ?? null;
1018
+ }
1019
+ }
1020
+ if (args._max) {
1021
+ result._max = {};
1022
+ for (const f of selectedAggregateFields(args._max)) {
1023
+ const k = `max_${sanitizeParamName(f)}`;
1024
+ result._max[f] = row[k] ?? null;
1025
+ }
1026
+ }
1027
+
1028
+ return result;
1029
+ }
1030
+
1031
+ async groupBy(args: any): Promise<any[]> {
1032
+ const params: Record<string, any> = {};
1033
+ const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
1034
+ const byFields = normalizeByFields(args?.by);
1035
+ if (byFields.length === 0) throw new Error("groupBy requires 'by' fields");
1036
+ const byCols = byFields.map((b: string) => quote(b, this.dialect)).join(', ');
1037
+ const aggs: string[] = ['COUNT(*) AS _count'];
1038
+ if (args?._sum) for (const f of selectedAggregateFields(args._sum)) aggs.push(`SUM(${quote(f, this.dialect)}) AS _sum_${f}`);
1039
+ if (args?._avg) for (const f of selectedAggregateFields(args._avg)) aggs.push(`AVG(${quote(f, this.dialect)}) AS _avg_${f}`);
1040
+ if (args?._min) for (const f of selectedAggregateFields(args._min)) aggs.push(`MIN(${quote(f, this.dialect)}) AS _min_${f}`);
1041
+ if (args?._max) for (const f of selectedAggregateFields(args._max)) aggs.push(`MAX(${quote(f, this.dialect)}) AS _max_${f}`);
1042
+ let query = `SELECT ${byCols}, ${aggs.join(', ')} FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''} GROUP BY ${byCols}`;
1043
+ const orderSql = buildOrderBy(args?.orderBy, this.dialect);
1044
+ const hasSkip = args?.skip !== undefined && args?.skip !== null;
1045
+ const hasTake = args?.take !== undefined && args?.take !== null;
1046
+ if (orderSql) query += ` ${orderSql}`;
1047
+ else if (hasSkip || hasTake) query += ` ORDER BY ${byCols}`;
1048
+ if (hasSkip || hasTake) {
1049
+ const skip = toNonNegativeInt(args?.skip);
1050
+ const take = toNonNegativeInt(args?.take, 1);
1051
+ if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
1052
+ if (hasTake) query += ` LIMIT ${take}`;
1053
+ query += ` OFFSET ${skip}`;
1054
+ } else {
1055
+ query += ` OFFSET ${skip} ROWS`;
1056
+ if (hasTake) query += ` FETCH NEXT ${take} ROWS ONLY`;
1057
+ }
1058
+ }
1059
+ return this.doExec(query, params);
1060
+ }
1061
+
1062
+ async vectorSearch(args: {
1063
+ vector: number[];
1064
+ take?: number;
1065
+ where?: any;
1066
+ vectorField?: string;
1067
+ distanceMetric?: 'cosine' | 'euclidean' | 'dot';
1068
+ vectorElementType?: 'float32' | 'float16' | 'uint8';
1069
+ }): Promise<(T & { distance: number })[]> {
1070
+ const vectorField = args.vectorField || 'embedding';
1071
+ const METRICS = ['cosine', 'euclidean', 'dot'];
1072
+ const ELEMENT_TYPES = ['float32', 'float16', 'uint8'];
1073
+ const metric = METRICS.includes(args.distanceMetric as string) ? args.distanceMetric! : 'cosine';
1074
+ const elementType = ELEMENT_TYPES.includes(args.vectorElementType as string) ? args.vectorElementType! : 'float32';
1075
+ const take = args.take ?? 10;
1076
+ const dim = Array.isArray(args.vector) ? args.vector.length : 0;
1077
+ const vectorJson = JSON.stringify(args.vector);
1078
+ const col = quote(vectorField, this.dialect);
1079
+ const params: Record<string, any> = { query_vector: vectorJson };
1080
+
1081
+ // 1. Try native dialect execution
1082
+ if (this.dialect === 'postgres') {
1083
+ const op = metric === 'cosine' ? '<=>' : metric === 'dot' ? '<#>' : '<->';
1084
+ const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
1085
+ const query = `SELECT *, (${col} ${op} @query_vector::vector) AS distance FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''} ORDER BY distance ASC LIMIT ${take}`;
1086
+ try {
1087
+ return await this.doExec<(T & { distance: number })>(query, params);
1088
+ } catch (err: any) {
1089
+ const msg = String(err?.message || '').toLowerCase();
1090
+ if (!msg.includes('vector') && !msg.includes('operator does not exist')) throw err;
1091
+ }
1092
+ } else if (this.dialect === 'mssql') {
1093
+ const distFn = metric === 'cosine' ? 'cosine' : metric === 'euclidean' ? 'euclidean' : 'dot';
1094
+ const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
1095
+ 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`;
1096
+ try {
1097
+ return await this.doExec<(T & { distance: number })>(query, params);
1098
+ } catch (err: any) {
1099
+ const msg = String(err?.message || '').toLowerCase();
1100
+ const isUnsupported =
1101
+ msg.includes('vector_distance') ||
1102
+ msg.includes('syntax near') ||
1103
+ msg.includes('not a recognized built-in function') ||
1104
+ msg.includes('not a defined system type') ||
1105
+ msg.includes('type "vector"') ||
1106
+ msg.includes('pgvector') ||
1107
+ msg.includes('operator does not exist') ||
1108
+ msg.includes('limit of 1998') ||
1109
+ err?.number === 195 ||
1110
+ err?.number === 102 ||
1111
+ err?.number === 319 ||
1112
+ err?.originalError?.number === 319;
1113
+ if (!isUnsupported) throw err;
1114
+ }
1115
+ }
1116
+
1117
+ // 2. Fallback: in-memory similarity computation when native vector support is unavailable
1118
+ const rows = await this.findMany({ where: args.where });
1119
+
1120
+ const scored: { row: T; dist: number }[] = [];
1121
+ for (const row of rows) {
1122
+ const raw = (row as any)[vectorField];
1123
+ if (!raw) continue;
1124
+ let vec: number[] = [];
1125
+ try { vec = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { continue; }
1126
+ if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
1127
+
1128
+ let dot = 0, m1 = 0, m2 = 0;
1129
+ for (let i = 0; i < args.vector.length; i++) {
1130
+ const av = args.vector[i] ?? 0;
1131
+ const bv = vec[i] ?? 0;
1132
+ dot += av * bv;
1133
+ m1 += av ** 2;
1134
+ m2 += bv ** 2;
1135
+ }
1136
+ const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
1137
+ const dist = metric === 'cosine'
1138
+ ? 1 - cosine
1139
+ : metric === 'dot'
1140
+ ? -dot
1141
+ : Math.sqrt(args.vector.reduce((s, v, i) => s + (v - (vec[i] ?? 0)) ** 2, 0));
1142
+ scored.push({ row, dist });
1143
+ }
1144
+ scored.sort((a, b) => a.dist - b.dist);
1145
+ return scored.slice(0, take).map(s => ({ ...s.row as any, distance: s.dist }));
1146
+ }
1147
+ }
1148
+
1149
+ // ─── Factory ──────────────────────────────────────────────────────────────────────
1150
+
1151
+ export function createAn5Adapter(config: AnyAdapterConfig): AnyAdapter;
1152
+ export function createAn5Adapter<TModels extends object>(
1153
+ config: AnyAdapterConfig
1154
+ ): import('./typed').TypedAn5Adapter<TModels>;
1155
+ export function createAn5Adapter(config: AnyAdapterConfig): AnyAdapter {
1156
+ if (isSheetsConfig(config)) return new An5SheetsAdapter(config);
1157
+ if (config.connectionString && config.connectionString.trim().startsWith('googlesheets://')) {
1158
+ return new An5SheetsAdapter(parseSheetsConnectionString(config.connectionString));
1159
+ }
1160
+ return new An5Adapter(config);
1161
+ }
1162
+
1163
+ export const createAdapter = createAn5Adapter;
1164
+
1165
+ export function createBrowserSqliteAdapter(config: any): An5Adapter {
1166
+ const { SqliteBrowserEngine } = require('./sqlite/browserEngine.js');
1167
+ return new An5Adapter({ engine: new SqliteBrowserEngine(config) });
1168
+ }
1169
+
1170
+ function normalizeAffectedCount(result: any): number {
1171
+ if (typeof result === 'number') return result;
1172
+ if (!result) return 0;
1173
+ if (Array.isArray(result.rowsAffected)) return Number(result.rowsAffected[0] ?? 0);
1174
+ if (typeof result.rowsAffected === 'number') return result.rowsAffected;
1175
+ if (typeof result.count === 'number') return result.count;
1176
+ return 0;
1177
+ }
1178
+
1179
+ export function executorFromAdapter(adapterLike: any): any {
1180
+ if (!adapterLike) return adapterLike;
1181
+ if (typeof adapterLike === 'function' && adapterLike.executeRaw) {
1182
+ return adapterLike;
1183
+ }
1184
+ return Object.assign(
1185
+ async (queryText: string, params?: Record<string, any>) => {
1186
+ return typeof adapterLike === 'function'
1187
+ ? adapterLike(queryText, params)
1188
+ : adapterLike.exec(queryText, params);
1189
+ },
1190
+ {
1191
+ executeRaw: async (queryText: string, params?: Record<string, any>) => {
1192
+ if (typeof adapterLike._executeRaw === 'function') {
1193
+ return adapterLike._executeRaw(queryText, params);
1194
+ }
1195
+ if (typeof adapterLike.executeRaw === 'function') {
1196
+ return adapterLike.executeRaw(queryText, params);
1197
+ }
1198
+ const res = typeof adapterLike === 'function'
1199
+ ? await adapterLike(queryText, params)
1200
+ : await adapterLike.exec(queryText, params);
1201
+ return normalizeAffectedCount(res);
1202
+ },
1203
+ transaction: adapterLike.$transaction
1204
+ ? async (fn: (txExecutor: any) => Promise<any>, options?: { timeout?: number }) => {
1205
+ return adapterLike.$transaction(async (tx: any) => fn(executorFromAdapter(tx)), options);
1206
+ }
1207
+ : undefined,
1208
+ beginTransaction: adapterLike.$begin
1209
+ ? async () => {
1210
+ const tx = await adapterLike.$begin();
1211
+ return {
1212
+ executor: executorFromAdapter(tx),
1213
+ commit: () => tx.$commit(),
1214
+ rollback: () => tx.$rollback(),
1215
+ };
1216
+ }
1217
+ : undefined,
1218
+ }
1219
+ );
1220
+ }