@an5/adapters 0.2.13 → 0.2.14

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 CHANGED
@@ -68,7 +68,7 @@ dragged in three drivers would not fit.
68
68
  <dependency>
69
69
  <groupId>io.github.an5orm</groupId>
70
70
  <artifactId>an5-adapters-java</artifactId>
71
- <version>0.2.13</version>
71
+ <version>0.2.14</version>
72
72
  </dependency>
73
73
  ```
74
74
 
@@ -91,7 +91,7 @@ rather than reimplementing it, so the dialect rules and the where builder exist
91
91
 
92
92
  ```kotlin
93
93
  dependencies {
94
- implementation("io.github.an5orm:an5-adapters-kotlin:0.2.13")
94
+ implementation("io.github.an5orm:an5-adapters-kotlin:0.2.14")
95
95
  }
96
96
  ```
97
97
 
@@ -115,7 +115,7 @@ input. It needs the repository secrets `MAVEN_CENTRAL_USERNAME` and `MAVEN_CENTR
115
115
  ### Swift
116
116
 
117
117
  ```swift
118
- .package(url: "https://github.com/an5ORM/an5Adapters.git", from: "0.2.13")
118
+ .package(url: "https://github.com/an5ORM/an5Adapters.git", from: "0.2.14")
119
119
  ```
120
120
 
121
121
  A SwiftPM package that links the **system** SQLite — the database every Apple platform
@@ -169,7 +169,7 @@ returns the NBase hits — id, metadata and `distance` — instead of table rows
169
169
  With an `nbase://` connection string, search returns the hits themselves:
170
170
 
171
171
  ```ts
172
- const hits = await db.table('Article').vectorSearch({ vector: queryEmbedding, take: 5 });
172
+ const hits = await db.article.vectorSearch({ vector: queryEmbedding, take: 5 });
173
173
  // hits[0] = { id: 'Article:42', an5Id: '42', an5Model: 'Article', distance: 0.11 }
174
174
  ```
175
175
 
@@ -185,10 +185,10 @@ const db = createAn5Adapter({
185
185
  nbase: 'nbase://localhost:1307',
186
186
  });
187
187
 
188
- await db.table('Article').indexVectorsInNBase({ vectorField: 'embedding' });
188
+ await db.article.indexVectorsInNBase({ vectorField: 'embedding' });
189
189
  // → { indexed: 1200 }
190
190
 
191
- const hits = await db.table('Article').vectorSearch({ vector: queryEmbedding, take: 5 });
191
+ const hits = await db.article.vectorSearch({ vector: queryEmbedding, take: 5 });
192
192
  // hits[0] = { id, title, ..., distance }
193
193
  ```
194
194
 
@@ -229,9 +229,9 @@ that already holds JSON text (`'[0.1, 0.2]'`) keeps working, so nothing has to
229
229
  be migrated.
230
230
 
231
231
  ```ts
232
- await db.table('Article').create({ data: { title: '…', embedding: queryEmbedding } });
232
+ await db.article.create({ data: { title: '…', embedding: queryEmbedding } });
233
233
 
234
- const row = await db.table('Article').findFirst({ where: { title: '…' } });
234
+ const row = await db.article.findFirst({ where: { title: '…' } });
235
235
  row.embedding; // number[]
236
236
  ```
237
237
 
@@ -260,7 +260,8 @@ const db = createAn5Adapter({
260
260
  ```
261
261
 
262
262
  `vectorStrategy` pins one strategy instead of probing — `'auto'` (the default),
263
- `'sqlite-vec'`, `'udf'`, `'sql'` or `'memory'`. Every other runtime implements
263
+ `'sqlite-vec'`, `'udf'`, `'sql'` or `'memory'`. In TypeScript it can also be
264
+ supplied per query on `vectorSearch`. Every other runtime implements
264
265
  the same four strategies with the same order; `sqlx` (Rust) and JDBC (Java,
265
266
  Kotlin) use `sql` for JSON text and `memory` for BLOB columns without
266
267
  driver-specific vector functions.
@@ -134,9 +134,12 @@ export declare class ViewClient<T = any> {
134
134
  export declare class An5AdapterTx {
135
135
  private readonly handle;
136
136
  readonly dialect: Dialect;
137
+ private readonly parentVectorSupport;
138
+ readonly vectorStrategy?: VectorStrategyPreference | undefined;
137
139
  [key: string]: any;
138
140
  private closed;
139
- constructor(handle: TransactionHandle, dialect: Dialect);
141
+ constructor(handle: TransactionHandle, dialect: Dialect, parentVectorSupport?: SqliteVectorSupport | null, vectorStrategy?: VectorStrategyPreference | undefined);
142
+ sqliteVectorSupport(): Promise<SqliteVectorSupport | null>;
140
143
  exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]>;
141
144
  _executeRaw(query: string, params?: Record<string, any>): Promise<number>;
142
145
  $commit(): Promise<void>;
@@ -271,6 +274,7 @@ export declare class AdapterTableClient<T = any> {
271
274
  vectorField?: string;
272
275
  distanceMetric?: 'cosine' | 'euclidean' | 'dot';
273
276
  vectorElementType?: 'float32' | 'float16' | 'uint8';
277
+ vectorStrategy?: VectorStrategyPreference;
274
278
  }): Promise<(T & {
275
279
  distance: number;
276
280
  })[]>;
@@ -360,7 +360,7 @@ class An5Adapter {
360
360
  }
361
361
  /** `vectorStrategy` from the config, so a project can pin one strategy. */
362
362
  get vectorStrategy() {
363
- return this._engineConfig?.vectorStrategy;
363
+ return this._engineConfig?.vectorStrategy ?? this.engine?.vectorSupport?.options?.vectorStrategy;
364
364
  }
365
365
  /** INTERNAL: used by AdapterTableClient for DML statements needing row count */
366
366
  async _executeRaw(query, params) {
@@ -449,7 +449,7 @@ class An5Adapter {
449
449
  return Promise.all(fn);
450
450
  const engine = await this.requireEngine();
451
451
  const handle = await engine.beginTransaction();
452
- const txAdapter = new An5AdapterTx(handle, this.dialect);
452
+ const txAdapter = new An5AdapterTx(handle, this.dialect, engine.vectorSupport ?? null, this.vectorStrategy);
453
453
  try {
454
454
  const result = await fn(txAdapter);
455
455
  await handle.commit();
@@ -465,7 +465,7 @@ class An5Adapter {
465
465
  throw new Error('Interactive transactions are not supported by the Google Sheets adapter');
466
466
  }
467
467
  const engine = await this.requireEngine();
468
- return new An5AdapterTx(await engine.beginTransaction(), this.dialect);
468
+ return new An5AdapterTx(await engine.beginTransaction(), this.dialect, engine.vectorSupport ?? null, this.vectorStrategy);
469
469
  }
470
470
  table(modelName) {
471
471
  if (this.sheetsAdapter)
@@ -633,12 +633,17 @@ class ViewClient {
633
633
  exports.ViewClient = ViewClient;
634
634
  // ─── Transaction-scoped adapter ────────────────────────────────────────────────────
635
635
  class An5AdapterTx {
636
- constructor(handle, dialect) {
636
+ constructor(handle, dialect, parentVectorSupport = null, vectorStrategy) {
637
637
  this.handle = handle;
638
638
  this.dialect = dialect;
639
+ this.parentVectorSupport = parentVectorSupport;
640
+ this.vectorStrategy = vectorStrategy;
639
641
  this.closed = false;
640
642
  return (0, metadata_1.createAdapterProxy)(this, (name) => this.table(name));
641
643
  }
644
+ async sqliteVectorSupport() {
645
+ return this.dialect === 'sqlite' ? this.parentVectorSupport : null;
646
+ }
642
647
  async exec(query, params) {
643
648
  this.ensureOpen();
644
649
  return this.handle.exec(query, params);
@@ -1412,7 +1417,7 @@ class AdapterTableClient {
1412
1417
  take,
1413
1418
  tail: whereSql ? ` WHERE ${whereSql}` : '',
1414
1419
  params,
1415
- preference: this.adapter.vectorStrategy,
1420
+ preference: args.vectorStrategy ?? this.adapter.vectorStrategy ?? support?.options?.vectorStrategy,
1416
1421
  });
1417
1422
  if (ranked)
1418
1423
  return this.decodeVectorFields(ranked.rows);
@@ -351,12 +351,8 @@ class SqliteVectorSupport {
351
351
  * since taken new float32 rows would otherwise rank just the old ones.
352
352
  */
353
353
  async hasBinaryRows(exec, table, column, tail, params) {
354
- const key = `${table}.${column}`;
355
- if (!this.binaryRows.has(key)) {
356
- const rows = await exec(`SELECT 1 FROM ${table}${tail ? `${tail} AND` : ' WHERE'} typeof(${column}) = 'blob' LIMIT 1`, params).catch(() => []);
357
- this.binaryRows.set(key, rows.length > 0);
358
- }
359
- return this.binaryRows.get(key) === true;
354
+ const rows = await exec(`SELECT 1 FROM ${table}${tail ? `${tail} AND` : ' WHERE'} typeof(${column}) = 'blob' LIMIT 1`, params).catch(() => []);
355
+ return rows.length > 0;
360
356
  }
361
357
  reset() {
362
358
  this.caps = null;
@@ -406,7 +402,7 @@ async function runSqliteVectorSearch(args) {
406
402
  for (const strategy of planSqliteVectorStrategies(caps, declaredType, args.preference)) {
407
403
  if (strategy === 'sql') {
408
404
  const binary = args.vectorSupport
409
- ? await args.vectorSupport.hasBinaryRows(args.exec, args.rawTable, args.rawColumn, args.tail ?? '', args.params)
405
+ ? await args.vectorSupport.hasBinaryRows(args.exec, args.table, args.column, args.tail ?? '', args.params)
410
406
  : (await args.exec(`SELECT 1 FROM ${args.table}${args.tail ? `${args.tail} AND` : ' WHERE'} typeof(${args.column}) = 'blob' LIMIT 1`, args.params)).length > 0;
411
407
  if (binary)
412
408
  continue;
@@ -908,7 +908,20 @@ namespace An5Orm
908
908
  public List<(T Item, double Distance)> VectorSearch(VectorSearchArgs args)
909
909
  {
910
910
  args ??= new VectorSearchArgs();
911
- return VectorSearch(args.Vector, args.Take, null, null, args.VectorField, args.DistanceMetric);
911
+ Dictionary<string, object> parameters = null;
912
+ string whereSql = null;
913
+ if (args.Where != null)
914
+ {
915
+ parameters = new Dictionary<string, object>();
916
+ whereSql = SqlBuilder.ParseWhere(args.Where, parameters, _dialect);
917
+ }
918
+ return VectorSearch(
919
+ args.Vector,
920
+ args.Take,
921
+ string.IsNullOrEmpty(whereSql) ? null : whereSql,
922
+ parameters,
923
+ args.VectorField,
924
+ args.DistanceMetric);
912
925
  }
913
926
 
914
927
  public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null, string orderBy = null)
package/java/pom.xml CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  <groupId>io.github.an5orm</groupId>
19
19
  <artifactId>an5-adapters-java</artifactId>
20
- <version>0.2.13</version>
20
+ <version>0.2.14</version>
21
21
  <packaging>jar</packaging>
22
22
 
23
23
  <name>AN5 ORM Java adapter</name>
@@ -486,7 +486,6 @@ public class An5TableClient {
486
486
  int limit = take == null ? 10 : take;
487
487
  String field = vectorField == null || vectorField.isEmpty() ? "embedding" : vectorField;
488
488
  String distanceMetric = metric == null || metric.isEmpty() ? "cosine" : metric;
489
- int dimension = vector.length;
490
489
 
491
490
  Map<String, Object> params = new LinkedHashMap<String, Object>();
492
491
  SqlBuilder.Where clause = SqlBuilder.parseWhere(where, dialect, params, "");
@@ -9,7 +9,7 @@ plugins {
9
9
  // phone app cannot resolve is not a mobile runtime. Maven consumers can still use the
10
10
  // published Maven coordinates; this module publishes both.
11
11
  group = "io.github.an5orm"
12
- version = "0.2.13"
12
+ version = "0.2.14"
13
13
 
14
14
  repositories {
15
15
  // The Java runtime is a sibling checkout, not a published artifact during development:
@@ -63,7 +63,7 @@ const db = createAn5Adapter({
63
63
  connectionString: 'sqlite:///app.db',
64
64
  sqliteVec: '/absolute/path/an5_vector.so', // .dylib / .dll on other platforms
65
65
  });
66
- await db.table('Document').vectorSearch({ vector: query, take: 10 });
66
+ await db.document.vectorSearch({ vector: query, take: 10 });
67
67
  ```
68
68
 
69
69
  ```python
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@an5/adapters",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Standalone language adapters for AN5 ORM - TypeScript, Python, .NET, Go, Rust, Java, Kotlin, Swift, and Google Sheets query execution.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -313,17 +313,14 @@ class SqliteVectorSupport:
313
313
  Checked only for the `sql` strategy, where a legacy JSON column that has
314
314
  since taken new float32 rows would otherwise rank just the old ones.
315
315
  """
316
- key = f"{table}.{column}"
317
- if key not in self._has_binary_rows:
318
- clause = (tail + " AND ") if tail else " WHERE "
319
- try:
320
- rows = exec_fn(
321
- f"SELECT 1 FROM {table}{clause}typeof({column}) = 'blob' LIMIT 1",
322
- list(params))
323
- self._has_binary_rows[key] = bool(rows)
324
- except Exception:
325
- self._has_binary_rows[key] = False
326
- return self._has_binary_rows[key]
316
+ clause = (tail + " AND ") if tail else " WHERE "
317
+ try:
318
+ rows = exec_fn(
319
+ f"SELECT 1 FROM {table}{clause}typeof({column}) = 'blob' LIMIT 1",
320
+ list(params))
321
+ return bool(rows)
322
+ except Exception:
323
+ return False
327
324
 
328
325
  def capabilities(self) -> Dict[str, bool]:
329
326
  """Probes once and caches the answer."""
@@ -415,7 +412,7 @@ def run_sqlite_vector_search(
415
412
  else read_sqlite_column_type(exec_fn, raw_table, raw_column))
416
413
  for strategy in plan_sqlite_vector_strategies(caps, declared, preference):
417
414
  if strategy == STRATEGY_SQL:
418
- binary = (support.has_binary_rows(exec_fn, raw_table, raw_column, tail, list(params or []))
415
+ binary = (support.has_binary_rows(exec_fn, table, column, tail, list(params or []))
419
416
  if support is not None
420
417
  else bool(exec_fn("SELECT 1 FROM " + table + (tail + " AND " if tail else " WHERE ")
421
418
  + f"typeof({column}) = 'blob' LIMIT 1", list(params or []))))
@@ -411,7 +411,7 @@ export class An5Adapter {
411
411
 
412
412
  /** `vectorStrategy` from the config, so a project can pin one strategy. */
413
413
  get vectorStrategy(): VectorStrategyPreference | undefined {
414
- return this._engineConfig?.vectorStrategy;
414
+ return this._engineConfig?.vectorStrategy ?? (this.engine as any)?.vectorSupport?.options?.vectorStrategy;
415
415
  }
416
416
 
417
417
  /** INTERNAL: used by AdapterTableClient for DML statements needing row count */
@@ -500,7 +500,7 @@ export class An5Adapter {
500
500
  if (Array.isArray(fn)) return Promise.all(fn);
501
501
  const engine = await this.requireEngine();
502
502
  const handle = await engine.beginTransaction();
503
- const txAdapter = new An5AdapterTx(handle, this.dialect);
503
+ const txAdapter = new An5AdapterTx(handle, this.dialect, engine.vectorSupport ?? null, this.vectorStrategy);
504
504
  try {
505
505
  const result = await fn(txAdapter);
506
506
  await handle.commit();
@@ -516,7 +516,7 @@ export class An5Adapter {
516
516
  throw new Error('Interactive transactions are not supported by the Google Sheets adapter');
517
517
  }
518
518
  const engine = await this.requireEngine();
519
- return new An5AdapterTx(await engine.beginTransaction(), this.dialect);
519
+ return new An5AdapterTx(await engine.beginTransaction(), this.dialect, engine.vectorSupport ?? null, this.vectorStrategy);
520
520
  }
521
521
 
522
522
  table<T = any>(modelName: string): AdapterTableClient<T> | SheetsTableClient<T> {
@@ -714,10 +714,16 @@ export class An5AdapterTx {
714
714
  constructor(
715
715
  private readonly handle: TransactionHandle,
716
716
  public readonly dialect: Dialect,
717
+ private readonly parentVectorSupport: SqliteVectorSupport | null = null,
718
+ public readonly vectorStrategy?: VectorStrategyPreference | undefined,
717
719
  ) {
718
720
  return createAdapterProxy(this, (name) => this.table(name));
719
721
  }
720
722
 
723
+ async sqliteVectorSupport(): Promise<SqliteVectorSupport | null> {
724
+ return this.dialect === 'sqlite' ? this.parentVectorSupport : null;
725
+ }
726
+
721
727
  async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
722
728
  this.ensureOpen();
723
729
  return this.handle.exec<T>(query, params);
@@ -1462,6 +1468,7 @@ export class AdapterTableClient<T = any> {
1462
1468
  vectorField?: string;
1463
1469
  distanceMetric?: 'cosine' | 'euclidean' | 'dot';
1464
1470
  vectorElementType?: 'float32' | 'float16' | 'uint8';
1471
+ vectorStrategy?: VectorStrategyPreference;
1465
1472
  }): Promise<(T & { distance: number })[]> {
1466
1473
  const vectorField = args.vectorField || 'embedding';
1467
1474
  const METRICS = ['cosine', 'euclidean', 'dot'];
@@ -1521,7 +1528,7 @@ export class AdapterTableClient<T = any> {
1521
1528
  take,
1522
1529
  tail: whereSql ? ` WHERE ${whereSql}` : '',
1523
1530
  params,
1524
- preference: (this.adapter as any).vectorStrategy,
1531
+ preference: args.vectorStrategy ?? (this.adapter as any).vectorStrategy ?? support?.options?.vectorStrategy,
1525
1532
  });
1526
1533
  if (ranked) return this.decodeVectorFields(ranked.rows) as (T & { distance: number })[];
1527
1534
  // No in-database strategy: score the column in the client instead.
@@ -422,15 +422,11 @@ export class SqliteVectorSupport {
422
422
  tail: string,
423
423
  params: Record<string, any>,
424
424
  ): Promise<boolean> {
425
- const key = `${table}.${column}`;
426
- if (!this.binaryRows.has(key)) {
427
- const rows = await exec(
428
- `SELECT 1 FROM ${table}${tail ? `${tail} AND` : ' WHERE'} typeof(${column}) = 'blob' LIMIT 1`,
429
- params,
430
- ).catch(() => []);
431
- this.binaryRows.set(key, rows.length > 0);
432
- }
433
- return this.binaryRows.get(key) === true;
425
+ const rows = await exec(
426
+ `SELECT 1 FROM ${table}${tail ? `${tail} AND` : ' WHERE'} typeof(${column}) = 'blob' LIMIT 1`,
427
+ params,
428
+ ).catch(() => []);
429
+ return rows.length > 0;
434
430
  }
435
431
 
436
432
  reset(): void {
@@ -510,7 +506,7 @@ export async function runSqliteVectorSearch(
510
506
  for (const strategy of planSqliteVectorStrategies(caps, declaredType, args.preference)) {
511
507
  if (strategy === 'sql') {
512
508
  const binary = args.vectorSupport
513
- ? await args.vectorSupport.hasBinaryRows(args.exec, args.rawTable, args.rawColumn, args.tail ?? '', args.params)
509
+ ? await args.vectorSupport.hasBinaryRows(args.exec, args.table, args.column, args.tail ?? '', args.params)
514
510
  : (await args.exec(
515
511
  `SELECT 1 FROM ${args.table}${args.tail ? `${args.tail} AND` : ' WHERE'} typeof(${args.column}) = 'blob' LIMIT 1`,
516
512
  args.params,