Try to parse a DDL statement from raw SQL. Returns `None` for non-DDL queries (SELECT, INSERT, etc.) that should flow through the normal planner. Returns `Some(Err(...))` when the SQL is structurally a DDL command but contains a reserved identifier that would be misrouted by the dispatcher.
(sql: &str)
| 19 | /// is structurally a DDL command but contains a reserved identifier that |
| 20 | /// would be misrouted by the dispatcher. |
| 21 | pub fn parse(sql: &str) -> Option<Result<NodedbStatement, SqlError>> { |
| 22 | let trimmed = sql.trim(); |
| 23 | if trimmed.is_empty() { |
| 24 | return None; |
| 25 | } |
| 26 | let upper = trimmed.to_uppercase(); |
| 27 | let parts: Vec<&str> = trimmed.split_whitespace().collect(); |
| 28 | if parts.is_empty() { |
| 29 | return None; |
| 30 | } |
| 31 | |
| 32 | // Graph DSL (`GRAPH ...`, `MATCH ...`, `OPTIONAL MATCH ...`) has its own |
| 33 | // tokenising parser — delegate early using token-aware dispatch so that |
| 34 | // leading block/line comments and quoted values containing DSL keywords |
| 35 | // are never mistakenly matched. |
| 36 | let first = lex::first_sql_word(trimmed).map(|w| w.to_uppercase()); |
| 37 | let is_graph = match first.as_deref() { |
| 38 | Some("GRAPH") | Some("MATCH") => true, |
| 39 | Some("OPTIONAL") => lex::second_sql_word(trimmed) |
| 40 | .map(|w| w.eq_ignore_ascii_case("MATCH")) |
| 41 | .unwrap_or(false), |
| 42 | _ => false, |
| 43 | }; |
| 44 | if is_graph { |
| 45 | return graph_parse::try_parse(trimmed).map(Ok); |
| 46 | } |
| 47 | |
| 48 | // Dispatch by family. Order matters only where prefixes overlap |
| 49 | // (e.g. DESCRIBE vs DESCRIBE SEQUENCE — handled inside each |
| 50 | // family's `try_parse`). A `Some(Err(...))` from any family |
| 51 | // short-circuits the chain — reserved-identifier errors must not |
| 52 | // be silently swallowed by the next family's `None` path. |
| 53 | macro_rules! try_family { |
| 54 | ($result:expr) => {{ |
| 55 | let r = $result; |
| 56 | if r.is_some() { |
| 57 | return r; |
| 58 | } |
| 59 | }}; |
| 60 | } |
| 61 | |
| 62 | // `SHOW GRAPH STATS` must be checked before the generic collection parser |
| 63 | // so its `SHOW` prefix is not consumed by `SHOW COLLECTIONS`/etc. |
| 64 | try_family!(graph_stats::try_parse(&upper, &parts, trimmed)); |
| 65 | // Conflict policy must be checked before the generic collection parser |
| 66 | // so "SET ON CONFLICT" does not fall through to the raw-SQL path. |
| 67 | try_family!(conflict_policy::try_parse(&upper, &parts, trimmed)); |
| 68 | try_family!(collection::try_parse(&upper, &parts, trimmed)); |
| 69 | try_family!(index::try_parse(&upper, &parts, trimmed)); |
| 70 | try_family!(trigger::try_parse(&upper, &parts, trimmed)); |
| 71 | try_family!(schedule::try_parse(&upper, &parts, trimmed)); |
| 72 | try_family!(sequence::try_parse(&upper, &parts, trimmed)); |
| 73 | try_family!(alert::try_parse(&upper, &parts, trimmed)); |
| 74 | try_family!(retention::try_parse(&upper, &parts, trimmed)); |
| 75 | try_family!(cluster_admin::try_parse(&upper, &parts, trimmed)); |
| 76 | try_family!(maintenance::try_parse(&upper, &parts, trimmed)); |
| 77 | try_family!(backup::try_parse(&upper, &parts, trimmed)); |
| 78 | // COPY FROM file-path form — must come after backup so STDIN forms fall through. |