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