1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
//! `Schema::table`: adds columns, indexes and constraints to an existing table.
use super::Blueprint;
use super::column::ColumnDefault;
use super::constraints::{Index, IndexKind};
use super::ddl::index_statement;
use super::schema_builder::{Schema, execute_statements, prepare_postgres_enums, quoted_table};
use super::validation::validate_table_name;
use crate::Error;
impl Schema {
/// Alters an existing table: adds the blueprint's columns
/// (`ALTER TABLE ... ADD COLUMN`), then its unique constraints, indexes,
/// foreign keys and CHECK constraints. Added in v13.
///
/// SQLite cannot add a constraint to an existing table. There a unique
/// constraint, or a `unique()` column, becomes the equivalent
/// `CREATE UNIQUE INDEX`, and a foreign key on a column added in the same
/// call becomes that column's `REFERENCES` clause (the column must then
/// be nullable without a default). Any other SQLite foreign key (on an
/// existing column, or a second one on the same new column), a CHECK,
/// a primary-key column, a `NOT NULL` column without a default or a
/// `CURRENT_TIMESTAMP` default fails with
/// [`Error::UnsupportedSchemaChange`] before any statement runs.
///
/// The statements run in order, in the task-scoped transaction when one is
/// active; MySQL/MariaDB commit each DDL statement on its own.
pub async fn table<F>(table_name: &str, callback: F) -> Result<(), Error>
where
F: FnOnce(&mut Blueprint),
{
validate_table_name(table_name)?;
let mut blueprint = Blueprint::new();
callback(&mut blueprint);
let driver = crate::Orm::driver()?;
let statements =
blueprint.alter_statements(table_name, driver, crate::database::ANY_POOL)?;
prepare_postgres_enums(&blueprint, driver).await?;
execute_statements(&statements).await
}
}
fn unsupported(driver: &str, change: String) -> Error {
Error::UnsupportedSchemaChange {
driver: driver.to_string(),
change,
}
}
impl Blueprint {
/// The `ALTER TABLE`/`CREATE INDEX` statements of [`Schema::table`],
/// validated as a whole before any of them runs.
pub(super) fn alter_statements(
&self,
table: &str,
driver: &str,
any_pool: bool,
) -> Result<Vec<String>, Error> {
self.reject_unreadable_temporal_columns(driver, any_pool)?;
// Validates every column, constraint and index and, on MySQL, the
// keys on TEXT columns, exactly like `CREATE TABLE`.
self.render(Some(table), driver, any_pool)?;
let sqlite = driver == "sqlite";
let quoted = quoted_table(driver, table);
let mut statements = vec![];
let mut sqlite_unique_columns = vec![];
for column in &self.columns {
// SQLite cannot add a UNIQUE column; a unique index is the same
// constraint.
let mut definition =
self.column_definition(column, driver, any_pool, column.is_unique && !sqlite)?;
if sqlite {
self.check_sqlite_added_column(column, driver)?;
if column.is_unique {
sqlite_unique_columns.push(column.name.as_str());
}
if let Some(foreign) = self
.foreign_keys
.iter()
.find(|foreign| foreign.column == column.name)
{
if let Some(name) = &foreign.name {
super::constraints::validate_constraint_name(name)?;
definition.push_str(&format!(" CONSTRAINT {name}"));
}
definition.push(' ');
definition.push_str(&foreign.column_reference_sql(driver)?);
}
}
statements.push(format!("ALTER TABLE {quoted} ADD COLUMN {definition};"));
}
for column in sqlite_unique_columns {
statements.push(index_statement(
&Index::new(&[column], IndexKind::Unique),
table,
driver,
)?);
}
for index in &self.indexes {
if index.kind == IndexKind::UniqueConstraint && !sqlite {
let name = index
.name
.clone()
.unwrap_or_else(|| index.default_name(table));
super::constraints::validate_constraint_name(&name)?;
statements.push(format!(
"ALTER TABLE {quoted} ADD CONSTRAINT {name} UNIQUE ({});",
index.columns_sql()?
));
} else {
statements.push(index_statement(index, table, driver)?);
}
}
for foreign in &self.foreign_keys {
if sqlite {
if !self
.columns
.iter()
.any(|column| column.name == foreign.column)
{
return Err(unsupported(
driver,
format!(
"SQLite cannot add a foreign key to the existing column `{}`; rebuild the table with Schema::create",
foreign.column
),
));
}
continue;
}
let name = foreign
.name
.clone()
.unwrap_or_else(|| foreign.default_name(table));
super::constraints::validate_constraint_name(&name)?;
statements.push(format!(
"ALTER TABLE {quoted} ADD CONSTRAINT {name} {};",
foreign.body_sql(driver)?
));
}
for check in &self.checks {
if sqlite {
return Err(unsupported(
driver,
"SQLite cannot add a CHECK constraint to an existing table; rebuild the table with Schema::create".to_string(),
));
}
statements.push(format!("ALTER TABLE {quoted} ADD {};", check.sql()?));
}
Ok(statements)
}
/// SQLite's `ADD COLUMN` limits, reported as a typed error instead of the
/// database's message after earlier statements ran.
fn check_sqlite_added_column(&self, column: &super::Column, driver: &str) -> Result<(), Error> {
let reason = if column.is_primary_key {
Some("a PRIMARY KEY column")
} else if matches!(column.default_value, Some(ColumnDefault::CurrentTimestamp)) {
Some("a column whose default is the current time")
} else if !column.is_nullable
&& matches!(column.default_value, None | Some(ColumnDefault::Null))
{
Some("a NOT NULL column without a default")
} else if self
.foreign_keys
.iter()
.any(|foreign| foreign.column == column.name)
&& !matches!(column.default_value, None | Some(ColumnDefault::Null))
{
Some("a foreign-key column with a non-NULL default")
} else if self
.foreign_keys
.iter()
.filter(|foreign| foreign.column == column.name)
.count()
> 1
{
// An added column has one `REFERENCES` clause; a second key would
// be dropped silently.
Some("a column with more than one foreign key")
} else {
None
};
match reason {
Some(reason) => Err(unsupported(
driver,
format!(
"SQLite cannot add {reason} (`{}`) to an existing table",
column.name
),
)),
None => Ok(()),
}
}
}
#[cfg(test)]
#[path = "alter_tests.rs"]
mod tests;