1use crate::ast::Value;
4use crate::ast::statement::{PointId, ShardKey};
5use crate::error::{QqlError, Span};
6use alloc::format;
7
8pub 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
32pub 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
62pub 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
91pub 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
110pub 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
124pub 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
147pub 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
165pub 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
178pub 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
195pub 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}