@an5/adapters 0.2.6 → 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 +96 -2
- package/dist/an5Adapter.d.ts +57 -0
- package/dist/an5Adapter.js +262 -4
- package/dist/base/types.d.ts +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -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/dotnet/An5Adapter.cs +41 -18
- package/dotnet/Base/Core.cs +42 -2
- package/dotnet/Postgres/PostgresEngine.cs +58 -30
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/package.json +15 -8
- package/python/an5_adapter.py +74 -9
- package/python/base/__init__.py +1 -1
- package/python/base/metadata.py +25 -1
- package/python/base/sql.py +44 -14
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +53 -36
- 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 +299 -4
- package/typescript/src/base/types.ts +6 -0
- package/typescript/src/index.ts +17 -0
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
package/python/table_client.py
CHANGED
|
@@ -4,9 +4,9 @@ import json
|
|
|
4
4
|
import uuid
|
|
5
5
|
from typing import Any, Dict, Generic, List, Optional, TypeVar
|
|
6
6
|
try:
|
|
7
|
-
from .base import DIALECT_MSSQL, DIALECT_POSTGRES,
|
|
7
|
+
from .base import DIALECT_MSSQL, DIALECT_POSTGRES, DIALECT_SQLITE, get_fields_for_model, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table, get_relations_for_model
|
|
8
8
|
except ImportError:
|
|
9
|
-
from base import DIALECT_MSSQL, DIALECT_POSTGRES,
|
|
9
|
+
from base import DIALECT_MSSQL, DIALECT_POSTGRES, DIALECT_SQLITE, get_fields_for_model, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table, get_relations_for_model
|
|
10
10
|
|
|
11
11
|
# ─── Select / Include helpers ────────────────────────────────────────────────────────
|
|
12
12
|
|
|
@@ -179,7 +179,11 @@ def _to_non_negative_int(value: Any, fallback: int = 0) -> int:
|
|
|
179
179
|
|
|
180
180
|
def _append_update_set(set_parts: List[str], values: List[Any], col: str, val: Any, dialect: str) -> None:
|
|
181
181
|
quoted = _quote(col, dialect)
|
|
182
|
-
|
|
182
|
+
# Only PostgreSQL uses `%s`; MSSQL and SQLite both use `?`. The condition is
|
|
183
|
+
# written as "is it Postgres" rather than "is it MSSQL" so a new dialect cannot
|
|
184
|
+
# fall into the `%s` branch by default — which once made SQLite generate SQL it
|
|
185
|
+
# could not parse.
|
|
186
|
+
placeholder = "%s" if dialect == DIALECT_POSTGRES else "?"
|
|
183
187
|
if isinstance(val, dict):
|
|
184
188
|
if "increment" in val:
|
|
185
189
|
set_parts.append(f"{quoted} = {quoted} + {placeholder}")
|
|
@@ -232,22 +236,31 @@ class AdapterTableClient(Generic[T]):
|
|
|
232
236
|
|
|
233
237
|
@property
|
|
234
238
|
def _nolock(self) -> str:
|
|
235
|
-
|
|
239
|
+
# `WITH (NOLOCK)` is an MSSQL table hint. PostgreSQL has none and SQLite
|
|
240
|
+
# has no equivalent concept at all — leaving it in makes every generated
|
|
241
|
+
# SELECT invalid SQL, failing on the first one.
|
|
242
|
+
return "" if self._dialect in (DIALECT_POSTGRES, DIALECT_SQLITE) else " WITH (NOLOCK)"
|
|
236
243
|
|
|
237
244
|
@property
|
|
238
245
|
def _fields(self) -> List[Dict]:
|
|
239
|
-
|
|
246
|
+
# Uses the metadata resolver instead of a plain lookup: the generated
|
|
247
|
+
# client names tables in PascalCase while the metadata keys them in
|
|
248
|
+
# camelCase, so a plain lookup yields an empty list and silently skips
|
|
249
|
+
# `isId` (never generating the primary key).
|
|
250
|
+
fields = get_fields_for_model(self._model)
|
|
240
251
|
if isinstance(fields, dict):
|
|
241
252
|
raise TypeError(
|
|
242
253
|
"AN5 Python metadata is out of date. Regenerate with @an5/orm >= 1.0.4 "
|
|
243
254
|
"so MODEL_FIELDS uses a list of field objects."
|
|
244
255
|
)
|
|
245
|
-
return fields
|
|
256
|
+
return fields or []
|
|
246
257
|
|
|
247
258
|
def _pagination(self, take: Optional[int], skip: int, order_sql: str) -> str:
|
|
248
259
|
if take is None:
|
|
249
260
|
return ""
|
|
250
|
-
if self._dialect
|
|
261
|
+
if self._dialect in (DIALECT_POSTGRES, DIALECT_SQLITE):
|
|
262
|
+
# SQLite has `LIMIT ... OFFSET` like PostgreSQL. Taking the
|
|
263
|
+
# `OFFSET ... FETCH NEXT` branch produces SQL SQLite does not understand.
|
|
251
264
|
return f" LIMIT {take} OFFSET {skip}"
|
|
252
265
|
order_prefix = "" if order_sql else " ORDER BY (SELECT NULL)"
|
|
253
266
|
return f"{order_prefix} OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY"
|
|
@@ -304,7 +317,7 @@ class AdapterTableClient(Generic[T]):
|
|
|
304
317
|
|
|
305
318
|
cols = [k for k, v in data.items() if v is not None]
|
|
306
319
|
values = [data[c] for c in cols]
|
|
307
|
-
placeholders = ", ".join(["
|
|
320
|
+
placeholders = ", ".join(["%s" if self._dialect == DIALECT_POSTGRES else "?"] * len(cols))
|
|
308
321
|
col_list = ", ".join(_quote(c, self._dialect) for c in cols)
|
|
309
322
|
query = f"INSERT INTO {self._table_sql} ({col_list}) VALUES ({placeholders})"
|
|
310
323
|
self._adapter.execute(query, values)
|
|
@@ -490,7 +503,7 @@ class AdapterTableClient(Generic[T]):
|
|
|
490
503
|
query += f" ORDER BY {by_cols}"
|
|
491
504
|
if has_take or has_skip:
|
|
492
505
|
final_skip = _to_non_negative_int(skip)
|
|
493
|
-
if self._dialect
|
|
506
|
+
if self._dialect in (DIALECT_POSTGRES, DIALECT_SQLITE):
|
|
494
507
|
if has_take:
|
|
495
508
|
query += f" LIMIT {_to_non_negative_int(take, 1)}"
|
|
496
509
|
query += f" OFFSET {final_skip}"
|
|
@@ -508,34 +521,38 @@ class AdapterTableClient(Generic[T]):
|
|
|
508
521
|
where_sql = _parse_where(self._model, where, params, self._dialect)
|
|
509
522
|
|
|
510
523
|
# 1. Primary path: Native database SQL vector query execution (MSSQL VECTOR_DISTANCE / Postgres pgvector)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
query
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
query += f" WHERE {field_sql} IS NOT NULL AND ({where_sql})"
|
|
524
|
+
# SQLite has no vector operators at all, so skip it entirely: building
|
|
525
|
+
# the SQL and catching the error would cost work and still end up on the
|
|
526
|
+
# in-memory path in step 2.
|
|
527
|
+
native_rows = None
|
|
528
|
+
if self._dialect != DIALECT_SQLITE:
|
|
529
|
+
try:
|
|
530
|
+
field_sql = _quote(vector_field, self._dialect)
|
|
531
|
+
if self._dialect == DIALECT_POSTGRES:
|
|
532
|
+
op = "<=>" if distance_metric == "cosine" else ("<->" if distance_metric == "euclidean" else "<#>")
|
|
533
|
+
query = f"SELECT *, ({field_sql} {op} %s::vector) AS distance FROM {self._table_sql}"
|
|
534
|
+
query_params = [vec_json] + list(params.values())
|
|
535
|
+
if where_sql:
|
|
536
|
+
query += f" WHERE {field_sql} IS NOT NULL AND ({where_sql})"
|
|
537
|
+
else:
|
|
538
|
+
query += f" WHERE {field_sql} IS NOT NULL"
|
|
539
|
+
query += f" ORDER BY distance ASC LIMIT {take}"
|
|
528
540
|
else:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
541
|
+
placeholder = "?"
|
|
542
|
+
query = f"SELECT TOP ({take}) *, VECTOR_DISTANCE('{distance_metric}', CAST({field_sql} AS VECTOR({dim}, float32)), CAST({placeholder} AS VECTOR({dim}, float32))) AS distance FROM {self._table_sql}{self._nolock}"
|
|
543
|
+
query_params = [vec_json] + list(params.values())
|
|
544
|
+
if where_sql:
|
|
545
|
+
query += f" WHERE {field_sql} IS NOT NULL AND ({where_sql})"
|
|
546
|
+
else:
|
|
547
|
+
query += f" WHERE {field_sql} IS NOT NULL"
|
|
548
|
+
query += " ORDER BY distance ASC"
|
|
549
|
+
|
|
550
|
+
native_rows = self._adapter.exec(query, query_params)
|
|
551
|
+
|
|
552
|
+
if native_rows is not None:
|
|
553
|
+
return native_rows
|
|
554
|
+
except Exception:
|
|
555
|
+
pass
|
|
539
556
|
|
|
540
557
|
# 2. Secondary fallback: In-memory similarity computation if DB engine lacks native vector extension
|
|
541
558
|
rows = self.find_many(where=where)
|
package/rust/Cargo.toml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "an5-adapters"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Standalone database adapter runtime for AN5 ORM (Rust) — connection handling, dialect-aware CRUD, aggregates and vector search."
|
|
6
|
+
license = "MIT"
|
|
7
|
+
repository = "https://github.com/an5ORM/an5Adapters"
|
|
8
|
+
homepage = "https://github.com/an5ORM/an5Adapters/tree/main/rust"
|
|
9
|
+
documentation = "https://docs.rs/an5-adapters"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
keywords = ["orm", "sql", "database", "postgres", "sqlx"]
|
|
12
|
+
categories = ["database", "database-implementations"]
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
serde = { version = "1", features = ["derive"] }
|
|
16
|
+
serde_json = "1"
|
|
17
|
+
sqlx = { version = "0.8", default-features = false, features = [
|
|
18
|
+
"any",
|
|
19
|
+
"runtime-tokio",
|
|
20
|
+
"json",
|
|
21
|
+
] }
|
|
22
|
+
tokio = { version = "1", features = ["rt"] }
|
|
23
|
+
|
|
24
|
+
[dev-dependencies]
|
|
25
|
+
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
|
26
|
+
sqlx = { version = "0.8", default-features = false, features = [
|
|
27
|
+
"any",
|
|
28
|
+
"runtime-tokio",
|
|
29
|
+
"json",
|
|
30
|
+
"sqlite",
|
|
31
|
+
] }
|
package/rust/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# an5-adapters
|
|
2
|
+
|
|
3
|
+
Database adapter runtime for [AN5 ORM](https://github.com/an5ORM/an5) in Rust.
|
|
4
|
+
|
|
5
|
+
`An5Adapter` owns the connection and the dialect; `TableClient` provides the ORM
|
|
6
|
+
CRUD surface on top of [`sqlx`](https://github.com/launchbadge/sqlx). Dialects are
|
|
7
|
+
detected from the connection string, so SQL Server, PostgreSQL and SQLite are
|
|
8
|
+
handled without configuration. This crate mirrors the TypeScript, Python, .NET
|
|
9
|
+
and Go adapter packages.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```toml
|
|
14
|
+
[dependencies]
|
|
15
|
+
an5-adapters = "0.1"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The crate is driver-agnostic: it depends on `sqlx` with `any`, so the application
|
|
19
|
+
picks the driver it needs and enables the matching `sqlx` feature.
|
|
20
|
+
|
|
21
|
+
```toml
|
|
22
|
+
[dependencies]
|
|
23
|
+
sqlx = { version = "0.8", default-features = false, features = ["any", "runtime-tokio", "postgres"] }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```rust
|
|
29
|
+
use an5_adapters::{An5Adapter, FindManyArgs};
|
|
30
|
+
use serde_json::json;
|
|
31
|
+
|
|
32
|
+
#[tokio::main]
|
|
33
|
+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
34
|
+
// Register the drivers this binary needs, then hand the URL to the adapter.
|
|
35
|
+
sqlx::any::install_drivers(&[sqlx::postgres::any::DRIVER])?;
|
|
36
|
+
let db = An5Adapter::connect("postgres://localhost:5432/mydb").await?;
|
|
37
|
+
|
|
38
|
+
// Dynamic access, the Rust equivalent of `db.user` in TypeScript.
|
|
39
|
+
let users = db
|
|
40
|
+
.table("User")
|
|
41
|
+
.find_many(&FindManyArgs {
|
|
42
|
+
r#where: Some(json!({ "email": { "contains": "@example.com" } })),
|
|
43
|
+
take: 10,
|
|
44
|
+
..Default::default()
|
|
45
|
+
})
|
|
46
|
+
.await?;
|
|
47
|
+
|
|
48
|
+
println!("{} users", users.len());
|
|
49
|
+
Ok(())
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Generated Rust clients (`an5-client`) wrap this crate and expose one typed handle
|
|
54
|
+
per model — `db.user().find_many(&UserFindManyArgs { .. })` — instead of the
|
|
55
|
+
dynamic `db.table("User")` access shown above.
|
|
56
|
+
|
|
57
|
+
### Features
|
|
58
|
+
|
|
59
|
+
- `An5Adapter::connect` / `create_an5_adapter`, with the connection string read
|
|
60
|
+
from `DATABASE_URL` when omitted
|
|
61
|
+
- Dialect detection and identifier quoting for MSSQL, PostgreSQL and SQLite
|
|
62
|
+
- `TableClient`: `find_many`, `find_first`, `find_unique`, `count`, `create`,
|
|
63
|
+
`create_many`, `update`, `update_many`, `delete_many`, `upsert`, `aggregate`,
|
|
64
|
+
`group_by` and `vector_search`
|
|
65
|
+
- Transactions, raw SQL execution and view clients
|
|
66
|
+
- `base` module with the where/order-by builders, adapter metadata and vector
|
|
67
|
+
distance math
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|