Skip to main content

lutra_sql/
lib.rs

1//! SQL Abstract Syntax Tree (AST) types
2#![cfg_attr(not(feature = "std"), no_std)]
3
4#[cfg(not(feature = "std"))]
5#[macro_use]
6extern crate alloc;
7
8mod display_utils;
9mod dml;
10mod query;
11mod string;
12
13#[cfg(not(feature = "std"))]
14use alloc::{
15    boxed::Box,
16    string::{String, ToString},
17    vec::Vec,
18};
19
20use core::cmp::Ordering;
21use core::fmt::{self, Display};
22use core::hash;
23
24use display_utils::{NewLine, SpaceOrNewline};
25
26pub use self::dml::{Assignment, AssignmentTarget, Delete, FromTable, Insert, Update};
27pub use self::query::{
28    Copy, Cte, CteAsMaterialized, Distinct, ExprWithAlias, Join, JoinConstraint, JoinOperator,
29    LateralView, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, Query, RelExpr, RelNamed,
30    Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, TableAlias, TableVersion,
31    Values, With,
32};
33
34pub use self::string::escape as escape_string;
35pub use display_utils::{DisplayCommaSeparated, Indent};
36
37pub struct DisplaySeparated<'a, T>
38where
39    T: fmt::Display,
40{
41    slice: &'a [T],
42    sep: &'static str,
43}
44
45impl<T> fmt::Display for DisplaySeparated<'_, T>
46where
47    T: fmt::Display,
48{
49    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50        let mut delim = "";
51        for t in self.slice {
52            f.write_str(delim)?;
53            delim = self.sep;
54            t.fmt(f)?;
55        }
56        Ok(())
57    }
58}
59
60pub fn display_separated<'a, T>(slice: &'a [T], sep: &'static str) -> DisplaySeparated<'a, T>
61where
62    T: fmt::Display,
63{
64    DisplaySeparated { slice, sep }
65}
66
67pub fn display_comma_separated<T>(slice: &[T]) -> DisplaySeparated<'_, T>
68where
69    T: fmt::Display,
70{
71    DisplaySeparated { slice, sep: ", " }
72}
73
74/// An identifier, decomposed into its value or character data and the quote style.
75#[derive(Debug, Clone)]
76pub struct Ident {
77    /// The value of the identifier without quotes.
78    pub value: String,
79    /// The starting quote if any. Valid quote characters are the single quote,
80    /// double quote, backtick, and opening square bracket.
81    pub quote_style: Option<char>,
82}
83
84impl PartialEq for Ident {
85    fn eq(&self, other: &Self) -> bool {
86        let Ident {
87            value,
88            quote_style,
89            // exhaustiveness check; we ignore spans in comparisons
90        } = self;
91
92        value == &other.value && quote_style == &other.quote_style
93    }
94}
95
96impl core::hash::Hash for Ident {
97    fn hash<H: hash::Hasher>(&self, state: &mut H) {
98        let Ident {
99            value,
100            quote_style,
101            // exhaustiveness check; we ignore spans in hashes
102        } = self;
103
104        value.hash(state);
105        quote_style.hash(state);
106    }
107}
108
109impl Eq for Ident {}
110
111impl PartialOrd for Ident {
112    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
113        Some(self.cmp(other))
114    }
115}
116
117impl Ord for Ident {
118    fn cmp(&self, other: &Self) -> Ordering {
119        let Ident {
120            value,
121            quote_style,
122            // exhaustiveness check; we ignore spans in ordering
123        } = self;
124
125        let Ident {
126            value: other_value,
127            quote_style: other_quote_style,
128            // exhaustiveness check; we ignore spans in ordering
129        } = other;
130
131        // First compare by value, then by quote_style
132        value
133            .cmp(other_value)
134            .then_with(|| quote_style.cmp(other_quote_style))
135    }
136}
137
138impl Ident {
139    /// Create a new identifier with the given value and no quotes and an empty span.
140    pub fn new<S>(value: S) -> Self
141    where
142        S: Into<String>,
143    {
144        Ident {
145            value: value.into(),
146            quote_style: None,
147        }
148    }
149
150    /// Create a new quoted identifier with the given quote and value. This function
151    /// panics if the given quote is not a valid quote character.
152    pub fn with_quote_if_needed<S>(quote: char, value: S) -> Self
153    where
154        S: Into<String>,
155    {
156        let value = value.into();
157        let quote_style = if valid_ident_regex().is_match(&value) && !is_keyword(&value) {
158            None
159        } else {
160            Some(quote)
161        };
162        Ident { value, quote_style }
163    }
164}
165
166fn valid_ident_regex() -> &'static regex::Regex {
167    static VALID_IDENT: once_cell::race::OnceBox<regex::Regex> = once_cell::race::OnceBox::new();
168    VALID_IDENT.get_or_init(|| {
169        // One of:
170        // - `*`
171        // - An ident starting with `a-z_\$` and containing other characters `a-z0-9_\$`
172        //
173        // We could replace this with pomsky (regex<>pomsky : sql<>prql)
174        // ^ ('*' | [ascii_lower '_$'] [ascii_lower ascii_digit '_$']* ) $
175        Box::new(regex::Regex::new(r"^((\*)|(^[a-z_\$][a-z0-9_\$]*))$").unwrap())
176    })
177}
178
179fn is_keyword(ident: &str) -> bool {
180    const KEYWORDS: &[&str] = &[
181        "select", "from", "where", "group", "by", "limit", "offset", "distinct", "on", "none",
182        "some", "end", "time",
183    ];
184    KEYWORDS.contains(&ident)
185}
186
187impl From<&str> for Ident {
188    fn from(value: &str) -> Self {
189        Ident {
190            value: value.to_string(),
191            quote_style: None,
192        }
193    }
194}
195
196impl fmt::Display for Ident {
197    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
198        match self.quote_style {
199            Some(q) if q == '"' || q == '\'' || q == '`' => {
200                let escaped = string::escape(&self.value, q);
201                write!(f, "{q}{escaped}{q}")
202            }
203            Some('[') => write!(f, "[{}]", self.value),
204            None => f.write_str(&self.value),
205            _ => panic!("unexpected quote style"),
206        }
207    }
208}
209
210/// A name of a table, view, custom type, etc., possibly multi-part, i.e. db.schema.obj
211#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
212pub struct ObjectName(pub Vec<Ident>);
213
214impl From<Vec<Ident>> for ObjectName {
215    fn from(idents: Vec<Ident>) -> Self {
216        ObjectName(idents)
217    }
218}
219
220impl fmt::Display for ObjectName {
221    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
222        write!(f, "{}", display_separated(&self.0, "."))
223    }
224}
225
226/// A WHEN clause in a CASE expression containing both
227/// the condition and its corresponding result
228#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
229pub struct CaseWhen {
230    pub condition: Expr,
231    pub result: Expr,
232}
233
234impl fmt::Display for CaseWhen {
235    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
236        f.write_str("WHEN ")?;
237        self.condition.fmt(f)?;
238        f.write_str(" THEN")?;
239        SpaceOrNewline.fmt(f)?;
240        Indent(&self.result).fmt(f)?;
241        Ok(())
242    }
243}
244
245/// An SQL expression of any type.
246///
247/// # Semantics / Type Checking
248///
249/// The parser does not distinguish between expressions of different types
250/// (e.g. boolean vs string). The caller is responsible for detecting and
251/// validating types as necessary (for example  `WHERE 1` vs `SELECT 1=1`)
252/// See the [README.md] for more details.
253///
254/// [README.md]: https://github.com/apache/datafusion-sqlparser-rs/blob/main/README.md#syntax-vs-semantics
255///
256/// # Equality and Hashing Does not Include Source Locations
257///
258/// The `Expr` type implements `PartialEq` and `Eq` based on the semantic value
259/// of the expression (not bitwise comparison). This means that `Expr` instances
260/// that are semantically equivalent but have different spans (locations in the
261/// source tree) will compare as equal.
262#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
263pub enum Expr {
264    /// Direct SQL source
265    Source(String),
266    Identifier(Ident),
267    CompoundIdentifier(Vec<Ident>),
268    IndexBy(Vec<Expr>),
269    Case {
270        operand: Option<Box<Expr>>,
271        cases: Vec<CaseWhen>,
272        else_result: Option<Box<Expr>>,
273    },
274    Subquery(Box<Query>),
275}
276
277impl fmt::Display for Expr {
278    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279        match self {
280            Expr::Source(s) => f.write_str(s),
281            Expr::Identifier(s) => write!(f, "{s}"),
282            Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")),
283
284            Expr::IndexBy(keys) => {
285                f.write_str("(ROW_NUMBER() OVER (")?;
286                if !keys.is_empty() {
287                    f.write_str("ORDER BY ")?;
288                    display_comma_separated(keys).fmt(f)?;
289                }
290                f.write_str(")-1)::int8")
291            }
292
293            Expr::Case {
294                operand,
295                cases,
296                else_result,
297            } => {
298                f.write_str("CASE")?;
299                if let Some(operand) = operand {
300                    f.write_str(" ")?;
301                    operand.fmt(f)?;
302                }
303                for case in cases {
304                    SpaceOrNewline.fmt(f)?;
305                    Indent(case).fmt(f)?;
306                }
307                if let Some(else_result) = else_result {
308                    SpaceOrNewline.fmt(f)?;
309                    Indent("ELSE").fmt(f)?;
310                    SpaceOrNewline.fmt(f)?;
311                    Indent(Indent(else_result)).fmt(f)?;
312                }
313                SpaceOrNewline.fmt(f)?;
314                f.write_str("END")
315            }
316            Expr::Subquery(s) => {
317                f.write_str("(")?;
318                SpaceOrNewline.fmt(f)?;
319                Indent(s).fmt(f)?;
320                SpaceOrNewline.fmt(f)?;
321                f.write_str(")")
322            }
323        }
324    }
325}