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