Skip to main content

spacetimedb_sql_parser/parser/
mod.rs

1use errors::{SqlParseError, SqlRequired, SqlUnsupported};
2use sqlparser::ast::{
3    BinaryOperator, Expr, Function, FunctionArg, FunctionArgExpr, Ident, Join, JoinConstraint, JoinOperator,
4    ObjectName, Query, SelectItem, TableAlias, TableFactor, TableWithJoins, UnaryOperator, Value,
5    WildcardAdditionalOptions,
6};
7
8use spacetimedb_lib::sats::raw_identifier::RawNamespacedIdentifier;
9
10use crate::ast::{
11    BinOp, LogOp, Parameter, Project, ProjectElem, ProjectExpr, SqlExpr, SqlFrom, SqlIdent, SqlJoin, SqlLiteral,
12};
13
14pub mod errors;
15pub mod recursion;
16pub mod sql;
17pub mod sub;
18
19pub type SqlParseResult<T> = core::result::Result<T, SqlParseError>;
20
21/// Methods for parsing a relation expression.
22/// Note we abstract over the type of the relation expression,
23/// as each language has a different definition for it.
24trait RelParser {
25    type Ast;
26
27    /// Parse a top level relation expression
28    fn parse_query(query: Query) -> SqlParseResult<Self::Ast>;
29
30    /// Parse a FROM clause
31    fn parse_from(mut tables: Vec<TableWithJoins>) -> SqlParseResult<SqlFrom> {
32        if tables.is_empty() {
33            return Err(SqlRequired::From.into());
34        }
35        if tables.len() > 1 {
36            return Err(SqlUnsupported::ImplicitJoins.into());
37        }
38        let TableWithJoins { relation, joins } = tables.swap_remove(0);
39        let (name, alias) = Self::parse_relvar(relation)?;
40        if joins.is_empty() {
41            return Ok(SqlFrom::Expr(name, alias));
42        }
43        Ok(SqlFrom::Join(name, alias, Self::parse_joins(joins)?))
44    }
45
46    /// Parse a sequence of JOIN clauses
47    fn parse_joins(joins: Vec<Join>) -> SqlParseResult<Vec<SqlJoin>> {
48        joins.into_iter().map(Self::parse_join).collect()
49    }
50
51    /// Parse a single JOIN clause
52    fn parse_join(join: Join) -> SqlParseResult<SqlJoin> {
53        let (var, alias) = Self::parse_relvar(join.relation)?;
54        match join.join_operator {
55            JoinOperator::CrossJoin => Ok(SqlJoin { var, alias, on: None }),
56            JoinOperator::Inner(JoinConstraint::None) => Ok(SqlJoin { var, alias, on: None }),
57            JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp {
58                left,
59                op: BinaryOperator::Eq,
60                right,
61            })) if matches!(*left, Expr::Identifier(..) | Expr::CompoundIdentifier(..))
62                && matches!(*right, Expr::Identifier(..) | Expr::CompoundIdentifier(..)) =>
63            {
64                Ok(SqlJoin {
65                    var,
66                    alias,
67                    on: Some(parse_expr(
68                        Expr::BinaryOp {
69                            left,
70                            op: BinaryOperator::Eq,
71                            right,
72                        },
73                        0,
74                    )?),
75                })
76            }
77            _ => Err(SqlUnsupported::JoinType.into()),
78        }
79    }
80
81    /// Parse a table reference in a FROM clause
82    fn parse_relvar(expr: TableFactor) -> SqlParseResult<(SqlIdent, SqlIdent)> {
83        match expr {
84            // Relvar no alias
85            TableFactor::Table {
86                name,
87                alias: None,
88                args: None,
89                with_hints,
90                version: None,
91                partitions,
92            } if with_hints.is_empty() && partitions.is_empty() => {
93                let name = parse_ident(name)?;
94                let alias = name.clone();
95                Ok((name, alias))
96            }
97            // Relvar with alias
98            TableFactor::Table {
99                name,
100                alias: Some(TableAlias { name: alias, columns }),
101                args: None,
102                with_hints,
103                version: None,
104                partitions,
105            } if with_hints.is_empty() && partitions.is_empty() && columns.is_empty() => {
106                Ok((parse_ident(name)?, alias.into()))
107            }
108            _ => Err(SqlUnsupported::From(expr).into()),
109        }
110    }
111}
112
113/// Parse the items of a SELECT clause
114pub(crate) fn parse_projection(mut items: Vec<SelectItem>) -> SqlParseResult<Project> {
115    if items.len() == 1 {
116        return parse_project_or_agg(items.swap_remove(0));
117    }
118    Ok(Project::Exprs(
119        items
120            .into_iter()
121            .map(parse_project_elem)
122            .collect::<SqlParseResult<_>>()?,
123    ))
124}
125
126/// Parse a SELECT clause with only a single item
127pub(crate) fn parse_project_or_agg(item: SelectItem) -> SqlParseResult<Project> {
128    match item {
129        SelectItem::Wildcard(WildcardAdditionalOptions {
130            opt_exclude: None,
131            opt_except: None,
132            opt_rename: None,
133            opt_replace: None,
134        }) => Ok(Project::Star(None)),
135        SelectItem::QualifiedWildcard(
136            table_name,
137            WildcardAdditionalOptions {
138                opt_exclude: None,
139                opt_except: None,
140                opt_rename: None,
141                opt_replace: None,
142            },
143        ) => Ok(Project::Star(Some(parse_ident(table_name)?))),
144        SelectItem::UnnamedExpr(Expr::Function(_)) => Err(SqlUnsupported::AggregateWithoutAlias.into()),
145        SelectItem::ExprWithAlias {
146            expr: Expr::Function(agg_fn),
147            alias,
148        } => parse_agg_fn(agg_fn, alias.into()),
149        SelectItem::UnnamedExpr(_) | SelectItem::ExprWithAlias { .. } => {
150            Ok(Project::Exprs(vec![parse_project_elem(item)?]))
151        }
152        item => Err(SqlUnsupported::Projection(item).into()),
153    }
154}
155
156/// Parse an aggregate function in a select list
157fn parse_agg_fn(agg_fn: Function, alias: SqlIdent) -> SqlParseResult<Project> {
158    fn is_count(name: &ObjectName) -> bool {
159        name.0.len() == 1
160            && name
161                .0
162                .first()
163                .is_some_and(|Ident { value, .. }| value.to_lowercase() == "count")
164    }
165    match agg_fn {
166        Function {
167            name,
168            args,
169            over: None,
170            distinct: false,
171            special: false,
172            order_by,
173        } if is_count(&name)
174            && order_by.is_empty()
175            && args.len() == 1
176            && args
177                .first()
178                .is_some_and(|arg| matches!(arg, FunctionArg::Unnamed(FunctionArgExpr::Wildcard))) =>
179        {
180            Ok(Project::Count(alias))
181        }
182        agg_fn => Err(SqlUnsupported::Aggregate(agg_fn).into()),
183    }
184}
185
186/// Parse an item in a SELECT clause
187pub(crate) fn parse_project_elem(item: SelectItem) -> SqlParseResult<ProjectElem> {
188    match item {
189        SelectItem::Wildcard(_) => Err(SqlUnsupported::MixedWildcardProject.into()),
190        SelectItem::QualifiedWildcard(..) => Err(SqlUnsupported::MixedWildcardProject.into()),
191        SelectItem::UnnamedExpr(expr) => match parse_proj(expr)? {
192            ProjectExpr::Var(name) => Ok(ProjectElem(ProjectExpr::Var(name.clone()), name)),
193            ProjectExpr::Field(name, field) => Ok(ProjectElem(ProjectExpr::Field(name, field.clone()), field)),
194        },
195        SelectItem::ExprWithAlias { expr, alias } => Ok(ProjectElem(parse_proj(expr)?, alias.into())),
196    }
197}
198
199/// Parse a column projection
200pub(crate) fn parse_proj(expr: Expr) -> SqlParseResult<ProjectExpr> {
201    match expr {
202        Expr::Identifier(ident) => Ok(ProjectExpr::Var(ident.into())),
203        Expr::CompoundIdentifier(idents) if idents.len() >= 2 => {
204            let (table, field) = parse_qualified_field(idents)?;
205            Ok(ProjectExpr::Field(table, field))
206        }
207        _ => Err(SqlUnsupported::ProjectionExpr(expr).into()),
208    }
209}
210
211/// Parse a qualified column reference.
212/// The last part is the column; the rest is the (possibly namespaced) table name or alias,
213/// joined with dots to match [`parse_parts`].
214pub(crate) fn parse_qualified_field(mut idents: Vec<Ident>) -> SqlParseResult<(SqlIdent, SqlIdent)> {
215    let field = idents.pop().expect("caller checked idents.len() >= 2").into();
216    let table = parse_parts(idents)?;
217    Ok((table, field))
218}
219
220// These types determine the size of [`parse_expr`]'s stack frame on 64-bit targets.
221// Changing their sizes will require updating the recursion limit to avoid stack overflows.
222// wasm32 has different type layouts, so this guard does not apply there.
223#[cfg(target_pointer_width = "64")]
224const _: () = assert!(size_of::<Expr>() == 168);
225#[cfg(target_pointer_width = "64")]
226const _: () = assert!(size_of::<SqlParseResult<SqlExpr>>() == 40);
227
228/// Parse a scalar expression
229fn parse_expr(expr: Expr, depth: usize) -> SqlParseResult<SqlExpr> {
230    recursion::guard(depth, recursion::MAX_RECURSION_EXPR, "sql-parser::parse_expr")?;
231    match expr {
232        Expr::Nested(expr) => parse_expr(*expr, depth + 1),
233        Expr::Value(Value::Placeholder(param)) if &param == ":sender" => Ok(SqlExpr::Param(Parameter::Sender)),
234        Expr::Value(v) => Ok(SqlExpr::Lit(parse_literal(v)?)),
235        Expr::UnaryOp {
236            op: UnaryOperator::Plus,
237            expr,
238        } => Ok(SqlExpr::Lit(parse_signed_literal_expr(
239            UnaryOperator::Plus,
240            *expr,
241            SqlUnsupported::Expr,
242        )?)),
243        Expr::UnaryOp {
244            op: UnaryOperator::Minus,
245            expr,
246        } => Ok(SqlExpr::Lit(parse_signed_literal_expr(
247            UnaryOperator::Minus,
248            *expr,
249            SqlUnsupported::Expr,
250        )?)),
251        Expr::Identifier(ident) => Ok(SqlExpr::Var(ident.into())),
252        Expr::CompoundIdentifier(idents) if idents.len() >= 2 => {
253            let (table, field) = parse_qualified_field(idents)?;
254            Ok(SqlExpr::Field(table, field))
255        }
256        Expr::BinaryOp {
257            left,
258            op: BinaryOperator::And,
259            right,
260        } => {
261            let l = parse_expr(*left, depth + 1)?;
262            let r = parse_expr(*right, depth + 1)?;
263            Ok(SqlExpr::Log(Box::new(l), Box::new(r), LogOp::And))
264        }
265        Expr::BinaryOp {
266            left,
267            op: BinaryOperator::Or,
268            right,
269        } => {
270            let l = parse_expr(*left, depth + 1)?;
271            let r = parse_expr(*right, depth + 1)?;
272            Ok(SqlExpr::Log(Box::new(l), Box::new(r), LogOp::Or))
273        }
274        Expr::BinaryOp { left, op, right } => {
275            let l = parse_expr(*left, depth + 1)?;
276            let r = parse_expr(*right, depth + 1)?;
277            Ok(SqlExpr::Bin(Box::new(l), Box::new(r), parse_binop(op)?))
278        }
279        _ => Err(SqlUnsupported::Expr(expr).into()),
280    }
281}
282
283fn parse_signed_literal_expr(
284    op: UnaryOperator,
285    expr: Expr,
286    unsupported: fn(Expr) -> SqlUnsupported,
287) -> SqlParseResult<SqlLiteral> {
288    match expr {
289        Expr::Value(Value::Number(n, _)) => {
290            let sign = match op {
291                UnaryOperator::Plus => "+",
292                UnaryOperator::Minus => "-",
293                _ => unreachable!("caller only passes unary plus/minus"),
294            };
295            Ok(SqlLiteral::Num(format!("{sign}{n}").into_boxed_str()))
296        }
297        expr => Err(unsupported(Expr::UnaryOp {
298            op,
299            expr: Box::new(expr),
300        })
301        .into()),
302    }
303}
304
305/// Parse a literal expression.
306pub(crate) fn parse_literal_expr(expr: Expr, unsupported: fn(Expr) -> SqlUnsupported) -> SqlParseResult<SqlLiteral> {
307    match expr {
308        Expr::Value(value) => parse_literal(value),
309        Expr::UnaryOp {
310            op: UnaryOperator::Plus,
311            expr,
312        } => parse_signed_literal_expr(UnaryOperator::Plus, *expr, unsupported),
313        Expr::UnaryOp {
314            op: UnaryOperator::Minus,
315            expr,
316        } => parse_signed_literal_expr(UnaryOperator::Minus, *expr, unsupported),
317        expr => Err(unsupported(expr).into()),
318    }
319}
320
321/// Parse an optional scalar expression
322pub(crate) fn parse_expr_opt(opt: Option<Expr>) -> SqlParseResult<Option<SqlExpr>> {
323    opt.map(|expr| parse_expr(expr, 0)).transpose()
324}
325
326/// Parse a scalar binary operator
327pub(crate) fn parse_binop(op: BinaryOperator) -> SqlParseResult<BinOp> {
328    match op {
329        BinaryOperator::Eq => Ok(BinOp::Eq),
330        BinaryOperator::NotEq => Ok(BinOp::Ne),
331        BinaryOperator::Lt => Ok(BinOp::Lt),
332        BinaryOperator::LtEq => Ok(BinOp::Lte),
333        BinaryOperator::Gt => Ok(BinOp::Gt),
334        BinaryOperator::GtEq => Ok(BinOp::Gte),
335        _ => Err(SqlUnsupported::BinOp(op).into()),
336    }
337}
338
339/// Parse a literal expression
340pub(crate) fn parse_literal(value: Value) -> SqlParseResult<SqlLiteral> {
341    match value {
342        Value::Boolean(v) => Ok(SqlLiteral::Bool(v)),
343        Value::Number(v, _) => Ok(SqlLiteral::Num(v.into_boxed_str())),
344        Value::SingleQuotedString(s) => Ok(SqlLiteral::Str(s.into_boxed_str())),
345        Value::HexStringLiteral(s) => Ok(SqlLiteral::Hex(s.into_boxed_str())),
346        _ => Err(SqlUnsupported::Literal(value).into()),
347    }
348}
349
350/// Parse an identifier
351pub(crate) fn parse_ident(ObjectName(parts): ObjectName) -> SqlParseResult<SqlIdent> {
352    parse_parts(parts)
353}
354
355/// Parse an identifier.
356///
357/// Multi-part names are joined back together with dots, because submodule tables are stored
358/// in the catalog under namespace-prefixed names (e.g. `lib.library_table`). Nesting means
359/// arbitrarily many parts are legal: `auth.baz.baz_items` is a single catalog name, not a
360/// three-level qualification. Note that qualified *column* references (`t.a`) never reach
361/// here — they are parsed as `Expr::CompoundIdentifier` instead.
362pub(crate) fn parse_parts(mut parts: Vec<Ident>) -> SqlParseResult<SqlIdent> {
363    if parts.len() == 1 {
364        return Ok(parts.swap_remove(0).into());
365    }
366    let joined = parts.iter().map(|p| p.value.as_str()).collect::<Vec<_>>().join(".");
367    Ok(SqlIdent(RawNamespacedIdentifier::new(joined)))
368}