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}