on main help sign in
Add support for unique and non-B-tree indices in alter_column
1680906fabianlindfors
src/migrations/alter_column.rs+12−9
@@ -157,8 +157,8 @@ impl Action for AlterColumn {
157
157
158 // Duplicate any indices to the temporary column
158 // Duplicate any indices to the temporary column
159 let indices = common::get_indices_for_column(db, &table.real_name, &column.real_name)?;
159 let indices = common::get_indices_for_column(db, &table.real_name, &column.real_name)?;
160− for (index_name, index_oid) in indices {
160+ for index in indices {
161− let index_columns: Vec<String> = common::get_index_columns(db, &index_name)?
161+ let index_columns: Vec<String> = common::get_index_columns(db, &index.name)?
162 .into_iter()
162 .into_iter()
163 .map(|idx_column| {
163 .map(|idx_column| {
164 // Replace column with temporary column for new index
164 // Replace column with temporary column for new index
@@ -169,15 +169,18 @@ impl Action for AlterColumn {
169 }
169 }
170 })
170 })
171 .collect();
171 .collect();
172− let temp_index_name = self.temp_index_name(ctx, index_oid);
172+ let temp_index_name = self.temp_index_name(ctx, index.oid);
173+
174+ let unique_def = if index.unique { "UNIQUE" } else { "" };
173
175
174 db.query(&format!(
176 db.query(&format!(
175 r#"
177 r#"
176− CREATE INDEX CONCURRENTLY IF NOT EXISTS "{new_index_name}" ON "{table}" ({columns})
178+ CREATE {unique_def} INDEX CONCURRENTLY IF NOT EXISTS "{new_index_name}" ON "{table}" USING {index_type} ({columns})
177 "#,
179 "#,
178 new_index_name = temp_index_name,
180 new_index_name = temp_index_name,
179 table = table.real_name,
181 table = table.real_name,
180 columns = index_columns.join(", "),
182 columns = index_columns.join(", "),
183+ index_type = index.index_type,
181 ))
184 ))
182 .context("failed to create temporary index")?;
185 .context("failed to create temporary index")?;
183 }
186 }
@@ -280,7 +283,7 @@ impl Action for AlterColumn {
280
283
281 // Replace old indices with the new temporary ones created for the temporary column
284 // Replace old indices with the new temporary ones created for the temporary column
282 let indices = common::get_indices_for_column(db, &self.table, &self.column)?;
285 let indices = common::get_indices_for_column(db, &self.table, &self.column)?;
283− for (current_index_name, index_oid) in indices {
286+ for current_index in indices {
284 // To keep the index handling idempotent, we need to do the following:
287 // To keep the index handling idempotent, we need to do the following:
285 // 1. Add a prefix to the existing index
288 // 1. Add a prefix to the existing index
286 // 2. Rename temporary index to its final name
289 // 2. Rename temporary index to its final name
@@ -288,7 +291,7 @@ impl Action for AlterColumn {
288
291
289 // Add prefix (if not already added) to existing index
292 // Add prefix (if not already added) to existing index
290 let prefix = "__reshape_old";
293 let prefix = "__reshape_old";
291− let target_index_name = current_index_name.trim_start_matches(prefix);
294+ let target_index_name = current_index.name.trim_start_matches(prefix);
292 let old_index_name = format!("{}_{}", prefix, target_index_name);
295 let old_index_name = format!("{}_{}", prefix, target_index_name);
293 db.query(&format!(
296 db.query(&format!(
294 r#"
297 r#"
@@ -300,7 +303,7 @@ impl Action for AlterColumn {
300 .context("failed to rename old index")?;
303 .context("failed to rename old index")?;
301
304
302 // Rename temporary index to real name
305 // Rename temporary index to real name
303− let temp_index_name = self.temp_index_name(ctx, index_oid);
306+ let temp_index_name = self.temp_index_name(ctx, current_index.oid);
304 db.query(&format!(
307 db.query(&format!(
305 r#"
308 r#"
306 ALTER INDEX IF EXISTS "{temp_index_name}" RENAME TO "{target_index_name}"
309 ALTER INDEX IF EXISTS "{temp_index_name}" RENAME TO "{target_index_name}"
@@ -388,8 +391,8 @@ impl Action for AlterColumn {
388 // Safely remove any indices created for the temporary column
391 // Safely remove any indices created for the temporary column
389 let temp_column_name = self.temporary_column_name(ctx);
392 let temp_column_name = self.temporary_column_name(ctx);
390 let indices = common::get_indices_for_column(db, &self.table, &temp_column_name)?;
393 let indices = common::get_indices_for_column(db, &self.table, &temp_column_name)?;
391− for (_, index_oid) in indices {
394+ for index in indices {
392− let temp_index_name = self.temp_index_name(ctx, index_oid);
395+ let temp_index_name = self.temp_index_name(ctx, index.oid);
393 db.query(&format!(
396 db.query(&format!(
394 r#"
397 r#"
395 DROP INDEX CONCURRENTLY IF EXISTS "{index_name}"
398 DROP INDEX CONCURRENTLY IF EXISTS "{index_name}"
src/migrations/common.rs+20−3
@@ -173,18 +173,30 @@ fn get_primary_key_columns_for_table(
173 Ok(primary_key_columns)
173 Ok(primary_key_columns)
174}
174}
175
175
176+pub struct Index {
177+ pub name: String,
178+ pub oid: u32,
179+ pub unique: bool,
180+ pub index_type: String,
181+}
182+
176pub fn get_indices_for_column(
183pub fn get_indices_for_column(
177 db: &mut dyn Conn,
184 db: &mut dyn Conn,
178 table: &str,
185 table: &str,
179 column: &str,
186 column: &str,
180−) -> anyhow::Result<Vec<(String, u32)>> {
187+) -> anyhow::Result<Vec<Index>> {
181 let indices = db
188 let indices = db
182 .query(&format!(
189 .query(&format!(
183 "
190 "
184− SELECT i.relname AS index_name, i.oid AS index_oid
191+ SELECT
192+ i.relname AS name,
193+ i.oid AS oid,
194+ ix.indisunique AS unique,
195+ am.amname AS type
185 FROM pg_index ix
196 FROM pg_index ix
186 JOIN pg_class t ON t.oid = ix.indrelid
197 JOIN pg_class t ON t.oid = ix.indrelid
187 JOIN pg_class i ON i.oid = ix.indexrelid
198 JOIN pg_class i ON i.oid = ix.indexrelid
199+ JOIN pg_am am ON i.relam = am.oid
188 JOIN pg_attribute a ON
200 JOIN pg_attribute a ON
189 a.attrelid = t.oid AND
201 a.attrelid = t.oid AND
190 a.attnum = ANY(ix.indkey)
202 a.attnum = ANY(ix.indkey)
@@ -196,7 +208,12 @@ pub fn get_indices_for_column(
196 column = column,
208 column = column,
197 ))?
209 ))?
198 .iter()
210 .iter()
199− .map(|row| (row.get("index_name"), row.get("index_oid")))
211+ .map(|row| Index {
212+ name: row.get("name"),
213+ oid: row.get("oid"),
214+ unique: row.get("unique"),
215+ index_type: row.get("type"),
216+ })
200 .collect();
217 .collect();
201
218
202 Ok(indices)
219 Ok(indices)
tests/add_index.rs+6−6
@@ -126,11 +126,11 @@ fn add_index_unique() {
126 let (is_ready, is_valid, is_unique): (bool, bool, bool) = db
126 let (is_ready, is_valid, is_unique): (bool, bool, bool) = db
127 .query(
127 .query(
128 "
128 "
129− SELECT pg_index.indisready, pg_index.indisvalid, pg_index.indisunique
129+ SELECT pg_index.indisready, pg_index.indisvalid, pg_index.indisunique
130− FROM pg_catalog.pg_index
130+ FROM pg_catalog.pg_index
131− JOIN pg_catalog.pg_class ON pg_index.indexrelid = pg_class.oid
131+ JOIN pg_catalog.pg_class ON pg_index.indexrelid = pg_class.oid
132− WHERE pg_class.relname = 'name_idx'
132+ WHERE pg_class.relname = 'name_idx'
133− ",
133+ ",
134 &[],
134 &[],
135 )
135 )
136 .unwrap()
136 .unwrap()
@@ -191,7 +191,7 @@ fn add_index_with_type() {
191 );
191 );
192
192
193 test.intermediate(|db, _| {
193 test.intermediate(|db, _| {
194− // Ensure index is valid, ready and unique
194+ // Ensure index is valid, ready and has the right type
195 let (is_ready, is_valid, index_type): (bool, bool, String) = db
195 let (is_ready, is_valid, index_type): (bool, bool, String) = db
196 .query(
196 .query(
197 "
197 "
tests/alter_column.rs+159
@@ -474,3 +474,162 @@ fn alter_column_with_index() {
474
474
475 test.run();
475 test.run();
476}
476}
477+
478+#[test]
479+fn alter_column_with_unique_index() {
480+ let mut test = Test::new("Alter column with unique index");
481+
482+ test.first_migration(
483+ r#"
484+ name = "create_user_table"
485+
486+ [[actions]]
487+ type = "create_table"
488+ name = "users"
489+ primary_key = ["id"]
490+
491+ [[actions.columns]]
492+ name = "id"
493+ type = "INTEGER"
494+
495+ [[actions.columns]]
496+ name = "name"
497+ type = "TEXT"
498+
499+ [[actions]]
500+ type = "add_index"
501+ table = "users"
502+
503+ [actions.index]
504+ name = "name_idx"
505+ columns = ["name"]
506+ unique = true
507+ "#,
508+ );
509+
510+ test.second_migration(
511+ r#"
512+ name = "uppercase_name"
513+
514+ [[actions]]
515+ type = "alter_column"
516+ table = "users"
517+ column = "name"
518+ up = "UPPER(name)"
519+ down = "LOWER(name)"
520+ "#,
521+ );
522+
523+ test.after_first(|db| {
524+ db.simple_query("INSERT INTO users (id, name) VALUES (1, 'Test')")
525+ .unwrap();
526+ });
527+
528+ test.intermediate(|old_db, new_db| {
529+ // Try inserting a value which duplicates the uppercase value of an existing row
530+ let result = new_db.simple_query("INSERT INTO users (id, name) VALUES (2, 'TEST')");
531+ assert!(
532+ result.is_err(),
533+ "expected duplicate insert to new schema to fail"
534+ );
535+
536+ // Try inserting a value which duplicates the lowercase value of an existing row
537+ new_db
538+ .simple_query("INSERT INTO users (id, name) VALUES (2, 'JOHN')")
539+ .unwrap();
540+ let result = old_db.simple_query("INSERT INTO users (id, name) VALUES (3, 'john')");
541+ assert!(
542+ result.is_err(),
543+ "expected duplicate insert to old schema to fail"
544+ );
545+ });
546+
547+ test.after_completion(|db| {
548+ // Make sure index still exists
549+ let is_unique: bool = db
550+ .query(
551+ "
552+ SELECT pg_index.indisunique
553+ FROM pg_catalog.pg_index
554+ JOIN pg_catalog.pg_class ON pg_index.indexrelid = pg_class.oid
555+ WHERE pg_class.relname = 'name_idx'
556+ ",
557+ &[],
558+ )
559+ .unwrap()
560+ .first()
561+ .map(|row| row.get("indisunique"))
562+ .unwrap();
563+ assert!(is_unique, "expected index to still be unique");
564+ });
565+
566+ test.run();
567+}
568+
569+#[test]
570+fn alter_column_with_hash_index() {
571+ let mut test = Test::new("Alter column with custom index type");
572+
573+ test.first_migration(
574+ r#"
575+ name = "create_user_table"
576+
577+ [[actions]]
578+ type = "create_table"
579+ name = "users"
580+ primary_key = ["id"]
581+
582+ [[actions.columns]]
583+ name = "id"
584+ type = "INTEGER"
585+
586+ [[actions.columns]]
587+ name = "name"
588+ type = "TEXT"
589+
590+ [[actions]]
591+ type = "add_index"
592+ table = "users"
593+
594+ [actions.index]
595+ name = "name_idx"
596+ columns = ["name"]
597+ type = "hash"
598+ "#,
599+ );
600+
601+ test.second_migration(
602+ r#"
603+ name = "uppercase_name"
604+
605+ [[actions]]
606+ type = "alter_column"
607+ table = "users"
608+ column = "name"
609+ up = "UPPER(name)"
610+ down = "LOWER(name)"
611+ "#,
612+ );
613+
614+ test.after_completion(|db| {
615+ // Make sure index still has type GIN
616+ let index_type: String = db
617+ .query(
618+ "
619+ SELECT pg_am.amname
620+ FROM pg_catalog.pg_index
621+ JOIN pg_catalog.pg_class ON pg_index.indexrelid = pg_class.oid
622+ JOIN pg_catalog.pg_am ON pg_class.relam = pg_am.oid
623+ WHERE pg_class.relname = 'name_idx'
624+ ",
625+ &[],
626+ )
627+ .unwrap()
628+ .first()
629+ .map(|row| row.get("amname"))
630+ .unwrap();
631+ assert_eq!("hash", index_type);
632+ });
633+
634+ test.run();
635+}