@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.
- package/README.md +263 -0
- package/dotnet/An5Adapter.cs +275 -0
- package/dotnet/Base/Core.cs +82 -0
- package/dotnet/Mssql/MssqlEngine.cs +144 -0
- package/dotnet/Postgres/PostgresEngine.cs +121 -0
- package/package.json +46 -0
- package/python/an5_adapter.py +97 -0
- package/python/base/__init__.py +3 -0
- package/python/base/dialects.py +8 -0
- package/python/base/metadata.py +9 -0
- package/python/base/sql.py +121 -0
- package/python/mssql/__init__.py +1 -0
- package/python/mssql/provider.py +62 -0
- package/python/postgres/__init__.py +1 -0
- package/python/postgres/provider.py +15 -0
- package/python/table_client.py +209 -0
- package/typescript/an5Adapter.d.ts +117 -0
- package/typescript/an5Adapter.js +447 -0
- package/typescript/an5Adapter.ts +474 -0
- package/typescript/base/index.d.ts +3 -0
- package/typescript/base/index.js +19 -0
- package/typescript/base/index.ts +3 -0
- package/typescript/base/metadata.d.ts +14 -0
- package/typescript/base/metadata.js +17 -0
- package/typescript/base/metadata.ts +28 -0
- package/typescript/base/sql.d.ts +4 -0
- package/typescript/base/sql.js +143 -0
- package/typescript/base/sql.ts +98 -0
- package/typescript/base/types.d.ts +21 -0
- package/typescript/base/types.js +2 -0
- package/typescript/base/types.ts +24 -0
- package/typescript/config.d.ts +17 -0
- package/typescript/config.js +92 -0
- package/typescript/config.ts +95 -0
- package/typescript/googlesheets/adapter.d.ts +31 -0
- package/typescript/googlesheets/adapter.js +138 -0
- package/typescript/googlesheets/adapter.ts +167 -0
- package/typescript/googlesheets/an5SheetsAdapter.d.ts +3 -0
- package/typescript/googlesheets/an5SheetsAdapter.js +8 -0
- package/typescript/googlesheets/an5SheetsAdapter.ts +3 -0
- package/typescript/googlesheets/config.d.ts +17 -0
- package/typescript/googlesheets/config.js +29 -0
- package/typescript/googlesheets/config.ts +33 -0
- package/typescript/googlesheets/helpers.d.ts +9 -0
- package/typescript/googlesheets/helpers.js +155 -0
- package/typescript/googlesheets/helpers.ts +124 -0
- package/typescript/googlesheets/index.d.ts +2 -0
- package/typescript/googlesheets/index.js +9 -0
- package/typescript/googlesheets/index.ts +2 -0
- package/typescript/googlesheets/parseConnectionString.d.ts +11 -0
- package/typescript/googlesheets/parseConnectionString.js +44 -0
- package/typescript/googlesheets/parseConnectionString.ts +44 -0
- package/typescript/googlesheets/retry.d.ts +3 -0
- package/typescript/googlesheets/retry.js +26 -0
- package/typescript/googlesheets/retry.ts +23 -0
- package/typescript/googlesheets/sqlExecutor.d.ts +12 -0
- package/typescript/googlesheets/sqlExecutor.js +306 -0
- package/typescript/googlesheets/sqlExecutor.ts +301 -0
- package/typescript/googlesheets/tableClient.d.ts +78 -0
- package/typescript/googlesheets/tableClient.js +394 -0
- package/typescript/googlesheets/tableClient.ts +434 -0
- package/typescript/googlesheets/types.d.ts +4 -0
- package/typescript/googlesheets/types.js +3 -0
- package/typescript/googlesheets/types.ts +6 -0
- package/typescript/index.d.ts +2 -0
- package/typescript/index.js +14 -0
- package/typescript/index.ts +15 -0
- package/typescript/mssql/engine.d.ts +16 -0
- package/typescript/mssql/engine.js +138 -0
- package/typescript/mssql/engine.ts +132 -0
- package/typescript/mssql/index.d.ts +1 -0
- package/typescript/mssql/index.js +17 -0
- package/typescript/mssql/index.ts +1 -0
- package/typescript/mysql/engine.d.ts +13 -0
- package/typescript/mysql/engine.js +77 -0
- package/typescript/mysql/engine.ts +80 -0
- package/typescript/mysql/index.d.ts +1 -0
- package/typescript/mysql/index.js +17 -0
- package/typescript/mysql/index.ts +1 -0
- package/typescript/postgres/engine.d.ts +13 -0
- package/typescript/postgres/engine.js +86 -0
- package/typescript/postgres/engine.ts +87 -0
- package/typescript/postgres/index.d.ts +1 -0
- package/typescript/postgres/index.js +17 -0
- package/typescript/postgres/index.ts +1 -0
- package/typescript/sqlite/engine.d.ts +13 -0
- package/typescript/sqlite/engine.js +58 -0
- package/typescript/sqlite/engine.ts +55 -0
- package/typescript/sqlite/index.d.ts +1 -0
- package/typescript/sqlite/index.js +17 -0
- 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
|
+
}
|