Skip to content

feat(sql): load remote SQLite and DuckDB databases - #272

Open
hampsterx wants to merge 2 commits into
mainfrom
feat/remote-file-databases
Open

feat(sql): load remote SQLite and DuckDB databases#272
hampsterx wants to merge 2 commits into
mainfrom
feat/remote-file-databases

Conversation

@hampsterx

@hampsterx hampsterx commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Load SQLite and DuckDB databases that live on a remote filesystem, addressed by the object URI the storage service already names them with: s3://analytics/snapshots/events.duckdb.
  • Route a filesystem-scheme source URI whose object carries a database extension to the SQL source rather than a format reader, so --source-table keeps selecting a table inside the database.
  • Resolve the engine from the staged file's header, so .db needs no disambiguation and a mislabeled object still loads.
  • Stage the object for the complete ingestion lifetime with size checks, byte verification, cleanup, and no remote writeback.

Changes

  • Add object staging to dlt_filesystem and database routing to the SQL source, covering s3://, r2://, gs://, az://, adls://, and abfss:// through the filesystem credential parsing those schemes already share.
  • Identify the engine from SQLite's header marker or DuckDB's DUCK magic, falling back to an unambiguous extension only for an empty object.
  • Reject selections a single database connection cannot open: globs, # fragments, credentials in the authority, and the split form that puts the object path on --source-table.
  • Validate a dry run against the SQL source the real run uses, without downloading the object.
  • Exclude mq-bridge-py==0.3.9 after its newly published macOS build corrupted values and failed the MQBridge suite; clean resolution selects 0.3.8.
  • Document the contract on filesystem.md, keep the DuckDB and SQLite pages usage-first, and record the grammar plus rejected alternatives in ADR-001.

Test plan

  • Full Docker-free suite: 980 passed, 44 skipped
  • S3, Azure, and GCS emulator cases: 10 passed in tests/main/filesystem/test_remote_integration.py
  • Ruff formatting/checks, ty, metadata validation
  • Strict Sphinx build
  • Independent Codex and GLM-5.2 reviews, with confirmed findings fixed or dispositioned
  • GitHub Actions unit matrix on macOS and Windows, Linux integration matrix, Codecov, Read the Docs, gitleaks, and OCI build

References

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • coderabbit-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9ca2dc5-2310-4d66-b570-ed840dc708a0

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

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 Aug 2, 2026

Copy link
Copy Markdown

@hampsterx
hampsterx force-pushed the feat/remote-file-databases branch 2 times, most recently from d5e8cdb to f7ff7e9 Compare August 2, 2026 22:33
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.49550% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.15%. Comparing base (7e3d5f8) to head (d944285).

Files with missing lines Patch % Lines
src/dlt_filesystem/staging.py 94.38% 5 Missing ⚠️
src/dlt_filesystem/util/auth.py 91.52% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #272      +/-   ##
==========================================
+ Coverage   59.45%   60.15%   +0.70%     
==========================================
  Files         234      236       +2     
  Lines       10977    11142     +165     
==========================================
+ Hits         6526     6703     +177     
+ Misses       4451     4439      -12     

☔ 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.

@amotl amotl linked an issue Aug 2, 2026 that may be closed by this pull request

@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.

Thank you for this improvement. I've added a comment with a question if the routing could be made differently, so that s3://analytics/snapshots/events.duckdb could be used as an input URL as-is.

Comment thread docs/supported-sources/duckdb.md Outdated
duckdb:///?location=<percent-encoded-s3-az-or-gs-uri>&<storage-options>
```

For example, this loads `main.events` from `s3://analytics/snapshots/events.duckdb`:

@amotl amotl Aug 2, 2026

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.

Is it possible to treat .duckdb and .sqlite resources using a reader, or otherwise dispatch processing differently, so that ingesting from s3://analytics/snapshots/events.duckdb actually becomes possible without encoding that request in a different way? The URL reads so well!

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.

Yes, and it reads better this way. Pushed: s3://analytics/snapshots/events.duckdb now works as written, and the location= parameter form is gone.

Routing. A filesystem-scheme source URI whose object carries a database extension (.db, .ddb, .duckdb, .sqlite, .sqlite3) goes to the SQL source instead of a format reader. --source-table main.events keeps selecting a table inside the database, and SQL reflection, chunking, and type mapping stay on the path. Credentials and storage options are the same query parameters the matching file source takes, so there is no second encoding layer:

omniload ingest \
  --source-uri 's3://analytics/snapshots/events.duckdb?access_key_id=ACCESS&secret_access_key=SECRET' \
  --source-table 'main.events' \
  --dest-uri 'duckdb:///local.duckdb' \
  --dest-table 'raw.events'

I went with dispatch rather than a reader. A reader would have to reimplement reflection, chunking, type mapping and table selection that the SQL source already owns, one engine at a time, and dlt_filesystem would grow a SQLAlchemy dependency. The routing decision stays in omniload; dlt_filesystem only gained a generic "stage this object" primitive.

Engine detection is from the file header, SQLite's marker at offset 0 and DuckDB's DUCK behind its checksum, because .db names both engines and an object's name is not evidence of its contents. So a mislabeled object still loads, and a non-database reports that rather than failing somewhere inside the driver. Only an empty object has no header, and it falls back to an unambiguous extension so a freshly created database stays loadable.

Scope: s3://, r2://, gs://, az://, adls://, abfss://, the schemes whose credential parsing is already shared with the file sources. The connectors added in GH-254 build their fsspec client inside dlt_source(), so staging cannot reach them without lifting that construction into a shared "authorized filesystem for this URI" helper. Happy to do that as a follow-up if you want databases on the other transports; it is the same seam GH-256 is about.

Two things the change deliberately does not do. The split form (--source-uri s3:// plus the object path on --source-table) is rejected with a message naming the carrier to use, since the table has to name a table inside the database. And a dry run now validates against the SQL source the real run uses, instead of the storage source, without downloading the object.

Docs: the contract lives in one place on filesystem.md (Database files), with the DuckDB and SQLite pages carrying just the example and a link. ADR-001 records the grammar and the rejected alternatives.

Verified: full Docker-free suite (980 passed, 44 skipped), the S3, Azure and GCS emulator cases (10 passed in that file), ruff, ty, and a strict Sphinx build. Independent Codex and GLM-5.2 reviews both flagged the split form, which is now handled; GLM caught the dry-run inconsistency.

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.

That sounds excellent, thank you. Now that the other PR got merged first, this one only needs to resolve conflicts and will be ready to go.

- Load SQLite and DuckDB source files from S3, Azure Blob Storage, and Google Cloud Storage.
- Stage each object for the complete ingestion lifetime with size checks, byte verification, cleanup, and no remote writeback.
- Reuse filesystem credential parsing while keeping credentials out of logs, errors, and object representations.
- Document the remote URI contract and cover parsing, lifecycle, compatibility, and emulator-backed ingestion.
Load a remote SQLite or DuckDB database from the URI its storage service
already names it with, `s3://analytics/snapshots/events.duckdb`, rather than
encoding that location into a `location` query parameter of a `duckdb:///`
URI. A filesystem-scheme source URI whose object carries a database extension
routes to the SQL source instead of a format reader, so `--source-table` keeps
selecting a table inside the database and SQL reflection, chunking, and type
mapping stay on the path.

Resolve the engine from the staged file's header (SQLite's marker at offset 0,
DuckDB's `DUCK` behind its checksum), because `.db` names both engines and an
object's name is not evidence of its contents. An empty object, a database
created but not yet written to, falls back to an unambiguous extension.

Cover the schemes whose credential parsing is already shared: s3, r2, gs, az,
adls, and abfss. Reject the split form (`--source-uri s3://` plus the object
path on `--source-table`) by naming the carrier to use, because the table
selects a table inside the database. Validate a dry run against the SQL source
the real run uses, without downloading the object.
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.

Database: Load file-based databases from remote filesystems

2 participants