Skip to main content

sql_cli/sql/parser/
file_cte_parser.rs

1/// FILE CTE Parser Module
2///
3/// Parses FILE CTE specifications for enumerating filesystem metadata as a
4/// virtual table. See `docs/FILE_CTE_DESIGN.md` for design rationale.
5///
6/// Syntax:
7/// ```sql
8/// WITH files AS (
9///     FILE PATH './data'
10///     [RECURSIVE]
11///     [GLOB '*.csv']
12///     [MAX_DEPTH 5]
13///     [MAX_FILES 100000]
14///     [FOLLOW_LINKS]
15///     [INCLUDE_HIDDEN]
16/// )
17/// SELECT ... FROM files;
18/// ```
19use super::ast::FileCTESpec;
20use super::lexer::Token;
21
22pub struct FileCteParser;
23
24impl FileCteParser {
25    /// Main entry point to parse a FILE CTE specification.
26    ///
27    /// Called after the opening `(` and the `FILE` keyword have been consumed.
28    /// Parses clauses until hitting the closing `)`.
29    pub fn parse(parser: &mut crate::sql::recursive_parser::Parser) -> Result<FileCTESpec, String> {
30        // Expect PATH keyword
31        if let Token::Identifier(id) = &parser.current_token {
32            if id.to_uppercase() != "PATH" {
33                return Err(format!("Expected PATH keyword in FILE CTE, found '{}'", id));
34            }
35        } else {
36            return Err("Expected PATH keyword in FILE CTE".to_string());
37        }
38        parser.advance();
39
40        // Parse PATH string
41        let path = match &parser.current_token {
42            Token::StringLiteral(p) => p.clone(),
43            _ => return Err("Expected string literal after PATH keyword".to_string()),
44        };
45        parser.advance();
46
47        // Initialize optional fields with defaults
48        let mut recursive = false;
49        let mut glob: Option<String> = None;
50        let mut max_depth: Option<usize> = None;
51        let mut max_files: Option<usize> = None;
52        let mut follow_links = false;
53        let mut include_hidden = false;
54
55        // Parse optional clauses until closing parenthesis
56        while !matches!(parser.current_token, Token::RightParen)
57            && !matches!(parser.current_token, Token::Eof)
58        {
59            if let Token::Identifier(id) = &parser.current_token {
60                match id.to_uppercase().as_str() {
61                    "RECURSIVE" => {
62                        parser.advance();
63                        recursive = true;
64                    }
65                    "GLOB" => {
66                        parser.advance();
67                        glob = Some(Self::parse_string(parser, "GLOB")?);
68                    }
69                    "MAX_DEPTH" => {
70                        parser.advance();
71                        max_depth = Some(Self::parse_usize(parser, "MAX_DEPTH")?);
72                    }
73                    "MAX_FILES" => {
74                        parser.advance();
75                        max_files = Some(Self::parse_usize(parser, "MAX_FILES")?);
76                    }
77                    "FOLLOW_LINKS" => {
78                        parser.advance();
79                        follow_links = true;
80                    }
81                    "INCLUDE_HIDDEN" => {
82                        parser.advance();
83                        include_hidden = true;
84                    }
85                    _ => {
86                        return Err(format!(
87                            "Unexpected keyword '{}' in FILE CTE specification",
88                            id
89                        ));
90                    }
91                }
92            } else {
93                break;
94            }
95        }
96
97        Ok(FileCTESpec {
98            path,
99            recursive,
100            glob,
101            max_depth,
102            max_files,
103            follow_links,
104            include_hidden,
105        })
106    }
107
108    fn parse_string(
109        parser: &mut crate::sql::recursive_parser::Parser,
110        clause: &str,
111    ) -> Result<String, String> {
112        match &parser.current_token {
113            Token::StringLiteral(s) => {
114                let s = s.clone();
115                parser.advance();
116                Ok(s)
117            }
118            _ => Err(format!("Expected string literal after {} clause", clause)),
119        }
120    }
121
122    fn parse_usize(
123        parser: &mut crate::sql::recursive_parser::Parser,
124        clause: &str,
125    ) -> Result<usize, String> {
126        match &parser.current_token {
127            Token::NumberLiteral(n) => {
128                let val = n
129                    .parse::<usize>()
130                    .map_err(|_| format!("Invalid {} value: {}", clause, n))?;
131                parser.advance();
132                Ok(val)
133            }
134            _ => Err(format!("Expected integer after {} clause", clause)),
135        }
136    }
137}