1use super::core::Value;
6use crate::quote_literal;
7
8impl Value {
9 pub fn to_sql_literal(&self) -> String {
14 match self {
15 Value::Null => "NULL".into(),
16 Value::Bool(value) => if *value { "TRUE" } else { "FALSE" }.into(),
17 Value::Integer(value) => value.to_string(),
18 Value::Float(value) => finite_float_literal(*value),
19 Value::String(value)
20 | Value::Uuid(value)
21 | Value::Ulid(value)
22 | Value::Regex(value) => quote_literal(value),
23 Value::Bytes(value) => quote_literal(&format!("\\x{}", hex::encode(value))),
24 Value::Array(values) | Value::Set(values) => array_literal(values),
25 Value::Object(values) => object_literal(values),
26 Value::DateTime(value) | Value::NaiveDateTime(value) => {
27 quote_literal(&value.to_string())
28 }
29 Value::Duration(value) => quote_literal(&value.to_string()),
30 Value::Decimal(value) => value.to_string(),
31 Value::Geometry(value) => quote_literal(&format!("{value:?}")),
34 Value::Range {
35 start,
36 end,
37 inclusive,
38 } => {
39 let start = start
40 .as_deref()
41 .map_or_else(|| "unbounded".into(), Value::to_sql_literal);
42 let end = end
43 .as_deref()
44 .map_or_else(|| "unbounded".into(), Value::to_sql_literal);
45 quote_literal(&format!(
46 "{start}{}{end}",
47 if *inclusive { "..=" } else { ".." }
48 ))
49 }
50 Value::Record { table, id } => quote_literal(&format!("{table}:{id}")),
51 Value::ArrayCell(cell) => {
52 let system_time = cell
53 .system_time
54 .map_or_else(|| "NULL".into(), |value| value.to_string());
55 quote_literal(&format!(
56 "ArrayCell(coords={}, attrs={}, system_time={system_time})",
57 array_literal(&cell.coords),
58 array_literal(&cell.attrs)
59 ))
60 }
61 Value::Vector(values) => {
62 let elements = values
63 .iter()
64 .map(|value| finite_float_literal(f64::from(*value)))
65 .collect::<Vec<_>>();
66 format!("ARRAY[{}]", elements.join(", "))
67 }
68 }
69 }
70}
71
72fn finite_float_literal(value: f64) -> String {
73 if value.is_finite() {
74 value.to_string()
75 } else {
76 quote_literal(&value.to_string())
77 }
78}
79
80fn array_literal(values: &[Value]) -> String {
81 let values = values.iter().map(Value::to_sql_literal).collect::<Vec<_>>();
82 format!("ARRAY[{}]", values.join(", "))
83}
84
85fn object_literal(values: &std::collections::HashMap<String, Value>) -> String {
86 let mut entries = values.iter().collect::<Vec<_>>();
87 entries.sort_unstable_by_key(|(key, _)| *key);
88 let entries = entries
89 .into_iter()
90 .map(|(key, value)| format!("{key}: {}", value.to_sql_literal()))
91 .collect::<Vec<_>>();
92 quote_literal(&format!("{{{}}}", entries.join(", ")))
93}
94
95#[cfg(test)]
96mod tests {
97 use std::collections::HashMap;
98 use std::sync::Arc;
99
100 use super::*;
101 use crate::array_cell::ArrayCell;
102 use crate::geometry::Geometry;
103
104 #[test]
105 fn renders_every_value_variant_without_null_fallback() {
106 let values = vec![
107 Value::Null,
108 Value::Bool(true),
109 Value::Integer(-7),
110 Value::Float(1.5),
111 Value::String("O'Reilly\n雪".into()),
112 Value::Bytes(vec![0, 0xff]),
113 Value::Array(vec![Value::Integer(1)]),
114 Value::Object(HashMap::from([(
115 "key".into(),
116 Value::String("value".into()),
117 )])),
118 Value::Uuid("id'quoted".into()),
119 Value::Ulid("ulid'quoted".into()),
120 Value::DateTime(crate::datetime::NdbDateTime::from_micros(0)),
121 Value::NaiveDateTime(crate::datetime::NdbDateTime::from_micros(0)),
122 Value::Duration(crate::datetime::NdbDuration::from_micros(1)),
123 Value::Decimal(rust_decimal::Decimal::new(125, 2)),
124 Value::Geometry(Geometry::point(1.0, 2.0)),
125 Value::Set(vec![Value::Bool(false)]),
126 Value::Regex("a'b".into()),
127 Value::Range {
128 start: Some(Box::new(Value::Integer(1))),
129 end: None,
130 inclusive: true,
131 },
132 Value::Record {
133 table: "ta'ble".into(),
134 id: "id'雪".into(),
135 },
136 Value::ArrayCell(ArrayCell {
137 coords: vec![Value::Integer(1)],
138 attrs: vec![Value::String("a'b".into())],
139 system_time: Some(7),
140 }),
141 Value::Vector(Arc::from([1.0_f32, 2.0_f32])),
142 ];
143 for value in values {
144 let literal = value.to_sql_literal();
145 if !matches!(value, Value::Null) {
146 assert_ne!(
147 literal, "NULL",
148 "non-null variant must not fall back to NULL: {value:?}"
149 );
150 }
151 }
152 }
153
154 #[test]
155 fn string_derived_values_use_shared_quote_literal() {
156 let value = Value::Record {
157 table: "t'\n雪".into(),
158 id: "i'\r".into(),
159 };
160 assert_eq!(
161 Value::String("t'\n雪".into()).to_sql_literal(),
162 quote_literal("t'\n雪")
163 );
164 assert_eq!(value.to_sql_literal(), quote_literal("t'\n雪:i'\r"));
165 assert_eq!(
166 Value::Bytes(vec![0xab]).to_sql_literal(),
167 quote_literal("\\xab")
168 );
169 }
170
171 #[test]
172 fn nonfinite_floats_are_quoted_and_nested_object_is_sorted() {
173 assert_eq!(
174 Value::Float(f64::NAN).to_sql_literal(),
175 quote_literal("NaN")
176 );
177 assert_eq!(
178 Value::Float(f64::INFINITY).to_sql_literal(),
179 quote_literal("inf")
180 );
181 let object = Value::Object(HashMap::from([
182 ("z'key".into(), Value::Float(f64::NEG_INFINITY)),
183 ("a".into(), Value::Array(vec![Value::String("o'k".into())])),
184 ]));
185 assert_eq!(
186 object.to_sql_literal(),
187 quote_literal("{a: ARRAY['o''k'], z'key: '-inf'}")
188 );
189 }
190
191 #[test]
192 fn array_cell_nested_objects_are_deterministic() {
193 let left = HashMap::from([
194 ("z".to_string(), Value::Integer(2)),
195 ("a".to_string(), Value::String("x'y".into())),
196 ]);
197 let right = HashMap::from([
198 ("a".to_string(), Value::String("x'y".into())),
199 ("z".to_string(), Value::Integer(2)),
200 ]);
201 let render = |object| {
202 Value::ArrayCell(ArrayCell {
203 coords: vec![Value::Object(object)],
204 attrs: Vec::new(),
205 system_time: None,
206 })
207 .to_sql_literal()
208 };
209 assert_eq!(render(left), render(right));
210 }
211
212 #[test]
213 fn geometry_and_ranges_have_explicit_safe_representations() {
214 assert_eq!(
215 Value::Geometry(Geometry::point(1.0, 2.0)).to_sql_literal(),
216 quote_literal("Point { coordinates: [1.0, 2.0] }")
217 );
218 assert_eq!(
219 Value::Range {
220 start: Some(Box::new(Value::String("a'b".into()))),
221 end: Some(Box::new(Value::Integer(2))),
222 inclusive: false
223 }
224 .to_sql_literal(),
225 quote_literal("'a''b'..2")
226 );
227 }
228}