Skip to content

refactor(core): flatten RowEvent once in the core - #1133

Merged
thekevinscott merged 5 commits into
mainfrom
claude/1131-flatten-row-event
Sep 18, 2026
Merged

thekevinscott merged 5 commits into
mainfrom
claude/1131-flatten-row-event

Conversation

@thekevinbot

Copy link
Copy Markdown
Collaborator

Fixes #1131. Part of #1072.

Both bindings carried the same arm-for-arm match over RowEvent's four
variants, producing the same {table, action, row, old_row, error, file_path}
record. That decision is language-neutral, so it moves into the core as
dirsql::flatten_row_event (#[doc(hidden)], packages/rust/src/row_event_flat.rs),
the shape extension_resolution established. Each binding keeps only its
marshaling half: value_row_to_py_dict over the borrowed rows plus the GIL
step for PyO3, value_row_to_js plus the unsafe-integer error for napi.

Rows are borrowed (Option<&'a Row>), so no clone is added in the core; the
clone stays at the binding boundary where it was already paid.

PlainRowEvent / row_event_to_plain are gone from packages/python/src/lib.rs,
and the four Python tests that only restated the flattening moved into the core
(joined by a fifth: Error with a named table, which neither binding covered).
The napi tests stay put — they assert on the napi RowEvent struct and cover
value marshaling and the unsafe-integer error path, which is the binding's own
half, not the flattening.

No public API changed in any SDK. No migration fragment.

One shape note

row_event_flat.rs imports Value at module level under #[cfg(test)] rather
than inside the test module: unit lint's no-out-of-module-import rule
forbids a unit test importing crate::db::Value directly, and an unconditional
module-level import would be unused in a non-test build. This mirrors how
differ.rs tests reach Value through use super::*.

Cross-SDK Parity

Parity-restoring, not drift: no SDK's observable surface changes, and the
flattening that was implemented twice is now implemented once for both.
PARITY.md gains a "Where the row-event flattening lives" note under Core
Types, matching the extension_resolution note. No follow-up issues needed.

Manual verification

Built both bindings and watched a real directory through each SDK, observing
all four variants.

Python (uv run maturin develop --uv, then a script driving db.watch() over
a temp dir):

insert: table='items' row={'id': 1, 'name': 'one'} old_row=None error=None file_path='a.json'
update: table='items' row={'id': 1, 'name': 'two'} old_row={'id': 1, 'name': 'one'} error=None file_path='a.json'
delete: table='items' row={'id': 1, 'name': 'two'} old_row=None error=None file_path='a.json'
error: table='items' row=None old_row=None error='JSONDecodeError: Expecting value: line 1 column 1 (char 0)' file_path='b.json'

Node (pnpm build, then a script driving for await (const e of db.watch())):

insert: table="items" row={"name":"one","id":1} oldRow=undefined error=undefined filePath="a.json"
update: table="items" row={"name":"two","id":1} oldRow={"id":1,"name":"one"} error=undefined filePath="a.json"
delete: table="items" row={"id":1,"name":"two"} oldRow=undefined error=undefined filePath="a.json"
error: table="items" row=undefined oldRow=undefined error="GenericFailure, Unexpected token 'o', \"not json\" is not valid JSON" filePath="b.json"

Same field values, same action strings, same error path as before the move.

just preflight (with nightly on PATH for the Rust coverage gate): 0 failing
pairs, 3 skipped (packaging). The Rust coverage floors did not move — the
change adds fully-covered core lines and removes none, so no re-pin of
[rust.coverage].

E2E Verification

  • Ran e2e suites locally for every affected SDK
  • Python SDK e2e: pass (37 passed)
  • TypeScript SDK e2e: pass (17 passed, 5 files)
  • Rust core e2e (if applicable): pass (cargo test --workspace)
  • packages/python/e2e-attestations/<branch>.json receipt written if packages/python changed (just e2e-attest-python)
  • packages/ts/e2e-attestations/<branch>.json receipt written if packages/ts changed (just e2e-attest-ts)
  • Command(s) run: just e2e-attest-python, just e2e-attest-ts, cargo test --workspace, just preflight
  • Result summary: all green; both receipts committed.

Changelog / Migrations

  • Changelog fragment added under <root>/<pkg>/changelog.d/ for each changed package (packages/rust, packages/python, packages/ts)
  • Migration fragment added under <root>/<pkg>/migrations.d/ (or: not required -- additive/bugfix only) — not required: no public API change, no behavior change

Both bindings carried an arm-for-arm match over the four RowEvent variants
producing the same flat record. Move that decision into a pure, unit-tested
dirsql::flatten_row_event and leave each binding its marshaling half.

Fixes #1131
@thekevinscott
thekevinscott merged commit 92e70ff into main Sep 18, 2026
77 checks passed
@thekevinscott
thekevinscott deleted the claude/1131-flatten-row-event branch September 18, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: flatten RowEvent in the core instead of twice in the bindings

2 participants