format_sql_query/
lib.rs

1/*!
2Collection of types and helpers for building hopefully correctly escaped SQL queries.
3
4Example usage
5=============
6
7```rust
8use format_sql_query::*;
9
10println!("SELECT {} FROM {} WHERE {} = {}", Column("foo bar".into()), SchemaTable("foo".into(), "baz".into()), Column("blah".into()), QuotedData("hello 'world' foo"));
11// SELECT "foo bar" FROM foo.baz WHERE blah = 'hello ''world'' foo'
12```
13
14Design goals
15============
16
17* All objects will implement `Display` to get escaped and perhaps quoted formatting that can be used directly in SQL statements.
18* Avoid allocations by making most types just wrappers around string slices.
19* New-type patter that is used to construct an object out of strings and other objects.
20* Generous `From` trait implementations to make it easy to construct objects from strings.
21* All single field new-type objects will implement `.as_str()` to get original value.
22* Types that are string slice wrappers implement `Copy` to make them easy to use.
23* Types should implement `Eq` and `Ord`.
24* New-type objects with more than one filed should have getters.
25* When returning types make sure they don't reference self but the original string slice lifetime.
26
27All objects are using base escaping rules wrappers:
28
29* `ObjectConcat` for table names, schemas, columns etc.
30* `QuotedDataConcat` for data values
31*/
32use itertools::Itertools;
33use std::fmt::{self, Display};
34use std::marker::PhantomData;
35
36mod predicates;
37pub use predicates::*;
38mod data_type;
39pub use data_type::*;
40
41/// Concatenation of strings with object escaping rules.
42///
43/// Escaping rules:
44/// * as-is, if does not contain " or space
45/// * surround " and escape " with ""
46#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
47pub struct ObjectConcat<'i>(pub &'i [&'i str]);
48
49impl fmt::Display for ObjectConcat<'_> {
50    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51        if self.0.iter().any(|o| o.contains("'") || o.contains("\\")) {
52            // MonetDB does not like ' or \ in column names
53            return Err(fmt::Error);
54        }
55
56        if self.0.iter().any(|o| o.contains(" ") || o.contains("\"")) {
57            f.write_str("\"")?;
58            for part in self.0.iter().flat_map(|o| o.split("\"").intersperse("\"\"")) {
59                f.write_str(part)?;
60            }
61            f.write_str("\"")?;
62        } else {
63            for o in self.0.iter() {
64                f.write_str(o)?;
65            }
66        }
67        Ok(())
68    }
69}
70
71//TODO: reimplement using const generics when stable
72/// Owned variant of `ObjectConcat` to be returned as `impl Display`.
73pub struct ObjectConcatDisplay<'i>(Box<[&'i str]>);
74
75impl<'i> ObjectConcatDisplay<'i> {
76    pub fn as_quoted_data(self) -> QuotedDataConcatDisplay<'i> {
77        self.into()
78    }
79}
80
81impl fmt::Display for ObjectConcatDisplay<'_> {
82    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83        ObjectConcat(&self.0).fmt(f)
84    }
85}
86
87/// Concatenation of strings with quoted data escaping rules.
88///
89/// Escaping rules:
90/// * put in ' and escape ' with ''
91/// * escape / with //
92#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
93pub struct QuotedDataConcat<'i>(pub &'i [&'i str]);
94
95impl fmt::Display for QuotedDataConcat<'_> {
96    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97        f.write_str("'")?;
98        for part in self.0.iter().flat_map(|o| o.split("'").intersperse("''")) {
99            for part in part.split("\\").intersperse("\\\\") {
100                f.write_str(part)?;
101            }
102        }
103        f.write_str("'")?;
104        Ok(())
105    }
106}
107
108//TODO: reimplement using const generics when stable
109/// Owned variant of `QuotedDataConcat` to be returned as `impl Display`.
110pub struct QuotedDataConcatDisplay<'i>(Box<[&'i str]>);
111
112impl fmt::Display for QuotedDataConcatDisplay<'_> {
113    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114        QuotedDataConcat(&self.0).fmt(f)
115    }
116}
117
118impl<'i> From<ObjectConcatDisplay<'i>> for QuotedDataConcatDisplay<'i> {
119    fn from(v: ObjectConcatDisplay<'i>) -> QuotedDataConcatDisplay<'i> {
120        QuotedDataConcatDisplay(v.0)
121    }
122}
123
124/// Generic object like table, schema, column etc. based `ObjectConcat` escaping rules.
125#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
126pub struct Object<'i>(pub &'i str);
127
128impl<'i> Object<'i> {
129    /// Gets original value.
130    pub fn as_str(&self) -> &'i str {
131        self.0
132    }
133
134    /// Gets object represented as quoted data.
135    pub fn as_quoted_data(&self) -> QuotedDataConcatDisplay<'i> {
136        QuotedDataConcatDisplay(Box::new([self.as_str()]))
137    }
138}
139
140impl<'i> From<&'i str> for Object<'i> {
141    fn from(value: &'i str) -> Object<'i> {
142        Object(value.into())
143    }
144}
145
146impl fmt::Display for Object<'_> {
147    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148        ObjectConcat(&[self.0]).fmt(f)
149    }
150}
151
152/// Strings and other data in single quotes.
153#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
154pub struct QuotedData<'i>(pub &'i str);
155
156impl<'i> From<&'i str> for QuotedData<'i> {
157    fn from(value: &'i str) -> QuotedData<'i> {
158        QuotedData(value)
159    }
160}
161
162impl<'i> QuotedData<'i> {
163    pub fn map<F>(self, f: F) -> MapQuotedData<'i, F>
164    where
165        F: Fn(&'i str) -> String,
166    {
167        MapQuotedData(self.0, f)
168    }
169
170    /// Gets original value.
171    pub fn as_str(&self) -> &'i str {
172        self.0
173    }
174}
175
176impl fmt::Display for QuotedData<'_> {
177    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178        QuotedDataConcat(&[self.0]).fmt(f)
179    }
180}
181
182/// Wrapper around `QuotedData` that maps its content.
183pub struct MapQuotedData<'i, F>(&'i str, F);
184
185impl<'i, F> fmt::Display for MapQuotedData<'i, F>
186where
187    F: Fn(&'i str) -> String,
188{
189    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
190        let data = self.1(self.0);
191        QuotedData(&data).fmt(f)
192    }
193}
194
195/// Represents database schema name.
196#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
197pub struct Schema<'i>(pub Object<'i>);
198
199impl<'i> Schema<'i> {
200    /// Gets original value.
201    pub fn as_str(&self) -> &'i str {
202        self.0.as_str()
203    }
204
205    /// Gets object represented as quoted data.
206    pub fn as_quoted_data(&self) -> QuotedDataConcatDisplay<'i> {
207        self.0.as_quoted_data()
208    }
209}
210
211impl<'i, O: Into<Object<'i>> > From<O> for Schema<'i> {
212    fn from(value: O) -> Schema<'i> {
213        Schema(value.into())
214    }
215}
216
217impl fmt::Display for Schema<'_> {
218    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
219        self.0.fmt(f)
220    }
221}
222
223/// Represents database table name.
224#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
225pub struct Table<'i>(pub Object<'i>);
226
227impl<'i> Table<'i> {
228    /// Constructs `SchemaTable` from this table and given schema.
229    pub fn with_schema(self, schema: impl Into<Schema<'i>>) -> SchemaTable<'i> {
230        SchemaTable(schema.into(), self)
231    }
232
233    /// Returns object implementing `Display` to format this table name with given postfix.
234    pub fn with_postfix(&self, postfix: &'i str) -> ObjectConcatDisplay<'i> {
235        ObjectConcatDisplay(Box::new([self.as_str(), postfix]))
236    }
237
238    /// Returns object implementing `Display` to format this table name with given postfix
239    /// separated with given separator.
240    pub fn with_postfix_sep(&self, postfix: &'i str, separator: &'i str) -> ObjectConcatDisplay<'i> {
241        ObjectConcatDisplay(Box::new([self.as_str(), separator, postfix]))
242    }
243
244    /// Gets original value.
245    pub fn as_str(&self) -> &'i str {
246        self.0.as_str()
247    }
248
249    /// Gets object represented as quoted data.
250    pub fn as_quoted_data(&self) -> QuotedDataConcatDisplay<'i> {
251        self.0.as_quoted_data()
252    }
253}
254
255impl<'i, O: Into<Object<'i>> > From<O> for Table<'i> {
256    fn from(table: O) -> Table<'i> {
257        Table(table.into())
258    }
259}
260
261impl fmt::Display for Table<'_> {
262    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263        self.0.fmt(f)
264    }
265}
266
267/// Represents table name in a schema.
268#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
269pub struct SchemaTable<'i>(pub Schema<'i>, pub Table<'i>);
270
271impl<'i> SchemaTable<'i> {
272    /// Gets `Schema` part
273    pub fn schema(&self) -> Schema<'i> {
274        self.0
275    }
276
277    /// Gets `Table` part
278    pub fn table(&self) -> Table<'i> {
279        self.1
280    }
281
282    fn as_array(&self) -> [&'i str; 3] {
283        [self.0.as_str(), ".", self.1.as_str()]
284    }
285
286    /// Returns object implementing `Display` to format this table name with given postfix.
287    pub fn with_postfix(&self, postfix: &'i str) -> impl Display + 'i {
288        let a = self.as_array();
289        ObjectConcatDisplay(Box::new([a[0], a[1], a[2], postfix]))
290    }
291
292    /// Returns object implementing `Display` to format this table name with given postfix
293    /// separated with given separator.
294    pub fn with_postfix_sep(&self, postfix: &'i str, separator: &'i str) -> ObjectConcatDisplay<'i> {
295        let a = self.as_array();
296        ObjectConcatDisplay(Box::new([a[0], a[1], a[2], separator, postfix]))
297    }
298
299    /// Gets object represented as quoted data.
300    pub fn as_quoted_data(&self) -> QuotedDataConcatDisplay<'i> {
301        QuotedDataConcatDisplay(Box::new(self.as_array()))
302    }
303}
304
305impl<'i, S: Into<Schema<'i>>, T: Into<Table<'i>>> From<(S, T)> for SchemaTable<'i> {
306    fn from((schema, table): (S, T)) -> SchemaTable<'i> {
307        SchemaTable(schema.into(), table.into())
308    }
309}
310
311impl fmt::Display for SchemaTable<'_> {
312    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
313        ObjectConcat(&self.as_array()).fmt(f)
314    }
315}
316
317/// Represents table column name.
318#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
319pub struct Column<'i>(pub Object<'i>);
320
321impl<'i> Column<'i> {
322    /// Gets original value.
323    pub fn as_str(&self) -> &'i str {
324        self.0.as_str()
325    }
326
327    /// Gets object represented as quoted data.
328    pub fn as_quoted_data(&self) -> QuotedDataConcatDisplay<'i> {
329        self.0.as_quoted_data()
330    }
331}
332
333impl<'i, O: Into<Object<'i>>> From<O> for Column<'i> {
334    fn from(value: O) -> Column<'i> {
335        Column(value.into())
336    }
337}
338
339impl fmt::Display for Column<'_> {
340    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
341        self.0.fmt(f)
342    }
343}
344
345/// Represents column type for given SQL `Dialect`.
346#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
347pub struct ColumnType<D: Dialect>(pub Object<'static>, pub PhantomData<D>);
348
349impl<D: Dialect> ColumnType<D> {
350    /// Gets original value.
351    pub fn as_str(&self) -> &'static str {
352        self.0.as_str()
353    }
354}
355
356impl<D, O: Into<Object<'static>>> From<O> for ColumnType<D> where D: Dialect {
357    fn from(column_type: O) -> ColumnType<D> {
358        ColumnType(column_type.into(), PhantomData)
359    }
360}
361
362impl<D: Dialect> fmt::Display for ColumnType<D> {
363    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
364        self.0.fmt(f)
365    }
366}
367
368/// Represents column name and type for given SQL `Dialect`.
369#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
370pub struct ColumnSchema<'i, D: Dialect>(pub Column<'i>, pub ColumnType<D>);
371
372impl<'i, D: Dialect> ColumnSchema<'i, D> {
373    /// Gets `Column` part
374    pub fn column(&self) -> &Column<'i> {
375        &self.0
376    }
377
378    /// Gets `ColumnType` part
379    pub fn column_type(&self) -> &ColumnType<D> {
380        &self.1
381    }
382}
383
384impl<'i, D: Dialect, C: Into<Column<'i>>, T: Into<ColumnType<D>>> From<(C, T)> for ColumnSchema<'i, D> {
385    fn from((name, r#type): (C, T)) -> ColumnSchema<'i, D> {
386        ColumnSchema(name.into(), r#type.into())
387    }
388}
389
390impl<D: Dialect> fmt::Display for ColumnSchema<'_, D> {
391    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
392        write!(f, "{} {}", self.0, self.1)
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    #[test]
401    fn build_select() {
402        assert_eq!(
403            r#"SELECT "foo bar" FROM foo.baz_quix WHERE blah = 'hello ''world'' foo'"#,
404            &format!(
405                "SELECT {} FROM {} WHERE {} = {}",
406                Column("foo bar".into()),
407                SchemaTable("foo".into(), "baz".into()).with_postfix("_quix"),
408                Column("blah".into()),
409                QuotedData("hello 'world' foo")
410            )
411        )
412    }
413
414    #[test]
415    fn build_object_concat() {
416        assert_eq!(
417            r#""hello ""world"" foo_""quix""""#,
418            &format!(
419                "{}",
420                ObjectConcat(&[r#"hello "world" foo"#, r#"_"quix""#])
421            )
422        );
423
424        assert_eq!(
425            "foo_bar_baz",
426            &format!(
427                "{}",
428                ObjectConcat(&["foo_", "bar", "_baz"])
429            )
430        );
431    }
432}