@an5/adapters 0.2.11 → 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 (91) hide show
  1. package/README.md +133 -2
  2. package/dist/an5Adapter.d.ts +37 -0
  3. package/dist/an5Adapter.js +138 -35
  4. package/dist/base/types.d.ts +14 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +8 -1
  7. package/dist/sqlite/browserEngine.d.ts +13 -0
  8. package/dist/sqlite/browserEngine.js +13 -0
  9. package/dist/sqlite/engine.d.ts +5 -0
  10. package/dist/sqlite/engine.js +10 -0
  11. package/dist/sqlite/index.d.ts +1 -0
  12. package/dist/sqlite/index.js +1 -0
  13. package/dist/sqlite/vector.d.ts +155 -0
  14. package/dist/sqlite/vector.js +436 -0
  15. package/dotnet/An5Adapter.cs +85 -27
  16. package/dotnet/Base/Core.cs +14 -0
  17. package/dotnet/Mssql/MssqlEngine.cs +44 -1
  18. package/dotnet/Postgres/PostgresEngine.cs +2 -0
  19. package/dotnet/Sqlite/SqliteEngine.cs +82 -2
  20. package/dotnet/Sqlite/SqliteVectors.cs +401 -0
  21. package/golang/an5_adapter.go +84 -32
  22. package/golang/base/vectors.go +263 -0
  23. package/golang/sqlite/vector.go +248 -0
  24. package/golang/sqlite_vector_test.go +288 -0
  25. package/golang/table_client.go +67 -3
  26. package/java/pom.xml +177 -0
  27. package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
  28. package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
  29. package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
  30. package/java/src/main/java/an5/adapters/An5Query.java +100 -0
  31. package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
  32. package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
  33. package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
  34. package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
  35. package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
  36. package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
  37. package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
  38. package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
  39. package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
  40. package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
  41. package/java/src/test/java/An5AdaptersSmoke.java +733 -0
  42. package/java/src/test/java/Json.java +174 -0
  43. package/java/src/test/java/QuerySemantics.java +111 -0
  44. package/kotlin/build.gradle.kts +145 -0
  45. package/kotlin/settings.gradle.kts +10 -0
  46. package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
  47. package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
  48. package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
  49. package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
  50. package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
  51. package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
  52. package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
  53. package/native/sqlite/CMakeLists.txt +12 -0
  54. package/native/sqlite/README.md +117 -0
  55. package/native/sqlite/an5_vector.c +209 -0
  56. package/package.json +33 -7
  57. package/python/an5_adapter.py +37 -6
  58. package/python/base/__init__.py +15 -0
  59. package/python/base/vectors.py +434 -0
  60. package/python/sqlite/__init__.py +2 -2
  61. package/python/sqlite/provider.py +21 -1
  62. package/python/table_client.py +129 -59
  63. package/rust/src/adapter.rs +342 -24
  64. package/rust/src/base/mod.rs +7 -0
  65. package/rust/src/base/vectors.rs +293 -0
  66. package/rust/src/lib.rs +3 -3
  67. package/scripts/sqlite-native-build.js +32 -0
  68. package/swift/Package.swift +22 -0
  69. package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
  70. package/swift/Sources/An5Adapters/Metadata.swift +183 -0
  71. package/swift/Sources/An5Adapters/Query.swift +105 -0
  72. package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
  73. package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
  74. package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
  75. package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
  76. package/swift/Sources/An5Adapters/TableClient.swift +671 -0
  77. package/swift/Sources/An5Adapters/Types.swift +127 -0
  78. package/swift/Sources/An5Adapters/Vectors.swift +86 -0
  79. package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
  80. package/swift/Sources/CSQLite/module.modulemap +8 -0
  81. package/swift/Sources/CSQLite/shim.h +9 -0
  82. package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
  83. package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
  84. package/typescript/src/an5Adapter.ts +146 -29
  85. package/typescript/src/base/types.ts +15 -0
  86. package/typescript/src/index.ts +13 -0
  87. package/typescript/src/sqlite/browserEngine.ts +24 -0
  88. package/typescript/src/sqlite/engine.ts +13 -0
  89. package/typescript/src/sqlite/index.ts +1 -1
  90. package/typescript/src/sqlite/vector.ts +541 -0
  91. package/golang/sqlite/sqlite.go +0 -14
@@ -19,21 +19,34 @@ namespace An5Orm
19
19
  private readonly int _commandTimeout;
20
20
 
21
21
  public An5Adapter(string connectionString, int commandTimeout = 60)
22
+ : this(connectionString, commandTimeout, null) { }
23
+
24
+ public An5Adapter(string connectionString, int commandTimeout, SqliteVectorSupport vectorSupport)
22
25
  {
23
26
  ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
24
27
  _commandTimeout = commandTimeout;
25
28
  _engine = DialectDetector.Detect(connectionString) switch
26
29
  {
27
30
  Dialect.Postgres => new PostgresEngine(connectionString, commandTimeout),
28
- Dialect.Sqlite => new SqliteEngine(connectionString, commandTimeout),
31
+ Dialect.Sqlite => new SqliteEngine(connectionString, commandTimeout, vectorSupport),
29
32
  _ => new MssqlEngine(connectionString, commandTimeout)
30
33
  };
31
34
  }
32
35
 
33
- public An5Adapter(An5AdapterOptions options) : this(options.ConnectionString, options.CommandTimeout) { }
36
+ public An5Adapter(An5AdapterOptions options)
37
+ : this(options.ConnectionString, options.CommandTimeout,
38
+ new SqliteVectorSupport { SqliteVecPath = options.SqliteVec, Preference = options.VectorStrategy })
39
+ { }
34
40
 
35
41
  // ── Raw query execution ────────────────────────────────────────────────
36
42
 
43
+ /// <summary>
44
+ /// SQLite only: the vector hooks of the configured connection, or null on
45
+ /// the other providers. `VectorSearch` needs them to rank a `VECTOR(n)`
46
+ /// column inside the database.
47
+ /// </summary>
48
+ public SqliteVectorSupport VectorSupport => _engine.VectorSupport;
49
+
37
50
  public List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters = null)
38
51
  => _engine.QueryRaw(sql, parameters);
39
52
 
@@ -1006,10 +1019,29 @@ namespace An5Orm
1006
1019
  string whereClause = null, Dictionary<string, object> parameters = null,
1007
1020
  string vectorField = "Embedding", string distanceMetric = "cosine")
1008
1021
  {
1009
- // 1. Primary path: Native database SQL vector query execution (MSSQL VECTOR_DISTANCE / Postgres pgvector)
1010
- // SQLite has no vector operator, so it is skipped outright: building the
1011
- // statement only to have it rejected costs a round trip and lands on the
1012
- // in-memory path below anyway.
1022
+ // 1. SQLite ranks through sqlite-vec, a driver function or json_each,
1023
+ // whichever this connection supports, so the table never has to
1024
+ // leave the database.
1025
+ if (_dialect == Dialect.Sqlite)
1026
+ {
1027
+ var p = parameters != null ? new Dictionary<string, object>(parameters) : new Dictionary<string, object>();
1028
+ var quotedVectorField = SqlQuote.QuoteName(vectorField, _dialect);
1029
+ var ranked = SqliteVectors.RunSearch(
1030
+ _adapter.QueryRaw,
1031
+ _tableName,
1032
+ _rawTable.Trim('[', ']'),
1033
+ quotedVectorField,
1034
+ vectorField,
1035
+ vector,
1036
+ distanceMetric,
1037
+ take,
1038
+ string.IsNullOrWhiteSpace(whereClause) ? "" : $" WHERE {whereClause}",
1039
+ p,
1040
+ _adapter.VectorSupport);
1041
+ if (ranked != null) return ToRanked(ranked, take);
1042
+ }
1043
+
1044
+ // 2. Primary path: Native database SQL vector query execution (MSSQL VECTOR_DISTANCE / Postgres pgvector)
1013
1045
  if (_dialect != Dialect.Sqlite)
1014
1046
  try
1015
1047
  {
@@ -1063,7 +1095,7 @@ namespace An5Orm
1063
1095
  // Secondary fallback: In-memory similarity computation if DB instance lacks native vector extension
1064
1096
  }
1065
1097
 
1066
- // 2. Secondary fallback: In-memory similarity computation
1098
+ // 3. Secondary fallback: In-memory similarity computation
1067
1099
  var rows = FindMany(whereClause, parameters);
1068
1100
  var results = new List<(T, double)>();
1069
1101
  var vectorProp = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
@@ -1071,33 +1103,59 @@ namespace An5Orm
1071
1103
 
1072
1104
  foreach (var row in rows)
1073
1105
  {
1074
- var rawVal = vectorProp.GetValue(row);
1075
- if (rawVal == null) continue;
1076
- List<double> vec = null;
1077
- try
1078
- {
1079
- if (rawVal is string s) vec = JsonSerializer.Deserialize<List<double>>(s);
1080
- }
1081
- catch { continue; }
1106
+ // A float32 BLOB, a legacy JSON text column or an already-decoded
1107
+ // array all decode the same way.
1108
+ var vec = SqliteVectors.DecodeVector(vectorProp.GetValue(row));
1082
1109
  if (vec == null || vec.Count != vector.Count) continue;
1083
-
1084
- double dot = 0, m1 = 0, m2 = 0;
1085
- for (int i = 0; i < vector.Count; i++)
1086
- {
1087
- dot += vector[i] * vec[i];
1088
- m1 += vector[i] * vector[i];
1089
- m2 += vec[i] * vec[i];
1090
- }
1091
- double cosine = (m1 > 0 && m2 > 0) ? dot / (Math.Sqrt(m1) * Math.Sqrt(m2)) : 0;
1092
- double dist = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? 1.0 - cosine :
1093
- distanceMetric.Equals("dot", StringComparison.OrdinalIgnoreCase) ? -dot : EuclideanDistance(vector, vec);
1094
- results.Add((row, dist));
1110
+ var dist = SqliteVectors.VectorDistance(vector, vec, distanceMetric);
1111
+ if (!dist.HasValue) continue;
1112
+ results.Add((row, dist.Value));
1095
1113
  }
1096
1114
 
1097
1115
  results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
1098
1116
  return results.GetRange(0, Math.Min(take, results.Count));
1099
1117
  }
1100
1118
 
1119
+ /// <summary>
1120
+ /// Projects the ranked rows the in-database strategies return into the
1121
+ /// caller's entity type, taking `distance` from the added column.
1122
+ /// </summary>
1123
+ private List<(T, double)> ToRanked(List<Dictionary<string, object>> rows, int take)
1124
+ {
1125
+ var results = new List<(T, double)>();
1126
+ foreach (var row in rows)
1127
+ {
1128
+ var item = Activator.CreateInstance<T>();
1129
+ foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
1130
+ {
1131
+ if (!row.TryGetValue(prop.Name, out var value) || value == null) continue;
1132
+ if (prop.Name.Equals("Distance", StringComparison.OrdinalIgnoreCase))
1133
+ {
1134
+ if (value is double d) prop.SetValue(item, d);
1135
+ continue;
1136
+ }
1137
+ // A vector column arrives as float32 bytes; the entity declares
1138
+ // it as float[].
1139
+ if (prop.PropertyType == typeof(float[]))
1140
+ {
1141
+ var vec = SqliteVectors.DecodeVector(value);
1142
+ if (vec != null)
1143
+ {
1144
+ prop.SetValue(item, vec.Select(v => (float)v).ToArray());
1145
+ continue;
1146
+ }
1147
+ }
1148
+ try { prop.SetValue(item, Convert.ChangeType(value, prop.PropertyType)); } catch { }
1149
+ }
1150
+ double distance = row.TryGetValue("distance", out var raw) && raw != null
1151
+ ? Convert.ToDouble(raw)
1152
+ : 0;
1153
+ results.Add((item, distance));
1154
+ if (results.Count >= take) break;
1155
+ }
1156
+ return results;
1157
+ }
1158
+
1101
1159
  private static double EuclideanDistance(List<double> left, List<double> right)
1102
1160
  {
1103
1161
  double sum = 0;
@@ -18,6 +18,17 @@ public enum Dialect
18
18
  public string ConnectionString { get; set; }
19
19
  public int CommandTimeout { get; set; } = 60;
20
20
  public int ConnectRetryCount { get; set; } = 3;
21
+ /// <summary>
22
+ /// SQLite only: path to the sqlite-vec extension binary. Loaded on every
23
+ /// connection so vector search ranks inside the database. Other
24
+ /// providers ignore it.
25
+ /// </summary>
26
+ public string SqliteVec { get; set; }
27
+ /// <summary>
28
+ /// SQLite only: pin one of "sqlite-vec" | "udf" | "sql" | "memory"
29
+ /// instead of probing for the fastest strategy available.
30
+ /// </summary>
31
+ public string VectorStrategy { get; set; }
21
32
  }
22
33
 
23
34
  // ─── Query Engine Interface ────────────────────────────────────────────────
@@ -29,6 +40,9 @@ public enum Dialect
29
40
  List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters) where T : new();
30
41
  int ExecuteRaw(string sql, Dictionary<string, object> parameters);
31
42
  An5TransactionBase BeginTransaction();
43
+
44
+ /// <summary>SQLite only: extension loading and distance-function registration.</summary>
45
+ SqliteVectorSupport VectorSupport { get; }
32
46
  }
33
47
 
34
48
  // ─── Transaction base ──────────────────────────────────────────────────────
@@ -1,6 +1,7 @@
1
1
  using System;
2
2
  using System.Collections.Generic;
3
3
  using System.Data;
4
+ using System.Globalization;
4
5
  using System.Reflection;
5
6
  using Microsoft.Data.SqlClient;
6
7
 
@@ -11,6 +12,8 @@ namespace An5Orm
11
12
  internal class MssqlEngine : IQueryEngine
12
13
  {
13
14
  public Dialect Dialect => Dialect.Mssql;
15
+ // Only SQLite has a vector hook; this provider has none to offer.
16
+ public SqliteVectorSupport VectorSupport => null;
14
17
  private readonly string _connectionString;
15
18
  private readonly int _commandTimeout;
16
19
 
@@ -32,6 +35,33 @@ namespace An5Orm
32
35
  return conn;
33
36
  }
34
37
 
38
+ /// <summary>
39
+ /// Converts a value into something SqlClient can bind.
40
+ /// </summary>
41
+ /// <remarks>
42
+ /// A <c>VECTOR</c> column is transmitted as a JSON array over TDS, so a float or
43
+ /// double array is written in that form. Binding the array itself is not an option:
44
+ /// SqlClient has no mapping for it, and the failure names the array rather than the
45
+ /// column. Driver versions before 6.1 have no <c>SqlVector</c> either, so the JSON
46
+ /// form is what every version accepts.
47
+ /// </remarks>
48
+ private static object BindableValue(object value)
49
+ {
50
+ if (value is float[] floats)
51
+ {
52
+ var parts = new string[floats.Length];
53
+ for (int i = 0; i < floats.Length; i++) parts[i] = floats[i].ToString("R", CultureInfo.InvariantCulture);
54
+ return "[" + string.Join(", ", parts) + "]";
55
+ }
56
+ if (value is double[] doubles)
57
+ {
58
+ var parts = new string[doubles.Length];
59
+ for (int i = 0; i < doubles.Length; i++) parts[i] = doubles[i].ToString("R", CultureInfo.InvariantCulture);
60
+ return "[" + string.Join(", ", parts) + "]";
61
+ }
62
+ return value;
63
+ }
64
+
35
65
  private SqlCommand BuildCommand(
36
66
  SqlConnection conn, string sql, Dictionary<string, object> parameters)
37
67
  {
@@ -42,7 +72,7 @@ namespace An5Orm
42
72
  foreach (var kv in parameters)
43
73
  {
44
74
  var paramName = kv.Key.StartsWith("@") ? kv.Key : "@" + kv.Key;
45
- cmd.Parameters.AddWithValue(paramName, kv.Value ?? DBNull.Value);
75
+ cmd.Parameters.AddWithValue(paramName, BindableValue(kv.Value) ?? DBNull.Value);
46
76
  }
47
77
  }
48
78
  return cmd;
@@ -87,6 +117,19 @@ namespace An5Orm
87
117
  if (!HasColumn(reader, prop.Name)) continue;
88
118
  var val = reader[prop.Name];
89
119
  if (val == DBNull.Value) continue;
120
+ // A `VECTOR` column arrives as a JSON array, which no converter
121
+ // bridges to the `float[]` the entity declares.
122
+ if (prop.PropertyType == typeof(float[]))
123
+ {
124
+ var vector = SqliteVectors.DecodeVector(val);
125
+ if (vector != null)
126
+ {
127
+ var floats = new float[vector.Count];
128
+ for (int i = 0; i < vector.Count; i++) floats[i] = (float)vector[i];
129
+ prop.SetValue(item, floats);
130
+ continue;
131
+ }
132
+ }
90
133
  try { prop.SetValue(item, Convert.ChangeType(val, prop.PropertyType)); } catch { }
91
134
  }
92
135
  results.Add(item);
@@ -10,6 +10,8 @@ namespace An5Orm
10
10
  internal class PostgresEngine : IQueryEngine
11
11
  {
12
12
  public Dialect Dialect => Dialect.Postgres;
13
+ // Only SQLite has a vector hook; this provider has none to offer.
14
+ public SqliteVectorSupport VectorSupport => null;
13
15
  private readonly string _connectionString;
14
16
  private readonly int _commandTimeout;
15
17
 
@@ -18,13 +18,15 @@ namespace An5Orm
18
18
  internal class SqliteEngine : IQueryEngine
19
19
  {
20
20
  public Dialect Dialect => Dialect.Sqlite;
21
+ public SqliteVectorSupport VectorSupport { get; }
21
22
  private readonly string _connectionString;
22
23
  private readonly int _commandTimeout;
23
24
 
24
- public SqliteEngine(string connectionString, int commandTimeout)
25
+ public SqliteEngine(string connectionString, int commandTimeout, SqliteVectorSupport vectorSupport = null)
25
26
  {
26
27
  _connectionString = SqliteConnectionString.Normalize(connectionString);
27
28
  _commandTimeout = commandTimeout;
29
+ VectorSupport = vectorSupport ?? new SqliteVectorSupport();
28
30
  }
29
31
 
30
32
  // Set while a transaction is open; statements then join it instead of
@@ -40,9 +42,71 @@ namespace An5Orm
40
42
  using var pragma = conn.CreateCommand();
41
43
  pragma.CommandText = "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;";
42
44
  pragma.ExecuteNonQuery();
45
+ PrepareVectorSearch(conn);
43
46
  return conn;
44
47
  }
45
48
 
49
+ /// <summary>
50
+ /// Registers the distance functions and loads sqlite-vec so a `VECTOR(n)`
51
+ /// column is ranked by SQLite instead of by the client. Neither is
52
+ /// required: a connection that refuses both falls back to `json_each`, or
53
+ /// to scoring in C#.
54
+ ///
55
+ /// This runs per connection because the engine opens one per statement,
56
+ /// and a user function only exists for the connection that declared it.
57
+ /// </summary>
58
+ private void PrepareVectorSearch(Microsoft.Data.Sqlite.SqliteConnection conn)
59
+ {
60
+ var support = VectorSupport;
61
+ if (support == null) return;
62
+
63
+ var hasVec = false;
64
+ if (!string.IsNullOrWhiteSpace(support.SqliteVecPath))
65
+ {
66
+ try { conn.LoadExtension(support.SqliteVecPath); }
67
+ catch { /* the extension is optional */ }
68
+ }
69
+ if (!hasVec)
70
+ {
71
+ try
72
+ {
73
+ using var probe = conn.CreateCommand();
74
+ probe.CommandText = "SELECT vec_version()";
75
+ probe.ExecuteScalar();
76
+ hasVec = true;
77
+ }
78
+ catch { /* not loaded */ }
79
+ }
80
+
81
+ var hasUdf = false;
82
+ try {
83
+ using var native = conn.CreateCommand();
84
+ native.CommandText = "SELECT an5_vector_version()";
85
+ native.ExecuteScalar();
86
+ support.Report(false, true, true);
87
+ return;
88
+ } catch { /* no AN5 native extension */ }
89
+ try
90
+ {
91
+ foreach (var metric in SqliteVectors.An5Functions.Keys)
92
+ {
93
+ var name = SqliteVectors.An5Functions[metric];
94
+ var distance = SqliteVectors.MakeDistanceFunction(metric);
95
+ // The metric is baked into `distance`; the empty state is
96
+ // only there to satisfy the delegate shape.
97
+ conn.CreateFunction<object[], object>(
98
+ name,
99
+ Array.Empty<object>(),
100
+ (_state, args) => distance(args),
101
+ true);
102
+ }
103
+ hasUdf = true;
104
+ }
105
+ catch { /* the bundle may not allow user functions */ }
106
+
107
+ support.Report(hasVec, hasUdf, true);
108
+ }
109
+
46
110
  private ConnectionScope<Microsoft.Data.Sqlite.SqliteConnection> Acquire()
47
111
  => _txnConn != null
48
112
  ? new ConnectionScope<Microsoft.Data.Sqlite.SqliteConnection>(_txnConn, owned: false)
@@ -67,7 +131,9 @@ namespace An5Orm
67
131
  param.ParameterName = paramName;
68
132
  // A JSON vector or a bool needs a type SQLite will store in a
69
133
  // column rather than coerce to an integer.
70
- if (value is bool flag) param.Value = flag ? 1 : 0;
134
+ if (value is float[] || value is double[] || value is List<double>)
135
+ param.Value = SqliteVectors.EncodeVector(SqliteVectors.DecodeVector(value));
136
+ else if (value is bool flag) param.Value = flag ? 1 : 0;
71
137
  else param.Value = value;
72
138
  cmd.Parameters.Add(param);
73
139
  }
@@ -108,6 +174,20 @@ namespace An5Orm
108
174
  if (!HasColumn(reader, prop.Name)) continue;
109
175
  var val = reader[prop.Name];
110
176
  if (val == DBNull.Value) continue;
177
+ // A `VECTOR(n)` column stores a float32 BLOB (and older ones a
178
+ // JSON text column) while the entity declares it as float[],
179
+ // which no converter bridges.
180
+ if (prop.PropertyType == typeof(float[]))
181
+ {
182
+ var vector = SqliteVectors.DecodeVector(val);
183
+ if (vector != null)
184
+ {
185
+ var floats = new float[vector.Count];
186
+ for (int i = 0; i < vector.Count; i++) floats[i] = (float)vector[i];
187
+ prop.SetValue(item, floats);
188
+ continue;
189
+ }
190
+ }
111
191
  try { prop.SetValue(item, Convert.ChangeType(val, prop.PropertyType)); } catch { }
112
192
  }
113
193
  results.Add(item);