Skip to content

Commit 2b76980

Browse files
committed
fix: restore cargo-mutants damage and add MVCC registry unit tests
Restore the Expression::Case match arm in process_where_subqueries that was accidentally deleted by an in-place mutation test run. Add unit tests for TransactionRegistry covering commit snapshot_seqs guard, recovery functions, check_committed boundaries, is_directly_visible dispatch, snapshot isolation invalid txn_id handling, and get_commit_sequence fallback paths.
1 parent a63f182 commit 2b76980

File tree

2 files changed

+340
-1
lines changed

2 files changed

+340
-1
lines changed

src/executor/subquery.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,42 @@ impl Executor {
297297
}
298298
}
299299

300-
/* ~ changed by cargo-mutants ~ */
300+
Expression::Case(case) => {
301+
// Process the operand (if present)
302+
let processed_value = if let Some(ref value) = case.value {
303+
Some(Box::new(self.process_where_subqueries(value, ctx)?))
304+
} else {
305+
None
306+
};
307+
308+
// Process each WHEN clause
309+
let processed_whens: Result<Vec<WhenClause>> = case
310+
.when_clauses
311+
.iter()
312+
.map(|when| {
313+
Ok(WhenClause {
314+
token: when.token.clone(),
315+
condition: self.process_where_subqueries(&when.condition, ctx)?,
316+
then_result: self.process_where_subqueries(&when.then_result, ctx)?,
317+
})
318+
})
319+
.collect();
320+
321+
// Process the ELSE clause (if present)
322+
let processed_else = if let Some(ref else_val) = case.else_value {
323+
Some(Box::new(self.process_where_subqueries(else_val, ctx)?))
324+
} else {
325+
None
326+
};
327+
328+
Ok(Expression::Case(Box::new(CaseExpression {
329+
token: case.token.clone(),
330+
value: processed_value,
331+
when_clauses: processed_whens?,
332+
else_value: processed_else,
333+
})))
334+
}
335+
301336
Expression::Cast(cast) => {
302337
let processed_expr = self.process_where_subqueries(&cast.expr, ctx)?;
303338
Ok(Expression::Cast(CastExpression {

src/storage/mvcc/registry.rs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,4 +1110,308 @@ mod tests {
11101110
let (txn4, _) = registry.begin_transaction();
11111111
assert!(registry.is_visible(txn3, txn4));
11121112
}
1113+
1114+
// === commit_transaction: line 371 override_count > 0 ===
1115+
1116+
#[test]
1117+
fn test_commit_read_committed_skips_snapshot_seqs() {
1118+
let registry = TransactionRegistry::new();
1119+
registry.set_global_isolation_level(IsolationLevel::ReadCommitted);
1120+
1121+
let (txn_id, _) = registry.begin_transaction();
1122+
registry.commit_transaction(txn_id);
1123+
1124+
// override_count == 0, global != snapshot => snapshot_seqs NOT populated
1125+
assert!(registry.snapshot_seqs.lock().is_empty());
1126+
}
1127+
1128+
// === recover_committed_transaction: lines 416, 432 ===
1129+
1130+
#[test]
1131+
fn test_recover_committed_advances_next_txn_id() {
1132+
let registry = TransactionRegistry::new();
1133+
1134+
// Recover txn 100 with commit_seq 50
1135+
registry.recover_committed_transaction(100, 50);
1136+
// next_txn_id must advance past 100
1137+
let (new_id, _) = registry.begin_transaction();
1138+
assert!(new_id > 100);
1139+
}
1140+
1141+
#[test]
1142+
fn test_recover_committed_advances_next_sequence() {
1143+
let registry = TransactionRegistry::new();
1144+
1145+
registry.recover_committed_transaction(10, 200);
1146+
// next_sequence must advance past 200
1147+
let (_, begin_seq) = registry.begin_transaction();
1148+
assert!(begin_seq > 200);
1149+
}
1150+
1151+
#[test]
1152+
fn test_recover_committed_descending_order() {
1153+
let registry = TransactionRegistry::new();
1154+
1155+
// Recover in descending order — next_txn_id must still track the max
1156+
registry.recover_committed_transaction(100, 50);
1157+
registry.recover_committed_transaction(50, 30);
1158+
1159+
let (new_id, _) = registry.begin_transaction();
1160+
assert!(new_id > 100, "next_txn_id should be >= 100, got {}", new_id);
1161+
}
1162+
1163+
// === recover_aborted_transaction: lines 448, 455 ===
1164+
1165+
#[test]
1166+
fn test_recover_aborted_marks_aborted() {
1167+
let registry = TransactionRegistry::new();
1168+
1169+
registry.recover_aborted_transaction(42);
1170+
1171+
// Must be aborted, not committed, not active
1172+
assert!(!registry.is_committed(42));
1173+
assert!(!registry.is_active(42));
1174+
let state = registry.transactions.lock().get(42).copied();
1175+
assert!(state.is_some());
1176+
assert!(state.unwrap().is_aborted());
1177+
}
1178+
1179+
#[test]
1180+
fn test_recover_aborted_advances_next_txn_id() {
1181+
let registry = TransactionRegistry::new();
1182+
1183+
registry.recover_aborted_transaction(100);
1184+
// next_txn_id must advance past 100
1185+
let (new_id, _) = registry.begin_transaction();
1186+
assert!(new_id > 100);
1187+
}
1188+
1189+
#[test]
1190+
fn test_recover_aborted_descending_order() {
1191+
let registry = TransactionRegistry::new();
1192+
1193+
registry.recover_aborted_transaction(200);
1194+
registry.recover_aborted_transaction(100);
1195+
1196+
// next_txn_id must still be >= 200
1197+
let (new_id, _) = registry.begin_transaction();
1198+
assert!(new_id > 200, "next_txn_id should be > 200, got {}", new_id);
1199+
}
1200+
1201+
#[test]
1202+
fn test_recover_aborted_not_visible() {
1203+
let registry = TransactionRegistry::new();
1204+
1205+
registry.recover_aborted_transaction(5);
1206+
1207+
let (viewer, _) = registry.begin_transaction();
1208+
// Aborted txn must never be visible
1209+
assert!(!registry.is_visible(5, viewer));
1210+
}
1211+
1212+
// === check_committed: line 512 ===
1213+
1214+
#[test]
1215+
fn test_check_committed_negative_txn_id_not_committed() {
1216+
let registry = TransactionRegistry::new();
1217+
1218+
// Start a real transaction so next_txn_id > 0
1219+
let (txn_id, _) = registry.begin_transaction();
1220+
registry.commit_transaction(txn_id);
1221+
1222+
// Negative txn_id must NOT be committed.
1223+
// Catches && -> || mutation: (-5 > 0 || -5 <= next) would wrongly be true
1224+
assert!(!registry.check_committed(-5));
1225+
assert!(!registry.check_committed(-100));
1226+
}
1227+
1228+
#[test]
1229+
fn test_check_committed_future_txn_id() {
1230+
let registry = TransactionRegistry::new();
1231+
1232+
// No transactions started yet, so txn_id 1 is beyond next_txn_id
1233+
assert!(!registry.check_committed(1));
1234+
}
1235+
1236+
#[test]
1237+
fn test_check_committed_valid_committed() {
1238+
let registry = TransactionRegistry::new();
1239+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1240+
1241+
let (txn_id, _) = registry.begin_transaction();
1242+
registry.commit_transaction(txn_id);
1243+
1244+
assert!(registry.check_committed(txn_id));
1245+
}
1246+
1247+
#[test]
1248+
fn test_check_committed_boundary_next_txn_id() {
1249+
let registry = TransactionRegistry::new();
1250+
1251+
let (txn_id, _) = registry.begin_transaction();
1252+
registry.commit_transaction(txn_id);
1253+
1254+
// txn_id == next_txn_id should be committed (boundary: <= next)
1255+
assert!(registry.check_committed(txn_id));
1256+
// txn_id + 1 > next_txn_id should NOT be committed
1257+
assert!(!registry.check_committed(txn_id + 1));
1258+
}
1259+
1260+
// === is_directly_visible: line 523 ===
1261+
1262+
#[test]
1263+
fn test_is_directly_visible_recovery() {
1264+
let registry = TransactionRegistry::new();
1265+
1266+
// RECOVERY_TRANSACTION_ID is always directly visible
1267+
assert!(registry.is_directly_visible(RECOVERY_TRANSACTION_ID));
1268+
}
1269+
1270+
#[test]
1271+
fn test_is_directly_visible_normal_txn() {
1272+
let registry = TransactionRegistry::new();
1273+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1274+
1275+
let (txn_id, _) = registry.begin_transaction();
1276+
// Active txn is NOT directly visible
1277+
assert!(!registry.is_directly_visible(txn_id));
1278+
1279+
registry.commit_transaction(txn_id);
1280+
// Committed txn IS directly visible
1281+
assert!(registry.is_directly_visible(txn_id));
1282+
}
1283+
1284+
#[test]
1285+
fn test_is_directly_visible_non_recovery_negative() {
1286+
let registry = TransactionRegistry::new();
1287+
1288+
// A negative txn_id that is NOT RECOVERY_TRANSACTION_ID should not be visible
1289+
assert!(!registry.is_directly_visible(-99));
1290+
}
1291+
1292+
// === is_visible_snapshot: lines 541, 570 ===
1293+
1294+
#[test]
1295+
fn test_snapshot_committed_viewer_fallback() {
1296+
// When the viewer has already committed, the match guard
1297+
// `is_active_or_committing()` fails → falls back to check_committed.
1298+
let registry = TransactionRegistry::new();
1299+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1300+
1301+
let (txn1, _) = registry.begin_transaction();
1302+
registry.commit_transaction(txn1);
1303+
1304+
let (txn2, _) = registry.begin_transaction();
1305+
registry.commit_transaction(txn2);
1306+
1307+
// txn1 is committed → visible to committed viewer txn2
1308+
assert!(registry.is_visible(txn1, txn2));
1309+
}
1310+
1311+
#[test]
1312+
fn test_snapshot_invalid_version_txn_zero() {
1313+
let registry = TransactionRegistry::new();
1314+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1315+
1316+
let (viewer, _) = registry.begin_transaction();
1317+
1318+
// version_txn_id 0 is invalid — not visible
1319+
// Catches || → && mutation: (0 <= 0 && 0 > next) = (true && false) = false
1320+
// but || → && would never return false for valid-looking IDs
1321+
assert!(!registry.is_visible(0, viewer));
1322+
}
1323+
1324+
#[test]
1325+
fn test_snapshot_invalid_version_txn_negative() {
1326+
let registry = TransactionRegistry::new();
1327+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1328+
1329+
let (viewer, _) = registry.begin_transaction();
1330+
1331+
// Negative txn_id (not RECOVERY_TRANSACTION_ID) is invalid
1332+
assert!(!registry.is_visible(-50, viewer));
1333+
}
1334+
1335+
#[test]
1336+
fn test_snapshot_future_version_txn_not_visible() {
1337+
let registry = TransactionRegistry::new();
1338+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1339+
1340+
let (viewer, _) = registry.begin_transaction();
1341+
1342+
// version_txn_id beyond next_txn_id is invalid
1343+
// Catches > → == mutation: 9999 == next would be false (not caught),
1344+
// but next+1 is immediately beyond
1345+
let next = registry.next_txn_id.load(Ordering::Acquire);
1346+
assert!(!registry.is_visible(next + 1, viewer));
1347+
assert!(!registry.is_visible(next + 100, viewer));
1348+
}
1349+
1350+
#[test]
1351+
fn test_snapshot_boundary_version_equals_next() {
1352+
// version_txn_id == next_txn_id should be valid (it's an assigned ID)
1353+
// Catches > → >= mutation at line 570
1354+
let registry = TransactionRegistry::new();
1355+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1356+
1357+
let (txn1, _) = registry.begin_transaction();
1358+
registry.commit_transaction(txn1);
1359+
1360+
// txn1 == next_txn_id at this point
1361+
let (viewer, _) = registry.begin_transaction();
1362+
// txn1 committed before viewer — should be visible
1363+
assert!(registry.is_visible(txn1, viewer));
1364+
}
1365+
1366+
// === get_commit_sequence: line 608 ===
1367+
1368+
#[test]
1369+
fn test_get_commit_sequence_invalid_txn_id() {
1370+
let registry = TransactionRegistry::new();
1371+
1372+
// txn_id 0 is invalid
1373+
assert_eq!(registry.get_commit_sequence(0), None);
1374+
// Negative is invalid
1375+
assert_eq!(registry.get_commit_sequence(-1), None);
1376+
}
1377+
1378+
#[test]
1379+
fn test_get_commit_sequence_future_txn_id() {
1380+
let registry = TransactionRegistry::new();
1381+
1382+
// No transactions started, txn_id 1 is beyond next_txn_id
1383+
assert_eq!(registry.get_commit_sequence(1), None);
1384+
}
1385+
1386+
#[test]
1387+
fn test_get_commit_sequence_active() {
1388+
let registry = TransactionRegistry::new();
1389+
1390+
let (txn_id, _) = registry.begin_transaction();
1391+
// Active transaction has no commit_seq
1392+
assert_eq!(registry.get_commit_sequence(txn_id), None);
1393+
}
1394+
1395+
#[test]
1396+
fn test_get_commit_sequence_aborted() {
1397+
let registry = TransactionRegistry::new();
1398+
1399+
let (txn_id, _) = registry.begin_transaction();
1400+
registry.abort_transaction(txn_id);
1401+
// Aborted transaction has no commit_seq
1402+
assert_eq!(registry.get_commit_sequence(txn_id), None);
1403+
}
1404+
1405+
#[test]
1406+
fn test_get_commit_sequence_committed_with_snapshot() {
1407+
let registry = TransactionRegistry::new();
1408+
registry.set_global_isolation_level(IsolationLevel::SnapshotIsolation);
1409+
1410+
let (txn_id, _) = registry.begin_transaction();
1411+
let commit_seq = registry.commit_transaction(txn_id);
1412+
1413+
// With snapshot isolation, exact commit_seq is stored
1414+
assert_eq!(registry.get_commit_sequence(txn_id), Some(commit_seq));
1415+
assert!(commit_seq > 0);
1416+
}
11131417
}

0 commit comments

Comments
 (0)