@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
@@ -0,0 +1,293 @@
1
+ //! SQLite vector codec, distance and strategy plan.
2
+ //!
3
+ //! SQLite has no vector type, so a `VECTOR(n)` column stores a BLOB of little-endian
4
+ //! float32 values. The ranking itself is built in [`crate::adapter`] from these
5
+ //! helpers.
6
+ //!
7
+ //! The strategies, in the order every runtime tries them:
8
+ //!
9
+ //! 1. `sqlite-vec` — the extension, when it loads.
10
+ //! 2. `udf` — `an5_vec_cosine` / `an5_vec_l2` / `an5_vec_ip` registered with the driver.
11
+ //! 3. `sql` — `json_each` brute force in plain SQL.
12
+ //! 4. `memory` — the column is loaded and scored in Rust.
13
+ //!
14
+ //! `sqlx` connects through a pooled `AnyPool` and offers no way to register a SQL
15
+ //! function or load an extension, so a stock Rust adapter reaches `sql` and
16
+ //! `memory`. That is enough to rank in the database; the first two need a driver
17
+ //! built for it.
18
+
19
+ use std::collections::HashMap;
20
+
21
+ use serde_json::Value;
22
+
23
+ /// The size of one stored value; a column's byte length is its dimension times this.
24
+ pub const BYTES_PER_VECTOR_FLOAT: usize = 4;
25
+
26
+ pub const STRATEGY_SQLITE_VEC: &str = "sqlite-vec";
27
+ pub const STRATEGY_UDF: &str = "udf";
28
+ pub const STRATEGY_SQL: &str = "sql";
29
+ pub const STRATEGY_MEMORY: &str = "memory";
30
+
31
+ /// Scalar functions the adapter registers itself, when the driver allows it.
32
+ pub const AN5_VECTOR_FUNCTIONS: [(&str, &str); 3] = [
33
+ ("cosine", "an5_vec_cosine"),
34
+ ("euclidean", "an5_vec_l2"),
35
+ ("dot", "an5_vec_ip"),
36
+ ];
37
+
38
+ /// Scalar functions the sqlite-vec extension provides.
39
+ pub const SQLITE_VEC_FUNCTIONS: [(&str, &str); 3] = [
40
+ ("cosine", "vec_distance_cosine"),
41
+ ("euclidean", "vec_distance_l2"),
42
+ ("dot", "vec_distance_ip"),
43
+ ];
44
+
45
+ /// Returns a known metric name, defaulting to cosine.
46
+ pub fn normalize_metric(metric: &str) -> &str {
47
+ match metric.to_lowercase().as_str() {
48
+ "euclidean" | "dot" => metric,
49
+ _ => "cosine",
50
+ }
51
+ }
52
+
53
+ /// Returns the function name a strategy uses for a metric.
54
+ pub fn distance_function(strategy: &str, metric: &str) -> Option<&'static str> {
55
+ let table = match strategy {
56
+ STRATEGY_SQLITE_VEC => &SQLITE_VEC_FUNCTIONS,
57
+ STRATEGY_UDF => &AN5_VECTOR_FUNCTIONS,
58
+ _ => return None,
59
+ };
60
+ let metric = normalize_metric(metric);
61
+ table
62
+ .iter()
63
+ .find(|(name, _)| *name == metric)
64
+ .map(|(_, function)| *function)
65
+ }
66
+
67
+ /// Encodes a vector as the little-endian float32 BLOB the column stores.
68
+ pub fn encode_vector(values: &[f64]) -> Vec<u8> {
69
+ let mut bytes = Vec::with_capacity(values.len() * BYTES_PER_VECTOR_FLOAT);
70
+ for value in values {
71
+ bytes.extend_from_slice(&(*value as f32).to_le_bytes());
72
+ }
73
+ bytes
74
+ }
75
+
76
+ /// Reads a numeric sequence, or JSON text holding one.
77
+ pub fn parse_vector(value: &Value) -> Option<Vec<f64>> {
78
+ match value {
79
+ Value::Array(items) => {
80
+ let mut out = Vec::with_capacity(items.len());
81
+ for item in items {
82
+ out.push(item.as_f64()?);
83
+ }
84
+ if out.is_empty() {
85
+ None
86
+ } else {
87
+ Some(out)
88
+ }
89
+ }
90
+ Value::String(text) => parse_vector_text(text),
91
+ _ => None,
92
+ }
93
+ }
94
+
95
+ /// Parses the JSON text a legacy `VECTOR(n)` column holds.
96
+ pub fn parse_vector_text(raw: &str) -> Option<Vec<f64>> {
97
+ let trimmed = raw.trim();
98
+ if !trimmed.starts_with('[') {
99
+ return None;
100
+ }
101
+ match serde_json::from_str::<Vec<f64>>(trimmed) {
102
+ Ok(values) if !values.is_empty() => Some(values),
103
+ _ => None,
104
+ }
105
+ }
106
+
107
+ /// Decodes a stored vector: a float32 BLOB, a legacy JSON text column, or an
108
+ /// array the driver already decoded.
109
+ pub fn decode_vector(value: &Value) -> Option<Vec<f64>> {
110
+ if let Some(bytes) = crate::adapter::blob_bytes(value) {
111
+ return decode_vector_bytes(&bytes);
112
+ }
113
+ if let Some(text) = value.as_str() {
114
+ return parse_vector_text(text);
115
+ }
116
+ parse_vector(value)
117
+ }
118
+
119
+ /// Decodes the float32 BLOB a `VECTOR(n)` column stores.
120
+ ///
121
+ /// A column written by an older version holds JSON text instead, which is not a
122
+ /// multiple of four bytes once encoded, so the length check tells them apart.
123
+ pub fn decode_vector_bytes(bytes: &[u8]) -> Option<Vec<f64>> {
124
+ if let Some(values) = parse_vector_text(&String::from_utf8_lossy(bytes)) {
125
+ return Some(values);
126
+ }
127
+ if bytes.is_empty() || bytes.len() % BYTES_PER_VECTOR_FLOAT != 0 {
128
+ return None;
129
+ }
130
+ Some(
131
+ bytes
132
+ .chunks_exact(BYTES_PER_VECTOR_FLOAT)
133
+ .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) as f64)
134
+ .collect(),
135
+ )
136
+ }
137
+
138
+ /// True when the value still has to become bytes on its way into a column.
139
+ pub fn needs_vector_encoding(value: &Value) -> bool {
140
+ value.is_array()
141
+ }
142
+
143
+ /// True when generated metadata describes a `VECTOR(n)` column.
144
+ ///
145
+ /// The metadata value is either the declared SQL type as a string or a record with
146
+ /// `sql` / `ts` / `kind` members, so both shapes are accepted.
147
+ pub fn is_vector_field(definition: &Value) -> bool {
148
+ match definition {
149
+ Value::String(text) => {
150
+ text.contains("[]") || text.trim().to_uppercase().starts_with("VECTOR")
151
+ }
152
+ Value::Object(map) => {
153
+ if map.get("kind").and_then(Value::as_str) == Some("vector") {
154
+ return true;
155
+ }
156
+ for key in ["sql", "type"] {
157
+ if let Some(sql) = map.get(key).and_then(Value::as_str) {
158
+ if sql_looks_vector(sql) {
159
+ return true;
160
+ }
161
+ }
162
+ }
163
+ map.get("ts")
164
+ .and_then(Value::as_str)
165
+ .map(|ts| ts.contains("[]"))
166
+ .unwrap_or(false)
167
+ }
168
+ _ => false,
169
+ }
170
+ }
171
+
172
+ /// True when a declared SQL type names a vector column.
173
+ fn sql_looks_vector(sql: &str) -> bool {
174
+ let trimmed = sql.trim().to_uppercase();
175
+ match trimmed.strip_prefix("VECTOR") {
176
+ None => false,
177
+ Some(rest) => rest.is_empty() || rest.starts_with('(') || rest.starts_with(' '),
178
+ }
179
+ }
180
+
181
+ /// The ordered strategies to try for this connection.
182
+ ///
183
+ /// `declared_type` is the column's DDL type: the JSON strategy only reaches rows
184
+ /// stored as text, so a `BLOB` column drops it from the order. `preference` pins a
185
+ /// strategy instead of probing.
186
+ pub fn plan_vector_strategies(
187
+ capabilities: &HashMap<&str, bool>,
188
+ declared_type: Option<&str>,
189
+ preference: Option<&str>,
190
+ ) -> Vec<&'static str> {
191
+ if let Some(preference) = preference.filter(|p| !p.is_empty() && *p != "auto") {
192
+ // `memory` is the caller's own path, not a query, so it yields no plan.
193
+ return match preference {
194
+ STRATEGY_MEMORY => Vec::new(),
195
+ STRATEGY_SQLITE_VEC => vec![STRATEGY_SQLITE_VEC],
196
+ STRATEGY_UDF => vec![STRATEGY_UDF],
197
+ _ => vec![STRATEGY_SQL],
198
+ };
199
+ }
200
+
201
+ let mut available: Vec<&'static str> = Vec::new();
202
+ if capabilities.get("vec").copied().unwrap_or(false) {
203
+ available.push(STRATEGY_SQLITE_VEC);
204
+ }
205
+ if capabilities.get("udf").copied().unwrap_or(false) {
206
+ available.push(STRATEGY_UDF);
207
+ }
208
+ if capabilities.get("json1").copied().unwrap_or(false) {
209
+ available.push(STRATEGY_SQL);
210
+ }
211
+
212
+ let declared = declared_type.unwrap_or_default().trim().to_uppercase();
213
+ if !declared.is_empty()
214
+ && !["TEXT", "CHAR", "CLOB", "STRING", "JSON"]
215
+ .iter()
216
+ .any(|token| declared.contains(token))
217
+ {
218
+ // A BLOB column has no JSON to read, so json_each could only produce NULLs.
219
+ available.retain(|strategy| *strategy != STRATEGY_SQL);
220
+ }
221
+ available
222
+ }
223
+
224
+ #[cfg(test)]
225
+ mod tests {
226
+ use super::*;
227
+
228
+ #[test]
229
+ fn encodes_little_endian_float32() {
230
+ let bytes = encode_vector(&[1.0, -2.0, 0.5]);
231
+ assert_eq!(bytes.len(), 12);
232
+ assert_eq!(&bytes[0..4], &[0x00, 0x00, 0x80, 0x3f]);
233
+ assert_eq!(&bytes[4..8], &[0x00, 0x00, 0x00, 0xc0]);
234
+ }
235
+
236
+ #[test]
237
+ fn round_trips_a_blob() {
238
+ assert_eq!(decode_vector_bytes(&encode_vector(&[0.25, -0.5, 3.0])), Some(vec![0.25, -0.5, 3.0]));
239
+ }
240
+
241
+ #[test]
242
+ fn reads_the_legacy_json_text_column() {
243
+ assert_eq!(decode_vector(&Value::from("[1, 0, 0]")), Some(vec![1.0, 0.0, 0.0]));
244
+ assert_eq!(decode_vector_bytes(b"[1, 0, 0]"), Some(vec![1.0, 0.0, 0.0]));
245
+ assert_eq!(decode_vector(&Value::from("[1, 2]")), Some(vec![1.0, 2.0]));
246
+ }
247
+
248
+ #[test]
249
+ fn rejects_values_it_cannot_read() {
250
+ assert!(decode_vector(&Value::Null).is_none());
251
+ assert!(decode_vector(&Value::from("not json")).is_none());
252
+ assert!(decode_vector_bytes(&[1, 2, 3]).is_none());
253
+ assert!(decode_vector_bytes(&[]).is_none());
254
+ }
255
+
256
+ #[test]
257
+ fn recognises_vector_columns() {
258
+ assert!(is_vector_field(&serde_json::json!({ "ts": "number[] | string", "sql": "VECTOR(3)" })));
259
+ assert!(is_vector_field(&serde_json::json!({ "ts": "number[] | string" })));
260
+ assert!(is_vector_field(&serde_json::json!({ "kind": "vector" })));
261
+ assert!(is_vector_field(&Value::from("number[] | string")));
262
+ assert!(!is_vector_field(&serde_json::json!({ "ts": "string", "sql": "TEXT" })));
263
+ assert!(!is_vector_field(&Value::Null));
264
+ }
265
+
266
+ #[test]
267
+ fn plans_the_strategies_in_order() {
268
+ let all = HashMap::from([("vec", true), ("udf", true), ("json1", true)]);
269
+ assert_eq!(
270
+ plan_vector_strategies(&all, Some("BLOB"), None),
271
+ vec![STRATEGY_SQLITE_VEC, STRATEGY_UDF]
272
+ );
273
+ let json_only = HashMap::from([("json1", true)]);
274
+ assert_eq!(plan_vector_strategies(&json_only, Some("TEXT"), None), vec![STRATEGY_SQL]);
275
+ // A BLOB column has no JSON to read.
276
+ assert!(plan_vector_strategies(&json_only, Some("BLOB"), None).is_empty());
277
+ assert!(plan_vector_strategies(&HashMap::new(), Some("TEXT"), None).is_empty());
278
+ assert_eq!(plan_vector_strategies(&all, Some("BLOB"), Some("udf")), vec![STRATEGY_UDF]);
279
+ assert!(plan_vector_strategies(&all, Some("BLOB"), Some("memory")).is_empty());
280
+ }
281
+
282
+ #[test]
283
+ fn maps_each_metric_to_its_function() {
284
+ assert_eq!(distance_function(STRATEGY_UDF, "cosine"), Some("an5_vec_cosine"));
285
+ assert_eq!(distance_function(STRATEGY_UDF, "euclidean"), Some("an5_vec_l2"));
286
+ assert_eq!(distance_function(STRATEGY_UDF, "dot"), Some("an5_vec_ip"));
287
+ assert_eq!(
288
+ distance_function(STRATEGY_SQLITE_VEC, "cosine"),
289
+ Some("vec_distance_cosine")
290
+ );
291
+ assert_eq!(distance_function(STRATEGY_SQL, "cosine"), None);
292
+ }
293
+ }
package/rust/src/lib.rs CHANGED
@@ -31,9 +31,9 @@ pub mod adapter;
31
31
  pub mod base;
32
32
 
33
33
  pub use adapter::{
34
- AggregateArgs, An5Adapter, CountArgs, CreateArgs, CreateManyArgs, DeleteManyArgs, FindManyArgs,
35
- GroupByArgs, Result, RowMap, TableClient, UpdateArgs, UpdateManyArgs, UpsertArgs,
36
- VectorSearchArgs, ViewClient,
34
+ blob_bytes, blob_value, AggregateArgs, An5Adapter, CountArgs, CreateArgs, CreateManyArgs,
35
+ DeleteManyArgs, FindManyArgs, GroupByArgs, Result, RowMap, TableClient, UpdateArgs,
36
+ UpdateManyArgs, UpsertArgs, VectorSearchArgs, ViewClient,
37
37
  };
38
38
  pub use base::{
39
39
  add_table_override, build_order_by, build_where, cosine_similarity, dot_product,
@@ -0,0 +1,32 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { execFileSync } = require('node:child_process');
4
+
5
+ function buildNative(directory) {
6
+ const source = path.resolve(__dirname, '../native/sqlite');
7
+ fs.mkdirSync(directory, { recursive: true });
8
+ if (process.platform === 'win32') {
9
+ execFileSync('cmake', ['-S', source, '-B', directory, '-DCMAKE_BUILD_TYPE=Release'], { stdio: 'inherit' });
10
+ execFileSync('cmake', ['--build', directory, '--config', 'Release'], { stdio: 'inherit' });
11
+ const dll = path.join(directory, 'Release', 'an5_vector.dll');
12
+ return fs.existsSync(dll) ? dll : path.join(directory, 'an5_vector.dll');
13
+ }
14
+ const binary = path.join(directory, process.platform === 'darwin' ? 'an5_vector.dylib' : 'an5_vector.so');
15
+ const flags = ['-std=c99', '-O3', '-Wall', '-Wextra', '-Werror', '-fPIC',
16
+ process.platform === 'darwin' ? '-dynamiclib' : '-shared'];
17
+ if (process.env.SQLITE_INCLUDE_DIR) flags.push('-I', process.env.SQLITE_INCLUDE_DIR);
18
+ if (process.env.AN5_NATIVE_SANITIZE) flags.push('-fsanitize=undefined', '-fno-sanitize-recover=all');
19
+ execFileSync(process.env.CC || 'cc', [...flags, path.join(source, 'an5_vector.c'), '-lm', '-o', binary], { stdio: 'inherit' });
20
+ return binary;
21
+ }
22
+
23
+ module.exports = { buildNative };
24
+ if (require.main === module) {
25
+ try {
26
+ console.log(buildNative(path.resolve(process.argv[2] || path.join(__dirname, '../native/sqlite/build'))));
27
+ } catch (error) {
28
+ console.error('Native vector build requires a C compiler and sqlite3ext.h (Windows: CMake and a C compiler).');
29
+ console.error(error.message);
30
+ process.exitCode = 1;
31
+ }
32
+ }
@@ -0,0 +1,22 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ // The product is `An5Adapters`. A consumer refers to it as
5
+ // `.product(name: "An5Adapters", package: "an5Adapters")` when it depends on this repository
6
+ // by URL — SwiftPM derives the package identity from the URL's last path component. A
7
+ // `.package(path:)` dependency takes the checkout's directory name instead, so from a local
8
+ // clone the identity is `swift` and that argument must match.
9
+ let package = Package(
10
+ name: "An5Adapters",
11
+ products: [
12
+ .library(name: "An5Adapters", targets: ["An5Adapters"])
13
+ ],
14
+ targets: [
15
+ // The system SQLite, reached through a shim header rather than a copy of the C API.
16
+ // SQLite is what the other mobile-facing clients read and write on device, and the
17
+ // runtime is always present on iOS, tvOS, watchOS, macOS and Android.
18
+ .systemLibrary(name: "CSQLite", path: "Sources/CSQLite"),
19
+ .target(name: "An5Adapters", dependencies: ["CSQLite"]),
20
+ .testTarget(name: "An5AdaptersTests", dependencies: ["An5Adapters"]),
21
+ ]
22
+ )
@@ -0,0 +1,160 @@
1
+ import Foundation
2
+
3
+ /// The AN5 runtime: a driver, the dialect it speaks, and the metadata that gives models
4
+ /// their tables.
5
+ ///
6
+ /// ```swift
7
+ /// let driver = try SQLiteDriver(path: ":memory:")
8
+ /// let db = try An5Adapter(driver: driver, connectionString: "sqlite::memory:", metadata: An5Metadata.registry)
9
+ /// try db.table("User").create(["name": "Ada", "score": 10])
10
+ /// let adults = try db.table("User").findMany(where: ["age": ["gte": 18]])
11
+ /// ```
12
+ ///
13
+ /// One connection is held for its lifetime, so an in-memory database survives between
14
+ /// statements — which is what makes it usable in a test.
15
+ public final class An5Adapter {
16
+
17
+ public let driver: SQLDriver
18
+ public let dialect: Dialect
19
+ public let metadata: Metadata
20
+
21
+ /// The open transaction, when there is one. Every statement inside it has to use this
22
+ /// driver's single connection, which is why the flag exists at all: a rollback that ran
23
+ /// on a different connection would return to an already committed one.
24
+ ///
25
+ /// Shared with the scoped adapter `transaction` hands to the caller, so a nested
26
+ /// transaction is refused no matter which handle the inner call went through.
27
+ private var state = TransactionState()
28
+
29
+ /// Opens a runtime over an existing driver.
30
+ ///
31
+ /// - Parameters:
32
+ /// - driver: the SQL surface to run statements through.
33
+ /// - connectionString: the project's connection string; decides the dialect.
34
+ /// - metadata: the generated models' tables and columns.
35
+ public convenience init(driver: SQLDriver, connectionString: String, metadata: Metadata = .empty) {
36
+ self.init(driver: driver, dialect: Dialect.detect(connectionString), metadata: metadata)
37
+ }
38
+
39
+ /// Opens a runtime when the dialect is already known.
40
+ ///
41
+ /// Separate from the connection-string form because re-deriving the dialect from a bare
42
+ /// name like `sqlite` does not work: `Dialect.detect` reads a scheme or a file suffix,
43
+ /// and neither is present, so it falls through to SQL Server and every statement in a
44
+ /// transaction comes out with `WITH (NOLOCK)`.
45
+ init(driver: SQLDriver, dialect: Dialect, metadata: Metadata = .empty) {
46
+ self.driver = driver
47
+ self.dialect = dialect
48
+ self.metadata = metadata
49
+ }
50
+
51
+ /// Opens a runtime over the system SQLite.
52
+ public convenience init(
53
+ path: String,
54
+ metadata: Metadata = .empty,
55
+ connectionString: String? = nil
56
+ ) throws {
57
+ try self.init(
58
+ driver: SQLiteDriver(path: path),
59
+ connectionString: connectionString ?? "sqlite::memory:",
60
+ metadata: metadata
61
+ )
62
+ }
63
+
64
+ /// A runtime over the same driver that shares this one's transaction state.
65
+ private func scoped() -> An5Adapter {
66
+ let scoped = An5Adapter(driver: driver, dialect: dialect, metadata: metadata)
67
+ scoped.state = state
68
+ return scoped
69
+ }
70
+
71
+ /// The table client for a model.
72
+ public func table(_ model: String) -> TableClient {
73
+ TableClient(adapter: self, model: model)
74
+ }
75
+
76
+ /// A read-only client for a database view.
77
+ public func view(_ name: String) -> ViewClient {
78
+ ViewClient(adapter: self, view: name)
79
+ }
80
+
81
+ /// Pins one SQLite vector strategy instead of probing for the fastest one available:
82
+ /// `sqlite-vec`, `udf`, `sql` or `memory`. `nil` probes.
83
+ ///
84
+ /// SQLite has no vector type, so a `VECTOR(n)` column is ranked by the sqlite-vec
85
+ /// extension, a distance function the driver registers, `json_each` in plain SQL, or in
86
+ /// the client. The bundled `SQLiteDriver` registers the distance functions and loads
87
+ /// `SQLiteDriver.sqliteVecPath` when one is set.
88
+ public var vectorStrategy: String?
89
+
90
+ /// What this SQLite connection can do for vector search, probed once and cached.
91
+ ///
92
+ /// `nil` on the other dialects. The bundled driver registers the `an5_vec_*` functions
93
+ /// itself, so `udf` is reported whenever it is in use; a custom `SQLDriver` normally
94
+ /// reaches only `json_each` and the in-memory path.
95
+ public private(set) lazy var sqliteCapabilities: SqliteVectors.Capabilities = {
96
+ SqliteVectors.Capabilities(
97
+ vec: self.probe("SELECT vec_version()"),
98
+ udf: self.probe("SELECT an5_vec_cosine(zeroblob(4), zeroblob(4))"),
99
+ json1: self.probe("SELECT json_valid('[1]')")
100
+ )
101
+ }()
102
+
103
+ /// True when a probe statement ran, whatever it returned.
104
+ private func probe(_ sql: String) -> Bool {
105
+ (try? driver.query(sql, [])) != nil
106
+ }
107
+
108
+ /// A vector column's declared DDL type, or `nil` when it cannot be read.
109
+ ///
110
+ /// It is what decides whether the `json_each` strategy can reach the rows in the column:
111
+ /// a `BLOB` column has no JSON to walk.
112
+ public func queryColumnType(table: String, column: String) throws -> String? {
113
+ let rows = try driver.query(
114
+ "SELECT type FROM pragma_table_info(?) WHERE name = ?", [.text(table), .text(column)]
115
+ )
116
+ return rows.first?["type"].flatMap { $0 as? String }
117
+ }
118
+
119
+ /// Runs a query and returns its rows keyed by column name.
120
+ public func query(_ sql: String, _ parameters: [Any?] = []) throws -> [Row] {
121
+ try driver.query(sql, parameters.compactMap { SQLValue.encode($0) })
122
+ }
123
+
124
+ /// Runs a statement that returns no rows and gives back the affected row count.
125
+ @discardableResult
126
+ public func execute(_ sql: String, _ parameters: [Any?] = []) throws -> Int {
127
+ try driver.execute(sql, parameters.compactMap { SQLValue.encode($0) })
128
+ }
129
+
130
+ /// Runs `body` inside a transaction, committing on return and rolling back on failure.
131
+ ///
132
+ /// Nesting is rejected rather than flattened: an inner commit would make the outer
133
+ /// rollback a partial save, which is the opposite of what the caller asked for.
134
+ public func transaction<T>(_ body: (An5Adapter) throws -> T) throws -> T {
135
+ guard !state.inTransaction else {
136
+ throw An5Error.invalidQuery("a nested transaction is not supported")
137
+ }
138
+ state.inTransaction = true
139
+ defer { state.inTransaction = false }
140
+ do {
141
+ try driver.beginTransaction()
142
+ } catch {
143
+ throw An5Error.driver("cannot begin a transaction: \(error)")
144
+ }
145
+ do {
146
+ let result = try body(scoped())
147
+ try driver.commit()
148
+ return result
149
+ } catch {
150
+ try? driver.rollback()
151
+ throw error
152
+ }
153
+ }
154
+ }
155
+
156
+ /// Whether a transaction is open, shared between an adapter and the scoped handle its
157
+ /// transaction hands out.
158
+ private final class TransactionState {
159
+ var inTransaction = false
160
+ }