Persistence: empty table -> no CSV (per Iteration 2 follow-up)

The Iteration-2 rule wrote a header-only CSV for every existing
table, which surprised users who created a table and saw a file
appear before any data went in. Tighten the rule: a CSV exists
iff the table has rows. Persistence::write_table_data now
delegates to delete_table_data when the snapshot is empty,
removing any prior CSV. The schema-only invariant (YAML knows the
table; CSV knows its rows) is preserved.

The cascade-delete integration test was rewritten to assert the
CSVs vanish; two new tests pin the rule (create -> no CSV;
delete --all-rows -> CSV removed).

Tests: 291 passing (256 lib + 9 + 9 + 17), 0 failing, 0 skipped.
This commit is contained in:
claude@clouddev1
2026-05-07 21:49:28 +00:00
parent 5c076f6d8f
commit 5410075398
2 changed files with 95 additions and 5 deletions
+12
View File
@@ -155,7 +155,19 @@ impl Persistence {
/// Write `data/<table>.csv` from a table snapshot. Atomic
/// per file. Creates the `data/` directory if missing
/// (tolerant of fresh projects).
///
/// **Empty tables produce no CSV.** A header-only file
/// would carry no information beyond what `project.yaml`
/// already records, so an empty snapshot is treated
/// identically to "drop this table's data file": the CSV
/// is removed if it exists, no file is created if it
/// doesn't. This keeps the rule "data lives in CSV; no
/// data, no CSV" consistent and avoids surprising users
/// with files they didn't ask for.
pub fn write_table_data(&self, table: &TableSnapshot) -> Result<(), PersistenceError> {
if table.rows.is_empty() {
return self.delete_table_data(&table.name);
}
let data_dir = self.project_path.join(DATA_DIR);
fs::create_dir_all(&data_dir).map_err(|source| PersistenceError::Io {
operation: "create",
+83 -5
View File
@@ -240,11 +240,89 @@ fn delete_with_cascade_rewrites_both_csvs() {
assert_eq!(result.rows_affected, 1);
});
let customers_csv = read_csv(&path, "Customers").expect("Customers.csv");
let orders_csv = read_csv(&path, "Orders").expect("Orders.csv");
// Both CSVs should be header-only after cascade.
assert_eq!(customers_csv.lines().count(), 1, "got: {customers_csv}");
assert_eq!(orders_csv.lines().count(), 1, "got: {orders_csv}");
// Both CSVs should be gone after the cascade leaves both
// tables empty: empty table -> no CSV (the rule from
// Persistence::write_table_data; see ADR-0015 §4 commentary).
assert!(
read_csv(&path, "Customers").is_none(),
"Customers.csv should be gone after cascade leaves it empty",
);
assert!(
read_csv(&path, "Orders").is_none(),
"Orders.csv should be gone after cascade leaves it empty",
);
}
#[test]
fn create_table_does_not_write_csv_for_empty_table() {
let data = tempdir();
let (_p, db, path) = open_project(&data);
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
],
vec!["id".to_string()],
Some("create table Customers with pk id:serial".to_string()),
)
.await
.unwrap();
});
// Schema landed in YAML.
let yaml = read_yaml(&path);
assert!(yaml.contains("- name: Customers"), "yaml missing table:\n{yaml}");
// ...but no CSV until there's data.
assert!(
read_csv(&path, "Customers").is_none(),
"no CSV should exist for an empty table",
);
}
#[test]
fn delete_all_rows_removes_csv() {
let data = tempdir();
let (_p, db, path) = open_project(&data);
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec { name: "id".to_string(), ty: Type::Serial },
ColumnSpec { name: "Name".to_string(), ty: Type::Text },
],
vec!["id".to_string()],
Some("create table Customers with pk id:serial".to_string()),
)
.await
.unwrap();
db.insert(
"Customers".to_string(),
None,
vec![Value::Text("Alice".to_string())],
Some("insert into Customers ('Alice')".to_string()),
)
.await
.unwrap();
// CSV exists once there's data.
assert!(read_csv(&path, "Customers").is_some());
db.delete(
"Customers".to_string(),
RowFilter::AllRows,
Some("delete from Customers --all-rows".to_string()),
)
.await
.unwrap();
});
assert!(
read_csv(&path, "Customers").is_none(),
"CSV should be removed when the table becomes empty",
);
}
#[test]