diff --git a/src/persistence/mod.rs b/src/persistence/mod.rs
index b6823da..40a2c6c 100644
--- a/src/persistence/mod.rs
+++ b/src/persistence/mod.rs
@@ -155,7 +155,19 @@ impl Persistence {
/// Write `data/
.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",
diff --git a/tests/iteration2_persistence.rs b/tests/iteration2_persistence.rs
index 3d1b7b6..b1a0535 100644
--- a/tests/iteration2_persistence.rs
+++ b/tests/iteration2_persistence.rs
@@ -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]