Skip to content

feat(filesystem): read Azure through pyarrow.fs - #297

Merged
amotl merged 1 commit into
panodata:mainfrom
hampsterx:feat/filesystem-azure-arrow
Aug 21, 2026
Merged

feat(filesystem): read Azure through pyarrow.fs#297
amotl merged 1 commit into
panodata:mainfrom
hampsterx:feat/filesystem-azure-arrow

Conversation

@hampsterx

Copy link
Copy Markdown
Contributor

Summary

  • Read Azure Blob Storage and ADLS Gen2 sources (az://, adls://, abfss://) through pyarrow.fs.AzureFileSystem instead of adlfs, for the per-file open cost every glob load pays.
  • Measured against the Azurite emulator: opening and reading 300 small blobs costs 2.4 ms/blob through Arrow against 6.7 ms/blob through adlfs, and a 64 MB read runs at 456 MB/s against 193 MB/s. Listing is already one native recursive request per glob through the shared lister.
  • Carry over every credential mode the connector accepts: account key, SAS token, service principal, and connection string. One Arrow client serves flat-namespace and hierarchical-namespace accounts and detects which it is talking to.
  • Map a custom endpoint onto both the Blob and the Data Lake authority, since Arrow reaches a hierarchical-namespace account through the latter. A DfsEndpoint in a connection string is used as given; otherwise it is derived from the Blob endpoint.
  • Leave file-level incremental selection and its cursor identity untouched, so no cursors reset. Writes, and the staging download that materializes a remote database file, continue to use adlfs.

Part of #233. S3 and R2 moved in #290; GCS stays on gcsfs, as agreed in that issue.

Behavior changes

Both are refused rather than accepted and ignored, and both are documented in docs/supported-sources/azure-storage.md and the changelog.

  • api_version is no longer accepted on a source URI. Arrow pins the API version its bundled SDK speaks and offers no override. This is not cosmetic: an endpoint that enforces the version check is now unreadable through the source, and Azurite is such an endpoint, so the emulator container is started with --skipApiVersionCheck.
  • A source connection string has to name its account. Arrow takes the storage account as the root of the filesystem, so a string that identifies its account by endpoint alone (a bare SharedAccessSignature with a custom BlobEndpoint and no AccountName) is rejected with a named error.

Building a source in Python rather than through a URI, connection arguments now have to be pyarrow.fs.AzureFileSystem keywords; an adlfs-only argument is rejected by name rather than ignored.

Review

Two independent pre-push reviews completed, both repo-aware, and both findings were applied.

  • A SAS token now carries the leading ? Arrow requires. Arrow appends the token to the account URL verbatim, and both carriers hand it over without one, so every SAS-authenticated read failed with OutOfRangeInput. Construction cannot catch this, because AzureFileSystem performs no I/O until first use; an emulator test that generates an account SAS and reads through both carriers now does.
  • A Private Link hostname (acct.privatelink.blob.core.windows.net) derived the Blob authority for both services, sending Data Lake requests to the wrong host. The service label is now substituted wherever it sits in the suffix.
  • A connection string's DfsEndpoint was silently dropped, and the connection string was parsed twice per source build. Both fixed.

Changes

  • Read Azure through pyarrow.fs, behind the _filesystem() seam S3 already uses, with an Azure wrapper whose _strip_protocol removes only the scheme prefix so a blob name keeps ?, # and percent escapes.
  • Resolve one connection string once into account, endpoints and credential through public Azure SDK APIs.
  • Translate credentials and endpoints into Arrow's authority-and-scheme pairs, with the endpoint validation rules the S3 swap applies.
  • Start the Azure emulator container with --skipApiVersionCheck, and drop the adlfs API-version test fixture it replaces.

10 files changed, 997 insertions, 100 deletions.

Test plan

  • Full non-integration suite: 1,281 passed, 46 skipped.
  • Azure emulator integration tests: 16 passed, up from 9.
  • Whole remote-filesystem integration lane (S3, Azure, GCS): 34 passed.
  • Azure source and destination matrices in the warehouse suite: 175 passed.
  • New: a second run picks up only a newly uploaded blob, and run options stay out of the Arrow constructor.
  • New: az://, adls:// and abfss:// each read through the same client; blob names carrying #, ? and %2F survive discovery and open.
  • New: shallow, recursive, concrete and unmatched selections each cost one native listing request, recursive only when the pattern crosses a level.
  • New: a generated account SAS reads end to end through both carriers against the emulator, which validates the signature.
  • New: the mapped keywords are pinned against pyarrow.fs.AzureFileSystem itself, so a wrong keyword name fails rather than passing.
  • Two connection-string accounts still hash to distinct incremental resources.
  • Ruff formatting and lint checks, ty check, validate-pyproject.
  • Strict Sphinx build, warnings treated as errors.
  • _azurefs confirmed present in the pyarrow 25.0.1 wheels for the CI matrix (cp310 and cp314; manylinux, macOS arm64, win_amd64).

Live service-principal authentication is not exercised, here or before this change: the emulator validates only a token's shape, needs HTTPS, and ClientSecretCredential would still have to reach Entra ID for a token.

No visual surface changed, so screenshots are not applicable.

Every glob load pays a per-file open, and that is where the Azure source
spent most of its time. Reading `az://`, `adls://` and `abfss://` through
`pyarrow.fs.AzureFileSystem` instead of adlfs cuts it: measured against
Azurite, opening and reading 300 small blobs costs 2.4 ms/blob through Arrow
against 6.7 ms/blob through adlfs, and a 64 MB read runs at 456 MB/s against
193 MB/s. Listing already benefits, because the vendored lister issues one
native recursive request for any Arrow-backed client.

The native client does not speak the fsspec contract the lister needs, so it
goes behind the `_filesystem()` seam S3 introduced, wrapped by an
`_AzureArrowFSWrapper` whose `_strip_protocol` removes only the scheme
prefix: a blob name may contain `?` or `#` literally, and the generic wrapper
reads the rest of the path as a URL.

Credentials map field by field. Arrow takes no connection string, so one parse
resolves the account, both endpoints and the credential: the account key comes
off the parsed client's credential and the SAS off its composed URL, through the
same public SDK APIs that already resolve account and endpoint identity. A SAS
is normalised to the leading-`?` form, which Arrow requires because it appends
the token to the account URL verbatim; either form is accepted on input.

`account_host` becomes Arrow's authority-and-scheme pair for both the Blob and
the Data Lake endpoint, since Arrow reaches a hierarchical-namespace account
through the latter and naming only the former would send half the requests to
the public cloud. A suffix carrying a service label yields its sibling by
substitution, so a Private Link host pairs correctly, and a connection string
that names a `DfsEndpoint` has it used as given rather than derived. An endpoint
has to name its account, which is what makes both of Arrow's authority forms
derivable, and the endpoint rules S3 applies (HTTP schemes only, no credentials,
no query or fragment) carry over.

Two parameters lose meaning, and both are refused rather than ignored.
`api_version` has no Arrow equivalent, because Arrow pins the version its
bundled SDK speaks; an emulator that enforces the version check now needs
`--skipApiVersionCheck`, which is how the Azurite test container is started.
A source connection string that identifies its account by endpoint alone,
with no `AccountName`, cannot be addressed at all.

Incremental behaviour and cursor identity are untouched, so nothing resets.
adlfs stays for the remote-database staging download and for writes, which
dlt's filesystem destination builds itself.
@coderabbitai

coderabbitai Bot commented Aug 21, 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: 56f01efd-8d54-4b1c-93a6-7a4b27ad23dc

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

Copy link
Copy Markdown

Documentation build overview

📚 omniload | 🛠️ Build #34164787 | 📁 Comparing 8f72a85 against latest (e040e53)

  🔍 Preview build  

2 files changed
± changelog.html
± supported-sources/azure-storage.html

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.87640% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 61.82%. Comparing base (e040e53) to head (8f72a85).

Files with missing lines Patch % Lines
src/dlt_filesystem/source/impl/remote.py 94.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #297      +/-   ##
==========================================
+ Coverage   61.66%   61.82%   +0.15%     
==========================================
  Files         235      235              
  Lines       11408    11487      +79     
==========================================
+ Hits         7035     7102      +67     
- Misses       4373     4385      +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 commented Aug 21, 2026

Copy link
Copy Markdown
Member

Excellent, thank you! Released with v0.11.0.

@amotl
amotl merged commit 3541d4d into panodata:main Aug 21, 2026
14 checks passed
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.

2 participants