@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.
@@ -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
+ }