use super::{Pattern, PatternContext, PatternError};
use crate::live_migrate::plan::{Step, StepKind, StepParameters};
use crate::migrate::SchemaOperation;
use crate::migrate::schema::{IndexKindSchema, IndexSchema, IndexTargetSchema};
pub struct IndexDependent;
impl Pattern for IndexDependent {
const ID: &'static str = "index_dependent";
const IDEMPOTENT_PREDICATE: bool = false;
fn emit(op: &SchemaOperation, _ctx: &PatternContext) -> Result<Vec<Step>, PatternError> {
let index = match op {
SchemaOperation::AddIndex(index) => index,
_ => {
return Err(PatternError::WrongOperation {
pattern: Self::ID,
reason: "expected AddIndex".to_string(),
});
}
};
let create_sql = render_create_index(index)?;
let gate_query = format!(
"SELECT indvalid FROM pg_index WHERE indexrelid = '{name}'::regclass",
name = index.name,
);
Ok(vec![
Step {
kind: StepKind::ExpandSchema,
ordinal: 0,
parameters: StepParameters::ExpandSchema {
sql_segments: vec![create_sql],
},
},
Step {
kind: StepKind::ValidateBackfill,
ordinal: 1,
parameters: StepParameters::ValidateBackfill { gate_query },
},
])
}
}
pub(super) fn render_create_index(index: &IndexSchema) -> Result<String, PatternError> {
let unique = match index.kind {
IndexKindSchema::NonUnique => "",
IndexKindSchema::UniqueConstraint | IndexKindSchema::UniqueIndex => "UNIQUE ",
};
let target = render_index_target(&index.target)?;
Ok(format!(
"CREATE {unique}INDEX CONCURRENTLY {name} ON {table} {target}",
name = quote_ident(&index.name),
table = quote_ident(&index.table),
))
}
fn render_index_target(target: &IndexTargetSchema) -> Result<String, PatternError> {
match target {
IndexTargetSchema::Columns(cols) => {
if cols.is_empty() {
return Err(PatternError::Invariant {
pattern: IndexDependent::ID,
detail: "index target has zero columns".to_string(),
});
}
let mut out = String::from("(");
for (i, col) in cols.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str("e_ident(&col.name));
}
out.push(')');
Ok(out)
}
IndexTargetSchema::Expression(expr) => Ok(format!("({expr})")),
}
}
fn quote_ident(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 2);
out.push('"');
out.push_str(name);
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::migrate::schema::{
IndexColumnSchema, IndexNullsOrderSchema, IndexOrderSchema, IndexTypeSchema,
};
fn ctx() -> PatternContext {
PatternContext::with_defaults()
}
fn index(name: &str, kind: IndexKindSchema) -> IndexSchema {
IndexSchema {
extension_dependency: None,
include: Vec::new(),
index_type: IndexTypeSchema::BTree,
kind,
name: name.to_string(),
nulls_not_distinct: false,
predicate: None,
requires_out_of_transaction: false,
table: "vehicle".to_string(),
target: IndexTargetSchema::Columns(vec![IndexColumnSchema {
name: "owner_id".to_string(),
nulls: IndexNullsOrderSchema::Default,
opclass: None,
order: IndexOrderSchema::Asc,
}]),
}
}
#[test]
fn emits_two_step_concurrent_build_sequence() {
let op = SchemaOperation::AddIndex(index("idx_vehicle_owner", IndexKindSchema::NonUnique));
let steps = IndexDependent::emit(&op, &ctx()).unwrap();
assert_eq!(steps.len(), 2);
assert_eq!(steps[0].kind, StepKind::ExpandSchema);
assert_eq!(steps[1].kind, StepKind::ValidateBackfill);
}
#[test]
fn emitted_ordinals_are_sequential_and_kinds_are_consistent() {
let op = SchemaOperation::AddIndex(index("idx_vehicle_owner", IndexKindSchema::NonUnique));
let steps = IndexDependent::emit(&op, &ctx()).unwrap();
for (idx, step) in steps.iter().enumerate() {
assert_eq!(step.ordinal as usize, idx);
assert_eq!(step.kind, step.parameters.kind());
}
}
#[test]
fn create_index_emits_concurrently_clause() {
let op = SchemaOperation::AddIndex(index("idx_vehicle_owner", IndexKindSchema::NonUnique));
let steps = IndexDependent::emit(&op, &ctx()).unwrap();
let StepParameters::ExpandSchema { sql_segments } = &steps[0].parameters else {
panic!("expected ExpandSchema");
};
assert_eq!(sql_segments.len(), 1);
assert!(sql_segments[0].contains("CONCURRENTLY"));
assert!(sql_segments[0].contains("\"idx_vehicle_owner\""));
assert!(sql_segments[0].contains("\"vehicle\""));
assert!(sql_segments[0].contains("\"owner_id\""));
assert!(!sql_segments[0].contains("UNIQUE"));
}
#[test]
fn validate_query_targets_pg_index_indvalid() {
let op = SchemaOperation::AddIndex(index("idx_vehicle_owner", IndexKindSchema::NonUnique));
let steps = IndexDependent::emit(&op, &ctx()).unwrap();
let StepParameters::ValidateBackfill { gate_query } = &steps[1].parameters else {
panic!("expected ValidateBackfill");
};
assert!(gate_query.contains("pg_index"));
assert!(gate_query.contains("indvalid"));
assert!(gate_query.contains("idx_vehicle_owner"));
}
#[test]
fn rejects_drop_index() {
let op = SchemaOperation::DropIndex(index("idx_vehicle_owner", IndexKindSchema::NonUnique));
let err = IndexDependent::emit(&op, &ctx()).unwrap_err();
assert!(matches!(err, PatternError::WrongOperation { .. }));
}
#[test]
fn empty_column_list_is_invariant_violation() {
let mut idx = index("idx_vehicle_owner", IndexKindSchema::NonUnique);
idx.target = IndexTargetSchema::Columns(Vec::new());
let op = SchemaOperation::AddIndex(idx);
let err = IndexDependent::emit(&op, &ctx()).unwrap_err();
assert!(matches!(err, PatternError::Invariant { .. }));
}
}