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(¤t, &coldef)),
187 AlterTableStmt::RenameColumn(rename) => crate::alter_rewrite::rename_column(
188 ¤t,
189 &rename.old_column_name,
190 &rename.new_column_name,
191 ),
192 AlterTableStmt::DropColumn(drop) => {
193 crate::alter_rewrite::drop_column(¤t, &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}