@an5/adapters 0.2.5 → 0.2.6

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 (82) hide show
  1. package/README.md +189 -305
  2. package/dist/an5Adapter.d.ts +85 -7
  3. package/dist/an5Adapter.js +736 -64
  4. package/dist/base/metadata.d.ts +10 -0
  5. package/dist/base/metadata.js +107 -0
  6. package/dist/base/sql.d.ts +19 -1
  7. package/dist/base/sql.js +152 -24
  8. package/dist/base/types.d.ts +4 -1
  9. package/dist/browser.d.ts +4 -1
  10. package/dist/browser.js +9 -2
  11. package/dist/googlesheets/adapter.d.ts +1 -0
  12. package/dist/googlesheets/adapter.js +9 -2
  13. package/dist/googlesheets/config.d.ts +4 -4
  14. package/dist/googlesheets/helpers.js +2 -0
  15. package/dist/googlesheets/parseConnectionString.js +5 -2
  16. package/dist/googlesheets/sqlExecutor.js +29 -24
  17. package/dist/googlesheets/tableClient.js +14 -5
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +7 -1
  20. package/dist/mssql/engine.js +8 -4
  21. package/dist/mysql/engine.js +1 -1
  22. package/dist/postgres/engine.js +1 -1
  23. package/dist/sqlite/browserEngine.d.ts +27 -0
  24. package/dist/sqlite/browserEngine.js +161 -0
  25. package/dist/sqlite/engine.js +15 -3
  26. package/dist/sqlite/index.d.ts +1 -0
  27. package/dist/sqlite/index.js +1 -0
  28. package/dist/typed.d.ts +5 -0
  29. package/dist/typed.js +2 -0
  30. package/dotnet/An5Adapter.cs +1089 -275
  31. package/dotnet/Base/Core.cs +340 -82
  32. package/dotnet/Mssql/MssqlEngine.cs +145 -144
  33. package/dotnet/Postgres/PostgresEngine.cs +121 -121
  34. package/golang/an5_adapter.go +314 -0
  35. package/golang/base/base.go +100 -0
  36. package/golang/base/metadata.go +116 -0
  37. package/golang/base/where.go +311 -0
  38. package/golang/go.mod +3 -0
  39. package/golang/mssql/mssql.go +32 -0
  40. package/golang/postgres/postgres.go +37 -0
  41. package/golang/sqlite/sqlite.go +14 -0
  42. package/golang/sqlite_test.go +42 -0
  43. package/golang/table_client.go +1329 -0
  44. package/package.json +17 -7
  45. package/python/an5_adapter.py +119 -97
  46. package/python/base/__init__.py +3 -3
  47. package/python/base/dialects.py +11 -8
  48. package/python/base/metadata.py +35 -10
  49. package/python/base/sql.py +172 -121
  50. package/python/mssql/__init__.py +1 -1
  51. package/python/mssql/provider.py +62 -62
  52. package/python/postgres/__init__.py +1 -1
  53. package/python/postgres/provider.py +15 -15
  54. package/python/table_client.py +619 -216
  55. package/typescript/src/an5Adapter.ts +1220 -505
  56. package/typescript/src/base/index.ts +3 -3
  57. package/typescript/src/base/metadata.ts +148 -28
  58. package/typescript/src/base/sql.ts +264 -98
  59. package/typescript/src/base/types.ts +27 -24
  60. package/typescript/src/browser.ts +10 -6
  61. package/typescript/src/config.ts +95 -95
  62. package/typescript/src/googlesheets/adapter.ts +269 -259
  63. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  64. package/typescript/src/googlesheets/config.ts +47 -47
  65. package/typescript/src/googlesheets/helpers.ts +144 -143
  66. package/typescript/src/googlesheets/index.ts +3 -3
  67. package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
  68. package/typescript/src/googlesheets/retry.ts +126 -126
  69. package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
  70. package/typescript/src/googlesheets/tableClient.ts +461 -455
  71. package/typescript/src/googlesheets/types.ts +6 -6
  72. package/typescript/src/index.ts +31 -27
  73. package/typescript/src/mssql/engine.ts +134 -132
  74. package/typescript/src/mssql/index.ts +1 -1
  75. package/typescript/src/mysql/engine.ts +80 -80
  76. package/typescript/src/mysql/index.ts +1 -1
  77. package/typescript/src/postgres/engine.ts +87 -87
  78. package/typescript/src/postgres/index.ts +1 -1
  79. package/typescript/src/sqlite/browserEngine.ts +192 -0
  80. package/typescript/src/sqlite/engine.ts +66 -55
  81. package/typescript/src/sqlite/index.ts +3 -1
  82. package/typescript/src/typed.ts +52 -0
@@ -1,275 +1,1089 @@
1
- // An5Adapter.cs
2
- // Standalone .NET runtime adapter for AN5 ORM.
3
-
4
- using System;
5
- using System.Collections.Generic;
6
- using System.Reflection;
7
- using System.Text;
8
- using System.Text.Json;
9
-
10
- namespace An5Orm
11
- {
12
- // ─── Adapter ────────────────────────────────────────────────────────────────
13
-
14
- public class An5Adapter : IDisposable
15
- {
16
- public string ConnectionString { get; }
17
- public Dialect Dialect => _engine.Dialect;
18
- private readonly IQueryEngine _engine;
19
- private readonly int _commandTimeout;
20
-
21
- public An5Adapter(string connectionString, int commandTimeout = 60)
22
- {
23
- ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
24
- _commandTimeout = commandTimeout;
25
- _engine = DialectDetector.Detect(connectionString) switch
26
- {
27
- Dialect.Postgres => new PostgresEngine(connectionString, commandTimeout),
28
- _ => new MssqlEngine(connectionString, commandTimeout)
29
- };
30
- }
31
-
32
- public An5Adapter(An5AdapterOptions options) : this(options.ConnectionString, options.CommandTimeout) { }
33
-
34
- // ── Raw query execution ────────────────────────────────────────────────
35
-
36
- public List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters = null)
37
- => _engine.QueryRaw(sql, parameters);
38
-
39
- public List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters = null) where T : new()
40
- => _engine.QueryRaw<T>(sql, parameters);
41
-
42
- public int ExecuteRaw(string sql, Dictionary<string, object> parameters = null)
43
- => _engine.ExecuteRaw(sql, parameters);
44
-
45
- // ── Transaction ────────────────────────────────────────────────────────
46
-
47
- public An5Transaction BeginTransaction()
48
- {
49
- return new An5Transaction(_engine.BeginTransaction());
50
- }
51
-
52
- public TResult Transaction<TResult>(Func<An5Adapter, TResult> fn)
53
- {
54
- using var txScope = _engine.BeginTransaction();
55
- try
56
- {
57
- var result = fn(this);
58
- txScope.Commit();
59
- return result;
60
- }
61
- catch
62
- {
63
- txScope.Rollback();
64
- throw;
65
- }
66
- }
67
-
68
- // ── Table client factory ───────────────────────────────────────────────
69
-
70
- public AdapterTableClient<T> Table<T>(string tableName) where T : new()
71
- => new AdapterTableClient<T>(this, tableName, _engine.Dialect);
72
-
73
- public void Dispose() { }
74
- }
75
-
76
- // ─── Transaction wrapper ───────────────────────────────────────────────────
77
-
78
- public class An5Transaction : IDisposable
79
- {
80
- private readonly An5TransactionBase _inner;
81
-
82
- internal An5Transaction(An5TransactionBase inner) => _inner = inner;
83
-
84
- public void Commit() => _inner.Commit();
85
- public void Rollback() => _inner.Rollback();
86
- public void Dispose() => _inner.Dispose();
87
- }
88
-
89
- // ─── Typed Table Client ────────────────────────────────────────────────────
90
-
91
- public class AdapterTableClient<T> where T : new()
92
- {
93
- private readonly An5Adapter _adapter;
94
- private readonly string _tableName;
95
- private readonly Dialect _dialect;
96
-
97
- public AdapterTableClient(An5Adapter adapter, string tableName, Dialect dialect)
98
- {
99
- _adapter = adapter;
100
- _tableName = SqlQuote.QuoteTable(tableName, dialect);
101
- _dialect = dialect;
102
- }
103
-
104
- private string Nolock => _dialect == Dialect.Mssql ? " WITH (NOLOCK)" : "";
105
-
106
- private string BuildPagination(string orderBy, int? skip, int? take)
107
- {
108
- if (take == null) return "";
109
- if (_dialect == Dialect.Postgres)
110
- return $" LIMIT {take.Value} OFFSET {skip ?? 0}";
111
- var o = !string.IsNullOrEmpty(orderBy) ? $" ORDER BY {orderBy}" : " ORDER BY (SELECT NULL)";
112
- return $"{o} OFFSET {skip ?? 0} ROWS FETCH NEXT {take.Value} ROWS ONLY";
113
- }
114
-
115
- public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null,
116
- string orderBy = null, int? skip = null, int? take = null)
117
- {
118
- var sb = new StringBuilder($"SELECT * FROM {_tableName}{Nolock}");
119
- if (!string.IsNullOrEmpty(whereClause)) sb.Append($" WHERE {whereClause}");
120
- if (take != null && _dialect == Dialect.Postgres)
121
- {
122
- if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
123
- sb.Append(BuildPagination(orderBy, skip, take));
124
- }
125
- else
126
- {
127
- if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
128
- sb.Append(BuildPagination(orderBy, skip, take));
129
- }
130
- return _adapter.QueryRaw<T>(sb.ToString(), parameters);
131
- }
132
-
133
- public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null, string orderBy = null)
134
- {
135
- var rows = FindMany(whereClause, parameters, orderBy, skip: null, take: 1);
136
- return rows.Count > 0 ? rows[0] : default;
137
- }
138
-
139
- public T FindUnique(object id, string idColumnName = "Id")
140
- {
141
- return FindFirst($"{SqlQuote.QuoteName(idColumnName, _dialect)} = @id",
142
- new Dictionary<string, object> { { "id", id } });
143
- }
144
-
145
- public int Count(string whereClause = null, Dictionary<string, object> parameters = null)
146
- {
147
- var sql = $"SELECT COUNT(*) FROM {_tableName}";
148
- if (!string.IsNullOrEmpty(whereClause)) sql += $" WHERE {whereClause}";
149
- var rows = _adapter.QueryRaw(sql, parameters);
150
- if (rows.Count > 0)
151
- {
152
- var val = rows[0].Values.GetEnumerator();
153
- val.MoveNext();
154
- return Convert.ToInt32(val.Current);
155
- }
156
- return 0;
157
- }
158
-
159
- public T Create(T entity)
160
- {
161
- var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
162
- var cols = new List<string>();
163
- var vals = new List<string>();
164
- var parameters = new Dictionary<string, object>();
165
-
166
- foreach (var prop in props)
167
- {
168
- var val = prop.GetValue(entity);
169
- if (val == null) continue;
170
- cols.Add(SqlQuote.QuoteName(prop.Name, _dialect));
171
- vals.Add("@" + prop.Name);
172
- parameters["@" + prop.Name] = val;
173
- }
174
-
175
- var sql = $"INSERT INTO {_tableName} ({string.Join(", ", cols)}) VALUES ({string.Join(", ", vals)})";
176
- _adapter.ExecuteRaw(sql, parameters);
177
- return entity;
178
- }
179
-
180
- public T Update(T entity, string idColumnName = "Id")
181
- {
182
- var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
183
- var sets = new List<string>();
184
- var parameters = new Dictionary<string, object>();
185
- object idVal = null;
186
-
187
- foreach (var prop in props)
188
- {
189
- var val = prop.GetValue(entity);
190
- if (prop.Name.Equals(idColumnName, StringComparison.OrdinalIgnoreCase))
191
- {
192
- idVal = val;
193
- }
194
- else if (val != null)
195
- {
196
- sets.Add($"{SqlQuote.QuoteName(prop.Name, _dialect)} = @{prop.Name}");
197
- parameters["@" + prop.Name] = val;
198
- }
199
- }
200
-
201
- if (idVal == null) throw new InvalidOperationException($"Cannot update entity without {idColumnName}");
202
- parameters["@_id"] = idVal;
203
- var quotedIdCol = SqlQuote.QuoteName(idColumnName, _dialect);
204
- var sql = $"UPDATE {_tableName} SET {string.Join(", ", sets)} WHERE {quotedIdCol} = @_id";
205
- _adapter.ExecuteRaw(sql, parameters);
206
- return entity;
207
- }
208
-
209
- public bool Delete(object id, string idColumnName = "Id")
210
- {
211
- var quotedIdCol = SqlQuote.QuoteName(idColumnName, _dialect);
212
- var sql = $"DELETE FROM {_tableName} WHERE {quotedIdCol} = @id";
213
- var n = _adapter.ExecuteRaw(sql, new Dictionary<string, object> { { "@id", id } });
214
- return n > 0;
215
- }
216
-
217
- public int DeleteMany(string whereClause = null, Dictionary<string, object> parameters = null)
218
- {
219
- var sql = $"DELETE FROM {_tableName}";
220
- if (!string.IsNullOrEmpty(whereClause)) sql += $" WHERE {whereClause}";
221
- return _adapter.ExecuteRaw(sql, parameters);
222
- }
223
-
224
- public T Upsert(T entity, string idColumnName = "Id")
225
- {
226
- var idProp = typeof(T).GetProperty(idColumnName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
227
- if (idProp == null) throw new InvalidOperationException($"Property {idColumnName} not found on {typeof(T).Name}");
228
- var idVal = idProp.GetValue(entity);
229
- var existing = idVal != null ? FindUnique(idVal, idColumnName) : default;
230
- if (existing != null) return Update(entity, idColumnName);
231
- return Create(entity);
232
- }
233
-
234
- // ── Vector Search ──────────────────────────────────────────────────────
235
-
236
- public List<(T Item, double Distance)> VectorSearch(
237
- List<double> vector, int take = 10,
238
- string whereClause = null, Dictionary<string, object> parameters = null,
239
- string vectorField = "Embedding", string distanceMetric = "cosine")
240
- {
241
- var rows = FindMany(whereClause, parameters);
242
- var results = new List<(T, double)>();
243
- var vectorProp = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
244
- if (vectorProp == null) return results;
245
-
246
- foreach (var row in rows)
247
- {
248
- var rawVal = vectorProp.GetValue(row);
249
- if (rawVal == null) continue;
250
- List<double> vec = null;
251
- try
252
- {
253
- if (rawVal is string s) vec = JsonSerializer.Deserialize<List<double>>(s);
254
- }
255
- catch { continue; }
256
- if (vec == null || vec.Count != vector.Count) continue;
257
-
258
- double dot = 0, m1 = 0, m2 = 0;
259
- for (int i = 0; i < vector.Count; i++)
260
- {
261
- dot += vector[i] * vec[i];
262
- m1 += vector[i] * vector[i];
263
- m2 += vec[i] * vec[i];
264
- }
265
- double cosine = (m1 > 0 && m2 > 0) ? dot / (Math.Sqrt(m1) * Math.Sqrt(m2)) : 0;
266
- double dist = distanceMetric == "cosine" ? 1.0 - cosine :
267
- distanceMetric == "dot" ? -dot : Math.Sqrt(vector.Count);
268
- results.Add((row, dist));
269
- }
270
-
271
- results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
272
- return results.GetRange(0, Math.Min(take, results.Count));
273
- }
274
- }
275
- }
1
+ // An5Adapter.cs
2
+ // Standalone .NET runtime adapter for AN5 ORM.
3
+
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.Reflection;
7
+ using System.Text;
8
+ using System.Text.Json;
9
+
10
+ namespace An5Orm
11
+ {
12
+ // ─── Adapter ────────────────────────────────────────────────────────────────
13
+
14
+ public class An5Adapter : IDisposable
15
+ {
16
+ public string ConnectionString { get; }
17
+ public Dialect Dialect => _engine.Dialect;
18
+ private readonly IQueryEngine _engine;
19
+ private readonly int _commandTimeout;
20
+
21
+ public An5Adapter(string connectionString, int commandTimeout = 60)
22
+ {
23
+ ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
24
+ _commandTimeout = commandTimeout;
25
+ _engine = DialectDetector.Detect(connectionString) switch
26
+ {
27
+ Dialect.Postgres => new PostgresEngine(connectionString, commandTimeout),
28
+ _ => new MssqlEngine(connectionString, commandTimeout)
29
+ };
30
+ }
31
+
32
+ public An5Adapter(An5AdapterOptions options) : this(options.ConnectionString, options.CommandTimeout) { }
33
+
34
+ // ── Raw query execution ────────────────────────────────────────────────
35
+
36
+ public List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters = null)
37
+ => _engine.QueryRaw(sql, parameters);
38
+
39
+ public List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters = null) where T : new()
40
+ => _engine.QueryRaw<T>(sql, parameters);
41
+
42
+ public int ExecuteRaw(string sql, Dictionary<string, object> parameters = null)
43
+ => _engine.ExecuteRaw(sql, parameters);
44
+
45
+ // ── Stored Procedure Execution ────────────────────────────────────────
46
+
47
+ public List<T> QueryProc<T>(string procName, Dictionary<string, object> parameters = null) where T : new()
48
+ {
49
+ var paramList = new List<string>();
50
+ if (parameters != null)
51
+ {
52
+ foreach (var k in parameters.Keys)
53
+ {
54
+ paramList.Add(Dialect == Dialect.Postgres ? "@" + k : "@" + k + " = @" + k);
55
+ }
56
+ }
57
+ var sql = Dialect == Dialect.Postgres
58
+ ? $"CALL {procName}({string.Join(", ", paramList)})"
59
+ : (paramList.Count > 0 ? $"EXEC {procName} {string.Join(", ", paramList)}" : $"EXEC {procName}");
60
+ return QueryRaw<T>(sql, parameters);
61
+ }
62
+
63
+ public int ExecuteProc(string procName, Dictionary<string, object> parameters = null)
64
+ {
65
+ var paramList = new List<string>();
66
+ if (parameters != null)
67
+ {
68
+ foreach (var k in parameters.Keys)
69
+ {
70
+ paramList.Add(Dialect == Dialect.Postgres ? "@" + k : "@" + k + " = @" + k);
71
+ }
72
+ }
73
+ var sql = Dialect == Dialect.Postgres
74
+ ? $"CALL {procName}({string.Join(", ", paramList)})"
75
+ : (paramList.Count > 0 ? $"EXEC {procName} {string.Join(", ", paramList)}" : $"EXEC {procName}");
76
+ return ExecuteRaw(sql, parameters);
77
+ }
78
+
79
+ // ── Transaction ────────────────────────────────────────────────────────
80
+
81
+ public An5Transaction BeginTransaction()
82
+ {
83
+ return new An5Transaction(_engine.BeginTransaction());
84
+ }
85
+
86
+ public TResult Transaction<TResult>(Func<An5Adapter, TResult> fn)
87
+ {
88
+ using var txScope = _engine.BeginTransaction();
89
+ try
90
+ {
91
+ var result = fn(this);
92
+ txScope.Commit();
93
+ return result;
94
+ }
95
+ catch
96
+ {
97
+ txScope.Rollback();
98
+ throw;
99
+ }
100
+ }
101
+
102
+ // ── Table & View client factory ────────────────────────────────────────
103
+
104
+ public AdapterTableClient<T> Table<T>(string tableName) where T : new()
105
+ => new AdapterTableClient<T>(this, tableName, _engine.Dialect);
106
+
107
+ public ViewClient<T> View<T>(string viewName) where T : new()
108
+ => new ViewClient<T>(this, viewName, _engine.Dialect);
109
+
110
+ public ViewClient<T> View<T>(string viewName, string modelName) where T : new()
111
+ => new ViewClient<T>(this, viewName, modelName, _engine.Dialect);
112
+
113
+ public void Dispose() { }
114
+ }
115
+
116
+ // ─── Adapter metadata injection ─────────────────────────────────────────────
117
+
118
+ public static class An5AdapterMetadata
119
+ {
120
+ public static void Set(AdapterMetadata metadata)
121
+ => An5Metadata.SetAdapterMetadata(metadata);
122
+
123
+ public static Dictionary<string, string> GetModelToTable()
124
+ => An5Metadata.GetModelToTable();
125
+
126
+ public static Dictionary<string, RelationDef> GetRelationsForModel(string modelName)
127
+ => An5Metadata.GetRelationsForModel(modelName);
128
+
129
+ public static object GetFieldsForModel(string modelName)
130
+ => An5Metadata.GetFieldsForModel(modelName);
131
+ }
132
+
133
+ // ─── Transaction wrapper ───────────────────────────────────────────────────
134
+
135
+ public class An5Transaction : IDisposable
136
+ {
137
+ private readonly An5TransactionBase _inner;
138
+
139
+ internal An5Transaction(An5TransactionBase inner) => _inner = inner;
140
+
141
+ public void Commit() => _inner.Commit();
142
+ public void Rollback() => _inner.Rollback();
143
+ public void Dispose() => _inner.Dispose();
144
+ }
145
+
146
+ // ─── Read-Only View Client ──────────────────────────────────────────────────
147
+
148
+ public class ViewClient<T> where T : new()
149
+ {
150
+ private readonly AdapterTableClient<T> _tableClient;
151
+ public string ViewName { get; }
152
+
153
+ public ViewClient(An5Adapter adapter, string viewName, Dialect dialect)
154
+ {
155
+ ViewName = viewName;
156
+ _tableClient = new AdapterTableClient<T>(adapter, viewName, dialect);
157
+ }
158
+
159
+ public ViewClient(An5Adapter adapter, string viewName, string modelName, Dialect dialect)
160
+ {
161
+ ViewName = viewName;
162
+ _tableClient = new AdapterTableClient<T>(adapter, viewName, modelName, dialect);
163
+ }
164
+
165
+ public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null, string orderBy = null, int? skip = null, int? take = null)
166
+ => _tableClient.FindMany(whereClause, parameters, orderBy, skip, take);
167
+
168
+ public List<T> FindMany(FindManyArgs args)
169
+ => _tableClient.FindMany(args);
170
+
171
+ public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null, string orderBy = null)
172
+ => _tableClient.FindFirst(whereClause, parameters, orderBy);
173
+
174
+ public T FindFirst(FindManyArgs args)
175
+ => _tableClient.FindFirst(args);
176
+
177
+ public T FindUnique(object id, string idColumnName = "Id")
178
+ => _tableClient.FindUnique(id, idColumnName);
179
+
180
+ public T FindUnique(FindManyArgs args)
181
+ => _tableClient.FindUnique(args);
182
+
183
+ public int Count(string whereClause = null, Dictionary<string, object> parameters = null)
184
+ => _tableClient.Count(whereClause, parameters);
185
+
186
+ public int Count(CountArgs args)
187
+ => _tableClient.Count(args);
188
+
189
+ public List<(T Item, double Distance)> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
190
+ => _tableClient.VectorSearch(vector, take, whereClause, parameters, vectorField, distanceMetric);
191
+
192
+ public List<(T Item, double Distance)> VectorSearch(VectorSearchArgs args)
193
+ => _tableClient.VectorSearch(args);
194
+
195
+ public Dictionary<string, object> Aggregate(AggregateArgs args)
196
+ => _tableClient.Aggregate(args);
197
+
198
+ public List<T> GroupBy(GroupByArgs args)
199
+ => _tableClient.GroupBy(args);
200
+
201
+ public T Create(T entity) => throw new NotSupportedException($"View '{ViewName}' is read-only. Create is not allowed.");
202
+ public T Update(T entity, string idColumnName = "Id") => throw new NotSupportedException($"View '{ViewName}' is read-only. Update is not allowed.");
203
+ public bool Delete(object id, string idColumnName = "Id") => throw new NotSupportedException($"View '{ViewName}' is read-only. Delete is not allowed.");
204
+ public int DeleteMany(string whereClause = null, Dictionary<string, object> parameters = null) => throw new NotSupportedException($"View '{ViewName}' is read-only. DeleteMany is not allowed.");
205
+ public T Upsert(T entity, string idColumnName = "Id") => throw new NotSupportedException($"View '{ViewName}' is read-only. Upsert is not allowed.");
206
+ }
207
+
208
+ // ─── ORM-style Args types ────────────────────────────────────────────────────
209
+
210
+ public class FindManyArgs
211
+ {
212
+ public object Where { get; set; }
213
+ public object OrderBy { get; set; }
214
+ public int? Skip { get; set; }
215
+ public int? Take { get; set; }
216
+ public object Select { get; set; }
217
+ public object Include { get; set; }
218
+ }
219
+
220
+ public class CountArgs
221
+ {
222
+ public object Where { get; set; }
223
+ }
224
+
225
+ public class CreateArgs
226
+ {
227
+ public Dictionary<string, object> Data { get; set; }
228
+ public object Include { get; set; }
229
+ public object Select { get; set; }
230
+ }
231
+
232
+ public class CreateManyArgs
233
+ {
234
+ public List<Dictionary<string, object>> Data { get; set; }
235
+ public bool SkipDuplicates { get; set; }
236
+ }
237
+
238
+ public class UpdateArgs
239
+ {
240
+ public object Where { get; set; }
241
+ public Dictionary<string, object> Data { get; set; }
242
+ public object Include { get; set; }
243
+ public object Select { get; set; }
244
+ }
245
+
246
+ public class UpdateManyArgs
247
+ {
248
+ public object Where { get; set; }
249
+ public Dictionary<string, object> Data { get; set; }
250
+ }
251
+
252
+ public class DeleteManyArgs
253
+ {
254
+ public object Where { get; set; }
255
+ }
256
+
257
+ public class UpsertArgs
258
+ {
259
+ public object Where { get; set; }
260
+ public Dictionary<string, object> Create { get; set; }
261
+ public Dictionary<string, object> Update { get; set; }
262
+ }
263
+
264
+ public class AggregateArgs
265
+ {
266
+ public object Where { get; set; }
267
+ public object Count { get; set; }
268
+ public object Sum { get; set; }
269
+ public object Avg { get; set; }
270
+ public object Min { get; set; }
271
+ public object Max { get; set; }
272
+ }
273
+
274
+ public class GroupByArgs
275
+ {
276
+ public object By { get; set; }
277
+ public object Where { get; set; }
278
+ public object OrderBy { get; set; }
279
+ public int? Skip { get; set; }
280
+ public int? Take { get; set; }
281
+ public object Sum { get; set; }
282
+ public object Avg { get; set; }
283
+ public object Min { get; set; }
284
+ public object Max { get; set; }
285
+ }
286
+
287
+ public class VectorSearchArgs
288
+ {
289
+ public List<double> Vector { get; set; }
290
+ public int Take { get; set; } = 10;
291
+ public object Where { get; set; }
292
+ public string VectorField { get; set; } = "Embedding";
293
+ public string DistanceMetric { get; set; } = "cosine";
294
+ }
295
+
296
+ internal class DictionaryModel
297
+ {
298
+ }
299
+
300
+ internal static class ArgsHelper
301
+ {
302
+ public static List<string> SelectedFields(object select)
303
+ {
304
+ var keys = new List<string>();
305
+ switch (select)
306
+ {
307
+ case null:
308
+ return keys;
309
+ case string s when !string.IsNullOrEmpty(s):
310
+ keys.Add(s);
311
+ return keys;
312
+ case System.Collections.IList list:
313
+ foreach (var item in list)
314
+ if (item is string key) keys.Add(key);
315
+ return keys;
316
+ case Dictionary<string, object> map:
317
+ foreach (var kv in map)
318
+ if (kv.Value is bool b && b) keys.Add(kv.Key);
319
+ return keys;
320
+ case Dictionary<string, bool> bmap:
321
+ foreach (var kv in bmap)
322
+ if (kv.Value) keys.Add(kv.Key);
323
+ return keys;
324
+ }
325
+ return keys;
326
+ }
327
+
328
+ public static bool HasRelationSelect(object select, Dictionary<string, RelationDef> relations)
329
+ {
330
+ foreach (var key in SelectedFields(select))
331
+ if (key == "_count" || relations.ContainsKey(key)) return true;
332
+ return false;
333
+ }
334
+
335
+ public static List<string> SelectedAggregateFields(object fields)
336
+ {
337
+ var outList = new List<string>();
338
+ switch (fields)
339
+ {
340
+ case null:
341
+ break;
342
+ case string s when !string.IsNullOrEmpty(s):
343
+ outList.Add(s);
344
+ break;
345
+ case System.Collections.IList list:
346
+ foreach (var item in list)
347
+ if (item is string key && !string.IsNullOrEmpty(key)) outList.Add(key);
348
+ break;
349
+ case Dictionary<string, object> map:
350
+ foreach (var kv in map)
351
+ if (kv.Value is bool b && b) outList.Add(kv.Key);
352
+ break;
353
+ case Dictionary<string, bool> bmap:
354
+ foreach (var kv in bmap)
355
+ if (kv.Value) outList.Add(kv.Key);
356
+ break;
357
+ }
358
+ outList.Sort();
359
+ return outList;
360
+ }
361
+
362
+ public static List<string> NormalizeByFields(object by)
363
+ {
364
+ var outList = new List<string>();
365
+ switch (by)
366
+ {
367
+ case string s when !string.IsNullOrEmpty(s):
368
+ outList.Add(s);
369
+ break;
370
+ case System.Collections.IList list:
371
+ foreach (var item in list)
372
+ if (item is string key && !string.IsNullOrEmpty(key)) outList.Add(key);
373
+ break;
374
+ }
375
+ return outList;
376
+ }
377
+
378
+ public static Dictionary<string, object> ProjectDict(Dictionary<string, object> row, object select)
379
+ {
380
+ var projected = new Dictionary<string, object>();
381
+ foreach (var key in SelectedFields(select))
382
+ if (row.TryGetValue(key, out var v)) projected[key] = v;
383
+ if (row.TryGetValue("_count", out var count)) projected["_count"] = count;
384
+ return projected;
385
+ }
386
+ }
387
+
388
+ // ─── Typed Table Client ────────────────────────────────────────────────────
389
+
390
+ public class AdapterTableClient<T> where T : new()
391
+ {
392
+ private readonly An5Adapter _adapter;
393
+ private readonly string _tableName;
394
+ private readonly string _rawTable;
395
+ private readonly string _modelName;
396
+ private readonly Dialect _dialect;
397
+
398
+ public AdapterTableClient(An5Adapter adapter, string tableName, Dialect dialect)
399
+ {
400
+ _adapter = adapter;
401
+ _rawTable = tableName;
402
+ _tableName = SqlQuote.QuoteTable(tableName, dialect);
403
+ _modelName = tableName;
404
+ _dialect = dialect;
405
+ }
406
+
407
+ public AdapterTableClient(An5Adapter adapter, string tableName, string modelName, Dialect dialect)
408
+ {
409
+ _adapter = adapter;
410
+ _rawTable = tableName;
411
+ _tableName = SqlQuote.QuoteTable(tableName, dialect);
412
+ _modelName = modelName;
413
+ _dialect = dialect;
414
+ }
415
+
416
+ public string TableName => _rawTable;
417
+ public string ModelName => _modelName;
418
+
419
+ private string Nolock => _dialect == Dialect.Mssql ? " WITH (NOLOCK)" : "";
420
+
421
+ private string BuildPagination(string orderBy, int? skip, int? take)
422
+ {
423
+ if (take == null) return "";
424
+ if (_dialect == Dialect.Postgres)
425
+ return $" LIMIT {take.Value} OFFSET {skip ?? 0}";
426
+ var o = !string.IsNullOrEmpty(orderBy) ? $" ORDER BY {orderBy}" : " ORDER BY (SELECT NULL)";
427
+ return $"{o} OFFSET {skip ?? 0} ROWS FETCH NEXT {take.Value} ROWS ONLY";
428
+ }
429
+
430
+ public List<T> FindMany(string whereClause = null, Dictionary<string, object> parameters = null,
431
+ string orderBy = null, int? skip = null, int? take = null)
432
+ {
433
+ var sb = new StringBuilder($"SELECT * FROM {_tableName}{Nolock}");
434
+ if (!string.IsNullOrEmpty(whereClause)) sb.Append($" WHERE {whereClause}");
435
+ if (take != null && _dialect == Dialect.Postgres)
436
+ {
437
+ if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
438
+ if (skip != null) sb.Append($" OFFSET {skip.Value} ROWS");
439
+ sb.Append($" LIMIT {take.Value}");
440
+ }
441
+ else
442
+ {
443
+ if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
444
+ sb.Append(BuildPagination(orderBy, skip, take));
445
+ }
446
+ return _adapter.QueryRaw<T>(sb.ToString(), parameters);
447
+ }
448
+
449
+ public List<T> FindMany(FindManyArgs args)
450
+ {
451
+ var parameters = new Dictionary<string, object>();
452
+ var whereSql = SqlBuilder.ParseWhere(args?.Where, parameters, _dialect);
453
+ var orderSql = SqlBuilder.BuildOrderBy(args?.OrderBy, _dialect);
454
+ var skip = args?.Skip;
455
+ var take = args?.Take;
456
+ var hasSkip = skip != null;
457
+
458
+ var cols = "*";
459
+ var keys = ArgsHelper.SelectedFields(args?.Select);
460
+ var isPostgres = _dialect == Dialect.Postgres;
461
+
462
+ if (keys.Count > 0 && !ArgsHelper.HasRelationSelect(args?.Select, An5Metadata.GetRelationsForModel(_modelName)))
463
+ {
464
+ var quoted = keys.ConvertAll(k => SqlQuote.QuoteName(k, _dialect));
465
+ cols = string.Join(", ", quoted);
466
+ }
467
+
468
+ string query;
469
+ if (take != null && !hasSkip)
470
+ {
471
+ if (!isPostgres)
472
+ {
473
+ query = $"SELECT TOP ({take.Value}) {cols} FROM {_tableName}{Nolock}";
474
+ if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
475
+ if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
476
+ }
477
+ else
478
+ {
479
+ query = $"SELECT {cols} FROM {_tableName}";
480
+ if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
481
+ if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
482
+ query += $" LIMIT {take.Value}";
483
+ }
484
+ }
485
+ else if (hasSkip)
486
+ {
487
+ if (!isPostgres)
488
+ {
489
+ query = $"SELECT {cols} FROM {_tableName}{Nolock}";
490
+ if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
491
+ query += $" {(!string.IsNullOrEmpty(orderSql) ? orderSql : "ORDER BY (SELECT NULL)")}";
492
+ query += $" OFFSET {skip.Value} ROWS";
493
+ if (take != null) query += $" FETCH NEXT {take.Value} ROWS ONLY";
494
+ }
495
+ else
496
+ {
497
+ query = $"SELECT {cols} FROM {_tableName}";
498
+ if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
499
+ if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
500
+ if (take != null) query += $" LIMIT {take.Value}";
501
+ else query += " LIMIT ALL";
502
+ query += $" OFFSET {skip.Value}";
503
+ }
504
+ }
505
+ else
506
+ {
507
+ query = $"SELECT {cols} FROM {_tableName}{Nolock}";
508
+ if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
509
+ if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
510
+ }
511
+
512
+ var rows = _adapter.QueryRaw<T>(query, parameters);
513
+
514
+ var include = args?.Include != null && args.Include is Dictionary<string, object> incDict && incDict.Count > 0
515
+ ? incDict
516
+ : null;
517
+ if (include != null)
518
+ ResolveIncludes(rows, include);
519
+
520
+ var select = args?.Select;
521
+ if (select != null)
522
+ {
523
+ if (ArgsHelper.HasRelationSelect(select, An5Metadata.GetRelationsForModel(_modelName)))
524
+ ResolveIncludes(rows, select as Dictionary<string, object>);
525
+ return ApplyProjection(rows, select);
526
+ }
527
+
528
+ return rows;
529
+ }
530
+
531
+ // ── ORM-standard FindFirst / FindUnique / Count ────────────────────────
532
+
533
+ public T FindFirst(FindManyArgs args)
534
+ {
535
+ var list = FindMany(new FindManyArgs
536
+ {
537
+ Where = args?.Where,
538
+ OrderBy = args?.OrderBy,
539
+ Take = 1,
540
+ Select = args?.Select,
541
+ Include = args?.Include,
542
+ });
543
+ return list.Count > 0 ? list[0] : default;
544
+ }
545
+
546
+ public T FindUnique(FindManyArgs args)
547
+ => FindFirst(args);
548
+
549
+ public int Count(CountArgs args)
550
+ {
551
+ var parameters = new Dictionary<string, object>();
552
+ var whereSql = args != null ? SqlBuilder.ParseWhere(args.Where, parameters, _dialect) : "";
553
+ var sql = $"SELECT COUNT(*) FROM {_tableName}{Nolock}";
554
+ if (!string.IsNullOrEmpty(whereSql)) sql += $" WHERE {whereSql}";
555
+ var rows = _adapter.QueryRaw(sql, parameters);
556
+ if (rows.Count > 0)
557
+ {
558
+ using var e = rows[0].Values.GetEnumerator();
559
+ if (e.MoveNext() && e.Current != null) return Convert.ToInt32(e.Current);
560
+ }
561
+ return 0;
562
+ }
563
+
564
+ // ── Projection / Include helpers ──────────────────────────────────────
565
+
566
+ private List<T> ApplyProjection(List<T> rows, object select)
567
+ {
568
+ var result = new List<T>();
569
+ var relations = An5Metadata.GetRelationsForModel(_modelName);
570
+ var isDictSelect = select is Dictionary<string, object> || select is Dictionary<string, bool>;
571
+ if (!isDictSelect)
572
+ {
573
+ var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
574
+ var selectedKeys = new HashSet<string>(ArgsHelper.SelectedFields(select), StringComparer.OrdinalIgnoreCase);
575
+ foreach (var row in rows)
576
+ {
577
+ var item = new T();
578
+ foreach (var prop in props)
579
+ if (selectedKeys.Contains(prop.Name))
580
+ prop.SetValue(item, prop.GetValue(row));
581
+ result.Add(item);
582
+ }
583
+ return result;
584
+ }
585
+
586
+ // Relation select keys may reference a nested dict; keep it simple and
587
+ // copy only selected top-level properties.
588
+ var dictSelect = select as Dictionary<string, object>;
589
+ var boolSet = select as Dictionary<string, bool>;
590
+ foreach (var row in rows)
591
+ {
592
+ var item = new T();
593
+ var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
594
+ foreach (var prop in props)
595
+ {
596
+ if (dictSelect != null && dictSelect.TryGetValue(prop.Name, out var sv) && sv is bool b && b)
597
+ prop.SetValue(item, prop.GetValue(row));
598
+ else if (boolSet != null && boolSet.TryGetValue(prop.Name, out var b2) && b2)
599
+ prop.SetValue(item, prop.GetValue(row));
600
+ }
601
+ result.Add(item);
602
+ }
603
+ _ = relations;
604
+ return result;
605
+ }
606
+
607
+ private void ResolveIncludes(List<T> rows, Dictionary<string, object> include)
608
+ {
609
+ if (rows.Count == 0 || include == null || include.Count == 0) return;
610
+ var modelRelations = An5Metadata.GetRelationsForModel(_modelName);
611
+ if (modelRelations.Count == 0) return;
612
+
613
+ foreach (var kv in include)
614
+ {
615
+ var key = kv.Key;
616
+ if (kv.Value is bool b && !b) continue;
617
+ if (key == "_count")
618
+ {
619
+ foreach (var rel in modelRelations)
620
+ {
621
+ if (rel.Value.RelationType != "many") continue;
622
+ var relKeys = new HashSet<object>();
623
+ foreach (var r in rows)
624
+ {
625
+ var lk = GetPropValue(r, rel.Value.LocalKey);
626
+ if (lk != null) relKeys.Add(lk);
627
+ }
628
+ var childClient = new AdapterTableClient<DictionaryModel>(_adapter, rel.Value.ModelName, _dialect);
629
+ var matches = childClient.FindManyDynamic(new FindManyArgs
630
+ {
631
+ Where = new Dictionary<string, object> { { rel.Value.ForeignKey, new Dictionary<string, object> { { "in", ToList(relKeys) } } } },
632
+ });
633
+ var cmap = new Dictionary<object, int>();
634
+ foreach (var m in matches)
635
+ if (m.ContainsKey(rel.Value.ForeignKey) && m[rel.Value.ForeignKey] != null)
636
+ cmap[m[rel.Value.ForeignKey]] = cmap.GetValueOrDefault(m[rel.Value.ForeignKey]) + 1;
637
+ foreach (var r in rows)
638
+ {
639
+ var lk = GetPropValue(r, rel.Value.LocalKey);
640
+ var countProp = typeof(T).GetProperty("_count", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
641
+ var countBox = countProp != null ? countProp.GetValue(r) as Dictionary<string, object> : null;
642
+ countBox ??= new Dictionary<string, object>();
643
+ countBox[rel.Key] = lk != null && cmap.TryGetValue(lk, out var n) ? n : 0;
644
+ if (countProp != null) TrySetProp(r, "_count", countBox);
645
+ }
646
+ }
647
+ continue;
648
+ }
649
+
650
+ var relation = modelRelations.TryGetValue(key, out var rdef) ? rdef : null;
651
+ if (relation == null) continue;
652
+ var isMany = relation.RelationType == "many";
653
+ var joinKey = isMany ? relation.LocalKey : relation.ForeignKey;
654
+ var matchKey = isMany ? relation.ForeignKey : relation.LocalKey;
655
+
656
+ var uniqueKeys = new HashSet<object>();
657
+ foreach (var r in rows)
658
+ {
659
+ var jv = GetPropValue(r, joinKey);
660
+ if (jv != null) uniqueKeys.Add(jv);
661
+ }
662
+ foreach (var r in rows)
663
+ {
664
+ if (!isMany) { r.GetType(); }
665
+ }
666
+ if (uniqueKeys.Count == 0)
667
+ {
668
+ foreach (var r in rows)
669
+ TrySetProp(r, key, isMany ? new List<object>() : null);
670
+ continue;
671
+ }
672
+
673
+ var childIncludeClient = new AdapterTableClient<DictionaryModel>(_adapter, relation.ModelName, _dialect);
674
+ var subArgs = kv.Value as Dictionary<string, object> ?? new Dictionary<string, object>();
675
+ var subWhere = new Dictionary<string, object>
676
+ {
677
+ { matchKey, new Dictionary<string, object> { { "in", ToList(uniqueKeys) } } },
678
+ };
679
+ if (subArgs.TryGetValue("where", out var subWhereVal) && subWhereVal is Dictionary<string, object> sw)
680
+ foreach (var swKv in sw) subWhere[swKv.Key] = swKv.Value;
681
+
682
+ var orderBy = subArgs.TryGetValue("orderBy", out var oB) ? oB : null;
683
+ var children = childIncludeClient.FindManyDynamic(new FindManyArgs
684
+ {
685
+ Where = subWhere,
686
+ OrderBy = orderBy,
687
+ });
688
+
689
+ Dictionary<object, List<Dictionary<string, object>>> groupMap = new();
690
+ foreach (var c in children)
691
+ {
692
+ var gk = isMany ? (c.ContainsKey(relation.ForeignKey) ? c[relation.ForeignKey] : null)
693
+ : (c.ContainsKey(relation.LocalKey) ? c[relation.LocalKey] : null);
694
+ if (gk == null) continue;
695
+ if (!groupMap.TryGetValue(gk, out var list)) { list = new List<Dictionary<string, object>>(); groupMap[gk] = list; }
696
+ list.Add(c);
697
+ }
698
+
699
+ foreach (var r in rows)
700
+ {
701
+ var gk = isMany ? GetPropValue(r, relation.LocalKey) : GetPropValue(r, relation.ForeignKey);
702
+ var matches = gk != null && groupMap.TryGetValue(gk, out var ml) ? ml : new List<Dictionary<string, object>>();
703
+ if (isMany)
704
+ TrySetProp(r, key, matches);
705
+ else
706
+ TrySetProp(r, key, matches.Count > 0 ? matches[0] : null);
707
+ }
708
+ }
709
+ }
710
+
711
+ private List<Dictionary<string, object>> FindManyDynamic(FindManyArgs args)
712
+ {
713
+ var parameters = new Dictionary<string, object>();
714
+ var whereSql = SqlBuilder.ParseWhere(args?.Where, parameters, _dialect);
715
+ var orderSql = SqlBuilder.BuildOrderBy(args?.OrderBy, _dialect);
716
+ var query = $"SELECT * FROM {_tableName}{Nolock}";
717
+ if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
718
+ if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
719
+ return _adapter.QueryRaw(query, parameters);
720
+ }
721
+
722
+ private static object GetPropValue(object obj, string propName)
723
+ {
724
+ var prop = obj.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
725
+ return prop?.GetValue(obj);
726
+ }
727
+
728
+ private static void TrySetProp(object obj, string propName, object value)
729
+ {
730
+ var prop = obj.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
731
+ if (prop == null || !prop.CanWrite) return;
732
+ if (value == null) { prop.SetValue(obj, null); return; }
733
+ try
734
+ {
735
+ if (prop.PropertyType.IsAssignableFrom(value.GetType()))
736
+ prop.SetValue(obj, value);
737
+ else if (prop.PropertyType == typeof(string))
738
+ prop.SetValue(obj, System.Text.Json.JsonSerializer.Serialize(value));
739
+ }
740
+ catch { }
741
+ }
742
+
743
+ private static List<object> ToList(ICollection<object> src)
744
+ {
745
+ var list = new List<object>(src.Count);
746
+ foreach (var item in src) list.Add(item);
747
+ return list;
748
+ }
749
+
750
+ // ── ORM-standard Aggregate / GroupBy ───────────────────────────────────
751
+
752
+ public Dictionary<string, object> Aggregate(AggregateArgs args)
753
+ {
754
+ args ??= new AggregateArgs();
755
+ var parameters = new Dictionary<string, object>();
756
+ var whereSql = SqlBuilder.ParseWhere(args.Where, parameters, _dialect);
757
+ var aggs = new List<string>();
758
+ var countFields = ArgsHelper.SelectedAggregateFields(args.Count);
759
+ if (countFields.Count == 0 && args.Count != null && args.Count is not bool)
760
+ {
761
+ // noop
762
+ }
763
+ if (args.Count != null)
764
+ {
765
+ if (countFields.Count == 0)
766
+ aggs.Add("COUNT(*) AS cnt_all");
767
+ else
768
+ foreach (var f in countFields)
769
+ aggs.Add($"COUNT({SqlQuote.QuoteName(f, _dialect)}) AS cnt_{ArgsHelper_Sanitize(f)}");
770
+ }
771
+ AddAggregates(aggs, "SUM", args.Sum);
772
+ AddAggregates(aggs, "AVG", args.Avg);
773
+ AddAggregates(aggs, "MIN", args.Min);
774
+ AddAggregates(aggs, "MAX", args.Max);
775
+ if (aggs.Count == 0)
776
+ throw new InvalidOperationException("Aggregate requires at least one aggregator field");
777
+
778
+ var sql = $"SELECT {string.Join(", ", aggs)} FROM {_tableName}{Nolock}";
779
+ if (!string.IsNullOrEmpty(whereSql)) sql += $" WHERE {whereSql}";
780
+ var rows = _adapter.QueryRaw(sql, parameters);
781
+ var row = rows.Count > 0 ? rows[0] : new Dictionary<string, object>();
782
+ var result = new Dictionary<string, object>();
783
+
784
+ if (args.Count != null)
785
+ {
786
+ var all = ToNumber(row.TryGetValue("cnt_all", out var ca) ? ca : row.TryGetValue("_count", out var cc) ? cc : 0);
787
+ var countRes = new Dictionary<string, object> { { "_all", all } };
788
+ foreach (var f in countFields)
789
+ {
790
+ if (f == "_all") continue;
791
+ var v = row.TryGetValue("cnt_" + ArgsHelper_Sanitize(f), out var cv) ? cv : all;
792
+ countRes[f] = ToNumber(v);
793
+ }
794
+ result["_count"] = countRes;
795
+ }
796
+ AddAggregateResult(result, "sum", "SUM", args.Sum, row);
797
+ AddAggregateResult(result, "avg", "AVG", args.Avg, row);
798
+ AddAggregateResult(result, "min", "MIN", args.Min, row);
799
+ AddAggregateResult(result, "max", "MAX", args.Max, row);
800
+ return result;
801
+ }
802
+
803
+ private void AddAggregates(List<string> aggs, string op, object fields)
804
+ {
805
+ foreach (var f in ArgsHelper.SelectedAggregateFields(fields))
806
+ aggs.Add($"{op}({SqlQuote.QuoteName(f, _dialect)}) AS {op.ToLowerInvariant()}_{ArgsHelper_Sanitize(f)}");
807
+ }
808
+
809
+ private void AddAggregateResult(Dictionary<string, object> result, string key, string op, object fields, Dictionary<string, object> row)
810
+ {
811
+ var list = ArgsHelper.SelectedAggregateFields(fields);
812
+ if (list.Count == 0) return;
813
+ var sub = new Dictionary<string, object>();
814
+ foreach (var f in list)
815
+ {
816
+ var kk = key + "_" + ArgsHelper_Sanitize(f);
817
+ var v = row.TryGetValue(kk, out var rv) ? rv : null;
818
+ sub[f] = v == null ? null : ToNumber(v);
819
+ }
820
+ result["_" + key] = sub;
821
+ }
822
+
823
+ private static string ArgsHelper_Sanitize(string name) => SqlBuilder.SanitizeParamName(name);
824
+
825
+ private static double ToNumber(object v)
826
+ {
827
+ if (v == null) return 0;
828
+ try { return Convert.ToDouble(v); } catch { return 0; }
829
+ }
830
+
831
+ public List<T> GroupBy(GroupByArgs args)
832
+ {
833
+ args ??= new GroupByArgs();
834
+ var byFields = ArgsHelper.NormalizeByFields(args.By);
835
+ if (byFields.Count == 0)
836
+ throw new InvalidOperationException("groupBy requires 'by' fields");
837
+
838
+ var parameters = new Dictionary<string, object>();
839
+ var whereSql = SqlBuilder.ParseWhere(args.Where, parameters, _dialect);
840
+ var byCols = string.Join(", ", byFields.ConvertAll(f => SqlQuote.QuoteName(f, _dialect)));
841
+ var aggs = new List<string> { "COUNT(*) AS _count" };
842
+ AddAggregates(aggs, "SUM", args.Sum);
843
+ AddAggregates(aggs, "AVG", args.Avg);
844
+ AddAggregates(aggs, "MIN", args.Min);
845
+ AddAggregates(aggs, "MAX", args.Max);
846
+
847
+ var query = $"SELECT {byCols}, {string.Join(", ", aggs)} FROM {_tableName}";
848
+ if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
849
+ query += $" GROUP BY {byCols}";
850
+
851
+ var orderSql = SqlBuilder.BuildOrderBy(args.OrderBy, _dialect);
852
+ var hasSkip = args.Skip != null;
853
+ var hasTake = args.Take != null;
854
+ if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
855
+ else if (hasSkip || hasTake) query += $" ORDER BY {byCols}";
856
+ if (hasSkip || hasTake)
857
+ {
858
+ var skip = args.Skip ?? 0;
859
+ var take = args.Take ?? 1;
860
+ if (_dialect == Dialect.Postgres)
861
+ {
862
+ if (hasTake) query += $" LIMIT {take}";
863
+ query += $" OFFSET {skip}";
864
+ }
865
+ else
866
+ {
867
+ query += $" OFFSET {skip} ROWS";
868
+ if (hasTake) query += $" FETCH NEXT {take} ROWS ONLY";
869
+ }
870
+ }
871
+ return _adapter.QueryRaw<T>(query, parameters);
872
+ }
873
+
874
+ // ── ORM-standard VectorSearch ──────────────────────────────────────────
875
+
876
+ public List<(T Item, double Distance)> VectorSearch(VectorSearchArgs args)
877
+ {
878
+ args ??= new VectorSearchArgs();
879
+ return VectorSearch(args.Vector, args.Take, null, null, args.VectorField, args.DistanceMetric);
880
+ }
881
+
882
+ public T FindFirst(string whereClause = null, Dictionary<string, object> parameters = null, string orderBy = null)
883
+ {
884
+ var rows = FindMany(whereClause, parameters, orderBy, skip: null, take: 1);
885
+ return rows.Count > 0 ? rows[0] : default;
886
+ }
887
+
888
+ public T FindUnique(object id, string idColumnName = "Id")
889
+ {
890
+ return FindFirst($"{SqlQuote.QuoteName(idColumnName, _dialect)} = @id",
891
+ new Dictionary<string, object> { { "id", id } });
892
+ }
893
+
894
+ public int Count(string whereClause = null, Dictionary<string, object> parameters = null)
895
+ {
896
+ var sql = $"SELECT COUNT(*) FROM {_tableName}";
897
+ if (!string.IsNullOrEmpty(whereClause)) sql += $" WHERE {whereClause}";
898
+ var rows = _adapter.QueryRaw(sql, parameters);
899
+ if (rows.Count > 0)
900
+ {
901
+ var val = rows[0].Values.GetEnumerator();
902
+ val.MoveNext();
903
+ return Convert.ToInt32(val.Current);
904
+ }
905
+ return 0;
906
+ }
907
+
908
+ public T Create(T entity)
909
+ {
910
+ var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
911
+ var cols = new List<string>();
912
+ var vals = new List<string>();
913
+ var parameters = new Dictionary<string, object>();
914
+
915
+ foreach (var prop in props)
916
+ {
917
+ var val = prop.GetValue(entity);
918
+ if (val == null) continue;
919
+ cols.Add(SqlQuote.QuoteName(prop.Name, _dialect));
920
+ vals.Add("@" + prop.Name);
921
+ parameters["@" + prop.Name] = val;
922
+ }
923
+
924
+ var sql = $"INSERT INTO {_tableName} ({string.Join(", ", cols)}) VALUES ({string.Join(", ", vals)})";
925
+ _adapter.ExecuteRaw(sql, parameters);
926
+ return entity;
927
+ }
928
+
929
+ public T Update(T entity, string idColumnName = "Id")
930
+ {
931
+ var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
932
+ var sets = new List<string>();
933
+ var parameters = new Dictionary<string, object>();
934
+ object idVal = null;
935
+
936
+ foreach (var prop in props)
937
+ {
938
+ var val = prop.GetValue(entity);
939
+ if (prop.Name.Equals(idColumnName, StringComparison.OrdinalIgnoreCase))
940
+ {
941
+ idVal = val;
942
+ }
943
+ else if (val != null)
944
+ {
945
+ sets.Add($"{SqlQuote.QuoteName(prop.Name, _dialect)} = @{prop.Name}");
946
+ parameters["@" + prop.Name] = val;
947
+ }
948
+ }
949
+
950
+ if (idVal == null) throw new InvalidOperationException($"Cannot update entity without {idColumnName}");
951
+ parameters["@_id"] = idVal;
952
+ var quotedIdCol = SqlQuote.QuoteName(idColumnName, _dialect);
953
+ var sql = $"UPDATE {_tableName} SET {string.Join(", ", sets)} WHERE {quotedIdCol} = @_id";
954
+ _adapter.ExecuteRaw(sql, parameters);
955
+ return entity;
956
+ }
957
+
958
+ public bool Delete(object id, string idColumnName = "Id")
959
+ {
960
+ var quotedIdCol = SqlQuote.QuoteName(idColumnName, _dialect);
961
+ var sql = $"DELETE FROM {_tableName} WHERE {quotedIdCol} = @id";
962
+ var n = _adapter.ExecuteRaw(sql, new Dictionary<string, object> { { "@id", id } });
963
+ return n > 0;
964
+ }
965
+
966
+ public int DeleteMany(string whereClause = null, Dictionary<string, object> parameters = null)
967
+ {
968
+ var sql = $"DELETE FROM {_tableName}";
969
+ if (!string.IsNullOrEmpty(whereClause)) sql += $" WHERE {whereClause}";
970
+ return _adapter.ExecuteRaw(sql, parameters);
971
+ }
972
+
973
+ public T Upsert(T entity, string idColumnName = "Id")
974
+ {
975
+ var idProp = typeof(T).GetProperty(idColumnName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
976
+ if (idProp == null) throw new InvalidOperationException($"Property {idColumnName} not found on {typeof(T).Name}");
977
+ var idVal = idProp.GetValue(entity);
978
+ var existing = idVal != null ? FindUnique(idVal, idColumnName) : default;
979
+ if (existing != null) return Update(entity, idColumnName);
980
+ return Create(entity);
981
+ }
982
+
983
+ // ── Vector Search ──────────────────────────────────────────────────────
984
+
985
+ public List<(T Item, double Distance)> VectorSearch(
986
+ List<double> vector, int take = 10,
987
+ string whereClause = null, Dictionary<string, object> parameters = null,
988
+ string vectorField = "Embedding", string distanceMetric = "cosine")
989
+ {
990
+ // 1. Primary path: Native database SQL vector query execution (MSSQL VECTOR_DISTANCE / Postgres pgvector)
991
+ try
992
+ {
993
+ var dim = vector.Count;
994
+ var vecJson = JsonSerializer.Serialize(vector);
995
+ var p = parameters != null ? new Dictionary<string, object>(parameters) : new Dictionary<string, object>();
996
+ p["query_vector"] = vecJson;
997
+ var quotedVectorField = SqlQuote.QuoteName(vectorField, _dialect);
998
+
999
+ string sql;
1000
+ if (_dialect == Dialect.Postgres)
1001
+ {
1002
+ string op = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? "<=>" :
1003
+ (distanceMetric.Equals("euclidean", StringComparison.OrdinalIgnoreCase) ? "<->" : "<#>");
1004
+ sql = $"SELECT *, ({quotedVectorField} {op} @query_vector::vector) AS distance FROM {_tableName}";
1005
+ if (!string.IsNullOrWhiteSpace(whereClause))
1006
+ sql += $" WHERE {quotedVectorField} IS NOT NULL AND ({whereClause})";
1007
+ else
1008
+ sql += $" WHERE {quotedVectorField} IS NOT NULL";
1009
+ sql += $" ORDER BY distance ASC LIMIT {take}";
1010
+ }
1011
+ else
1012
+ {
1013
+ sql = $"SELECT TOP ({take}) *, VECTOR_DISTANCE('{distanceMetric}', CAST({quotedVectorField} AS VECTOR({dim}, float32)), CAST(@query_vector AS VECTOR({dim}, float32))) AS distance FROM {_tableName}{Nolock}";
1014
+ if (!string.IsNullOrWhiteSpace(whereClause))
1015
+ sql += $" WHERE {quotedVectorField} IS NOT NULL AND ({whereClause})";
1016
+ else
1017
+ sql += $" WHERE {quotedVectorField} IS NOT NULL";
1018
+ sql += " ORDER BY distance ASC";
1019
+ }
1020
+
1021
+ var nativeRows = _adapter.QueryRaw<T>(sql, p);
1022
+ if (nativeRows != null && nativeRows.Count > 0)
1023
+ {
1024
+ var nativeResults = new List<(T, double)>();
1025
+ foreach (var row in nativeRows)
1026
+ {
1027
+ var distanceProp = typeof(T).GetProperty("Distance", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
1028
+ double dist = 0;
1029
+ if (distanceProp != null && distanceProp.GetValue(row) is double dVal)
1030
+ {
1031
+ dist = dVal;
1032
+ }
1033
+ nativeResults.Add((row, dist));
1034
+ }
1035
+ return nativeResults;
1036
+ }
1037
+ }
1038
+ catch
1039
+ {
1040
+ // Secondary fallback: In-memory similarity computation if DB instance lacks native vector extension
1041
+ }
1042
+
1043
+ // 2. Secondary fallback: In-memory similarity computation
1044
+ var rows = FindMany(whereClause, parameters);
1045
+ var results = new List<(T, double)>();
1046
+ var vectorProp = typeof(T).GetProperty(vectorField, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
1047
+ if (vectorProp == null) return results;
1048
+
1049
+ foreach (var row in rows)
1050
+ {
1051
+ var rawVal = vectorProp.GetValue(row);
1052
+ if (rawVal == null) continue;
1053
+ List<double> vec = null;
1054
+ try
1055
+ {
1056
+ if (rawVal is string s) vec = JsonSerializer.Deserialize<List<double>>(s);
1057
+ }
1058
+ catch { continue; }
1059
+ if (vec == null || vec.Count != vector.Count) continue;
1060
+
1061
+ double dot = 0, m1 = 0, m2 = 0;
1062
+ for (int i = 0; i < vector.Count; i++)
1063
+ {
1064
+ dot += vector[i] * vec[i];
1065
+ m1 += vector[i] * vector[i];
1066
+ m2 += vec[i] * vec[i];
1067
+ }
1068
+ double cosine = (m1 > 0 && m2 > 0) ? dot / (Math.Sqrt(m1) * Math.Sqrt(m2)) : 0;
1069
+ double dist = distanceMetric.Equals("cosine", StringComparison.OrdinalIgnoreCase) ? 1.0 - cosine :
1070
+ distanceMetric.Equals("dot", StringComparison.OrdinalIgnoreCase) ? -dot : EuclideanDistance(vector, vec);
1071
+ results.Add((row, dist));
1072
+ }
1073
+
1074
+ results.Sort((a, b) => a.Item2.CompareTo(b.Item2));
1075
+ return results.GetRange(0, Math.Min(take, results.Count));
1076
+ }
1077
+
1078
+ private static double EuclideanDistance(List<double> left, List<double> right)
1079
+ {
1080
+ double sum = 0;
1081
+ for (int i = 0; i < left.Count; i++)
1082
+ {
1083
+ var diff = left[i] - right[i];
1084
+ sum += diff * diff;
1085
+ }
1086
+ return Math.Sqrt(sum);
1087
+ }
1088
+ }
1089
+ }