use crate::ast::protobuf::{AlterTableType, ObjectType, RawStmt, TransactionStmtKind};
use crate::ast::NodeEnum;
use crate::Finding;
pub(crate) const ID: &str = "concurrently-in-transaction";
pub(crate) const MESSAGE: &str =
"CREATE/DROP INDEX CONCURRENTLY, REINDEX CONCURRENTLY, and DETACH PARTITION CONCURRENTLY \
cannot run inside a transaction block; this statement runs inside a transaction and will \
fail at runtime.";
pub(crate) const GUIDANCE: &str = "Run the CONCURRENTLY statement outside the transaction — put it in \
its own migration, or move it before BEGIN / after COMMIT. (Note: many migration tools also wrap \
each migration in an implicit transaction; disable that for this migration.)";
fn is_concurrently_index_op(node: &NodeEnum) -> bool {
match node {
NodeEnum::IndexStmt(i) => i.concurrent,
NodeEnum::DropStmt(d) => {
d.concurrent
&& matches!(
ObjectType::try_from(d.remove_type),
Ok(ObjectType::ObjectIndex)
)
}
NodeEnum::ReindexStmt(r) => crate::rules::reindex_is_concurrent(r),
NodeEnum::AlterTableStmt(_) => crate::rules::alter_table_cmds(node).iter().any(|cmd| {
AlterTableType::try_from(cmd.subtype) == Ok(AlterTableType::AtDetachPartition)
&& matches!(
cmd.def.as_ref().and_then(|n| n.node.as_ref()),
Some(NodeEnum::PartitionCmd(pc)) if pc.concurrent
)
}),
_ => false,
}
}
pub(crate) fn in_transaction_flags(stmts: &[RawStmt], assume_in_transaction: bool) -> Vec<bool> {
let mut in_txn = assume_in_transaction;
let mut out = Vec::with_capacity(stmts.len());
for raw in stmts {
out.push(in_txn);
if let Some(NodeEnum::TransactionStmt(t)) = raw.stmt.as_ref().and_then(|b| b.node.as_ref())
{
match TransactionStmtKind::try_from(t.kind) {
Ok(TransactionStmtKind::TransStmtBegin | TransactionStmtKind::TransStmtStart) => {
in_txn = true;
}
Ok(
TransactionStmtKind::TransStmtCommit | TransactionStmtKind::TransStmtRollback,
) => {
in_txn = t.chain;
}
_ => {}
}
}
}
out
}
pub(crate) fn concurrently_in_transaction_indices(
stmts: &[RawStmt],
assume_in_transaction: bool,
) -> Vec<usize> {
let in_txn = in_transaction_flags(stmts, assume_in_transaction);
stmts
.iter()
.enumerate()
.filter_map(|(i, raw)| {
let node = raw.stmt.as_ref().and_then(|b| b.node.as_ref())?;
(in_txn[i] && is_concurrently_index_op(node)).then_some(i)
})
.collect()
}
fn fix_adds_concurrently(fix: &crate::Fix) -> bool {
fix.edits.iter().any(|e| {
e.replacement
.split(|c: char| !c.is_ascii_alphanumeric())
.any(|word| word.eq_ignore_ascii_case("CONCURRENTLY"))
})
}
pub(crate) fn suppress_concurrently_fix_in_transaction(
stmts: &[RawStmt],
findings: &mut [Finding],
assume_in_transaction: bool,
) {
let in_txn = in_transaction_flags(stmts, assume_in_transaction);
for f in findings.iter_mut() {
if !f.fix.as_ref().is_some_and(fix_adds_concurrently) {
continue;
}
debug_assert!(
f.statement_index < in_txn.len(),
"finding statement_index {} out of range for {} statements",
f.statement_index,
in_txn.len()
);
if in_txn.get(f.statement_index).copied().unwrap_or(true) {
f.fix = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn indices(sql: &str) -> Vec<usize> {
concurrently_in_transaction_indices(&crate::ast::parse(sql).unwrap().protobuf.stmts, false)
}
fn indices_assumed(sql: &str) -> Vec<usize> {
concurrently_in_transaction_indices(&crate::ast::parse(sql).unwrap().protobuf.stmts, true)
}
#[test]
fn detector() {
assert_eq!(
indices("BEGIN; CREATE INDEX CONCURRENTLY i ON t (x); COMMIT;"),
vec![1]
);
assert_eq!(
indices("BEGIN; DROP INDEX CONCURRENTLY i; COMMIT;"),
vec![1]
);
assert_eq!(
indices("START TRANSACTION; REINDEX INDEX CONCURRENTLY i; COMMIT;"),
vec![1]
);
assert!(indices("CREATE INDEX CONCURRENTLY i ON t (x);").is_empty());
assert!(indices("BEGIN; COMMIT; CREATE INDEX CONCURRENTLY i ON t (x);").is_empty());
assert!(indices("BEGIN; CREATE INDEX i ON t (x); COMMIT;").is_empty());
assert!(indices("BEGIN; ROLLBACK; CREATE INDEX CONCURRENTLY i ON t (x);").is_empty());
assert_eq!(
indices("BEGIN; CREATE INDEX CONCURRENTLY i ON t (x); ROLLBACK;"),
vec![1]
);
}
#[test]
fn detects_detach_partition_concurrently_in_transaction() {
assert_eq!(
indices("BEGIN; ALTER TABLE p DETACH PARTITION p1 CONCURRENTLY; COMMIT;"),
vec![1]
);
assert!(indices("ALTER TABLE p DETACH PARTITION p1 CONCURRENTLY;").is_empty());
assert!(indices("BEGIN; ALTER TABLE p DETACH PARTITION p1; COMMIT;").is_empty());
}
#[test]
fn commit_and_chain_keeps_the_transaction_open() {
assert_eq!(
indices("BEGIN; COMMIT AND CHAIN; CREATE INDEX CONCURRENTLY i ON t (x); COMMIT;"),
vec![2]
);
assert_eq!(
indices("BEGIN; ROLLBACK AND CHAIN; CREATE INDEX CONCURRENTLY i ON t (x); COMMIT;"),
vec![2]
);
assert!(indices("BEGIN; COMMIT; CREATE INDEX CONCURRENTLY i ON t (x);").is_empty());
}
#[test]
fn in_transaction_flags_track_block_boundaries() {
let flags = |sql: &str| {
super::in_transaction_flags(&crate::ast::parse(sql).unwrap().protobuf.stmts, false)
};
assert_eq!(
flags("BEGIN; CREATE INDEX i ON t (x); COMMIT; SELECT 1;"),
vec![false, true, true, false]
);
assert_eq!(flags("SELECT 1; SELECT 2;"), vec![false, false]);
let seeded = super::in_transaction_flags(
&crate::ast::parse("SELECT 1;").unwrap().protobuf.stmts,
true,
);
assert_eq!(seeded, vec![true]);
}
#[test]
fn assume_in_transaction_flags_top_level_concurrently() {
assert_eq!(
indices_assumed("CREATE INDEX CONCURRENTLY i ON t (x);"),
vec![0]
);
assert!(indices_assumed("COMMIT; CREATE INDEX CONCURRENTLY i ON t (x);").is_empty());
assert_eq!(
indices_assumed("BEGIN; CREATE INDEX CONCURRENTLY i ON t (x); COMMIT;"),
vec![1]
);
assert!(indices("CREATE INDEX CONCURRENTLY i ON t (x);").is_empty());
}
}