Skip to main content

featherdb_query/
parser.rs

1//! SQL parser using sqlparser-rs
2
3use crate::planner::{LogicalPlan, Planner, ShowGrantsTarget as PlanShowGrantsTarget};
4use featherdb_catalog::{
5    AlterTableOp, Catalog, Column, ColumnConstraint, ForeignKey, ForeignKeyAction,
6};
7use featherdb_core::{suggest_keyword, ColumnType, Error, QueryContext, QueryError, Result, Value};
8use sqlparser::ast::{self, Statement};
9use sqlparser::dialect::GenericDialect;
10use sqlparser::parser::Parser as SqlParser;
11use sqlparser::tokenizer::{Token, Tokenizer};
12use std::collections::HashMap;
13
14/// Parsed SHOW GRANTS target
15#[derive(Debug, Clone)]
16pub enum ShowGrantsTarget {
17    /// SHOW GRANTS ON <table>
18    Table(String),
19    /// SHOW GRANTS FOR '<api_key_id>'
20    ApiKey(String),
21}
22
23/// Custom statement for FeatherDB extensions that sqlparser doesn't handle
24#[derive(Debug, Clone)]
25pub enum CustomStatement {
26    /// SHOW GRANTS ON <table> or SHOW GRANTS FOR '<key>'
27    ShowGrants(ShowGrantsTarget),
28}
29
30/// Result of parsing - either a standard statement or a custom one
31#[derive(Debug)]
32#[allow(clippy::large_enum_variant)]
33pub enum ParseResult {
34    /// Standard SQL statement handled by sqlparser
35    Standard(Statement),
36    /// Custom FeatherDB statement
37    Custom(CustomStatement),
38}
39
40/// SQL parser wrapper with enhanced error messages
41pub struct Parser;
42
43impl Parser {
44    /// Parse a SQL string into statements with rich error context
45    pub fn parse(sql: &str) -> Result<Vec<Statement>> {
46        let dialect = GenericDialect {};
47
48        SqlParser::parse_sql(&dialect, sql).map_err(|e| Self::convert_parse_error(sql, e))
49    }
50
51    /// Parse a single statement with rich error context
52    pub fn parse_one(sql: &str) -> Result<Statement> {
53        let mut statements = Self::parse(sql)?;
54
55        if statements.is_empty() {
56            return Err(Error::Query(
57                QueryError::new("Empty query", sql, 1, 1)
58                    .with_help("Provide a valid SQL statement like SELECT, INSERT, UPDATE, DELETE, or CREATE TABLE")
59            ));
60        }
61
62        if statements.len() > 1 {
63            return Err(Error::Query(
64                QueryError::new("Multiple statements not supported", sql, 1, 1).with_help(
65                    "Execute one statement at a time, or use transactions for multiple operations",
66                ),
67            ));
68        }
69
70        Ok(statements.remove(0))
71    }
72
73    /// Parse a statement with support for custom FeatherDB extensions
74    ///
75    /// This handles statements like SHOW GRANTS that sqlparser doesn't support.
76    pub fn parse_extended(sql: &str) -> Result<ParseResult> {
77        // First, check for custom statements
78        if let Some(custom) = Self::try_parse_custom(sql)? {
79            return Ok(ParseResult::Custom(custom));
80        }
81
82        // Fall back to standard parsing
83        let stmt = Self::parse_one(sql)?;
84        Ok(ParseResult::Standard(stmt))
85    }
86
87    /// Parse SQL and convert to LogicalPlan, handling custom FeatherDB statements
88    ///
89    /// This is the unified entry point that handles both standard SQL statements
90    /// (via the Planner) and custom FeatherDB extensions (like SHOW GRANTS).
91    /// Returns the plan and any subquery plans collected during planning.
92    pub fn parse_to_plan(
93        sql: &str,
94        catalog: &Catalog,
95    ) -> Result<(LogicalPlan, HashMap<usize, LogicalPlan>)> {
96        match Self::parse_extended(sql)? {
97            ParseResult::Standard(stmt) => {
98                let planner = Planner::new(catalog);
99                let plan = planner.plan(&stmt)?;
100                let subquery_plans = planner.take_subquery_plans();
101                Ok((plan, subquery_plans))
102            }
103            ParseResult::Custom(custom) => {
104                Ok((Self::plan_custom(custom, catalog)?, HashMap::new()))
105            }
106        }
107    }
108
109    /// Convert a custom FeatherDB statement to a LogicalPlan
110    fn plan_custom(stmt: CustomStatement, catalog: &Catalog) -> Result<LogicalPlan> {
111        match stmt {
112            CustomStatement::ShowGrants(target) => {
113                // Convert parser's ShowGrantsTarget to planner's ShowGrantsTarget
114                let plan_target = match target {
115                    ShowGrantsTarget::Table(t) => PlanShowGrantsTarget::Table(t),
116                    ShowGrantsTarget::ApiKey(k) => PlanShowGrantsTarget::ApiKey(k),
117                };
118                let planner = Planner::new(catalog);
119                planner.plan_show_grants(plan_target)
120            }
121        }
122    }
123
124    /// Check if a SQL statement is read-only (for custom statements)
125    ///
126    /// Returns true for SELECT, SHOW, EXPLAIN, etc.
127    pub fn is_custom_read_only(stmt: &CustomStatement) -> bool {
128        match stmt {
129            // SHOW GRANTS is always read-only
130            CustomStatement::ShowGrants(_) => true,
131        }
132    }
133
134    /// Try to parse a custom FeatherDB statement
135    fn try_parse_custom(sql: &str) -> Result<Option<CustomStatement>> {
136        let sql_upper = sql.trim().to_uppercase();
137
138        // Check for SHOW GRANTS
139        if sql_upper.starts_with("SHOW GRANTS") {
140            return Self::parse_show_grants(sql).map(Some);
141        }
142
143        Ok(None)
144    }
145
146    /// Parse SHOW GRANTS statement
147    ///
148    /// Syntax:
149    /// - SHOW GRANTS ON <table_name>
150    /// - SHOW GRANTS FOR '<api_key_id>'
151    fn parse_show_grants(sql: &str) -> Result<CustomStatement> {
152        let sql_trimmed = sql.trim();
153        let sql_upper = sql_trimmed.to_uppercase();
154
155        // Remove "SHOW GRANTS" prefix
156        let rest = sql_trimmed[11..].trim();
157        let rest_upper = sql_upper[11..].trim();
158
159        if rest_upper.starts_with("ON ") {
160            // SHOW GRANTS ON <table>
161            let table_part = rest[3..].trim();
162
163            // Remove trailing semicolon if present
164            let table_name = table_part.trim_end_matches(';').trim();
165
166            if table_name.is_empty() {
167                return Err(Error::InvalidQuery {
168                    message: "SHOW GRANTS ON requires a table name".to_string(),
169                });
170            }
171
172            Ok(CustomStatement::ShowGrants(ShowGrantsTarget::Table(
173                table_name.to_string(),
174            )))
175        } else if rest_upper.starts_with("FOR ") {
176            // SHOW GRANTS FOR '<key>'
177            let key_part = rest[4..].trim();
178
179            // Extract the API key ID (may be quoted with single quotes)
180            let key_id = if key_part.starts_with('\'') && key_part.len() > 2 {
181                // Find the closing quote
182                let end = key_part[1..]
183                    .find('\'')
184                    .ok_or_else(|| Error::InvalidQuery {
185                        message: "Unterminated string in SHOW GRANTS FOR".to_string(),
186                    })?;
187                key_part[1..=end].to_string()
188            } else {
189                // Unquoted identifier
190                key_part
191                    .split_whitespace()
192                    .next()
193                    .unwrap_or("")
194                    .trim_end_matches(';')
195                    .to_string()
196            };
197
198            if key_id.is_empty() {
199                return Err(Error::InvalidQuery {
200                    message: "SHOW GRANTS FOR requires an API key ID".to_string(),
201                });
202            }
203
204            Ok(CustomStatement::ShowGrants(ShowGrantsTarget::ApiKey(
205                key_id,
206            )))
207        } else {
208            Err(Error::InvalidQuery {
209                message: "SHOW GRANTS requires ON <table> or FOR '<api_key_id>'".to_string(),
210            })
211        }
212    }
213
214    /// Convert sqlparser error to rich QueryError
215    fn convert_parse_error(sql: &str, error: sqlparser::parser::ParserError) -> Error {
216        let error_str = error.to_string();
217        let _ctx = QueryContext::new(sql); // Keep for future use
218
219        // Try to extract position from error message
220        let (line, column, message) = Self::extract_position_from_error(&error_str, sql);
221
222        // Try to find keyword suggestions
223        let suggestion = Self::find_keyword_suggestion(sql, &error_str, column);
224
225        let mut query_error = QueryError::new(&message, sql, line, column);
226
227        if let Some(sugg) = suggestion {
228            query_error = query_error.with_suggestion(sugg);
229        }
230
231        // Add helpful context based on error type
232        if error_str.contains("Expected") {
233            query_error = query_error.with_help("Check the SQL syntax near the indicated position");
234        }
235
236        Error::Query(query_error)
237    }
238
239    /// Extract line/column position from sqlparser error message
240    fn extract_position_from_error(error: &str, sql: &str) -> (usize, usize, String) {
241        // sqlparser errors often contain "at Line: X, Column: Y"
242        let mut line = 1;
243        let mut column = 1;
244        let mut message = error.to_string();
245
246        // Try to parse "Line: X, Column: Y" pattern
247        if let Some(line_pos) = error.find("Line:") {
248            if let Some(col_pos) = error.find("Column:") {
249                // Extract line number
250                let line_start = line_pos + 5;
251                let line_end = error[line_start..]
252                    .find(',')
253                    .map(|p| line_start + p)
254                    .unwrap_or(error.len());
255                if let Ok(l) = error[line_start..line_end].trim().parse::<usize>() {
256                    line = l;
257                }
258
259                // Extract column number
260                let col_start = col_pos + 7;
261                let col_end = error[col_start..]
262                    .find(|c: char| !c.is_ascii_digit())
263                    .map(|p| col_start + p)
264                    .unwrap_or(error.len());
265                if let Ok(c) = error[col_start..col_end].trim().parse::<usize>() {
266                    column = c;
267                }
268
269                // Clean up the message
270                if let Some(msg_end) = error.find(" at Line:") {
271                    message = error[..msg_end].to_string();
272                }
273            }
274        }
275
276        // Try to find position from "near" keyword
277        if let Some(near_pos) = error.find("near \"") {
278            let start = near_pos + 6;
279            if let Some(end) = error[start..].find('"') {
280                let token = &error[start..start + end];
281                // Find the token in the SQL
282                if let Some(token_pos) = sql.find(token) {
283                    let ctx = QueryContext::new(sql);
284                    let (l, c) = ctx.position_from_offset(token_pos);
285                    line = l;
286                    column = c;
287                }
288            }
289        }
290
291        (line, column, message)
292    }
293
294    /// Find keyword suggestions based on the error and SQL
295    fn find_keyword_suggestion(sql: &str, _error: &str, column: usize) -> Option<String> {
296        // Tokenize to find potential typos
297        let dialect = GenericDialect {};
298        let mut tokenizer = Tokenizer::new(&dialect, sql);
299
300        if let Ok(tokens) = tokenizer.tokenize() {
301            // Find the token near the error column
302            let mut current_col = 1;
303            for token in tokens {
304                match &token {
305                    Token::Word(word) => {
306                        let word_str = word.value.as_str();
307                        let word_len = word_str.len();
308
309                        // Check if this token is near the error position
310                        if current_col <= column && column <= current_col + word_len {
311                            // Check if it's a potential keyword typo
312                            if let Some(correct_keyword) = suggest_keyword(word_str) {
313                                return Some(format!("Did you mean '{}'?", correct_keyword));
314                            }
315                        }
316
317                        current_col += word_len;
318                    }
319                    Token::Whitespace(_) => {
320                        current_col += 1;
321                    }
322                    _ => {
323                        // Count token length approximately
324                        current_col += 1;
325                    }
326                }
327            }
328        }
329
330        // Check the first word for common typos
331        let first_word = sql.split_whitespace().next().unwrap_or("");
332        if let Some(correct_keyword) = suggest_keyword(first_word) {
333            return Some(format!("Did you mean '{}'?", correct_keyword));
334        }
335
336        None
337    }
338
339    /// Convert SQL data type to FeatherDB column type
340    pub fn convert_data_type(data_type: &ast::DataType) -> Result<ColumnType> {
341        match data_type {
342            ast::DataType::Boolean => Ok(ColumnType::Boolean),
343
344            ast::DataType::SmallInt(_)
345            | ast::DataType::Int(_)
346            | ast::DataType::Integer(_)
347            | ast::DataType::BigInt(_)
348            | ast::DataType::TinyInt(_) => Ok(ColumnType::Integer),
349
350            ast::DataType::Float(_)
351            | ast::DataType::Real
352            | ast::DataType::Double
353            | ast::DataType::DoublePrecision
354            | ast::DataType::Decimal(_)
355            | ast::DataType::Numeric(_) => Ok(ColumnType::Real),
356
357            ast::DataType::Char(len) | ast::DataType::Varchar(len) => {
358                let max_len = len.as_ref().and_then(|l| {
359                    if let ast::CharacterLength::IntegerLength { length, .. } = l {
360                        Some(*length as usize)
361                    } else {
362                        None
363                    }
364                });
365                Ok(ColumnType::Text { max_len })
366            }
367
368            ast::DataType::Text => Ok(ColumnType::Text { max_len: None }),
369
370            ast::DataType::Binary(_)
371            | ast::DataType::Varbinary(_)
372            | ast::DataType::Blob(_)
373            | ast::DataType::Bytea => Ok(ColumnType::Blob { max_len: None }),
374
375            ast::DataType::Timestamp(_, _) | ast::DataType::Datetime(_) | ast::DataType::Date => {
376                Ok(ColumnType::Timestamp)
377            }
378
379            other => Err(Error::Unsupported {
380                feature: format!("Data type: {:?}", other),
381            }),
382        }
383    }
384
385    /// Parse ALTER TABLE operations from an AlterTable statement
386    pub fn parse_alter_table_ops(
387        table_name: &ast::ObjectName,
388        operations: &[ast::AlterTableOperation],
389    ) -> Result<(String, Vec<AlterTableOp>)> {
390        let tbl_name = table_name
391            .0
392            .first()
393            .map(|i| i.value.clone())
394            .unwrap_or_default();
395
396        let mut ops = Vec::new();
397
398        for operation in operations {
399            let op = Self::convert_alter_operation(operation)?;
400            ops.push(op);
401        }
402
403        Ok((tbl_name, ops))
404    }
405
406    /// Convert a single ALTER TABLE operation from AST
407    fn convert_alter_operation(operation: &ast::AlterTableOperation) -> Result<AlterTableOp> {
408        match operation {
409            ast::AlterTableOperation::AddColumn {
410                column_def,
411                if_not_exists: _,
412                ..
413            } => {
414                let col_type = Self::convert_data_type(&column_def.data_type)?;
415                let mut constraints = Vec::new();
416
417                for option in &column_def.options {
418                    match &option.option {
419                        ast::ColumnOption::NotNull => {
420                            constraints.push(ColumnConstraint::NotNull);
421                        }
422                        ast::ColumnOption::Unique { is_primary, .. } => {
423                            if *is_primary {
424                                constraints.push(ColumnConstraint::PrimaryKey);
425                            } else {
426                                constraints.push(ColumnConstraint::Unique);
427                            }
428                        }
429                        ast::ColumnOption::Default(expr) => {
430                            let value = Self::eval_constant_expr(expr)?;
431                            constraints.push(ColumnConstraint::Default(value));
432                        }
433                        _ => {}
434                    }
435                }
436
437                let column = Column {
438                    name: column_def.name.value.clone(),
439                    column_type: col_type,
440                    constraints,
441                    position: 0, // Will be set during apply
442                };
443
444                // Note: sqlparser 0.40 doesn't support column_position (FIRST/AFTER)
445                // Column will be added at the end by default
446                Ok(AlterTableOp::AddColumn {
447                    column,
448                    position: None,
449                })
450            }
451
452            ast::AlterTableOperation::DropColumn {
453                column_name,
454                if_exists,
455                ..
456            } => Ok(AlterTableOp::DropColumn {
457                name: column_name.value.clone(),
458                if_exists: *if_exists,
459            }),
460
461            ast::AlterTableOperation::RenameColumn {
462                old_column_name,
463                new_column_name,
464            } => Ok(AlterTableOp::RenameColumn {
465                old_name: old_column_name.value.clone(),
466                new_name: new_column_name.value.clone(),
467            }),
468
469            ast::AlterTableOperation::RenameTable { table_name } => {
470                let new_name = table_name
471                    .0
472                    .first()
473                    .map(|i| i.value.clone())
474                    .unwrap_or_default();
475                Ok(AlterTableOp::RenameTable { new_name })
476            }
477
478            ast::AlterTableOperation::AlterColumn { column_name, op } => {
479                Self::convert_alter_column_op(&column_name.value, op)
480            }
481
482            other => Err(Error::Unsupported {
483                feature: format!("ALTER TABLE operation: {:?}", other),
484            }),
485        }
486    }
487
488    /// Convert ALTER COLUMN operations
489    fn convert_alter_column_op(
490        column_name: &str,
491        op: &ast::AlterColumnOperation,
492    ) -> Result<AlterTableOp> {
493        match op {
494            ast::AlterColumnOperation::SetNotNull => Ok(AlterTableOp::AlterColumnNullable {
495                name: column_name.to_string(),
496                nullable: false,
497            }),
498
499            ast::AlterColumnOperation::DropNotNull => Ok(AlterTableOp::AlterColumnNullable {
500                name: column_name.to_string(),
501                nullable: true,
502            }),
503
504            ast::AlterColumnOperation::SetDefault { value } => {
505                let default_value = Self::eval_constant_expr(value)?;
506                Ok(AlterTableOp::AlterColumnDefault {
507                    name: column_name.to_string(),
508                    default: Some(default_value),
509                })
510            }
511
512            ast::AlterColumnOperation::DropDefault => Ok(AlterTableOp::AlterColumnDefault {
513                name: column_name.to_string(),
514                default: None,
515            }),
516
517            other => Err(Error::Unsupported {
518                feature: format!("ALTER COLUMN operation: {:?}", other),
519            }),
520        }
521    }
522
523    /// Evaluate a constant expression to a Value
524    fn eval_constant_expr(expr: &ast::Expr) -> Result<Value> {
525        match expr {
526            ast::Expr::Value(v) => Self::convert_value(v),
527            ast::Expr::UnaryOp { op, expr } => {
528                let val = Self::eval_constant_expr(expr)?;
529                match op {
530                    ast::UnaryOperator::Minus => match val {
531                        Value::Integer(i) => Ok(Value::Integer(-i)),
532                        Value::Real(r) => Ok(Value::Real(-r)),
533                        _ => Err(Error::InvalidQuery {
534                            message: "Cannot negate non-numeric value".into(),
535                        }),
536                    },
537                    _ => Err(Error::Unsupported {
538                        feature: format!("Unary operator in constant: {:?}", op),
539                    }),
540                }
541            }
542            _ => Err(Error::InvalidQuery {
543                message: format!("Expected constant expression, got: {:?}", expr),
544            }),
545        }
546    }
547
548    /// Convert an AST Value to a FeatherDB Value
549    fn convert_value(value: &ast::Value) -> Result<Value> {
550        match value {
551            ast::Value::Number(n, _) => {
552                if n.contains('.') {
553                    Ok(Value::Real(n.parse().map_err(|_| Error::InvalidQuery {
554                        message: format!("Invalid number: {}", n),
555                    })?))
556                } else {
557                    Ok(Value::Integer(n.parse().map_err(|_| {
558                        Error::InvalidQuery {
559                            message: format!("Invalid integer: {}", n),
560                        }
561                    })?))
562                }
563            }
564            ast::Value::SingleQuotedString(s) | ast::Value::DoubleQuotedString(s) => {
565                Ok(Value::Text(s.clone()))
566            }
567            ast::Value::Boolean(b) => Ok(Value::Boolean(*b)),
568            ast::Value::Null => Ok(Value::Null),
569            _ => Err(Error::Unsupported {
570                feature: format!("Value type: {:?}", value),
571            }),
572        }
573    }
574
575    // =========================================================================
576    // Foreign Key Parsing
577    // =========================================================================
578
579    /// Parse foreign key constraints from table-level constraints
580    ///
581    /// Handles syntax like:
582    /// ```sql
583    /// FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
584    /// CONSTRAINT fk_name FOREIGN KEY (col1, col2) REFERENCES other(a, b)
585    /// ```
586    pub fn parse_foreign_key_constraint(
587        constraint: &ast::TableConstraint,
588    ) -> Result<Option<ForeignKey>> {
589        match constraint {
590            ast::TableConstraint::ForeignKey {
591                name,
592                columns,
593                foreign_table,
594                referred_columns,
595                on_delete,
596                on_update,
597                ..
598            } => {
599                let fk_columns: Vec<String> = columns.iter().map(|c| c.value.clone()).collect();
600                let ref_table = foreign_table
601                    .0
602                    .first()
603                    .map(|i| i.value.clone())
604                    .unwrap_or_default();
605                let ref_columns: Vec<String> =
606                    referred_columns.iter().map(|c| c.value.clone()).collect();
607
608                let mut fk = ForeignKey::new(fk_columns, ref_table, ref_columns);
609
610                // Set constraint name if provided
611                if let Some(name_ident) = name {
612                    fk = fk.with_name(name_ident.value.clone());
613                }
614
615                // Parse ON DELETE action
616                if let Some(action) = on_delete {
617                    fk = fk.on_delete(Self::convert_referential_action(action));
618                }
619
620                // Parse ON UPDATE action
621                if let Some(action) = on_update {
622                    fk = fk.on_update(Self::convert_referential_action(action));
623                }
624
625                Ok(Some(fk))
626            }
627            _ => Ok(None),
628        }
629    }
630
631    /// Parse inline foreign key reference from column options
632    ///
633    /// Handles syntax like:
634    /// ```sql
635    /// user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
636    /// ```
637    pub fn parse_inline_foreign_key(
638        column_name: &str,
639        options: &[ast::ColumnOptionDef],
640    ) -> Result<Option<ForeignKey>> {
641        for option in options {
642            if let ast::ColumnOption::ForeignKey {
643                foreign_table,
644                referred_columns,
645                on_delete,
646                on_update,
647                ..
648            } = &option.option
649            {
650                let ref_table = foreign_table
651                    .0
652                    .first()
653                    .map(|i| i.value.clone())
654                    .unwrap_or_default();
655
656                // If no referred columns specified, assume same column name
657                let ref_columns: Vec<String> = if referred_columns.is_empty() {
658                    vec![column_name.to_string()]
659                } else {
660                    referred_columns.iter().map(|c| c.value.clone()).collect()
661                };
662
663                let mut fk = ForeignKey::new(vec![column_name.to_string()], ref_table, ref_columns);
664
665                // Parse ON DELETE action
666                if let Some(action) = on_delete {
667                    fk = fk.on_delete(Self::convert_referential_action(action));
668                }
669
670                // Parse ON UPDATE action
671                if let Some(action) = on_update {
672                    fk = fk.on_update(Self::convert_referential_action(action));
673                }
674
675                return Ok(Some(fk));
676            }
677        }
678        Ok(None)
679    }
680
681    /// Convert sqlparser ReferentialAction to ForeignKeyAction
682    fn convert_referential_action(action: &ast::ReferentialAction) -> ForeignKeyAction {
683        match action {
684            ast::ReferentialAction::NoAction => ForeignKeyAction::NoAction,
685            ast::ReferentialAction::Restrict => ForeignKeyAction::Restrict,
686            ast::ReferentialAction::Cascade => ForeignKeyAction::Cascade,
687            ast::ReferentialAction::SetNull => ForeignKeyAction::SetNull,
688            ast::ReferentialAction::SetDefault => ForeignKeyAction::SetDefault,
689        }
690    }
691
692    /// Parse all foreign keys from a CREATE TABLE statement
693    ///
694    /// Returns a vector of ForeignKey constraints found in both:
695    /// - Table-level FOREIGN KEY constraints
696    /// - Inline REFERENCES clauses on columns
697    pub fn parse_create_table_foreign_keys(
698        columns: &[ast::ColumnDef],
699        constraints: &[ast::TableConstraint],
700    ) -> Result<Vec<ForeignKey>> {
701        let mut foreign_keys = Vec::new();
702
703        // Parse inline foreign keys from column definitions
704        for column in columns {
705            if let Some(fk) = Self::parse_inline_foreign_key(&column.name.value, &column.options)? {
706                foreign_keys.push(fk);
707            }
708        }
709
710        // Parse table-level foreign key constraints
711        for constraint in constraints {
712            if let Some(fk) = Self::parse_foreign_key_constraint(constraint)? {
713                foreign_keys.push(fk);
714            }
715        }
716
717        Ok(foreign_keys)
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    #[test]
726    fn test_parse_select() {
727        let stmt = Parser::parse_one("SELECT * FROM users").unwrap();
728        assert!(matches!(stmt, Statement::Query(_)));
729    }
730
731    #[test]
732    fn test_parse_insert() {
733        let stmt = Parser::parse_one("INSERT INTO users (id, name) VALUES (1, 'Alice')").unwrap();
734        assert!(matches!(stmt, Statement::Insert { .. }));
735    }
736
737    #[test]
738    fn test_parse_create_table() {
739        let stmt =
740            Parser::parse_one("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
741                .unwrap();
742        assert!(matches!(stmt, Statement::CreateTable { .. }));
743    }
744
745    #[test]
746    fn test_parse_error() {
747        let result = Parser::parse_one("SELEKT * FROM users");
748        assert!(result.is_err());
749
750        // Check that we get a helpful error
751        if let Err(e) = result {
752            let error_str = e.to_string();
753            // Should contain suggestion for SELEKT -> SELECT
754            assert!(error_str.contains("SELECT") || error_str.contains("error"));
755        }
756    }
757
758    #[test]
759    fn test_parse_error_with_typo() {
760        let result = Parser::parse_one("SELECT * FORM users");
761        assert!(result.is_err());
762
763        if let Err(Error::Query(qe)) = result {
764            // Should suggest FROM
765            assert!(
766                qe.suggestion
767                    .as_ref()
768                    .map(|s| s.contains("FROM"))
769                    .unwrap_or(false)
770                    || qe.message.contains("FROM")
771                    || qe.message.contains("FORM"),
772                "Error should mention FROM typo: {:?}",
773                qe
774            );
775        }
776    }
777
778    #[test]
779    fn test_empty_query_error() {
780        let result = Parser::parse_one("");
781        assert!(result.is_err());
782
783        if let Err(Error::Query(qe)) = result {
784            assert!(qe.help.is_some(), "Empty query error should have help text");
785        }
786    }
787
788    #[test]
789    fn test_multiline_error_position() {
790        let sql = "SELECT *\nFORM users\nWHERE id = 1";
791        let result = Parser::parse_one(sql);
792        assert!(result.is_err());
793        // Error should be at or near line 2
794    }
795
796    // =========================================================================
797    // Foreign Key Parsing Tests
798    // =========================================================================
799
800    #[test]
801    fn test_parse_create_table_with_foreign_key() {
802        let sql = r#"
803            CREATE TABLE orders (
804                id INTEGER PRIMARY KEY,
805                user_id INTEGER NOT NULL,
806                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
807            )
808        "#;
809        let stmt = Parser::parse_one(sql).unwrap();
810        assert!(matches!(stmt, Statement::CreateTable { .. }));
811
812        if let Statement::CreateTable {
813            columns,
814            constraints,
815            ..
816        } = stmt
817        {
818            let fks = Parser::parse_create_table_foreign_keys(&columns, &constraints).unwrap();
819            assert_eq!(fks.len(), 1);
820            assert_eq!(fks[0].columns, vec!["user_id"]);
821            assert_eq!(fks[0].references_table, "users");
822            assert_eq!(fks[0].references_columns, vec!["id"]);
823            assert_eq!(fks[0].on_delete, ForeignKeyAction::Cascade);
824        }
825    }
826
827    #[test]
828    fn test_parse_inline_foreign_key() {
829        let sql = r#"
830            CREATE TABLE items (
831                id INTEGER PRIMARY KEY,
832                order_id INTEGER REFERENCES orders(id)
833            )
834        "#;
835        let stmt = Parser::parse_one(sql).unwrap();
836
837        if let Statement::CreateTable {
838            columns,
839            constraints,
840            ..
841        } = stmt
842        {
843            let fks = Parser::parse_create_table_foreign_keys(&columns, &constraints).unwrap();
844            assert_eq!(fks.len(), 1);
845            assert_eq!(fks[0].columns, vec!["order_id"]);
846            assert_eq!(fks[0].references_table, "orders");
847            assert_eq!(fks[0].references_columns, vec!["id"]);
848        }
849    }
850
851    #[test]
852    fn test_parse_named_foreign_key_constraint() {
853        let sql = r#"
854            CREATE TABLE orders (
855                id INTEGER PRIMARY KEY,
856                user_id INTEGER,
857                CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
858            )
859        "#;
860        let stmt = Parser::parse_one(sql).unwrap();
861
862        if let Statement::CreateTable {
863            columns,
864            constraints,
865            ..
866        } = stmt
867        {
868            let fks = Parser::parse_create_table_foreign_keys(&columns, &constraints).unwrap();
869            assert_eq!(fks.len(), 1);
870            assert_eq!(fks[0].name, Some("fk_orders_user".to_string()));
871        }
872    }
873
874    #[test]
875    fn test_parse_foreign_key_with_on_update() {
876        let sql = r#"
877            CREATE TABLE orders (
878                id INTEGER PRIMARY KEY,
879                user_id INTEGER,
880                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
881            )
882        "#;
883        let stmt = Parser::parse_one(sql).unwrap();
884
885        if let Statement::CreateTable {
886            columns,
887            constraints,
888            ..
889        } = stmt
890        {
891            let fks = Parser::parse_create_table_foreign_keys(&columns, &constraints).unwrap();
892            assert_eq!(fks.len(), 1);
893            assert_eq!(fks[0].on_delete, ForeignKeyAction::SetNull);
894            assert_eq!(fks[0].on_update, ForeignKeyAction::Cascade);
895        }
896    }
897
898    #[test]
899    fn test_parse_multi_column_foreign_key() {
900        let sql = r#"
901            CREATE TABLE order_items (
902                order_id INTEGER,
903                product_id INTEGER,
904                PRIMARY KEY (order_id, product_id),
905                FOREIGN KEY (order_id, product_id) REFERENCES orders(id, product_id)
906            )
907        "#;
908        let stmt = Parser::parse_one(sql).unwrap();
909
910        if let Statement::CreateTable {
911            columns,
912            constraints,
913            ..
914        } = stmt
915        {
916            let fks = Parser::parse_create_table_foreign_keys(&columns, &constraints).unwrap();
917            assert_eq!(fks.len(), 1);
918            assert_eq!(fks[0].columns, vec!["order_id", "product_id"]);
919            assert_eq!(fks[0].references_columns, vec!["id", "product_id"]);
920        }
921    }
922
923    #[test]
924    fn test_parse_multiple_foreign_keys() {
925        let sql = r#"
926            CREATE TABLE order_items (
927                id INTEGER PRIMARY KEY,
928                order_id INTEGER REFERENCES orders(id),
929                product_id INTEGER,
930                FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
931            )
932        "#;
933        let stmt = Parser::parse_one(sql).unwrap();
934
935        if let Statement::CreateTable {
936            columns,
937            constraints,
938            ..
939        } = stmt
940        {
941            let fks = Parser::parse_create_table_foreign_keys(&columns, &constraints).unwrap();
942            assert_eq!(fks.len(), 2);
943
944            // Inline FK comes first
945            assert_eq!(fks[0].columns, vec!["order_id"]);
946            assert_eq!(fks[0].references_table, "orders");
947
948            // Table-level FK comes second
949            assert_eq!(fks[1].columns, vec!["product_id"]);
950            assert_eq!(fks[1].references_table, "products");
951            assert_eq!(fks[1].on_delete, ForeignKeyAction::Cascade);
952        }
953    }
954
955    // =========================================================================
956    // SHOW GRANTS Parsing Tests
957    // =========================================================================
958
959    #[test]
960    fn test_parse_show_grants_on_table() {
961        let result = Parser::parse_extended("SHOW GRANTS ON users").unwrap();
962        if let ParseResult::Custom(CustomStatement::ShowGrants(ShowGrantsTarget::Table(name))) =
963            result
964        {
965            assert_eq!(name, "users");
966        } else {
967            panic!("Expected ShowGrants Table");
968        }
969    }
970
971    #[test]
972    fn test_parse_show_grants_on_table_with_semicolon() {
973        let result = Parser::parse_extended("SHOW GRANTS ON users;").unwrap();
974        if let ParseResult::Custom(CustomStatement::ShowGrants(ShowGrantsTarget::Table(name))) =
975            result
976        {
977            assert_eq!(name, "users");
978        } else {
979            panic!("Expected ShowGrants Table");
980        }
981    }
982
983    #[test]
984    fn test_parse_show_grants_for_key() {
985        let result = Parser::parse_extended("SHOW GRANTS FOR 'fdb_abc123'").unwrap();
986        if let ParseResult::Custom(CustomStatement::ShowGrants(ShowGrantsTarget::ApiKey(key))) =
987            result
988        {
989            assert_eq!(key, "fdb_abc123");
990        } else {
991            panic!("Expected ShowGrants ApiKey");
992        }
993    }
994
995    #[test]
996    fn test_parse_show_grants_for_key_unquoted() {
997        let result = Parser::parse_extended("SHOW GRANTS FOR my_key").unwrap();
998        if let ParseResult::Custom(CustomStatement::ShowGrants(ShowGrantsTarget::ApiKey(key))) =
999            result
1000        {
1001            assert_eq!(key, "my_key");
1002        } else {
1003            panic!("Expected ShowGrants ApiKey");
1004        }
1005    }
1006
1007    #[test]
1008    fn test_parse_show_grants_error_no_target() {
1009        let result = Parser::parse_extended("SHOW GRANTS");
1010        assert!(result.is_err());
1011    }
1012
1013    #[test]
1014    fn test_parse_show_grants_error_empty_table() {
1015        let result = Parser::parse_extended("SHOW GRANTS ON ");
1016        assert!(result.is_err());
1017    }
1018
1019    #[test]
1020    fn test_parse_grant_statement() {
1021        // GRANT is handled by sqlparser, just verify it parses
1022        let result = Parser::parse_one("GRANT SELECT ON users TO test_key");
1023        assert!(result.is_ok());
1024        if let Ok(Statement::Grant { .. }) = result {
1025            // Expected
1026        } else {
1027            panic!("Expected Grant statement");
1028        }
1029    }
1030
1031    #[test]
1032    fn test_parse_revoke_statement() {
1033        // REVOKE is handled by sqlparser, just verify it parses
1034        let result = Parser::parse_one("REVOKE SELECT ON users FROM test_key");
1035        assert!(result.is_ok());
1036        if let Ok(Statement::Revoke { .. }) = result {
1037            // Expected
1038        } else {
1039            panic!("Expected Revoke statement");
1040        }
1041    }
1042}