Skip to content

Add Apache Arrow provider - #52330

Open
dabla wants to merge 240 commits into
apache:mainfrom
dabla:feature/apache-arrow-provider
Open

Add Apache Arrow provider#52330
dabla wants to merge 240 commits into
apache:mainfrom
dabla:feature/apache-arrow-provider

Conversation

@dabla

@dabla dabla commented Jun 27, 2025

Copy link
Copy Markdown
Contributor

Apache Arrow provider (apache-airflow-providers-apache-arrow)

Adds a new Airflow provider backed by Apache Arrow and the
Arrow Database Connectivity (ADBC) standard.

What's included

AdbcHook

A general-purpose DbApiHook subclass that connects to any ADBC-compatible database via the
adbc-driver-manager. It exposes the full Airflow SQL hook surface (get_records, run,
insert_rows, …) while internally operating on Arrow RecordBatch objects for zero-copy,
columnar data transfer.

Key capabilities:

  • Arrow-native insertsinsert_rows converts rows to Arrow RecordBatch objects and
    uses the cursor's native bind API when available, falling back to executemany otherwise.
  • Chunked bulk loading — rows are committed in configurable chunks (commit_every) to
    bound transaction size and lock duration.
  • Dynamic driver discovery — the driver shared library is located automatically from the
    installed wheel; the entrypoint can also be overridden via connection extras.
  • Dialect-aware — SQL placeholder style is normalised per-dialect before execution.
  • Connection extrasdriver, entrypoint, db_kwargs, conn_kwargs, and dialect
    are all configurable from the Airflow connection UI without code changes.

Supported databases (via optional extras)

Extra Driver package
sqlite adbc-driver-sqlite
postgresql adbc-driver-postgresql
snowflake adbc-driver-snowflake
bigquery adbc-driver-bigquery
flightsql adbc-driver-flightsql

Installation

# Core provider only
pip install apache-airflow-providers-apache-arrow

# With a specific driver
pip install "apache-airflow-providers-apache-arrow[postgresql]"

# With all bundled drivers
pip install "apache-airflow-providers-apache-arrow[all]"

Requirements

  • apache-airflow >= 2.11.0
  • apache-airflow-providers-common-sql >= 1.28.2
  • adbc-driver-manager >= 1.7.0
  • pyarrow >= 16.1.0 (Python < 3.13) / >= 18.0.0 (Python ≥ 3.13)
  • more-itertools >= 9.0.0

Link to the discussion thread on the devlist: https://lists.apache.org/thread/v1nxnr5gocvzs12pknjxkw64mrtpg2o0


^ Add meaningful description above
Read the Pull Request Guidelines for more information.
In case of fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
In case of a new dependency, check compliance with the ASF 3rd Party License Policy.
In case of backwards incompatible changes please leave a note in a newsfragment file, named {pr_number}.significant.rst or {issue_number}.significant.rst, in airflow-core/newsfragments.

@potiuk
potiuk marked this pull request as draft June 27, 2025 11:12
@potiuk

potiuk commented Jun 27, 2025

Copy link
Copy Markdown
Member

Just for anyone looking here - this is a draft for discussion between me, @dabla and @zeroshade - we will still need to start a DISCUSSION thread for the new provider - and we think Arrow and ADBC is a good addition. But we have to first discuss the approach :)

Comment thread providers/apache/arrow/docs/connections/adbc.rst Outdated
Comment thread providers/apache/arrow/README.rst
Comment thread providers/apache/arrow/README.rst
Comment thread providers/apache/arrow/pyproject.toml Outdated
Comment thread providers/apache/arrow/pyproject.toml Outdated
Comment thread providers/apache/arrow/src/airflow/providers/apache/arrow/hooks/adbc.py Outdated
Comment thread providers/apache/arrow/src/airflow/providers/apache/arrow/hooks/adbc.py Outdated
Comment thread providers/apache/arrow/src/airflow/providers/apache/arrow/hooks/adbc.py Outdated
Comment thread providers/apache/arrow/tests/unit/apache/arrow/hooks/test_adbc.py
@vikramkoka

Copy link
Copy Markdown
Contributor

As @potiuk mentioned, I believe this needs a devlist conversation first.
Looking forward to it

@potiuk

potiuk commented Jun 28, 2025

Copy link
Copy Markdown
Member

As @potiuk mentioned, I believe this needs a devlist conversation first. Looking forward to it

Yep. This one is mostly to gather learnigs, get feedback from @zeroshade and see how we can turn it into a "convincing" devlist proposal - by showing some use cases and small POC of implementation and what it allows :).

We'll experiment a bit with it and gather our thoughts and see what can come out of it.

dabla and others added 16 commits July 9, 2026 15:06
# Conflicts:
#	dev/breeze/doc/images/output_build-docs.svg
#	dev/breeze/doc/images/output_build-docs.txt
#	dev/breeze/doc/images/output_release-management_add-back-references.svg
#	dev/breeze/doc/images/output_release-management_add-back-references.txt
#	dev/breeze/doc/images/output_release-management_classify-provider-changes.svg
#	dev/breeze/doc/images/output_release-management_classify-provider-changes.txt
#	dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg
#	dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt
#	dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg
#	dev/breeze/doc/images/output_release-management_generate-providers-metadata.txt
#	dev/breeze/doc/images/output_release-management_prepare-provider-distributions.svg
#	dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt
#	dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg
#	dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt
#	dev/breeze/doc/images/output_release-management_publish-docs.svg
#	dev/breeze/doc/images/output_release-management_publish-docs.txt
#	dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg
#	dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt
#	dev/breeze/doc/images/output_workflow-run_publish-docs.svg
#	dev/breeze/doc/images/output_workflow-run_publish-docs.txt
#	uv.lock
@dabla
dabla requested a review from potiuk July 15, 2026 05:45
@dabla

dabla commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Just for anyone looking here - this is a draft for discussion between me, @dabla and @zeroshade - we will still need to start a DISCUSSION thread for the new provider - and we think Arrow and ADBC is a good addition. But we have to first discuss the approach :)

A discussion was already started on the devlist in beginning of march this year.

@dabla
dabla force-pushed the feature/apache-arrow-provider branch from 4675498 to 2c63de3 Compare July 15, 2026 09:19
@dabla
dabla force-pushed the feature/apache-arrow-provider branch from c80496f to 4508f06 Compare July 16, 2026 14:12
@dabla
dabla force-pushed the feature/apache-arrow-provider branch from 96d77a6 to 29253c4 Compare July 22, 2026 10:38
if not target_fields:
target_fields = table_schema.names
else:
table_schema = schema([field for field in table_schema if field.name in target_fields])

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.

The filtered schema keeps the table's column order, but _generate_insert_sql below emits the column list in target_fields order, and _to_record_batch indexes rows positionally against this schema. When the two orders differ, values land in the wrong columns.

With target_fields=["last", "first"] against a table declared (first, last), the filtered schema comes back as [first, last], so the SQL says INSERT INTO t (last, first) while the RecordBatch is {first: 'Smith', last: 'Alice'}. Same-typed columns silently swap; differently-typed ones blow up with ArrowInvalid: Could not convert 'Alice' with type str: tried to convert to int64.

Ordering the filtered schema by target_fields instead of by the table schema fixes both cases:

fields = {field.name: field for field in table_schema}
table_schema = schema([fields[name] for name in target_fields])

cur, executemany=executemany, fast_executemany=fast_executemany
)

for chunked_rows in chunked(rows, commit_every):

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.

The docstring says commit_every=0 inserts all rows in one transaction, but more_itertools.chunked(rows, 0) yields no chunks at all, so nothing is inserted and the method logs "Loaded a total of 0 rows" without raising. DbApiHook.insert_rows reads 0 as "don't commit mid-way", so this also diverges from the base class.

chunked(rows, commit_every or None) restores the documented behaviour, since chunked(rows, None) yields a single chunk containing everything.

Picks native Arrow bind when the cursor supports it; otherwise falls back
to executemany, optionally enabling fast_executemany on the cursor first.
"""
use_native_bind = hasattr(cursor, "bind")

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.

adbc_driver_manager.dbapi.Cursor has no bind method, neither on 1.11.0 nor on the >=1.7.0 floor declared in pyproject.toml (its public surface is adbc_ingest, adbc_prepare, execute, executemany, ...). So hasattr(cursor, "bind") is always False against a real driver, _resolve_execute_batch always returns _execute_executemany, and both _execute_native_bind and the "Native Arrow bind supported!" log are unreachable in production.

test_insert_rows_native_bind only passes because setup_method assigns self.cur.bind = mock.MagicMock() onto a plain MagicMock, so the test greenlights a path that cannot execute. Since the Arrow-native transfer is the headline of the provider, worth pinning down what the intended fast path is: cursor.adbc_ingest, or executemany with the RecordBatch (which does work, I checked it against the SQLite driver)?

"handlers that are specifically designed for your database."
)
if cursor.description is not None:
return list(zip(*cursor.fetch_arrow_table().to_pydict().values()))

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.

Routing through to_pydict() collapses duplicate column names, so get_records silently drops columns whenever a query selects the same name twice. SELECT a.id, b.id FROM a JOIN b ... produces an Arrow table with two id columns, but to_pydict() keeps only the last, and the zip then yields 1-tuples where every other DbApiHook returns 2-tuples.

Iterating the columns positionally avoids it and behaves the same for the normal and empty-result cases:

table = cursor.fetch_arrow_table()
return list(zip(*(column.to_pylist() for column in table.columns)))

entrypoint=self.entrypoint,
db_kwargs=self.db_kwargs,
conn_kwargs=self.conn_kwargs,
autocommit=False,

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.

autocommit is pinned to False here and there's no set_autocommit override, so with supports_autocommit = True the base _create_autocommit_connection ends up running conn.autocommit = autocommit against an ADBC dbapi.Connection. That only sets an unused Python attribute: after conn.autocommit = True the connection is still in manual-commit mode, and conn.commit() keeps succeeding instead of erroring. insert_rows(..., autocommit=True) and run(..., autocommit=True) therefore do nothing at the driver level.

ADBC's actual lever is the autocommit= kwarg on connect() or adbc_connection.set_options(**{"adbc.connection.autocommit": "true"}). Worth noting that wiring it up properly also needs the conn.commit() inside the insert_rows chunk loop made conditional: once autocommit is really on, SQLite raises ProgrammingError: INVALID_STATE: No active transaction, cannot commit on that call.

* - Key
- Type
- Description
* - ``autocommit``

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.

These four keys aren't ADBC option names. Against the real SQLite driver each one is rejected: NotSupportedError: NOT_IMPLEMENTED: [SQLite] Unknown connection option autocommit='true', and the same for read_only, current_catalog and current_db_schema. The dotted form is the canonical spelling, and of the four only adbc.connection.autocommit is actually accepted.

The db_kwargs table above has the mirror-image problem: adbc.connection.autocommit is listed there, but it's a connection option and belongs under conn_kwargs. timeout (the key test_get_conn_forwards_db_kwargs asserts) is rejected as an unknown database option too. The tests can't catch any of this because they patch connect, so the passthrough assertions pass while the documented values would fail against a driver. Distinct from the earlier db_kwargs threads, which were about whether to forward options at all rather than which names are valid.

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.

6 participants