@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
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
//! Runtime database adapter for AN5 (Rust).
|
|
2
|
+
//!
|
|
3
|
+
//! `An5Adapter` owns the connection pool; `TableClient` provides the ORM CRUD
|
|
4
|
+
//! surface. Rows are `serde_json::Value` maps, matching the Go and TypeScript
|
|
5
|
+
//! adapters, so the same query code works across languages.
|
|
6
|
+
|
|
7
|
+
use serde_json::{Map, Value};
|
|
8
|
+
use sqlx::any::AnyArguments;
|
|
9
|
+
use sqlx::{Any, AnyConnection, AnyPool, Row};
|
|
10
|
+
use std::collections::HashMap;
|
|
11
|
+
|
|
12
|
+
use crate::base::{
|
|
13
|
+
build_order_by, build_where, get_fields_for_model, resolve_table, set_adapter_metadata,
|
|
14
|
+
vector_distance, AdapterMetadata, Dialect, RelationDef, Where,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/// A database row as a plain JSON object.
|
|
18
|
+
pub type RowMap = Map<String, Value>;
|
|
19
|
+
|
|
20
|
+
/// Generates an RFC 4122 version 4 UUID from the system entropy source.
|
|
21
|
+
///
|
|
22
|
+
/// Used to fill string primary keys that the schema marks as `@default(uuid())`
|
|
23
|
+
/// so `create` works on engines without a server-side UUID default. Matches the
|
|
24
|
+
/// TypeScript adapter's `generateUUID`.
|
|
25
|
+
fn generate_uuid() -> String {
|
|
26
|
+
use std::io::Read;
|
|
27
|
+
|
|
28
|
+
let mut bytes = [0u8; 16];
|
|
29
|
+
if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
|
|
30
|
+
if f.read_exact(&mut bytes).is_err() {
|
|
31
|
+
// Fall through to the time-seeded fallback below.
|
|
32
|
+
bytes = [0u8; 16];
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
let nanos = std::time::SystemTime::now()
|
|
36
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
37
|
+
.map(|d| d.as_nanos())
|
|
38
|
+
.unwrap_or(0);
|
|
39
|
+
bytes[..16].copy_from_slice(&nanos.to_le_bytes()[..16]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Version 4, variant 1.
|
|
43
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
44
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
45
|
+
|
|
46
|
+
let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
|
|
47
|
+
format!(
|
|
48
|
+
"{}-{}-{}-{}-{}",
|
|
49
|
+
&hex[0..8],
|
|
50
|
+
&hex[8..12],
|
|
51
|
+
&hex[12..16],
|
|
52
|
+
&hex[16..20],
|
|
53
|
+
&hex[20..32]
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Fills in a string primary key with a generated UUID when the caller omitted
|
|
58
|
+
/// it and the schema declares that column as a string.
|
|
59
|
+
fn apply_generated_primary_key(model: &str, data: &mut RowMap) {
|
|
60
|
+
let Some(fields) = get_fields_for_model(model) else {
|
|
61
|
+
return;
|
|
62
|
+
};
|
|
63
|
+
let Some(fields) = fields.as_object() else {
|
|
64
|
+
return;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
for (name, def) in fields {
|
|
68
|
+
let is_id = def
|
|
69
|
+
.get("isId")
|
|
70
|
+
.and_then(|v| v.as_bool())
|
|
71
|
+
.unwrap_or_else(|| name.eq_ignore_ascii_case("id"));
|
|
72
|
+
if !is_id || data.contains_key(name) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let declared = def
|
|
77
|
+
.get("sql")
|
|
78
|
+
.or_else(|| def.get("ts"))
|
|
79
|
+
.or_else(|| def.get("type"))
|
|
80
|
+
.and_then(|v| v.as_str())
|
|
81
|
+
.unwrap_or("")
|
|
82
|
+
.to_ascii_lowercase();
|
|
83
|
+
let is_string = matches!(
|
|
84
|
+
declared.split('(').next().unwrap_or("").trim(),
|
|
85
|
+
"string" | "uuid" | "uniqueidentifier" | "nvarchar" | "varchar" | "text" | "char"
|
|
86
|
+
);
|
|
87
|
+
if is_string {
|
|
88
|
+
data.insert(name.clone(), Value::String(generate_uuid()));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Result alias for adapter operations.
|
|
94
|
+
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
|
95
|
+
|
|
96
|
+
// ─── Args types (ORM-standard structured args) ───────────────────────────────
|
|
97
|
+
|
|
98
|
+
/// Configures `find_many` / `find_first` / `find_unique`.
|
|
99
|
+
#[derive(Debug, Default, Clone)]
|
|
100
|
+
pub struct FindManyArgs {
|
|
101
|
+
/// ORM-style where object, e.g. `{"name": {"contains": "Jo"}}`.
|
|
102
|
+
pub r#where: Option<Value>,
|
|
103
|
+
pub order_by: Option<Value>,
|
|
104
|
+
pub skip: i64,
|
|
105
|
+
pub take: i64,
|
|
106
|
+
/// `Vec<String>` of column names, or an object of column -> bool.
|
|
107
|
+
pub select: Option<Value>,
|
|
108
|
+
/// Relation keys to eager-load.
|
|
109
|
+
pub include: Option<Value>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
impl FindManyArgs {
|
|
113
|
+
pub fn new() -> Self {
|
|
114
|
+
Self::default()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Sets the where clause from any serializable value.
|
|
118
|
+
pub fn where_value(mut self, value: Value) -> Self {
|
|
119
|
+
self.r#where = Some(value);
|
|
120
|
+
self
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Configures `count`.
|
|
125
|
+
#[derive(Debug, Default, Clone)]
|
|
126
|
+
pub struct CountArgs {
|
|
127
|
+
pub r#where: Option<Value>,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Configures `delete_many`.
|
|
131
|
+
#[derive(Debug, Default, Clone)]
|
|
132
|
+
pub struct DeleteManyArgs {
|
|
133
|
+
pub r#where: Option<Value>,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Configures `create`.
|
|
137
|
+
#[derive(Debug, Default, Clone)]
|
|
138
|
+
pub struct CreateArgs {
|
|
139
|
+
pub data: RowMap,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Configures `create_many`.
|
|
143
|
+
#[derive(Debug, Default, Clone)]
|
|
144
|
+
pub struct CreateManyArgs {
|
|
145
|
+
pub data: Vec<RowMap>,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Configures `update`.
|
|
149
|
+
#[derive(Debug, Default, Clone)]
|
|
150
|
+
pub struct UpdateArgs {
|
|
151
|
+
pub r#where: Option<Value>,
|
|
152
|
+
/// Supports `{ "field": { "set"|"increment"|"decrement"|"multiply"|"divide": v } }`.
|
|
153
|
+
pub data: Value,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Configures `update_many`.
|
|
157
|
+
#[derive(Debug, Default, Clone)]
|
|
158
|
+
pub struct UpdateManyArgs {
|
|
159
|
+
pub r#where: Option<Value>,
|
|
160
|
+
pub data: Value,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Configures `upsert`.
|
|
164
|
+
#[derive(Debug, Default, Clone)]
|
|
165
|
+
pub struct UpsertArgs {
|
|
166
|
+
pub r#where: Option<Value>,
|
|
167
|
+
pub create: RowMap,
|
|
168
|
+
pub update: Value,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Configures `aggregate`.
|
|
172
|
+
#[derive(Debug, Default, Clone)]
|
|
173
|
+
pub struct AggregateArgs {
|
|
174
|
+
pub r#where: Option<Value>,
|
|
175
|
+
pub count: Option<Value>,
|
|
176
|
+
pub sum: Option<Value>,
|
|
177
|
+
pub avg: Option<Value>,
|
|
178
|
+
pub min: Option<Value>,
|
|
179
|
+
pub max: Option<Value>,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Configures `group_by`.
|
|
183
|
+
#[derive(Debug, Default, Clone)]
|
|
184
|
+
pub struct GroupByArgs {
|
|
185
|
+
/// Column name or list of column names.
|
|
186
|
+
pub by: Option<Value>,
|
|
187
|
+
pub r#where: Option<Value>,
|
|
188
|
+
pub order_by: Option<Value>,
|
|
189
|
+
pub skip: i64,
|
|
190
|
+
pub take: i64,
|
|
191
|
+
pub sum: Option<Value>,
|
|
192
|
+
pub avg: Option<Value>,
|
|
193
|
+
pub min: Option<Value>,
|
|
194
|
+
pub max: Option<Value>,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Configures `vector_search`.
|
|
198
|
+
#[derive(Debug, Default, Clone)]
|
|
199
|
+
pub struct VectorSearchArgs {
|
|
200
|
+
pub vector: Vec<f64>,
|
|
201
|
+
pub take: i64,
|
|
202
|
+
pub r#where: Option<Value>,
|
|
203
|
+
pub vector_field: String,
|
|
204
|
+
/// `cosine` (default), `euclidean` or `dot`.
|
|
205
|
+
pub distance_metric: String,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── An5Adapter ─────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
/// Manages runtime database execution.
|
|
211
|
+
#[derive(Clone)]
|
|
212
|
+
pub struct An5Adapter {
|
|
213
|
+
pool: AnyPool,
|
|
214
|
+
dialect: Dialect,
|
|
215
|
+
connection_string: String,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
impl An5Adapter {
|
|
219
|
+
/// Build an adapter from a connection string using the given driver.
|
|
220
|
+
///
|
|
221
|
+
/// The driver must be registered beforehand (for example
|
|
222
|
+
/// `sqlx::any::install_postgres()`), which lets one binary speak several
|
|
223
|
+
/// dialects while the SQL itself is generated per dialect.
|
|
224
|
+
pub async fn connect(connection_string: &str) -> Result<Self> {
|
|
225
|
+
let pool = AnyPool::connect(connection_string).await?;
|
|
226
|
+
Ok(Self {
|
|
227
|
+
dialect: Dialect::detect(connection_string),
|
|
228
|
+
pool,
|
|
229
|
+
connection_string: connection_string.to_string(),
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Build an adapter from an existing pool, detecting the dialect from
|
|
234
|
+
/// `connection_string`.
|
|
235
|
+
pub fn from_pool(pool: AnyPool, connection_string: &str) -> Self {
|
|
236
|
+
Self {
|
|
237
|
+
dialect: Dialect::detect(connection_string),
|
|
238
|
+
pool,
|
|
239
|
+
connection_string: connection_string.to_string(),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Verify the connection is alive.
|
|
244
|
+
pub async fn connect_check(&self) -> Result<()> {
|
|
245
|
+
sqlx::query("SELECT 1").execute(&self.pool).await?;
|
|
246
|
+
Ok(())
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Close the pool.
|
|
250
|
+
pub async fn disconnect(&self) -> Result<()> {
|
|
251
|
+
self.pool.close().await;
|
|
252
|
+
Ok(())
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// The detected dialect.
|
|
256
|
+
pub fn dialect(&self) -> Dialect {
|
|
257
|
+
self.dialect
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// The connection string this adapter was built with.
|
|
261
|
+
pub fn connection_string(&self) -> &str {
|
|
262
|
+
&self.connection_string
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Underlying pool, for escape-hatch queries.
|
|
266
|
+
pub fn pool(&self) -> &AnyPool {
|
|
267
|
+
&self.pool
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Register the metadata emitted by the generated client.
|
|
271
|
+
pub fn set_metadata(&self, meta: AdapterMetadata) {
|
|
272
|
+
set_adapter_metadata(meta);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Table-scoped CRUD client.
|
|
276
|
+
pub fn table(&self, name: &str) -> TableClient {
|
|
277
|
+
let resolved = resolve_table(name);
|
|
278
|
+
TableClient {
|
|
279
|
+
adapter: self.clone(),
|
|
280
|
+
table_name: name.to_string(),
|
|
281
|
+
physical_table: resolved,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Point a model alias at a different physical table.
|
|
286
|
+
///
|
|
287
|
+
/// Schema-less engines (SQLite) have no `[dbo]` prefix, so the generated
|
|
288
|
+
/// mapping needs a per-model override.
|
|
289
|
+
pub fn add_table_override(&self, model: &str, table: &str) {
|
|
290
|
+
crate::base::add_table_override(model, table);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// Read-only view client.
|
|
294
|
+
pub fn view(&self, name: &str) -> ViewClient {
|
|
295
|
+
ViewClient {
|
|
296
|
+
table: self.table(name),
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// Execute a SELECT and return rows as JSON maps.
|
|
301
|
+
pub async fn query_raw(&self, sql: &str, args: &[Value]) -> Result<Vec<RowMap>> {
|
|
302
|
+
let mut q = sqlx::query(sql);
|
|
303
|
+
for a in args {
|
|
304
|
+
q = bind_value(q, a);
|
|
305
|
+
}
|
|
306
|
+
let rows = q.fetch_all(&self.pool).await?;
|
|
307
|
+
rows_to_maps(&rows)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Execute a non-SELECT statement and return the affected row count.
|
|
311
|
+
pub async fn execute_raw(&self, sql: &str, args: &[Value]) -> Result<u64> {
|
|
312
|
+
let mut q = sqlx::query(sql);
|
|
313
|
+
for a in args {
|
|
314
|
+
q = bind_value(q, a);
|
|
315
|
+
}
|
|
316
|
+
Ok(q.execute(&self.pool).await?.rows_affected())
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// Run `f` inside a transaction, committing on success and rolling back on
|
|
320
|
+
/// error.
|
|
321
|
+
///
|
|
322
|
+
/// The closure receives a pooled connection that is already inside an
|
|
323
|
+
/// explicit transaction, which keeps the signature free of lifetime
|
|
324
|
+
/// gymnastics around `sqlx::Transaction`.
|
|
325
|
+
pub async fn transaction<T, F>(&self, f: F) -> Result<T>
|
|
326
|
+
where
|
|
327
|
+
for<'c> F: FnOnce(
|
|
328
|
+
&'c mut AnyConnection,
|
|
329
|
+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T>> + Send + 'c>>,
|
|
330
|
+
{
|
|
331
|
+
let mut conn = self.pool.acquire().await?;
|
|
332
|
+
sqlx::query("BEGIN").execute(&mut *conn).await?;
|
|
333
|
+
match f(&mut conn).await {
|
|
334
|
+
Ok(v) => {
|
|
335
|
+
sqlx::query("COMMIT").execute(&mut *conn).await?;
|
|
336
|
+
Ok(v)
|
|
337
|
+
}
|
|
338
|
+
Err(e) => {
|
|
339
|
+
let _ = sqlx::query("ROLLBACK").execute(&mut *conn).await;
|
|
340
|
+
Err(e)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fn bind_value<'q>(
|
|
347
|
+
q: sqlx::query::Query<'q, Any, AnyArguments<'q>>,
|
|
348
|
+
v: &Value,
|
|
349
|
+
) -> sqlx::query::Query<'q, Any, AnyArguments<'q>> {
|
|
350
|
+
match v {
|
|
351
|
+
Value::Null => q.bind(None::<String>),
|
|
352
|
+
Value::Bool(b) => q.bind(*b),
|
|
353
|
+
Value::Number(n) => {
|
|
354
|
+
if let Some(i) = n.as_i64() {
|
|
355
|
+
q.bind(i)
|
|
356
|
+
} else if let Some(f) = n.as_f64() {
|
|
357
|
+
q.bind(f)
|
|
358
|
+
} else {
|
|
359
|
+
q.bind(n.to_string())
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
Value::String(s) => q.bind(s.clone()),
|
|
363
|
+
other => q.bind(other.to_string()),
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/// Decodes one column of an `Any` row into a JSON value.
|
|
368
|
+
///
|
|
369
|
+
/// The `Any` driver hands back a runtime-typed value and keeps its `AnyValueKind`
|
|
370
|
+
/// enum crate-private, so each column is probed against the supported Rust types
|
|
371
|
+
/// in order instead of switching on a discriminant.
|
|
372
|
+
fn column_to_value(row: &sqlx::any::AnyRow, idx: usize) -> Result<Value> {
|
|
373
|
+
macro_rules! probe {
|
|
374
|
+
($t:ty, $wrap:expr) => {
|
|
375
|
+
if let Ok(v) = row.try_get::<$t, _>(idx) {
|
|
376
|
+
return Ok($wrap(v));
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// bool before the numeric types, and integers before floats, so a bool
|
|
382
|
+
// column never decodes as 0/1 and an int column keeps integer precision.
|
|
383
|
+
probe!(bool, Value::from);
|
|
384
|
+
probe!(i64, Value::from);
|
|
385
|
+
probe!(f64, Value::from);
|
|
386
|
+
probe!(String, Value::String);
|
|
387
|
+
probe!(Vec<u8>, |b: Vec<u8>| {
|
|
388
|
+
Value::String(String::from_utf8_lossy(&b).into_owned())
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// An undecodable column (driver-specific type) becomes null rather than
|
|
392
|
+
// failing the whole query.
|
|
393
|
+
Ok(Value::Null)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fn rows_to_maps(rows: &[sqlx::any::AnyRow]) -> Result<Vec<RowMap>> {
|
|
397
|
+
let mut out = Vec::with_capacity(rows.len());
|
|
398
|
+
for row in rows {
|
|
399
|
+
let mut map = RowMap::new();
|
|
400
|
+
for (idx, column) in row.columns.iter().enumerate() {
|
|
401
|
+
map.insert(column.name.to_string(), column_to_value(row, idx)?);
|
|
402
|
+
}
|
|
403
|
+
out.push(map);
|
|
404
|
+
}
|
|
405
|
+
Ok(out)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ─── TableClient ────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
/// Table-scoped CRUD operations.
|
|
411
|
+
#[derive(Clone)]
|
|
412
|
+
pub struct TableClient {
|
|
413
|
+
adapter: An5Adapter,
|
|
414
|
+
/// Model name as written by the caller (`User`, `user`, `users`, ...).
|
|
415
|
+
table_name: String,
|
|
416
|
+
/// Resolved physical table, e.g. `[dbo].[Users]`.
|
|
417
|
+
physical_table: String,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
impl TableClient {
|
|
421
|
+
/// The model name this client was opened with.
|
|
422
|
+
pub fn model_name(&self) -> &str {
|
|
423
|
+
&self.table_name
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// The resolved physical table name.
|
|
427
|
+
pub fn table_name(&self) -> &str {
|
|
428
|
+
&self.physical_table
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fn quoted_table(&self) -> String {
|
|
432
|
+
self.adapter.dialect.quote_table(&self.physical_table)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
fn nolock(&self) -> &'static str {
|
|
436
|
+
self.adapter.dialect.nolock()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
fn ph(&self, n: usize) -> String {
|
|
440
|
+
self.adapter.dialect.placeholder(n)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
fn build_where(&self, where_value: &Option<Value>) -> Where {
|
|
444
|
+
match where_value {
|
|
445
|
+
Some(v) => build_where(v, self.adapter.dialect, &|n| self.ph(n)),
|
|
446
|
+
None => Where::default(),
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// Fetch rows matching the structured args.
|
|
451
|
+
pub async fn find_many(&self, args: &FindManyArgs) -> Result<Vec<RowMap>> {
|
|
452
|
+
let w = self.build_where(&args.r#where);
|
|
453
|
+
let order = args
|
|
454
|
+
.order_by
|
|
455
|
+
.as_ref()
|
|
456
|
+
.map(|o| build_order_by(o, self.adapter.dialect))
|
|
457
|
+
.unwrap_or_default();
|
|
458
|
+
|
|
459
|
+
let (cols, has_rel_select) = selected_fields(args.select.as_ref());
|
|
460
|
+
let cols = if !cols.is_empty() && !has_rel_select {
|
|
461
|
+
cols
|
|
462
|
+
.iter()
|
|
463
|
+
.map(|c| self.adapter.dialect.quote_identifier(c))
|
|
464
|
+
.collect::<Vec<_>>()
|
|
465
|
+
.join(", ")
|
|
466
|
+
} else {
|
|
467
|
+
"*".to_string()
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
let mut sql;
|
|
471
|
+
let d = self.adapter.dialect;
|
|
472
|
+
match (args.take > 0, args.skip > 0) {
|
|
473
|
+
(true, false) if d.supports_limit_offset() => {
|
|
474
|
+
sql = format!("SELECT {} FROM {}", cols, self.quoted_table());
|
|
475
|
+
push_clauses(&mut sql, &w, &order, self.nolock());
|
|
476
|
+
sql.push_str(&format!(" LIMIT {}", args.take));
|
|
477
|
+
}
|
|
478
|
+
(true, false) => {
|
|
479
|
+
sql = format!(
|
|
480
|
+
"SELECT TOP ({}) {} FROM {}{}",
|
|
481
|
+
args.take,
|
|
482
|
+
cols,
|
|
483
|
+
self.quoted_table(),
|
|
484
|
+
self.nolock()
|
|
485
|
+
);
|
|
486
|
+
push_clauses(&mut sql, &w, &order, "");
|
|
487
|
+
}
|
|
488
|
+
(_, true) if d.supports_limit_offset() => {
|
|
489
|
+
sql = format!("SELECT {} FROM {}", cols, self.quoted_table());
|
|
490
|
+
push_clauses(&mut sql, &w, &order, "");
|
|
491
|
+
if args.take > 0 {
|
|
492
|
+
sql.push_str(&format!(" LIMIT {}", args.take));
|
|
493
|
+
} else {
|
|
494
|
+
sql.push_str(" LIMIT ALL");
|
|
495
|
+
}
|
|
496
|
+
sql.push_str(&format!(" OFFSET {}", args.skip));
|
|
497
|
+
}
|
|
498
|
+
(_, true) => {
|
|
499
|
+
sql = format!("SELECT {} FROM {}{}", cols, self.quoted_table(), self.nolock());
|
|
500
|
+
push_clauses(&mut sql, &w, &order, "");
|
|
501
|
+
if order.is_empty() {
|
|
502
|
+
sql.push_str(" ORDER BY (SELECT NULL)");
|
|
503
|
+
}
|
|
504
|
+
sql.push_str(&format!(" OFFSET {} ROWS", args.skip));
|
|
505
|
+
if args.take > 0 {
|
|
506
|
+
sql.push_str(&format!(" FETCH NEXT {} ROWS ONLY", args.take));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
_ => {
|
|
510
|
+
sql = format!("SELECT {} FROM {}{}", cols, self.quoted_table(), self.nolock());
|
|
511
|
+
push_clauses(&mut sql, &w, &order, "");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let mut rows = self.adapter.query_raw(&sql, &w.args).await?;
|
|
516
|
+
|
|
517
|
+
if let Some(sel) = &args.select {
|
|
518
|
+
if has_rel_select {
|
|
519
|
+
self.resolve_select_relations(&mut rows, sel).await?;
|
|
520
|
+
}
|
|
521
|
+
for row in rows.iter_mut() {
|
|
522
|
+
project_fields(row, sel);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if let Some(inc) = &args.include {
|
|
526
|
+
self.resolve_includes(&mut rows, inc).await?;
|
|
527
|
+
}
|
|
528
|
+
Ok(rows)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// First row matching the args, or `None`.
|
|
532
|
+
pub async fn find_first(&self, args: &FindManyArgs) -> Result<Option<RowMap>> {
|
|
533
|
+
let mut copied = args.clone();
|
|
534
|
+
copied.take = 1;
|
|
535
|
+
Ok(self.find_many(&copied).await?.into_iter().next())
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/// Single row matching unique criteria.
|
|
539
|
+
pub async fn find_unique(&self, args: &FindManyArgs) -> Result<Option<RowMap>> {
|
|
540
|
+
self.find_first(args).await
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/// Total number of matching rows.
|
|
544
|
+
pub async fn count(&self, args: &CountArgs) -> Result<i64> {
|
|
545
|
+
let w = self.build_where(&args.r#where);
|
|
546
|
+
let mut sql = format!(
|
|
547
|
+
"SELECT COUNT(*) AS cnt FROM {}{}",
|
|
548
|
+
self.quoted_table(),
|
|
549
|
+
self.nolock()
|
|
550
|
+
);
|
|
551
|
+
if !w.is_empty() {
|
|
552
|
+
sql.push_str(&format!(" WHERE {}", w.clause));
|
|
553
|
+
}
|
|
554
|
+
let rows = self.adapter.query_raw(&sql, &w.args).await?;
|
|
555
|
+
Ok(rows
|
|
556
|
+
.first()
|
|
557
|
+
.and_then(|r| r.get("cnt"))
|
|
558
|
+
.and_then(|v| v.as_i64().or_else(|| v.as_f64().map(|f| f as i64)))
|
|
559
|
+
.unwrap_or(0))
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// Insert one row and return it.
|
|
563
|
+
///
|
|
564
|
+
/// Uses the dialect's returning clause (`OUTPUT INSERTED.*` on SQL Server,
|
|
565
|
+
/// `RETURNING *` on PostgreSQL/SQLite) so database-generated columns such
|
|
566
|
+
/// as an auto-increment id come back populated. Falls back to the submitted
|
|
567
|
+
/// values when the engine does not support it.
|
|
568
|
+
pub async fn create(&self, data: &RowMap) -> Result<RowMap> {
|
|
569
|
+
if data.is_empty() {
|
|
570
|
+
return Err("create requires at least one field".into());
|
|
571
|
+
}
|
|
572
|
+
let mut data = data.clone();
|
|
573
|
+
apply_generated_primary_key(&self.table_name, &mut data);
|
|
574
|
+
let data = &data;
|
|
575
|
+
let (scalars, _relations) = split_relation_writes(&self.table_name, data);
|
|
576
|
+
|
|
577
|
+
let mut cols: Vec<String> = Vec::new();
|
|
578
|
+
let mut placeholders: Vec<String> = Vec::new();
|
|
579
|
+
let mut args: Vec<Value> = Vec::new();
|
|
580
|
+
for (k, v) in &scalars {
|
|
581
|
+
cols.push(self.adapter.dialect.quote_identifier(k));
|
|
582
|
+
placeholders.push(self.ph(args.len() + 1));
|
|
583
|
+
args.push(v.clone());
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let returning = match self.adapter.dialect {
|
|
587
|
+
Dialect::Mssql => " OUTPUT INSERTED.*",
|
|
588
|
+
Dialect::Postgres | Dialect::Sqlite => " RETURNING *",
|
|
589
|
+
};
|
|
590
|
+
let sql = format!(
|
|
591
|
+
"INSERT INTO {} ({}) VALUES ({}){}",
|
|
592
|
+
self.quoted_table(),
|
|
593
|
+
cols.join(", "),
|
|
594
|
+
placeholders.join(", "),
|
|
595
|
+
returning
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
match self.adapter.query_raw(&sql, &args).await {
|
|
599
|
+
Ok(rows) if !rows.is_empty() => Ok(rows.into_iter().next().unwrap()),
|
|
600
|
+
// The engine rejected the returning clause; insert without it.
|
|
601
|
+
_ => {
|
|
602
|
+
let sql = format!(
|
|
603
|
+
"INSERT INTO {} ({}) VALUES ({})",
|
|
604
|
+
self.quoted_table(),
|
|
605
|
+
cols.join(", "),
|
|
606
|
+
placeholders.join(", ")
|
|
607
|
+
);
|
|
608
|
+
self.adapter.execute_raw(&sql, &args).await?;
|
|
609
|
+
Ok(scalars)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/// Insert many rows, returning the number inserted.
|
|
615
|
+
pub async fn create_many(&self, data: &[RowMap]) -> Result<u64> {
|
|
616
|
+
let mut count = 0;
|
|
617
|
+
for row in data {
|
|
618
|
+
self.create(row).await?;
|
|
619
|
+
count += 1;
|
|
620
|
+
}
|
|
621
|
+
Ok(count)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/// Update rows matching `where` with `data`.
|
|
625
|
+
pub async fn update(&self, args: &UpdateArgs) -> Result<u64> {
|
|
626
|
+
let w = self.build_where(&args.r#where);
|
|
627
|
+
let (sets, bind) = build_set_clause(&args.data, &|n| self.ph(n))?;
|
|
628
|
+
if sets.is_empty() {
|
|
629
|
+
return Ok(0);
|
|
630
|
+
}
|
|
631
|
+
let mut sql = format!("UPDATE {} SET {}", self.quoted_table(), sets);
|
|
632
|
+
if !w.is_empty() {
|
|
633
|
+
sql.push_str(&format!(" WHERE {}", w.clause));
|
|
634
|
+
}
|
|
635
|
+
let mut all = bind;
|
|
636
|
+
all.extend(w.args);
|
|
637
|
+
self.adapter.execute_raw(&sql, &all).await
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/// Alias of `update` kept for API parity with the other adapters.
|
|
641
|
+
pub async fn update_many(&self, args: &UpdateManyArgs) -> Result<u64> {
|
|
642
|
+
self.update(&UpdateArgs {
|
|
643
|
+
r#where: args.r#where.clone(),
|
|
644
|
+
data: args.data.clone(),
|
|
645
|
+
})
|
|
646
|
+
.await
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/// Delete rows matching `where`.
|
|
650
|
+
pub async fn delete_many(&self, args: &DeleteManyArgs) -> Result<u64> {
|
|
651
|
+
let w = self.build_where(&args.r#where);
|
|
652
|
+
let mut sql = format!("DELETE FROM {}", self.quoted_table());
|
|
653
|
+
if !w.is_empty() {
|
|
654
|
+
sql.push_str(&format!(" WHERE {}", w.clause));
|
|
655
|
+
}
|
|
656
|
+
self.adapter.execute_raw(&sql, &w.args).await
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/// Update when a match exists, otherwise insert.
|
|
660
|
+
pub async fn upsert(&self, args: &UpsertArgs) -> Result<RowMap> {
|
|
661
|
+
let existing = self.find_first(&FindManyArgs {
|
|
662
|
+
r#where: args.r#where.clone(),
|
|
663
|
+
take: 1,
|
|
664
|
+
..Default::default()
|
|
665
|
+
})
|
|
666
|
+
.await?;
|
|
667
|
+
if existing.is_some() {
|
|
668
|
+
self.update(&UpdateArgs {
|
|
669
|
+
r#where: args.r#where.clone(),
|
|
670
|
+
data: args.update.clone(),
|
|
671
|
+
})
|
|
672
|
+
.await?;
|
|
673
|
+
Ok(existing.unwrap())
|
|
674
|
+
} else {
|
|
675
|
+
self.create(&args.create).await
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/// Aggregations over matching rows.
|
|
680
|
+
pub async fn aggregate(&self, args: &AggregateArgs) -> Result<RowMap> {
|
|
681
|
+
let w = self.build_where(&args.r#where);
|
|
682
|
+
let mut select = String::from("COUNT(*) AS _count");
|
|
683
|
+
if let Some(Value::Array(cols)) = &args.sum {
|
|
684
|
+
for c in cols {
|
|
685
|
+
if let Some(name) = c.as_str() {
|
|
686
|
+
select.push_str(&format!(
|
|
687
|
+
", SUM({}) AS _sum_{}",
|
|
688
|
+
self.adapter.dialect.quote_identifier(name),
|
|
689
|
+
name
|
|
690
|
+
));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if let Some(Value::Array(cols)) = &args.avg {
|
|
695
|
+
for c in cols {
|
|
696
|
+
if let Some(name) = c.as_str() {
|
|
697
|
+
select.push_str(&format!(
|
|
698
|
+
", AVG({}) AS _avg_{}",
|
|
699
|
+
self.adapter.dialect.quote_identifier(name),
|
|
700
|
+
name
|
|
701
|
+
));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if let Some(Value::Array(cols)) = &args.min {
|
|
706
|
+
for c in cols {
|
|
707
|
+
if let Some(name) = c.as_str() {
|
|
708
|
+
select.push_str(&format!(
|
|
709
|
+
", MIN({}) AS _min_{}",
|
|
710
|
+
self.adapter.dialect.quote_identifier(name),
|
|
711
|
+
name
|
|
712
|
+
));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if let Some(Value::Array(cols)) = &args.max {
|
|
717
|
+
for c in cols {
|
|
718
|
+
if let Some(name) = c.as_str() {
|
|
719
|
+
select.push_str(&format!(
|
|
720
|
+
", MAX({}) AS _max_{}",
|
|
721
|
+
self.adapter.dialect.quote_identifier(name),
|
|
722
|
+
name
|
|
723
|
+
));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let mut sql = format!(
|
|
729
|
+
"SELECT {} FROM {}{}",
|
|
730
|
+
select,
|
|
731
|
+
self.quoted_table(),
|
|
732
|
+
self.nolock()
|
|
733
|
+
);
|
|
734
|
+
if !w.is_empty() {
|
|
735
|
+
sql.push_str(&format!(" WHERE {}", w.clause));
|
|
736
|
+
}
|
|
737
|
+
let rows = self.adapter.query_raw(&sql, &w.args).await?;
|
|
738
|
+
Ok(rows.into_iter().next().unwrap_or_default())
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/// Group rows by one or more columns.
|
|
742
|
+
pub async fn group_by(&self, args: &GroupByArgs) -> Result<Vec<RowMap>> {
|
|
743
|
+
let cols: Vec<String> = match &args.by {
|
|
744
|
+
Some(Value::Array(items)) => items
|
|
745
|
+
.iter()
|
|
746
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
747
|
+
.collect(),
|
|
748
|
+
Some(Value::String(s)) => vec![s.clone()],
|
|
749
|
+
_ => Vec::new(),
|
|
750
|
+
};
|
|
751
|
+
if cols.is_empty() {
|
|
752
|
+
return Err("group_by requires at least one column".into());
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
let mut select: Vec<String> = cols
|
|
756
|
+
.iter()
|
|
757
|
+
.map(|c| self.adapter.dialect.quote_identifier(c))
|
|
758
|
+
.collect();
|
|
759
|
+
for (agg, values) in [
|
|
760
|
+
("SUM", &args.sum),
|
|
761
|
+
("AVG", &args.avg),
|
|
762
|
+
("MIN", &args.min),
|
|
763
|
+
("MAX", &args.max),
|
|
764
|
+
] {
|
|
765
|
+
if let Some(Value::Array(items)) = values {
|
|
766
|
+
for item in items {
|
|
767
|
+
if let Some(name) = item.as_str() {
|
|
768
|
+
select.push(format!(
|
|
769
|
+
"{}({}) AS {}_{}",
|
|
770
|
+
agg,
|
|
771
|
+
self.adapter.dialect.quote_identifier(name),
|
|
772
|
+
agg.to_lowercase(),
|
|
773
|
+
name
|
|
774
|
+
));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
let w = self.build_where(&args.r#where);
|
|
781
|
+
let mut sql = format!(
|
|
782
|
+
"SELECT {} FROM {}{}",
|
|
783
|
+
select.join(", "),
|
|
784
|
+
self.quoted_table(),
|
|
785
|
+
self.nolock()
|
|
786
|
+
);
|
|
787
|
+
if !w.is_empty() {
|
|
788
|
+
sql.push_str(&format!(" WHERE {}", w.clause));
|
|
789
|
+
}
|
|
790
|
+
sql.push_str(&format!(
|
|
791
|
+
" GROUP BY {}",
|
|
792
|
+
cols
|
|
793
|
+
.iter()
|
|
794
|
+
.map(|c| self.adapter.dialect.quote_identifier(c))
|
|
795
|
+
.collect::<Vec<_>>()
|
|
796
|
+
.join(", ")
|
|
797
|
+
));
|
|
798
|
+
if let Some(ob) = &args.order_by {
|
|
799
|
+
let order = build_order_by(ob, self.adapter.dialect);
|
|
800
|
+
if !order.is_empty() {
|
|
801
|
+
sql.push(' ');
|
|
802
|
+
sql.push_str(&order);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if self.adapter.dialect.supports_limit_offset() {
|
|
806
|
+
if args.take > 0 {
|
|
807
|
+
sql.push_str(&format!(" LIMIT {}", args.take));
|
|
808
|
+
}
|
|
809
|
+
if args.skip > 0 {
|
|
810
|
+
sql.push_str(&format!(" OFFSET {}", args.skip));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
self.adapter.query_raw(&sql, &w.args).await
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/// Vector similarity search.
|
|
818
|
+
///
|
|
819
|
+
/// Tries the database's native distance function first and falls back to
|
|
820
|
+
/// in-memory scoring when the engine does not provide one, mirroring the
|
|
821
|
+
/// behaviour of the Go and TypeScript adapters.
|
|
822
|
+
pub async fn vector_search(&self, args: &VectorSearchArgs) -> Result<Vec<RowMap>> {
|
|
823
|
+
let take = if args.take > 0 { args.take } else { 10 };
|
|
824
|
+
let field = if args.vector_field.is_empty() {
|
|
825
|
+
"embedding"
|
|
826
|
+
} else {
|
|
827
|
+
&args.vector_field
|
|
828
|
+
};
|
|
829
|
+
let metric = if args.distance_metric.is_empty() {
|
|
830
|
+
"cosine"
|
|
831
|
+
} else {
|
|
832
|
+
&args.distance_metric
|
|
833
|
+
};
|
|
834
|
+
let quoted_field = self.adapter.dialect.quote_identifier(field);
|
|
835
|
+
let w = self.build_where(&args.r#where);
|
|
836
|
+
|
|
837
|
+
// Native path: MSSQL VECTOR_DISTANCE / Postgres pgvector operators.
|
|
838
|
+
let native_sql = match self.adapter.dialect {
|
|
839
|
+
Dialect::Postgres => {
|
|
840
|
+
let op = match metric.to_lowercase().as_str() {
|
|
841
|
+
"euclidean" => "<->",
|
|
842
|
+
"dot" => "<#>",
|
|
843
|
+
_ => "<=>",
|
|
844
|
+
};
|
|
845
|
+
Some(format!(
|
|
846
|
+
"SELECT *, ({quoted_field} {op} $1::vector) AS distance FROM {}{} WHERE {quoted_field} IS NOT NULL",
|
|
847
|
+
self.quoted_table(),
|
|
848
|
+
suffix_where(self.nolock(), &w)
|
|
849
|
+
))
|
|
850
|
+
}
|
|
851
|
+
Dialect::Mssql => {
|
|
852
|
+
let dim = args.vector.len();
|
|
853
|
+
Some(format!(
|
|
854
|
+
"SELECT TOP ({take}) *, VECTOR_DISTANCE('{metric}', CAST({quoted_field} AS VECTOR({dim}, float32)), CAST(? AS VECTOR({dim}, float32))) AS distance FROM {}{} WHERE {quoted_field} IS NOT NULL",
|
|
855
|
+
self.quoted_table(),
|
|
856
|
+
self.nolock()
|
|
857
|
+
))
|
|
858
|
+
}
|
|
859
|
+
Dialect::Sqlite => None,
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
if let Some(sql) = native_sql {
|
|
863
|
+
let mut bind: Vec<Value> = Vec::new();
|
|
864
|
+
if !w.is_empty() {
|
|
865
|
+
bind.extend(w.args.clone());
|
|
866
|
+
}
|
|
867
|
+
match self.adapter.query_raw(&sql, &bind).await {
|
|
868
|
+
Ok(mut rows) if !rows.is_empty() => {
|
|
869
|
+
let mut ordered = score_rows(&mut rows, &args.vector, metric);
|
|
870
|
+
ordered.truncate(take as usize);
|
|
871
|
+
return Ok(ordered);
|
|
872
|
+
}
|
|
873
|
+
Ok(_) => {}
|
|
874
|
+
Err(e) => {
|
|
875
|
+
// Fall through to the in-memory path on driver errors.
|
|
876
|
+
let _ = e;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Fallback: in-memory similarity over the filtered rows.
|
|
882
|
+
let rows = self
|
|
883
|
+
.find_many(&FindManyArgs {
|
|
884
|
+
r#where: args.r#where.clone(),
|
|
885
|
+
..Default::default()
|
|
886
|
+
})
|
|
887
|
+
.await?;
|
|
888
|
+
|
|
889
|
+
let mut scored: Vec<(RowMap, f64)> = Vec::new();
|
|
890
|
+
for mut row in rows {
|
|
891
|
+
let raw = row.get(field).cloned();
|
|
892
|
+
let row_vec = match raw {
|
|
893
|
+
Some(Value::String(s)) => parse_vector(&s),
|
|
894
|
+
Some(Value::Array(items)) => Some(
|
|
895
|
+
items
|
|
896
|
+
.iter()
|
|
897
|
+
.filter_map(|v| v.as_f64())
|
|
898
|
+
.collect::<Vec<f64>>(),
|
|
899
|
+
),
|
|
900
|
+
_ => None,
|
|
901
|
+
};
|
|
902
|
+
if let Some(rv) = row_vec {
|
|
903
|
+
if rv.len() == args.vector.len() {
|
|
904
|
+
let d = vector_distance(&args.vector, &rv, metric);
|
|
905
|
+
row.insert("distance".to_string(), Value::from(d));
|
|
906
|
+
scored.push((row, d));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
911
|
+
scored.truncate(take as usize);
|
|
912
|
+
Ok(scored.into_iter().map(|(r, _)| r).collect())
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async fn resolve_includes(&self, rows: &mut [RowMap], include: &Value) -> Result<()> {
|
|
916
|
+
let requested = match include {
|
|
917
|
+
Value::Array(items) => items.clone(),
|
|
918
|
+
Value::Object(m) => m
|
|
919
|
+
.iter()
|
|
920
|
+
.filter(|(_, v)| matches!(v, Value::Bool(true)))
|
|
921
|
+
.map(|(k, _)| Value::String(k.clone()))
|
|
922
|
+
.collect(),
|
|
923
|
+
_ => Vec::new(),
|
|
924
|
+
};
|
|
925
|
+
for key in requested {
|
|
926
|
+
let key = match key.as_str() {
|
|
927
|
+
Some(k) => k.to_string(),
|
|
928
|
+
None => continue,
|
|
929
|
+
};
|
|
930
|
+
self.resolve_select_relations(rows, &Value::Array(vec![Value::String(key)]))
|
|
931
|
+
.await?;
|
|
932
|
+
}
|
|
933
|
+
Ok(())
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/// Eager-loads the requested relations onto each row.
|
|
937
|
+
///
|
|
938
|
+
/// Boxed because eager loading re-enters `find_many` on the related table,
|
|
939
|
+
/// which would otherwise make this future infinitely sized.
|
|
940
|
+
pub fn resolve_select_relations<'a>(
|
|
941
|
+
&'a self,
|
|
942
|
+
rows: &'a mut [RowMap],
|
|
943
|
+
select: &'a Value,
|
|
944
|
+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
|
|
945
|
+
Box::pin(async move {
|
|
946
|
+
let requested: Vec<String> = match select {
|
|
947
|
+
Value::Array(items) => items
|
|
948
|
+
.iter()
|
|
949
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
950
|
+
.collect(),
|
|
951
|
+
Value::Object(m) => m
|
|
952
|
+
.iter()
|
|
953
|
+
.filter(|(_, v)| matches!(v, Value::Bool(true)))
|
|
954
|
+
.map(|(k, _)| k.clone())
|
|
955
|
+
.collect(),
|
|
956
|
+
_ => Vec::new(),
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
let relations: HashMap<String, RelationDef> =
|
|
960
|
+
crate::base::get_relations_for_model(&self.table_name);
|
|
961
|
+
|
|
962
|
+
for key in requested {
|
|
963
|
+
if key == "_count" {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
let def = match relations.get(&key) {
|
|
967
|
+
Some(d) => d.clone(),
|
|
968
|
+
None => continue,
|
|
969
|
+
};
|
|
970
|
+
let target = self.adapter.table(&def.model_name);
|
|
971
|
+
let fk = if def.foreign_key.is_empty() {
|
|
972
|
+
"id".to_string()
|
|
973
|
+
} else {
|
|
974
|
+
def.foreign_key.clone()
|
|
975
|
+
};
|
|
976
|
+
let lk = if def.local_key.is_empty() {
|
|
977
|
+
"id".to_string()
|
|
978
|
+
} else {
|
|
979
|
+
def.local_key.clone()
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
for row in rows.iter_mut() {
|
|
983
|
+
let parent_value = match row.get(&lk) {
|
|
984
|
+
Some(v) if !v.is_null() => v.clone(),
|
|
985
|
+
_ => continue,
|
|
986
|
+
};
|
|
987
|
+
let child_rows = target
|
|
988
|
+
.find_many(&FindManyArgs {
|
|
989
|
+
r#where: Some(serde_json::json!({ fk.clone(): parent_value })),
|
|
990
|
+
..Default::default()
|
|
991
|
+
})
|
|
992
|
+
.await?;
|
|
993
|
+
if def.relation_type == "many" {
|
|
994
|
+
row.insert(
|
|
995
|
+
key.clone(),
|
|
996
|
+
Value::Array(child_rows.into_iter().map(Value::Object).collect()),
|
|
997
|
+
);
|
|
998
|
+
} else {
|
|
999
|
+
row.insert(
|
|
1000
|
+
key.clone(),
|
|
1001
|
+
child_rows
|
|
1002
|
+
.into_iter()
|
|
1003
|
+
.next()
|
|
1004
|
+
.map(Value::Object)
|
|
1005
|
+
.unwrap_or(Value::Null),
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
Ok(())
|
|
1011
|
+
})
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ─── ViewClient ─────────────────────────────────────────────────────────────
|
|
1016
|
+
|
|
1017
|
+
/// Read-only operations over a database view.
|
|
1018
|
+
#[derive(Clone)]
|
|
1019
|
+
pub struct ViewClient {
|
|
1020
|
+
table: TableClient,
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
impl ViewClient {
|
|
1024
|
+
pub async fn find_many(&self, args: &FindManyArgs) -> Result<Vec<RowMap>> {
|
|
1025
|
+
self.table.find_many(args).await
|
|
1026
|
+
}
|
|
1027
|
+
pub async fn find_first(&self, args: &FindManyArgs) -> Result<Option<RowMap>> {
|
|
1028
|
+
self.table.find_first(args).await
|
|
1029
|
+
}
|
|
1030
|
+
pub async fn find_unique(&self, args: &FindManyArgs) -> Result<Option<RowMap>> {
|
|
1031
|
+
self.table.find_unique(args).await
|
|
1032
|
+
}
|
|
1033
|
+
pub async fn count(&self, args: &CountArgs) -> Result<i64> {
|
|
1034
|
+
self.table.count(args).await
|
|
1035
|
+
}
|
|
1036
|
+
pub async fn aggregate(&self, args: &AggregateArgs) -> Result<RowMap> {
|
|
1037
|
+
self.table.aggregate(args).await
|
|
1038
|
+
}
|
|
1039
|
+
pub async fn group_by(&self, args: &GroupByArgs) -> Result<Vec<RowMap>> {
|
|
1040
|
+
self.table.group_by(args).await
|
|
1041
|
+
}
|
|
1042
|
+
pub async fn vector_search(&self, args: &VectorSearchArgs) -> Result<Vec<RowMap>> {
|
|
1043
|
+
self.table.vector_search(args).await
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
1048
|
+
|
|
1049
|
+
fn push_clauses(sql: &mut String, w: &Where, order: &str, extra: &str) {
|
|
1050
|
+
if !w.is_empty() {
|
|
1051
|
+
sql.push_str(&format!(" WHERE {}", w.clause));
|
|
1052
|
+
}
|
|
1053
|
+
if !order.is_empty() {
|
|
1054
|
+
sql.push(' ');
|
|
1055
|
+
sql.push_str(order);
|
|
1056
|
+
}
|
|
1057
|
+
if !extra.is_empty() {
|
|
1058
|
+
sql.push(' ');
|
|
1059
|
+
sql.push_str(extra);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
fn suffix_where(nolock: &str, w: &Where) -> String {
|
|
1064
|
+
let mut s = String::new();
|
|
1065
|
+
if !nolock.is_empty() {
|
|
1066
|
+
s.push(' ');
|
|
1067
|
+
s.push_str(nolock);
|
|
1068
|
+
}
|
|
1069
|
+
if !w.is_empty() {
|
|
1070
|
+
s.push_str(&format!(" AND {}", w.clause));
|
|
1071
|
+
}
|
|
1072
|
+
s
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/// Extracts scalar column names from a select value, reporting whether any
|
|
1076
|
+
/// relation key was requested.
|
|
1077
|
+
fn selected_fields(select: Option<&Value>) -> (Vec<String>, bool) {
|
|
1078
|
+
match select {
|
|
1079
|
+
Some(Value::Array(items)) => (
|
|
1080
|
+
items
|
|
1081
|
+
.iter()
|
|
1082
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
1083
|
+
.collect(),
|
|
1084
|
+
false,
|
|
1085
|
+
),
|
|
1086
|
+
Some(Value::Object(m)) => {
|
|
1087
|
+
let mut cols = Vec::new();
|
|
1088
|
+
let mut has_rel = false;
|
|
1089
|
+
for (k, v) in m {
|
|
1090
|
+
if matches!(v, Value::Bool(true)) {
|
|
1091
|
+
if k == "_count" || k.contains('_') {
|
|
1092
|
+
has_rel = true;
|
|
1093
|
+
}
|
|
1094
|
+
cols.push(k.clone());
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
(cols, has_rel)
|
|
1098
|
+
}
|
|
1099
|
+
_ => (Vec::new(), false),
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/// Keeps only the selected keys on a row, mirroring `projectFields` in Go/TS.
|
|
1104
|
+
fn project_fields(row: &mut RowMap, select: &Value) {
|
|
1105
|
+
let keys: Vec<String> = match select {
|
|
1106
|
+
Value::Array(items) => items
|
|
1107
|
+
.iter()
|
|
1108
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
1109
|
+
.collect(),
|
|
1110
|
+
Value::Object(m) => m
|
|
1111
|
+
.iter()
|
|
1112
|
+
.filter(|(_, v)| matches!(v, Value::Bool(true)))
|
|
1113
|
+
.map(|(k, _)| k.clone())
|
|
1114
|
+
.collect(),
|
|
1115
|
+
_ => return,
|
|
1116
|
+
};
|
|
1117
|
+
let count = row.get("_count").cloned();
|
|
1118
|
+
let projected: RowMap = keys
|
|
1119
|
+
.iter()
|
|
1120
|
+
.filter_map(|k| row.get(k).map(|v| (k.clone(), v.clone())))
|
|
1121
|
+
.collect();
|
|
1122
|
+
*row = projected;
|
|
1123
|
+
if let Some(c) = count {
|
|
1124
|
+
row.insert("_count".to_string(), c);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/// Separates relation writes from scalar columns using the model metadata.
|
|
1129
|
+
fn split_relation_writes(model: &str, data: &RowMap) -> (RowMap, RowMap) {
|
|
1130
|
+
let relations = crate::base::get_relations_for_model(model);
|
|
1131
|
+
let mut scalars = RowMap::new();
|
|
1132
|
+
let mut rels = RowMap::new();
|
|
1133
|
+
for (k, v) in data {
|
|
1134
|
+
if relations.contains_key(k) {
|
|
1135
|
+
rels.insert(k.clone(), v.clone());
|
|
1136
|
+
} else {
|
|
1137
|
+
scalars.insert(k.clone(), v.clone());
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
(scalars, rels)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/// Builds `col = ph` pairs, expanding field operations such as `increment`.
|
|
1144
|
+
fn build_set_clause(data: &Value, ph: &dyn Fn(usize) -> String) -> Result<(String, Vec<Value>)> {
|
|
1145
|
+
let obj = match data {
|
|
1146
|
+
Value::Object(m) => m,
|
|
1147
|
+
Value::Null => return Ok((String::new(), Vec::new())),
|
|
1148
|
+
_ => return Err("update data must be an object".into()),
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
let mut sets: Vec<String> = Vec::new();
|
|
1152
|
+
let mut args: Vec<Value> = Vec::new();
|
|
1153
|
+
|
|
1154
|
+
for (col, val) in obj {
|
|
1155
|
+
let quoted = format!("[{}]", col);
|
|
1156
|
+
match val {
|
|
1157
|
+
Value::Object(op) if op.contains_key("set") => {
|
|
1158
|
+
args.push(op["set"].clone());
|
|
1159
|
+
sets.push(format!("{} = {}", quoted, ph(args.len())));
|
|
1160
|
+
}
|
|
1161
|
+
Value::Object(op) if op.contains_key("increment") => {
|
|
1162
|
+
args.push(op["increment"].clone());
|
|
1163
|
+
sets.push(format!("{} = {} + {}", quoted, quoted, ph(args.len())));
|
|
1164
|
+
}
|
|
1165
|
+
Value::Object(op) if op.contains_key("decrement") => {
|
|
1166
|
+
args.push(op["decrement"].clone());
|
|
1167
|
+
sets.push(format!("{} = {} - {}", quoted, quoted, ph(args.len())));
|
|
1168
|
+
}
|
|
1169
|
+
Value::Object(op) if op.contains_key("multiply") => {
|
|
1170
|
+
args.push(op["multiply"].clone());
|
|
1171
|
+
sets.push(format!("{} = {} * {}", quoted, quoted, ph(args.len())));
|
|
1172
|
+
}
|
|
1173
|
+
Value::Object(op) if op.contains_key("divide") => {
|
|
1174
|
+
args.push(op["divide"].clone());
|
|
1175
|
+
sets.push(format!("{} = {} / {}", quoted, quoted, ph(args.len())));
|
|
1176
|
+
}
|
|
1177
|
+
other => {
|
|
1178
|
+
args.push(other.clone());
|
|
1179
|
+
sets.push(format!("{} = {}", quoted, ph(args.len())));
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
Ok((sets.join(", "), args))
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/// Parses a JSON array string into a vector.
|
|
1188
|
+
fn parse_vector(raw: &str) -> Option<Vec<f64>> {
|
|
1189
|
+
let trimmed = raw.trim().trim_start_matches('[').trim_end_matches(']');
|
|
1190
|
+
if trimmed.trim().is_empty() {
|
|
1191
|
+
return Some(Vec::new());
|
|
1192
|
+
}
|
|
1193
|
+
let mut out = Vec::new();
|
|
1194
|
+
for part in trimmed.split(',') {
|
|
1195
|
+
match part.trim().parse::<f64>() {
|
|
1196
|
+
Ok(v) => out.push(v),
|
|
1197
|
+
Err(_) => return None,
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
Some(out)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/// Sorts rows by their `distance` field, falling back to in-memory similarity.
|
|
1204
|
+
fn score_rows(rows: &mut [RowMap], query: &[f64], metric: &str) -> Vec<RowMap> {
|
|
1205
|
+
for row in rows.iter_mut() {
|
|
1206
|
+
if let Some(d) = row.get("distance").and_then(|v| v.as_f64()) {
|
|
1207
|
+
row.insert("distance".to_string(), Value::from(d));
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if let Some(Value::String(s)) = row.get("embedding").cloned() {
|
|
1211
|
+
if let Some(v) = parse_vector(&s) {
|
|
1212
|
+
if v.len() == query.len() {
|
|
1213
|
+
let d = vector_distance(query, &v, metric);
|
|
1214
|
+
row.insert("distance".to_string(), Value::from(d));
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
rows.sort_by(|a, b| {
|
|
1220
|
+
let av = a.get("distance").and_then(|v| v.as_f64()).unwrap_or(f64::MAX);
|
|
1221
|
+
let bv = b.get("distance").and_then(|v| v.as_f64()).unwrap_or(f64::MAX);
|
|
1222
|
+
av.partial_cmp(&bv).unwrap_or(std::cmp::Ordering::Equal)
|
|
1223
|
+
});
|
|
1224
|
+
rows.to_vec()
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
#[cfg(test)]
|
|
1228
|
+
mod tests {
|
|
1229
|
+
use super::*;
|
|
1230
|
+
use serde_json::json;
|
|
1231
|
+
|
|
1232
|
+
async fn client() -> TableClient {
|
|
1233
|
+
// No queries are issued here: these tests only exercise SQL construction
|
|
1234
|
+
// helpers, but a pool still needs a Tokio runtime to be created.
|
|
1235
|
+
let pool = AnyPool::connect_lazy("sqlite::memory:").expect("lazy pool");
|
|
1236
|
+
An5Adapter::from_pool(pool, "sqlite::memory:").table("User")
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
#[tokio::test]
|
|
1240
|
+
async fn resolves_physical_table() {
|
|
1241
|
+
crate::base::set_adapter_metadata(AdapterMetadata {
|
|
1242
|
+
model_to_table: HashMap::from([("User".to_string(), "[dbo].[Users]".to_string())]),
|
|
1243
|
+
..Default::default()
|
|
1244
|
+
});
|
|
1245
|
+
let c = client().await;
|
|
1246
|
+
assert_eq!(c.table_name(), "[dbo].[Users]");
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
#[test]
|
|
1250
|
+
fn build_set_clause_expands_operations() {
|
|
1251
|
+
// serde_json orders object keys, so the clause is deterministic
|
|
1252
|
+
// regardless of how the caller built the update payload.
|
|
1253
|
+
let (sets, args) = build_set_clause(
|
|
1254
|
+
&json!({ "total": { "increment": 5 }, "name": { "set": "x" } }),
|
|
1255
|
+
&|n| format!("?{}", n),
|
|
1256
|
+
)
|
|
1257
|
+
.expect("set clause");
|
|
1258
|
+
assert_eq!(sets, "[name] = ?1, [total] = [total] + ?2");
|
|
1259
|
+
assert_eq!(args, vec![json!("x"), json!(5)]);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
#[test]
|
|
1263
|
+
fn selected_fields_detects_relations() {
|
|
1264
|
+
let (cols, has_rel) = selected_fields(Some(&json!({ "id": true, "user_email": true })));
|
|
1265
|
+
assert_eq!(cols, vec!["id".to_string(), "user_email".to_string()]);
|
|
1266
|
+
assert!(has_rel);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
#[test]
|
|
1270
|
+
fn project_fields_keeps_selection() {
|
|
1271
|
+
let mut row = json!({ "id": 1, "name": "a", "email": "b" })
|
|
1272
|
+
.as_object()
|
|
1273
|
+
.unwrap()
|
|
1274
|
+
.clone();
|
|
1275
|
+
project_fields(&mut row, &json!({ "id": true }));
|
|
1276
|
+
assert_eq!(row.len(), 1);
|
|
1277
|
+
assert!(row.contains_key("id"));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
#[test]
|
|
1281
|
+
fn parses_vector_strings() {
|
|
1282
|
+
assert_eq!(parse_vector("[1.0, 2.5]"), Some(vec![1.0, 2.5]));
|
|
1283
|
+
assert_eq!(parse_vector("[]"), Some(vec![]));
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
#[test]
|
|
1287
|
+
fn split_relations_uses_metadata() {
|
|
1288
|
+
crate::base::set_adapter_metadata(AdapterMetadata {
|
|
1289
|
+
relation_map: HashMap::from([(
|
|
1290
|
+
"Order".to_string(),
|
|
1291
|
+
HashMap::from([(
|
|
1292
|
+
"user".to_string(),
|
|
1293
|
+
RelationDef {
|
|
1294
|
+
model_name: "User".to_string(),
|
|
1295
|
+
relation_type: "one".to_string(),
|
|
1296
|
+
foreign_key: "user_id".to_string(),
|
|
1297
|
+
local_key: "id".to_string(),
|
|
1298
|
+
},
|
|
1299
|
+
)]),
|
|
1300
|
+
)]),
|
|
1301
|
+
..Default::default()
|
|
1302
|
+
});
|
|
1303
|
+
let (scalars, rels) = split_relation_writes(
|
|
1304
|
+
"Order",
|
|
1305
|
+
json!({ "total": 10, "user": { "connect": 1 } })
|
|
1306
|
+
.as_object()
|
|
1307
|
+
.unwrap(),
|
|
1308
|
+
);
|
|
1309
|
+
assert!(scalars.contains_key("total"));
|
|
1310
|
+
assert!(rels.contains_key("user"));
|
|
1311
|
+
}
|
|
1312
|
+
}
|