@an5/adapters 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +263 -0
  2. package/dotnet/An5Adapter.cs +275 -0
  3. package/dotnet/Base/Core.cs +82 -0
  4. package/dotnet/Mssql/MssqlEngine.cs +144 -0
  5. package/dotnet/Postgres/PostgresEngine.cs +121 -0
  6. package/package.json +46 -0
  7. package/python/an5_adapter.py +97 -0
  8. package/python/base/__init__.py +3 -0
  9. package/python/base/dialects.py +8 -0
  10. package/python/base/metadata.py +9 -0
  11. package/python/base/sql.py +121 -0
  12. package/python/mssql/__init__.py +1 -0
  13. package/python/mssql/provider.py +62 -0
  14. package/python/postgres/__init__.py +1 -0
  15. package/python/postgres/provider.py +15 -0
  16. package/python/table_client.py +209 -0
  17. package/typescript/an5Adapter.d.ts +117 -0
  18. package/typescript/an5Adapter.js +447 -0
  19. package/typescript/an5Adapter.ts +474 -0
  20. package/typescript/base/index.d.ts +3 -0
  21. package/typescript/base/index.js +19 -0
  22. package/typescript/base/index.ts +3 -0
  23. package/typescript/base/metadata.d.ts +14 -0
  24. package/typescript/base/metadata.js +17 -0
  25. package/typescript/base/metadata.ts +28 -0
  26. package/typescript/base/sql.d.ts +4 -0
  27. package/typescript/base/sql.js +143 -0
  28. package/typescript/base/sql.ts +98 -0
  29. package/typescript/base/types.d.ts +21 -0
  30. package/typescript/base/types.js +2 -0
  31. package/typescript/base/types.ts +24 -0
  32. package/typescript/config.d.ts +17 -0
  33. package/typescript/config.js +92 -0
  34. package/typescript/config.ts +95 -0
  35. package/typescript/googlesheets/adapter.d.ts +31 -0
  36. package/typescript/googlesheets/adapter.js +138 -0
  37. package/typescript/googlesheets/adapter.ts +167 -0
  38. package/typescript/googlesheets/an5SheetsAdapter.d.ts +3 -0
  39. package/typescript/googlesheets/an5SheetsAdapter.js +8 -0
  40. package/typescript/googlesheets/an5SheetsAdapter.ts +3 -0
  41. package/typescript/googlesheets/config.d.ts +17 -0
  42. package/typescript/googlesheets/config.js +29 -0
  43. package/typescript/googlesheets/config.ts +33 -0
  44. package/typescript/googlesheets/helpers.d.ts +9 -0
  45. package/typescript/googlesheets/helpers.js +155 -0
  46. package/typescript/googlesheets/helpers.ts +124 -0
  47. package/typescript/googlesheets/index.d.ts +2 -0
  48. package/typescript/googlesheets/index.js +9 -0
  49. package/typescript/googlesheets/index.ts +2 -0
  50. package/typescript/googlesheets/parseConnectionString.d.ts +11 -0
  51. package/typescript/googlesheets/parseConnectionString.js +44 -0
  52. package/typescript/googlesheets/parseConnectionString.ts +44 -0
  53. package/typescript/googlesheets/retry.d.ts +3 -0
  54. package/typescript/googlesheets/retry.js +26 -0
  55. package/typescript/googlesheets/retry.ts +23 -0
  56. package/typescript/googlesheets/sqlExecutor.d.ts +12 -0
  57. package/typescript/googlesheets/sqlExecutor.js +306 -0
  58. package/typescript/googlesheets/sqlExecutor.ts +301 -0
  59. package/typescript/googlesheets/tableClient.d.ts +78 -0
  60. package/typescript/googlesheets/tableClient.js +394 -0
  61. package/typescript/googlesheets/tableClient.ts +434 -0
  62. package/typescript/googlesheets/types.d.ts +4 -0
  63. package/typescript/googlesheets/types.js +3 -0
  64. package/typescript/googlesheets/types.ts +6 -0
  65. package/typescript/index.d.ts +2 -0
  66. package/typescript/index.js +14 -0
  67. package/typescript/index.ts +15 -0
  68. package/typescript/mssql/engine.d.ts +16 -0
  69. package/typescript/mssql/engine.js +138 -0
  70. package/typescript/mssql/engine.ts +132 -0
  71. package/typescript/mssql/index.d.ts +1 -0
  72. package/typescript/mssql/index.js +17 -0
  73. package/typescript/mssql/index.ts +1 -0
  74. package/typescript/mysql/engine.d.ts +13 -0
  75. package/typescript/mysql/engine.js +77 -0
  76. package/typescript/mysql/engine.ts +80 -0
  77. package/typescript/mysql/index.d.ts +1 -0
  78. package/typescript/mysql/index.js +17 -0
  79. package/typescript/mysql/index.ts +1 -0
  80. package/typescript/postgres/engine.d.ts +13 -0
  81. package/typescript/postgres/engine.js +86 -0
  82. package/typescript/postgres/engine.ts +87 -0
  83. package/typescript/postgres/index.d.ts +1 -0
  84. package/typescript/postgres/index.js +17 -0
  85. package/typescript/postgres/index.ts +1 -0
  86. package/typescript/sqlite/engine.d.ts +13 -0
  87. package/typescript/sqlite/engine.js +58 -0
  88. package/typescript/sqlite/engine.ts +55 -0
  89. package/typescript/sqlite/index.d.ts +1 -0
  90. package/typescript/sqlite/index.js +17 -0
  91. package/typescript/sqlite/index.ts +1 -0
package/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # an5Adapters
2
+
3
+ Standalone runtime adapters for AN5 ORM. Provides connection pooling, query execution, typed table clients in TypeScript, Python, .NET, and Google Sheets API.
4
+
5
+ Adapters are runtime packages only. They do not import generated `an5Client` artifacts; generated clients or applications can pass model metadata explicitly when table-name mapping or field type coercion is needed.
6
+
7
+ ## Features
8
+
9
+ - **Connection pooling** — Managed connection pools with configurable limits
10
+ - **Type-safe table clients** — Generic CRUD operations with type inference
11
+ - **Full query support** — WHERE, ORDER BY, pagination, aggregates
12
+ - **Vector search** — Cosine, euclidean, and dot product similarity
13
+ - **Transactions** — Begin/commit/rollback with automatic cleanup
14
+ - **Cross-language** — Same API in TypeScript, Python, and .NET
15
+ - **Google Sheets** — Use spreadsheets as a database with the same CRUD API
16
+
17
+ ## Installation
18
+
19
+ ### TypeScript
20
+
21
+ ```bash
22
+ npm install an5-adapters
23
+ ```
24
+
25
+ ### Python
26
+
27
+ ```bash
28
+ pip install an5-adapters
29
+ ```
30
+
31
+ ### .NET
32
+
33
+ ```bash
34
+ dotnet add package An5Adapters
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### TypeScript
40
+
41
+ ```typescript
42
+ import { createAn5Adapter, setAdapterMetadata } from 'an5-adapters';
43
+
44
+ setAdapterMetadata({
45
+ modelToTable: { User: 'dbo.users' },
46
+ modelFields: {
47
+ User: {
48
+ id: { ts: 'string', sql: 'uniqueidentifier', isId: true },
49
+ active: { ts: 'boolean', sql: 'bit' },
50
+ },
51
+ },
52
+ });
53
+
54
+ const db = createAn5Adapter({
55
+ connectionString: 'sqlserver://localhost:1433;database=mydb;user=sa;password=pass',
56
+ });
57
+
58
+ // Table client
59
+ const users = db.table<User>('users');
60
+ await users.findMany({ where: { active: true }, take: 10 });
61
+
62
+ // Raw queries
63
+ const rows = await db.exec('SELECT * FROM users WHERE id = @id', { id: '123' });
64
+
65
+ // Transactions
66
+ await db.$transaction(async (tx) => {
67
+ await tx.table('users').create({ data: { name: 'John' } });
68
+ });
69
+ ```
70
+
71
+ ### Python
72
+
73
+ ```python
74
+ from an5_adapter import create_an5_adapter
75
+ from base.metadata import set_adapter_metadata
76
+
77
+ set_adapter_metadata({
78
+ "modelToTable": {"User": "dbo.users"},
79
+ "modelFields": {
80
+ "User": {
81
+ "id": {"py": "str", "sql": "uniqueidentifier", "isId": True},
82
+ "active": {"py": "bool", "sql": "bit"},
83
+ }
84
+ },
85
+ })
86
+
87
+ db = create_an5_adapter("sqlserver://localhost:1433;database=mydb;user=sa;password=pass")
88
+
89
+ # Table client
90
+ users = db.table("User")
91
+ users.find_many(where={"active": True}, take=10)
92
+
93
+ # Raw queries
94
+ rows = db.exec("SELECT * FROM users WHERE id = ?", params=["123"])
95
+
96
+ # Transactions
97
+ db.transaction(lambda tx: tx.table("User").create({"name": "John"}))
98
+ ```
99
+
100
+ ### .NET
101
+
102
+ ```csharp
103
+ using An5Orm;
104
+
105
+ var db = new An5Adapter(connectionString);
106
+
107
+ // Table client
108
+ var users = db.Table<User>("dbo.users");
109
+ var activeUsers = users.FindMany("IsActive = @p", new { p = true });
110
+
111
+ // Raw queries
112
+ var rows = db.QueryRaw("SELECT * FROM users WHERE Id = @id", new { id = "123" });
113
+
114
+ // Transactions
115
+ db.Transaction(tx => {
116
+ tx.Table<User>("dbo.users").Create(new User { Name = "John" });
117
+ });
118
+ ```
119
+
120
+ ### Google Sheets
121
+
122
+ ```typescript
123
+ import { createAn5Adapter } from 'an5-adapters';
124
+
125
+ const db = createAn5Adapter({
126
+ spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms',
127
+ // Option 1: client email + private key
128
+ clientEmail: 'sa@project.iam.gserviceaccount.com',
129
+ privateKey: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----',
130
+ // Option 2: full service account JSON
131
+ // credentials: { client_email: '...', private_key: '...' },
132
+ // Optional: map model names to sheet names
133
+ sheetMapping: { users: 'UsersData', orders: 'OrdersData' },
134
+ });
135
+
136
+ // Table client (same API as SQL adapters)
137
+ const users = db.table<User>('users');
138
+ await users.findMany({ where: { active: true }, take: 10 });
139
+ await users.create({ data: { name: 'John', email: 'john@example.com' } });
140
+ await users.update({ where: { email: 'john@example.com' }, data: { name: 'Johnny' } });
141
+ await users.delete({ where: { email: 'john@example.com' } });
142
+
143
+ // Raw range access (Google Sheets specific)
144
+ const rawData = await db.readRange('Sheet1!A1:C10');
145
+ await db.writeRange('Sheet1!A1:B2', [['Name', 'Age'], ['Alice', '30']]);
146
+ await db.appendRange('Sheet1!A:A', [['Bob', '25']]);
147
+
148
+ // Auto-creates sheet + header row on first create()
149
+ await db.table('orders').create({ data: { id: '1', total: 100 } });
150
+
151
+ // List, delete sheets
152
+ const sheets = await db.listSheets();
153
+ await db.deleteSheet('OldSheet');
154
+
155
+ // Clear data (keeps headers) or delete all rows
156
+ await db.table('users').clear();
157
+ await db.table('users').deleteAll();
158
+ ```
159
+
160
+ ### Integrated factory (auto-detect adapter)
161
+
162
+ ```typescript
163
+ import { createAn5Adapter, createAdapter, An5Adapter } from 'an5-adapters';
164
+
165
+ // Auto-detects from connection string
166
+ const sqlDb = createAn5Adapter({ connectionString: 'sqlserver://localhost:1433;database=mydb;user=sa;password=pass' });
167
+
168
+ const sheetsDb = createAn5Adapter({
169
+ connectionString: 'googlesheets://spreadsheetId;clientEmail=sa@project.iam.gserviceaccount.com;privateKey=...',
170
+ });
171
+
172
+ // Or use the Sheets config object directly (also auto-detected)
173
+ const sheetsDb2 = createAdapter({
174
+ spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms',
175
+ clientEmail: 'sa@project.iam.gserviceaccount.com',
176
+ privateKey: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----',
177
+ });
178
+
179
+ // Constructor form also delegates googlesheets:// to the Sheets adapter
180
+ const sheetsDb3 = new An5Adapter({
181
+ connectionString: 'googlesheets://spreadsheetId;clientEmail=sa@project.iam.gserviceaccount.com;privateKey=...',
182
+ });
183
+ ```
184
+
185
+ **Notes:**
186
+ - Each model/table maps to a **sheet tab** (first row = headers)
187
+ - Sheets without header rows get auto-created on first `create()`
188
+ - Type coercion can use optional adapter metadata (`setAdapterMetadata`) for field types and model-to-table mapping
189
+ - Numeric strings (without leading zeros) are auto-coerced; `"00123"` stays string
190
+ - Boolean strings `"true"` / `"false"` are auto-coerced
191
+ - Sheet names with spaces are automatically escaped (A1 notation)
192
+ - Supports service account JSON or individual `clientEmail`+`privateKey`
193
+ - Automatic retry with exponential backoff for rate limits (429/500/503)
194
+
195
+ ### Provider Imports
196
+
197
+ Use the package root for normal applications:
198
+
199
+ ```typescript
200
+ import { createAn5Adapter } from 'an5-adapters';
201
+ ```
202
+
203
+ Provider folders are still available to source-level consumers through `typescript/*`, but the public factory in `typescript/an5Adapter.ts` is the preferred entry point. The old `unified.ts` entry point has been removed because the factory now lives directly in `An5Adapter`.
204
+
205
+ ## API Reference
206
+
207
+ ### An5Adapter / An5SheetsAdapter
208
+
209
+ | Method | Description |
210
+ |--------|-------------|
211
+ | `exec(query, params)` | Execute query, return rows (SQL only) |
212
+ | `table<T>(name)` | Get typed table client |
213
+ | `$transaction(fn)` | Execute in transaction |
214
+ | `$connect()` | Open connection / authenticate |
215
+ | `$disconnect()` | Close connection / clear auth |
216
+ | `readRange(range)` | Read raw sheet range (Sheets only) |
217
+ | `writeRange(range, values)` | Write raw sheet range (Sheets only) |
218
+ | `appendRange(range, values)` | Append rows to sheet (Sheets only) |
219
+ | `listSheets()` | List all sheet tab names (Sheets only) |
220
+ | `deleteSheet(name)` | Delete a sheet tab (Sheets only) |
221
+
222
+ ### AdapterTableClient / SheetsTableClient
223
+
224
+ | Method | Description |
225
+ |--------|-------------|
226
+ | `findMany(args)` | Query multiple rows |
227
+ | `findFirst(args)` | Query single row |
228
+ | `findUnique(where)` | Find by unique key |
229
+ | `count(where)` | Count rows |
230
+ | `create(data)` | Insert row |
231
+ | `createMany(data)` | Bulk insert |
232
+ | `update(where, data)` | Update row |
233
+ | `updateMany(where, data)` | Update multiple rows |
234
+ | `delete(where)` | Delete row |
235
+ | `deleteMany(where)` | Delete multiple rows |
236
+ | `upsert(where, create, update)` | Insert or update |
237
+ | `aggregate(args)` | SUM, AVG, MIN, MAX, COUNT |
238
+ | `groupBy(args)` | Group by fields |
239
+ | `vectorSearch(args)` | Semantic similarity search |
240
+ | `clear()` | Clear all data rows, keep headers (Sheets only) |
241
+ | `deleteAll()` | Delete all data rows including headers (Sheets only) |
242
+
243
+ ## Provider Layout
244
+
245
+ - TypeScript providers live under `typescript/{base,mssql,postgres,mysql,sqlite,googlesheets}`.
246
+ - Python providers live under `python/{base,mssql,postgres}`, with `python/an5_adapter.py` kept as the public facade.
247
+ - .NET providers live under `dotnet/{Base,Mssql,Postgres}`, with `dotnet/an5Adapter.cs` kept as the public facade.
248
+ - Adapters do not depend on generated `an5Client` artifacts; generated clients may pass metadata in explicitly when they need model/table mapping.
249
+
250
+ ## Testing
251
+
252
+ ```bash
253
+ # TypeScript/Node
254
+ node test/unit.test.js
255
+
256
+ # Python
257
+ python -m compileall python
258
+ python test/smoke.py
259
+ ```
260
+
261
+ ## License
262
+
263
+ MIT
@@ -0,0 +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
+ }
@@ -0,0 +1,82 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Data.Common;
4
+
5
+ namespace An5Orm
6
+ {
7
+ public enum Dialect
8
+ {
9
+ Mssql,
10
+ Postgres
11
+ }
12
+
13
+ // ─── Config ────────────────────────────────────────────────────────────────
14
+
15
+ public class An5AdapterOptions
16
+ {
17
+ public string ConnectionString { get; set; }
18
+ public int CommandTimeout { get; set; } = 60;
19
+ public int ConnectRetryCount { get; set; } = 3;
20
+ }
21
+
22
+ // ─── Query Engine Interface ────────────────────────────────────────────────
23
+
24
+ internal interface IQueryEngine
25
+ {
26
+ Dialect Dialect { get; }
27
+ List<Dictionary<string, object>> QueryRaw(string sql, Dictionary<string, object> parameters);
28
+ List<T> QueryRaw<T>(string sql, Dictionary<string, object> parameters) where T : new();
29
+ int ExecuteRaw(string sql, Dictionary<string, object> parameters);
30
+ An5TransactionBase BeginTransaction();
31
+ }
32
+
33
+ // ─── Transaction base ──────────────────────────────────────────────────────
34
+
35
+ internal abstract class An5TransactionBase : IDisposable
36
+ {
37
+ public abstract void Commit();
38
+ public abstract void Rollback();
39
+ public abstract void Dispose();
40
+ }
41
+
42
+ // ─── Dialect detection ─────────────────────────────────────────────────────
43
+
44
+ internal static class DialectDetector
45
+ {
46
+ public static Dialect Detect(string connectionString)
47
+ {
48
+ var cs = (connectionString ?? "").Trim().ToLowerInvariant();
49
+ if (cs.StartsWith("postgres://") || cs.StartsWith("postgresql://") || cs.Contains("host="))
50
+ return Dialect.Postgres;
51
+ return Dialect.Mssql;
52
+ }
53
+ }
54
+
55
+ // ─── Quoting ───────────────────────────────────────────────────────────────
56
+
57
+ internal static class SqlQuote
58
+ {
59
+ public static string QuoteName(string name, Dialect dialect)
60
+ {
61
+ if (dialect == Dialect.Postgres)
62
+ {
63
+ if (name.StartsWith("[") && name.EndsWith("]"))
64
+ return "\"" + name.Substring(1, name.Length - 2) + "\"";
65
+ return "\"" + name + "\"";
66
+ }
67
+ return name.StartsWith("[") ? name : "[" + name + "]";
68
+ }
69
+
70
+ public static string QuoteTable(string tableName, Dialect dialect)
71
+ {
72
+ if (tableName.StartsWith("[") || tableName.StartsWith("\""))
73
+ return tableName;
74
+ if (tableName.Contains("."))
75
+ {
76
+ var parts = tableName.Split('.');
77
+ return string.Join(".", Array.ConvertAll(parts, p => QuoteName(p, dialect)));
78
+ }
79
+ return QuoteName(tableName, dialect);
80
+ }
81
+ }
82
+ }