@@ -1022,4 +1022,244 @@ TEST(ParserTests, ExhaustiveParserErrors) {
10221022 }
10231023}
10241024
1025+ // ============= TupleView / next_view Tests =============
1026+
1027+ // Helper: build a two-column (INT, TEXT) HeapTable with N rows.
1028+ namespace {
1029+ struct TupleViewTestCtx {
1030+ StorageManager disk;
1031+ BufferPoolManager sm;
1032+ Schema schema;
1033+ std::unique_ptr<HeapTable> table;
1034+
1035+ explicit TupleViewTestCtx (const std::string& name)
1036+ : disk(" ./test_data" ),
1037+ sm(config::Config::DEFAULT_BUFFER_POOL_SIZE, disk) {
1038+ schema.add_column (" id" , ValueType::TYPE_INT64);
1039+ schema.add_column (" tag" , ValueType::TYPE_TEXT);
1040+ table = std::make_unique<HeapTable>(name, sm, schema);
1041+ table->create ();
1042+ }
1043+
1044+ void insert (int64_t id, const std::string& tag) {
1045+ table->insert (Tuple ({Value::make_int64 (id), Value::make_text (tag)}));
1046+ }
1047+ };
1048+ } // namespace
1049+
1050+ // 1. Basic scan via next_view: correct row count and values for SELECT *
1051+ TEST (TupleViewTests, BasicScanSelectStar) {
1052+ const std::string name = " tv_basic" ;
1053+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1054+
1055+ TupleViewTestCtx ctx (name);
1056+ ctx.insert (1 , " a" );
1057+ ctx.insert (2 , " b" );
1058+ ctx.insert (3 , " c" );
1059+
1060+ // Build a SeqScan wrapped by a SELECT * ProjectOperator (no txn = fast path)
1061+ std::vector<std::unique_ptr<parser::Expression>> cols;
1062+ cols.push_back (std::make_unique<parser::ColumnExpr>(" *" ));
1063+
1064+ auto scan = std::make_unique<SeqScanOperator>(
1065+ std::make_shared<HeapTable>(name, ctx.sm , ctx.schema ), nullptr , nullptr );
1066+ auto proj =
1067+ std::make_unique<ProjectOperator>(std::move (scan), std::move (cols));
1068+
1069+ ASSERT_TRUE (proj->init ());
1070+ ASSERT_TRUE (proj->open ());
1071+
1072+ HeapTable::TupleView view;
1073+ int count = 0 ;
1074+ while (proj->next_view (view)) {
1075+ count++;
1076+ // Values should be accessible through the view
1077+ EXPECT_FALSE (view.get_value (0 ).is_null ());
1078+ EXPECT_FALSE (view.get_value (1 ).is_null ());
1079+ }
1080+ proj->close ();
1081+
1082+ EXPECT_EQ (count, 3 );
1083+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1084+ }
1085+
1086+ // 2. Deleted tuples (xmax != 0) are skipped by next_view
1087+ TEST (TupleViewTests, DeletedTuplesSkipped) {
1088+ const std::string name = " tv_deleted" ;
1089+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1090+
1091+ TupleViewTestCtx ctx (name);
1092+ auto id1 = ctx.table ->insert (Tuple ({Value::make_int64 (10 ), Value::make_text (" alive" )}));
1093+ auto id2 = ctx.table ->insert (Tuple ({Value::make_int64 (20 ), Value::make_text (" dead" )}));
1094+ // Mark id2 as deleted by setting xmax != 0
1095+ ctx.table ->remove (id2, /* xmax=*/ 1 );
1096+
1097+ std::vector<std::unique_ptr<parser::Expression>> cols;
1098+ cols.push_back (std::make_unique<parser::ColumnExpr>(" *" ));
1099+
1100+ auto scan = std::make_unique<SeqScanOperator>(
1101+ std::make_shared<HeapTable>(name, ctx.sm , ctx.schema ), nullptr , nullptr );
1102+ auto proj = std::make_unique<ProjectOperator>(std::move (scan), std::move (cols));
1103+
1104+ ASSERT_TRUE (proj->init ());
1105+ ASSERT_TRUE (proj->open ());
1106+
1107+ HeapTable::TupleView view;
1108+ int count = 0 ;
1109+ while (proj->next_view (view)) {
1110+ count++;
1111+ // Only the alive row should come through
1112+ EXPECT_EQ (view.get_value (0 ).to_int64 (), 10 );
1113+ }
1114+ proj->close ();
1115+
1116+ EXPECT_EQ (count, 1 );
1117+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1118+ }
1119+
1120+ // 3. Non-identity column projection: SELECT tag, id (columns reversed)
1121+ // get_value must resolve physical indices through column_mapping.
1122+ TEST (TupleViewTests, NonIdentityProjectionValues) {
1123+ const std::string name = " tv_proj" ;
1124+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1125+
1126+ TupleViewTestCtx ctx (name);
1127+ ctx.insert (42 , " hello" );
1128+
1129+ // SELECT tag, id (logical 0 -> physical 1, logical 1 -> physical 0)
1130+ std::vector<std::unique_ptr<parser::Expression>> cols;
1131+ cols.push_back (std::make_unique<parser::ColumnExpr>(" tag" ));
1132+ cols.push_back (std::make_unique<parser::ColumnExpr>(" id" ));
1133+
1134+ auto scan = std::make_unique<SeqScanOperator>(
1135+ std::make_shared<HeapTable>(name, ctx.sm , ctx.schema ), nullptr , nullptr );
1136+ auto proj = std::make_unique<ProjectOperator>(std::move (scan), std::move (cols));
1137+
1138+ ASSERT_TRUE (proj->init ());
1139+ ASSERT_TRUE (proj->open ());
1140+
1141+ HeapTable::TupleView view;
1142+ ASSERT_TRUE (proj->next_view (view));
1143+
1144+ // Logical column 0 is "tag" -> physical index 1 -> "hello"
1145+ EXPECT_EQ (view.get_value (0 ).as_text (), " hello" );
1146+ // Logical column 1 is "id" -> physical index 0 -> 42
1147+ EXPECT_EQ (view.get_value (1 ).to_int64 (), 42 );
1148+
1149+ // No more rows
1150+ EXPECT_FALSE (proj->next_view (view));
1151+ proj->close ();
1152+
1153+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1154+ }
1155+
1156+ // 4. Computed-expression projection returns false immediately without consuming rows.
1157+ // Callers must fall back to next() for computed projections.
1158+ TEST (TupleViewTests, ComputedProjectionDoesNotConsumeRows) {
1159+ const std::string name = " tv_computed" ;
1160+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1161+
1162+ TupleViewTestCtx ctx (name);
1163+ ctx.insert (5 , " x" );
1164+ ctx.insert (6 , " y" );
1165+
1166+ // SELECT id + 1 (computed expression — not a simple column reference)
1167+ std::vector<std::unique_ptr<parser::Expression>> cols;
1168+ cols.push_back (std::make_unique<parser::BinaryExpr>(
1169+ std::make_unique<parser::ColumnExpr>(" id" ), parser::TokenType::Plus,
1170+ std::make_unique<parser::ConstantExpr>(Value::make_int64 (1 ))));
1171+
1172+ auto scan = std::make_unique<SeqScanOperator>(
1173+ std::make_shared<HeapTable>(name, ctx.sm , ctx.schema ), nullptr , nullptr );
1174+ auto proj = std::make_unique<ProjectOperator>(std::move (scan), std::move (cols));
1175+
1176+ ASSERT_TRUE (proj->init ());
1177+ ASSERT_TRUE (proj->open ());
1178+
1179+ // next_view should return false immediately (unsupported path).
1180+ HeapTable::TupleView view;
1181+ EXPECT_FALSE (proj->next_view (view));
1182+
1183+ // Rows must still be readable via the regular next() path.
1184+ // Reopen to reset state — use a fresh operator.
1185+ proj->close ();
1186+
1187+ std::vector<std::unique_ptr<parser::Expression>> cols2;
1188+ cols2.push_back (std::make_unique<parser::BinaryExpr>(
1189+ std::make_unique<parser::ColumnExpr>(" id" ), parser::TokenType::Plus,
1190+ std::make_unique<parser::ConstantExpr>(Value::make_int64 (1 ))));
1191+
1192+ auto scan2 = std::make_unique<SeqScanOperator>(
1193+ std::make_shared<HeapTable>(name, ctx.sm , ctx.schema ), nullptr , nullptr );
1194+ auto proj2 = std::make_unique<ProjectOperator>(std::move (scan2), std::move (cols2));
1195+
1196+ ASSERT_TRUE (proj2->init ());
1197+ ASSERT_TRUE (proj2->open ());
1198+
1199+ Tuple t;
1200+ int count = 0 ;
1201+ while (proj2->next (t)) {
1202+ count++;
1203+ // id + 1: first row is 5+1=6, second is 6+1=7
1204+ EXPECT_GT (t.get (0 ).to_int64 (), 5 );
1205+ }
1206+ proj2->close ();
1207+ EXPECT_EQ (count, 2 );
1208+
1209+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1210+ }
1211+
1212+ // 5. table_schema is set correctly in next_view (non-null)
1213+ TEST (TupleViewTests, TableSchemaSetByNextView) {
1214+ const std::string name = " tv_schema" ;
1215+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1216+
1217+ TupleViewTestCtx ctx (name);
1218+ ctx.insert (99 , " z" );
1219+
1220+ auto iter = ctx.table ->scan ();
1221+ HeapTable::TupleView view;
1222+ ASSERT_TRUE (iter.next_view (view));
1223+
1224+ EXPECT_NE (view.table_schema , nullptr );
1225+ EXPECT_NE (view.schema , nullptr );
1226+ EXPECT_EQ (view.table_schema ->column_count (), 2u );
1227+
1228+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1229+ }
1230+
1231+ // 6. FilterOperator::next_view filters correctly (materializes per-row for condition eval)
1232+ TEST (TupleViewTests, FilterOperatorNextView) {
1233+ const std::string name = " tv_filter" ;
1234+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1235+
1236+ TupleViewTestCtx ctx (name);
1237+ ctx.insert (1 , " a" );
1238+ ctx.insert (2 , " b" );
1239+ ctx.insert (3 , " c" );
1240+
1241+ // WHERE id >= 2
1242+ auto condition = std::make_unique<parser::BinaryExpr>(
1243+ std::make_unique<parser::ColumnExpr>(" id" ), parser::TokenType::Ge,
1244+ std::make_unique<parser::ConstantExpr>(Value::make_int64 (2 )));
1245+
1246+ auto scan = std::make_unique<SeqScanOperator>(
1247+ std::make_shared<HeapTable>(name, ctx.sm , ctx.schema ), nullptr , nullptr );
1248+ auto filter = std::make_unique<FilterOperator>(std::move (scan), std::move (condition));
1249+
1250+ ASSERT_TRUE (filter->init ());
1251+ ASSERT_TRUE (filter->open ());
1252+
1253+ HeapTable::TupleView view;
1254+ int count = 0 ;
1255+ while (filter->next_view (view)) {
1256+ count++;
1257+ EXPECT_GE (view.get_value (0 ).to_int64 (), 2 );
1258+ }
1259+ filter->close ();
1260+
1261+ EXPECT_EQ (count, 2 );
1262+ static_cast <void >(std::remove ((" ./test_data/" + name + " .heap" ).c_str ()));
1263+ }
1264+
10251265} // namespace
0 commit comments