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}