@an5/adapters 0.2.6 → 0.2.10
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 +96 -2
- package/dist/an5Adapter.d.ts +57 -0
- package/dist/an5Adapter.js +263 -4
- package/dist/base/sql.js +16 -15
- package/dist/base/types.d.ts +6 -0
- package/dist/googlesheets/helpers.js +13 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -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/sqlite/engine.js +2 -1
- package/dotnet/An5Adapter.cs +41 -18
- package/dotnet/Base/Core.cs +56 -26
- package/dotnet/Postgres/PostgresEngine.cs +58 -30
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/golang/base/where.go +32 -49
- package/golang/base/where_test.go +57 -0
- package/package.json +26 -11
- package/python/an5_adapter.py +74 -9
- package/python/base/__init__.py +1 -1
- package/python/base/metadata.py +25 -1
- package/python/base/sql.py +50 -21
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +53 -36
- package/rust/Cargo.toml +31 -0
- package/rust/README.md +71 -0
- package/rust/src/adapter.rs +1314 -0
- package/rust/src/base/dialect.rs +201 -0
- package/rust/src/base/metadata.rs +138 -0
- package/rust/src/base/mod.rs +17 -0
- package/rust/src/base/where_builder.rs +386 -0
- package/rust/src/lib.rs +75 -0
- package/typescript/src/an5Adapter.ts +300 -4
- package/typescript/src/base/sql.ts +15 -14
- package/typescript/src/base/types.ts +6 -0
- package/typescript/src/googlesheets/helpers.ts +11 -8
- package/typescript/src/index.ts +17 -0
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
- package/typescript/src/sqlite/engine.ts +2 -1
|
@@ -7,6 +7,25 @@ import {
|
|
|
7
7
|
} from './googlesheets';
|
|
8
8
|
import { parseSheetsConnectionString } from './googlesheets/parseConnectionString';
|
|
9
9
|
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* File extensions that mean SQLite when a connection string has no scheme.
|
|
13
|
+
*
|
|
14
|
+
* `.sqlite3` is here as well as in `@an5/orm`'s `detectProvider`, because the ORM
|
|
15
|
+
* validates field types and writes the DDL from the provider this file picks: if the
|
|
16
|
+
* two lists differed, a `.sqlite3` file would be pushed as SQL Server DDL.
|
|
17
|
+
*/
|
|
18
|
+
const SQLITE_FILE_SUFFIXES = ['.sqlite', '.sqlite3', '.db'];
|
|
19
|
+
import {
|
|
20
|
+
NBaseError,
|
|
21
|
+
buildVectorId,
|
|
22
|
+
parseVectorId,
|
|
23
|
+
isNBaseConnectionString,
|
|
24
|
+
parseNBaseConnectionString,
|
|
25
|
+
createNBaseVectorClient,
|
|
26
|
+
type NBaseAdapterConfig,
|
|
27
|
+
type NBaseVectorClient,
|
|
28
|
+
} from './nbase';
|
|
10
29
|
import { buildOrderBy, parseWhere, quote } from './base/sql';
|
|
11
30
|
import {
|
|
12
31
|
createAdapterProxy,
|
|
@@ -204,6 +223,34 @@ export class An5Adapter {
|
|
|
204
223
|
private _engineType: 'postgres' | 'mysql' | 'sqlite' | 'mssql' | null = null;
|
|
205
224
|
private _engineConfig: An5AdapterConfig | null = null;
|
|
206
225
|
sheetsAdapter: An5SheetsAdapter | null = null;
|
|
226
|
+
private _nbase: NBaseVectorClient | null = null;
|
|
227
|
+
private _nbaseConfig: NBaseAdapterConfig | null = null;
|
|
228
|
+
private _nbaseOnly = false;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Returns the NBase client for this adapter, if the project configured one.
|
|
232
|
+
* The connection string may also point straight at NBase, which keeps
|
|
233
|
+
* `DATABASE_URL=nbase://host:port` working on its own.
|
|
234
|
+
*/
|
|
235
|
+
nbaseClient(): { client: NBaseVectorClient; config: NBaseAdapterConfig } | null {
|
|
236
|
+
if (this._nbase && this._nbaseConfig) return { client: this._nbase, config: this._nbaseConfig };
|
|
237
|
+
|
|
238
|
+
if (!this._nbaseConfig) return null;
|
|
239
|
+
const config = this._nbaseConfig;
|
|
240
|
+
|
|
241
|
+
const client: NBaseVectorClient = createNBaseVectorClient(config);
|
|
242
|
+
this._nbase = client;
|
|
243
|
+
this._nbaseConfig = config;
|
|
244
|
+
return { client, config };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* True when the adapter only knows about NBase, so there is no relational
|
|
249
|
+
* database to read rows from.
|
|
250
|
+
*/
|
|
251
|
+
get nbaseOnly(): boolean {
|
|
252
|
+
return this._nbaseOnly === true;
|
|
253
|
+
}
|
|
207
254
|
|
|
208
255
|
get dialect(): Dialect {
|
|
209
256
|
if (this.sheetsAdapter) return 'googlesheets';
|
|
@@ -220,6 +267,25 @@ export class An5Adapter {
|
|
|
220
267
|
return createAdapterProxy(this, (name) => this.table(name));
|
|
221
268
|
}
|
|
222
269
|
|
|
270
|
+
// Keep the config even when the caller supplies its own engine: the NBase
|
|
271
|
+
// settings and anything else on the config are read from it later.
|
|
272
|
+
this._engineConfig = adapterConfig as An5AdapterConfig;
|
|
273
|
+
|
|
274
|
+
// `nbase://host:port` as the connection string configures the vector store
|
|
275
|
+
// on its own: there is no relational database, so the adapter is
|
|
276
|
+
// vector-only. The same string passed as `nbase` keeps the database from
|
|
277
|
+
// `connectionString` as the source of rows.
|
|
278
|
+
const rawConnectionString = String(this._engineConfig.connectionString ?? '').trim();
|
|
279
|
+
if (isNBaseConnectionString(rawConnectionString)) {
|
|
280
|
+
this._nbaseConfig = parseNBaseConnectionString(rawConnectionString);
|
|
281
|
+
this._nbaseOnly = true;
|
|
282
|
+
return createAdapterProxy(this, (name) => this.table(name));
|
|
283
|
+
}
|
|
284
|
+
const attached = this._engineConfig.nbase;
|
|
285
|
+
if (attached && isNBaseConnectionString(attached)) {
|
|
286
|
+
this._nbaseConfig = parseNBaseConnectionString(attached);
|
|
287
|
+
}
|
|
288
|
+
|
|
223
289
|
if ((adapterConfig as any).engine) {
|
|
224
290
|
this._engine = (adapterConfig as any).engine;
|
|
225
291
|
this._engineType = (this._engine?.dialect as any) || 'sqlite';
|
|
@@ -233,18 +299,28 @@ export class An5Adapter {
|
|
|
233
299
|
return createAdapterProxy(this, (name) => this.table(name));
|
|
234
300
|
}
|
|
235
301
|
|
|
302
|
+
// Lower-cased for the dialect test only; the connection string itself keeps its
|
|
303
|
+
// case for the driver. A URI scheme is case-insensitive, and `@an5/orm` reads the
|
|
304
|
+
// provider the same way to validate field types and write DDL — if the two
|
|
305
|
+
// disagreed, a schema would be checked against one database and run on another.
|
|
236
306
|
const cs = (adapterConfig.connectionString || '').trim();
|
|
237
|
-
|
|
307
|
+
const forDialect = cs.toLowerCase();
|
|
308
|
+
|
|
309
|
+
if (forDialect.startsWith('googlesheets://')) {
|
|
238
310
|
this.sheetsAdapter = new An5SheetsAdapter(parseSheetsConnectionString(cs));
|
|
239
311
|
return createAdapterProxy(this, (name) => this.table(name));
|
|
240
312
|
}
|
|
241
313
|
|
|
242
314
|
this._engineConfig = adapterConfig;
|
|
243
|
-
if (
|
|
315
|
+
if (forDialect.startsWith('postgres://') || forDialect.startsWith('postgresql://')) {
|
|
244
316
|
this._engineType = 'postgres';
|
|
245
|
-
} else if (
|
|
317
|
+
} else if (forDialect.startsWith('mysql://') || forDialect.startsWith('mariadb://')) {
|
|
246
318
|
this._engineType = 'mysql';
|
|
247
|
-
} else if (
|
|
319
|
+
} else if (
|
|
320
|
+
forDialect === ':memory:'
|
|
321
|
+
|| forDialect.startsWith('sqlite:')
|
|
322
|
+
|| SQLITE_FILE_SUFFIXES.some((suffix) => forDialect.endsWith(suffix))
|
|
323
|
+
) {
|
|
248
324
|
this._engineType = 'sqlite';
|
|
249
325
|
} else {
|
|
250
326
|
this._engineType = 'mssql';
|
|
@@ -336,9 +412,35 @@ export class An5Adapter {
|
|
|
336
412
|
|
|
337
413
|
async $connect(): Promise<void> {
|
|
338
414
|
if (this.sheetsAdapter) return this.sheetsAdapter.$connect();
|
|
415
|
+
if (this._nbaseOnly) {
|
|
416
|
+
// NBase is stateless over HTTP, so there is nothing to open. Report the
|
|
417
|
+
// endpoint so a misconfiguration is obvious.
|
|
418
|
+
const config = this.nbaseClient()?.config;
|
|
419
|
+
if (config) await this.nbaseClient()!.client.health();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
339
422
|
await (await this.requireEngine()).connect();
|
|
340
423
|
}
|
|
341
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Health check for the configured NBase instance, or the database when the
|
|
427
|
+
* adapter has none.
|
|
428
|
+
*/
|
|
429
|
+
async $nbaseHealth(): Promise<{ connected: boolean; endpoint?: string; error?: string }> {
|
|
430
|
+
const nbase = this.nbaseClient();
|
|
431
|
+
if (!nbase) return { connected: false, error: 'No NBase instance configured.' };
|
|
432
|
+
try {
|
|
433
|
+
const health = await nbase.client.health();
|
|
434
|
+
return { connected: health.status === 'ok', endpoint: nbase.config.url };
|
|
435
|
+
} catch (err) {
|
|
436
|
+
return {
|
|
437
|
+
connected: false,
|
|
438
|
+
endpoint: nbase.config.url,
|
|
439
|
+
error: err instanceof Error ? err.message : String(err),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
342
444
|
async $disconnect(): Promise<void> {
|
|
343
445
|
if (this.sheetsAdapter) return this.sheetsAdapter.$disconnect();
|
|
344
446
|
await (await this.requireEngine()).disconnect();
|
|
@@ -1059,6 +1161,181 @@ export class AdapterTableClient<T = any> {
|
|
|
1059
1161
|
return this.doExec(query, params);
|
|
1060
1162
|
}
|
|
1061
1163
|
|
|
1164
|
+
/**
|
|
1165
|
+
* Runs similarity search in NBase and hydrates the matching rows from the
|
|
1166
|
+
* relational table, keeping the `{ ...row, distance }` shape the other
|
|
1167
|
+
* engines return.
|
|
1168
|
+
*/
|
|
1169
|
+
private async searchViaNBase(
|
|
1170
|
+
nbase: { client: NBaseVectorClient; config: NBaseAdapterConfig },
|
|
1171
|
+
args: { vector: number[]; take?: number; where?: any },
|
|
1172
|
+
metric: 'cosine' | 'euclidean' | 'dot',
|
|
1173
|
+
take: number,
|
|
1174
|
+
): Promise<(T & { distance: number })[]> {
|
|
1175
|
+
const { client, config } = nbase;
|
|
1176
|
+
const idField = config.idField ?? 'an5Id';
|
|
1177
|
+
const modelField = config.modelField ?? 'an5Model';
|
|
1178
|
+
|
|
1179
|
+
const results = await client.search(args.vector, {
|
|
1180
|
+
k: take,
|
|
1181
|
+
// NBase implements cosine and euclidean; a dot-product request falls back
|
|
1182
|
+
// to cosine, which still orders the results sensibly.
|
|
1183
|
+
distanceMetric: metric === 'euclidean' ? 'euclidean' : 'cosine',
|
|
1184
|
+
includeMetadata: true,
|
|
1185
|
+
...(config.method ? { method: config.method } : {}),
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// Map each hit back to its row id. Ids are written as `Model:id`, and the
|
|
1189
|
+
// metadata key is honoured for projects that index vectors themselves.
|
|
1190
|
+
const ids: string[] = [];
|
|
1191
|
+
const distances = new Map<string, number>();
|
|
1192
|
+
for (const hit of results) {
|
|
1193
|
+
const fromMetadata = hit.metadata?.[idField];
|
|
1194
|
+
const rawId = fromMetadata !== undefined && fromMetadata !== null
|
|
1195
|
+
? String(fromMetadata)
|
|
1196
|
+
: parseVectorId(hit.id).id;
|
|
1197
|
+
if (!rawId) continue;
|
|
1198
|
+
const model = hit.metadata?.[modelField] ?? parseVectorId(hit.id).model;
|
|
1199
|
+
ids.push(model === this.modelName ? rawId : `${model}:${rawId}`);
|
|
1200
|
+
distances.set(ids[ids.length - 1] as string, hit.dist);
|
|
1201
|
+
}
|
|
1202
|
+
if (ids.length === 0) return [];
|
|
1203
|
+
|
|
1204
|
+
const pk = this.primaryKeyColumn();
|
|
1205
|
+
const inList = ids.map((_, i) => `@nbase_id_${i}`).join(', ');
|
|
1206
|
+
const idParams: Record<string, any> = {};
|
|
1207
|
+
ids.forEach((id, i) => { idParams[`nbase_id_${i}`] = id; });
|
|
1208
|
+
idParams.take = take;
|
|
1209
|
+
|
|
1210
|
+
const selectColumns = this.modelFields().map((f) => quote(f, this.dialect)).join(', ');
|
|
1211
|
+
const columns = selectColumns ? `${selectColumns}, ${quote(pk, this.dialect)} AS __an5_nbase_key` : `${quote(pk, this.dialect)} AS __an5_nbase_key`;
|
|
1212
|
+
const query = `SELECT ${columns} FROM ${this.tableName} WHERE ${quote(pk, this.dialect)} IN (${inList})`;
|
|
1213
|
+
|
|
1214
|
+
const rows = await this.adapter.exec<Record<string, any>>(query, idParams);
|
|
1215
|
+
const byKey = new Map<string, Record<string, any>>();
|
|
1216
|
+
for (const row of rows ?? []) {
|
|
1217
|
+
byKey.set(String(row.__an5_nbase_key), row);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const hydrated: (T & { distance: number })[] = [];
|
|
1221
|
+
for (const id of ids) {
|
|
1222
|
+
const row = byKey.get(id);
|
|
1223
|
+
if (!row) continue;
|
|
1224
|
+
const { __an5_nbase_key, ...rest } = row;
|
|
1225
|
+
hydrated.push({ ...(rest as T), distance: distances.get(id) ?? 0 } as T & { distance: number });
|
|
1226
|
+
if (hydrated.length >= take) break;
|
|
1227
|
+
}
|
|
1228
|
+
return hydrated;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Vector search for an adapter configured with only an NBase connection
|
|
1233
|
+
* string. The hit id and metadata are the result, since there is no
|
|
1234
|
+
* relational row to hydrate.
|
|
1235
|
+
*/
|
|
1236
|
+
private async searchViaNBaseOnly(
|
|
1237
|
+
nbase: { client: NBaseVectorClient; config: NBaseAdapterConfig },
|
|
1238
|
+
args: { vector: number[]; take?: number },
|
|
1239
|
+
metric: 'cosine' | 'euclidean' | 'dot',
|
|
1240
|
+
take: number,
|
|
1241
|
+
): Promise<Array<Record<string, unknown> & { distance: number }>> {
|
|
1242
|
+
const { client, config } = nbase;
|
|
1243
|
+
const idField = config.idField ?? 'an5Id';
|
|
1244
|
+
const results = await client.search(args.vector, {
|
|
1245
|
+
k: take,
|
|
1246
|
+
distanceMetric: metric === 'euclidean' ? 'euclidean' : 'cosine',
|
|
1247
|
+
includeMetadata: true,
|
|
1248
|
+
...(config.method ? { method: config.method } : {}),
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
return results.map((hit) => {
|
|
1252
|
+
const metadata = { ...(hit.metadata ?? {}) };
|
|
1253
|
+
if (metadata[idField] === undefined) {
|
|
1254
|
+
const parsed = parseVectorId(hit.id);
|
|
1255
|
+
if (parsed.id) metadata[idField] = parsed.id;
|
|
1256
|
+
if (parsed.model) metadata[config.modelField ?? 'an5Model'] = parsed.model;
|
|
1257
|
+
}
|
|
1258
|
+
return { id: hit.id, ...metadata, distance: hit.dist };
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/** The primary key column registered for this model, falling back to `id`. */
|
|
1263
|
+
private primaryKeyColumn(): string {
|
|
1264
|
+
const fields = this.modelFields();
|
|
1265
|
+
const marked = fields.find((f) => f === 'id');
|
|
1266
|
+
return marked ?? fields[0] ?? 'id';
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** Scalar column names known for this model from the generated metadata. */
|
|
1270
|
+
private modelFields(): string[] {
|
|
1271
|
+
const fields = getFieldsForModel(this.modelName);
|
|
1272
|
+
if (fields && typeof fields === 'object' && !Array.isArray(fields)) {
|
|
1273
|
+
return Object.keys(fields as Record<string, unknown>);
|
|
1274
|
+
}
|
|
1275
|
+
return [];
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Pushes a table's embedding column into NBase so it can be searched there.
|
|
1280
|
+
*
|
|
1281
|
+
* Each vector keeps the row id in its metadata, which is what lets
|
|
1282
|
+
* `vectorSearch` return real rows instead of bare vector ids.
|
|
1283
|
+
*/
|
|
1284
|
+
async indexVectorsInNBase(args: {
|
|
1285
|
+
vectorField?: string;
|
|
1286
|
+
where?: any;
|
|
1287
|
+
take?: number;
|
|
1288
|
+
batchSize?: number;
|
|
1289
|
+
} = {}): Promise<{ indexed: number }> {
|
|
1290
|
+
const nbase = this.adapter.nbaseClient?.() ?? null;
|
|
1291
|
+
if (!nbase) {
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
'No NBase instance configured. Use a nbase:// connection string, e.g. createAn5Adapter({ connectionString: "nbase://localhost:1307" }).',
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
if (this.adapter.nbaseOnly) {
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
'indexVectorsInNBase reads the vectors from a table, but this adapter was created with an nbase:// connection string and has no database. ' +
|
|
1299
|
+
'Create the adapter with your database connection string to index, or add the vectors with the NBase client directly.',
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
const vectorField = args.vectorField || 'embedding';
|
|
1303
|
+
const batchSize = args.batchSize ?? 200;
|
|
1304
|
+
const pk = this.primaryKeyColumn();
|
|
1305
|
+
const rows = await this.findMany({ ...(args.where ? { where: args.where } : {}), ...(args.take ? { take: args.take } : {}) });
|
|
1306
|
+
|
|
1307
|
+
let indexed = 0;
|
|
1308
|
+
for (let i = 0; i < rows.length; i += batchSize) {
|
|
1309
|
+
const batch = rows.slice(i, i + batchSize);
|
|
1310
|
+
const vectors = batch
|
|
1311
|
+
.map((row) => {
|
|
1312
|
+
const raw = (row as Record<string, unknown>)[vectorField];
|
|
1313
|
+
if (raw === undefined || raw === null) return null;
|
|
1314
|
+
let vector: number[];
|
|
1315
|
+
try {
|
|
1316
|
+
vector = typeof raw === 'string' ? JSON.parse(raw) : (raw as number[]);
|
|
1317
|
+
} catch {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
if (!Array.isArray(vector) || vector.length === 0) return null;
|
|
1321
|
+
return {
|
|
1322
|
+
id: buildVectorId(this.modelName, String((row as Record<string, unknown>)[pk])),
|
|
1323
|
+
vector,
|
|
1324
|
+
metadata: {
|
|
1325
|
+
[nbase.config.idField ?? 'an5Id']: String((row as Record<string, unknown>)[pk]),
|
|
1326
|
+
[nbase.config.modelField ?? 'an5Model']: this.modelName,
|
|
1327
|
+
},
|
|
1328
|
+
};
|
|
1329
|
+
})
|
|
1330
|
+
.filter((v): v is NonNullable<typeof v> => v !== null);
|
|
1331
|
+
|
|
1332
|
+
if (vectors.length === 0) continue;
|
|
1333
|
+
const result = await nbase.client.addVectors(vectors);
|
|
1334
|
+
indexed += result.count;
|
|
1335
|
+
}
|
|
1336
|
+
return { indexed };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1062
1339
|
async vectorSearch(args: {
|
|
1063
1340
|
vector: number[];
|
|
1064
1341
|
take?: number;
|
|
@@ -1078,6 +1355,25 @@ export class AdapterTableClient<T = any> {
|
|
|
1078
1355
|
const col = quote(vectorField, this.dialect);
|
|
1079
1356
|
const params: Record<string, any> = { query_vector: vectorJson };
|
|
1080
1357
|
|
|
1358
|
+
// 0. NBase, when the project configured one: the vectors live in the vector
|
|
1359
|
+
// database, so the search never loads the table into memory.
|
|
1360
|
+
const nbase = this.adapter.nbaseClient?.() ?? null;
|
|
1361
|
+
if (nbase) {
|
|
1362
|
+
try {
|
|
1363
|
+
// With an NBase-only connection string there is no table to read rows
|
|
1364
|
+
// from, so the hits are returned as the vector metadata itself.
|
|
1365
|
+
if (this.adapter.nbaseOnly) {
|
|
1366
|
+
return (await this.searchViaNBaseOnly(nbase, args, metric, take)) as (T & { distance: number })[];
|
|
1367
|
+
}
|
|
1368
|
+
return await this.searchViaNBase(nbase, args, metric, take);
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
if (!(err instanceof NBaseError)) throw err;
|
|
1371
|
+
// A NBase outage must not take vector search down with it; fall through
|
|
1372
|
+
// to the database instead of failing the query.
|
|
1373
|
+
console.warn(`[an5] NBase vector search unavailable (${err.message}); falling back to ${this.dialect}.`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1081
1377
|
// 1. Try native dialect execution
|
|
1082
1378
|
if (this.dialect === 'postgres') {
|
|
1083
1379
|
const op = metric === 'cosine' ? '<=>' : metric === 'dot' ? '<#>' : '<->';
|
|
@@ -88,10 +88,11 @@ export function parseWhere(
|
|
|
88
88
|
if (!where) return '';
|
|
89
89
|
const conditions: string[] = [];
|
|
90
90
|
|
|
91
|
+
const relations = ctx?.relationMap?.[modelName] || {};
|
|
91
92
|
const cleanWhere: Record<string, any> = {};
|
|
92
93
|
for (const [key, value] of Object.entries(where)) {
|
|
93
94
|
if (
|
|
94
|
-
key.includes('_') &&
|
|
95
|
+
key.includes('_') && !relations[key] &&
|
|
95
96
|
value && typeof value === 'object' &&
|
|
96
97
|
!(value instanceof Date) &&
|
|
97
98
|
!isOperatorValue(value)
|
|
@@ -102,23 +103,23 @@ export function parseWhere(
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const relations = ctx?.relationMap?.[modelName] || {};
|
|
106
106
|
const colPrefix = ctx?.colPrefix || '';
|
|
107
107
|
|
|
108
108
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
109
|
+
if (value === undefined) continue;
|
|
109
110
|
if (key === 'OR' && Array.isArray(value)) {
|
|
110
|
-
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx))
|
|
111
|
-
|
|
111
|
+
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx) || '1=1');
|
|
112
|
+
conditions.push(sub.length > 0 ? `(${sub.join(' OR ')})` : '1=0');
|
|
112
113
|
} else if (key === 'AND') {
|
|
113
114
|
const items = Array.isArray(value) ? value : [value];
|
|
114
115
|
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}and_${i}_`, ctx)).filter(Boolean);
|
|
115
116
|
if (sub.length > 0) conditions.push(`(${sub.join(' AND ')})`);
|
|
116
117
|
} else if (key === 'NOT') {
|
|
117
118
|
const items = Array.isArray(value) ? value : [value];
|
|
118
|
-
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx))
|
|
119
|
-
if (sub.length > 0) conditions.push(`NOT (${sub.join('
|
|
119
|
+
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx) || '1=1');
|
|
120
|
+
if (sub.length > 0) conditions.push(`NOT (${sub.join(' OR ')})`);
|
|
120
121
|
} else if (relations[key] && value && typeof value === 'object') {
|
|
121
|
-
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, prefix
|
|
122
|
+
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, `${prefix}${key}_`, ctx);
|
|
122
123
|
if (clause) conditions.push(clause);
|
|
123
124
|
} else {
|
|
124
125
|
const pname = sanitizeParamName(`${prefix}${key}`);
|
|
@@ -203,28 +204,28 @@ function buildRelationClause(
|
|
|
203
204
|
|
|
204
205
|
if (isMany) {
|
|
205
206
|
if (value.some !== undefined) {
|
|
206
|
-
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, prefix
|
|
207
|
+
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, `${prefix}some_`, ctx, childAlias);
|
|
207
208
|
parts.push(buildExists(inner));
|
|
208
209
|
}
|
|
209
210
|
if (value.none !== undefined) {
|
|
210
|
-
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, prefix
|
|
211
|
+
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, `${prefix}none_`, ctx, childAlias);
|
|
211
212
|
parts.push(`NOT ${buildExists(inner)}`);
|
|
212
213
|
}
|
|
213
214
|
if (value.every !== undefined) {
|
|
214
215
|
// "every" = no related row violates the condition.
|
|
215
|
-
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, prefix
|
|
216
|
+
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, `${prefix}every_`, ctx, childAlias);
|
|
216
217
|
const negated = inner ? `NOT (${inner})` : '';
|
|
217
218
|
const violated = `EXISTS (SELECT 1 FROM ${childTable} AS ${childAlias} WHERE ${correlation}${negated ? ` AND ${negated}` : ' AND 1=0'})`;
|
|
218
219
|
parts.push(`NOT ${violated}`);
|
|
219
220
|
}
|
|
220
221
|
} else {
|
|
221
222
|
if (value.is !== undefined) {
|
|
222
|
-
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, prefix
|
|
223
|
-
parts.push(buildExists(inner));
|
|
223
|
+
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, `${prefix}is_`, ctx, childAlias);
|
|
224
|
+
parts.push(value.is === null ? `NOT ${buildExists(inner)}` : buildExists(inner));
|
|
224
225
|
}
|
|
225
226
|
if (value.isNot !== undefined) {
|
|
226
|
-
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, prefix
|
|
227
|
-
parts.push(`NOT ${buildExists(inner)}`);
|
|
227
|
+
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, `${prefix}isNot_`, ctx, childAlias);
|
|
228
|
+
parts.push(value.isNot === null ? buildExists(inner) : `NOT ${buildExists(inner)}`);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
@@ -2,6 +2,12 @@ export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets'
|
|
|
2
2
|
|
|
3
3
|
export interface An5AdapterConfig {
|
|
4
4
|
connectionString?: string;
|
|
5
|
+
/**
|
|
6
|
+
* NBase (Neural Vector Database) endpoint, written as a connection string,
|
|
7
|
+
* e.g. `nbase://localhost:1307`. Attach it here when vector search should
|
|
8
|
+
* run in NBase while the rows stay in `connectionString`'s database.
|
|
9
|
+
*/
|
|
10
|
+
nbase?: string;
|
|
5
11
|
engine?: QueryEngine;
|
|
6
12
|
db?: any;
|
|
7
13
|
driver?: any;
|
|
@@ -21,9 +21,7 @@ export function matchWhere(row: Record<string, any>, where: any): boolean {
|
|
|
21
21
|
key.includes('_') &&
|
|
22
22
|
value && typeof value === 'object' &&
|
|
23
23
|
!(value instanceof Date) &&
|
|
24
|
-
!
|
|
25
|
-
!(value as any).not && !(value as any).gte &&
|
|
26
|
-
!(value as any).lte && !(value as any).gt && !(value as any).lt
|
|
24
|
+
!['equals', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'not', 'gte', 'lte', 'gt', 'lt'].some(op => op in value)
|
|
27
25
|
) {
|
|
28
26
|
Object.assign(cleanWhere, value);
|
|
29
27
|
} else {
|
|
@@ -32,12 +30,14 @@ export function matchWhere(row: Record<string, any>, where: any): boolean {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
33
|
+
if (value === undefined) continue;
|
|
35
34
|
if (key === 'OR' && Array.isArray(value)) {
|
|
36
35
|
if (!value.some((v: any) => matchWhere(row, v))) return false;
|
|
37
36
|
continue;
|
|
38
37
|
}
|
|
39
|
-
if (key === 'AND'
|
|
40
|
-
|
|
38
|
+
if (key === 'AND') {
|
|
39
|
+
const clauses = Array.isArray(value) ? value : [value];
|
|
40
|
+
if (!clauses.every((v: any) => matchWhere(row, v))) return false;
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
43
|
if (key === 'NOT') {
|
|
@@ -57,17 +57,20 @@ export function matchWhere(row: Record<string, any>, where: any): boolean {
|
|
|
57
57
|
|
|
58
58
|
if (v.not !== undefined) {
|
|
59
59
|
if (v.not === null) { if (cellVal === null || cellVal === undefined) return false; }
|
|
60
|
+
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
61
|
+
if (matchWhere(row, { [key]: v.not })) return false;
|
|
62
|
+
}
|
|
60
63
|
else if (normalize(cellVal) == normalize(v.not)) return false;
|
|
61
64
|
}
|
|
62
65
|
if (v.equals !== undefined && normalize(cellVal) != normalize(v.equals)) return false;
|
|
63
66
|
if (v.contains !== undefined) {
|
|
64
|
-
if (
|
|
67
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).includes(normalize(v.contains))) return false;
|
|
65
68
|
}
|
|
66
69
|
if (v.startsWith !== undefined) {
|
|
67
|
-
if (
|
|
70
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).startsWith(normalize(v.startsWith))) return false;
|
|
68
71
|
}
|
|
69
72
|
if (v.endsWith !== undefined) {
|
|
70
|
-
if (
|
|
73
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).endsWith(normalize(v.endsWith))) return false;
|
|
71
74
|
}
|
|
72
75
|
if (v.gte !== undefined && !(Number(cellVal) >= Number(v.gte))) return false;
|
|
73
76
|
if (v.lte !== undefined && !(Number(cellVal) <= Number(v.lte))) return false;
|
package/typescript/src/index.ts
CHANGED
|
@@ -7,6 +7,23 @@ export {
|
|
|
7
7
|
setAdapterMetadata,
|
|
8
8
|
} from './an5Adapter';
|
|
9
9
|
export { parseWhere, buildOrderBy, quote } from './base/sql';
|
|
10
|
+
export {
|
|
11
|
+
NBaseVectorClient,
|
|
12
|
+
NBaseError,
|
|
13
|
+
createNBaseVectorClient,
|
|
14
|
+
createNBaseClient,
|
|
15
|
+
parseNBaseConnectionString,
|
|
16
|
+
buildVectorId,
|
|
17
|
+
parseVectorId,
|
|
18
|
+
} from './nbase';
|
|
19
|
+
export type {
|
|
20
|
+
NBaseAdapterConfig,
|
|
21
|
+
NBaseClientConfig,
|
|
22
|
+
NBaseVector,
|
|
23
|
+
NBaseSearchOptions,
|
|
24
|
+
NBaseSearchResult,
|
|
25
|
+
NBaseStats,
|
|
26
|
+
} from './nbase';
|
|
10
27
|
export type {
|
|
11
28
|
An5AdapterConfig,
|
|
12
29
|
AdapterMetadata,
|