on main help sign in
Fix changing nullability not working with alter_column

The existing test to set a column as NOT NULL didn't actually check that the constraint was added, which has also been fixed. I also added a new test for the opposite change, making a NOT NULL column nullable. The README has also been updated with a new example for setting a column as NOT NULL.

commit7cc4f23db9f0f7ecceddbadc787fc9c2aa874d5b authorfabianlindfors <fabian@flapplabs.se> dateFri 4 Feb 2022, 17:47 (5 years ago) parentaff5527
3 files changed+117−3
README.md+16−1 src/migrations/alter_column.rs+1−1 tests/alter_column.rs+100−1
README.md+16−1
@@ -351,7 +351,7 @@ up = "data['path']['to']['value'] #>> '{}'"
351351
352352The `alter_column` action enables many different changes to an existing column, for example renaming, changing type and changing existing values.
353353
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.
355355
356356*Example: rename `last_name` column on `users` table to `family_name`*
357357
@@ -395,6 +395,21 @@ down = "index - 1" # Decrement to revert for old schema
395395 name = "index"
396396```
397397
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+
398413*Example: change default value of `created_at` column to current time*
399414
400415```toml
src/migrations/alter_column.rs+1−1
@@ -187,7 +187,7 @@ impl Action for AlterColumn {
187187 // This constraint is set as NOT VALID so it doesn't apply to existing rows and
188188 // the existing rows don't need to be scanned under an exclusive lock.
189189 // 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) {
191191 let query = format!(
192192 r#"
193193 ALTER TABLE "{table}"
tests/alter_column.rs+100−1
@@ -113,7 +113,6 @@ fn alter_column_set_not_null() {
113113 table = "users"
114114 column = "name"
115115 up = "COALESCE(name, 'TEST_DEFAULT_VALUE')"
116− down = "name"
117116
118117 [actions.changes]
119118 nullable = false
@@ -159,6 +158,106 @@ fn alter_column_set_not_null() {
159158 .query_one("SELECT name from users WHERE id = 4", &[])
160159 .unwrap();
161160 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");
162261 });
163262
164263 test.run();