Skip to main content

vibesql_executor/
drop_table.rs

1//! DROP TABLE statement execution
2
3use vibesql_ast::DropTableStmt;
4use vibesql_storage::Database;
5
6use crate::{errors::ExecutorError, privilege_checker::PrivilegeChecker};
7
8/// Executor for DROP TABLE statements
9pub struct DropTableExecutor;
10
11impl DropTableExecutor {
12    /// Execute a DROP TABLE statement
13    ///
14    /// # Arguments
15    ///
16    /// * `stmt` - The DROP TABLE statement AST node
17    /// * `database` - The database to drop the table from
18    ///
19    /// # Returns
20    ///
21    /// Success message or error
22    ///
23    /// # Examples
24    ///
25    /// ```
26    /// use vibesql_ast::{ColumnDef, CreateTableStmt, DropTableStmt};
27    /// use vibesql_executor::{CreateTableExecutor, DropTableExecutor};
28    /// use vibesql_storage::Database;
29    /// use vibesql_types::DataType;
30    ///
31    /// let mut db = Database::new();
32    /// let create_stmt = CreateTableStmt { temporary: false,
33    ///     if_not_exists: false,
34    ///     table_name: "users".to_string(),
35    ///     columns: vec![ColumnDef {
36    ///         name: "id".to_string(),
37    ///         data_type: DataType::Integer,
38    ///         nullable: false,
39    ///         constraints: vec![],
40    ///         default_value: None,
41    ///         comment: None,
42    ///         generated_expr: None, is_exact_integer_type: false,
43    ///     }],
44    ///     table_constraints: vec![],
45    ///     table_options: vec![],
46    ///     quoted: false,
47    ///     name_source: None,
48    ///     as_query: None, without_rowid: false,
49    /// };
50    /// CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
51    ///
52    /// let stmt = DropTableStmt { table_name: "users".to_string(), if_exists: false, quoted: false };
53    ///
54    /// let result = DropTableExecutor::execute(&stmt, &mut db);
55    /// assert!(result.is_ok());
56    /// ```
57    pub fn execute(stmt: &DropTableStmt, database: &mut Database) -> Result<String, ExecutorError> {
58        // `sqlite_master`/`sqlite_schema` may never be dropped. SQLite errors
59        // `table sqlite_master may not be dropped` (sqlite3 3.51.0, table-5.2),
60        // echoing the canonical (lowercase) name regardless of the user's casing
61        // — `DROP TABLE SQLITE_MASTER` still reports `sqlite_master`. This guard
62        // precedes the existence check because sqlite_master is a virtual table
63        // that is not registered in the catalog. Other non-existent `sqlite_`
64        // names fall through to the normal "no such table" path, matching
65        // sqlite3 (issue #5614).
66        if crate::sqlite_schema::is_sqlite_schema_table(&stmt.table_name) {
67            return Err(ExecutorError::SqliteCompatError(
68                "table sqlite_master may not be dropped".to_string(),
69            ));
70        }
71
72        // Check if table exists
73        let table_exists = database.catalog.table_exists(&stmt.table_name);
74
75        // If IF EXISTS is specified and table doesn't exist, succeed silently
76        if stmt.if_exists && !table_exists {
77            return Ok(format!("Table '{}' does not exist (IF EXISTS specified)", stmt.table_name));
78        }
79
80        // If table doesn't exist and IF EXISTS is not specified, return error
81        if !table_exists {
82            return Err(ExecutorError::TableNotFound(stmt.table_name.clone()));
83        }
84
85        // Check DROP privilege on the table
86        PrivilegeChecker::check_drop(database, &stmt.table_name)?;
87
88        // Drop all indexes associated with this table first
89        let dropped_indexes = database.catalog.drop_table_indexes(&stmt.table_name);
90        let index_count = dropped_indexes.len();
91
92        // Drop physical indexes from storage. Use the schema-qualified name so a
93        // temp-table index and a same-named main-table index are dropped
94        // independently — the storage index manager is schema-aware (#5540), and
95        // a bare name would resolve temp-shadows-main and could drop the wrong
96        // one.
97        for index in &dropped_indexes {
98            let qualified = format!("{}.{}", index.schema(), index.name);
99            // Try to drop from B-tree storage (ignore errors if not found)
100            let _ = database.drop_index(&qualified);
101            // Try to drop from spatial storage (ignore errors if not found).
102            // Spatial indexes are schema-aware (#5558), so use the
103            // schema-qualified name to drop exactly this index and leave a
104            // same-named index in another schema intact.
105            let _ = database.drop_spatial_index(&qualified);
106        }
107
108        // Cascade-drop triggers defined ON this table, matching sqlite3 3.51.0:
109        // `DROP TABLE t` removes every trigger whose `ON t` target is the dropped
110        // table (temp or main). Must run while the table still exists in the
111        // catalog so the trigger's schema binding can resolve. A trigger that only
112        // *references* the table from its body, and a view referencing the table,
113        // are intentionally left alone (sqlite3 leaves those — the view errors on
114        // use). See `Catalog::drop_table_triggers`.
115        let dropped_triggers = database.catalog.drop_table_triggers(&stmt.table_name);
116        let trigger_count = dropped_triggers.len();
117
118        // Drop the table from storage (this also removes from catalog)
119        database
120            .drop_table(&stmt.table_name)
121            .map_err(|e| ExecutorError::StorageError(e.to_string()))?;
122
123        // Return success message
124        match (index_count, trigger_count) {
125            (0, 0) => Ok(format!("Table '{}' dropped successfully", stmt.table_name)),
126            (i, 0) => Ok(format!(
127                "Table '{}' and {} associated index(es) dropped successfully",
128                stmt.table_name, i
129            )),
130            (0, t) => Ok(format!(
131                "Table '{}' and {} associated trigger(s) dropped successfully",
132                stmt.table_name, t
133            )),
134            (i, t) => Ok(format!(
135                "Table '{}', {} associated index(es), and {} associated trigger(s) dropped successfully",
136                stmt.table_name, i, t
137            )),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use vibesql_ast::{ColumnDef, CreateTableStmt};
145    use vibesql_types::DataType;
146
147    use super::*;
148    use crate::CreateTableExecutor;
149
150    #[test]
151    fn test_drop_existing_table() {
152        let mut db = Database::new();
153
154        // Create a table first
155        let create_stmt = CreateTableStmt {
156            temporary: false,
157            if_not_exists: false,
158            table_name: "users".to_string(),
159            columns: vec![ColumnDef {
160                name: "id".to_string(),
161                data_type: DataType::Integer,
162                nullable: false,
163                constraints: vec![],
164                default_value: None,
165                comment: None,
166                generated_expr: None,
167                is_exact_integer_type: false,
168            }],
169            table_constraints: vec![],
170            table_options: vec![],
171            quoted: false,
172            name_source: None,
173            as_query: None,
174            without_rowid: false,
175        };
176        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
177        assert!(db.catalog.table_exists("users"));
178
179        // Now drop it
180        let drop_stmt =
181            DropTableStmt { table_name: "users".to_string(), if_exists: false, quoted: false };
182
183        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
184        assert!(result.is_ok());
185        assert_eq!(result.unwrap(), "Table 'users' dropped successfully");
186
187        // Verify table no longer exists
188        assert!(!db.catalog.table_exists("users"));
189        assert!(db.get_table("users").is_none());
190    }
191
192    #[test]
193    fn test_drop_nonexistent_table_without_if_exists() {
194        let mut db = Database::new();
195
196        let drop_stmt = DropTableStmt {
197            table_name: "nonexistent".to_string(),
198            if_exists: false,
199            quoted: false,
200        };
201
202        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
203        assert!(result.is_err());
204        assert!(matches!(result, Err(ExecutorError::TableNotFound(_))));
205    }
206
207    #[test]
208    fn test_drop_nonexistent_table_with_if_exists() {
209        let mut db = Database::new();
210
211        let drop_stmt =
212            DropTableStmt { table_name: "nonexistent".to_string(), if_exists: true, quoted: false };
213
214        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
215        assert!(result.is_ok());
216        assert_eq!(result.unwrap(), "Table 'nonexistent' does not exist (IF EXISTS specified)");
217    }
218
219    #[test]
220    fn test_drop_existing_table_with_if_exists() {
221        let mut db = Database::new();
222
223        // Create a table first
224        let create_stmt = CreateTableStmt {
225            temporary: false,
226            if_not_exists: false,
227            table_name: "products".to_string(),
228            columns: vec![ColumnDef {
229                name: "id".to_string(),
230                data_type: DataType::Integer,
231                nullable: false,
232                constraints: vec![],
233                default_value: None,
234                comment: None,
235                generated_expr: None,
236                is_exact_integer_type: false,
237            }],
238            table_constraints: vec![],
239            table_options: vec![],
240            quoted: false,
241            name_source: None,
242            as_query: None,
243            without_rowid: false,
244        };
245        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
246
247        // Drop it with IF EXISTS
248        let drop_stmt =
249            DropTableStmt { table_name: "products".to_string(), if_exists: true, quoted: false };
250
251        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
252        assert!(result.is_ok());
253        assert_eq!(result.unwrap(), "Table 'products' dropped successfully");
254
255        // Verify table no longer exists
256        assert!(!db.catalog.table_exists("products"));
257    }
258
259    #[test]
260    fn test_drop_table_with_data() {
261        let mut db = Database::new();
262
263        // Create a table with data
264        let create_stmt = CreateTableStmt {
265            temporary: false,
266            if_not_exists: false,
267            table_name: "customers".to_string(),
268            columns: vec![
269                ColumnDef {
270                    name: "id".to_string(),
271                    data_type: DataType::Integer,
272                    nullable: false,
273                    constraints: vec![],
274                    default_value: None,
275                    comment: None,
276                    generated_expr: None,
277                    is_exact_integer_type: false,
278                },
279                ColumnDef {
280                    name: "name".to_string(),
281                    data_type: DataType::Varchar { max_length: Some(100) },
282                    nullable: false,
283                    constraints: vec![],
284                    default_value: None,
285                    comment: None,
286                    generated_expr: None,
287                    is_exact_integer_type: false,
288                },
289            ],
290            table_constraints: vec![],
291            table_options: vec![],
292            quoted: false,
293            name_source: None,
294            as_query: None,
295            without_rowid: false,
296        };
297        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
298
299        // Insert some data
300        use vibesql_storage::Row;
301        use vibesql_types::SqlValue;
302        let row =
303            Row::new(vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("Alice"))]);
304        db.insert_row("customers", row).unwrap();
305
306        // Verify data exists
307        assert_eq!(db.get_table("customers").unwrap().row_count(), 1);
308
309        // Drop the table
310        let drop_stmt =
311            DropTableStmt { table_name: "customers".to_string(), if_exists: false, quoted: false };
312
313        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
314        assert!(result.is_ok());
315
316        // Verify table and data are gone
317        assert!(!db.catalog.table_exists("customers"));
318        assert!(db.get_table("customers").is_none());
319    }
320
321    #[test]
322    fn test_drop_and_recreate_table() {
323        let mut db = Database::new();
324
325        // Create table
326        let create_stmt = CreateTableStmt {
327            temporary: false,
328            if_not_exists: false,
329            table_name: "temp".to_string(),
330            columns: vec![ColumnDef {
331                name: "id".to_string(),
332                data_type: DataType::Integer,
333                nullable: false,
334                constraints: vec![],
335                default_value: None,
336                comment: None,
337                generated_expr: None,
338                is_exact_integer_type: false,
339            }],
340            table_constraints: vec![],
341            table_options: vec![],
342            quoted: false,
343            name_source: None,
344            as_query: None,
345            without_rowid: false,
346        };
347        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
348
349        // Drop it
350        let drop_stmt =
351            DropTableStmt { table_name: "temp".to_string(), if_exists: false, quoted: false };
352        DropTableExecutor::execute(&drop_stmt, &mut db).unwrap();
353
354        // Recreate it
355        let result = CreateTableExecutor::execute(&create_stmt, &mut db);
356        assert!(result.is_ok());
357        assert!(db.catalog.table_exists("temp"));
358    }
359
360    #[test]
361    fn test_drop_multiple_tables() {
362        let mut db = Database::new();
363
364        // Create multiple tables
365        for name in &["table1", "table2", "table3"] {
366            let create_stmt = CreateTableStmt {
367                temporary: false,
368                if_not_exists: false,
369                table_name: name.to_string(),
370                columns: vec![ColumnDef {
371                    name: "id".to_string(),
372                    data_type: DataType::Integer,
373                    nullable: false,
374                    constraints: vec![],
375                    default_value: None,
376                    comment: None,
377                    generated_expr: None,
378                    is_exact_integer_type: false,
379                }],
380                table_constraints: vec![],
381                table_options: vec![],
382                quoted: false,
383                name_source: None,
384                as_query: None,
385                without_rowid: false,
386            };
387            CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
388        }
389
390        assert_eq!(db.list_tables().len(), 3);
391
392        // Drop them one by one
393        for name in &["table1", "table2", "table3"] {
394            let drop_stmt =
395                DropTableStmt { table_name: name.to_string(), if_exists: false, quoted: false };
396            let result = DropTableExecutor::execute(&drop_stmt, &mut db);
397            assert!(result.is_ok());
398        }
399
400        assert_eq!(db.list_tables().len(), 0);
401    }
402
403    #[test]
404    fn test_drop_table_case_sensitivity() {
405        let mut db = Database::new();
406
407        // Create table with specific case
408        let create_stmt = CreateTableStmt {
409            temporary: false,
410            if_not_exists: false,
411            table_name: "MyTable".to_string(),
412            columns: vec![ColumnDef {
413                name: "id".to_string(),
414                data_type: DataType::Integer,
415                nullable: false,
416                constraints: vec![],
417                default_value: None,
418                comment: None,
419                generated_expr: None,
420                is_exact_integer_type: false,
421            }],
422            table_constraints: vec![],
423            table_options: vec![],
424            quoted: false,
425            name_source: None,
426            as_query: None,
427            without_rowid: false,
428        };
429        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
430
431        // Try to drop with exact case - should succeed
432        let drop_stmt =
433            DropTableStmt { table_name: "MyTable".to_string(), if_exists: false, quoted: false };
434        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
435        assert!(result.is_ok());
436    }
437
438    #[test]
439    fn test_drop_table_cascades_to_indexes() {
440        use vibesql_ast::{CreateIndexStmt, IndexColumn, OrderDirection};
441
442        use crate::CreateIndexExecutor;
443
444        let mut db = Database::new();
445
446        // Create table
447        let create_stmt = CreateTableStmt {
448            temporary: false,
449            if_not_exists: false,
450            table_name: "users".to_string(),
451            columns: vec![
452                ColumnDef {
453                    name: "id".to_string(),
454                    data_type: DataType::Integer,
455                    nullable: false,
456                    constraints: vec![],
457                    default_value: None,
458                    comment: None,
459                    generated_expr: None,
460                    is_exact_integer_type: false,
461                },
462                ColumnDef {
463                    name: "email".to_string(),
464                    data_type: DataType::Varchar { max_length: Some(255) },
465                    nullable: false,
466                    constraints: vec![],
467                    default_value: None,
468                    comment: None,
469                    generated_expr: None,
470                    is_exact_integer_type: false,
471                },
472            ],
473            table_constraints: vec![],
474            table_options: vec![],
475            quoted: false,
476            name_source: None,
477            as_query: None,
478            without_rowid: false,
479        };
480        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
481
482        // Create indexes on the table
483        let index1_stmt = CreateIndexStmt {
484            index_name: "idx_users_email".to_string(),
485            if_not_exists: false,
486            table_name: "users".to_string(),
487            index_type: vibesql_ast::IndexType::BTree { unique: false },
488            columns: vec![IndexColumn::Column {
489                column_name: "email".to_string(),
490                prefix_length: None,
491                direction: OrderDirection::Asc,
492            }],
493            where_clause: None,
494        };
495        CreateIndexExecutor::execute(&index1_stmt, &mut db).unwrap();
496
497        let index2_stmt = CreateIndexStmt {
498            index_name: "idx_users_id".to_string(),
499            if_not_exists: false,
500            table_name: "users".to_string(),
501            index_type: vibesql_ast::IndexType::BTree { unique: false },
502            columns: vec![IndexColumn::Column {
503                column_name: "id".to_string(),
504                prefix_length: None,
505                direction: OrderDirection::Asc,
506            }],
507            where_clause: None,
508        };
509        CreateIndexExecutor::execute(&index2_stmt, &mut db).unwrap();
510
511        // Verify indexes exist
512        assert!(db.index_exists("idx_users_email"));
513        assert!(db.index_exists("idx_users_id"));
514
515        // Drop the table
516        let drop_stmt =
517            DropTableStmt { table_name: "users".to_string(), if_exists: false, quoted: false };
518        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
519        assert!(result.is_ok());
520
521        // Verify table is dropped
522        assert!(!db.catalog.table_exists("users"));
523
524        // Verify indexes are also dropped (CASCADE behavior)
525        assert!(!db.index_exists("idx_users_email"));
526        assert!(!db.index_exists("idx_users_id"));
527    }
528
529    #[test]
530    fn test_drop_and_recreate_table_with_same_index_names() {
531        use vibesql_ast::{CreateIndexStmt, IndexColumn, OrderDirection};
532
533        use crate::CreateIndexExecutor;
534
535        let mut db = Database::new();
536
537        // Create table
538        let create_stmt = CreateTableStmt {
539            temporary: false,
540            if_not_exists: false,
541            table_name: "products".to_string(),
542            columns: vec![
543                ColumnDef {
544                    name: "id".to_string(),
545                    data_type: DataType::Integer,
546                    nullable: false,
547                    constraints: vec![],
548                    default_value: None,
549                    comment: None,
550                    generated_expr: None,
551                    is_exact_integer_type: false,
552                },
553                ColumnDef {
554                    name: "name".to_string(),
555                    data_type: DataType::Varchar { max_length: Some(100) },
556                    nullable: false,
557                    constraints: vec![],
558                    default_value: None,
559                    comment: None,
560                    generated_expr: None,
561                    is_exact_integer_type: false,
562                },
563            ],
564            table_constraints: vec![],
565            table_options: vec![],
566            quoted: false,
567            name_source: None,
568            as_query: None,
569            without_rowid: false,
570        };
571        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
572
573        // Create index
574        let index_stmt = CreateIndexStmt {
575            index_name: "idx_products_name".to_string(),
576            if_not_exists: false,
577            table_name: "products".to_string(),
578            index_type: vibesql_ast::IndexType::BTree { unique: false },
579            columns: vec![IndexColumn::Column {
580                column_name: "name".to_string(),
581                prefix_length: None,
582                direction: OrderDirection::Asc,
583            }],
584            where_clause: None,
585        };
586        CreateIndexExecutor::execute(&index_stmt, &mut db).unwrap();
587
588        // Verify index exists
589        assert!(db.index_exists("idx_products_name"));
590
591        // Drop the table (should cascade to drop index)
592        let drop_stmt =
593            DropTableStmt { table_name: "products".to_string(), if_exists: false, quoted: false };
594        DropTableExecutor::execute(&drop_stmt, &mut db).unwrap();
595
596        // Verify both table and index are dropped
597        assert!(!db.catalog.table_exists("products"));
598        assert!(!db.index_exists("idx_products_name"));
599
600        // Recreate table with same name
601        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
602
603        // Create index with same name - should succeed (no IndexAlreadyExists error)
604        let result = CreateIndexExecutor::execute(&index_stmt, &mut db);
605        assert!(result.is_ok(), "Should be able to recreate index with same name after table drop");
606        assert!(db.index_exists("idx_products_name"));
607    }
608
609    #[test]
610    fn test_drop_table_without_indexes() {
611        let mut db = Database::new();
612
613        // Create table without indexes
614        let create_stmt = CreateTableStmt {
615            temporary: false,
616            if_not_exists: false,
617            table_name: "simple_table".to_string(),
618            columns: vec![ColumnDef {
619                name: "id".to_string(),
620                data_type: DataType::Integer,
621                nullable: false,
622                constraints: vec![],
623                default_value: None,
624                comment: None,
625                generated_expr: None,
626                is_exact_integer_type: false,
627            }],
628            table_constraints: vec![],
629            table_options: vec![],
630            quoted: false,
631            name_source: None,
632            as_query: None,
633            without_rowid: false,
634        };
635        CreateTableExecutor::execute(&create_stmt, &mut db).unwrap();
636
637        // Drop table without indexes - should still work
638        let drop_stmt = DropTableStmt {
639            table_name: "simple_table".to_string(),
640            if_exists: false,
641            quoted: false,
642        };
643        let result = DropTableExecutor::execute(&drop_stmt, &mut db);
644        assert!(result.is_ok(), "Dropping table without indexes should still work");
645        assert!(!db.catalog.table_exists("simple_table"));
646    }
647}