1use core::ops::ControlFlow;
23
24use sqlparser::ast::{Statement, Value, ValueWithSpan, VisitMut, VisitorMut};
25
26#[derive(Debug, Clone)]
30pub enum ParamValue {
31 Null,
32 Bool(bool),
33 Int64(i64),
34 Float64(f64),
35 Decimal(rust_decimal::Decimal),
37 Text(String),
38 Timestamp(nodedb_types::datetime::NdbDateTime),
40 Timestamptz(nodedb_types::datetime::NdbDateTime),
42}
43
44pub fn bind_params(stmt: &mut Statement, params: &[ParamValue]) {
46 if params.is_empty() {
47 return;
48 }
49 let mut binder = ParamBinder { params };
50 let _ = stmt.visit(&mut binder);
51}
52
53struct ParamBinder<'a> {
59 params: &'a [ParamValue],
60}
61
62impl VisitorMut for ParamBinder<'_> {
63 type Break = ();
64
65 fn pre_visit_value(&mut self, value: &mut ValueWithSpan) -> ControlFlow<Self::Break> {
66 if let Value::Placeholder(p) = &value.value
70 && let Some(v) = placeholder_to_value(p, self.params)
71 {
72 value.value = v;
73 }
74 ControlFlow::Continue(())
75 }
76}
77
78fn placeholder_to_value(placeholder: &str, params: &[ParamValue]) -> Option<Value> {
79 let idx_str = placeholder.strip_prefix('$')?;
80 let idx: usize = idx_str.parse().ok()?;
81 let param = params.get(idx.checked_sub(1)?)?;
82 Some(match param {
83 ParamValue::Null => Value::Null,
84 ParamValue::Bool(true) => Value::Boolean(true),
85 ParamValue::Bool(false) => Value::Boolean(false),
86 ParamValue::Int64(n) => Value::Number(n.to_string(), false),
87 ParamValue::Float64(f) => Value::Number(float_literal_text(*f), false),
88 ParamValue::Decimal(d) => Value::Number(d.to_string(), false),
89 ParamValue::Text(s) => Value::SingleQuotedString(s.clone()),
90 ParamValue::Timestamp(dt) => Value::SingleQuotedString(dt.to_iso8601()),
93 ParamValue::Timestamptz(dt) => Value::SingleQuotedString(dt.to_iso8601()),
94 })
95}
96
97pub(crate) fn float_literal_text(f: f64) -> String {
105 format!("{f:e}")
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::parser::statement::parse_sql;
112
113 fn bind_and_format(sql: &str, params: &[ParamValue]) -> String {
114 let mut stmts = parse_sql(sql).unwrap();
115 for stmt in &mut stmts {
116 bind_params(stmt, params);
117 }
118 stmts
119 .iter()
120 .map(|s| s.to_string())
121 .collect::<Vec<_>>()
122 .join("; ")
123 }
124
125 #[test]
126 fn bind_select_where() {
127 let result = bind_and_format(
128 "SELECT * FROM users WHERE id = $1",
129 &[ParamValue::Int64(42)],
130 );
131 assert!(result.contains("id = 42"), "got: {result}");
132 }
133
134 #[test]
135 fn bind_string_param() {
136 let result = bind_and_format(
137 "SELECT * FROM users WHERE name = $1",
138 &[ParamValue::Text("alice".into())],
139 );
140 assert!(result.contains("name = 'alice'"), "got: {result}");
141 }
142
143 #[test]
144 fn bind_null_param() {
145 let result = bind_and_format("SELECT * FROM users WHERE name = $1", &[ParamValue::Null]);
146 assert!(result.contains("name = NULL"), "got: {result}");
147 }
148
149 #[test]
150 fn bind_multiple_params() {
151 let result = bind_and_format(
152 "SELECT * FROM users WHERE age > $1 AND name = $2",
153 &[ParamValue::Int64(18), ParamValue::Text("bob".into())],
154 );
155 assert!(result.contains("age > 18"), "got: {result}");
156 assert!(result.contains("name = 'bob'"), "got: {result}");
157 }
158
159 #[test]
160 fn bind_insert_values() {
161 let result = bind_and_format(
162 "INSERT INTO users (id, name) VALUES ($1, $2)",
163 &[ParamValue::Int64(1), ParamValue::Text("eve".into())],
164 );
165 assert!(result.contains("1, 'eve'"), "got: {result}");
166 }
167
168 #[test]
169 fn bind_bool_param() {
170 let result = bind_and_format(
171 "SELECT * FROM users WHERE active = $1",
172 &[ParamValue::Bool(true)],
173 );
174 assert!(result.contains("active = true"), "got: {result}");
175 }
176
177 #[test]
178 fn no_params_noop() {
179 let result = bind_and_format("SELECT 1", &[]);
180 assert!(result.contains("SELECT 1"));
181 }
182
183 #[test]
184 fn bind_cte_body_placeholders() {
185 let result = bind_and_format(
186 "WITH x AS (SELECT $1 AS v) SELECT v FROM x",
187 &[ParamValue::Int64(42)],
188 );
189 assert!(
190 result.contains("SELECT 42"),
191 "CTE body placeholder not substituted: {result}"
192 );
193 assert!(!result.contains("$1"), "placeholder survived: {result}");
194 }
195
196 #[test]
197 fn bind_recursive_cte_placeholders() {
198 let result = bind_and_format(
199 "WITH RECURSIVE chain AS (SELECT $1 AS id UNION ALL SELECT chain.id + 1 FROM chain WHERE chain.id < $2) SELECT * FROM chain",
200 &[ParamValue::Int64(1), ParamValue::Int64(10)],
201 );
202 assert!(
203 !result.contains("$1") && !result.contains("$2"),
204 "got: {result}"
205 );
206 }
207
208 #[test]
209 fn bind_array_elements() {
210 let result = bind_and_format(
211 "SELECT ARRAY[$1, $2, $3]",
212 &[
213 ParamValue::Int64(1),
214 ParamValue::Int64(2),
215 ParamValue::Int64(3),
216 ],
217 );
218 assert!(
219 !result.contains("$1"),
220 "array placeholder survived: {result}"
221 );
222 assert!(
223 result.contains('1') && result.contains('2') && result.contains('3'),
224 "got: {result}"
225 );
226 }
227
228 #[test]
229 fn bind_any_op_rhs() {
230 let result = bind_and_format(
231 "SELECT * FROM t WHERE id = ANY($1)",
232 &[ParamValue::Text("{a,b}".into())],
233 );
234 assert!(!result.contains("$1"), "got: {result}");
235 }
236
237 #[test]
238 fn bind_all_op_rhs() {
239 let result = bind_and_format(
240 "SELECT * FROM t WHERE id = ALL($1)",
241 &[ParamValue::Text("{a,b}".into())],
242 );
243 assert!(!result.contains("$1"), "got: {result}");
244 }
245
246 #[test]
247 fn bind_update_returning() {
248 let result = bind_and_format(
249 "UPDATE t SET n = 1 WHERE id = $1 RETURNING $2 AS tag",
250 &[ParamValue::Int64(7), ParamValue::Text("note".into())],
251 );
252 assert!(result.contains("id = 7"), "got: {result}");
253 assert!(result.contains("'note'"), "got: {result}");
254 assert!(!result.contains("$2"), "got: {result}");
255 }
256
257 #[test]
258 fn bind_delete_returning() {
259 let result = bind_and_format(
260 "DELETE FROM t WHERE id = $1 RETURNING $2 AS tag",
261 &[ParamValue::Int64(3), ParamValue::Text("gone".into())],
262 );
263 assert!(
264 result.contains("id = 3") && result.contains("'gone'"),
265 "got: {result}"
266 );
267 assert!(!result.contains("$2"), "got: {result}");
268 }
269
270 #[test]
271 fn bind_update_limit() {
272 let result = bind_and_format(
273 "UPDATE t SET n = 1 WHERE id > 0 LIMIT $1",
274 &[ParamValue::Int64(5)],
275 );
276 assert!(!result.contains("$1"), "got: {result}");
277 }
278
279 #[test]
280 fn bind_interval_value_placeholder() {
281 let result = bind_and_format(
282 "SELECT now() - INTERVAL $1",
283 &[ParamValue::Text("1 day".into())],
284 );
285 assert!(!result.contains("$1"), "got: {result}");
286 }
287
288 #[test]
289 fn bind_update_from_subquery() {
290 let result = bind_and_format(
291 "UPDATE t SET n = s.y FROM (SELECT $1 AS y) s WHERE t.id = s.y",
292 &[ParamValue::Int64(7)],
293 );
294 assert!(!result.contains("$1"), "got: {result}");
295 }
296
297 #[test]
298 fn bind_insert_on_conflict_update_placeholder() {
299 let result = bind_and_format(
300 "INSERT INTO t (id, n) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET n = $3",
301 &[
302 ParamValue::Int64(1),
303 ParamValue::Int64(2),
304 ParamValue::Int64(99),
305 ],
306 );
307 assert!(!result.contains("$3"), "got: {result}");
308 }
309
310 #[test]
311 fn bind_window_partition_by_placeholder() {
312 let result = bind_and_format(
313 "SELECT LAG(x) OVER (PARTITION BY $1 ORDER BY b) FROM t",
314 &[ParamValue::Text("user_id".into())],
315 );
316 assert!(!result.contains("$1"), "got: {result}");
317 }
318
319 #[test]
322 fn a_float_param_resolves_to_a_float() {
323 use crate::resolver::expr::convert_value;
324 use crate::types::SqlValue;
325 for f in [1.5, 2.0, 0.1, -3.25, 1e300, 1e-300, f64::INFINITY] {
326 let literal = placeholder_to_value("$1", &[ParamValue::Float64(f)])
327 .expect("the placeholder names the one parameter");
328 assert_eq!(
329 convert_value(&literal).expect("a float literal resolves"),
330 SqlValue::Float(f),
331 "{f}"
332 );
333 }
334 let literal = placeholder_to_value("$1", &[ParamValue::Float64(f64::NAN)])
335 .expect("the placeholder names the one parameter");
336 assert!(
337 matches!(convert_value(&literal), Ok(SqlValue::Float(f)) if f.is_nan()),
338 "{literal}"
339 );
340 }
341
342 #[test]
343 fn bind_window_order_by_placeholder() {
344 let result = bind_and_format(
345 "SELECT LAG(x) OVER (PARTITION BY a ORDER BY $1) FROM t",
346 &[ParamValue::Text("ts".into())],
347 );
348 assert!(!result.contains("$1"), "got: {result}");
349 }
350}