Skip to main content

vibesql_executor/alter/
mod.rs

1//! ALTER TABLE executor
2
3mod columns;
4mod constraints;
5mod table_options;
6mod validation;
7
8use vibesql_ast::*;
9use vibesql_storage::Database;
10
11use crate::{errors::ExecutorError, privilege_checker::PrivilegeChecker};
12
13/// Executor for ALTER TABLE statements
14pub struct AlterTableExecutor;
15
16impl AlterTableExecutor {
17    /// Execute an ALTER TABLE statement.
18    ///
19    /// Convenience wrapper over [`execute_with_source`](Self::execute_with_source)
20    /// for callers that do not have the original statement text (e.g. an AST
21    /// built programmatically). Without the source text, ADD COLUMN falls back
22    /// to reconstructing `sqlite_master.sql` from the schema rather than editing
23    /// the verbatim text in place. See issue #5625.
24    pub fn execute(
25        stmt: &AlterTableStmt,
26        database: &mut Database,
27    ) -> Result<String, ExecutorError> {
28        Self::execute_with_source(stmt, database, None)
29    }
30
31    /// Execute an ALTER TABLE statement, optionally given the verbatim original
32    /// statement text (`alter_sql`).
33    ///
34    /// After a successful structural ALTER, two pieces of bookkeeping run
35    /// (issue #5625):
36    ///
37    /// 1. **Catalog schema sync.** Column operations (ADD/DROP/RENAME/MODIFY)
38    ///    only mutate the *storage* `Table` copy of the schema. The *catalog*
39    ///    copy — read by `sqlite_master`, `PRAGMA table_info`, and DML column
40    ///    resolution — is then re-synced from the storage copy so both stay
41    ///    consistent. (RENAME TABLE syncs via drop+create and is exempt.)
42    ///
43    /// 2. **`sql_source` upkeep.** SQLite edits the verbatim `CREATE TABLE` text
44    ///    in `sqlite_master.sql` in place on ALTER rather than reconstructing it.
45    ///    For ADD COLUMN, RENAME COLUMN, DROP COLUMN, and RENAME TO the stored
46    ///    verbatim text is edited to match SQLite (when `alter_sql` is available
47    ///    / the structure permits; RENAME TO is handled in
48    ///    `table_options::execute_rename_table`). For the remaining variants, and
49    ///    whenever an in-place edit cannot apply cleanly, the stale text is
50    ///    invalidated so it is reconstructed from the (now-synced) schema. Either
51    ///    way the stored text remains re-parseable on reload (issue #5619).
52    pub fn execute_with_source(
53        stmt: &AlterTableStmt,
54        database: &mut Database,
55        alter_sql: Option<&str>,
56    ) -> Result<String, ExecutorError> {
57        // Get table name from the statement and check ALTER privilege
58        let table_name = match stmt {
59            AlterTableStmt::AddColumn(s) => &s.table_name,
60            AlterTableStmt::DropColumn(s) => &s.table_name,
61            AlterTableStmt::AlterColumn(s) => match s {
62                AlterColumnStmt::SetDefault { table_name, .. } => table_name,
63                AlterColumnStmt::DropDefault { table_name, .. } => table_name,
64                AlterColumnStmt::SetNotNull { table_name, .. } => table_name,
65                AlterColumnStmt::DropNotNull { table_name, .. } => table_name,
66            },
67            AlterTableStmt::AddConstraint(s) => &s.table_name,
68            AlterTableStmt::DropConstraint(s) => &s.table_name,
69            AlterTableStmt::RenameTable(s) => &s.table_name,
70            AlterTableStmt::RenameColumn(s) => &s.table_name,
71            AlterTableStmt::ModifyColumn(s) => &s.table_name,
72            AlterTableStmt::ChangeColumn(s) => &s.table_name,
73        };
74        PrivilegeChecker::check_alter(database, table_name)?;
75
76        let result = match stmt {
77            AlterTableStmt::AddColumn(add_column) => {
78                columns::execute_add_column(add_column, database)
79            }
80            AlterTableStmt::DropColumn(drop_column) => {
81                columns::execute_drop_column(drop_column, database)
82            }
83            AlterTableStmt::AlterColumn(alter_column) => {
84                columns::execute_alter_column(alter_column, database)
85            }
86            AlterTableStmt::AddConstraint(add_constraint) => {
87                constraints::execute_add_constraint(add_constraint, database)
88            }
89            AlterTableStmt::DropConstraint(drop_constraint) => {
90                constraints::execute_drop_constraint(drop_constraint, database)
91            }
92            AlterTableStmt::RenameTable(rename_table) => {
93                // For RENAME TABLE, invalidate both old and new table names
94                let old_name = &rename_table.table_name;
95                let new_name = &rename_table.new_table_name;
96                // `execute_rename_table` edits the verbatim CREATE TABLE text in
97                // place (rewriting the table name to the double-quoted new name,
98                // matching sqlite3) and keeps the catalog + storage copies in
99                // sync via its drop+create, so no further bookkeeping is needed
100                // here (issue #5634).
101                let result = table_options::execute_rename_table(rename_table, database);
102                if result.is_ok() {
103                    // Invalidate the database-level columnar cache for both table names
104                    // since the cache key is based on table name
105                    database.invalidate_columnar_cache(old_name);
106                    database.invalidate_columnar_cache(new_name);
107                }
108                return result;
109            }
110            AlterTableStmt::RenameColumn(rename_column) => {
111                columns::execute_rename_column(rename_column, database)
112            }
113            AlterTableStmt::ModifyColumn(modify_column) => {
114                columns::execute_modify_column(modify_column, database)
115            }
116            AlterTableStmt::ChangeColumn(change_column) => {
117                columns::execute_change_column(change_column, database)
118            }
119        };
120
121        // Invalidate the database-level columnar cache since table structure changed.
122        // This ensures subsequent reads via `database.get_columnar()` return
123        // fresh data with the updated schema rather than stale cached data.
124        if result.is_ok() {
125            database.invalidate_columnar_cache(table_name);
126
127            // Update the stored verbatim CREATE TABLE text: edit it in place for
128            // the variants SQLite edits in place (ADD COLUMN / RENAME COLUMN),
129            // otherwise invalidate so it is reconstructed (issue #5625).
130            update_sql_source_after_alter(database, table_name, stmt, alter_sql);
131
132            // Re-sync the catalog schema copy from the (mutated) storage copy.
133            // Column ALTERs only touched the storage `Table`; the catalog copy —
134            // read by sqlite_master / PRAGMA table_info / DML resolution — would
135            // otherwise stay stale (issue #5625).
136            sync_catalog_schema_from_storage(database, table_name);
137        }
138
139        result
140    }
141}
142
143/// Copy the (just-mutated) storage `Table` schema for `table_name` into the
144/// catalog so both copies of the schema stay consistent after a column ALTER.
145///
146/// VibeSQL keeps a table's schema in two places: the storage `Table` (mutated
147/// by the ALTER column executors) and the catalog `TableSchema` (read by
148/// `sqlite_master`, `PRAGMA table_info`, and column resolution for DML). They
149/// start identical at CREATE time; this restores that invariant after an ALTER.
150/// No-op if the storage table is missing. See issue #5625.
151fn sync_catalog_schema_from_storage(database: &mut Database, table_name: &str) {
152    let schema = match database.get_table(table_name) {
153        Some(table) => table.schema.clone(),
154        None => return,
155    };
156    database.catalog.replace_table_schema(table_name, schema);
157}
158
159/// Edit (or invalidate) the verbatim `CREATE TABLE` text on the storage `Table`
160/// schema for `table_name` after a successful column ALTER.
161///
162/// For ADD COLUMN, RENAME COLUMN, and DROP COLUMN — the cases SQLite edits in
163/// place — the stored text is edited to match SQLite (when the verbatim
164/// `alter_sql` is available / the structure permits). (RENAME TO is edited in
165/// place separately, in `table_options::execute_rename_table`.) For every other
166/// variant, and whenever the in-place edit cannot be applied cleanly, the stale
167/// text is invalidated so `sqlite_master.sql` falls back to reconstruction from
168/// the synced schema. The result is always re-parseable on reload (issue #5619).
169fn update_sql_source_after_alter(
170    database: &mut Database,
171    table_name: &str,
172    stmt: &AlterTableStmt,
173    alter_sql: Option<&str>,
174) {
175    let Some(table) = database.get_table_mut(table_name) else {
176        return;
177    };
178    let Some(current) = table.schema.sql_source.clone() else {
179        // No verbatim text to edit; nothing to do (reconstruction is used).
180        return;
181    };
182
183    let edited: Option<String> = match stmt {
184        AlterTableStmt::AddColumn(_) => alter_sql
185            .and_then(crate::alter_rewrite::extract_add_column_text)
186            .and_then(|coldef| crate::alter_rewrite::append_column(&current, &coldef)),
187        AlterTableStmt::RenameColumn(rename) => crate::alter_rewrite::rename_column(
188            &current,
189            &rename.old_column_name,
190            &rename.new_column_name,
191        ),
192        AlterTableStmt::DropColumn(drop) => {
193            crate::alter_rewrite::drop_column(&current, &drop.column_name)
194        }
195        // ALTER/MODIFY/CHANGE COLUMN, ADD/DROP CONSTRAINT: SQLite's in-place
196        // editing for these is not yet matched; reconstruct instead.
197        _ => None,
198    };
199
200    match edited {
201        Some(text) => table.schema_mut().set_sql_source(text),
202        None => table.schema_mut().invalidate_sql_source(),
203    }
204}