Skip to main content

ferrule_sql/
render.rs

1//! SQL-literal rendering helpers for inline value substitution.
2//!
3//! These functions turn a neutral [`Value`] into a backend-appropriate
4//! SQL literal that can be spliced directly into a statement string.
5//! They are the lowest layer of the parameter-substitution and
6//! write-path machinery: callers higher up (parameter substitution in
7//! `ferrule-core`, the `copy` write path here) compose them into full
8//! statements. The rendering is purely string-building and performs no
9//! I/O, so it is synchronous and allocation-bounded by the input value.
10
11use crate::backend::Backend;
12use crate::value::Value;
13
14/// Render a `Value` into a SQL literal suitable for inline substitution.
15///
16/// Backend-aware only for booleans, where dialects diverge (Oracle has
17/// no native boolean and uses `1`/`0`; the rest use `TRUE`/`FALSE`).
18/// Numeric and decimal values pass through unquoted; strings and the
19/// catch-all `other` arm are single-quote-escaped via [`quote_string`];
20/// raw bytes render as a backend-portable `X'..'` hex literal.
21pub fn render_value(value: &Value, backend: Backend) -> String {
22    match value {
23        Value::Null => "NULL".to_string(),
24        Value::Bool(b) => match backend {
25            #[cfg(feature = "oracle")]
26            Backend::Oracle => {
27                if *b {
28                    "1".to_string()
29                } else {
30                    "0".to_string()
31                }
32            }
33            #[cfg(feature = "postgres")]
34            Backend::Postgres => bool_literal(*b),
35            #[cfg(feature = "mysql")]
36            Backend::MySql => bool_literal(*b),
37            #[cfg(feature = "mssql")]
38            Backend::MsSql => bool_literal(*b),
39            #[cfg(feature = "sqlite")]
40            Backend::Sqlite => bool_literal(*b),
41        },
42        Value::Int64(i) => i.to_string(),
43        Value::Float64(f) => f.to_string(),
44        Value::Decimal(d) => d.clone(),
45        Value::String(s) => quote_string(s),
46        Value::Bytes(_b) => {
47            // Bytes in parameter substitution are rare; render as a hex literal.
48            // This is best-effort and backend-specific.
49            let hex: String = _b.iter().map(|b| format!("{:02x}", b)).collect();
50            format!("X'{}'", hex)
51        }
52        other => quote_string(&other.to_string()),
53    }
54}
55
56/// Render a boolean as an ANSI SQL literal (`TRUE` / `FALSE`).
57///
58/// Private because Oracle does not use it (it renders `1`/`0` in
59/// [`render_value`]); kept narrow to the dialects that accept the
60/// keyword form.
61fn bool_literal(b: bool) -> String {
62    if b {
63        "TRUE".to_string()
64    } else {
65        "FALSE".to_string()
66    }
67}
68
69/// Quote a string for SQL: wrap in single quotes, escape `'` as `''`.
70///
71/// This is the SQL-standard single-quote escaping accepted by every
72/// supported backend; it guards against injection through string
73/// values when building literals inline rather than via bind
74/// parameters.
75pub fn quote_string(v: &str) -> String {
76    let mut out = String::with_capacity(v.len() + 2);
77    out.push('\'');
78    for ch in v.chars() {
79        if ch == '\'' {
80            out.push('\'');
81            out.push('\'');
82        } else {
83            out.push(ch);
84        }
85    }
86    out.push('\'');
87    out
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_quote_escape() {
96        assert_eq!(quote_string("O'Brien"), "'O''Brien'");
97        assert_eq!(quote_string("hello"), "'hello'");
98        assert_eq!(quote_string("it's a test"), "'it''s a test'");
99    }
100}