Skip to main content

kyu_parser/parser/
ddl.rs

1//! DDL statement parsers: CREATE NODE TABLE, CREATE REL TABLE, DROP, ALTER, COPY.
2
3use chumsky::prelude::*;
4use smol_str::SmolStr;
5
6use crate::ast::*;
7use crate::span::Spanned;
8use crate::token::Token;
9
10use super::expression::expression_parser;
11use super::pattern::ident;
12
13type ParserError = Simple<Token>;
14
15/// Parse a type name token as a SmolStr (handles type keywords + identifiers).
16fn type_name() -> impl Parser<Token, Spanned<SmolStr>, Error = ParserError> + Clone {
17    select! {
18        Token::BoolType => SmolStr::new("BOOL"),
19        Token::Int8Type => SmolStr::new("INT8"),
20        Token::Int16Type => SmolStr::new("INT16"),
21        Token::Int32Type => SmolStr::new("INT32"),
22        Token::Int64Type => SmolStr::new("INT64"),
23        Token::Int128Type => SmolStr::new("INT128"),
24        Token::UInt8Type => SmolStr::new("UINT8"),
25        Token::UInt16Type => SmolStr::new("UINT16"),
26        Token::UInt32Type => SmolStr::new("UINT32"),
27        Token::UInt64Type => SmolStr::new("UINT64"),
28        Token::FloatType => SmolStr::new("FLOAT"),
29        Token::DoubleType => SmolStr::new("DOUBLE"),
30        Token::StringType => SmolStr::new("STRING"),
31        Token::DateType => SmolStr::new("DATE"),
32        Token::TimestampType => SmolStr::new("TIMESTAMP"),
33        Token::IntervalType => SmolStr::new("INTERVAL"),
34        Token::BlobType => SmolStr::new("BLOB"),
35        Token::UuidType => SmolStr::new("UUID"),
36        Token::SerialType => SmolStr::new("SERIAL"),
37        Token::Ident(name) => name,
38    }
39    .map_with_span(|n, s| (n, s))
40}
41
42/// Parse a column definition: `name TYPE [DEFAULT expr]`
43fn column_definition() -> impl Parser<Token, ColumnDefinition, Error = ParserError> + Clone {
44    let name = ident().map_with_span(|n, s| (n, s));
45    let data_type = type_name();
46    let default = just(Token::Default)
47        .ignore_then(expression_parser())
48        .or_not();
49
50    name.then(data_type)
51        .then(default)
52        .map(|((name, data_type), default_value)| ColumnDefinition {
53            name,
54            data_type,
55            default_value,
56        })
57}
58
59/// Parse IF NOT EXISTS.
60fn if_not_exists() -> impl Parser<Token, bool, Error = ParserError> + Clone {
61    just(Token::If)
62        .then_ignore(just(Token::Not))
63        .then_ignore(just(Token::Exists))
64        .or_not()
65        .map(|o| o.is_some())
66}
67
68/// Parse IF EXISTS.
69fn if_exists() -> impl Parser<Token, bool, Error = ParserError> + Clone {
70    just(Token::If)
71        .then_ignore(just(Token::Exists))
72        .or_not()
73        .map(|o| o.is_some())
74}
75
76/// Parse CREATE NODE TABLE statement.
77pub fn create_node_table() -> impl Parser<Token, CreateNodeTable, Error = ParserError> + Clone {
78    let primary_key = just(Token::Primary)
79        .ignore_then(just(Token::Key))
80        .ignore_then(
81            ident()
82                .map_with_span(|n, s| (n, s))
83                .delimited_by(just(Token::LeftParen), just(Token::RightParen)),
84        );
85
86    just(Token::Create)
87        .ignore_then(just(Token::Node))
88        .ignore_then(just(Token::Table))
89        .ignore_then(if_not_exists())
90        .then(ident().map_with_span(|n, s| (n, s)))
91        .then(
92            column_definition()
93                .separated_by(just(Token::Comma))
94                .then_ignore(just(Token::Comma).or_not())
95                .then(primary_key)
96                .delimited_by(just(Token::LeftParen), just(Token::RightParen)),
97        )
98        .map(|((if_not_exists, name), (columns, primary_key))| CreateNodeTable {
99            name,
100            if_not_exists,
101            columns,
102            primary_key,
103        })
104        .labelled("create node table")
105}
106
107/// Parse CREATE REL TABLE statement.
108pub fn create_rel_table() -> impl Parser<Token, CreateRelTable, Error = ParserError> + Clone {
109    let from_to = just(Token::From)
110        .ignore_then(ident().map_with_span(|n, s| (n, s)))
111        .then_ignore(just(Token::To))
112        .then(ident().map_with_span(|n, s| (n, s)));
113
114    let columns = just(Token::Comma)
115        .ignore_then(column_definition().separated_by(just(Token::Comma)))
116        .or_not()
117        .map(|c| c.unwrap_or_default());
118
119    just(Token::Create)
120        .ignore_then(just(Token::Rel))
121        .ignore_then(just(Token::Table))
122        .ignore_then(if_not_exists())
123        .then(ident().map_with_span(|n, s| (n, s)))
124        .then(
125            from_to
126                .then(columns)
127                .delimited_by(just(Token::LeftParen), just(Token::RightParen)),
128        )
129        .map(
130            |((if_not_exists, name), ((from_table, to_table), columns))| CreateRelTable {
131                name,
132                if_not_exists,
133                from_table,
134                to_table,
135                columns,
136            },
137        )
138        .labelled("create rel table")
139}
140
141/// Parse DROP statement.
142pub fn drop_statement() -> impl Parser<Token, DropStatement, Error = ParserError> + Clone {
143    just(Token::Drop)
144        .ignore_then(just(Token::Table).to(DropObjectType::Table))
145        .then(if_exists())
146        .then(ident().map_with_span(|n, s| (n, s)))
147        .map(|((object_type, if_exists), name)| DropStatement {
148            object_type,
149            name,
150            if_exists,
151        })
152        .labelled("drop statement")
153}
154
155/// Parse ALTER TABLE statement.
156pub fn alter_table() -> impl Parser<Token, AlterTable, Error = ParserError> + Clone {
157    let add_column = just(Token::Add)
158        .ignore_then(just(Token::Column).or_not())
159        .ignore_then(column_definition())
160        .map(AlterAction::AddColumn);
161
162    let drop_column = just(Token::Drop)
163        .ignore_then(just(Token::Column).or_not())
164        .ignore_then(ident().map_with_span(|n, s| (n, s)))
165        .map(AlterAction::DropColumn);
166
167    let rename_column = just(Token::Rename)
168        .ignore_then(just(Token::Column).or_not())
169        .ignore_then(ident().map_with_span(|n, s| (n, s)))
170        .then_ignore(just(Token::To))
171        .then(ident().map_with_span(|n, s| (n, s)))
172        .map(|(old_name, new_name)| AlterAction::RenameColumn { old_name, new_name });
173
174    let rename_table = just(Token::Rename)
175        .ignore_then(just(Token::To))
176        .ignore_then(ident().map_with_span(|n, s| (n, s)))
177        .map(AlterAction::RenameTable);
178
179    let action = choice((add_column, drop_column, rename_column, rename_table));
180
181    just(Token::Alter)
182        .ignore_then(just(Token::Table))
183        .ignore_then(ident().map_with_span(|n, s| (n, s)))
184        .then(action)
185        .map(|(table_name, action)| AlterTable { table_name, action })
186        .labelled("alter table")
187}
188
189/// Parse COPY FROM statement.
190pub fn copy_from() -> impl Parser<Token, CopyFrom, Error = ParserError> + Clone {
191    let option = ident()
192        .map_with_span(|n, s| (n, s))
193        .then_ignore(just(Token::Eq).or(just(Token::Colon)))
194        .then(expression_parser());
195
196    let options = option
197        .separated_by(just(Token::Comma))
198        .delimited_by(just(Token::LeftParen), just(Token::RightParen))
199        .or_not()
200        .map(|o| o.unwrap_or_default());
201
202    just(Token::Copy)
203        .ignore_then(ident().map_with_span(|n, s| (n, s)))
204        .then_ignore(just(Token::From))
205        .then(expression_parser())
206        .then(options)
207        .map(|((table_name, source), options)| CopyFrom {
208            table_name,
209            source,
210            options,
211        })
212        .labelled("copy from")
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::lexer::Lexer;
219
220    fn parse_with<T>(
221        parser: impl Parser<Token, T, Error = ParserError>,
222        src: &str,
223    ) -> Option<T> {
224        let (toks, errors) = Lexer::new(src).lex();
225        assert!(errors.is_empty(), "lex errors: {errors:?}");
226        let len = src.len();
227        let stream = chumsky::Stream::from_iter(
228            len..len + 1,
229            toks.into_iter()
230                .filter(|(tok, _)| !matches!(tok, Token::Eof)),
231        );
232        let (result, errors) = parser.then_ignore(end()).parse_recovery(stream);
233        if !errors.is_empty() {
234            eprintln!("parse errors: {errors:?}");
235        }
236        result
237    }
238
239    #[test]
240    fn create_node_table_basic() {
241        let stmt = parse_with(
242            create_node_table(),
243            "CREATE NODE TABLE Person (name STRING, age INT64, PRIMARY KEY (name))",
244        )
245        .unwrap();
246        assert_eq!(stmt.name.0.as_str(), "Person");
247        assert_eq!(stmt.columns.len(), 2);
248        assert_eq!(stmt.primary_key.0.as_str(), "name");
249        assert!(!stmt.if_not_exists);
250    }
251
252    #[test]
253    fn create_node_table_if_not_exists() {
254        let stmt = parse_with(
255            create_node_table(),
256            "CREATE NODE TABLE IF NOT EXISTS Person (id INT64, PRIMARY KEY (id))",
257        )
258        .unwrap();
259        assert!(stmt.if_not_exists);
260    }
261
262    #[test]
263    fn create_rel_table_basic() {
264        let stmt = parse_with(
265            create_rel_table(),
266            "CREATE REL TABLE Knows (FROM Person TO Person, since INT64)",
267        )
268        .unwrap();
269        assert_eq!(stmt.name.0.as_str(), "Knows");
270        assert_eq!(stmt.from_table.0.as_str(), "Person");
271        assert_eq!(stmt.to_table.0.as_str(), "Person");
272        assert_eq!(stmt.columns.len(), 1);
273    }
274
275    #[test]
276    fn drop_table() {
277        let stmt = parse_with(drop_statement(), "DROP TABLE Person").unwrap();
278        assert_eq!(stmt.name.0.as_str(), "Person");
279        assert!(!stmt.if_exists);
280    }
281
282    #[test]
283    fn drop_table_if_exists() {
284        let stmt = parse_with(drop_statement(), "DROP TABLE IF EXISTS Person").unwrap();
285        assert!(stmt.if_exists);
286    }
287
288    #[test]
289    fn alter_add_column() {
290        let stmt = parse_with(
291            alter_table(),
292            "ALTER TABLE Person ADD email STRING",
293        )
294        .unwrap();
295        assert!(matches!(stmt.action, AlterAction::AddColumn(_)));
296    }
297
298    #[test]
299    fn alter_drop_column() {
300        let stmt = parse_with(alter_table(), "ALTER TABLE Person DROP age").unwrap();
301        assert!(matches!(stmt.action, AlterAction::DropColumn(_)));
302    }
303
304    #[test]
305    fn alter_rename_column() {
306        let stmt = parse_with(
307            alter_table(),
308            "ALTER TABLE Person RENAME name TO full_name",
309        )
310        .unwrap();
311        assert!(matches!(
312            stmt.action,
313            AlterAction::RenameColumn { .. }
314        ));
315    }
316
317    #[test]
318    fn copy_from_basic() {
319        let stmt = parse_with(
320            copy_from(),
321            "COPY Person FROM 'persons.csv'",
322        )
323        .unwrap();
324        assert_eq!(stmt.table_name.0.as_str(), "Person");
325    }
326}