diff --git a/test_data/abort_locks_log.dat b/test_data/abort_locks_log.dat new file mode 100644 index 0000000..fe61869 Binary files /dev/null and b/test_data/abort_locks_log.dat differ diff --git a/test_data/abort_log_fail.dat b/test_data/abort_log_fail.dat new file mode 100644 index 0000000..e69de29 diff --git a/test_data/abort_overflow10_log.dat b/test_data/abort_overflow10_log.dat new file mode 100644 index 0000000..034d929 Binary files /dev/null and b/test_data/abort_overflow10_log.dat differ diff --git a/test_data/abort_overflow_log.dat b/test_data/abort_overflow_log.dat new file mode 100644 index 0000000..f0513fd Binary files /dev/null and b/test_data/abort_overflow_log.dat differ diff --git a/test_data/commit_locks_log.dat b/test_data/commit_locks_log.dat new file mode 100644 index 0000000..0694a63 Binary files /dev/null and b/test_data/commit_locks_log.dat differ diff --git a/test_data/commit_log_fail.dat b/test_data/commit_log_fail.dat new file mode 100644 index 0000000..e69de29 diff --git a/test_data/del_idx_restore.dat b/test_data/del_idx_restore.dat new file mode 100644 index 0000000..fe61869 Binary files /dev/null and b/test_data/del_idx_restore.dat differ diff --git a/test_data/del_rid_log.dat b/test_data/del_rid_log.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/del_rid_log.dat differ diff --git a/test_data/idx_abort_log.dat b/test_data/idx_abort_log.dat new file mode 100644 index 0000000..9cc1494 Binary files /dev/null and b/test_data/idx_abort_log.dat differ diff --git a/test_data/idx_insert_fault.dat b/test_data/idx_insert_fault.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/idx_insert_fault.dat differ diff --git a/test_data/idx_rm_fault.dat b/test_data/idx_rm_fault.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/idx_rm_fault.dat differ diff --git a/test_data/ins_rid_log.dat b/test_data/ins_rid_log.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/ins_rid_log.dat differ diff --git a/test_data/overflow10_log.dat b/test_data/overflow10_log.dat new file mode 100644 index 0000000..bce351f Binary files /dev/null and b/test_data/overflow10_log.dat differ diff --git a/test_data/overflow_log.dat b/test_data/overflow_log.dat new file mode 100644 index 0000000..178e69f Binary files /dev/null and b/test_data/overflow_log.dat differ diff --git a/test_data/phys_remove_fault.dat b/test_data/phys_remove_fault.dat new file mode 100644 index 0000000..d7b2cdf Binary files /dev/null and b/test_data/phys_remove_fault.dat differ diff --git a/test_data/prepare_log.dat b/test_data/prepare_log.dat new file mode 100644 index 0000000..3e7b42d Binary files /dev/null and b/test_data/prepare_log.dat differ diff --git a/test_data/prepare_log_fail.dat b/test_data/prepare_log_fail.dat new file mode 100644 index 0000000..e69de29 diff --git a/test_data/shared_abort_log.dat b/test_data/shared_abort_log.dat new file mode 100644 index 0000000..d7b2cdf Binary files /dev/null and b/test_data/shared_abort_log.dat differ diff --git a/test_data/shared_locks_log.dat b/test_data/shared_locks_log.dat new file mode 100644 index 0000000..0694a63 Binary files /dev/null and b/test_data/shared_locks_log.dat differ diff --git a/test_data/txn_log.dat b/test_data/txn_log.dat new file mode 100644 index 0000000..ef547e0 Binary files /dev/null and b/test_data/txn_log.dat differ diff --git a/test_data/unknown_log.dat b/test_data/unknown_log.dat new file mode 100644 index 0000000..d7b2cdf Binary files /dev/null and b/test_data/unknown_log.dat differ diff --git a/test_data/upd_both_found.dat b/test_data/upd_both_found.dat new file mode 100644 index 0000000..99b17ee Binary files /dev/null and b/test_data/upd_both_found.dat differ diff --git a/test_data/upd_idx_ins_fault.dat b/test_data/upd_idx_ins_fault.dat new file mode 100644 index 0000000..cf8a45a Binary files /dev/null and b/test_data/upd_idx_ins_fault.dat differ diff --git a/test_data/upd_idx_log.dat b/test_data/upd_idx_log.dat new file mode 100644 index 0000000..fe61869 Binary files /dev/null and b/test_data/upd_idx_log.dat differ diff --git a/test_data/upd_new_notfound.dat b/test_data/upd_new_notfound.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/upd_new_notfound.dat differ diff --git a/test_data/upd_old_notfound.dat b/test_data/upd_old_notfound.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/upd_old_notfound.dat differ diff --git a/test_data/upd_phys_rm_fault.dat b/test_data/upd_phys_rm_fault.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/upd_phys_rm_fault.dat differ diff --git a/test_data/upd_undo_rm_fault.dat b/test_data/upd_undo_rm_fault.dat new file mode 100644 index 0000000..23c6823 Binary files /dev/null and b/test_data/upd_undo_rm_fault.dat differ diff --git a/tests/operator_tests.cpp b/tests/operator_tests.cpp index c955f31..25ce68e 100644 --- a/tests/operator_tests.cpp +++ b/tests/operator_tests.cpp @@ -1252,4 +1252,147 @@ TEST_F(OperatorTests, HashJoinNullKeys) { join->close(); } +TEST_F(OperatorTests, ProjectNextViewNonSimpleProjection) { + // Test ProjectOperator::next_view() with non-simple projection (!is_simple_projection_) + // next_view() returns false early when is_simple_projection_ is false + // (BufferScanOperator doesn't implement next_view(), so we test the early-return path) + Schema schema = make_schema( + {{"id", common::ValueType::TYPE_INT64}, {"name", common::ValueType::TYPE_TEXT}}); + std::vector data; + data.push_back(make_tuple({common::Value::make_int64(1), common::Value::make_text("alice")})); + + auto scan = make_buffer_scan("test_table", data, schema); + + // Use a constant expression instead of column reference — this makes is_simple_projection_ = + // false + std::vector> cols; + cols.push_back(const_expr(common::Value::make_int64(42))); // constant, not column + auto project = make_project(std::move(scan), std::move(cols)); + + ASSERT_TRUE(project->init()); + ASSERT_TRUE(project->open()); + + // next() should work (materializes the constant) + Tuple tuple; + EXPECT_TRUE(project->next(tuple)); + EXPECT_EQ(tuple.size(), 1U); + + // next_view() returns false early for non-simple projection + storage::HeapTable::TupleView view; + EXPECT_FALSE(project->next_view(view)); + + project->close(); +} + +TEST_F(OperatorTests, LimitNextView) { + // Test LimitOperator::next_view() — BufferScanOperator doesn't implement next_view() + // (uses base stub that returns false), so we test the early-return paths + Schema schema = make_schema({{"id", common::ValueType::TYPE_INT64}}); + std::vector data; + for (int i = 0; i < 5; i++) { + data.push_back(make_tuple({common::Value::make_int64(i)})); + } + + auto scan = make_buffer_scan("test_table", data, schema); + auto limit = make_limit(std::move(scan), 2); // limit 2 + + ASSERT_TRUE(limit->init()); + ASSERT_TRUE(limit->open()); + + // LimitOperator::next_view() calls child_->next_view() which returns false + // (BufferScan doesn't implement next_view). This exercises the early-return path. + storage::HeapTable::TupleView view; + EXPECT_FALSE(limit->next_view(view)); + limit->close(); +} + +TEST_F(OperatorTests, LimitNextViewZeroLimit) { + // Test LimitOperator::next_view() with limit=0 + Schema schema = make_schema({{"id", common::ValueType::TYPE_INT64}}); + std::vector data; + for (int i = 0; i < 5; i++) { + data.push_back(make_tuple({common::Value::make_int64(i)})); + } + + auto scan = make_buffer_scan("test_table", data, schema); + auto limit = make_limit(std::move(scan), 0); // limit 0 + + ASSERT_TRUE(limit->init()); + ASSERT_TRUE(limit->open()); + + // With limit=0, next_view() returns false early + // This exercises the limit check + storage::HeapTable::TupleView view; + EXPECT_FALSE(limit->next_view(view)); + limit->close(); +} + +TEST_F(OperatorTests, LimitNextViewOffsetExceedsTotal) { + // Test LimitOperator::next_view() when offset exceeds data size + Schema schema = make_schema({{"id", common::ValueType::TYPE_INT64}}); + std::vector data; + for (int i = 0; i < 3; i++) { + data.push_back(make_tuple({common::Value::make_int64(i)})); + } + + auto scan = make_buffer_scan("test_table", data, schema); + auto limit = make_limit(std::move(scan), 10, 100); // offset 100, limit 10 + + ASSERT_TRUE(limit->init()); + ASSERT_TRUE(limit->open()); + + // offset 100 exceeds data size 3, so child_->next_view() returns false + // This exercises the offset loop early-return + storage::HeapTable::TupleView view; + EXPECT_FALSE(limit->next_view(view)); + limit->close(); +} + +TEST_F(OperatorTests, FilterNextViewChildReturnsFalse) { + // Test FilterOperator::next_view() when child returns false + // BufferScanOperator doesn't implement next_view(), so child_->next_view() returns false + // This exercises the child returns false path + Schema schema = make_schema({{"id", common::ValueType::TYPE_INT64}}); + std::vector data; + for (int i = 0; i < 5; i++) { + data.push_back(make_tuple({common::Value::make_int64(i)})); + } + + auto scan = make_buffer_scan("test_table", data, schema); + auto filter = make_filter( + std::move(scan), + binary_expr(col_expr("id"), TokenType::Ge, const_expr(common::Value::make_int64(2)))); + + ASSERT_TRUE(filter->init()); + ASSERT_TRUE(filter->open()); + + storage::HeapTable::TupleView view; + EXPECT_FALSE(filter->next_view(view)); + filter->close(); +} + +TEST_F(OperatorTests, FilterNextViewAllFiltered) { + // Test FilterOperator::next_view() when all tuples are filtered out + // This exercises the condition evaluate and loop iteration path + Schema schema = make_schema({{"id", common::ValueType::TYPE_INT64}}); + std::vector data; + for (int i = 0; i < 3; i++) { + data.push_back(make_tuple({common::Value::make_int64(i)})); + } + + auto scan = make_buffer_scan("test_table", data, schema); + // Filter: id > 100 (filters all) + auto filter = make_filter( + std::move(scan), + binary_expr(col_expr("id"), TokenType::Gt, const_expr(common::Value::make_int64(100)))); + + ASSERT_TRUE(filter->init()); + ASSERT_TRUE(filter->open()); + + storage::HeapTable::TupleView view; + // BufferScan next_view returns false immediately, so we never even get to evaluate condition + EXPECT_FALSE(filter->next_view(view)); + filter->close(); +} + } // namespace diff --git a/tests/query_executor_tests.cpp b/tests/query_executor_tests.cpp index 4f2e5f6..7797706 100644 --- a/tests/query_executor_tests.cpp +++ b/tests/query_executor_tests.cpp @@ -185,6 +185,34 @@ TEST_F(QueryExecutorTests, InsertIntoNonExistentTable) { EXPECT_FALSE(res.success()); } +TEST_F(QueryExecutorTests, InsertBatchModeSkipsLockAcquisition) { + // Test batch_insert_mode=true skips lock acquisition (line 217) + TestEnvironment env; + execute_sql(env.executor, "CREATE TABLE test_table (id INT, val INT)"); + + // Enable batch insert mode - skips lock acquisition per line 217 + env.executor.set_batch_insert_mode(true); + + // BEGIN transaction + execute_sql(env.executor, "BEGIN"); + + // Multi-row INSERT - should succeed without lock acquisition + const auto res = + execute_sql(env.executor, "INSERT INTO test_table VALUES (1, 10), (2, 20), (3, 30)"); + EXPECT_TRUE(res.success()); + EXPECT_EQ(res.rows_affected(), 3U); + + // COMMIT + execute_sql(env.executor, "COMMIT"); + + // Verify data was inserted + const auto select_res = execute_sql(env.executor, "SELECT * FROM test_table"); + EXPECT_EQ(select_res.row_count(), 3U); + + // Cleanup + env.executor.set_batch_insert_mode(false); +} + // ============= SELECT Tests ============= TEST_F(QueryExecutorTests, SelectStarFromEmptyTable) { diff --git a/tests/transaction_manager_tests.cpp b/tests/transaction_manager_tests.cpp index 899083b..da0628c 100644 --- a/tests/transaction_manager_tests.cpp +++ b/tests/transaction_manager_tests.cpp @@ -1508,4 +1508,108 @@ TEST(TransactionManagerTests, UndoLogUnknownType) { static_cast(std::remove("./test_data/unknown_test.heap")); } +TEST(TransactionManagerTests, UndoDeleteTableNotFound) { + // Test table metadata not found branch in DELETE undo path + // Manually add DELETE undo log for non-existent table, call abort() + auto catalog = Catalog::create(); + storage::StorageManager disk_manager("./test_data"); + storage::BufferPoolManager bpm(cloudsql::config::Config::DEFAULT_BUFFER_POOL_SIZE, + disk_manager); + LockManager lm; + TransactionManager tm(lm, *catalog, bpm, bpm.get_log_manager()); + + Transaction* txn = tm.begin(); + ASSERT_NE(txn, nullptr); + + // Add DELETE undo log for non-existent table + txn->add_undo_log(UndoLog::Type::DELETE, "nonexistent_delete_table", + HeapTable::TupleId(99, 99)); + + // abort() should hit table metadata lookup failure + tm.abort(txn); + EXPECT_EQ(txn->get_state(), TransactionState::ABORTED); +} + +TEST(TransactionManagerTests, UndoUpdatePhysicalRemoveFailure) { + // Test FAULT_PHYSICAL_REMOVE branch in UPDATE undo + storage::StorageManager disk_manager("./test_data"); + disk_manager.create_dir_if_not_exists(); + recovery::LogManager log_mgr("./test_data/upd_phys_rm_fault.dat"); + storage::BufferPoolManager bpm(cloudsql::config::Config::DEFAULT_BUFFER_POOL_SIZE, disk_manager, + &log_mgr); + auto catalog = Catalog::create(); + LockManager lm; + TransactionManager tm(lm, *catalog, bpm, &log_mgr); + executor::QueryExecutor exec(*catalog, bpm, lm, tm); + + static_cast(std::remove("./test_data/upd_phys_rm.heap")); + static_cast(std::remove("./test_data/upd_phys_rm.idx")); + + static_cast( + exec.execute(*Parser(std::make_unique("CREATE TABLE upd_phys_rm (id INT, val INT)")) + .parse_statement())); + static_cast( + exec.execute(*Parser(std::make_unique("CREATE INDEX idx_upr ON upd_phys_rm (val)")) + .parse_statement())); + static_cast( + exec.execute(*Parser(std::make_unique("INSERT INTO upd_phys_rm VALUES (1, 100)")) + .parse_statement())); + static_cast(exec.execute(*Parser(std::make_unique("COMMIT")).parse_statement())); + + // UPDATE then ROLLBACK with fault injection + static_cast(exec.execute(*Parser(std::make_unique("BEGIN")).parse_statement())); + static_cast(exec.execute( + *Parser(std::make_unique("UPDATE upd_phys_rm SET val = 999 WHERE id = 1")) + .parse_statement())); + + // Arm fault for physical_remove failure during UPDATE undo + cloudsql::common::FaultInjection::instance().set_fault(cloudsql::common::FAULT_PHYSICAL_REMOVE); + static_cast(exec.execute(*Parser(std::make_unique("ROLLBACK")).parse_statement())); + cloudsql::common::FaultInjection::instance().clear(); + + static_cast(std::remove("./test_data/upd_phys_rm.heap")); + static_cast(std::remove("./test_data/upd_phys_rm.idx")); +} + +TEST(TransactionManagerTests, UndoUpdateUndoRemoveFailure) { + // Test FAULT_UNDO_REMOVE branch in UPDATE undo old_rid restore + storage::StorageManager disk_manager("./test_data"); + disk_manager.create_dir_if_not_exists(); + recovery::LogManager log_mgr("./test_data/upd_undo_rm_fault.dat"); + storage::BufferPoolManager bpm(cloudsql::config::Config::DEFAULT_BUFFER_POOL_SIZE, disk_manager, + &log_mgr); + auto catalog = Catalog::create(); + LockManager lm; + TransactionManager tm(lm, *catalog, bpm, &log_mgr); + executor::QueryExecutor exec(*catalog, bpm, lm, tm); + + static_cast(std::remove("./test_data/upd_undo_rm.heap")); + static_cast(std::remove("./test_data/upd_undo_rm.idx")); + + static_cast( + exec.execute(*Parser(std::make_unique("CREATE TABLE upd_undo_rm (id INT, val INT)")) + .parse_statement())); + static_cast( + exec.execute(*Parser(std::make_unique("CREATE INDEX idx_uur ON upd_undo_rm (val)")) + .parse_statement())); + static_cast( + exec.execute(*Parser(std::make_unique("INSERT INTO upd_undo_rm VALUES (1, 100)")) + .parse_statement())); + static_cast(exec.execute(*Parser(std::make_unique("COMMIT")).parse_statement())); + + // UPDATE then ROLLBACK with fault injection for undo_remove + static_cast(exec.execute(*Parser(std::make_unique("BEGIN")).parse_statement())); + static_cast(exec.execute( + *Parser(std::make_unique("UPDATE upd_undo_rm SET val = 999 WHERE id = 1")) + .parse_statement())); + + // Arm fault for undo_remove failure during UPDATE undo (old_rid restore) + cloudsql::common::FaultInjection::instance().set_fault(cloudsql::common::FAULT_UNDO_REMOVE); + static_cast(exec.execute(*Parser(std::make_unique("ROLLBACK")).parse_statement())); + cloudsql::common::FaultInjection::instance().clear(); + + static_cast(std::remove("./test_data/upd_undo_rm.heap")); + static_cast(std::remove("./test_data/upd_undo_rm.idx")); +} + } // namespace