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
10 changes: 10 additions & 0 deletions PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ API surface comparison across the three language SDKs.
| Row event | `RowEvent` (class, frozen attrs; `file_path` on all variants) | `RowEvent` (enum: Insert/Update/Delete/Error; `file_path` on all variants) | `RowEvent` (plain object with action string; `filePath` on all variants) |
| Row type | `dict[str, Any]` | `HashMap<String, Value>` | `Record<string, unknown>` |

**Where the row-event flattening lives.** Both bindings publish the same flat
`{table, action, row, old_row, error, file_path}` record, and the decisions
behind it — which variant carries an old row, which one may have no table, the
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.

The `on_file` callback receives a single argument: the absolute filesystem
path of the matched file. `dirsql` does not read file contents — a callback
that needs the file body reads it itself. This is consistent across all three
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**Changed**

The extension module now flattens row events through the core's
`flatten_row_event` instead of its own copy of that match. No user-visible
change: `RowEvent` fields and `action` values are identical.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "just test-e2e",
"ran_at": 1789748553,
"exit_code": 0,
"commit": "d106fd4917b9dc93978646f6cb6d8f94374af617",
"branch": "claude/1131-flatten-row-event"
}
158 changes: 8 additions & 150 deletions packages/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#[cfg(feature = "extension-module")]
mod python {
use ::dirsql::extension_resolution::{self, ConfigSource, PlanEntry};
use ::dirsql::{DirSQL, Extension, Row, RowEvent, Table, Value};
use ::dirsql::{DirSQL, Extension, Row, RowEvent, Table, Value, flatten_row_event};
use pyo3::exceptions::{PyOverflowError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyByteArray, PyBytes, PyDict, PyInt, PyList};
Expand Down Expand Up @@ -279,90 +279,18 @@ mod python {
PyRuntimeError::new_err(e.to_string())
}

/// Pure, GIL-free intermediate for a row event. [`row_event_to_plain`]
/// builds it from a core [`RowEvent`] (unit-testable without a Python
/// interpreter); [`row_event_to_py`] then marshals it into the
/// Python-facing [`PyRowEvent`] (the GIL step).
struct PlainRowEvent {
table: Option<String>,
action: &'static str,
row: Option<Row>,
old_row: Option<Row>,
error: Option<String>,
file_path: String,
}

fn row_event_to_plain(event: &RowEvent) -> PlainRowEvent {
match event {
RowEvent::Insert {
table,
row,
file_path,
} => PlainRowEvent {
table: Some(table.clone()),
action: "insert",
row: Some(row.clone()),
old_row: None,
error: None,
file_path: file_path.clone(),
},
RowEvent::Update {
table,
old_row,
new_row,
file_path,
} => PlainRowEvent {
table: Some(table.clone()),
action: "update",
row: Some(new_row.clone()),
old_row: Some(old_row.clone()),
error: None,
file_path: file_path.clone(),
},
RowEvent::Delete {
table,
row,
file_path,
} => PlainRowEvent {
table: Some(table.clone()),
action: "delete",
row: Some(row.clone()),
old_row: None,
error: None,
file_path: file_path.clone(),
},
RowEvent::Error {
table,
file_path,
error,
} => PlainRowEvent {
table: table.clone(),
action: "error",
row: None,
old_row: None,
error: Some(error.clone()),
file_path: file_path.to_string_lossy().to_string(),
},
}
}

fn row_event_to_py(py: Python<'_>, event: &RowEvent) -> PyResult<PyRowEvent> {
let plain = row_event_to_plain(event);
let flat = flatten_row_event(event);
Ok(PyRowEvent {
table: plain.table,
action: plain.action.to_string(),
row: plain
.row
.as_ref()
.map(|r| value_row_to_py_dict(py, r))
.transpose()?,
old_row: plain
table: flat.table,
action: flat.action.to_string(),
row: flat.row.map(|r| value_row_to_py_dict(py, r)).transpose()?,
old_row: flat
.old_row
.as_ref()
.map(|r| value_row_to_py_dict(py, r))
.transpose()?,
error: plain.error,
file_path: Some(plain.file_path),
error: flat.error,
file_path: Some(flat.file_path),
})
}

Expand Down Expand Up @@ -609,76 +537,6 @@ mod python {
mod tests {
use super::*;

fn one_row() -> HashMap<String, Value> {
HashMap::from([("k".to_string(), Value::Integer(7))])
}

#[test]
fn plain_insert_maps_row_and_action() {
let p = row_event_to_plain(&RowEvent::Insert {
table: "t".into(),
row: one_row(),
file_path: "/f".into(),
});
assert_eq!(p.action, "insert");
assert_eq!(p.table.as_deref(), Some("t"));
assert_eq!(
p.row.as_ref().and_then(|r| r.get("k")),
Some(&Value::Integer(7))
);
assert!(p.old_row.is_none());
assert!(p.error.is_none());
assert_eq!(p.file_path, "/f");
}

#[test]
fn plain_update_carries_old_and_new() {
let mut new = one_row();
new.insert("k".to_string(), Value::Integer(9));
let p = row_event_to_plain(&RowEvent::Update {
table: "t".into(),
old_row: one_row(),
new_row: new,
file_path: "/f".into(),
});
assert_eq!(p.action, "update");
assert_eq!(
p.row.as_ref().and_then(|r| r.get("k")),
Some(&Value::Integer(9))
);
assert_eq!(
p.old_row.as_ref().and_then(|r| r.get("k")),
Some(&Value::Integer(7))
);
}

#[test]
fn plain_delete_has_row_no_old() {
let p = row_event_to_plain(&RowEvent::Delete {
table: "t".into(),
row: one_row(),
file_path: "/f".into(),
});
assert_eq!(p.action, "delete");
assert!(p.row.is_some());
assert!(p.old_row.is_none());
}

#[test]
fn plain_error_has_no_row_and_optional_table() {
let p = row_event_to_plain(&RowEvent::Error {
table: None,
file_path: std::path::PathBuf::from("/f"),
error: "boom".into(),
});
assert_eq!(p.action, "error");
assert!(p.table.is_none());
assert!(p.row.is_none());
assert!(p.old_row.is_none());
assert_eq!(p.error.as_deref(), Some("boom"));
assert_eq!(p.file_path, "/f");
}

#[test]
fn on_file_error_displays_inner() {
assert_eq!(OnFileError("bad".to_string()).to_string(), "bad");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**Changed**

`RowEvent` flattening now lives once in the core as the `#[doc(hidden)]`
`dirsql::flatten_row_event`, instead of being reimplemented per binding. No
observable behavior change: field values, the four `action` strings, and both
error paths are identical.
4 changes: 4 additions & 0 deletions packages/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub mod persist;
#[doc(hidden)]
pub mod progress;
#[doc(hidden)]
pub mod row_event_flat;
#[doc(hidden)]
pub mod scanner;
#[doc(hidden)]
pub mod sql_literal;
Expand Down Expand Up @@ -78,6 +80,8 @@ pub use crate::config::ExtensionSpec as Extension;
pub use crate::db::{DbError, Value};
pub use crate::differ::RowEvent;
#[doc(hidden)]
pub use crate::row_event_flat::{FlatRowEvent, flatten_row_event};
#[doc(hidden)]
pub use crate::watcher::FileEvent as RawFileEvent;

pub type Row = HashMap<String, Value>;
Expand Down
Loading
Loading