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