1use std::collections::HashMap;
4
5use serde_json::{Map as JsonMap, Value as JsonValue};
6
7use crate::expr::RelationFilterOp;
8use crate::{
9 BinaryOp, Error, Expr, FindManyArgs, IncludeRelation, OrderBy, OrderDir, Result, VectorNearest,
10};
11
12pub fn find_many_args_to_protocol_object(
14 args: &FindManyArgs,
15) -> Result<JsonMap<String, JsonValue>> {
16 let mut result = JsonMap::with_capacity(find_many_args_field_count(args));
17
18 if let Some(where_) = &args.where_ {
19 result.insert("where".to_string(), expr_to_filter_json(where_)?);
20 }
21
22 if !args.order_by.is_empty() {
23 result.insert(
24 "orderBy".to_string(),
25 JsonValue::Array(order_by_list_to_json(&args.order_by)?),
26 );
27 }
28
29 if let Some(take) = args.take {
30 result.insert("take".to_string(), JsonValue::from(take));
31 }
32
33 if let Some(skip) = args.skip {
34 result.insert("skip".to_string(), JsonValue::from(skip));
35 }
36
37 if !args.include.is_empty() {
38 result.insert(
39 "include".to_string(),
40 JsonValue::Object(include_map_to_json_object(&args.include)?),
41 );
42 }
43
44 if !args.select.is_empty() {
45 result.insert(
46 "select".to_string(),
47 JsonValue::Object(select_map_to_json_object(&args.select)),
48 );
49 }
50
51 if let Some(cursor) = &args.cursor {
52 result.insert(
53 "cursor".to_string(),
54 JsonValue::Object(cursor_map_to_json_object(cursor)),
55 );
56 }
57
58 if !args.distinct.is_empty() {
59 result.insert(
60 "distinct".to_string(),
61 JsonValue::Array(distinct_fields_to_json(&args.distinct)),
62 );
63 }
64
65 if let Some(nearest) = &args.nearest {
66 result.insert("nearest".to_string(), nearest_to_json(nearest));
67 }
68
69 Ok(result)
70}
71
72pub fn find_many_args_to_protocol_json(args: &FindManyArgs) -> Result<JsonValue> {
79 Ok(JsonValue::Object(find_many_args_to_protocol_object(args)?))
80}
81
82pub fn where_expr_to_protocol_json(expr: &Expr) -> Result<JsonValue> {
84 expr_to_filter_json(expr)
85}
86
87fn find_many_args_field_count(args: &FindManyArgs) -> usize {
88 let mut count = 0;
89 if args.where_.is_some() {
90 count += 1;
91 }
92 if !args.order_by.is_empty() {
93 count += 1;
94 }
95 if args.take.is_some() {
96 count += 1;
97 }
98 if args.skip.is_some() {
99 count += 1;
100 }
101 if !args.include.is_empty() {
102 count += 1;
103 }
104 if !args.select.is_empty() {
105 count += 1;
106 }
107 if args.cursor.is_some() {
108 count += 1;
109 }
110 if !args.distinct.is_empty() {
111 count += 1;
112 }
113 if args.nearest.is_some() {
114 count += 1;
115 }
116 count
117}
118
119fn include_relation_field_count(include: &IncludeRelation) -> usize {
120 let mut count = 0;
121 if include.where_.is_some() {
122 count += 1;
123 }
124 if !include.order_by.is_empty() {
125 count += 1;
126 }
127 if include.take.is_some() {
128 count += 1;
129 }
130 if include.skip.is_some() {
131 count += 1;
132 }
133 if include.cursor.is_some() {
134 count += 1;
135 }
136 if !include.distinct.is_empty() {
137 count += 1;
138 }
139 if !include.include.is_empty() {
140 count += 1;
141 }
142 count
143}
144
145fn order_by_list_to_json(order_by: &[OrderBy]) -> Result<Vec<JsonValue>> {
146 let mut result = Vec::with_capacity(order_by.len());
147 for order in order_by {
148 result.push(order_by_to_json(order)?);
149 }
150 Ok(result)
151}
152
153fn select_map_to_json_object(select: &HashMap<String, bool>) -> JsonMap<String, JsonValue> {
154 let mut result = JsonMap::with_capacity(select.len());
155 for (field, enabled) in select {
156 result.insert(field.clone(), JsonValue::Bool(*enabled));
157 }
158 result
159}
160
161fn cursor_map_to_json_object(cursor: &HashMap<String, crate::Value>) -> JsonMap<String, JsonValue> {
162 let mut result = JsonMap::with_capacity(cursor.len());
163 for (field, value) in cursor {
164 result.insert(strip_column_qualifier(field), value.to_json_plain());
165 }
166 result
167}
168
169fn distinct_fields_to_json(distinct: &[String]) -> Vec<JsonValue> {
170 let mut result = Vec::with_capacity(distinct.len());
171 for field in distinct {
172 result.push(JsonValue::String(strip_column_qualifier(field)));
173 }
174 result
175}
176
177fn include_map_to_json_object(
178 include: &HashMap<String, IncludeRelation>,
179) -> Result<JsonMap<String, JsonValue>> {
180 let mut result = JsonMap::with_capacity(include.len());
181 for (field, relation) in include {
182 result.insert(
183 field.clone(),
184 JsonValue::Object(include_relation_to_json_object(relation)?),
185 );
186 }
187 Ok(result)
188}
189
190fn include_relation_to_json_object(
191 include: &IncludeRelation,
192) -> Result<JsonMap<String, JsonValue>> {
193 let mut result = JsonMap::with_capacity(include_relation_field_count(include));
194
195 if let Some(where_) = &include.where_ {
196 result.insert("where".to_string(), expr_to_filter_json(where_)?);
197 }
198
199 if !include.order_by.is_empty() {
200 result.insert(
201 "orderBy".to_string(),
202 JsonValue::Array(order_by_list_to_json(&include.order_by)?),
203 );
204 }
205
206 if let Some(take) = include.take {
207 result.insert("take".to_string(), JsonValue::from(take));
208 }
209
210 if let Some(skip) = include.skip {
211 result.insert("skip".to_string(), JsonValue::from(skip));
212 }
213
214 if let Some(cursor) = &include.cursor {
215 result.insert(
216 "cursor".to_string(),
217 JsonValue::Object(cursor_map_to_json_object(cursor)),
218 );
219 }
220
221 if !include.distinct.is_empty() {
222 result.insert(
223 "distinct".to_string(),
224 JsonValue::Array(distinct_fields_to_json(&include.distinct)),
225 );
226 }
227
228 if !include.include.is_empty() {
229 result.insert(
230 "include".to_string(),
231 JsonValue::Object(include_map_to_json_object(&include.include)?),
232 );
233 }
234
235 Ok(result)
236}
237
238fn order_by_to_json(order: &OrderBy) -> Result<JsonValue> {
239 let mut result = JsonMap::new();
240 result.insert(
241 strip_column_qualifier(&order.column),
242 JsonValue::String(match order.direction {
243 OrderDir::Asc => "asc".to_string(),
244 OrderDir::Desc => "desc".to_string(),
245 }),
246 );
247 Ok(JsonValue::Object(result))
248}
249
250fn nearest_to_json(nearest: &VectorNearest) -> JsonValue {
251 let mut result = JsonMap::with_capacity(3);
252 result.insert(
253 "field".to_string(),
254 JsonValue::String(strip_column_qualifier(&nearest.field)),
255 );
256 result.insert(
257 "query".to_string(),
258 JsonValue::Array(
259 nearest
260 .query
261 .iter()
262 .map(|value| JsonValue::from(*value as f64))
263 .collect(),
264 ),
265 );
266 result.insert(
267 "metric".to_string(),
268 JsonValue::String(nearest.metric.as_str().to_string()),
269 );
270 JsonValue::Object(result)
271}
272
273fn expr_to_filter_json(expr: &Expr) -> Result<JsonValue> {
274 match expr {
275 Expr::Binary {
276 left,
277 op: BinaryOp::And,
278 right,
279 } => logical_expr_to_json("AND", left, right),
280 Expr::Binary {
281 left,
282 op: BinaryOp::Or,
283 right,
284 } => logical_expr_to_json("OR", left, right),
285 Expr::Not(inner) => {
286 let mut result = JsonMap::new();
287 result.insert("NOT".to_string(), expr_to_filter_json(inner)?);
288 Ok(JsonValue::Object(result))
289 }
290 Expr::Relation { op, relation } => {
291 relation_predicate_to_json(&relation.field, *op, relation.filter.as_ref())
292 }
293 Expr::Binary { left, op, right } => field_predicate_to_json(left, op, right),
294 Expr::IsNull(inner) => null_predicate_to_json(inner, true),
295 Expr::IsNotNull(inner) => null_predicate_to_json(inner, false),
296 other => Err(Error::InvalidQuery(format!(
297 "query cannot be serialized to engine JSON: unsupported expression {:?}",
298 other
299 ))),
300 }
301}
302
303fn relation_predicate_to_json(
304 field: &str,
305 op: RelationFilterOp,
306 filter: &Expr,
307) -> Result<JsonValue> {
308 let mut relation_spec = JsonMap::with_capacity(1);
309 relation_spec.insert(
310 match op {
311 RelationFilterOp::Some => "some".to_string(),
312 RelationFilterOp::None => "none".to_string(),
313 RelationFilterOp::Every => "every".to_string(),
314 },
315 expr_to_filter_json(filter)?,
316 );
317
318 let mut result = JsonMap::with_capacity(1);
319 result.insert(field.to_string(), JsonValue::Object(relation_spec));
320 Ok(JsonValue::Object(result))
321}
322
323fn logical_expr_to_json(name: &str, left: &Expr, right: &Expr) -> Result<JsonValue> {
324 let mut items =
325 Vec::with_capacity(logical_operand_count(name, left) + logical_operand_count(name, right));
326 collect_logical_operands(name, left, &mut items)?;
327 collect_logical_operands(name, right, &mut items)?;
328
329 let mut result = JsonMap::with_capacity(1);
330 result.insert(name.to_string(), JsonValue::Array(items));
331 Ok(JsonValue::Object(result))
332}
333
334fn logical_operand_count(name: &str, expr: &Expr) -> usize {
335 match (name, expr) {
336 (
337 "AND",
338 Expr::Binary {
339 left,
340 op: BinaryOp::And,
341 right,
342 },
343 ) => logical_operand_count(name, left) + logical_operand_count(name, right),
344 (
345 "OR",
346 Expr::Binary {
347 left,
348 op: BinaryOp::Or,
349 right,
350 },
351 ) => logical_operand_count(name, left) + logical_operand_count(name, right),
352 _ => 1,
353 }
354}
355
356fn collect_logical_operands(name: &str, expr: &Expr, out: &mut Vec<JsonValue>) -> Result<()> {
357 match (name, expr) {
358 (
359 "AND",
360 Expr::Binary {
361 left,
362 op: BinaryOp::And,
363 right,
364 },
365 ) => {
366 collect_logical_operands(name, left, out)?;
367 collect_logical_operands(name, right, out)?;
368 Ok(())
369 }
370 (
371 "OR",
372 Expr::Binary {
373 left,
374 op: BinaryOp::Or,
375 right,
376 },
377 ) => {
378 collect_logical_operands(name, left, out)?;
379 collect_logical_operands(name, right, out)?;
380 Ok(())
381 }
382 _ => {
383 out.push(expr_to_filter_json(expr)?);
384 Ok(())
385 }
386 }
387}
388
389fn field_predicate_to_json(left: &Expr, op: &BinaryOp, right: &Expr) -> Result<JsonValue> {
390 let field = match left {
391 Expr::Column(name) => strip_column_qualifier(name),
392 other => {
393 return Err(Error::InvalidQuery(format!(
394 "query cannot be serialized to engine JSON: unsupported field operand {:?}",
395 other
396 )));
397 }
398 };
399
400 let (operator, value) = match op {
401 BinaryOp::Eq => (None, expr_value_to_json(right)?),
402 BinaryOp::Ne => (Some("ne"), expr_value_to_json(right)?),
403 BinaryOp::Lt => (Some("lt"), expr_value_to_json(right)?),
404 BinaryOp::Le => (Some("lte"), expr_value_to_json(right)?),
405 BinaryOp::Gt => (Some("gt"), expr_value_to_json(right)?),
406 BinaryOp::Ge => (Some("gte"), expr_value_to_json(right)?),
407 BinaryOp::Like => like_operator_and_value(right)?,
408 BinaryOp::In => (Some("in"), list_expr_to_json_array(right)?),
409 BinaryOp::NotIn => (Some("notIn"), list_expr_to_json_array(right)?),
410 other => {
411 return Err(Error::InvalidQuery(format!(
412 "query cannot be serialized to engine JSON: unsupported binary op {:?}",
413 other
414 )));
415 }
416 };
417
418 let mut result = JsonMap::with_capacity(1);
419 match operator {
420 None => {
421 result.insert(field, value);
422 }
423 Some(op_name) => {
424 let mut operators = JsonMap::with_capacity(1);
425 operators.insert(op_name.to_string(), value);
426 result.insert(field, JsonValue::Object(operators));
427 }
428 }
429
430 Ok(JsonValue::Object(result))
431}
432
433fn null_predicate_to_json(inner: &Expr, is_null: bool) -> Result<JsonValue> {
434 let field = match inner {
435 Expr::Column(name) => strip_column_qualifier(name),
436 other => {
437 return Err(Error::InvalidQuery(format!(
438 "query cannot be serialized to engine JSON: unsupported null predicate {:?}",
439 other
440 )));
441 }
442 };
443
444 let mut operators = JsonMap::with_capacity(1);
445 operators.insert("isNull".to_string(), JsonValue::Bool(is_null));
446
447 let mut result = JsonMap::with_capacity(1);
448 result.insert(field, JsonValue::Object(operators));
449 Ok(JsonValue::Object(result))
450}
451
452fn like_operator_and_value(expr: &Expr) -> Result<(Option<&'static str>, JsonValue)> {
453 let value = match expr {
454 Expr::Param(value) => value.to_json_plain(),
455 other => {
456 return Err(Error::InvalidQuery(format!(
457 "query cannot be serialized to engine JSON: unsupported LIKE operand {:?}",
458 other
459 )));
460 }
461 };
462
463 let Some(pattern) = value.as_str() else {
464 return Ok((Some("like"), value));
465 };
466
467 if pattern.starts_with('%') && pattern.ends_with('%') && pattern.len() >= 2 {
468 return Ok((
469 Some("contains"),
470 JsonValue::String(pattern[1..pattern.len() - 1].to_string()),
471 ));
472 }
473
474 if let Some(stripped) = pattern.strip_prefix('%') {
475 return Ok((Some("endsWith"), JsonValue::String(stripped.to_string())));
476 }
477
478 if let Some(stripped) = pattern.strip_suffix('%') {
479 return Ok((Some("startsWith"), JsonValue::String(stripped.to_string())));
480 }
481
482 Ok((Some("like"), JsonValue::String(pattern.to_string())))
483}
484
485fn list_expr_to_json_array(expr: &Expr) -> Result<JsonValue> {
486 let Expr::List(items) = expr else {
487 return Err(Error::InvalidQuery(format!(
488 "query cannot be serialized to engine JSON: unsupported list operand {:?}",
489 expr
490 )));
491 };
492
493 let mut values = Vec::with_capacity(items.len());
494 for item in items {
495 values.push(expr_value_to_json(item)?);
496 }
497
498 Ok(JsonValue::Array(values))
499}
500
501fn expr_value_to_json(expr: &Expr) -> Result<JsonValue> {
502 match expr {
503 Expr::Param(value) => Ok(value.to_json_plain()),
504 other => Err(Error::InvalidQuery(format!(
505 "query cannot be serialized to engine JSON: unsupported value expression {:?}",
506 other
507 ))),
508 }
509}
510
511fn strip_column_qualifier(name: &str) -> String {
512 name.split_once("__")
513 .map(|(_, column)| column.to_string())
514 .unwrap_or_else(|| name.to_string())
515}
516
517#[cfg(test)]
518mod tests {
519 use std::collections::HashMap;
520
521 use serde_json::json;
522
523 use super::*;
524 use crate::{Column, Value, VectorMetric, VectorNearest};
525
526 #[test]
527 fn find_many_args_serializes_supported_filters_and_includes() {
528 let args = FindManyArgs {
529 where_: Some(
530 Column::<String>::new("Entry", "slug")
531 .contains("rust-entry-")
532 .and(Expr::column("Entry__id").gt(Expr::param(2))),
533 ),
534 order_by: vec![Column::<i32>::new("Entry", "id").asc()],
535 take: Some(2),
536 skip: Some(1),
537 include: HashMap::from([(
538 "author".to_string(),
539 IncludeRelation::with_filter(
540 Column::<String>::new("User", "email").eq("a@example.com"),
541 )
542 .with_order_by(Column::<i32>::new("User", "id").desc())
543 .with_take(1)
544 .with_include("posts", IncludeRelation::plain()),
545 )]),
546 select: HashMap::from([("id".to_string(), true), ("slug".to_string(), true)]),
547 cursor: Some(HashMap::from([("id".to_string(), Value::I32(10))])),
548 distinct: vec!["Entry__slug".to_string()],
549 nearest: Some(VectorNearest {
550 field: "Entry__embedding".to_string(),
551 query: vec![1.0, 2.0, 3.0],
552 metric: VectorMetric::Cosine,
553 }),
554 };
555
556 let json = find_many_args_to_protocol_json(&args).expect("serialization should succeed");
557 let object =
558 find_many_args_to_protocol_object(&args).expect("object serialization should succeed");
559
560 assert_eq!(
561 json,
562 json!({
563 "where": {
564 "AND": [
565 { "slug": { "contains": "rust-entry-" } },
566 { "id": { "gt": 2 } }
567 ]
568 },
569 "orderBy": [{ "id": "asc" }],
570 "take": 2,
571 "skip": 1,
572 "include": {
573 "author": {
574 "where": { "email": "a@example.com" },
575 "orderBy": [{ "id": "desc" }],
576 "take": 1,
577 "include": {
578 "posts": {}
579 }
580 }
581 },
582 "select": {
583 "id": true,
584 "slug": true
585 },
586 "cursor": {
587 "id": 10
588 },
589 "distinct": ["slug"],
590 "nearest": {
591 "field": "embedding",
592 "query": [1.0, 2.0, 3.0],
593 "metric": "cosine"
594 }
595 })
596 );
597 assert_eq!(JsonValue::Object(object), json);
598 }
599
600 #[test]
601 fn unsupported_expression_returns_invalid_query() {
602 let args = FindManyArgs {
603 where_: Some(Expr::exists(
604 crate::Select::from_table("Post")
605 .filter(Expr::column("Post__user_id").eq(Expr::column("User__id")))
606 .build()
607 .expect("valid select"),
608 )),
609 ..Default::default()
610 };
611
612 let err = find_many_args_to_protocol_json(&args).expect_err("exists is not serializable");
613 assert!(matches!(err, Error::InvalidQuery(_)));
614 }
615
616 #[test]
617 fn relation_predicates_serialize_to_relation_where_objects() {
618 let args = FindManyArgs {
619 where_: Some(
620 Expr::relation_some(
621 "posts",
622 "User",
623 "Post",
624 "user_id",
625 "id",
626 crate::Column::<String>::new("Post", "title")
627 .contains("rust")
628 .and(Expr::relation_none(
629 "comments",
630 "Post",
631 "Comment",
632 "post_id",
633 "id",
634 crate::Column::<bool>::new("Comment", "flagged").eq(false),
635 )),
636 )
637 .and(Expr::relation_every(
638 "posts",
639 "User",
640 "Post",
641 "user_id",
642 "id",
643 crate::Column::<bool>::new("Post", "published").eq(true),
644 )),
645 ),
646 ..Default::default()
647 };
648
649 let json =
650 find_many_args_to_protocol_json(&args).expect("relation filters should serialize");
651
652 assert_eq!(
653 json,
654 json!({
655 "where": {
656 "AND": [
657 {
658 "posts": {
659 "some": {
660 "AND": [
661 { "title": { "contains": "rust" } },
662 { "comments": { "none": { "flagged": false } } }
663 ]
664 }
665 }
666 },
667 {
668 "posts": {
669 "every": {
670 "published": true
671 }
672 }
673 }
674 ]
675 }
676 })
677 );
678 }
679}