@an5/adapters 0.2.5 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -305
- package/dist/an5Adapter.d.ts +142 -7
- package/dist/an5Adapter.js +998 -68
- package/dist/base/metadata.d.ts +10 -0
- package/dist/base/metadata.js +107 -0
- package/dist/base/sql.d.ts +19 -1
- package/dist/base/sql.js +152 -24
- package/dist/base/types.d.ts +10 -1
- package/dist/browser.d.ts +4 -1
- package/dist/browser.js +9 -2
- package/dist/googlesheets/adapter.d.ts +1 -0
- package/dist/googlesheets/adapter.js +9 -2
- package/dist/googlesheets/config.d.ts +4 -4
- package/dist/googlesheets/helpers.js +2 -0
- package/dist/googlesheets/parseConnectionString.js +5 -2
- package/dist/googlesheets/sqlExecutor.js +29 -24
- package/dist/googlesheets/tableClient.js +14 -5
- package/dist/index.d.ts +5 -1
- package/dist/index.js +15 -1
- package/dist/mssql/engine.js +8 -4
- package/dist/mysql/engine.js +1 -1
- package/dist/nbase/client.d.ts +92 -0
- package/dist/nbase/client.js +147 -0
- package/dist/nbase/index.d.ts +47 -0
- package/dist/nbase/index.js +80 -0
- package/dist/postgres/engine.js +1 -1
- package/dist/sqlite/browserEngine.d.ts +27 -0
- package/dist/sqlite/browserEngine.js +161 -0
- package/dist/sqlite/engine.js +15 -3
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/typed.d.ts +5 -0
- package/dist/typed.js +2 -0
- package/dotnet/An5Adapter.cs +1112 -275
- package/dotnet/Base/Core.cs +380 -82
- package/dotnet/Mssql/MssqlEngine.cs +145 -144
- package/dotnet/Postgres/PostgresEngine.cs +149 -121
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/golang/an5_adapter.go +314 -0
- package/golang/base/base.go +100 -0
- package/golang/base/metadata.go +116 -0
- package/golang/base/where.go +311 -0
- package/golang/go.mod +3 -0
- package/golang/mssql/mssql.go +32 -0
- package/golang/postgres/postgres.go +37 -0
- package/golang/sqlite/sqlite.go +14 -0
- package/golang/sqlite_test.go +42 -0
- package/golang/table_client.go +1329 -0
- package/package.json +26 -9
- package/python/an5_adapter.py +184 -97
- package/python/base/__init__.py +3 -3
- package/python/base/dialects.py +11 -8
- package/python/base/metadata.py +59 -10
- package/python/base/sql.py +202 -121
- package/python/mssql/__init__.py +1 -1
- package/python/mssql/provider.py +62 -62
- package/python/postgres/__init__.py +1 -1
- package/python/postgres/provider.py +15 -15
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +636 -216
- package/rust/Cargo.toml +31 -0
- package/rust/README.md +71 -0
- package/rust/src/adapter.rs +1312 -0
- package/rust/src/base/dialect.rs +172 -0
- package/rust/src/base/metadata.rs +134 -0
- package/rust/src/base/mod.rs +17 -0
- package/rust/src/base/where_builder.rs +405 -0
- package/rust/src/lib.rs +75 -0
- package/typescript/src/an5Adapter.ts +1515 -505
- package/typescript/src/base/index.ts +3 -3
- package/typescript/src/base/metadata.ts +148 -28
- package/typescript/src/base/sql.ts +264 -98
- package/typescript/src/base/types.ts +33 -24
- package/typescript/src/browser.ts +10 -6
- package/typescript/src/config.ts +95 -95
- package/typescript/src/googlesheets/adapter.ts +269 -259
- package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
- package/typescript/src/googlesheets/config.ts +47 -47
- package/typescript/src/googlesheets/helpers.ts +144 -143
- package/typescript/src/googlesheets/index.ts +3 -3
- package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
- package/typescript/src/googlesheets/retry.ts +126 -126
- package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
- package/typescript/src/googlesheets/tableClient.ts +461 -455
- package/typescript/src/googlesheets/types.ts +6 -6
- package/typescript/src/index.ts +48 -27
- package/typescript/src/mssql/engine.ts +134 -132
- package/typescript/src/mssql/index.ts +1 -1
- package/typescript/src/mysql/engine.ts +80 -80
- package/typescript/src/mysql/index.ts +1 -1
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
- package/typescript/src/postgres/engine.ts +87 -87
- package/typescript/src/postgres/index.ts +1 -1
- package/typescript/src/sqlite/browserEngine.ts +192 -0
- package/typescript/src/sqlite/engine.ts +66 -55
- package/typescript/src/sqlite/index.ts +3 -1
- package/typescript/src/typed.ts +52 -0
package/dist/an5Adapter.js
CHANGED
|
@@ -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
|
|
40
|
-
if (
|
|
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 (
|
|
272
|
+
if (forDialect.startsWith('postgres://') || forDialect.startsWith('postgresql://')) {
|
|
46
273
|
this._engineType = 'postgres';
|
|
47
274
|
}
|
|
48
|
-
else if (
|
|
275
|
+
else if (forDialect.startsWith('mysql://') || forDialect.startsWith('mariadb://')) {
|
|
49
276
|
this._engineType = 'mysql';
|
|
50
277
|
}
|
|
51
|
-
else if (
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = { ...
|
|
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
|
-
|
|
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
|
|
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
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
417
|
-
aggs.push(`SUM(${(0, sql_1.quote)(f, this.dialect)}) AS
|
|
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
|
|
420
|
-
aggs.push(`AVG(${(0, sql_1.quote)(f, this.dialect)}) AS
|
|
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
|
|
423
|
-
aggs.push(`MIN(${(0, sql_1.quote)(f, this.dialect)}) AS
|
|
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
|
|
426
|
-
aggs.push(`MAX(${(0, sql_1.quote)(f, this.dialect)}) AS
|
|
427
|
-
|
|
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
|
-
|
|
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
|
|
435
|
-
|
|
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
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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,
|
|
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
|
+
}
|