Skip to main content

vibesql_executor/
view_ddl.rs

1//! View DDL executor
2
3use vibesql_ast::{CreateViewStmt, DropViewStmt};
4use vibesql_catalog::{ViewDefinition, ViewDropBehavior};
5use vibesql_storage::Database;
6
7use crate::errors::ExecutorError;
8
9/// Executor for view DDL statements
10pub struct ViewExecutor;
11
12impl ViewExecutor {
13    /// Execute CREATE VIEW, CREATE OR REPLACE VIEW, or CREATE VIEW IF NOT EXISTS
14    pub fn execute_create_view(
15        stmt: &CreateViewStmt,
16        database: &mut Database,
17    ) -> Result<String, ExecutorError> {
18        let view_exists = database.catalog.get_view(&stmt.view_name).is_some();
19
20        // If IF NOT EXISTS and view already exists, just return success
21        if stmt.if_not_exists && view_exists {
22            return Ok(format!("View '{}' already exists (skipped)", stmt.view_name));
23        }
24
25        // If OR REPLACE, drop the view first if it exists
26        if stmt.or_replace && view_exists {
27            // Drop the existing view (no cascade needed for OR REPLACE)
28            database.catalog.drop_view(&stmt.view_name, false).map_err(|e| {
29                ExecutorError::StorageError(format!("Failed to drop existing view: {:?}", e))
30            })?;
31        }
32
33        // Tag temp views with the `temp` schema so they surface via
34        // sqlite_temp_master and are excluded from sqlite_master (#5541),
35        // mirroring the temp-trigger (#5532) and temp-index (#5513) tags.
36        let schema = if stmt.temporary { Some("temp".to_string()) } else { None };
37
38        // Create the view definition
39        let view_def = if let Some(ref sql) = stmt.sql_definition {
40            ViewDefinition::new_with_sql(
41                stmt.view_name.clone(),
42                stmt.columns.clone(),
43                *stmt.query.clone(),
44                stmt.with_check_option,
45                sql.clone(),
46            )
47        } else {
48            ViewDefinition::new(
49                stmt.view_name.clone(),
50                stmt.columns.clone(),
51                *stmt.query.clone(),
52                stmt.with_check_option,
53            )
54        }
55        .with_schema(schema);
56
57        // Add to catalog
58        database
59            .catalog
60            .create_view(view_def)
61            .map_err(|e| ExecutorError::StorageError(format!("Failed to create view: {:?}", e)))?;
62
63        if stmt.or_replace {
64            Ok(format!("View '{}' created or replaced", stmt.view_name))
65        } else {
66            Ok(format!("View '{}' created", stmt.view_name))
67        }
68    }
69
70    /// Execute DROP VIEW
71    pub fn execute_drop_view(
72        stmt: &DropViewStmt,
73        database: &mut Database,
74    ) -> Result<String, ExecutorError> {
75        // Determine drop behavior:
76        // - CASCADE: drop dependent views recursively
77        // - RESTRICT (explicit): fail if dependents exist
78        // - Neither: SQLite-compatible behavior (just drop, ignore dependents)
79        let drop_behavior = if stmt.cascade {
80            ViewDropBehavior::Cascade
81        } else if stmt.restrict {
82            ViewDropBehavior::Restrict
83        } else {
84            ViewDropBehavior::Silent // SQLite-compatible: allow dropping even with dependents
85        };
86
87        // Cascade-drop the INSTEAD OF triggers defined ON this view, matching
88        // sqlite3 3.51.0: an INSTEAD OF trigger cannot outlive the view it is
89        // attached to, so `DROP VIEW v` removes every trigger whose `ON v`
90        // target is the dropped view. Resolve the view's temp/main schema tag
91        // *before* the catalog removes it so the temp-shadows-main filter can be
92        // applied (a main trigger on a main view must not be dropped when a
93        // same-named temp view is dropped, and vice versa). A trigger on a
94        // *different* view is left alone. See `Catalog::drop_view_triggers`
95        // (view analogue of the table path in #5597).
96        let view_is_temp =
97            database.catalog.get_view(&stmt.view_name).is_some_and(|v| v.is_temp());
98
99        // Drop the view
100        let result = database.catalog.drop_view_with_behavior(&stmt.view_name, drop_behavior);
101
102        match result {
103            Ok(()) => {
104                let dropped_triggers =
105                    database.catalog.drop_view_triggers(&stmt.view_name, view_is_temp);
106                if dropped_triggers.is_empty() {
107                    Ok(format!("View '{}' dropped", stmt.view_name))
108                } else {
109                    Ok(format!(
110                        "View '{}' and {} associated trigger(s) dropped",
111                        stmt.view_name,
112                        dropped_triggers.len()
113                    ))
114                }
115            }
116            Err(e) => {
117                // If IF EXISTS and view doesn't exist, that's OK
118                if stmt.if_exists
119                    && matches!(e, vibesql_catalog::errors::CatalogError::ViewNotFound(_))
120                {
121                    Ok(format!("View '{}' does not exist (skipped)", stmt.view_name))
122                } else {
123                    Err(ExecutorError::StorageError(format!("Failed to drop view: {:?}", e)))
124                }
125            }
126        }
127    }
128}