Skip to main content

surrealdb_types/
sql.rs

1//! SQL utilities.
2
3use std::sync::Arc;
4
5pub use surrealdb_types_derive::write_sql;
6
7use crate as surrealdb_types;
8use crate::utils::escape::QuoteStr;
9
10/// Trait for types that can be converted to SQL representation.
11///
12/// ⚠️ **EXPERIMENTAL**: This trait is not stable and may change
13/// or be removed in any release without a major version bump.
14/// Use at your own risk.
15///
16/// There's an important distinction between this trait and `Display`.
17/// `Display` should be used for human-readable output, it does not particularly
18/// need to be SQL compatible but it may happen to be.
19/// `ToSql` should be used for SQL compatible output.
20///
21/// Example usage:
22/// ```rust
23/// use surrealdb_types::ToSql;
24/// use surrealdb_types::Datetime;
25/// use chrono::{TimeZone, Utc};
26/// let dt = Utc.with_ymd_and_hms(2025, 10, 3, 10, 2, 32).unwrap() + chrono::Duration::microseconds(873077);
27/// let datetime = Datetime::from(dt);
28/// assert_eq!(datetime.to_string(), "2025-10-03T10:02:32.873077Z");
29/// assert_eq!(datetime.to_sql(), "d'2025-10-03T10:02:32.873077Z'");
30/// ```
31pub trait ToSql {
32	/// Convert the type to a SQL string.
33	fn to_sql(&self) -> String {
34		let mut f = String::new();
35		self.fmt_sql(&mut f, SqlFormat::SingleLine);
36		f
37	}
38
39	/// Convert the type to a pretty-printed SQL string with indentation.
40	fn to_sql_pretty(&self) -> String {
41		let mut f = String::new();
42		self.fmt_sql(&mut f, SqlFormat::Indented(0));
43		f
44	}
45
46	/// Format the type to a SQL string.
47	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat);
48}
49
50/// SQL formatting mode for pretty printing.
51#[derive(Debug, Clone, Copy)]
52pub enum SqlFormat {
53	/// Single line formatting.
54	SingleLine,
55	/// Indented by the number of tabs specified.
56	Indented(u8),
57}
58
59impl SqlFormat {
60	/// Returns true if this is pretty (indented) formatting.
61	pub fn is_pretty(&self) -> bool {
62		matches!(self, SqlFormat::Indented(_))
63	}
64
65	/// Increments the indentation level.
66	pub fn increment(&self) -> Self {
67		match self {
68			SqlFormat::SingleLine => SqlFormat::SingleLine,
69			SqlFormat::Indented(level) => SqlFormat::Indented(level.saturating_add(1)),
70		}
71	}
72
73	/// Writes indentation to the string.
74	pub fn write_indent(&self, f: &mut String) {
75		if let SqlFormat::Indented(level) = self {
76			for _ in 0..*level {
77				f.push('\t');
78			}
79		}
80	}
81
82	/// Writes a separator (comma + space or comma + newline + indent).
83	pub fn write_separator(&self, f: &mut String) {
84		match self {
85			SqlFormat::SingleLine => f.push_str(", "),
86			SqlFormat::Indented(_) => {
87				f.push(',');
88				f.push('\n');
89				self.write_indent(f);
90			}
91		}
92	}
93}
94
95/// Formats a slice of items that implement ToSql with comma separation.
96pub fn fmt_sql_comma_separated<T: ToSql>(items: &[T], f: &mut String, fmt: SqlFormat) {
97	if fmt.is_pretty() && !items.is_empty() {
98		f.push('\n');
99		fmt.write_indent(f);
100	}
101	for (i, item) in items.iter().enumerate() {
102		if i > 0 {
103			fmt.write_separator(f);
104		}
105		item.fmt_sql(f, fmt);
106	}
107	if fmt.is_pretty() && !items.is_empty() {
108		f.push('\n');
109		// Write one level less indentation for the closing bracket
110		if let SqlFormat::Indented(level) = fmt
111			&& level > 0
112		{
113			for _ in 0..(level - 1) {
114				f.push('\t');
115			}
116		}
117	}
118}
119
120/// Formats key-value pairs with comma separation.
121pub fn fmt_sql_key_value<'a, V: ToSql + 'a>(
122	pairs: impl IntoIterator<Item = (impl AsRef<str>, &'a V)>,
123	f: &mut String,
124	fmt: SqlFormat,
125) {
126	use crate::utils::escape::EscapeObjectKey;
127
128	let pairs: Vec<_> = pairs.into_iter().collect();
129
130	if fmt.is_pretty() && !pairs.is_empty() {
131		f.push('\n');
132		fmt.write_indent(f);
133	}
134	for (i, (key, value)) in pairs.iter().enumerate() {
135		if i > 0 {
136			fmt.write_separator(f);
137		}
138		write_sql!(f, fmt, "{}: {}", EscapeObjectKey(key.as_ref()), value);
139	}
140	if fmt.is_pretty() && !pairs.is_empty() {
141		f.push('\n');
142		// Write one level less indentation for the closing bracket
143		if let SqlFormat::Indented(level) = fmt
144			&& level > 0
145		{
146			for _ in 0..(level - 1) {
147				f.push('\t');
148			}
149		}
150	}
151}
152
153impl ToSql for String {
154	#[inline]
155	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
156		f.push_str(self.as_str());
157	}
158}
159
160impl ToSql for str {
161	#[inline]
162	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
163		f.push_str(self);
164	}
165}
166
167impl ToSql for &str {
168	#[inline]
169	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
170		f.push_str(self);
171	}
172}
173
174impl ToSql for char {
175	#[inline]
176	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
177		f.push(*self);
178	}
179}
180
181impl ToSql for bool {
182	#[inline]
183	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
184		f.push_str(if *self {
185			"true"
186		} else {
187			"false"
188		})
189	}
190}
191
192macro_rules! impl_to_sql_for_numeric {
193	($($t:ty),+) => {
194		$(
195			impl ToSql for $t {
196				#[inline]
197				fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
198					f.push_str(&self.to_string())
199				}
200			}
201		)+
202	};
203}
204
205impl_to_sql_for_numeric!(u8, u16, u32, u64, i8, i16, i32, i64, usize, isize, f32, f64);
206
207impl<T: ToSql> ToSql for &T {
208	#[inline]
209	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
210		(**self).fmt_sql(f, fmt)
211	}
212}
213
214// Blanket impl for Box
215impl<T: ToSql + ?Sized> ToSql for Box<T> {
216	#[inline]
217	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
218		(**self).fmt_sql(f, fmt)
219	}
220}
221
222// Blanket impl for Arc
223impl<T: ToSql + ?Sized> ToSql for Arc<T> {
224	#[inline]
225	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
226		(**self).fmt_sql(f, fmt)
227	}
228}
229
230impl ToSql for uuid::Uuid {
231	#[inline]
232	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
233		f.push('u');
234		QuoteStr(&self.to_string()).fmt_sql(f, fmt);
235	}
236}
237
238impl ToSql for rust_decimal::Decimal {
239	#[inline]
240	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
241		self.to_string().fmt_sql(f, fmt);
242		f.push_str("dec");
243	}
244}