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(
99            |((if_not_exists, name), (columns, primary_key))| CreateNodeTable {
100                name,
101                if_not_exists,
102                columns,
103                primary_key,
104            },
105        )
106        .labelled("create node table")
107}
108
109/// Parse CREATE REL TABLE statement.
110pub fn create_rel_table() -> impl Parser<Token, CreateRelTable, Error = ParserError> + Clone {
111    let from_to = just(Token::From)
112        .ignore_then(ident().map_with_span(|n, s| (n, s)))
113        .then_ignore(just(Token::To))
114        .then(ident().map_with_span(|n, s| (n, s)));
115
116    let columns = just(Token::Comma)
117        .ignore_then(column_definition().separated_by(just(Token::Comma)))
118        .or_not()
119        .map(|c| c.unwrap_or_default());
120
121    just(Token::Create)
122        .ignore_then(just(Token::Rel))
123        .ignore_then(just(Token::Table))
124        .ignore_then(if_not_exists())
125        .then(ident().map_with_span(|n, s| (n, s)))
126        .then(
127            from_to
128                .then(columns)
129                .delimited_by(just(Token::LeftParen), just(Token::RightParen)),
130        )
131        .map(
132            |((if_not_exists, name), ((from_table, to_table), columns))| CreateRelTable {
133                name,
134                if_not_exists,
135                from_table,
136                to_table,
137                columns,
138            },
139        )
140        .labelled("create rel table")
141}
142
143/// Parse DROP statement.
144pub fn drop_statement() -> impl Parser<Token, DropStatement, Error = ParserError> + Clone {
145    just(Token::Drop)
146        .ignore_then(just(Token::Table).to(DropObjectType::Table))
147        .then(if_exists())
148        .then(ident().map_with_span(|n, s| (n, s)))
149        .map(|((object_type, if_exists), name)| DropStatement {
150            object_type,
151            name,
152            if_exists,
153        })
154        .labelled("drop statement")
155}
156
157/// Parse ALTER TABLE statement.
158pub fn alter_table() -> impl Parser<Token, AlterTable, Error = ParserError> + Clone {
159    let add_column = just(Token::Add)
160        .ignore_then(just(Token::Column).or_not())
161        .ignore_then(column_definition())
162        .map(AlterAction::AddColumn);
163
164    let drop_column = just(Token::Drop)
165        .ignore_then(just(Token::Column).or_not())
166        .ignore_then(ident().map_with_span(|n, s| (n, s)))
167        .map(AlterAction::DropColumn);
168
169    let rename_column = just(Token::Rename)
170        .ignore_then(just(Token::Column).or_not())
171        .ignore_then(ident().map_with_span(|n, s| (n, s)))
172        .then_ignore(just(Token::To))
173        .then(ident().map_with_span(|n, s| (n, s)))
174        .map(|(old_name, new_name)| AlterAction::RenameColumn { old_name, new_name });
175
176    let rename_table = just(Token::Rename)
177        .ignore_then(just(Token::To))
178        .ignore_then(ident().map_with_span(|n, s| (n, s)))
179        .map(AlterAction::RenameTable);
180
181    let action = choice((add_column, drop_column, rename_column, rename_table));
182
183    just(Token::Alter)
184        .ignore_then(just(Token::Table))
185        .ignore_then(ident().map_with_span(|n, s| (n, s)))
186        .then(action)
187        .map(|(table_name, action)| AlterTable { table_name, action })
188        .labelled("alter table")
189}
190
191/// Parse COPY FROM statement.
192pub fn copy_from() -> impl Parser<Token, CopyFrom, Error = ParserError> + Clone {
193    let option = ident()
194        .map_with_span(|n, s| (n, s))
195        .then_ignore(just(Token::Eq).or(just(Token::Colon)))
196        .then(expression_parser());
197
198    let options = option
199        .separated_by(just(Token::Comma))
200        .delimited_by(just(Token::LeftParen), just(Token::RightParen))
201        .or_not()
202        .map(|o| o.unwrap_or_default());
203
204    just(Token::Copy)
205        .ignore_then(ident().map_with_span(|n, s| (n, s)))
206        .then_ignore(just(Token::From))
207        .then(expression_parser())
208        .then(options)
209        .map(|((table_name, source), options)| CopyFrom {
210            table_name,
211            source,
212            options,
213        })
214        .labelled("copy from")
215}
216
217/// Parse LOAD FROM statement — RDF Linked Data import with auto schema inference.
218///
219/// Syntax: `LOAD FROM 'path/to/file.ttl'`
220pub fn load_from() -> impl Parser<Token, LoadFrom, Error = ParserError> + Clone {
221    just(Token::Load)
222        .ignore_then(just(Token::From))
223        .ignore_then(expression_parser())
224        .map(|source| LoadFrom { source })
225        .labelled("load from")
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::lexer::Lexer;
232
233    fn parse_with<T>(parser: impl Parser<Token, T, Error = ParserError>, src: &str) -> Option<T> {
234        let (toks, errors) = Lexer::new(src).lex();
235        assert!(errors.is_empty(), "lex errors: {errors:?}");
236        let len = src.len();
237        let stream = chumsky::Stream::from_iter(
238            len..len + 1,
239            toks.into_iter()
240                .filter(|(tok, _)| !matches!(tok, Token::Eof)),
241        );
242        let (result, errors) = parser.then_ignore(end()).parse_recovery(stream);
243        if !errors.is_empty() {
244            eprintln!("parse errors: {errors:?}");
245        }
246        result
247    }
248
249    #[test]
250    fn create_node_table_basic() {
251        let stmt = parse_with(
252            create_node_table(),
253            "CREATE NODE TABLE Person (name STRING, age INT64, PRIMARY KEY (name))",
254        )
255        .unwrap();
256        assert_eq!(stmt.name.0.as_str(), "Person");
257        assert_eq!(stmt.columns.len(), 2);
258        assert_eq!(stmt.primary_key.0.as_str(), "name");
259        assert!(!stmt.if_not_exists);
260    }
261
262    #[test]
263    fn create_node_table_if_not_exists() {
264        let stmt = parse_with(
265            create_node_table(),
266            "CREATE NODE TABLE IF NOT EXISTS Person (id INT64, PRIMARY KEY (id))",
267        )
268        .unwrap();
269        assert!(stmt.if_not_exists);
270    }
271
272    #[test]
273    fn create_rel_table_basic() {
274        let stmt = parse_with(
275            create_rel_table(),
276            "CREATE REL TABLE Knows (FROM Person TO Person, since INT64)",
277        )
278        .unwrap();
279        assert_eq!(stmt.name.0.as_str(), "Knows");
280        assert_eq!(stmt.from_table.0.as_str(), "Person");
281        assert_eq!(stmt.to_table.0.as_str(), "Person");
282        assert_eq!(stmt.columns.len(), 1);
283    }
284
285    #[test]
286    fn drop_table() {
287        let stmt = parse_with(drop_statement(), "DROP TABLE Person").unwrap();
288        assert_eq!(stmt.name.0.as_str(), "Person");
289        assert!(!stmt.if_exists);
290    }
291
292    #[test]
293    fn drop_table_if_exists() {
294        let stmt = parse_with(drop_statement(), "DROP TABLE IF EXISTS Person").unwrap();
295        assert!(stmt.if_exists);
296    }
297
298    #[test]
299    fn alter_add_column() {
300        let stmt = parse_with(alter_table(), "ALTER TABLE Person ADD email STRING").unwrap();
301        assert!(matches!(stmt.action, AlterAction::AddColumn(_)));
302    }
303
304    #[test]
305    fn alter_drop_column() {
306        let stmt = parse_with(alter_table(), "ALTER TABLE Person DROP age").unwrap();
307        assert!(matches!(stmt.action, AlterAction::DropColumn(_)));
308    }
309
310    #[test]
311    fn alter_rename_column() {
312        let stmt =
313            parse_with(alter_table(), "ALTER TABLE Person RENAME name TO full_name").unwrap();
314        assert!(matches!(stmt.action, AlterAction::RenameColumn { .. }));
315    }
316
317    #[test]
318    fn copy_from_basic() {
319        let stmt = parse_with(copy_from(), "COPY Person FROM 'persons.csv'").unwrap();
320        assert_eq!(stmt.table_name.0.as_str(), "Person");
321    }
322}