Skip to main content

qql_core/params/
value.rs

1//! Core value, point ID, and scalar parameter binding and resolution.
2
3use crate::ast::Value;
4use crate::ast::statement::{PointId, ShardKey};
5use crate::error::{QqlError, Span};
6use alloc::format;
7
8/// Resolve a named parameter from the lookup function.
9pub fn resolve_param<F>(name: &str, span: Option<Span>, lookup: &F) -> Result<Value, QqlError>
10where
11    F: Fn(&str) -> Option<Value>,
12{
13    let val = lookup(name).ok_or_else(|| {
14        QqlError::validation(
15            "QQL-BIND-MISSING-PARAM",
16            format!("missing value for named parameter ':{}'", name),
17            span,
18        )
19    })?;
20    if matches!(val, Value::Null) {
21        return Err(QqlError::validation(
22            "QQL-BIND-NULL-PARAM",
23            format!(
24                "parameter ':{name}' is null; QQL cannot bind null — pass a concrete value or remove the placeholder"
25            ),
26            span,
27        ));
28    }
29    Ok(val)
30}
31
32/// Resolve a positional parameter from the positional slice.
33pub fn resolve_positional(
34    idx: usize,
35    span: Option<Span>,
36    positional: &[Value],
37) -> Result<Value, QqlError> {
38    let val = positional.get(idx).cloned().ok_or_else(|| {
39        QqlError::validation(
40            "QQL-BIND-MISSING-PARAM",
41            format!(
42                "positional parameter ? index {} out of range (total provided: {})",
43                idx + 1,
44                positional.len()
45            ),
46            span,
47        )
48    })?;
49    if matches!(val, Value::Null) {
50        return Err(QqlError::validation(
51            "QQL-BIND-NULL-PARAM",
52            format!(
53                "positional parameter ?{} is null; QQL cannot bind null — pass a concrete value or remove the placeholder",
54                idx + 1
55            ),
56            span,
57        ));
58    }
59    Ok(val)
60}
61
62/// Recursively bind parameters into an AST `Value` in-place.
63pub fn bind_value<F>(value: &mut Value, lookup: &F, positional: &[Value]) -> Result<(), QqlError>
64where
65    F: Fn(&str) -> Option<Value>,
66{
67    match value {
68        Value::Param(name, span) => {
69            let resolved = resolve_param(name, span.as_deref().copied(), lookup)?;
70            *value = resolved;
71        }
72        Value::PositionalParam(idx, span) => {
73            let resolved = resolve_positional(*idx, span.as_deref().copied(), positional)?;
74            *value = resolved;
75        }
76        Value::List(items) => {
77            for item in items {
78                bind_value(item, lookup, positional)?;
79            }
80        }
81        Value::Dict(entries) => {
82            for (_k, v) in entries {
83                bind_value(v, lookup, positional)?;
84            }
85        }
86        _ => {}
87    }
88    Ok(())
89}
90
91/// Bind parameters into a `PointId` in-place.
92pub fn bind_point_id<F>(id: &mut PointId, lookup: &F, positional: &[Value]) -> Result<(), QqlError>
93where
94    F: Fn(&str) -> Option<Value>,
95{
96    match id {
97        PointId::Param(name, span) => {
98            let val = resolve_param(name, span.as_deref().copied(), lookup)?;
99            *id = value_to_point_id(&val, span.as_deref().copied())?;
100        }
101        PointId::PositionalParam(idx, span) => {
102            let val = resolve_positional(*idx, span.as_deref().copied(), positional)?;
103            *id = value_to_point_id(&val, span.as_deref().copied())?;
104        }
105        _ => {}
106    }
107    Ok(())
108}
109
110/// Convert a bound `Value` into a `PointId`, failing closed on type mismatch.
111pub fn value_to_point_id(val: &Value, span: Option<Span>) -> Result<PointId, QqlError> {
112    match val {
113        Value::Int(n) if *n >= 0 => Ok(PointId::Number(*n as u64)),
114        Value::UInt(n) => Ok(PointId::Number(*n)),
115        Value::Str(s) => Ok(PointId::String(s.clone())),
116        _ => Err(QqlError::validation(
117            "QQL-BIND-TYPE-MISMATCH",
118            format!("cannot bind {val:?} as point ID: expected non-negative integer or string"),
119            span,
120        )),
121    }
122}
123
124/// Bind parameters into a routing `ShardKey` in-place.
125pub fn bind_shard_key<F>(
126    key: &mut Option<ShardKey>,
127    lookup: &F,
128    positional: &[Value],
129) -> Result<(), QqlError>
130where
131    F: Fn(&str) -> Option<Value>,
132{
133    match key {
134        Some(ShardKey::Param(name, span)) => {
135            let val = resolve_param(name, span.as_deref().copied(), lookup)?;
136            *key = Some(value_to_shard_key(&val, span.as_deref().copied())?);
137        }
138        Some(ShardKey::PositionalParam(idx, span)) => {
139            let val = resolve_positional(*idx, span.as_deref().copied(), positional)?;
140            *key = Some(value_to_shard_key(&val, span.as_deref().copied())?);
141        }
142        _ => {}
143    }
144    Ok(())
145}
146
147/// Convert a bound `Value` into a `ShardKey`, failing closed on type mismatch.
148///
149/// Strings become keyword keys, non-negative integers numeric keys — the same
150/// split the parser enforces, so a bound tenant routes exactly like its
151/// literal spelling would.
152pub fn value_to_shard_key(val: &Value, span: Option<Span>) -> Result<ShardKey, QqlError> {
153    match val {
154        Value::Int(n) if *n >= 0 => Ok(ShardKey::Number(*n as u64)),
155        Value::UInt(n) => Ok(ShardKey::Number(*n)),
156        Value::Str(s) => Ok(ShardKey::Keyword(s.clone())),
157        _ => Err(QqlError::validation(
158            "QQL-BIND-TYPE-MISMATCH",
159            format!("cannot bind {val:?} as shard key: expected non-negative integer or string"),
160            span,
161        )),
162    }
163}
164
165/// Convert a bound `Value` into a `u64`, failing closed on type mismatch.
166pub fn value_to_u64(val: &Value, clause: &str, span: Option<Span>) -> Result<u64, QqlError> {
167    match val {
168        Value::Int(n) if *n >= 0 => Ok(*n as u64),
169        Value::UInt(n) => Ok(*n),
170        _ => Err(QqlError::validation(
171            "QQL-BIND-TYPE-MISMATCH",
172            format!("{clause} parameter must be a non-negative integer"),
173            span,
174        )),
175    }
176}
177
178/// Like `value_to_u64`, but rejects `0` for clauses requiring positive integers (e.g. `LIMIT`).
179pub fn value_to_positive_u64(
180    val: &Value,
181    clause: &str,
182    span: Option<Span>,
183) -> Result<u64, QqlError> {
184    let n = value_to_u64(val, clause, span)?;
185    if n == 0 {
186        return Err(QqlError::validation(
187            "QQL-BIND-TYPE-MISMATCH",
188            format!("{clause} parameter must be a positive integer"),
189            span,
190        ));
191    }
192    Ok(n)
193}
194
195/// Resolve a parameter string (`:name` or `?N`) to a `u64`.
196pub fn resolve_param_u64<F>(
197    param: &str,
198    span: Option<Span>,
199    lookup: &F,
200    positional: &[Value],
201    clause: &str,
202    positive: bool,
203) -> Result<u64, QqlError>
204where
205    F: Fn(&str) -> Option<Value>,
206{
207    let val = if let Some(name) = param.strip_prefix(':') {
208        resolve_param(name, span, lookup)?
209    } else if let Some(idx_str) = param.strip_prefix('?') {
210        let idx: usize = idx_str.parse().map_err(|_| {
211            QqlError::validation(
212                "QQL-BIND-TYPE-MISMATCH",
213                format!("invalid positional parameter reference in {clause}: {param}"),
214                span,
215            )
216        })?;
217        resolve_positional(idx, span, positional)?
218    } else {
219        resolve_param(param, span, lookup)?
220    };
221    if positive {
222        value_to_positive_u64(&val, clause, span)
223    } else {
224        value_to_u64(&val, clause, span)
225    }
226}