@an5/adapters 0.2.10 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +163 -2
  2. package/dist/an5Adapter.d.ts +37 -0
  3. package/dist/an5Adapter.js +190 -65
  4. package/dist/base/metadata.d.ts +2 -0
  5. package/dist/base/metadata.js +10 -0
  6. package/dist/base/sql.d.ts +2 -0
  7. package/dist/base/sql.js +24 -25
  8. package/dist/base/types.d.ts +14 -0
  9. package/dist/googlesheets/adapter.js +16 -8
  10. package/dist/googlesheets/config.d.ts +13 -0
  11. package/dist/googlesheets/config.js +5 -1
  12. package/dist/googlesheets/oauth.d.ts +3 -0
  13. package/dist/googlesheets/oauth.js +43 -0
  14. package/dist/googlesheets/parseConnectionString.js +12 -0
  15. package/dist/googlesheets/tableClient.js +5 -23
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +8 -1
  18. package/dist/sqlite/browserEngine.d.ts +13 -0
  19. package/dist/sqlite/browserEngine.js +13 -0
  20. package/dist/sqlite/engine.d.ts +5 -0
  21. package/dist/sqlite/engine.js +10 -0
  22. package/dist/sqlite/index.d.ts +1 -0
  23. package/dist/sqlite/index.js +1 -0
  24. package/dist/sqlite/vector.d.ts +155 -0
  25. package/dist/sqlite/vector.js +436 -0
  26. package/dotnet/An5Adapter.cs +85 -27
  27. package/dotnet/Base/Core.cs +14 -0
  28. package/dotnet/Mssql/MssqlEngine.cs +44 -1
  29. package/dotnet/Postgres/PostgresEngine.cs +2 -0
  30. package/dotnet/Sqlite/SqliteEngine.cs +82 -2
  31. package/dotnet/Sqlite/SqliteVectors.cs +401 -0
  32. package/golang/an5_adapter.go +84 -32
  33. package/golang/base/vectors.go +263 -0
  34. package/golang/sqlite/vector.go +248 -0
  35. package/golang/sqlite_vector_test.go +288 -0
  36. package/golang/table_client.go +67 -3
  37. package/java/pom.xml +177 -0
  38. package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
  39. package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
  40. package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
  41. package/java/src/main/java/an5/adapters/An5Query.java +100 -0
  42. package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
  43. package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
  44. package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
  45. package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
  46. package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
  47. package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
  48. package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
  49. package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
  50. package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
  51. package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
  52. package/java/src/test/java/An5AdaptersSmoke.java +733 -0
  53. package/java/src/test/java/Json.java +174 -0
  54. package/java/src/test/java/QuerySemantics.java +111 -0
  55. package/kotlin/build.gradle.kts +145 -0
  56. package/kotlin/settings.gradle.kts +10 -0
  57. package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
  58. package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
  59. package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
  60. package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
  61. package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
  62. package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
  63. package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
  64. package/native/sqlite/CMakeLists.txt +12 -0
  65. package/native/sqlite/README.md +117 -0
  66. package/native/sqlite/an5_vector.c +209 -0
  67. package/package.json +40 -9
  68. package/python/an5_adapter.py +37 -6
  69. package/python/base/__init__.py +15 -0
  70. package/python/base/vectors.py +434 -0
  71. package/python/sqlite/__init__.py +2 -2
  72. package/python/sqlite/provider.py +21 -1
  73. package/python/table_client.py +129 -59
  74. package/rust/src/adapter.rs +342 -24
  75. package/rust/src/base/mod.rs +7 -0
  76. package/rust/src/base/vectors.rs +293 -0
  77. package/rust/src/lib.rs +3 -3
  78. package/scripts/sqlite-native-build.js +32 -0
  79. package/swift/Package.swift +22 -0
  80. package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
  81. package/swift/Sources/An5Adapters/Metadata.swift +183 -0
  82. package/swift/Sources/An5Adapters/Query.swift +105 -0
  83. package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
  84. package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
  85. package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
  86. package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
  87. package/swift/Sources/An5Adapters/TableClient.swift +671 -0
  88. package/swift/Sources/An5Adapters/Types.swift +127 -0
  89. package/swift/Sources/An5Adapters/Vectors.swift +86 -0
  90. package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
  91. package/swift/Sources/CSQLite/module.modulemap +8 -0
  92. package/swift/Sources/CSQLite/shim.h +9 -0
  93. package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
  94. package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
  95. package/typescript/src/an5Adapter.ts +200 -58
  96. package/typescript/src/base/metadata.ts +8 -0
  97. package/typescript/src/base/sql.ts +23 -16
  98. package/typescript/src/base/types.ts +15 -0
  99. package/typescript/src/googlesheets/adapter.ts +17 -8
  100. package/typescript/src/googlesheets/config.ts +10 -1
  101. package/typescript/src/googlesheets/oauth.ts +31 -0
  102. package/typescript/src/googlesheets/parseConnectionString.ts +8 -0
  103. package/typescript/src/googlesheets/tableClient.ts +5 -19
  104. package/typescript/src/index.ts +13 -0
  105. package/typescript/src/sqlite/browserEngine.ts +24 -0
  106. package/typescript/src/sqlite/engine.ts +13 -0
  107. package/typescript/src/sqlite/index.ts +1 -1
  108. package/typescript/src/sqlite/vector.ts +541 -0
  109. package/golang/sqlite/sqlite.go +0 -14
@@ -26,10 +26,20 @@ import {
26
26
  type NBaseAdapterConfig,
27
27
  type NBaseVectorClient,
28
28
  } from './nbase';
29
- import { buildOrderBy, parseWhere, quote } from './base/sql';
29
+ import { bindParam, buildOrderBy, parseWhere, quote } from './base/sql';
30
+ import {
31
+ decodeVector,
32
+ isVectorField,
33
+ needsVectorEncoding,
34
+ runSqliteVectorSearch,
35
+ vectorDistance,
36
+ type SqliteVectorSupport,
37
+ type VectorStrategyPreference,
38
+ } from './sqlite/vector';
30
39
  import {
31
40
  createAdapterProxy,
32
41
  getFieldsForModel,
42
+ resolveIdField,
33
43
  getModelToTable,
34
44
  getRelationMap,
35
45
  getRelationsForModel,
@@ -51,39 +61,59 @@ function sanitizeParamName(name: string): string {
51
61
  return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
52
62
  }
53
63
 
64
+ /** Match only unique/primary-key violations, never general constraint errors. */
65
+ function isDuplicateKeyError(error: unknown, dialect: Dialect): boolean {
66
+ if (!error || typeof error !== 'object') return false;
67
+ const e = error as any;
68
+ if (dialect === 'postgres') return e.code === '23505';
69
+ if (dialect === 'mysql') return e.code === 'ER_DUP_ENTRY' || e.errno === 1062;
70
+ if (dialect === 'sqlite') {
71
+ return e.code === 'SQLITE_CONSTRAINT_UNIQUE' || e.code === 'SQLITE_CONSTRAINT_PRIMARYKEY'
72
+ || e.errcode === 2067 || e.errcode === 1555;
73
+ }
74
+ if (dialect === 'mssql') {
75
+ const number = e.number ?? e.originalError?.info?.number;
76
+ return number === 2601 || number === 2627;
77
+ }
78
+ return false;
79
+ }
80
+
54
81
  function appendUpdateSet(sets: string[], params: Record<string, any>, col: string, val: any, dialect: Dialect): void {
55
82
  const quoted = quote(col, dialect);
56
83
  const safeCol = sanitizeParamName(col);
57
- if (val && typeof val === 'object' && !(val instanceof Date)) {
84
+ // `TypedArray.prototype.set` is the copy method, not the update operator, so a
85
+ // vector written as a float32 BLOB must not be read as `{ set: ... }`.
86
+ const isLiteral = Array.isArray(val) || ArrayBuffer.isView(val) || val instanceof ArrayBuffer;
87
+ if (val && typeof val === 'object' && !(val instanceof Date) && !isLiteral) {
58
88
  if (val.increment !== undefined) {
59
- sets.push(`${quoted} = ${quoted} + @s_${safeCol}_inc`);
60
- params[`s_${safeCol}_inc`] = val.increment;
89
+ const p = bindParam(params, `s_${safeCol}_inc`, val.increment);
90
+ sets.push(`${quoted} = ${quoted} + @${p}`);
61
91
  return;
62
92
  }
63
93
  if (val.decrement !== undefined) {
64
- sets.push(`${quoted} = ${quoted} - @s_${safeCol}_dec`);
65
- params[`s_${safeCol}_dec`] = val.decrement;
94
+ const p = bindParam(params, `s_${safeCol}_dec`, val.decrement);
95
+ sets.push(`${quoted} = ${quoted} - @${p}`);
66
96
  return;
67
97
  }
68
98
  if (val.multiply !== undefined) {
69
- sets.push(`${quoted} = ${quoted} * @s_${safeCol}_mul`);
70
- params[`s_${safeCol}_mul`] = val.multiply;
99
+ const p = bindParam(params, `s_${safeCol}_mul`, val.multiply);
100
+ sets.push(`${quoted} = ${quoted} * @${p}`);
71
101
  return;
72
102
  }
73
103
  if (val.divide !== undefined) {
74
- sets.push(`${quoted} = ${quoted} / @s_${safeCol}_div`);
75
- params[`s_${safeCol}_div`] = val.divide;
104
+ const p = bindParam(params, `s_${safeCol}_div`, val.divide);
105
+ sets.push(`${quoted} = ${quoted} / @${p}`);
76
106
  return;
77
107
  }
78
108
  if (val.set !== undefined) {
79
- sets.push(`${quoted} = @s_${safeCol}_set`);
80
- params[`s_${safeCol}_set`] = val.set;
109
+ const p = bindParam(params, `s_${safeCol}_set`, val.set);
110
+ sets.push(`${quoted} = @${p}`);
81
111
  return;
82
112
  }
83
113
  }
84
114
 
85
- sets.push(`${quoted} = @s_${safeCol}`);
86
- params[`s_${safeCol}`] = val;
115
+ const p = bindParam(params, `s_${safeCol}`, val);
116
+ sets.push(`${quoted} = @${p}`);
87
117
  }
88
118
 
89
119
  function selectedAggregateFields(fields: any): string[] {
@@ -368,6 +398,22 @@ export class An5Adapter {
368
398
  return (await this.requireEngine()).exec<T>(query, params);
369
399
  }
370
400
 
401
+ /**
402
+ * The SQLite engine's vector hooks, or null when this is not a SQLite adapter.
403
+ * `vectorSearch` needs them to load sqlite-vec and register the distance
404
+ * functions; a connection with no hooks still searches in memory.
405
+ */
406
+ async sqliteVectorSupport(): Promise<SqliteVectorSupport | null> {
407
+ if (this.sheetsAdapter) return null;
408
+ const engine = await this.requireEngine();
409
+ return engine.dialect === 'sqlite' ? (engine.vectorSupport ?? null) : null;
410
+ }
411
+
412
+ /** `vectorStrategy` from the config, so a project can pin one strategy. */
413
+ get vectorStrategy(): VectorStrategyPreference | undefined {
414
+ return this._engineConfig?.vectorStrategy;
415
+ }
416
+
371
417
  /** INTERNAL: used by AdapterTableClient for DML statements needing row count */
372
418
  async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
373
419
  return (await this.requireEngine()).executeRaw(query, params);
@@ -714,6 +760,14 @@ export class AdapterTableClient<T = any> {
714
760
  public get dialect(): Dialect { return this.adapter.dialect; }
715
761
 
716
762
  public get tableName(): string {
763
+ const t = this.mappedTableName;
764
+ if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
765
+ if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
766
+ return quote(t, this.dialect);
767
+ }
768
+
769
+ /** The table name this model maps to, before any quoting is applied. */
770
+ private get mappedTableName(): string {
717
771
  const name = this.modelName;
718
772
  let t = name;
719
773
  const modelToTable = getModelToTable();
@@ -726,9 +780,17 @@ export class AdapterTableClient<T = any> {
726
780
  if (modelToTable[lower]) t = modelToTable[lower];
727
781
  }
728
782
  }
729
- if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
730
- if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
731
- return quote(t, this.dialect);
783
+ return t;
784
+ }
785
+
786
+ /**
787
+ * The bare table name, for helpers that take a string literal. `@@map` values
788
+ * may be pre-quoted or schema-qualified, neither of which `pragma_table_info`
789
+ * accepts, so the last segment is unwrapped here.
790
+ */
791
+ public get rawTableName(): string {
792
+ const last = this.mappedTableName.split('.').pop() ?? this.mappedTableName;
793
+ return last.replace(/^[\s]*[\[\]"`]+|[\[\]"`]+[\s]*$/g, '');
732
794
  }
733
795
 
734
796
  private get nolock(): string {
@@ -752,6 +814,53 @@ export class AdapterTableClient<T = any> {
752
814
  return this.adapter._executeRaw(query, params);
753
815
  }
754
816
 
817
+ /**
818
+ * Encode every `VECTOR(n)` value on its way into SQLite.
819
+ *
820
+ * The column holds a float32 BLOB, which is a third of the size of the JSON
821
+ * text it replaces and is what sqlite-vec reads. Values that already arrived
822
+ * as text or bytes are left alone, so rows written by an older version stay
823
+ * readable.
824
+ */
825
+ private encodeVectorFields(data: Record<string, any>): Record<string, any> {
826
+ if (this.dialect !== 'sqlite') return data;
827
+ const fields = getFieldsForModel(this.modelName);
828
+ const vectorFields = Object.keys(fields).filter((name) => isVectorField(fields[name]));
829
+ if (vectorFields.length === 0) return data;
830
+ const { encodeVector } = require('./sqlite/vector');
831
+ const out = { ...data };
832
+ for (const name of vectorFields) {
833
+ const value = out[name];
834
+ if (needsVectorEncoding(value)) {
835
+ const parsed = Array.from(value as ArrayLike<number>);
836
+ if (parsed.length > 0) out[name] = encodeVector(parsed);
837
+ }
838
+ }
839
+ return out;
840
+ }
841
+
842
+ /** Decode `VECTOR(n)` columns back into `number[]` after a read. */
843
+ private decodeVectorFields<T>(rows: T[]): T[] {
844
+ if (this.dialect !== 'sqlite' || !Array.isArray(rows) || rows.length === 0) return rows;
845
+ const fields = getFieldsForModel(this.modelName);
846
+ const vectorFields = Object.keys(fields).filter((name) => isVectorField(fields[name]));
847
+ if (vectorFields.length === 0) return rows;
848
+ return rows.map((row: any) => {
849
+ if (!row || typeof row !== 'object') return row;
850
+ let changed = false;
851
+ const out: any = { ...row };
852
+ for (const name of vectorFields) {
853
+ if (!(name in out)) continue;
854
+ const decoded = decodeVector(out[name]);
855
+ if (decoded) {
856
+ out[name] = decoded;
857
+ changed = true;
858
+ }
859
+ }
860
+ return changed ? out : row;
861
+ });
862
+ }
863
+
755
864
  async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any; include?: any }): Promise<T[]> {
756
865
  const params: Record<string, any> = {};
757
866
  const whereCtx = this.whereContext();
@@ -789,7 +898,8 @@ export class AdapterTableClient<T = any> {
789
898
  query = `SELECT ${cols} FROM ${this.tableName}`;
790
899
  if (whereSql) query += ` WHERE ${whereSql}`;
791
900
  if (orderSql) query += ` ${orderSql}`;
792
- query += ` LIMIT ${take ?? 'ALL'} OFFSET ${skip}`;
901
+ const limit = take ?? (this.dialect === 'sqlite' ? '-1' : this.dialect === 'mysql' ? '18446744073709551615' : 'ALL');
902
+ query += ` LIMIT ${limit} OFFSET ${skip}`;
793
903
  } else {
794
904
  // mssql requires ORDER BY before OFFSET/FETCH
795
905
  query = `SELECT ${cols} FROM ${this.tableName}${this.nolock}`;
@@ -806,7 +916,7 @@ export class AdapterTableClient<T = any> {
806
916
  if (orderSql) query += ` ${orderSql}`;
807
917
  }
808
918
 
809
- const rows = await this.doExec<T>(query, params);
919
+ const rows = this.decodeVectorFields(await this.doExec<T>(query, params));
810
920
 
811
921
  if (args?.include) {
812
922
  await resolveIncludes(this.modelName, rows, args.include, this.adapter);
@@ -854,11 +964,9 @@ export class AdapterTableClient<T = any> {
854
964
  }
855
965
 
856
966
  const fields = getFieldsForModel(this.modelName);
857
- const idFieldName = Object.prototype.hasOwnProperty.call(fields, 'id')
858
- ? 'id'
859
- : Object.keys(fields).find((name) => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
967
+ const idFieldName = resolveIdField(fields);
860
968
 
861
- const data: any = { ...rawData };
969
+ const data: any = this.encodeVectorFields(rawData);
862
970
  if (idFieldName) {
863
971
  const fieldDef: any = fields[idFieldName];
864
972
  const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || fieldDef?.type || '');
@@ -918,35 +1026,44 @@ export class AdapterTableClient<T = any> {
918
1026
  if (firstRow === undefined) return { count: 0 };
919
1027
  const firstCols = Object.keys(firstRow).filter(k => (firstRow as any)[k] !== undefined);
920
1028
 
921
- // Bulk INSERT for the common case (same columns, no skipDuplicates)
922
- if (firstCols.length > 0 && !args.skipDuplicates) {
1029
+ const sameColumns = args.data.every(row => {
1030
+ const cols = Object.keys(row).filter(k => (row as any)[k] !== undefined);
1031
+ return cols.length === firstCols.length && firstCols.every(col => cols.includes(col));
1032
+ });
1033
+
1034
+ const fields = getFieldsForModel(this.modelName);
1035
+ const idFieldName = resolveIdField(fields);
1036
+ const idDef = idFieldName ? fields[idFieldName] : undefined;
1037
+ const idType = typeof idDef === 'string' ? idDef : (idDef?.ts || idDef?.sql || idDef?.type || '');
1038
+ const generatesId = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(String(idType).toLowerCase());
1039
+ const needsGeneratedId = idFieldName && generatesId && args.data.some(row => !(row as any)[idFieldName]);
1040
+
1041
+ // Rows needing generated IDs follow create's default handling.
1042
+ if (firstCols.length > 0 && sameColumns && !needsGeneratedId && !args.skipDuplicates) {
923
1043
  const params: Record<string, any> = {};
924
1044
  const rowPlaceholders: string[] = [];
925
1045
 
926
1046
  for (let r = 0; r < args.data.length; r++) {
927
1047
  const row = args.data[r] as any;
1048
+ const encoded = this.encodeVectorFields(row);
928
1049
  const vals = firstCols.map(col => {
929
1050
  const p = `r${r}_${col}`;
930
- params[p] = row[col] ?? null;
1051
+ params[p] = encoded[col] ?? null;
931
1052
  return `@${p}`;
932
1053
  });
933
1054
  rowPlaceholders.push(`(${vals.join(', ')})`);
934
1055
  }
935
1056
 
936
1057
  const query = `INSERT INTO ${this.tableName} (${firstCols.map(c => quote(c, this.dialect)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
937
- try {
938
- await this.doExecuteRaw(query, params);
939
- return { count: args.data.length };
940
- } catch {
941
- // fallback below if bulk fails (e.g. mixed column sets)
942
- }
1058
+ await this.doExecuteRaw(query, params);
1059
+ return { count: args.data.length };
943
1060
  }
944
1061
 
945
1062
  // Row-by-row fallback
946
1063
  let count = 0;
947
1064
  for (const row of args.data) {
948
1065
  try { await this.create({ data: row }); count++; }
949
- catch (e) { if (!args.skipDuplicates) throw e; }
1066
+ catch (e) { if (!args.skipDuplicates || !isDuplicateKeyError(e, this.dialect)) throw e; }
950
1067
  }
951
1068
  return { count };
952
1069
  }
@@ -963,13 +1080,14 @@ export class AdapterTableClient<T = any> {
963
1080
  }
964
1081
  }
965
1082
 
1083
+ const data = this.encodeVectorFields(rawData);
966
1084
  const setCols = Object.keys(rawData).filter(k => rawData[k] !== undefined);
967
1085
  if (setCols.length > 0) {
968
1086
  const params: Record<string, any> = {};
969
1087
  const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
970
1088
  const sets: string[] = [];
971
1089
  for (const col of setCols) {
972
- appendUpdateSet(sets, params, col, rawData[col], this.dialect);
1090
+ appendUpdateSet(sets, params, col, data[col], this.dialect);
973
1091
  }
974
1092
  if (sets.length > 0) {
975
1093
  const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
@@ -1025,11 +1143,12 @@ export class AdapterTableClient<T = any> {
1025
1143
  async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
1026
1144
  const params: Record<string, any> = {};
1027
1145
  const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
1146
+ const data = this.encodeVectorFields(args.data as Record<string, any>);
1028
1147
  const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
1029
1148
  const sets: string[] = [];
1030
1149
 
1031
1150
  for (const col of setCols) {
1032
- appendUpdateSet(sets, params, col, (args.data as any)[col], this.dialect);
1151
+ appendUpdateSet(sets, params, col, data[col], this.dialect);
1033
1152
  }
1034
1153
 
1035
1154
  if (sets.length === 0) return { count: 0 };
@@ -1132,7 +1251,7 @@ export class AdapterTableClient<T = any> {
1132
1251
 
1133
1252
  async groupBy(args: any): Promise<any[]> {
1134
1253
  const params: Record<string, any> = {};
1135
- const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
1254
+ const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
1136
1255
  const byFields = normalizeByFields(args?.by);
1137
1256
  if (byFields.length === 0) throw new Error("groupBy requires 'by' fields");
1138
1257
  const byCols = byFields.map((b: string) => quote(b, this.dialect)).join(', ');
@@ -1151,8 +1270,8 @@ export class AdapterTableClient<T = any> {
1151
1270
  const skip = toNonNegativeInt(args?.skip);
1152
1271
  const take = toNonNegativeInt(args?.take, 1);
1153
1272
  if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
1154
- if (hasTake) query += ` LIMIT ${take}`;
1155
- query += ` OFFSET ${skip}`;
1273
+ const limit = hasTake ? take : (this.dialect === 'sqlite' ? '-1' : this.dialect === 'mysql' ? '18446744073709551615' : 'ALL');
1274
+ query += ` LIMIT ${limit} OFFSET ${skip}`;
1156
1275
  } else {
1157
1276
  query += ` OFFSET ${skip} ROWS`;
1158
1277
  if (hasTake) query += ` FETCH NEXT ${take} ROWS ONLY`;
@@ -1385,6 +1504,28 @@ export class AdapterTableClient<T = any> {
1385
1504
  const msg = String(err?.message || '').toLowerCase();
1386
1505
  if (!msg.includes('vector') && !msg.includes('operator does not exist')) throw err;
1387
1506
  }
1507
+ } else if (this.dialect === 'sqlite') {
1508
+ // SQLite has no vector type, so the ranking runs through sqlite-vec, a
1509
+ // driver function, or json_each — whichever this connection supports.
1510
+ const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
1511
+ const support = await (this.adapter as any).sqliteVectorSupport?.() ?? null;
1512
+ const ranked = await runSqliteVectorSearch({
1513
+ exec: (sql, p) => this.doExec<any>(sql, p),
1514
+ vectorSupport: support,
1515
+ table: this.tableName,
1516
+ rawTable: this.rawTableName,
1517
+ column: col,
1518
+ rawColumn: vectorField,
1519
+ vector: args.vector,
1520
+ metric,
1521
+ take,
1522
+ tail: whereSql ? ` WHERE ${whereSql}` : '',
1523
+ params,
1524
+ preference: (this.adapter as any).vectorStrategy,
1525
+ });
1526
+ if (ranked) return this.decodeVectorFields(ranked.rows) as (T & { distance: number })[];
1527
+ // No in-database strategy: score the column in the client instead.
1528
+ return this.scoreVectorsInMemory(args, metric, take, vectorField);
1388
1529
  } else if (this.dialect === 'mssql') {
1389
1530
  const distFn = metric === 'cosine' ? 'cosine' : metric === 'euclidean' ? 'euclidean' : 'dot';
1390
1531
  const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
@@ -1411,30 +1552,31 @@ export class AdapterTableClient<T = any> {
1411
1552
  }
1412
1553
 
1413
1554
  // 2. Fallback: in-memory similarity computation when native vector support is unavailable
1555
+ return this.scoreVectorsInMemory(args, metric, take, vectorField);
1556
+ }
1557
+
1558
+ /**
1559
+ * Rank the vector column in the client. This is the last resort for every
1560
+ * provider, and the only option on SQLite connections that offer neither
1561
+ * sqlite-vec, a driver function nor JSON1.
1562
+ *
1563
+ * The whole table is loaded, so it is only reasonable for a table small
1564
+ * enough to fit in memory.
1565
+ */
1566
+ private async scoreVectorsInMemory(
1567
+ args: { vector: number[]; where?: any },
1568
+ metric: 'cosine' | 'euclidean' | 'dot',
1569
+ take: number,
1570
+ vectorField = 'embedding',
1571
+ ): Promise<(T & { distance: number })[]> {
1414
1572
  const rows = await this.findMany({ where: args.where });
1415
1573
 
1416
1574
  const scored: { row: T; dist: number }[] = [];
1417
1575
  for (const row of rows) {
1418
- const raw = (row as any)[vectorField];
1419
- if (!raw) continue;
1420
- let vec: number[] = [];
1421
- try { vec = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { continue; }
1422
- if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
1423
-
1424
- let dot = 0, m1 = 0, m2 = 0;
1425
- for (let i = 0; i < args.vector.length; i++) {
1426
- const av = args.vector[i] ?? 0;
1427
- const bv = vec[i] ?? 0;
1428
- dot += av * bv;
1429
- m1 += av ** 2;
1430
- m2 += bv ** 2;
1431
- }
1432
- const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
1433
- const dist = metric === 'cosine'
1434
- ? 1 - cosine
1435
- : metric === 'dot'
1436
- ? -dot
1437
- : Math.sqrt(args.vector.reduce((s, v, i) => s + (v - (vec[i] ?? 0)) ** 2, 0));
1576
+ const vec = decodeVector((row as any)[vectorField]);
1577
+ if (!vec) continue;
1578
+ const dist = vectorDistance(args.vector, vec, metric);
1579
+ if (dist === null) continue;
1438
1580
  scored.push({ row, dist });
1439
1581
  }
1440
1582
  scored.sort((a, b) => a.dist - b.dist);
@@ -13,6 +13,14 @@ export interface ModelFieldMeta {
13
13
  [key: string]: any;
14
14
  }
15
15
 
16
+ /** Prefer schema primary-key metadata; retain name inference for legacy clients. */
17
+ export function resolveIdField(fields: Record<string, any>): string | undefined {
18
+ const primaryKey = Object.keys(fields).find(name => fields[name]?.isId === true);
19
+ if (primaryKey) return primaryKey;
20
+ if (Object.prototype.hasOwnProperty.call(fields, 'id')) return 'id';
21
+ return Object.keys(fields).find(name => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
22
+ }
23
+
16
24
  export interface AdapterMetadata {
17
25
  modelToTable?: Record<string, string>;
18
26
  modelFields?: Record<string, any>;
@@ -39,6 +39,15 @@ function sanitizeParamName(name: string): string {
39
39
  return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
40
40
  }
41
41
 
42
+ /** Allocate parameters without overwriting values from sibling clauses. */
43
+ export function bindParam(params: Record<string, any>, name: string, value: any): string {
44
+ let candidate = name;
45
+ let suffix = 1;
46
+ while (Object.prototype.hasOwnProperty.call(params, candidate)) candidate = `${name}_${suffix++}`;
47
+ params[candidate] = value;
48
+ return candidate;
49
+ }
50
+
42
51
  function normalizeSortDirection(dir: unknown): 'ASC' | 'DESC' {
43
52
  return typeof dir === 'string' && dir.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
44
53
  }
@@ -132,26 +141,24 @@ export function parseWhere(
132
141
  if (v.not !== undefined) {
133
142
  if (v.not === null) { conditions.push(`${col} IS NOT NULL`); }
134
143
  else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
135
- const nestedParams: Record<string, any> = {};
136
- const nestedSql = parseWhere(modelName, { [key]: v.not }, nestedParams, dialect, `${prefix}${key}_not_`, ctx);
137
- Object.assign(params, nestedParams);
144
+ const nestedSql = parseWhere(modelName, { [key]: v.not }, params, dialect, `${prefix}${key}_not_`, ctx);
138
145
  if (nestedSql) conditions.push(`NOT (${nestedSql})`);
139
- } else { const p = `${pname}_not`; params[p] = v.not; conditions.push(`${col} <> @${p}`); }
146
+ } else { const p = bindParam(params, `${pname}_not`, v.not); conditions.push(`${col} <> @${p}`); }
140
147
  }
141
148
  if (v.equals !== undefined) {
142
149
  if (v.equals === null) { conditions.push(`${col} IS NULL`); }
143
- else { const p = `${pname}_eq`; params[p] = v.equals; conditions.push(`${col} = @${p}`); }
150
+ else { const p = bindParam(params, `${pname}_eq`, v.equals); conditions.push(`${col} = @${p}`); }
144
151
  }
145
- if (v.contains !== undefined) { const p = `${pname}_co`; params[p] = `%${v.contains}%`; conditions.push(`${col} LIKE @${p}`); }
146
- if (v.startsWith !== undefined) { const p = `${pname}_sw`; params[p] = `${v.startsWith}%`; conditions.push(`${col} LIKE @${p}`); }
147
- if (v.endsWith !== undefined) { const p = `${pname}_ew`; params[p] = `%${v.endsWith}`; conditions.push(`${col} LIKE @${p}`); }
148
- if (v.gte !== undefined) { const p = `${pname}_gte`; params[p] = v.gte; conditions.push(`${col} >= @${p}`); }
149
- if (v.lte !== undefined) { const p = `${pname}_lte`; params[p] = v.lte; conditions.push(`${col} <= @${p}`); }
150
- if (v.gt !== undefined) { const p = `${pname}_gt`; params[p] = v.gt; conditions.push(`${col} > @${p}`); }
151
- if (v.lt !== undefined) { const p = `${pname}_lt`; params[p] = v.lt; conditions.push(`${col} < @${p}`); }
152
+ if (v.contains !== undefined) { const p = bindParam(params, `${pname}_co`, `%${v.contains}%`); conditions.push(`${col} LIKE @${p}`); }
153
+ if (v.startsWith !== undefined) { const p = bindParam(params, `${pname}_sw`, `${v.startsWith}%`); conditions.push(`${col} LIKE @${p}`); }
154
+ if (v.endsWith !== undefined) { const p = bindParam(params, `${pname}_ew`, `%${v.endsWith}`); conditions.push(`${col} LIKE @${p}`); }
155
+ if (v.gte !== undefined) { const p = bindParam(params, `${pname}_gte`, v.gte); conditions.push(`${col} >= @${p}`); }
156
+ if (v.lte !== undefined) { const p = bindParam(params, `${pname}_lte`, v.lte); conditions.push(`${col} <= @${p}`); }
157
+ if (v.gt !== undefined) { const p = bindParam(params, `${pname}_gt`, v.gt); conditions.push(`${col} > @${p}`); }
158
+ if (v.lt !== undefined) { const p = bindParam(params, `${pname}_lt`, v.lt); conditions.push(`${col} < @${p}`); }
152
159
  if (v.in !== undefined) {
153
160
  if (Array.isArray(v.in) && v.in.length > 0) {
154
- const ps = v.in.map((x: any, i: number) => { const p = `${pname}_in${i}`; params[p] = x; return `@${p}`; });
161
+ const ps = v.in.map((x: any, i: number) => { const p = bindParam(params, `${pname}_in${i}`, x); return `@${p}`; });
155
162
  conditions.push(`${col} IN (${ps.join(', ')})`);
156
163
  } else {
157
164
  conditions.push('1=0');
@@ -159,15 +166,15 @@ export function parseWhere(
159
166
  }
160
167
  if (v.notIn !== undefined) {
161
168
  if (Array.isArray(v.notIn) && v.notIn.length > 0) {
162
- const ps = v.notIn.map((x: any, i: number) => { const p = `${pname}_notin${i}`; params[p] = x; return `@${p}`; });
169
+ const ps = v.notIn.map((x: any, i: number) => { const p = bindParam(params, `${pname}_notin${i}`, x); return `@${p}`; });
163
170
  conditions.push(`${col} NOT IN (${ps.join(', ')})`);
164
171
  } else {
165
172
  conditions.push('1=1');
166
173
  }
167
174
  }
168
175
  } else {
169
- params[pname] = value;
170
- conditions.push(`${col} = @${pname}`);
176
+ const p = bindParam(params, pname, value);
177
+ conditions.push(`${col} = @${p}`);
171
178
  }
172
179
  }
173
180
  }
@@ -1,3 +1,5 @@
1
+ import type { SqliteVectorSupport, VectorStrategyPreference } from '../sqlite/vector';
2
+
1
3
  export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets';
2
4
 
3
5
  export interface An5AdapterConfig {
@@ -8,6 +10,17 @@ export interface An5AdapterConfig {
8
10
  * run in NBase while the rows stay in `connectionString`'s database.
9
11
  */
10
12
  nbase?: string;
13
+ /**
14
+ * Path to sqlite-vec or the AN5 native vector extension. On SQLite it is loaded before the
15
+ * vector capabilities are probed, so `vectorSearch` ranks inside the database
16
+ * instead of loading the table into memory. Other providers ignore it.
17
+ */
18
+ sqliteVec?: string;
19
+ /**
20
+ * Pin the SQLite vector search strategy. `auto` (the default) probes for
21
+ * sqlite-vec, a driver function and JSON1, and uses the first that works.
22
+ */
23
+ vectorStrategy?: VectorStrategyPreference;
11
24
  engine?: QueryEngine;
12
25
  db?: any;
13
26
  driver?: any;
@@ -30,4 +43,6 @@ export interface QueryEngine {
30
43
  connect(): Promise<void>;
31
44
  disconnect(): Promise<void>;
32
45
  beginTransaction(): Promise<TransactionHandle>;
46
+ /** SQLite only: extension loading and distance-function registration. */
47
+ vectorSupport?: SqliteVectorSupport;
33
48
  }
@@ -1,4 +1,5 @@
1
1
  import { An5SheetsAdapterConfig, resolveConfig } from './config';
2
+ import { sheetsTokenProvider } from './oauth';
2
3
  import type { sheets_v4 } from 'googleapis';
3
4
  import { execQuery } from './sqlExecutor';
4
5
  import { SheetsTableClient } from './tableClient';
@@ -8,12 +9,10 @@ import { createAdapterProxy } from '../base/metadata';
8
9
 
9
10
  // ─── Fetch-based API proxy for OAuth Access Token / API Key (browser-compatible) ──
10
11
 
11
- function createFetchApi(spreadsheetId: string, accessToken?: string, apiKey?: string) {
12
+ function createFetchApi(spreadsheetId: string, accessToken?: string, apiKey?: string, oauth: An5SheetsAdapterConfig = { spreadsheetId }) {
12
13
  const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`;
13
14
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
14
- if (accessToken) {
15
- headers['Authorization'] = `Bearer ${accessToken}`;
16
- }
15
+ const tokenProvider = sheetsTokenProvider({ ...oauth, ...(accessToken ? { accessToken } : {}) });
17
16
 
18
17
  async function request<T>(method: string, path: string, body?: any): Promise<T> {
19
18
  let url = path.startsWith('http') ? path : `${base}${path}`;
@@ -21,9 +20,14 @@ function createFetchApi(spreadsheetId: string, accessToken?: string, apiKey?: st
21
20
  const sep = url.includes('?') ? '&' : '?';
22
21
  url += `${sep}key=${encodeURIComponent(apiKey)}`;
23
22
  }
24
- const opts: RequestInit = { method, headers };
23
+ const token = await tokenProvider();
24
+ const opts: RequestInit = { method, headers: { ...headers, ...(token ? { Authorization: `Bearer ${token}` } : {}) } };
25
25
  if (body) opts.body = JSON.stringify(body);
26
- const res = await fetch(url, opts);
26
+ let res = await fetch(url, opts);
27
+ if (res.status === 401 && oauth.refreshToken) {
28
+ const refreshed = await tokenProvider(true);
29
+ res = await fetch(url, { ...opts, headers: { ...headers, Authorization: `Bearer ${refreshed}` } });
30
+ }
27
31
  if (!res.ok) {
28
32
  const text = await res.text().catch(() => '');
29
33
  const err = new Error(`Google Sheets API error (${res.status}): ${text}`);
@@ -85,13 +89,17 @@ export class An5SheetsAdapter {
85
89
  private sheetsCache: Promise<SheetMeta[]> | null = null;
86
90
 
87
91
  constructor(config: An5SheetsAdapterConfig) {
92
+ const browser = typeof window !== 'undefined' || (typeof self !== 'undefined' && typeof process === 'undefined');
93
+ if (browser && (config.refreshToken || config.oauthClientSecret || config.privateKey || config.credentials)) {
94
+ throw new Error('Browser Google Sheets connections require a user access token. Keep desktop OAuth refresh tokens and service account keys outside the browser.');
95
+ }
88
96
  this.config = resolveConfig(config);
89
97
  // Dynamic model access (`db.user` / `db.User`), same as An5Adapter.
90
98
  return createAdapterProxy(this, (name) => this.table(name));
91
99
  }
92
100
 
93
101
  private get isFetchMode(): boolean {
94
- return !!(this.config as any).accessToken || !!(this.config as any).apiKey;
102
+ return !!(this.config as any).accessToken || !!(this.config as any).refreshToken || !!(this.config as any).apiKey;
95
103
  }
96
104
 
97
105
  async getSheets(): Promise<any> {
@@ -100,7 +108,8 @@ export class An5SheetsAdapter {
100
108
  this.fetchApi = createFetchApi(
101
109
  this.config.spreadsheetId,
102
110
  (this.config as any).accessToken,
103
- (this.config as any).apiKey
111
+ (this.config as any).apiKey,
112
+ this.config as An5SheetsAdapterConfig
104
113
  );
105
114
  }
106
115
  return this.fetchApi;
@@ -10,6 +10,11 @@ export interface An5SheetsAdapterConfig {
10
10
  * When provided, the adapter uses raw fetch() instead of googleapis JWT, making it compatible
11
11
  * with browser environments. Mutually exclusive with clientEmail/privateKey/credentials. */
12
12
  accessToken?: string;
13
+ /** Offline desktop OAuth credentials. Keep these in secure storage, never source control. */
14
+ refreshToken?: string;
15
+ oauthClientId?: string;
16
+ oauthClientSecret?: string;
17
+ tokenExpiresAt?: number;
13
18
  /** Google API key for browser-based API access. */
14
19
  apiKey?: string;
15
20
  }
@@ -22,10 +27,14 @@ export function normalizeKey(key: string): string {
22
27
  }
23
28
 
24
29
  export function resolveConfig(config: An5SheetsAdapterConfig) {
25
- if (config.accessToken || config.apiKey) {
30
+ if (config.accessToken || config.refreshToken || config.apiKey) {
26
31
  return {
27
32
  spreadsheetId: config.spreadsheetId,
28
33
  accessToken: config.accessToken,
34
+ refreshToken: config.refreshToken,
35
+ oauthClientId: config.oauthClientId,
36
+ oauthClientSecret: config.oauthClientSecret,
37
+ tokenExpiresAt: config.tokenExpiresAt,
29
38
  apiKey: config.apiKey,
30
39
  sheetMapping: config.sheetMapping,
31
40
  };