Substitute `$N` placeholders in DSL SQL text with concrete literals. Uses sqlparser's own tokenizer so that string literals, quoted identifiers, comments, and dollar-quoted strings are never accidentally rewritten — the tokenizer has already classified them.
(sql: &str, params: &[ParamValue])
| 73 | /// identifiers, comments, and dollar-quoted strings are never |
| 74 | /// accidentally rewritten — the tokenizer has already classified them. |
| 75 | pub fn bind_dsl(sql: &str, params: &[ParamValue]) -> Result<BoundDslSql> { |
| 76 | if params.is_empty() { |
| 77 | return Ok(BoundDslSql(sql.to_owned())); |
| 78 | } |
| 79 | let dialect = PostgreSqlDialect {}; |
| 80 | let tokens = Tokenizer::new(&dialect, sql) |
| 81 | .tokenize() |
| 82 | .map_err(|e| SqlError::Parse { |
| 83 | detail: format!("tokenize DSL for parameter binding: {e}"), |
| 84 | })?; |
| 85 | |
| 86 | let mut out = String::with_capacity(sql.len()); |
| 87 | for tok in &tokens { |
| 88 | if let Token::Placeholder(p) = tok |
| 89 | && p.starts_with('$') |
| 90 | { |
| 91 | // Every `$N` must resolve to a provided parameter. A |
| 92 | // silent pass-through here would let an out-of-range |
| 93 | // placeholder reach the engine as a raw `$N` literal — |
| 94 | // exactly the bug class this module exists to close. |
| 95 | let replacement = |
| 96 | placeholder_literal_token(p, params).ok_or_else(|| SqlError::Parse { |
| 97 | detail: format!( |
| 98 | "DSL parameter bind: placeholder {p} has no corresponding \ |
| 99 | parameter ({len} provided)", |
| 100 | len = params.len() |
| 101 | ), |
| 102 | })?; |
| 103 | out.push_str(&replacement.to_string()); |
| 104 | continue; |
| 105 | } |
| 106 | out.push_str(&tok.to_string()); |
| 107 | } |
| 108 | Ok(BoundDslSql(out)) |
| 109 | } |
| 110 | |
| 111 | fn placeholder_literal_token(placeholder: &str, params: &[ParamValue]) -> Option<Token> { |
| 112 | let idx_str = placeholder.strip_prefix('$')?; |