Skip to content

Replace pure-Python implementation with Rust (libpgdump) via PyO3 - #18

Merged
gmr merged 5 commits into
mainfrom
rust-backend
Mar 31, 2026
Merged

gmr merged 5 commits into
mainfrom
rust-backend

Conversation

@gmr

@gmr gmr commented Mar 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Replace the pure-Python binary format parser with PyO3 bindings to the libpgdump Rust crate (v2.0.0)
  • Gain multi-format support (Custom -Fc, Directory -Fd, Tar -Ft), additional compression (LZ4, Zstd), and improved performance
  • Switch build system from hatchling to maturin for mixed Rust/Python packaging
  • Bump version to 5.0.0

Changes

New files:

  • Cargo.toml — PyO3 crate depending on libpgdump = "2"
  • src/lib.rs — PyO3 module registration
  • src/dump.rs — PyDump class wrapping libpgdump::Dump
  • src/entry.rs — PyEntry class exposing entry fields
  • src/errors.rs — Error mapping from Rust to Python exceptions

Rewritten:

  • pgdumplib/dump.py — Thin wrapper over the native module (from ~1200 lines of binary I/O to ~500 lines of delegation)

Updated:

  • pyproject.toml — maturin build backend, removed toposort dependency
  • pgdumplib/constants.py — Removed dead binary-parser constants, updated SUPPORTED_COMPRESSION_ALGORITHMS
  • bootstrap — Uses maturin develop instead of pip install -e
  • .github/workflows/testing.yaml — Adds Rust toolchain step
  • .github/workflows/deploy.yaml — Uses maturin-action for cross-platform wheel builds
  • tests/test_edge_cases.py — Removed 7 tests that tested old pure-Python binary parser internals

New capabilities

  • dump.set_format('Directory') / dump.set_format('Tar') — write in directory or tar format
  • dump.set_compression('lz4') / dump.set_compression('zstd') — LZ4 and Zstd compression
  • All existing public API preserved (load, new, Dump, Entry, converters, constants, exceptions)

Test plan

  • All 116 existing tests pass locally with PostgreSQL running
  • CI matrix: Python 3.11-3.14, PostgreSQL 15-18
  • Verify maturin develop builds cleanly
  • Verify pg_restore can read dumps created by the new implementation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Native Rust-backed extension for faster dump handling; added LZ4 and ZSTD compression support
  • Improvements

    • Package bumped to v5.0.0 and build backend switched to maturin; CI now builds cross-OS wheels and an sdist and publishes releases
    • Tooling/bootstrap updated with Rust toolchain support, updated virtualenv/bootstrap flow, and adjusted coverage/reporting paths; expanded supported Python and PostgreSQL targets
  • Documentation

    • Clarified public API entry points and documented new public exceptions
  • Tests

    • Removed legacy edge-case tests and updated remaining tests to match the new implementation

Replace the pure-Python binary format parser with PyO3 bindings to
the libpgdump Rust crate, gaining multi-format support (Custom, Directory,
Tar), additional compression algorithms (LZ4, Zstd), and improved
performance.

- Add Cargo.toml and src/ with PyO3 bindings (PyDump, PyEntry, error mapping)
- Switch build backend from hatchling to maturin
- Rewrite pgdumplib/dump.py as thin wrapper over the native module
- Remove toposort dependency (entry ordering handled by libpgdump)
- Remove dead constants (BLK_DATA, BLK_BLOBS, FORMAT_*, ZLIB_*, VERSION)
- Update SUPPORTED_COMPRESSION_ALGORITHMS to include LZ4 and Zstd
- Add set_format() and set_compression() methods to Dump
- Update CI to install Rust toolchain and build with maturin
- Update deploy workflow to build platform-specific wheels via maturin-action
- Remove edge case tests that tested old pure-Python binary parser internals
- Bump version to 5.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 31, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85bffd37-9027-470a-a537-0ca8e6ad14c7

📥 Commits

Reviewing files that changed from the base of the PR and between 202cc4f and a7168e2.

📒 Files selected for processing (2)
  • CLAUDE.md
  • tests/test_dump.py
✅ Files skipped from review due to trivial changes (1)
  • tests/test_dump.py

📝 Walkthrough

Walkthrough

Replaces Python-only dump parsing with a Rust PyO3 extension module (pgdumplib._pgdumplib), refactors the Python wrapper to delegate to that backend, adds Rust sources/Cargo manifest, switches packaging to maturin and version 5.0.0, and updates CI to build/upload cross-OS wheels and an sdist.

Changes

Cohort / File(s) Summary
Build system & packaging
pyproject.toml, Cargo.toml, bootstrap
Switched build backend to maturin, bumped project to 5.0.0, removed toposort, added [tool.maturin] config, added Cargo manifest for _pgdumplib, and updated bootstrap to install/use maturin/maturin develop.
CI / Release workflows
.github/workflows/deploy.yaml, .github/workflows/testing.yaml
Added cross-OS build-wheels matrix and build-sdist; deploy now downloads wheel artifacts and publishes to PyPI; testing workflow installs Rust toolchain and runs venv-bound tools; coverage path updated.
Rust extension crate
src/lib.rs, src/dump.rs, src/entry.rs, src/errors.rs
New PyO3 crate _pgdumplib exposing Dump and Entry, metadata/table/blob APIs, mutation methods, and libpgdump→Python exception mapping; module registration added.
Python wrapper & runtime
pgdumplib/dump.py, pgdumplib/__init__.py
Dump now delegates parsing/serialization to _pgdumplib.Dump; TableData moved to in-memory BytesIO with get_data(); added backend-backed properties and adjusted load/save/table_data_writer flows.
Constants & ignores
pgdumplib/constants.py, .gitignore
Removed legacy block/format/version constants and zlib defaults; expanded SUPPORTED_COMPRESSION_ALGORITHMS to include LZ4 and ZSTD. .gitignore added *.so, *.pyd, target/, *.dSYM/.
Tests & docs
CLAUDE.md, tests/test_edge_cases.py, tests/test_dump.py
Docs updated to document new public API and exceptions and broaden supported versions; edge-case tests removed/adapted; some test assertions and expected exception types updated.
Versioned artifacts / manifest
pyproject.toml, Cargo.toml
Project version bumped to 5.0.0; new Rust cdylib crate configuration (_pgdumplib) and PyO3 features declared.

Sequence Diagram

sequenceDiagram
    autonumber
    participant Client as Python client
    participant Wrapper as pgdumplib.Dump (Python)
    participant PyO3 as pgdumplib._pgdumplib (PyO3)
    participant Core as libpgdump (Rust core)

    Client->>Wrapper: Dump.load(filepath)
    Wrapper->>PyO3: Dump.load(path)
    PyO3->>Core: parse/load archive
    Core-->>PyO3: entries, metadata, data
    PyO3-->>Wrapper: Dump handle (methods/metadata)

    Client->>Wrapper: table_data_writer(...) / write bytes
    Wrapper->>PyO3: add_entry(...) / set_entry_data(...)
    PyO3->>Core: mutate entry data
    Core-->>PyO3: ack
    PyO3-->>Wrapper: ok

    Client->>Wrapper: save(path)
    Wrapper->>PyO3: save(path)
    PyO3->>Core: serialize to archive
    Core-->>PyO3: ok
    PyO3-->>Wrapper: ok
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐇 I hopped from Python’s knitted den,

Brought Rusty roots to bind the glen,
Wheels across each distant shore,
CI hums and artifacts soar—
A crunchy carrot for the core!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing the pure-Python dump parser with Rust bindings via PyO3/libpgdump, which is the central focus of this significant architectural refactor across the entire codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rust-backend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The bootstrap script was skipping virtualenv creation when CI=true,
but maturin develop requires a virtualenv. Remove the CI guard so the
venv is always created. Also remove the redundant "Build native
extension" step from the workflow since bootstrap already runs
maturin develop, fix coverage.xml path to match pyproject.toml config,
and use .venv/bin/ prefixed commands for lint and test steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CLAUDE.md (1)

13-14: ⚠️ Potential issue | 🟡 Minor

Documentation mentions toposort but dependency was removed.

The PR summary indicates that the toposort dependency was removed, but Line 13 still states "Manage dump entries with proper dependency resolution using topological sorting." This should be updated to reflect that dependency resolution is now handled by the Rust libpgdump layer.

📝 Suggested fix
-- Manage dump entries with proper dependency resolution using topological sorting
+- Manage dump entries with proper dependency resolution
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 13 - 14, Update the CLAUDE.md sentence that currently
reads "Manage dump entries with proper dependency resolution using topological
sorting" to remove the reference to the removed toposort dependency and state
that dependency resolution is now performed by the Rust libpgdump layer; locate
the exact sentence in CLAUDE.md (the string "Manage dump entries with proper
dependency resolution using topological sorting") and replace it with a concise
note such as "Manage dump entries with dependency resolution performed by the
Rust libpgdump layer" or similar wording that makes it clear the toposort JS
dependency was removed.
🧹 Nitpick comments (6)
tests/test_edge_cases.py (1)

26-28: Test relies on internal API and hardcoded dump_id.

The test accesses _dump (internal Rust backend) and assumes dump_id 1 is the encoding entry. This coupling to internal implementation details makes the test fragile if the Rust backend changes entry ordering or IDs.

Consider documenting why dump_id 1 is expected to be the encoding entry, or using a lookup method if one exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_edge_cases.py` around lines 26 - 28, The test directly uses the
internal Rust backend via dmp._dump.update_entry(1, ...) and assumes dump_id 1
is the encoding entry, which is fragile; change the test to avoid internal APIs
by finding the encoding entry dynamically (e.g., call the public lookup/search
method on dmp to locate the entry by its type/name or metadata and use its id)
or, if no public lookup exists, add/use a public helper on the dmp object to
resolve the encoding entry id instead of hardcoding 1; alternatively document in
the test why id 1 is guaranteed (but preferred: replace dmp._dump.update_entry
with a public API call that finds the correct entry id and then updates it).
.github/workflows/testing.yaml (1)

85-88: Redundant maturin installation and build.

The bootstrap script (line 76) already runs pip install maturin and maturin develop --extras dev. This step duplicates that work, installing maturin again and rebuilding the extension.

Consider removing this step since bootstrap handles it, or removing the maturin commands from bootstrap if you prefer explicit CI steps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/testing.yaml around lines 85 - 88, The "Build native
extension" CI step duplicates work already performed by the earlier "bootstrap"
step (which installs maturin and runs maturin develop --extras dev); remove the
redundant step by deleting the job block named "Build native extension" that
runs ".venv/bin/pip install maturin" and ".venv/bin/maturin develop" from the
workflow, or alternatively remove the maturin install/build calls from the
"bootstrap" step so only one place performs the native build—ensure you keep a
single canonical place (either "bootstrap" or the explicit build step) to avoid
double installation and rebuilds.
src/dump.rs (3)

36-38: Consider handling poisoned mutex instead of unwrap.

Throughout this file, .lock().unwrap() is used which will panic if the mutex is poisoned (i.e., a thread panicked while holding the lock). While this is acceptable for most use cases, production code might benefit from more graceful error handling.

♻️ Example alternative
 fn save(&self, path: &str) -> PyResult<()> {
-    self.inner.lock().unwrap().save(path).map_err(to_pyerr)
+    self.inner
+        .lock()
+        .map_err(|_| PyRuntimeError::new_err("Lock poisoned"))?
+        .save(path)
+        .map_err(to_pyerr)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dump.rs` around lines 36 - 38, The code currently calls .lock().unwrap()
(e.g., in save on self.inner) which will panic on a poisoned mutex; change these
to handle PoisonError by replacing .lock().unwrap() with a safe map_err branch
that converts the PoisonError into a PyResult error instead of panicking — for
example, use .lock().map_err(|pe| /* convert pe to PyErr via to_pyerr or create
a PyRuntimeError with context */ )? and then continue calling
.save(path).map_err(to_pyerr); update every other occurrence of .lock().unwrap()
in this file (references: save, self.inner, any other methods using
inner.lock()) so mutex poisoning is returned as a PyErr rather than causing a
panic.

144-163: Memory consideration: blobs loads all blob data into memory.

Similar to table_data, the blobs method loads all blob OIDs and data into memory as a Vec<Bound<'py, PyTuple>>. For dumps with many large blobs, this could be memory-intensive. Consider documenting this behavior or providing a streaming alternative in the future.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dump.rs` around lines 144 - 163, The blobs method currently collects all
blob OIDs and data into memory (function blobs, returning Vec<Bound<'py,
PyTuple>>), so add a clear doc comment on blobs explaining it loads all blob
data into memory and may be memory-intensive for large dumps, and (optionally)
add a streaming alternative named blobs_iter (or blobs_stream) that yields items
lazily (e.g., return a PyIterator/Python generator or implement an iterator that
yields (oid, PyBytes) tuples instead of collecting into a Vec) so callers can
consume blobs one-by-one without an in-memory Vec; update public API docs
accordingly.

133-142: Memory consideration: table_data loads all rows into memory.

The table_data method collects all rows into a Vec<String> before returning. For large tables, this could consume significant memory. The Python side iterates over this, so a streaming iterator would be more memory-efficient. However, PyO3's iterator support can be complex, so this tradeoff may be acceptable for the initial implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dump.rs` around lines 133 - 142, The current table_data method (in
dump.rs, function table_data) eagerly collects all rows into a Vec<String> which
can OOM for large tables; change the Rust->Python boundary to return a Python
iterator instead of Vec<String>: change the Rust signature to return
PyResult<PyObject> (or another PyIterator-compatible type), and wrap the
underlying dump.table_data(namespace, table) iterator (after mapping errors with
to_pyerr) in a PyO3-compatible iterator/generator so rows are yielded lazily to
Python rather than collected in memory; keep using dump.table_data and to_pyerr
but do not call collect() — yield each String through the PyO3 iterator
implementation.
pgdumplib/dump.py (1)

146-153: Acknowledged: format property shadows Python builtin.

Ruff flags format as shadowing the Python builtin. While technically true, this is a common pattern for properties representing format types. Renaming to dump_format would be cleaner but would break the public API.

♻️ Alternative naming (API-breaking)
     `@property`
-    def format(self) -> str:
+    def dump_format(self) -> str:
         """The dump format (Custom, Directory, Tar)"""
         return self._format

-    `@format.setter`
-    def format(self, value: str) -> None:
+    `@dump_format.setter`
+    def dump_format(self, value: str) -> None:
         self._format = value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pgdumplib/dump.py` around lines 146 - 153, The property name format shadows a
Python builtin; to avoid breaking the public API, add a new canonical property
dump_format that gets/sets self._format, then make the existing format
getter/setter thin wrappers that call dump_format and emit a DeprecationWarning
(use warnings.warn) so callers keep working while signaling migration; update
references to use dump_format internally and keep the same _format backing
field; ensure you modify both the format getter and format.setter to delegate to
dump_format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Line 38: The document still mentions "toposort" for dependency resolution
under the "Dependency Resolution" section; remove that reference and update the
wording to state that entry ordering is now handled by the Rust layer (instead
of using toposort). Specifically, edit the "Dependency Resolution" paragraph
that mentions `dump_id` and `toposort` to remove the `toposort` claim and
replace it with a short note that the Rust layer enforces correct ordering of
entries/dumps.

In `@pgdumplib/dump.py`:
- Around line 380-405: The code creates a new TABLE DATA entry (via
self._dump.add_entry) before checking for an existing entry, which can leave
orphan entries when table_data_writer is called multiple times; change the logic
in table_data_writer to call _find_existing_table_data(entry.namespace,
entry.tag, ...) before adding a new entry and, if an existing entry is found,
reuse that existing dump_id (and its copy_stmt) for writing instead of creating
a new entry; only call self._dump.add_entry when no existing entry exists, and
ensure you call self._dump.set_entry_data on the chosen dump_id (existing or
newly created) after collecting data from TableData to avoid leaving empty
duplicate TABLE DATA entries.
- Around line 282-301: The code currently preserves the original compression in
_loaded_compression but then forces the in-memory Dump to uncompressed via
_dump.set_compression(constants.COMPRESSION_NONE), so calling save() writes an
uncompressed file while compression_algorithm still reports the original; update
this by either documenting the behavior in the Dump.load()/class docstring to
state that loads normalize to no compression but report original via
compression_algorithm, or change the load path to preserve compression by
removing the _dump.set_compression(constants.COMPRESSION_NONE) call (or store
and reapply the original compression in save()), and ensure related APIs
(_loaded_compression, compression_algorithm, save()) remain consistent with the
chosen approach.

In `@src/errors.rs`:
- Around line 8-21: The map currently uses .unwrap() on a.into_pyobject(py)
which can panic; change the mapping to propagate conversion errors instead: call
a.into_pyobject(py)? (or map to a PyResult and collect::<PyResult<Vec<_>>>()?)
so the iterator returns a PyResult<Vec<_>> and then ?-propagate the error before
creating the PyTuple; update the surrounding function (to_pyerr / the closure
passed to Python::try_attach) to return PyResult so you can use ? for error
propagation rather than unwrapping. Ensure you still convert each successful
PyObject to .into_any() after the ?-checked conversion.

---

Outside diff comments:
In `@CLAUDE.md`:
- Around line 13-14: Update the CLAUDE.md sentence that currently reads "Manage
dump entries with proper dependency resolution using topological sorting" to
remove the reference to the removed toposort dependency and state that
dependency resolution is now performed by the Rust libpgdump layer; locate the
exact sentence in CLAUDE.md (the string "Manage dump entries with proper
dependency resolution using topological sorting") and replace it with a concise
note such as "Manage dump entries with dependency resolution performed by the
Rust libpgdump layer" or similar wording that makes it clear the toposort JS
dependency was removed.

---

Nitpick comments:
In @.github/workflows/testing.yaml:
- Around line 85-88: The "Build native extension" CI step duplicates work
already performed by the earlier "bootstrap" step (which installs maturin and
runs maturin develop --extras dev); remove the redundant step by deleting the
job block named "Build native extension" that runs ".venv/bin/pip install
maturin" and ".venv/bin/maturin develop" from the workflow, or alternatively
remove the maturin install/build calls from the "bootstrap" step so only one
place performs the native build—ensure you keep a single canonical place (either
"bootstrap" or the explicit build step) to avoid double installation and
rebuilds.

In `@pgdumplib/dump.py`:
- Around line 146-153: The property name format shadows a Python builtin; to
avoid breaking the public API, add a new canonical property dump_format that
gets/sets self._format, then make the existing format getter/setter thin
wrappers that call dump_format and emit a DeprecationWarning (use warnings.warn)
so callers keep working while signaling migration; update references to use
dump_format internally and keep the same _format backing field; ensure you
modify both the format getter and format.setter to delegate to dump_format.

In `@src/dump.rs`:
- Around line 36-38: The code currently calls .lock().unwrap() (e.g., in save on
self.inner) which will panic on a poisoned mutex; change these to handle
PoisonError by replacing .lock().unwrap() with a safe map_err branch that
converts the PoisonError into a PyResult error instead of panicking — for
example, use .lock().map_err(|pe| /* convert pe to PyErr via to_pyerr or create
a PyRuntimeError with context */ )? and then continue calling
.save(path).map_err(to_pyerr); update every other occurrence of .lock().unwrap()
in this file (references: save, self.inner, any other methods using
inner.lock()) so mutex poisoning is returned as a PyErr rather than causing a
panic.
- Around line 144-163: The blobs method currently collects all blob OIDs and
data into memory (function blobs, returning Vec<Bound<'py, PyTuple>>), so add a
clear doc comment on blobs explaining it loads all blob data into memory and may
be memory-intensive for large dumps, and (optionally) add a streaming
alternative named blobs_iter (or blobs_stream) that yields items lazily (e.g.,
return a PyIterator/Python generator or implement an iterator that yields (oid,
PyBytes) tuples instead of collecting into a Vec) so callers can consume blobs
one-by-one without an in-memory Vec; update public API docs accordingly.
- Around line 133-142: The current table_data method (in dump.rs, function
table_data) eagerly collects all rows into a Vec<String> which can OOM for large
tables; change the Rust->Python boundary to return a Python iterator instead of
Vec<String>: change the Rust signature to return PyResult<PyObject> (or another
PyIterator-compatible type), and wrap the underlying dump.table_data(namespace,
table) iterator (after mapping errors with to_pyerr) in a PyO3-compatible
iterator/generator so rows are yielded lazily to Python rather than collected in
memory; keep using dump.table_data and to_pyerr but do not call collect() —
yield each String through the PyO3 iterator implementation.

In `@tests/test_edge_cases.py`:
- Around line 26-28: The test directly uses the internal Rust backend via
dmp._dump.update_entry(1, ...) and assumes dump_id 1 is the encoding entry,
which is fragile; change the test to avoid internal APIs by finding the encoding
entry dynamically (e.g., call the public lookup/search method on dmp to locate
the entry by its type/name or metadata and use its id) or, if no public lookup
exists, add/use a public helper on the dmp object to resolve the encoding entry
id instead of hardcoding 1; alternatively document in the test why id 1 is
guaranteed (but preferred: replace dmp._dump.update_entry with a public API call
that finds the correct entry id and then updates it).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7266902-77b4-4380-90b4-5e59ec31c2f1

📥 Commits

Reviewing files that changed from the base of the PR and between 66b6084 and 0347d24.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • .github/workflows/deploy.yaml
  • .github/workflows/testing.yaml
  • .gitignore
  • CLAUDE.md
  • Cargo.toml
  • bootstrap
  • pgdumplib/__init__.py
  • pgdumplib/constants.py
  • pgdumplib/dump.py
  • pyproject.toml
  • src/dump.rs
  • src/entry.rs
  • src/errors.rs
  • src/lib.rs
  • tests/test_edge_cases.py

Comment thread CLAUDE.md Outdated
Comment thread pgdumplib/dump.py
Comment thread pgdumplib/dump.py Outdated
Comment thread src/errors.rs
- Update CLAUDE.md to replace stale toposort reference with
  Rust layer for dependency resolution
- Document that loaded dumps save without compression by
  default in the load() docstring
- Fix table_data_writer to check for existing TABLE DATA
  entries before creating new ones, preventing orphan entries
  on repeated calls for the same table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gmr

gmr commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

PR Monitor Summary

Response to CodeRabbit's Review

  • CLAUDE.md:38 - Updated stale toposort reference to reflect that the Rust layer now handles dependency resolution (resolved)
  • dump.py:301 - Added docstring documentation to load() clarifying that loaded dumps save without compression by default, and that set_compression() can be called after loading to change this (resolved)
  • dump.py:405 - Fixed table_data_writer to check for existing TABLE DATA entries before creating a new one, preventing orphan entries when the method is called multiple times for the same table (resolved)
  • errors.rs:21 - Respectfully disagreed: the .unwrap() on into_pyobject() for &str is safe because the error type is Infallible (the Rust never-type), meaning conversion cannot fail. Adding ? propagation would require unnecessary complexity for a code path that is guaranteed to succeed by the type system (resolved with reply)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
pgdumplib/dump.py (1)

47-69: Watch the new memory profile of TableData.

This now keeps the entire table payload in io.BytesIO until the context exits, and set_entry_data() then copies those bytes again into Rust. If large table exports are a supported path, this is a meaningful regression from the old temp-file-backed writer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pgdumplib/dump.py` around lines 47 - 69, The current TableData implementation
(seen in __init__, append, get_data) keeps the entire table in-memory via
io.BytesIO, causing a memory/regression when large exports are used and
set_entry_data copies those bytes into Rust; replace the in-memory buffer with a
disk-backed or spooled temporary file (e.g., tempfile.SpooledTemporaryFile or
tempfile.TemporaryFile) in __init__, change append to write encoded bytes into
that file, and update get_data (or provide an alternative like get_fileobj) to
either seek/read only when absolutely needed or return the file-like object so
set_entry_data can stream from disk without a full extra copy. Ensure you
close/unlink the temp file when the context exits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Line 21: The README line describing pgdumplib/dump.py is outdated: update the
sentence about TableData to remove the claim it uses temporary gzip-compressed
files and instead state that TableData buffers rows in an in-memory io.BytesIO
buffer and materializes them when get_data() is called; mention Dump and
TableData by name so readers can locate the implementation and adjust any
wording about lifecycle or memory behavior accordingly.

In `@pgdumplib/dump.py`:
- Around line 291-296: The preflight always calls _check_file_format on any
regular file (path.is_file()), which only recognizes constants.MAGIC ("PGDMP")
and therefore prevents tar archives (which are regular files) from reaching
_pgdumplib.Dump.load; change the guard so you only run _check_file_format when
the file actually starts with constants.MAGIC (e.g., open path, read the first
len(constants.MAGIC) bytes and compare to constants.MAGIC), otherwise skip the
format check and call _pgdumplib.Dump.load(str(path)) so tar support can be
handled by Dump.load.
- Around line 151-154: The property setter Dump.format currently only updates
the Python-side cache (self._format) and must also update the native/backend
dump so save() uses the new format; modify Dump.format setter to set
self._format = value and also propagate the change to the underlying native
object (self._dump) by calling the backend's API (e.g.,
self._dump.set_format(value) or assigning to self._dump.format if that is the
native attribute), ensuring the setter validates/normalizes the value the same
way before updating both places.
- Around line 225-248: The code validates dump_id but never passes it to the
underlying _dump.add_entry (and the bound Rust method _pgdumplib.Dump.add_entry
in src/dump.rs currently lacks a dump_id parameter), so callers thinking they
set a stable ID are silently ignored; modify the Python API (in the function
that checks dump_id and calls self._dump.add_entry) to raise a clear
NotImplementedError (or ValueError with a message like "explicit dump_id not
supported yet") whenever dump_id is not None to fail fast, and add a TODO
comment instructing that when src/dump.rs/_pgdumplib.Dump.add_entry is extended
to accept dump_id the call site should pass dump_id through to _dump.add_entry
and remove the failure.
- Around line 388-416: When reusing an existing TABLE DATA entry from
_find_existing_table_data, validate that the persisted COPY statement matches
the newly constructed copy_stmt (including column names and order) before
reusing dump_id; if they differ, do not reuse existing and instead create a new
entry via self._dump.add_entry (constants.TABLE_DATA) or raise an explicit
error. Locate the logic around copy_stmt, existing =
self._find_existing_table_data(...), and the subsequent branch that sets
dump_id; use self._dump.entry_data(existing) or the entry metadata to retrieve
the stored copy_stmt for comparison and only set dump_id = existing when they
are identical. Ensure TableData(writer) and subsequent set_entry_data still
operate on the correct dump_id when a new entry is created.

---

Nitpick comments:
In `@pgdumplib/dump.py`:
- Around line 47-69: The current TableData implementation (seen in __init__,
append, get_data) keeps the entire table in-memory via io.BytesIO, causing a
memory/regression when large exports are used and set_entry_data copies those
bytes into Rust; replace the in-memory buffer with a disk-backed or spooled
temporary file (e.g., tempfile.SpooledTemporaryFile or tempfile.TemporaryFile)
in __init__, change append to write encoded bytes into that file, and update
get_data (or provide an alternative like get_fileobj) to either seek/read only
when absolutely needed or return the file-like object so set_entry_data can
stream from disk without a full extra copy. Ensure you close/unlink the temp
file when the context exits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb8c86c0-9535-419a-8c57-9aa5401d5053

📥 Commits

Reviewing files that changed from the base of the PR and between ed0505c and 5459a24.

📒 Files selected for processing (2)
  • CLAUDE.md
  • pgdumplib/dump.py

Comment thread CLAUDE.md Outdated
Comment thread pgdumplib/dump.py
Comment thread pgdumplib/dump.py
Comment thread pgdumplib/dump.py
Comment thread pgdumplib/dump.py
- Sync format property setter with Rust backend via set_format()
- Fail fast with NotImplementedError for custom dump_id (unsupported in Rust)
- Allow tar archives through _check_file_format to Rust backend
- Validate column layout matches when reusing TABLE DATA entries
- Update CLAUDE.md to reflect in-memory TableData buffering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
CLAUDE.md (1)

13-13: Inconsistent reference to dependency resolution method.

Line 13 still mentions "topological sorting" but line 38 correctly states the Rust layer now handles entry ordering. This creates inconsistency in the documentation.

📝 Suggested fix
-- Handle BLOB data
-- Manage dump entries with proper dependency resolution using topological sorting
+- Handle BLOB data
+- Manage dump entries with proper dependency resolution
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 13, Update the inconsistent phrasing: replace the sentence
"Manage dump entries with proper dependency resolution using topological
sorting" with wording that reflects the Rust layer handling entry ordering
(e.g., "Manage dump entries; entry ordering and dependency resolution are
handled by the Rust layer") so the doc matches the later line stating the Rust
layer now handles entry ordering and removes mention of topological sorting.
tests/test_dump.py (1)

226-237: Test assertion creates coupling to libpgdump's error message wording.

The test asserts self.assertIn('invalid magic bytes', ...), which couples to the exact error message from the libpgdump Rust crate. Per src/errors.rs, InvalidHeader errors pass the message directly through to PyValueError without modification, meaning any future change to libpgdump's error wording will break this test even though the actual behavior (rejecting invalid files with a ValueError) remains correct.

Consider either:

  1. Accepting this coupling (if testing the full error flow including exact wording is intentional)
  2. Using a less specific assertion that checks only for ValueError type
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_dump.py` around lines 226 - 237, The test
test_invalid_binary_format couples to libpgdump's exact error wording by
asserting 'invalid magic bytes' in the exception string; instead drop the
brittle substring check and rely only on the exception type: keep the with
self.assertRaises(ValueError) around pgdumplib.load(temp_name) and remove the
self.assertIn(...) line (or replace it with a generic check like
assertIsInstance(context.exception, ValueError) if you prefer), so the test
verifies the ValueError behavior without depending on libpgdump's exact message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@CLAUDE.md`:
- Line 13: Update the inconsistent phrasing: replace the sentence "Manage dump
entries with proper dependency resolution using topological sorting" with
wording that reflects the Rust layer handling entry ordering (e.g., "Manage dump
entries; entry ordering and dependency resolution are handled by the Rust
layer") so the doc matches the later line stating the Rust layer now handles
entry ordering and removes mention of topological sorting.

In `@tests/test_dump.py`:
- Around line 226-237: The test test_invalid_binary_format couples to
libpgdump's exact error wording by asserting 'invalid magic bytes' in the
exception string; instead drop the brittle substring check and rely only on the
exception type: keep the with self.assertRaises(ValueError) around
pgdumplib.load(temp_name) and remove the self.assertIn(...) line (or replace it
with a generic check like assertIsInstance(context.exception, ValueError) if you
prefer), so the test verifies the ValueError behavior without depending on
libpgdump's exact message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20e88bd3-7d77-4bc6-8dec-0e31e7639569

📥 Commits

Reviewing files that changed from the base of the PR and between 5459a24 and 202cc4f.

📒 Files selected for processing (4)
  • CLAUDE.md
  • pgdumplib/dump.py
  • tests/test_dump.py
  • tests/test_edge_cases.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_edge_cases.py

- Fix inconsistent phrasing in CLAUDE.md line 13 to match line 38:
  replace topological sorting mention with Rust layer reference
- Remove brittle assertIn on libpgdump's exact error wording in
  test_invalid_binary_format; rely only on ValueError exception type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gmr
gmr merged commit 6c03032 into main Mar 31, 2026
33 checks passed
@gmr
gmr deleted the rust-backend branch March 31, 2026 16:44
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.

1 participant