spacetimedb_sql_parser/parser/
sub.rs

1//! The SpacetimeDB SQL subscription grammar
2//!
3//! ```ebnf
4//! query
5//!     = SELECT projection FROM relation [ WHERE predicate ]
6//!     ;
7//!
8//! projection
9//!     = STAR
10//!     | ident '.' STAR
11//!     ;
12//!
13//! relation
14//!     = table
15//!     | '(' query ')'
16//!     | relation [ [AS] ident ] { [INNER] JOIN relation [ [AS] ident ] ON predicate }
17//!     ;
18//!
19//! predicate
20//!     = expr
21//!     | predicate AND predicate
22//!     | predicate OR  predicate
23//!     ;
24//!
25//! expr
26//!     = literal
27//!     | ident
28//!     | field
29//!     | expr op expr
30//!     ;
31//!
32//! field
33//!     = ident '.' ident
34//!     ;
35//!
36//! op
37//!     = '='
38//!     | '<'
39//!     | '>'
40//!     | '<' '='
41//!     | '>' '='
42//!     | '!' '='
43//!     | '<' '>'
44//!     ;
45//!
46//! literal
47//!     = INTEGER
48//!     | FLOAT
49//!     | STRING
50//!     | HEX
51//!     | TRUE
52//!     | FALSE
53//!     ;
54//! ```
55
56use sqlparser::{
57    ast::{GroupByExpr, Query, Select, SetExpr, Statement},
58    dialect::PostgreSqlDialect,
59    parser::Parser,
60};
61
62use crate::ast::sub::SqlSelect;
63
64use super::{
65    errors::{SqlUnsupported, SubscriptionUnsupported},
66    parse_expr_opt, parse_projection, RelParser, SqlParseResult,
67};
68
69/// Parse a SQL string
70pub fn parse_subscription(sql: &str) -> SqlParseResult<SqlSelect> {
71    let mut stmts = Parser::parse_sql(&PostgreSqlDialect {}, sql)?;
72    match stmts.len() {
73        0 => Err(SqlUnsupported::Empty.into()),
74        1 => parse_statement(stmts.swap_remove(0))
75            .map(|ast| ast.qualify_vars())
76            .and_then(|ast| ast.find_unqualified_vars()),
77        _ => Err(SqlUnsupported::MultiStatement.into()),
78    }
79}
80
81/// Parse a SQL query
82fn parse_statement(stmt: Statement) -> SqlParseResult<SqlSelect> {
83    match stmt {
84        Statement::Query(query) => SubParser::parse_query(*query),
85        _ => Err(SubscriptionUnsupported::Dml.into()),
86    }
87}
88
89struct SubParser;
90
91impl RelParser for SubParser {
92    type Ast = SqlSelect;
93
94    fn parse_query(query: Query) -> SqlParseResult<Self::Ast> {
95        match query {
96            Query {
97                with: None,
98                body,
99                order_by,
100                limit: None,
101                offset: None,
102                fetch: None,
103                locks,
104            } if order_by.is_empty() && locks.is_empty() => parse_set_op(*body),
105            _ => Err(SubscriptionUnsupported::feature(query).into()),
106        }
107    }
108}
109
110/// Parse a set operation
111fn parse_set_op(expr: SetExpr) -> SqlParseResult<SqlSelect> {
112    match expr {
113        SetExpr::Select(select) => parse_select(*select).map(SqlSelect::qualify_vars),
114        _ => Err(SqlUnsupported::SetOp(Box::new(expr)).into()),
115    }
116}
117
118// Parse a SELECT statement
119fn parse_select(select: Select) -> SqlParseResult<SqlSelect> {
120    match select {
121        Select {
122            distinct: None,
123            top: None,
124            projection,
125            into: None,
126            from,
127            lateral_views,
128            selection,
129            group_by: GroupByExpr::Expressions(exprs),
130            cluster_by,
131            distribute_by,
132            sort_by,
133            having: None,
134            named_window,
135            qualify: None,
136        } if lateral_views.is_empty()
137            && exprs.is_empty()
138            && cluster_by.is_empty()
139            && distribute_by.is_empty()
140            && sort_by.is_empty()
141            && named_window.is_empty() =>
142        {
143            Ok(SqlSelect {
144                from: SubParser::parse_from(from)?,
145                filter: parse_expr_opt(selection)?,
146                project: parse_projection(projection)?,
147            })
148        }
149        _ => Err(SubscriptionUnsupported::Select(select).into()),
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use crate::parser::sub::parse_subscription;
156
157    #[test]
158    fn unsupported() {
159        for sql in [
160            "delete from t",
161            " ",
162            "",
163            "select distinct a from t",
164            "select * from (select * from t) join (select * from s) on a = b",
165        ] {
166            assert!(parse_subscription(sql).is_err());
167        }
168    }
169
170    #[test]
171    fn supported() {
172        for sql in [
173            "select * from t",
174            "select * from t where a = 1",
175            "select * from t where a <> 1",
176            "select * from t where a = 1 or a = 2",
177            "select t.* from t join s",
178            "select t.* from t join s on t.c = s.d",
179            "select a.* from t as a join s as b on a.c = b.d",
180            "select * from t where x = :sender",
181        ] {
182            assert!(parse_subscription(sql).is_ok());
183        }
184    }
185}