Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions include/internal/data_frame/data_frame.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,21 +259,28 @@ namespace csv {
*
* CSVRow copies share the underlying parsed row storage, so this is intended for
* filtered document views that should avoid reparsing or rematerializing fields.
* Existing cell edits are copied; subsequent edits to either frame are independent.
*/
DataFrame selected_rows(const std::vector<std::uint8_t>& include_rows) const {
if (include_rows.size() != this->rows.size()) {
throw std::invalid_argument("selected row mask size must match DataFrame row count");
}

std::vector<CSVRow> selected;
selected.reserve(this->rows.size());
DataFrame result;
result.rows.reserve(this->rows.size());
for (size_t row_index = 0; row_index < this->rows.size(); ++row_index) {
if (include_rows[row_index]) {
selected.push_back(this->rows[row_index]);
result.rows.push_back(this->rows[row_index]);
result.edits.emplace_back();
if (const RowOverlay* overlay = this->find_row_edits(row_index)) {
RowOverlay snapshot = overlay->snapshot();
if (!snapshot.empty()) {
*result.edits.back().ensure() = std::move(snapshot);
}
}
}
}

DataFrame result(std::move(selected));
result.col_names_ = this->col_names_;
result.physical_col_names_ = this->physical_col_names_;
result.column_indices_ = this->column_indices_;
Expand Down
7 changes: 7 additions & 0 deletions include/internal/data_frame/row_overlay.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ namespace csv {
return values.empty();
}

RowOverlay snapshot() const {
row_overlay_lock_guard lock(this);
RowOverlay result;
result.values = values;
return result;
}

private:
struct row_overlay_lock_guard {
explicit row_overlay_lock_guard(const RowOverlay* overlay)
Expand Down
126 changes: 126 additions & 0 deletions tests/test_data_frame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -946,3 +946,129 @@ TEST_CASE("DataFrame: const row erase is rejected", "[data_frame]") {
REQUIRE_THROWS_AS(row.erase(), std::runtime_error);
REQUIRE_THROWS_AS(row["name"] = "Blocked", std::runtime_error);
}

TEST_CASE("DataFrame: selected rows preserve independent sparse edits", "[data_frame][selected_rows]") {
std::istringstream input(
"id,name,value\n"
"1,Alice,10\n"
"2,Bob,20\n"
"3,Carol,30\n"
"4,David,40\n"
);
CSVReader reader(input);
DataFrame<> frame;
SECTION("Unkeyed source") { frame = DataFrame<>(reader); }
SECTION("Keyed source") { frame = DataFrame<>(reader, "id"); }

frame.at(0)["name"] = "Excluded";
frame.at(1)["name"] = "Robert";
frame.at(1)["value"] = "95";
frame.at(3)["value"] = "45";

const auto& source = frame;
auto selected = source.selected_rows({0, 1, 0, 2});
REQUIRE(selected.size() == 2);
REQUIRE(selected.columns() == frame.columns());
REQUIRE_THROWS_AS(selected["2"], std::runtime_error);
REQUIRE(std::vector<std::string>(selected.at(0)) == std::vector<std::string>{"2", "Robert", "95"});
REQUIRE(std::vector<std::string>(selected.at(1)) == std::vector<std::string>{"4", "David", "45"});
REQUIRE(selected.column("value") == std::vector<std::string>{"95", "45"});
REQUIRE(selected.column_view(1)[0].get<std::string>() == "Robert");
REQUIRE(selected.at(0).to_json() == "{\"id\":2,\"name\":\"Robert\",\"value\":95}");
REQUIRE(selected.at(0).to_json_array() == "[2,\"Robert\",95]");

// Selection should still share parsed bytes, not materialize edited rows.
REQUIRE(selected.at(0).get_underlying_row()["name"].get_sv().data()
== source.at(1).get_underlying_row()["name"].get_sv().data());

std::stringstream output;
auto writer = make_csv_writer(output);
writer << selected.columns();
for (const auto& row : selected) {
#ifdef CSV_HAS_CXX20
writer << row;
#else
writer << std::vector<std::string>(row);
#endif
}
REQUIRE(output.str() == "id,name,value\n2,Robert,95\n4,David,45\n");

const auto& const_selected = selected;
REQUIRE_THROWS_AS(const_selected.at(0)["name"] = "Blocked", std::runtime_error);
REQUIRE_THROWS_AS(const_selected.column_view("name")[0] = "Blocked", std::runtime_error);

// Edits on either frame must not leak into the other frame's overlay.
frame.at(1)["name"] = "Source only";
REQUIRE(selected.at(0)["name"].get<std::string>() == "Robert");
selected.at(0)["value"] = "100";
REQUIRE(frame.at(1)["value"].get<std::string>() == "95");

auto repeated = selected.selected_rows({1, 0});
REQUIRE(std::vector<std::string>(repeated.at(0)) == std::vector<std::string>{"2", "Robert", "100"});
selected.at(0)["name"] = "Selected only";
REQUIRE(repeated.at(0)["name"].get<std::string>() == "Robert");
}

TEST_CASE("DataFrame: selected rows preserve physical edit indices after structural changes", "[data_frame][selected_rows]") {
std::istringstream input("id,name,value\n1,Alice,10\n2,Bob,20\n3,Carol,30\n");
CSVFormat format;
format.column_names_policy(ColumnNamePolicy::CASE_INSENSITIVE);
CSVReader reader(input, format);
DataFrame<> frame(reader, "id");
frame.at(0)["name"] = "Hidden";
frame.at(1)["value"] = "95";
frame.at(2)["value"] = "";
REQUIRE(frame.column_view("name").erase());
REQUIRE(frame.at(0).erase());
frame.insert_row(0, {"4", "40"});

auto selected = frame.selected_rows({0, 1, 1});
REQUIRE(selected.columns() == std::vector<std::string>{"id", "value"});
REQUIRE(selected.index_of("VALUE") == 1);
REQUIRE_FALSE(selected.has_column("name"));
REQUIRE(std::vector<std::string>(selected.at(0)) == std::vector<std::string>{"2", "95"});
REQUIRE(std::vector<std::string>(selected.at(1)) == std::vector<std::string>{"3", ""});
REQUIRE(selected.column_view(1)[0].get<std::string>() == "95");
REQUIRE(selected.at(0).to_json() == "{\"id\":2,\"value\":95}");
REQUIRE(selected.at(1)["VALUE"].get<std::string>().empty());

std::stringstream output;
auto writer = make_csv_writer(output);
writer << selected.columns();
for (const auto& row : selected) {
#ifdef CSV_HAS_CXX20
writer << row;
#else
writer << std::vector<std::string>(row);
#endif
}
REQUIRE(output.str() == "id,value\n2,95\n3,\n");

auto repeated = selected.selected_rows({0, 1});
REQUIRE(std::vector<std::string>(repeated.at(0)) == std::vector<std::string>{"3", ""});
}

TEST_CASE("DataFrame: selected row masks keep boundary behavior", "[data_frame][selected_rows]") {
auto input = make_people_stream();
CSVReader reader(input);
DataFrame<> frame(reader);
// The unedited path must continue to work without any allocated edits.
auto all = frame.selected_rows({1, 1, 1});
REQUIRE(all.column("name") == std::vector<std::string>{"Alice", "Bob", "Carol"});
frame.at(1)["value"] = "95";

REQUIRE_THROWS_AS(frame.selected_rows({1, 0}), std::invalid_argument);
REQUIRE_THROWS_AS(frame.selected_rows({1, 0, 1, 0}), std::invalid_argument);
REQUIRE_THROWS_AS(frame.selected_rows({}), std::invalid_argument);
REQUIRE(frame.at(1)["value"].get<std::string>() == "95");
auto none = frame.selected_rows({0, 0, 0});
REQUIRE(none.empty());
REQUIRE(none.columns() == frame.columns());
REQUIRE(none.selected_rows({}).columns() == frame.columns());
auto edited_all = frame.selected_rows({1, 1, 1});
REQUIRE(edited_all.column("value") == std::vector<std::string>{"10", "95", "30"});

DataFrame<> empty;
REQUIRE(empty.selected_rows({}).empty());
REQUIRE_THROWS_AS(empty.selected_rows({1}), std::invalid_argument);
}
Loading