Skip to main content

qql_core/params/
input.rs

1//! Query input parameter binding and resolution.
2
3use super::value::{bind_point_id, bind_value, resolve_param, resolve_positional};
4use crate::ast::Value;
5use crate::ast::statement::{ContextPair, FeedbackItem, PointId, QueryInput};
6use crate::error::{QqlError, Span};
7use alloc::format;
8use alloc::string::String;
9
10/// Recursively bind parameters into a `QueryInput` in-place.
11pub fn bind_query_input<F>(
12    input: &mut QueryInput,
13    lookup: &F,
14    positional: &[Value],
15) -> Result<(), QqlError>
16where
17    F: Fn(&str) -> Option<Value>,
18{
19    match input {
20        QueryInput::Param(name, span) => {
21            let sp = span.as_deref().copied();
22            let val = resolve_param(name, sp, lookup)?;
23            *input = value_to_query_input(val, sp)?;
24        }
25        QueryInput::PositionalParam(idx, span) => {
26            let sp = span.as_deref().copied();
27            let val = resolve_positional(*idx, sp, positional)?;
28            *input = value_to_query_input(val, sp)?;
29        }
30        QueryInput::Point(point) => {
31            bind_point_id(point, lookup, positional)?;
32        }
33        QueryInput::Text {
34            text,
35            text_param,
36            options,
37            ..
38        } => {
39            if let Some(param) = text_param.take() {
40                if let Some(param_name) = param.strip_prefix(':') {
41                    let val = resolve_param(param_name, None, lookup)?;
42                    if let Value::Str(s) = val {
43                        *text = s;
44                    } else {
45                        return Err(QqlError::validation(
46                            "QQL-BIND-TYPE-MISMATCH",
47                            format!("parameter ':{param_name}' for TEXT query must be a string"),
48                            None,
49                        ));
50                    }
51                } else if let Some(idx_str) = param.strip_prefix('?') {
52                    let idx = idx_str.parse::<usize>().map_err(|_| {
53                        QqlError::validation(
54                            "QQL-BIND-INVALID-PARAMS",
55                            format!("invalid positional parameter index '?{idx_str}'"),
56                            None,
57                        )
58                    })?;
59                    let val = resolve_positional(idx, None, positional)?;
60                    if let Value::Str(s) = val {
61                        *text = s;
62                    } else {
63                        return Err(QqlError::validation(
64                            "QQL-BIND-TYPE-MISMATCH",
65                            format!("positional parameter ?{idx} for TEXT query must be a string"),
66                            None,
67                        ));
68                    }
69                } else {
70                    let val = resolve_param(&param, None, lookup)?;
71                    if let Value::Str(s) = val {
72                        *text = s;
73                    } else {
74                        return Err(QqlError::validation(
75                            "QQL-BIND-TYPE-MISMATCH",
76                            format!("parameter ':{param}' for TEXT query must be a string"),
77                            None,
78                        ));
79                    }
80                }
81            }
82            bind_option_values(options, lookup, positional)?;
83        }
84        QueryInput::Image { options, .. } => {
85            bind_option_values(options, lookup, positional)?;
86        }
87        QueryInput::Object {
88            object, options, ..
89        } => {
90            bind_value(object, lookup, positional)?;
91            bind_option_values(options, lookup, positional)?;
92        }
93        _ => {}
94    }
95    Ok(())
96}
97
98/// Bind parameters inside an inference `OPTIONS` dict in-place.
99fn bind_option_values<F>(
100    options: &mut [(String, Value)],
101    lookup: &F,
102    positional: &[Value],
103) -> Result<(), QqlError>
104where
105    F: Fn(&str) -> Option<Value>,
106{
107    for (_, value) in options {
108        bind_value(value, lookup, positional)?;
109    }
110    Ok(())
111}
112
113/// Convert a bound `Value` into a `QueryInput`, failing closed on type mismatch.
114pub fn value_to_query_input(val: Value, span: Option<Span>) -> Result<QueryInput, QqlError> {
115    match val {
116        Value::Str(s) => Ok(QueryInput::Text {
117            text: s,
118            model: None,
119            text_param: None,
120            options: alloc::vec::Vec::new(),
121        }),
122        Value::Int(n) if n >= 0 => Ok(QueryInput::Point(PointId::Number(n as u64))),
123        Value::UInt(n) => Ok(QueryInput::Point(PointId::Number(n))),
124        Value::List(_) | Value::Dict(_) | Value::F32Array(_) => {
125            let vec = crate::parser::helpers::vector_from_value(val, span)?;
126            Ok(QueryInput::Vector(vec))
127        }
128        _ => Err(QqlError::validation(
129            "QQL-BIND-TYPE-MISMATCH",
130            format!("unsupported value type for query input: {:?}", val),
131            span,
132        )),
133    }
134}
135
136/// Bind parameters into a `ContextPair` in-place.
137pub fn bind_context_pair<F>(
138    pair: &mut ContextPair,
139    lookup: &F,
140    positional: &[Value],
141) -> Result<(), QqlError>
142where
143    F: Fn(&str) -> Option<Value>,
144{
145    bind_query_input(&mut pair.positive, lookup, positional)?;
146    bind_query_input(&mut pair.negative, lookup, positional)?;
147    Ok(())
148}
149
150/// Bind parameters into a `FeedbackItem` in-place.
151pub fn bind_feedback_item<F>(
152    item: &mut FeedbackItem,
153    lookup: &F,
154    positional: &[Value],
155) -> Result<(), QqlError>
156where
157    F: Fn(&str) -> Option<Value>,
158{
159    bind_query_input(&mut item.example, lookup, positional)
160}