1use std::error::Error;
2use std::fmt;
3
4use serde_json::Value;
5
6use crate::error::NanoError;
7use crate::ir::ParamMap;
8use crate::json_output::{JS_MAX_SAFE_INTEGER_U64, is_js_safe_integer_i64};
9use crate::query::ast::{Literal, Param, QueryDecl};
10use crate::query::parser::parse_query;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum JsonParamMode {
14 Standard,
15 JavaScript,
16}
17
18#[derive(Debug)]
19pub enum RunInputError {
20 Core(NanoError),
21 Message(String),
22}
23
24impl RunInputError {
25 fn message(message: impl Into<String>) -> Self {
26 Self::Message(message.into())
27 }
28}
29
30impl fmt::Display for RunInputError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Self::Core(err) => err.fmt(f),
34 Self::Message(message) => f.write_str(message),
35 }
36 }
37}
38
39impl Error for RunInputError {
40 fn source(&self) -> Option<&(dyn Error + 'static)> {
41 match self {
42 Self::Core(err) => Some(err),
43 Self::Message(_) => None,
44 }
45 }
46}
47
48impl From<NanoError> for RunInputError {
49 fn from(value: NanoError) -> Self {
50 Self::Core(value)
51 }
52}
53
54pub type RunInputResult<T> = std::result::Result<T, RunInputError>;
55
56pub trait ToParam {
57 fn to_param(self) -> crate::error::Result<Literal>;
58}
59
60impl ToParam for Literal {
61 fn to_param(self) -> crate::error::Result<Literal> {
62 Ok(self)
63 }
64}
65
66impl ToParam for &Literal {
67 fn to_param(self) -> crate::error::Result<Literal> {
68 Ok(self.clone())
69 }
70}
71
72impl ToParam for String {
73 fn to_param(self) -> crate::error::Result<Literal> {
74 Ok(Literal::String(self))
75 }
76}
77
78impl ToParam for &String {
79 fn to_param(self) -> crate::error::Result<Literal> {
80 Ok(Literal::String(self.clone()))
81 }
82}
83
84impl ToParam for &str {
85 fn to_param(self) -> crate::error::Result<Literal> {
86 Ok(Literal::String(self.to_string()))
87 }
88}
89
90impl ToParam for bool {
91 fn to_param(self) -> crate::error::Result<Literal> {
92 Ok(Literal::Bool(self))
93 }
94}
95
96impl ToParam for i8 {
97 fn to_param(self) -> crate::error::Result<Literal> {
98 Ok(Literal::Integer(i64::from(self)))
99 }
100}
101
102impl ToParam for i16 {
103 fn to_param(self) -> crate::error::Result<Literal> {
104 Ok(Literal::Integer(i64::from(self)))
105 }
106}
107
108impl ToParam for i32 {
109 fn to_param(self) -> crate::error::Result<Literal> {
110 Ok(Literal::Integer(i64::from(self)))
111 }
112}
113
114impl ToParam for i64 {
115 fn to_param(self) -> crate::error::Result<Literal> {
116 Ok(Literal::Integer(self))
117 }
118}
119
120impl ToParam for isize {
121 fn to_param(self) -> crate::error::Result<Literal> {
122 let value = i64::try_from(self).map_err(|_| {
123 NanoError::Execution(format!(
124 "param value {} exceeds current engine range for numeric literals (max {})",
125 self,
126 i64::MAX
127 ))
128 })?;
129 Ok(Literal::Integer(value))
130 }
131}
132
133impl ToParam for u8 {
134 fn to_param(self) -> crate::error::Result<Literal> {
135 Ok(Literal::Integer(i64::from(self)))
136 }
137}
138
139impl ToParam for u16 {
140 fn to_param(self) -> crate::error::Result<Literal> {
141 Ok(Literal::Integer(i64::from(self)))
142 }
143}
144
145impl ToParam for u32 {
146 fn to_param(self) -> crate::error::Result<Literal> {
147 Ok(Literal::Integer(i64::from(self)))
148 }
149}
150
151impl ToParam for u64 {
152 fn to_param(self) -> crate::error::Result<Literal> {
153 let value = i64::try_from(self).map_err(|_| {
154 NanoError::Execution(format!(
155 "param value {} exceeds current engine range for numeric literals (max {})",
156 self,
157 i64::MAX
158 ))
159 })?;
160 Ok(Literal::Integer(value))
161 }
162}
163
164impl ToParam for usize {
165 fn to_param(self) -> crate::error::Result<Literal> {
166 let value = i64::try_from(self).map_err(|_| {
167 NanoError::Execution(format!(
168 "param value {} exceeds current engine range for numeric literals (max {})",
169 self,
170 i64::MAX
171 ))
172 })?;
173 Ok(Literal::Integer(value))
174 }
175}
176
177impl ToParam for f32 {
178 fn to_param(self) -> crate::error::Result<Literal> {
179 if !self.is_finite() {
180 return Err(NanoError::Execution(format!(
181 "invalid float parameter {}",
182 self
183 )));
184 }
185 Ok(Literal::Float(f64::from(self)))
186 }
187}
188
189impl ToParam for f64 {
190 fn to_param(self) -> crate::error::Result<Literal> {
191 if !self.is_finite() {
192 return Err(NanoError::Execution(format!(
193 "invalid float parameter {}",
194 self
195 )));
196 }
197 Ok(Literal::Float(self))
198 }
199}
200
201impl<T> ToParam for Vec<T>
202where
203 T: ToParam,
204{
205 fn to_param(self) -> crate::error::Result<Literal> {
206 let mut out = Vec::with_capacity(self.len());
207 for value in self {
208 out.push(value.to_param()?);
209 }
210 Ok(Literal::List(out))
211 }
212}
213
214impl<T> ToParam for &[T]
215where
216 T: Clone + ToParam,
217{
218 fn to_param(self) -> crate::error::Result<Literal> {
219 let mut out = Vec::with_capacity(self.len());
220 for value in self {
221 out.push(value.clone().to_param()?);
222 }
223 Ok(Literal::List(out))
224 }
225}
226
227impl<T, const N: usize> ToParam for [T; N]
228where
229 T: ToParam,
230{
231 fn to_param(self) -> crate::error::Result<Literal> {
232 let mut out = Vec::with_capacity(N);
233 for value in self {
234 out.push(value.to_param()?);
235 }
236 Ok(Literal::List(out))
237 }
238}
239
240#[macro_export]
241macro_rules! params {
242 () => {
243 ::std::result::Result::Ok($crate::ParamMap::new())
244 };
245 ($($key:expr => $value:expr),+ $(,)?) => {{
246 (|| -> $crate::error::Result<$crate::ParamMap> {
247 let mut map = $crate::ParamMap::new();
248 $(
249 map.insert(::std::convert::Into::<String>::into($key), $crate::ToParam::to_param($value)?);
250 )+
251 Ok(map)
252 })()
253 }};
254}
255
256pub fn find_named_query(query_source: &str, query_name: &str) -> RunInputResult<QueryDecl> {
257 let queries = parse_query(query_source)?;
258 queries
259 .queries
260 .into_iter()
261 .find(|query| query.name == query_name)
262 .ok_or_else(|| RunInputError::message(format!("query '{}' not found", query_name)))
263}
264
265pub fn json_params_to_param_map(
266 params: Option<&Value>,
267 query_params: &[Param],
268 mode: JsonParamMode,
269) -> RunInputResult<ParamMap> {
270 let mut map = ParamMap::new();
271 let object = match params {
272 Some(Value::Object(object)) => object,
273 Some(Value::Null) | None => {
274 for param in query_params {
276 if param.nullable {
277 map.insert(param.name.clone(), Literal::Null);
278 }
279 }
280 return Ok(map);
281 }
282 Some(other) => {
283 let message = match mode {
284 JsonParamMode::Standard => "params must be a JSON object".to_string(),
285 JsonParamMode::JavaScript => {
286 format!("params must be an object, got {}", json_type_name(other))
287 }
288 };
289 return Err(RunInputError::message(message));
290 }
291 };
292
293 for (key, value) in object {
294 let decl = query_params.iter().find(|param| param.name == *key);
295 if let Some(decl) = decl {
296 if matches!(value, Value::Null) {
297 if decl.nullable {
298 map.insert(key.clone(), Literal::Null);
299 } else {
300 return Err(RunInputError::message(format!(
301 "param '{}': null is not accepted for non-nullable parameter",
302 key
303 )));
304 }
305 } else {
306 let literal = json_value_to_literal_typed(key, value, &decl.type_name, mode)?;
307 map.insert(key.clone(), literal);
308 }
309 } else {
310 let literal = json_value_to_literal_inferred(key, value, mode)?;
311 map.insert(key.clone(), literal);
312 };
313 }
314
315 for param in query_params {
317 if param.nullable && !map.contains_key(¶m.name) {
318 map.insert(param.name.clone(), Literal::Null);
319 }
320 }
321
322 Ok(map)
323}
324
325fn json_value_to_literal_typed(
326 key: &str,
327 value: &Value,
328 type_name: &str,
329 mode: JsonParamMode,
330) -> RunInputResult<Literal> {
331 match type_name {
332 "String" => match value {
333 Value::String(value) => Ok(Literal::String(value.clone())),
334 other => Err(RunInputError::message(format!(
335 "param '{}': expected string, got {}",
336 key,
337 json_type_name(other)
338 ))),
339 },
340 "I32" => match mode {
341 JsonParamMode::Standard => {
342 let value = parse_i64_param(key, value, mode)?;
343 let value = i32::try_from(value).map_err(|_| {
344 RunInputError::message(format!("param '{}': value {} exceeds I32", key, value))
345 })?;
346 Ok(Literal::Integer(i64::from(value)))
347 }
348 JsonParamMode::JavaScript => {
349 let value = parse_i64_param(key, value, mode)?;
350 let value = i32::try_from(value).map_err(|_| {
351 RunInputError::message(format!(
352 "param '{}': value {} exceeds I32 range",
353 key, value
354 ))
355 })?;
356 Ok(Literal::Integer(i64::from(value)))
357 }
358 },
359 "I64" => Ok(Literal::Integer(parse_i64_param(key, value, mode)?)),
360 "U32" => {
361 let value = parse_u64_param(key, value, mode)?;
362 let value = match mode {
363 JsonParamMode::Standard => u32::try_from(value).map_err(|_| {
364 RunInputError::message(format!("param '{}': value {} exceeds U32", key, value))
365 })?,
366 JsonParamMode::JavaScript => u32::try_from(value).map_err(|_| {
367 RunInputError::message(format!(
368 "param '{}': value {} exceeds U32 range",
369 key, value
370 ))
371 })?,
372 };
373 Ok(Literal::Integer(i64::from(value)))
374 }
375 "U64" => {
376 let value = parse_u64_param(key, value, mode)?;
377 let value = match mode {
378 JsonParamMode::Standard => i64::try_from(value).map_err(|_| {
379 RunInputError::message(format!(
380 "param '{}': value {} exceeds current engine range for U64 (max {})",
381 key,
382 value,
383 i64::MAX
384 ))
385 })?,
386 JsonParamMode::JavaScript => i64::try_from(value).map_err(|_| {
387 RunInputError::message(format!(
388 "param '{}': value {} exceeds current engine range for U64 parameters (max {})",
389 key,
390 value,
391 i64::MAX
392 ))
393 })?,
394 };
395 Ok(Literal::Integer(value))
396 }
397 "F32" | "F64" => {
398 let value = value.as_f64().ok_or_else(|| match mode {
399 JsonParamMode::Standard => {
400 RunInputError::message(format!("param '{}': expected float", key))
401 }
402 JsonParamMode::JavaScript => RunInputError::message(format!(
403 "param '{}': expected float, got {}",
404 key,
405 json_type_name(value)
406 )),
407 })?;
408 Ok(Literal::Float(value))
409 }
410 "Bool" => {
411 let value = value.as_bool().ok_or_else(|| match mode {
412 JsonParamMode::Standard => {
413 RunInputError::message(format!("param '{}': expected boolean", key))
414 }
415 JsonParamMode::JavaScript => RunInputError::message(format!(
416 "param '{}': expected boolean, got {}",
417 key,
418 json_type_name(value)
419 )),
420 })?;
421 Ok(Literal::Bool(value))
422 }
423 "Date" => match value {
424 Value::String(value) => Ok(Literal::Date(value.clone())),
425 other => Err(match mode {
426 JsonParamMode::Standard => {
427 RunInputError::message(format!("param '{}': expected date string", key))
428 }
429 JsonParamMode::JavaScript => RunInputError::message(format!(
430 "param '{}': expected date string, got {}",
431 key,
432 json_type_name(other)
433 )),
434 }),
435 },
436 "DateTime" => match value {
437 Value::String(value) => Ok(Literal::DateTime(value.clone())),
438 other => Err(match mode {
439 JsonParamMode::Standard => {
440 RunInputError::message(format!("param '{}': expected datetime string", key))
441 }
442 JsonParamMode::JavaScript => RunInputError::message(format!(
443 "param '{}': expected datetime string, got {}",
444 key,
445 json_type_name(other)
446 )),
447 }),
448 },
449 "Blob" => match value {
450 Value::String(value) => Ok(Literal::String(value.clone())),
451 other => Err(RunInputError::message(format!(
452 "param '{}': expected blob URI string, got {}",
453 key,
454 json_type_name(other)
455 ))),
456 },
457 other if parse_list_item_type(other).is_some() => {
458 let item_type = parse_list_item_type(other).unwrap();
459 let items = value.as_array().ok_or_else(|| match mode {
460 JsonParamMode::Standard => {
461 RunInputError::message(format!("param '{}': expected array for {}", key, other))
462 }
463 JsonParamMode::JavaScript => RunInputError::message(format!(
464 "param '{}': expected array for {}, got {}",
465 key,
466 other,
467 json_type_name(value)
468 )),
469 })?;
470 let mut out = Vec::with_capacity(items.len());
471 for item in items {
472 out.push(json_value_to_literal_typed(key, item, item_type, mode)?);
473 }
474 Ok(Literal::List(out))
475 }
476 other if other.starts_with("Vector(") => {
477 let expected_dim = parse_vector_dim(other).ok_or_else(|| match mode {
478 JsonParamMode::Standard => RunInputError::message(format!(
479 "param '{}': invalid vector type '{}'",
480 key, other
481 )),
482 JsonParamMode::JavaScript => RunInputError::message(format!(
483 "param '{}': invalid vector type '{}' (expected Vector(N))",
484 key, other
485 )),
486 })?;
487 let items = value.as_array().ok_or_else(|| match mode {
488 JsonParamMode::Standard => {
489 RunInputError::message(format!("param '{}': expected array for {}", key, other))
490 }
491 JsonParamMode::JavaScript => RunInputError::message(format!(
492 "param '{}': expected array for {}, got {}",
493 key,
494 other,
495 json_type_name(value)
496 )),
497 })?;
498 if items.len() != expected_dim {
499 return Err(RunInputError::message(format!(
500 "param '{}': expected {} values for {}, got {}",
501 key,
502 expected_dim,
503 other,
504 items.len()
505 )));
506 }
507 let mut out = Vec::with_capacity(items.len());
508 for item in items {
509 let value = item.as_f64().ok_or_else(|| match mode {
510 JsonParamMode::Standard => RunInputError::message(format!(
511 "param '{}': vector element is not numeric",
512 key
513 )),
514 JsonParamMode::JavaScript => RunInputError::message(format!(
515 "param '{}': vector element '{}' is not numeric",
516 key, item
517 )),
518 })?;
519 out.push(Literal::Float(value));
520 }
521 Ok(Literal::List(out))
522 }
523 _ => match value {
524 Value::String(value) => Ok(Literal::String(value.clone())),
525 other => Err(RunInputError::message(format!(
526 "param '{}': expected string for type '{}', got {}",
527 key,
528 type_name,
529 json_type_name(other)
530 ))),
531 },
532 }
533}
534
535fn json_value_to_literal_inferred(
536 key: &str,
537 value: &Value,
538 mode: JsonParamMode,
539) -> RunInputResult<Literal> {
540 match value {
541 Value::String(value) => Ok(Literal::String(value.clone())),
542 Value::Bool(value) => Ok(Literal::Bool(*value)),
543 Value::Number(number) => match mode {
544 JsonParamMode::Standard => {
545 if let Some(value) = number.as_i64() {
546 Ok(Literal::Integer(value))
547 } else if let Some(value) = number.as_f64() {
548 Ok(Literal::Float(value))
549 } else {
550 Err(RunInputError::message(format!(
551 "param '{}': unsupported numeric value",
552 key
553 )))
554 }
555 }
556 JsonParamMode::JavaScript => {
557 if let Some(value) = number.as_i64() {
558 if !is_js_safe_integer_i64(value) {
559 return Err(RunInputError::message(format!(
560 "param '{}': integer {} exceeds JS safe integer range; use a decimal string and a typed query parameter for exact values",
561 key, value
562 )));
563 }
564 Ok(Literal::Integer(value))
565 } else if let Some(value) = number.as_u64() {
566 if value > JS_MAX_SAFE_INTEGER_U64 {
567 return Err(RunInputError::message(format!(
568 "param '{}': integer {} exceeds JS safe integer range; use a decimal string and a typed query parameter for exact values",
569 key, value
570 )));
571 }
572 let value = i64::try_from(value).map_err(|_| {
573 RunInputError::message(format!(
574 "param '{}': integer {} exceeds supported range (max {})",
575 key,
576 value,
577 i64::MAX
578 ))
579 })?;
580 Ok(Literal::Integer(value))
581 } else if let Some(value) = number.as_f64() {
582 Ok(Literal::Float(value))
583 } else {
584 Err(RunInputError::message(format!(
585 "param '{}': unsupported number value",
586 key
587 )))
588 }
589 }
590 },
591 Value::Array(values) => {
592 let mut out = Vec::with_capacity(values.len());
593 for value in values {
594 out.push(json_value_to_literal_inferred(key, value, mode)?);
595 }
596 Ok(Literal::List(out))
597 }
598 Value::Null => Ok(Literal::Null),
599 Value::Object(_) => Err(match mode {
600 JsonParamMode::Standard => {
601 RunInputError::message(format!("param '{}': object is not supported", key))
602 }
603 JsonParamMode::JavaScript => RunInputError::message(format!(
604 "param '{}': object values are not supported as query parameters",
605 key
606 )),
607 }),
608 }
609}
610
611fn parse_i64_param(key: &str, value: &Value, mode: JsonParamMode) -> RunInputResult<i64> {
612 match mode {
613 JsonParamMode::Standard => match value {
614 Value::Number(number) => number.as_i64().ok_or_else(|| {
615 RunInputError::message(format!("param '{}': expected integer number", key))
616 }),
617 Value::String(value) => value.parse::<i64>().map_err(|_| {
618 RunInputError::message(format!(
619 "param '{}': expected integer string, got '{}'",
620 key, value
621 ))
622 }),
623 _ => Err(RunInputError::message(format!(
624 "param '{}': expected integer",
625 key
626 ))),
627 },
628 JsonParamMode::JavaScript => match value {
629 Value::Number(number) => {
630 let parsed = if let Some(parsed) = number.as_i64() {
631 parsed
632 } else if let Some(parsed) = number.as_f64() {
633 if !parsed.is_finite() || parsed.fract() != 0.0 {
634 return Err(RunInputError::message(format!(
635 "param '{}': expected integer, got number",
636 key
637 )));
638 }
639 if parsed < i64::MIN as f64 || parsed > i64::MAX as f64 {
640 return Err(RunInputError::message(format!(
641 "param '{}': integer {} is outside i64 range",
642 key, parsed
643 )));
644 }
645 parsed as i64
646 } else {
647 return Err(RunInputError::message(format!(
648 "param '{}': expected integer, got number",
649 key
650 )));
651 };
652 if !is_js_safe_integer_i64(parsed) {
653 return Err(RunInputError::message(format!(
654 "param '{}': integer {} exceeds JS safe integer range; pass a decimal string for exact values",
655 key, parsed
656 )));
657 }
658 Ok(parsed)
659 }
660 Value::String(value) => value.parse::<i64>().map_err(|_| {
661 RunInputError::message(format!(
662 "param '{}': expected integer string, got '{}'",
663 key, value
664 ))
665 }),
666 other => Err(RunInputError::message(format!(
667 "param '{}': expected integer, got {}",
668 key,
669 json_type_name(other)
670 ))),
671 },
672 }
673}
674
675fn parse_u64_param(key: &str, value: &Value, mode: JsonParamMode) -> RunInputResult<u64> {
676 match mode {
677 JsonParamMode::Standard => match value {
678 Value::Number(number) => number.as_u64().ok_or_else(|| {
679 RunInputError::message(format!("param '{}': expected unsigned integer number", key))
680 }),
681 Value::String(value) => value.parse::<u64>().map_err(|_| {
682 RunInputError::message(format!(
683 "param '{}': expected unsigned integer string, got '{}'",
684 key, value
685 ))
686 }),
687 _ => Err(RunInputError::message(format!(
688 "param '{}': expected unsigned integer",
689 key
690 ))),
691 },
692 JsonParamMode::JavaScript => match value {
693 Value::Number(number) => {
694 let parsed = if let Some(parsed) = number.as_u64() {
695 parsed
696 } else if let Some(parsed) = number.as_f64() {
697 if !parsed.is_finite() || parsed.fract() != 0.0 || parsed < 0.0 {
698 return Err(RunInputError::message(format!(
699 "param '{}': expected unsigned integer, got number",
700 key
701 )));
702 }
703 if parsed > u64::MAX as f64 {
704 return Err(RunInputError::message(format!(
705 "param '{}': integer {} is outside u64 range",
706 key, parsed
707 )));
708 }
709 parsed as u64
710 } else {
711 return Err(RunInputError::message(format!(
712 "param '{}': expected unsigned integer, got number",
713 key
714 )));
715 };
716 if parsed > JS_MAX_SAFE_INTEGER_U64 {
717 return Err(RunInputError::message(format!(
718 "param '{}': integer {} exceeds JS safe integer range; pass a decimal string for exact values",
719 key, parsed
720 )));
721 }
722 Ok(parsed)
723 }
724 Value::String(value) => value.parse::<u64>().map_err(|_| {
725 RunInputError::message(format!(
726 "param '{}': expected unsigned integer string, got '{}'",
727 key, value
728 ))
729 }),
730 other => Err(RunInputError::message(format!(
731 "param '{}': expected unsigned integer, got {}",
732 key,
733 json_type_name(other)
734 ))),
735 },
736 }
737}
738
739fn parse_vector_dim(type_name: &str) -> Option<usize> {
740 let dim = type_name
741 .strip_prefix("Vector(")?
742 .strip_suffix(')')?
743 .parse::<usize>()
744 .ok()?;
745 if dim == 0 { None } else { Some(dim) }
746}
747
748fn parse_list_item_type(type_name: &str) -> Option<&str> {
749 Some(type_name.strip_prefix('[')?.strip_suffix(']')?.trim())
750}
751
752fn json_type_name(value: &Value) -> &'static str {
753 match value {
754 Value::Null => "null",
755 Value::Bool(_) => "boolean",
756 Value::Number(_) => "number",
757 Value::String(_) => "string",
758 Value::Array(_) => "array",
759 Value::Object(_) => "object",
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use serde_json::json;
766
767 use super::{JsonParamMode, ToParam, find_named_query, json_params_to_param_map};
768 use crate::query::ast::Literal;
769
770 #[test]
771 fn js_mode_rejects_unsafe_integer_numbers() {
772 let query = find_named_query(
773 "query find($id: U64) { match { $u: User } return { $u } }",
774 "find",
775 )
776 .expect("query should parse");
777
778 let error = json_params_to_param_map(
779 Some(&json!({ "id": 9_007_199_254_740_992u64 })),
780 &query.params,
781 JsonParamMode::JavaScript,
782 )
783 .expect_err("unsafe integer should fail");
784
785 assert_eq!(
786 error.to_string(),
787 "param 'id': integer 9007199254740992 exceeds JS safe integer range; pass a decimal string for exact values"
788 );
789 }
790
791 #[test]
792 fn standard_mode_preserves_ffi_param_object_error() {
793 let error = json_params_to_param_map(Some(&json!(["nope"])), &[], JsonParamMode::Standard)
794 .expect_err("non-object params should fail");
795
796 assert_eq!(error.to_string(), "params must be a JSON object");
797 }
798
799 #[test]
800 fn to_param_supports_lists_and_explicit_date_literals() {
801 let vector = vec![1_i32, 2_i32, 3_i32].to_param().expect("vector param");
802 match vector {
803 Literal::List(values) => {
804 assert!(matches!(values.first(), Some(Literal::Integer(1))));
805 assert!(matches!(values.get(1), Some(Literal::Integer(2))));
806 assert!(matches!(values.get(2), Some(Literal::Integer(3))));
807 }
808 other => panic!("expected list param, got {:?}", other),
809 }
810
811 let date = Literal::Date("2026-03-06".to_string())
812 .to_param()
813 .expect("date param");
814 assert!(matches!(date, Literal::Date(ref value) if value == "2026-03-06"));
815 }
816
817 #[test]
818 fn to_param_rejects_unsigned_values_outside_engine_range() {
819 let error = u64::MAX.to_param().expect_err("oversized u64 should fail");
820
821 assert_eq!(
822 error.to_string(),
823 format!(
824 "execution error: param value {} exceeds current engine range for numeric literals (max {})",
825 u64::MAX,
826 i64::MAX
827 )
828 );
829 }
830
831 #[test]
832 fn params_macro_builds_param_map() {
833 let params = params! {
834 "name" => "Alice",
835 "age" => 41_i32,
836 "scores" => [1_u8, 2_u8, 3_u8],
837 "published_at" => Literal::DateTime("2026-03-06T12:00:00Z".to_string()),
838 }
839 .expect("params");
840
841 assert!(matches!(
842 params.get("name"),
843 Some(Literal::String(value)) if value == "Alice"
844 ));
845 assert!(matches!(params.get("age"), Some(Literal::Integer(41))));
846 match params.get("scores") {
847 Some(Literal::List(values)) => {
848 assert!(matches!(values.first(), Some(Literal::Integer(1))));
849 assert!(matches!(values.get(1), Some(Literal::Integer(2))));
850 assert!(matches!(values.get(2), Some(Literal::Integer(3))));
851 }
852 other => panic!("expected list param, got {:?}", other),
853 }
854 assert!(matches!(
855 params.get("published_at"),
856 Some(Literal::DateTime(value)) if value == "2026-03-06T12:00:00Z"
857 ));
858 }
859
860 #[test]
861 fn typed_json_params_support_list_and_datetime_types() {
862 let query = find_named_query(
863 r#"
864query q($tags: [String], $days: [Date]?, $due_at: DateTime) {
865 match { $t: Task }
866 return { $t.slug }
867}
868"#,
869 "q",
870 )
871 .expect("query");
872
873 let params = json_params_to_param_map(
874 Some(&json!({
875 "tags": ["launch", "priority"],
876 "days": ["2026-04-01", "2026-04-02"],
877 "due_at": "2026-04-03T10:15:00Z"
878 })),
879 &query.params,
880 JsonParamMode::Standard,
881 )
882 .expect("typed params");
883
884 assert!(matches!(
885 params.get("due_at"),
886 Some(Literal::DateTime(value)) if value == "2026-04-03T10:15:00Z"
887 ));
888 match params.get("tags") {
889 Some(Literal::List(values)) => {
890 assert!(
891 matches!(values.first(), Some(Literal::String(value)) if value == "launch")
892 );
893 assert!(
894 matches!(values.get(1), Some(Literal::String(value)) if value == "priority")
895 );
896 }
897 other => panic!("expected string list param, got {:?}", other),
898 }
899 match params.get("days") {
900 Some(Literal::List(values)) => {
901 assert!(
902 matches!(values.first(), Some(Literal::Date(value)) if value == "2026-04-01")
903 );
904 assert!(
905 matches!(values.get(1), Some(Literal::Date(value)) if value == "2026-04-02")
906 );
907 }
908 other => panic!("expected date list param, got {:?}", other),
909 }
910 }
911
912 #[test]
913 fn nullable_param_omitted_becomes_null() {
914 let query = find_named_query(
915 "query q($name: String, $bio: String?) { match { $u: User } return { $u } }",
916 "q",
917 )
918 .expect("query");
919
920 let params = json_params_to_param_map(
921 Some(&json!({ "name": "Alice" })),
922 &query.params,
923 JsonParamMode::Standard,
924 )
925 .expect("should accept omitted nullable param");
926
927 assert!(matches!(params.get("name"), Some(Literal::String(v)) if v == "Alice"));
928 assert!(matches!(params.get("bio"), Some(Literal::Null)));
929 }
930
931 #[test]
932 fn nullable_param_explicit_null_becomes_null() {
933 let query = find_named_query(
934 "query q($name: String, $bio: String?) { match { $u: User } return { $u } }",
935 "q",
936 )
937 .expect("query");
938
939 let params = json_params_to_param_map(
940 Some(&json!({ "name": "Alice", "bio": null })),
941 &query.params,
942 JsonParamMode::Standard,
943 )
944 .expect("should accept explicit null for nullable param");
945
946 assert!(matches!(params.get("name"), Some(Literal::String(v)) if v == "Alice"));
947 assert!(matches!(params.get("bio"), Some(Literal::Null)));
948 }
949
950 #[test]
951 fn non_nullable_param_rejects_null() {
952 let query = find_named_query(
953 "query q($name: String) { match { $u: User } return { $u } }",
954 "q",
955 )
956 .expect("query");
957
958 let error = json_params_to_param_map(
959 Some(&json!({ "name": null })),
960 &query.params,
961 JsonParamMode::Standard,
962 )
963 .expect_err("null for non-nullable param should fail");
964
965 assert!(
966 error
967 .to_string()
968 .contains("null is not accepted for non-nullable parameter"),
969 "unexpected error: {}",
970 error
971 );
972 }
973
974 #[test]
975 fn nullable_param_with_value_works_normally() {
976 let query = find_named_query(
977 "query q($bio: String?) { match { $u: User } return { $u } }",
978 "q",
979 )
980 .expect("query");
981
982 let params = json_params_to_param_map(
983 Some(&json!({ "bio": "hello" })),
984 &query.params,
985 JsonParamMode::Standard,
986 )
987 .expect("should accept string value for nullable param");
988
989 assert!(matches!(params.get("bio"), Some(Literal::String(v)) if v == "hello"));
990 }
991
992 #[test]
993 fn inferred_null_param_becomes_literal_null() {
994 let params = json_params_to_param_map(
995 Some(&json!({ "extra": null })),
996 &[],
997 JsonParamMode::Standard,
998 )
999 .expect("inferred null should succeed");
1000
1001 assert!(matches!(params.get("extra"), Some(Literal::Null)));
1002 }
1003
1004 #[test]
1005 fn nullable_params_filled_when_params_is_none() {
1006 let query = find_named_query(
1007 "query q($bio: String?) { match { $u: User } return { $u } }",
1008 "q",
1009 )
1010 .expect("query");
1011
1012 let params = json_params_to_param_map(None, &query.params, JsonParamMode::Standard)
1013 .expect("None params should succeed with nullable declarations");
1014
1015 assert!(matches!(params.get("bio"), Some(Literal::Null)));
1016 }
1017}