Skip to content

Add Azure Blob Storage support to the DataFusion object storage layer - #73374

Merged
pankajastro merged 20 commits into
apache:mainfrom
pankajastro:fix-datafusion-azure-credentials-afl-214
Oct 5, 2026
Merged

pankajastro merged 20 commits into
apache:mainfrom
pankajastro:fix-datafusion-azure-credentials-afl-214

Conversation

@pankajastro

@pankajastro pankajastro commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

DataFusion credential resolution only handled AWS (and, in a sibling PR, GCS), so a wasb-backed DataSourceConfig failed with Unknown connection type wasb at registration, even though DataFusion's storage layer supports Azure Blob Storage.

Adds:

  • StorageType.AZURE and az:// URI recognition in DataSourceConfig
  • AzureObjectStorageProvider using DataFusion's MicrosoftAzure object store binding
  • A wasb branch in _get_credentials resolving, in order: Azure AD service principal (tenant_id extra + login/password as client_id/client_secret), SAS token (sas_token extra, parsed into query pairs), then shared key (password, or shared_access_key/account_key extra) — falling back to ambient/environment-based auth when none apply
  • Docs updated to mention Azure alongside S3

On the service-principal fields: DataFusion's MicrosoftAzure binding (crates/core/src/store.rs) panics if client_id/client_secret/tenant_id are only partially set — verified directly:

thread '<unnamed>' panicked at src/store.rs:114:17:
client_id, client_secret, tenat_id must be all set or all None

(PyO3 converts this to a PanicException rather than crashing the process, but it's still an ugly failure mode.) So this PR only forwards all three when the connection genuinely has all three (tenant_id extra plus both login and password); otherwise it falls through to the next auth mode rather than risking a partial combination.

Unsupported fields: connection_string, managed_identity_client_id, workload_identity_tenant_id, and a URL-form sas_token aren't supported by DataFusion's Azure binding. Setting any of these now raises a clear error instead of silently falling back to a different identity.

Scope: only az:// URIs are recognized for now — abfs:///abfss:// are not. Those schemes embed the storage account in the URI itself (e.g. abfss://container@account.dfs.core.windows.net/path), which is a different shape from az:// and raises a real question given host now takes priority when resolving the account from the connection: if the URI's account differs from the connection's, which one wins? That's a design decision worth its own PR rather than bundling in here.


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Sonnet 5)

Generated-by: Claude Code (Sonnet 5) following the guidelines

@pankajastro
pankajastro force-pushed the fix-datafusion-azure-credentials-afl-214 branch 3 times, most recently from 38a50f6 to e3a4925 Compare September 21, 2026 04:06
@pankajastro
pankajastro marked this pull request as ready for review September 21, 2026 08:59
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/config.py
@pankajastro
pankajastro force-pushed the fix-datafusion-azure-credentials-afl-214 branch from 268a566 to 417c9c8 Compare September 22, 2026 12:48
Comment thread providers/common/sql/docs/operators.rst Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/tests/unit/common/sql/datafusion/test_engine.py Outdated
Comment thread providers/common/sql/docs/operators.rst
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
@kaxil

kaxil commented Sep 22, 2026

Copy link
Copy Markdown
Member

Round 2 at 016a9bc. The three code comments from the first round are addressed: the account now comes from host first, the Azure error message uses .value, and the urllib.parse import is at module level. Keeping abfs:// and abfss:// for a separate PR is fine.

Four new items, all measured against the real datafusion binding and object_store source:

  • The worker's AZURE_* environment is read by from_env() before the connection's credential is overlaid, and object_store checks an environment access key or workload identity before a SAS token or client secret. With AZURE_STORAGE_ACCOUNT_KEY set, or the AKS workload-identity variables set, the connection's SAS or service principal is never used. The docs currently say ambient credentials come last.
  • _resolve_wasb_account keeps only the first host label and the binding has no endpoint parameter, so sovereign-cloud, private-endpoint and Azurite hosts are silently redirected to *.blob.core.windows.net.
  • A partial service principal (tenant_id with login or password missing) falls through to ambient auth or forwards the client secret as a shared key instead of raising.
  • With neither host nor login set, the account becomes the string "None", where the previous code let the binding fall back to AZURE_STORAGE_ACCOUNT_NAME. That is the shape of the default wasb_default connection the example DAG uses.

Inline comments have the details and suggested fixes. Verdict: hold until these are addressed.

@kaxil

kaxil commented Sep 23, 2026

Copy link
Copy Markdown
Member

@pankajastro Conflicts !

@pankajastro
pankajastro force-pushed the fix-datafusion-azure-credentials-afl-214 branch from e42b8fc to 85db1b2 Compare September 23, 2026 10:23
@pankajastro

Copy link
Copy Markdown
Member Author

@pankajastro Conflicts !

Rebased onto current main, thanks — should be conflict-free now.

Comment thread providers/common/sql/docs/operators.rst Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/tests/unit/common/sql/datafusion/test_engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/docs/operators.rst Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
@pankajastro
pankajastro force-pushed the fix-datafusion-azure-credentials-afl-214 branch from add8c41 to 268a08e Compare September 29, 2026 08:05
@kaxil
kaxil requested a review from Lee-W September 29, 2026 14:45
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
Comment thread providers/common/sql/docs/operators.rst Outdated
Comment thread providers/common/sql/tests/unit/common/sql/datafusion/test_engine.py Outdated
Comment thread providers/common/sql/src/airflow/providers/common/sql/datafusion/engine.py Outdated
pankajastro and others added 17 commits October 5, 2026 12:25
Missing comma made "Azure Blob Storage Parquet" read as one item
instead of two separate examples in the supported source list.
The Analytics operator docs listed Azure as supported in the intro
line but not in the Supported Storage Systems list, still flagged it
under "not yet supported", and had no usage example or credential
docs, unlike S3 and GCS.
The wasb credential branch took the storage account name unconditionally
from the connection's login field. For service-principal auth, login holds
the client_id, not the account name, so a connection whose host correctly
named the storage account still had the object store target the client_id
GUID instead. A connection with no login at all (key/SAS auth) silently
fell back to the AZURE_STORAGE_ACCOUNT_NAME environment variable.

Resolve the account from host first, matching WasbHook's own behavior, and
fall back to login only when host is empty. Also format the storage type
name consistently with .value (avoids drifting str-Enum formatting across
Python versions) and move the sas_token parsing import to the top of the
file.
abfs:// and abfss:// embed the storage account in the URI itself, a
different shape from az:// that engine.py does not parse; using them
currently fails with an unhelpful "Unsupported storage type" error with
no hint that Azure support exists under a different scheme.
Four issues surfaced by review, each verified against the real datafusion
binding and object_store source:

- An empty connection (no host or login -- the shape of the wasb_default
  connection airflow db creates) sent the literal string "None" as the
  account instead of omitting it, breaking the fallback to
  AZURE_STORAGE_ACCOUNT_NAME that worked before the host-priority fix.

- A partial service-principal config (tenant_id set but login or password
  missing) silently fell through to ambient auth, or sent the client
  secret as a shared key, instead of raising -- authenticating as a
  different identity than the one configured.

- The account name kept only the first DNS label, so a sovereign-cloud
  host (*.chinacloudapi.cn, *.usgovcloudapi.net) or the Azurite emulator's
  loopback URL was silently redirected to the public
  *.blob.core.windows.net account of the same name; the binding has no
  endpoint override to correct this after the fact.

- DataFusion's MicrosoftAzure always reads AZURE_* worker environment
  variables before overlaying the connection's credential, and checks an
  environment-derived access key or workload-identity token ahead of the
  connection's SAS token or client secret. In the common AKS
  workload-identity deployment, this meant the connection's credential was
  silently ignored in favor of the pod's identity. The binding has no way
  to skip its own environment read, so this can only be caught, not fixed.
WasbHook and GoogleBaseHook still fall back to fields written as
extra__wasb__<name> / extra__google_cloud_platform__<name> -- the spelling
older Airflow connection UIs used for custom extra fields. This code only
looked at the bare key, so a connection created that way had its sas_token
or connection_string silently invisible: the unsupported-field guard never
saw a prefixed connection_string, and a prefixed sas_token fell through to
ambient auth instead of being used.
Same facts, fewer words -- the mechanism and consequence were spelled out
twice in slightly different phrasing.
Same facts, plainer words -- "outrank", "ahead of", and "consulted last"
weren't adding anything a simpler verb wouldn't.
Names the actual library so it's clear this is DataFusion's underlying
binding's behavior, not an Airflow-defined setting, and contrasts it
directly with WasbHook's automatic az login to explain why it's worth
calling out at all.
The docs spellcheck job uses an American-English dictionary; "honors" is
already the spelling used elsewhere in this same doc tree.
Collapse the legacy-field lookup into one dict.get() call, and use the
walrus operator so tenant_id/sas_token are only computed where they're
actually checked, instead of unconditionally ahead of the branch. Also
consolidate the three shared-key test variants (password, shared_access_key
extra, account_key extra) into one parametrized test, picking up account_key
coverage that was previously untested.
The env-var precedence guard was missing two real object_store aliases
(AZURE_STORAGE_MASTER_KEY and the AZURE_CLIENT_ID/SECRET/TENANT_ID
triple, including the AKS workload-identity webhook's own env vars)
that silently outrank a connection's SAS token, while AZURE_STORAGE_SAS_KEY
never can and was raising for nothing.

The account-resolution check also rejected a non-public-cloud host even
when the worker had already set AZURE_STORAGE_ENDPOINT to fix exactly
that, so the documented workaround never actually worked.

Finally, the SAS-token branch was checked ahead of the shared-key extra,
the opposite of WasbHook.get_conn's order -- a connection with a current
shared_access_key but an expired SAS token would work through the hook
and fail here.

Co-Authored-By: Claude <noreply@anthropic.com>
They were declared next to the unrelated _AZURE_PUBLIC_SUFFIX constant,
~180 lines from _get_credentials, the only place that reads them --
inconsistent with the file's existing convention of keeping a constant
next to its sole user.

Co-Authored-By: Claude <noreply@anthropic.com>
The flat guard list raised for env vars that could never actually
outrank a connection's own credential in object_store's precedence
chain -- most notably AZURE_FEDERATED_TOKEN_FILE against a plain
shared-key connection, which blocks the single most common Azure
Kubernetes deployment shape (the workload-identity webhook injects it
into every labelled pod) even though the connection's own key always
wins there.

Also closes a gap in the sovereign-cloud/endpoint-override check: an
Azurite-style host (bare hostname or host:port, with no login) fell
through an unrelated Active-Directory-ID fallback and silently
resolved to the literal string "None" instead of raising.

Co-Authored-By: Claude <noreply@anthropic.com>
… tier

Each credential tier (access_key, service_principal, SAS) had a
"rejects when outranked" test and a separate "ignores when it can't be
outranked" test that only differed in which env vars were set and
whether the call was expected to raise. Folding both into a single
should_raise-parametrized test per tier keeps the same coverage with
far less repetition.

Co-Authored-By: Claude <noreply@anthropic.com>
Left over from incrementally adding the emulator-host colon check;
the suffix check and the endpoint-override check were always meant
to be one combined condition, not a nested if.

Co-Authored-By: Claude <noreply@anthropic.com>
…ring

object_store's resolve_sas_token() does read this variable into
self.sas_key; it's just checked after sas_query_pairs within that same
step, so it never wins once the connection sets sas_query_pairs. "Dead"
overstated that as unused entirely, which could mislead a future reader
into thinking the safety argument doesn't depend on precedence order.

Co-Authored-By: Claude <noreply@anthropic.com>
@pankajastro
pankajastro force-pushed the fix-datafusion-azure-credentials-afl-214 branch from a1c20f8 to 8289d47 Compare October 5, 2026 06:55
@pankajastro
pankajastro merged commit 5487c08 into apache:main Oct 5, 2026
108 checks passed
pankajastro added a commit to pankajastro/airflow that referenced this pull request Oct 5, 2026
PR apache#73374 added az:// support to the DataFusion object storage layer
but deliberately deferred abfs:// and abfss://, since they embed the
storage account in the URI itself (container@account.host/path)
rather than relying solely on the wasb connection, raising an open
question of which account should win on disagreement.

Datasources can now use either scheme. When the URI names an account
that disagrees with the one the wasb connection resolves to,
registering the datasource raises instead of silently picking one.

Co-Authored-By: Claude <noreply@anthropic.com>
pankajastro added a commit to pankajastro/airflow that referenced this pull request Oct 6, 2026
PR apache#73374 added az:// support to the DataFusion object storage layer
but deliberately deferred abfs:// and abfss://, since they embed the
storage account in the URI itself (container@account.host/path)
rather than relying solely on the wasb connection, raising an open
question of which account should win on disagreement.

Datasources can now use either scheme. When the URI names an account
that disagrees with the one the wasb connection resolves to,
registering the datasource raises instead of silently picking one.

Co-Authored-By: Claude <noreply@anthropic.com>
pankajastro added a commit to pankajastro/airflow that referenced this pull request Oct 7, 2026
PR apache#73374 added az:// support to the DataFusion object storage layer
but deliberately deferred abfs:// and abfss://, since they embed the
storage account in the URI itself (container@account.host/path)
rather than relying solely on the wasb connection, raising an open
question of which account should win on disagreement.

Datasources can now use either scheme. When the URI names an account
that disagrees with the one the wasb connection resolves to,
registering the datasource raises instead of silently picking one.

Co-Authored-By: Claude <noreply@anthropic.com>
pankajastro added a commit to pankajastro/airflow that referenced this pull request Oct 8, 2026
PR apache#73374 added az:// support to the DataFusion object storage layer
but deliberately deferred abfs:// and abfss://, since they embed the
storage account in the URI itself (container@account.host/path)
rather than relying solely on the wasb connection, raising an open
question of which account should win on disagreement.

Datasources can now use either scheme. When the URI names an account
that disagrees with the one the wasb connection resolves to,
registering the datasource raises instead of silently picking one.

Co-Authored-By: Claude <noreply@anthropic.com>
pankajastro added a commit to pankajastro/airflow that referenced this pull request Oct 9, 2026
PR apache#73374 added az:// support to the DataFusion object storage layer
but deliberately deferred abfs:// and abfss://, since they embed the
storage account in the URI itself (container@account.host/path)
rather than relying solely on the wasb connection, raising an open
question of which account should win on disagreement.

Datasources can now use either scheme. When the URI names an account
that disagrees with the one the wasb connection resolves to,
registering the datasource raises instead of silently picking one.

Co-Authored-By: Claude <noreply@anthropic.com>
pankajastro added a commit to pankajastro/airflow that referenced this pull request Oct 14, 2026
PR apache#73374 added az:// support to the DataFusion object storage layer
but deliberately deferred abfs:// and abfss://, since they embed the
storage account in the URI itself (container@account.host/path)
rather than relying solely on the wasb connection, raising an open
question of which account should win on disagreement.

Datasources can now use either scheme. When the URI names an account
that disagrees with the one the wasb connection resolves to,
registering the datasource raises instead of silently picking one.

Co-Authored-By: Claude <noreply@anthropic.com>
kaxil added a commit that referenced this pull request Oct 14, 2026
#74228)

PR #73374 added az:// support to the DataFusion object storage layer
but deliberately deferred abfs:// and abfss://, since they embed the
storage account in the URI itself (container@account.host/path)
rather than relying solely on the wasb connection, raising an open
question of which account should win on disagreement.

Datasources can now use either scheme. When the URI names an account
that disagrees with the one the wasb connection resolves to,
registering the datasource raises instead of silently picking one.

* Quote az:// and abfs(s):// in docstrings to fix docs spellcheck

Sphinx's spellchecker flattens unquoted docstring prose to plain text,
so the bare az:// and abfs(s):// in two docstrings were flagged as
misspelled words. Wrapping them in double backticks, the convention
already used elsewhere in this file, excludes them as RST literals.

* Pass host to register_object_store so bucket-style URIs resolve

DataFusion's object-store registry keys on (schema, host); omitting
host only matches an empty-authority URI like file:///path. Every
bucket/container-style scheme (s3://, gs://, az://, and now
abfs(s)://) therefore failed at query time with "No suitable object
store found", a pre-existing gap in the original az:// support
invisible to the test suite because it mocks SessionContext rather
than exercising DataFusion's real registry. Found by manually testing
abfs:// against a real storage account.

* Fix object-store registry collisions, LOCAL regression, and config mutation

Addresses review feedback: the abfs(s) object-store registry key collided
for two containers on the same storage account, an explicit
storage_type=StorageType.LOCAL with a bare path regressed when get_scheme
was generalized to take a URI, and a connection resolving no account
silently fell back to AZURE_STORAGE_ACCOUNT_NAME instead of the account
named in the URI.

Fixing the first of those by folding the container alone into the
registry's host key turned out to trade one collision for another -- two
different accounts sharing a container name would then collide instead.
Folding both account and container into the host key avoids both.

Also stopped register_datasource from mutating the caller's
DataSourceConfig.uri in place: reusing the same config across a second
engine fed the already-normalized URI back through get_bucket, which
misread the account+container host token as the container name. The
canonical URI is now threaded through locally instead.

Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants