@@ -150,6 +150,33 @@ impl Action for AlterColumn {
150150 common::batch_touch_rows(db, &table.real_name, &column.real_name)
151151 .context("failed to batch update existing rows")?;
152152
153+ // Duplicate any indices to the temporary column
154+ let indices = common::get_indices_for_column(db, &table.real_name, &column.real_name)?;
155+ for (index_name, index_oid) in indices {
156+ let index_columns: Vec<String> = common::get_index_columns(db, &index_name)?
157+ .into_iter()
158+ .map(|idx_column| {
159+ // Replace column with temporary column for new index
160+ if idx_column == column.real_name {
161+ temporary_column_name.to_string()
162+ } else {
163+ idx_column
164+ }
165+ })
166+ .collect();
167+ let temp_index_name = self.temp_index_name(ctx, index_oid);
168+
169+ db.query(&format!(
170+ r#"
171+ CREATE INDEX CONCURRENTLY IF NOT EXISTS "{new_index_name}" ON "{table}" ({columns})
172+ "#,
173+ new_index_name = temp_index_name,
174+ table = table.real_name,
175+ columns = index_columns.join(", "),
176+ ))
177+ .context("failed to create temporary index")?;
178+ }
179+
153180 // Add a temporary NOT NULL constraint if the column shouldn't be nullable.
154181 // This constraint is set as NOT VALID so it doesn't apply to existing rows and
155182 // the existing rows don't need to be scanned under an exclusive lock.
@@ -246,10 +273,52 @@ impl Action for AlterColumn {
246273 .context("failed to drop NOT NULL constraint")?;
247274 }
248275
276+ // Replace old indices with the new temporary ones created for the temporary column
277+ let indices = common::get_indices_for_column(db, &self.table, &self.column)?;
278+ for (current_index_name, index_oid) in indices {
279+ // To keep the index handling idempotent, we need to do the following:
280+ // 1. Add a prefix to the existing index
281+ // 2. Rename temporary index to its final name
282+ // 3. Drop existing index concurrently
283+
284+ // Add prefix (if not already added) to existing index
285+ let prefix = "__reshape_old";
286+ let target_index_name = current_index_name.trim_start_matches(prefix);
287+ let old_index_name = format!("{}_{}", prefix, target_index_name);
288+ db.query(&format!(
289+ r#"
290+ ALTER INDEX IF EXISTS "{current_name}" RENAME TO "{new_name}"
291+ "#,
292+ current_name = target_index_name,
293+ new_name = old_index_name,
294+ ))
295+ .context("failed to rename old index")?;
296+
297+ // Rename temporary index to real name
298+ let temp_index_name = self.temp_index_name(ctx, index_oid);
299+ db.query(&format!(
300+ r#"
301+ ALTER INDEX IF EXISTS "{temp_index_name}" RENAME TO "{target_index_name}"
302+ "#,
303+ temp_index_name = temp_index_name,
304+ target_index_name = target_index_name,
305+ ))
306+ .context("failed to rename temporary index")?;
307+
308+ // Drop old index concurrently
309+ db.query(&format!(
310+ r#"
311+ DROP INDEX CONCURRENTLY IF EXISTS "{old_index_name}"
312+ "#,
313+ old_index_name = old_index_name,
314+ ))
315+ .context("failed to drop old index")?;
316+ }
317+
249318 // Remove old column
250319 let query = format!(
251320 r#"
252− ALTER TABLE "{table}" DROP COLUMN "{column}" CASCADE
321+ ALTER TABLE "{table}" DROP COLUMN IF EXISTS "{column}" CASCADE
253322 "#,
254323 table = self.table,
255324 column = self.column,
@@ -311,6 +380,19 @@ impl Action for AlterColumn {
311380 }
312381
313382 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
383+ // Safely remove any indices created for the temporary column
384+ let temp_column_name = self.temporary_column_name(ctx);
385+ let indices = common::get_indices_for_column(db, &self.table, &temp_column_name)?;
386+ for (_, index_oid) in indices {
387+ let temp_index_name = self.temp_index_name(ctx, index_oid);
388+ db.query(&format!(
389+ r#"
390+ DROP INDEX CONCURRENTLY IF EXISTS "{index_name}"
391+ "#,
392+ index_name = temp_index_name,
393+ ))?;
394+ }
395+
314396 // Drop temporary column
315397 let query = format!(
316398 r#"
@@ -359,6 +441,10 @@ impl AlterColumn {
359441 format!("{}_alter_column_temporary", ctx.prefix())
360442 }
361443
444+ fn temp_index_name(&self, ctx: &MigrationContext, index_oid: u32) -> String {
445+ format!("{}_alter_column_temp_index_{}", ctx.prefix(), index_oid)
446+ }
447+
362448 fn can_short_circuit(&self) -> bool {
363449 self.changes.name.is_some()
364450 && self.changes.data_type.is_none()