diff --git a/PARITY.md b/PARITY.md index da83b0d85..d88861bfc 100644 --- a/PARITY.md +++ b/PARITY.md @@ -18,8 +18,10 @@ lowercase `action` names — are language-neutral. Since [#1131](https://github.com/thekevinscott/dirsql/issues/1131) they live once in `dirsql::flatten_row_event` (`#[doc(hidden)]`, not a public Rust API); each binding keeps only the marshaling of the borrowed rows into host values -(`value_row_to_py_dict` / `value_row_to_js`) and its own event struct. No -SDK-visible shape changed — this restores parity rather than introducing drift. +(`value_row_to_py_dict` / `value_row_to_js`) and its own event struct. The +CLI's HTTP `/events` serializer shares that same source, so the SSE payload and +the two SDK event objects cannot drift apart. No SDK-visible shape changed — +this restores parity rather than introducing drift. The `on_file` callback receives a single argument: the absolute filesystem path of the matched file. `dirsql` does not read file contents — a callback diff --git a/packages/python/e2e-attestations/claude-1139-sse-flatten.json b/packages/python/e2e-attestations/claude-1139-sse-flatten.json new file mode 100644 index 000000000..761f10abd --- /dev/null +++ b/packages/python/e2e-attestations/claude-1139-sse-flatten.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1789750671, + "exit_code": 0, + "commit": "5c33ad533545ff8791f462f42b2c3869b871ec7a", + "branch": "claude/1139-sse-flatten" +} diff --git a/packages/rust/changelog.d/2026-09-18-sse-shares-flatten-row-event.md b/packages/rust/changelog.d/2026-09-18-sse-shares-flatten-row-event.md new file mode 100644 index 000000000..7330ec41a --- /dev/null +++ b/packages/rust/changelog.d/2026-09-18-sse-shares-flatten-row-event.md @@ -0,0 +1,7 @@ +**Changed** + +The CLI's HTTP `/events` serializer now builds its JSON from +`dirsql::flatten_row_event` instead of its own per-variant `match`, leaving the +core as the single owner of the flattening decisions. The SSE payload is +unchanged byte for byte: same keys, same key order, same null-versus-absent +handling, same blob-to-hex rendering. diff --git a/packages/rust/src/cli/serialize.rs b/packages/rust/src/cli/serialize.rs index c487e8c31..0fe632c35 100644 --- a/packages/rust/src/cli/serialize.rs +++ b/packages/rust/src/cli/serialize.rs @@ -1,10 +1,11 @@ //! `Row` / `RowEvent` → JSON serialization for the HTTP API. -use serde_json::{Map, Value, json}; +use serde_json::{Map, Value}; use crate::Row; use crate::db::Value as CellValue; use crate::differ::RowEvent; +use crate::row_event_flat::flatten_row_event; pub(super) fn rows_to_json(rows: &[Row]) -> Vec { rows.iter().map(row_to_json).collect() @@ -35,52 +36,27 @@ pub(super) fn event_to_json(event: &RowEvent) -> String { } fn event_to_value(event: &RowEvent) -> Value { - match event { - RowEvent::Insert { - table, - row, - file_path, - } => json!({ - "action": "insert", - "table": table, - "file_path": file_path, - "row": row_to_json(row), - "old_row": Value::Null, - }), - RowEvent::Update { - table, - old_row, - new_row, - file_path, - } => json!({ - "action": "update", - "table": table, - "file_path": file_path, - "row": row_to_json(new_row), - "old_row": row_to_json(old_row), - }), - RowEvent::Delete { - table, - row, - file_path, - } => json!({ - "action": "delete", - "table": table, - "file_path": file_path, - "row": row_to_json(row), - "old_row": Value::Null, - }), - RowEvent::Error { - table, - file_path, - error, - } => json!({ - "action": "error", - "table": table, - "file_path": file_path.to_string_lossy(), - "error": error, - }), + let flat = flatten_row_event(event); + let mut map = Map::new(); + map.insert("action".into(), Value::from(flat.action)); + map.insert( + "table".into(), + flat.table.map_or(Value::Null, Value::String), + ); + map.insert("file_path".into(), Value::String(flat.file_path)); + match flat.error { + Some(error) => { + map.insert("error".into(), Value::String(error)); + } + None => { + map.insert("row".into(), flat.row.map_or(Value::Null, row_to_json)); + map.insert( + "old_row".into(), + flat.old_row.map_or(Value::Null, row_to_json), + ); + } } + Value::Object(map) } #[cfg(test)] @@ -116,6 +92,7 @@ mod tests { Some("posts/a.json"), ); assert!(parsed.get("old_row").unwrap().is_null()); + assert!(parsed.get("error").is_none()); assert_eq!(event_to_json(&event), parsed.to_string()); } @@ -157,6 +134,19 @@ mod tests { ); } + #[test] + fn error_event_omits_the_row_keys() { + let event = RowEvent::Error { + table: None, + file_path: PathBuf::from("bad.json"), + error: "parse failed".into(), + }; + let parsed = event_to_value(&event); + assert!(parsed.get("table").unwrap().is_null()); + assert!(parsed.get("row").is_none()); + assert!(parsed.get("old_row").is_none()); + } + #[test] fn delete_event_emits_expected_shape() { let mut row: Row = HashMap::new(); diff --git a/packages/ts/e2e-attestations/claude-1139-sse-flatten.json b/packages/ts/e2e-attestations/claude-1139-sse-flatten.json new file mode 100644 index 000000000..88623fddf --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1139-sse-flatten.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1789750728, + "exit_code": 0, + "commit": "7d0bce94d84c7e51ec1e6a2bf005b4a265624456", + "branch": "claude/1139-sse-flatten" +}