@an5/adapters 0.1.3 → 0.2.1
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 +44 -44
- package/dotnet/An5Adapter.cs +275 -275
- package/package.json +17 -10
- package/python/base/metadata.py +10 -10
- package/python/table_client.py +216 -219
- package/typescript/googlesheets/adapter.d.ts +3 -2
- package/typescript/googlesheets/adapter.js +74 -4
- package/typescript/googlesheets/adapter.ts +82 -8
- package/typescript/googlesheets/config.d.ts +11 -0
- package/typescript/googlesheets/config.js +7 -0
- package/typescript/googlesheets/config.ts +11 -0
- package/typescript/googlesheets/helpers.js +1 -1
- package/typescript/googlesheets/helpers.ts +1 -1
- package/typescript/googlesheets/retry.js +21 -8
- package/typescript/googlesheets/retry.ts +17 -8
- package/typescript/googlesheets/tableClient.js +4 -3
- package/typescript/googlesheets/tableClient.ts +6 -5
package/dotnet/An5Adapter.cs
CHANGED
|
@@ -1,275 +1,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
|
-
// ── 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
|
+
// ── 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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@an5/adapters",
|
|
3
|
-
"version": "0.1
|
|
2
|
+
"name": "@an5/adapters",
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Standalone language adapters for AN5 ORM - TypeScript, Python, .NET query execution.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "typescript/index.js",
|
|
@@ -17,15 +17,22 @@
|
|
|
17
17
|
"type": "git",
|
|
18
18
|
"url": "https://github.com/an5ORM/an5Adapters.git"
|
|
19
19
|
},
|
|
20
|
-
"keywords": [
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
20
|
+
"keywords": [
|
|
21
|
+
"an5",
|
|
22
|
+
"orm",
|
|
23
|
+
"adapter",
|
|
24
|
+
"typescript",
|
|
25
|
+
"python",
|
|
26
|
+
"dotnet"
|
|
27
|
+
],
|
|
28
|
+
"files": [
|
|
29
|
+
"typescript/**/*.{ts,js,d.ts}",
|
|
30
|
+
"python/**/*.py",
|
|
31
|
+
"dotnet/**/*",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
27
34
|
"scripts": {
|
|
28
|
-
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
29
36
|
"test": "node test/unit.test.js",
|
|
30
37
|
"test:python": "python -m compileall python 2>&1"
|
|
31
38
|
},
|
package/python/base/metadata.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from typing import Any, Dict
|
|
2
|
-
|
|
3
|
-
model_to_table: Dict[str, str] = {}
|
|
4
|
-
model_fields: Dict[str, Any] = {}
|
|
5
|
-
|
|
6
|
-
def set_adapter_metadata(metadata: Dict[str, Any]) -> None:
|
|
7
|
-
model_to_table.clear()
|
|
8
|
-
model_to_table.update(metadata.get("model_to_table") or metadata.get("modelToTable") or {})
|
|
9
|
-
model_fields.clear()
|
|
10
|
-
model_fields.update(metadata.get("model_fields") or metadata.get("modelFields") or {})
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
model_to_table: Dict[str, str] = {}
|
|
4
|
+
model_fields: Dict[str, Any] = {}
|
|
5
|
+
|
|
6
|
+
def set_adapter_metadata(metadata: Dict[str, Any]) -> None:
|
|
7
|
+
model_to_table.clear()
|
|
8
|
+
model_to_table.update(metadata.get("model_to_table") or metadata.get("modelToTable") or {})
|
|
9
|
+
model_fields.clear()
|
|
10
|
+
model_fields.update(metadata.get("model_fields") or metadata.get("modelFields") or {})
|