Skip to main content

vibesql_executor/advanced_objects/
views.rs

1//! Executor for VIEW objects (SQL:1999)
2
3use vibesql_ast::*;
4use vibesql_catalog::ViewDropBehavior;
5use vibesql_storage::Database;
6
7use crate::errors::ExecutorError;
8
9/// Execute CREATE VIEW statement
10pub fn execute_create_view(stmt: &CreateViewStmt, db: &mut Database) -> Result<(), ExecutorError> {
11    use vibesql_catalog::ViewDefinition;
12
13    // Check if view already exists
14    let view_exists = db.catalog.get_view(&stmt.view_name).is_some();
15
16    // If IF NOT EXISTS and view already exists, just return success
17    if stmt.if_not_exists && view_exists {
18        return Ok(());
19    }
20
21    // Guard against exponentially self-referential view nests before we (eagerly)
22    // execute the view body to derive its columns. VibeSQL materializes views by
23    // executing their queries, so a doubling nest (v2=v1∪v1, v4=v2∪v2, …) would
24    // expand exponentially and hang here. Match SQLite (ticket [d58ccbb3f1b],
25    // view3.test / #5394) by raising `too many references to "<view>": max 65535`
26    // at CREATE VIEW time instead. See crate::select::view_reference_guard.
27    crate::select::view_reference_guard::check_view_reference_limit(&stmt.query, db)?;
28
29    // If no explicit column list is provided, derive column names from the query.
30    // This ensures views with SELECT * preserve original column names. Use simple
31    // column names (without table prefix) for view schema compatibility.
32    //
33    // VibeSQL materializes views by executing them, so a deeply nested (doubling)
34    // view nest expands exponentially and would hang while deriving columns here
35    // (#5394, view3.test). When the body would re-materialize many views, derive
36    // the columns statically from the catalog instead. Ordinary views keep the
37    // eager execution path so all existing CREATE-time validations
38    // (set-operation arity, ORDER BY term resolution, etc.) surface as before.
39    const STATIC_COLUMN_DERIVATION_THRESHOLD: u64 = 64;
40    let columns = if stmt.columns.is_some() {
41        stmt.columns.clone()
42    } else {
43        let body_is_expensive =
44            crate::select::view_reference_guard::max_view_reference_count(&stmt.query, db)
45                > STATIC_COLUMN_DERIVATION_THRESHOLD;
46
47        // Static fast path for expensive bodies only (avoids exponential
48        // re-materialization of deep view nests).
49        let static_cols = if body_is_expensive {
50            crate::select::cte::try_static_select_columns(&stmt.query, db)
51        } else {
52            None
53        };
54
55        if let Some(cols) = static_cols {
56            Some(cols)
57        } else {
58            // Execute the query once to derive column names.
59            //
60            // SQLite-compatible error prefixing: in SQLite the "error in view <name>: "
61            // prefix is exclusively an ALTER-time schema-reparse phenomenon (alter.c);
62            // query-time view expansion errors are bare. VibeSQL compiles views eagerly,
63            // so both error classes surface here at CREATE VIEW time. We allow-list the
64            // prefix to OrderByTermNotInResultSet only — the sole class the TCL suite
65            // expects prefixed (window1.test 32.10, altertab.test). All other variants
66            // (notably SetOperationColumnMismatch, see select7.test 8.2) pass through
67            // bare, matching SQLite's query-time messages.
68            use crate::select::SelectExecutor;
69            let executor = SelectExecutor::new(db);
70            let result = executor.execute_with_simple_columns(&stmt.query).map_err(|e| match e {
71                e @ ExecutorError::OrderByTermNotInResultSet { .. } => ExecutorError::SqliteCompatError(
72                    format!("error in view {}: {}", stmt.view_name, e),
73                ),
74                other => other,
75            })?;
76            Some(result.columns)
77        }
78    };
79
80    // Tag temp views with the `temp` schema so they surface via
81    // sqlite_temp_master and are excluded from sqlite_master (#5541), mirroring
82    // the temp-trigger schema tag (#5532) and temp-index tag (#5513).
83    let schema = if stmt.temporary { Some("temp".to_string()) } else { None };
84
85    let view = if let Some(ref sql) = stmt.sql_definition {
86        ViewDefinition::new_with_sql(
87            stmt.view_name.clone(),
88            columns,
89            (*stmt.query).clone(),
90            stmt.with_check_option,
91            sql.clone(),
92        )
93    } else {
94        ViewDefinition::new(
95            stmt.view_name.clone(),
96            columns,
97            (*stmt.query).clone(),
98            stmt.with_check_option,
99        )
100    }
101    .with_schema(schema);
102
103    if stmt.or_replace || (stmt.if_not_exists && !view_exists) {
104        // For OR REPLACE, drop the view if it exists, then CREATE
105        if view_exists && stmt.or_replace {
106            let _ = db.catalog.drop_view(&stmt.view_name, false);
107        }
108        db.catalog.create_view(view)?;
109    } else {
110        // Regular CREATE VIEW (will fail if view already exists)
111        db.catalog.create_view(view)?;
112    }
113    Ok(())
114}
115
116/// Execute DROP VIEW statement
117pub fn execute_drop_view(stmt: &DropViewStmt, db: &mut Database) -> Result<(), ExecutorError> {
118    // Check if view exists
119    let view_exists = db.catalog.get_view(&stmt.view_name).is_some();
120
121    // If IF EXISTS is specified and view doesn't exist, succeed silently
122    if stmt.if_exists && !view_exists {
123        return Ok(());
124    }
125
126    // Determine drop behavior:
127    // - CASCADE: drop dependent views recursively
128    // - RESTRICT (explicit): fail if dependents exist
129    // - Neither: SQLite-compatible behavior (just drop, ignore dependents)
130    let drop_behavior = if stmt.cascade {
131        ViewDropBehavior::Cascade
132    } else if stmt.restrict {
133        ViewDropBehavior::Restrict
134    } else {
135        ViewDropBehavior::Silent // SQLite-compatible: allow dropping even with dependents
136    };
137
138    // Resolve the view's temp/main schema tag *before* the catalog removes it so
139    // the INSTEAD OF trigger cascade can apply the temp-shadows-main filter.
140    let view_is_temp = db.catalog.get_view(&stmt.view_name).is_some_and(|v| v.is_temp());
141
142    db.catalog.drop_view_with_behavior(&stmt.view_name, drop_behavior)?;
143
144    // Cascade-drop the INSTEAD OF triggers defined ON this view, matching sqlite3
145    // 3.51.0: an INSTEAD OF trigger cannot outlive the view it is attached to, so
146    // `DROP VIEW v` removes every trigger whose `ON v` target is the dropped view
147    // (leaving triggers on a *different* view alone). See
148    // `Catalog::drop_view_triggers` (view analogue of the table path in #5597).
149    db.catalog.drop_view_triggers(&stmt.view_name, view_is_temp);
150    Ok(())
151}