on main help sign in
Fix changing nullability not working with alter_column
7cc4f23fabianlindfors
README.md+16−1
@@ -351,7 +351,7 @@ up = "data['path']['to']['value'] #>> '{}'"
351
351
352The `alter_column` action enables many different changes to an existing column, for example renaming, changing type and changing existing values.
352The `alter_column` action enables many different changes to an existing column, for example renaming, changing type and changing existing values.
353
353
354−When performing more complex changes than a rename, `up` and `down` must be provided. These should be SQL expressions which determine how to transform between the new and old version of the column. Inside those expressions, you can reference the current column value by the column name.
354+When performing more complex changes than a rename, `up` and `down` should be provided. These should be SQL expressions which determine how to transform between the new and old version of the column. Inside those expressions, you can reference the current column value by the column name.
355
355
356*Example: rename `last_name` column on `users` table to `family_name`*
356*Example: rename `last_name` column on `users` table to `family_name`*
357
357
@@ -395,6 +395,21 @@ down = "index - 1" # Decrement to revert for old schema
395 name = "index"
395 name = "index"
396```
396```
397
397
398+*Example: make `name` column not nullable`*
399+
400+```toml
401+[[actions]]
402+type = "alter_column"
403+table = "users"
404+column = "name"
405+
406+# Use "N/A" for any rows that currently have a NULL name
407+up = "COALESCE(name, 'N/A')"
408+
409+ [actions.changes]
410+ nullable = false
411+```
412+
398*Example: change default value of `created_at` column to current time*
413*Example: change default value of `created_at` column to current time*
399
414
400```toml
415```toml
src/migrations/alter_column.rs+1−1
@@ -187,7 +187,7 @@ impl Action for AlterColumn {
187 // This constraint is set as NOT VALID so it doesn't apply to existing rows and
187 // This constraint is set as NOT VALID so it doesn't apply to existing rows and
188 // the existing rows don't need to be scanned under an exclusive lock.
188 // the existing rows don't need to be scanned under an exclusive lock.
189 // Thanks to this, we can set the full column as NOT NULL later with minimal locking.
189 // Thanks to this, we can set the full column as NOT NULL later with minimal locking.
190− if !column.nullable {
190+ if !self.changes.nullable.unwrap_or(column.nullable) {
191 let query = format!(
191 let query = format!(
192 r#"
192 r#"
193 ALTER TABLE "{table}"
193 ALTER TABLE "{table}"
tests/alter_column.rs+100−1
@@ -113,7 +113,6 @@ fn alter_column_set_not_null() {
113 table = "users"
113 table = "users"
114 column = "name"
114 column = "name"
115 up = "COALESCE(name, 'TEST_DEFAULT_VALUE')"
115 up = "COALESCE(name, 'TEST_DEFAULT_VALUE')"
116− down = "name"
117
116
118 [actions.changes]
117 [actions.changes]
119 nullable = false
118 nullable = false
@@ -159,6 +158,106 @@ fn alter_column_set_not_null() {
159 .query_one("SELECT name from users WHERE id = 4", &[])
158 .query_one("SELECT name from users WHERE id = 4", &[])
160 .unwrap();
159 .unwrap();
161 assert_eq!("Jane Doe", result.get::<_, &str>("name"));
160 assert_eq!("Jane Doe", result.get::<_, &str>("name"));
161+
162+ // Ensure NULL can't be inserted using the new schema
163+ let result = new_db.simple_query("INSERT INTO users (id, name) VALUES (5, NULL)");
164+ assert!(result.is_err(), "expected insert to fail");
165+ });
166+
167+ test.after_completion(|db| {
168+ // Ensure NULL can't be inserted
169+ let result = db.simple_query("INSERT INTO users (id, name) VALUES (5, NULL)");
170+ assert!(result.is_err(), "expected insert to fail");
171+ });
172+
173+ test.after_abort(|db| {
174+ // Ensure NULL can be inserted
175+ let result = db.simple_query("INSERT INTO users (id, name) VALUES (5, NULL)");
176+ assert!(result.is_ok(), "expected insert to succeed");
177+ });
178+
179+ test.run();
180+}
181+
182+#[test]
183+fn alter_column_set_nullable() {
184+ let mut test = Test::new("Set column nullable");
185+
186+ test.first_migration(
187+ r#"
188+ name = "create_user_table"
189+
190+ [[actions]]
191+ type = "create_table"
192+ name = "users"
193+ primary_key = ["id"]
194+
195+ [[actions.columns]]
196+ name = "id"
197+ type = "INTEGER"
198+
199+ [[actions.columns]]
200+ name = "name"
201+ type = "TEXT"
202+ nullable = false
203+ "#,
204+ );
205+
206+ test.second_migration(
207+ r#"
208+ name = "set_name_nullable"
209+
210+ [[actions]]
211+ type = "alter_column"
212+ table = "users"
213+ column = "name"
214+ down = "COALESCE(name, 'TEST_DEFAULT_VALUE')"
215+
216+ [actions.changes]
217+ nullable = true
218+ "#,
219+ );
220+
221+ test.after_first(|db| {
222+ // Insert a test user
223+ db.simple_query(
224+ "
225+ INSERT INTO users (id, name) VALUES
226+ (1, 'John Doe')
227+ ",
228+ )
229+ .unwrap();
230+ });
231+
232+ test.intermediate(|old_db, new_db| {
233+ // Insert data using new schema and make sure the old schema gets correct values
234+ new_db
235+ .simple_query("INSERT INTO users (id, name) VALUES (2, NULL)")
236+ .unwrap();
237+ let result = old_db
238+ .query_one("SELECT name from users WHERE id = 2", &[])
239+ .unwrap();
240+ assert_eq!("TEST_DEFAULT_VALUE", result.get::<_, &str>("name"));
241+
242+ // Ensure NULL can't be inserted using the old schema
243+ let result = old_db.simple_query("INSERT INTO users (id, name) VALUES (3, NULL)");
244+ assert!(result.is_err(), "expected insert to fail");
245+
246+ // Ensure NULL can be inserted using the new schema
247+ let result = new_db.simple_query("INSERT INTO users (id, name) VALUES (4, NULL)");
248+ assert!(result.is_ok(), "expected insert to succeed");
249+ });
250+
251+ test.after_completion(|db| {
252+ // Ensure NULL can be inserted
253+ let result = db.simple_query("INSERT INTO users (id, name) VALUES (5, NULL)");
254+ assert!(result.is_ok(), "expected insert to succeed");
255+ });
256+
257+ test.after_abort(|db| {
258+ // Ensure NULL can't be inserted
259+ let result = db.simple_query("INSERT INTO users (id, name) VALUES (5, NULL)");
260+ assert!(result.is_err(), "expected insert to fail");
162 });
261 });
163
262
164 test.run();
263 test.run();