Skip to content

feat(filesystem): add file-level incremental loading#239

Merged
amotl merged 1 commit into
panodata:mainfrom
hampsterx:feat/filesystem-file-incremental
Jul 20, 2026
Merged

feat(filesystem): add file-level incremental loading#239
amotl merged 1 commit into
panodata:mainfrom
hampsterx:feat/filesystem-file-incremental

Conversation

@hampsterx

@hampsterx hampsterx commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add opt-in filesystem incremental selection using file modification times while preserving the existing default behavior.
  • Isolate cursor state using stable, secret-free source and glob identities, and restrict the feature to append loads.
  • Require destination state sync with temporary pipeline directories, or an explicit persistent pipeline directory.
  • Document boundary, backfill, and full-refresh behavior, with local and remote transport coverage.

Review

  • Internal review: 4 findings fixed, 1 optional suggestion noted.
  • External review: Claude and GLM-5.2 found no defects; 4 optional suggestions noted. Codex was skipped because it is the authoring harness.
  • CI review: replaced one secret-like test fixture flagged by gitleaks.
  • Post-merge review fixes are tracked in fix(filesystem): always remove temporary pipeline state #243 because this PR merged before they were pushed.

Changes

17 files changed, with 926 insertions and 30 deletions across the CLI/API, filesystem source adapters, documentation, and tests.

Test plan

  • Full non-integration suite: 662 passed, 44 skipped
  • Remote filesystem incremental matrix against DuckDB: 5 passed
  • Ruff formatting and lint, ty, and project configuration validation
  • Strict Sphinx documentation build
  • Git whitespace validation
  • Targeted gitleaks scan of the corrected fixture

Closes #209

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 765abe1d-d84f-441c-b375-e31a92273e76

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds opt-in filesystem incremental loading based on modification-time cursors, with stable source identities, CLI/API validation, state-sync handling, resource wiring, documentation, and local and remote integration coverage.

Changes

Filesystem incremental loading

Layer / File(s) Summary
Incremental identity and source contracts
src/dlt_filesystem/source/model.py, src/dlt_filesystem/source/base.py, src/dlt_filesystem/source/impl/*, src/omniload/core/model.py
Adds filesystem incremental capability declarations, storage namespaces, and deterministic secret-free resource names.
Incremental resource construction
src/dlt_filesystem/source/adapter.py, tests/dlt_filesystem/test_source_incremental.py
Applies modification-date incremental hints before reader transformations and verifies lister behavior, identity isolation, and authentication-independent naming.
CLI and ingest validation
src/omniload/main.py, src/omniload/model.py, src/omniload/api.py
Adds the CLI/request option, validates source, destination, strategy, and state requirements, forwards the option, and cleans temporary pipeline directories.
Behavior validation and documentation
docs/..., tests/main/filesystem/*, tests/main/test_app.py, tests/util/__init__.py, tests/warehouse/filesystem/test_remote.py
Documents cursor selection and reset behavior and tests unchanged-file skipping, boundaries, backfills, persistence, validation, CLI help, and remote loading.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: coderabbit-review

Suggested reviewers: amotl

Poem

I hop through files beneath the moon,
Newer paths arrive in tune.
Old ones rest, their cursors kept,
Backfills wake when refreshes step.
Stable names guard secrets tight—
Incremental hops take flight! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: file-level incremental loading for filesystem sources.
Description check ✅ Passed The description is directly related to the changeset and accurately summarizes the added filesystem incremental mode.
Linked Issues check ✅ Passed The PR implements opt-in filesystem incremental loading for globbed files using modification times, matching issue #209.
Out of Scope Changes check ✅ Passed The changed docs, source code, CLI wiring, and tests all support the same filesystem incremental loading feature.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@read-the-docs-community

read-the-docs-community Bot commented Jul 20, 2026

Copy link
Copy Markdown

Documentation build overview

📚 omniload | 🛠️ Build #33659947 | 📁 Comparing e848936 against latest (8a42c05)

  🔍 Preview build  

3 files changed
± commands/ingest.html
± getting-started/incremental-loading.html
± supported-sources/filesystem.html

Add opt-in modification-time selection before filesystem reader I/O while preserving the existing default behavior.

Isolate cursor state by source and glob, restrict the mode to append loads, and require durable pipeline state.

Document reset and boundary semantics and cover local and remote transports.
@hampsterx
hampsterx force-pushed the feat/filesystem-file-incremental branch from dc49bf6 to e848936 Compare July 20, 2026 04:18
@hampsterx

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.59%. Comparing base (8a42c05) to head (e848936).

Files with missing lines Patch % Lines
src/dlt_filesystem/source/impl/remote.py 84.61% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #239      +/-   ##
==========================================
+ Coverage   55.37%   55.59%   +0.22%     
==========================================
  Files         210      210              
  Lines        9831     9878      +47     
==========================================
+ Hits         5444     5492      +48     
+ Misses       4387     4386       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/omniload/api.py (1)

279-284: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Temp pipeline dir is not cleaned up when pipeline.run() fails.

remove_temp_pipelines_dir() is called on the state-sync validation failure, the dry-run early return, and the success path, but not in the except BaseException handler around run_pipeline(). A failed run with a temp pipelines dir (the default when --pipelines-dir is omitted) leaks that directory on every failure, which is more consequential now that filesystem-incremental relies on this same temp-dir mechanics being correctly cleaned up.

♻️ Suggested fix: wrap cleanup in try/finally
     try:
         max_retries = 3
         for attempt in range(max_retries):
             ...
         report_errors(run_info)
         destination.post_load()
         getattr(source, "post_load", lambda: None)()
     except BaseException:
         release = getattr(source, "release", None)
         if callable(release):
             release()
         raise
+    finally:
+        remove_temp_pipelines_dir()
 
     end_time = datetime.now()
     elapsed = end_time - start_time
     elapsedHuman = f"in {humanize.precisedelta(elapsed)}"
-
-    remove_temp_pipelines_dir()

Also applies to: 637-670

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/omniload/api.py` around lines 279 - 284, Ensure the exception handler
surrounding run_pipeline() invokes remove_temp_pipelines_dir() before
propagating the failure, preferably via a try/finally structure that guarantees
cleanup for every run outcome. Preserve the existing cleanup behavior for
validation failures, dry-run returns, and successful execution, and use the
existing remove_temp_pipelines_dir function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/dlt_filesystem/test_source_incremental.py`:
- Around line 191-204: Add focused tests for _endpoint_namespace covering an
IPv6 host and an endpoint with no hostname, such as a path-only or malformed
URL. Assert the IPv6 result preserves bracketed host formatting and the
no-hostname case follows the helper’s existing fallback behavior, using the same
default namespace context as the current tests.

---

Nitpick comments:
In `@src/omniload/api.py`:
- Around line 279-284: Ensure the exception handler surrounding run_pipeline()
invokes remove_temp_pipelines_dir() before propagating the failure, preferably
via a try/finally structure that guarantees cleanup for every run outcome.
Preserve the existing cleanup behavior for validation failures, dry-run returns,
and successful execution, and use the existing remove_temp_pipelines_dir
function.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7aa0a68c-b8e6-49b5-80a2-2ba7051a4cd7

📥 Commits

Reviewing files that changed from the base of the PR and between 8a42c05 and e848936.

📒 Files selected for processing (17)
  • docs/commands/ingest.md
  • docs/getting-started/incremental-loading.md
  • docs/supported-sources/filesystem.md
  • src/dlt_filesystem/source/adapter.py
  • src/dlt_filesystem/source/base.py
  • src/dlt_filesystem/source/impl/local.py
  • src/dlt_filesystem/source/impl/remote.py
  • src/dlt_filesystem/source/model.py
  • src/omniload/api.py
  • src/omniload/core/model.py
  • src/omniload/main.py
  • src/omniload/model.py
  • tests/dlt_filesystem/test_source_incremental.py
  • tests/main/filesystem/test_file_incremental.py
  • tests/main/test_app.py
  • tests/util/__init__.py
  • tests/warehouse/filesystem/test_remote.py

Comment on lines +191 to +204
def test_endpoint_namespace_excludes_credentials_and_query_values():
assert (
_endpoint_namespace(
"https://user:password@MINIO.example:9000/storage/?token=secret",
"default",
)
== "minio.example:9000/storage"
)


def test_endpoint_namespace_normalizes_bare_host_and_url_forms():
assert _endpoint_namespace("account.blob.example", "default") == (
_endpoint_namespace("https://account.blob.example", "default")
)

@coderabbitai coderabbitai Bot Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing edge-case coverage for _endpoint_namespace's IPv6 and no-hostname branches.

Both _endpoint_namespace tests only exercise plain hostnames. The IPv6-bracketing branch (if ":" in host: host = f"[{host}]") and the "no hostname" fallback (e.g. a malformed or path-only endpoint) in src/dlt_filesystem/source/impl/remote.py (lines 26-27, 30-31) aren't covered. Since this helper directly feeds storage_namespace, which is hashed into the persistent incremental cursor key, an untested regression here could silently change/collide cursor identity for IPv6-hosted S3-compatible endpoints.

As per path instructions for tests/**/test_*.py: "Prefer flagging missing edge-case coverage over style nits."

🧪 Suggested additional test cases
 def test_endpoint_namespace_normalizes_bare_host_and_url_forms():
     assert _endpoint_namespace("account.blob.example", "default") == (
         _endpoint_namespace("https://account.blob.example", "default")
     )
+
+
+def test_endpoint_namespace_brackets_ipv6_hosts():
+    assert _endpoint_namespace("https://[::1]:9000/bucket", "default") == "[::1]:9000/bucket"
+
+
+def test_endpoint_namespace_falls_back_when_hostname_is_missing():
+    assert _endpoint_namespace("/just/a/path", "default") == "default"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_endpoint_namespace_excludes_credentials_and_query_values():
assert (
_endpoint_namespace(
"https://user:password@MINIO.example:9000/storage/?token=secret",
"default",
)
== "minio.example:9000/storage"
)
def test_endpoint_namespace_normalizes_bare_host_and_url_forms():
assert _endpoint_namespace("account.blob.example", "default") == (
_endpoint_namespace("https://account.blob.example", "default")
)
def test_endpoint_namespace_excludes_credentials_and_query_values():
assert (
_endpoint_namespace(
"https://user:password@MINIO.example:9000/storage/?token=secret",
"default",
)
== "minio.example:9000/storage"
)
def test_endpoint_namespace_normalizes_bare_host_and_url_forms():
assert _endpoint_namespace("account.blob.example", "default") == (
_endpoint_namespace("https://account.blob.example", "default")
)
def test_endpoint_namespace_brackets_ipv6_hosts():
assert _endpoint_namespace("https://[::1]:9000/bucket", "default") == "[::1]:9000/bucket"
def test_endpoint_namespace_falls_back_when_hostname_is_missing():
assert _endpoint_namespace("/just/a/path", "default") == "default"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/dlt_filesystem/test_source_incremental.py` around lines 191 - 204, Add
focused tests for _endpoint_namespace covering an IPv6 host and an endpoint with
no hostname, such as a path-only or malformed URL. Assert the IPv6 result
preserves bracketed host formatting and the no-hostname case follows the
helper’s existing fallback behavior, using the same default namespace context as
the current tests.

Source: Path instructions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@amotl, thanks for tracking this in #241. Keeping the focused IPv6 and no-hostname coverage follow-up out of this PR makes sense.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed - added focused coverage for bracketed IPv6 endpoints and missing-host fallback behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The focused coverage is in follow-up PR #243 because #239 merged before the reviewed fix commit was pushed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you!

@amotl amotl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Very sweet, thank you so much. 💯

Trivia: I've derived two tickets from minor comments by @coderabbitai that can be processed later at your disposal.

@amotl
amotl merged commit 2a571f1 into panodata:main Jul 20, 2026
14 checks passed
@hampsterx

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

Incremental loading for filesystem resources

2 participants