This is an automated email from the ASF dual-hosted git repository.
dabla pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new cc786845ad3 Build the ADBC database URI from the connection fields
(#74454)
cc786845ad3 is described below
commit cc786845ad372219fea9cb78fe69389f0126b345
Author: David Blain <[email protected]>
AuthorDate: Thu Nov 5 13:42:19 2026 +0100
Build the ADBC database URI from the connection fields (#74454)
* Build the ADBC database URI from the connection fields, not from
Airflow's connection URI
AdbcHook.uri handed the driver Airflow's connection URI with the scheme
swapped for the dialect. That URI carries the extras in its query string,
so a connection with the dialect extra (needed to pick the driver) yields
postgresql://user:pass@host/db?dialect=postgresql, which libpq refuses
with "invalid URI query parameter". A full URI in the Host field, as the
docs describe, was percent-encoded into the path for the same reason;
only hosts containing "::" went through unchanged.
The hook now passes a Host that is already a URI or a path through
unchanged (``://``, ``::``, or starting with ``/``, ``:`` or ``file:``)
and otherwise builds ``<dialect>://login:password@host:port/schema``
from the connection's fields. Port and Schema are no longer hidden in
the connection form, since they take part in that URI, and the docs
describe the two ways of filling the form.
* Regenerate the ADBC provider info and add libpq to the spelling wordlist
The ui-field-behaviour change in provider.yaml had not been carried
over to the generated get_provider_info.py, which the
update-providers-build-files check flagged, and the spellcheck did not
know libpq from the new AdbcHook.uri docstring.
---
docs/spelling_wordlist.txt | 1 +
providers/apache/arrow/docs/connections/adbc.rst | 22 ++++----
providers/apache/arrow/provider.yaml | 12 ++--
.../providers/apache/arrow/get_provider_info.py | 10 +++-
.../airflow/providers/apache/arrow/hooks/adbc.py | 43 +++++++++++----
.../tests/unit/apache/arrow/hooks/test_adbc.py | 64 ++++++++++++++++++++++
6 files changed, 124 insertions(+), 28 deletions(-)
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index ec0b8690b39..702198549b6 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -1040,6 +1040,7 @@ leq
LevelDB
leveldb
lexicographically
+libpq
libs
libz
Lifecycle
diff --git a/providers/apache/arrow/docs/connections/adbc.rst
b/providers/apache/arrow/docs/connections/adbc.rst
index ad840e7c922..df928af1570 100644
--- a/providers/apache/arrow/docs/connections/adbc.rst
+++ b/providers/apache/arrow/docs/connections/adbc.rst
@@ -36,18 +36,16 @@ Default Connection ID
Configuring the Connection
--------------------------
-Connection URL (Host field)
- The database URI passed to the driver. For drivers that use a standard
- connection string (e.g. ``postgresql://user:pass@host:5432/db`` or
- ``file::memory:``), put it here. If the value contains ``::`` the hook
- passes it through unchanged; otherwise the ``adbc://`` scheme prefix is
- replaced with the dialect name.
-
-Login / Password
- Convenience fields. When set, Airflow builds a URI of the form
- ``<dialect>://login:password@host/schema`` that is merged into
- ``db_kwargs["uri"]``. Driver-specific URIs in the Host field take
- precedence.
+Host or URI (Host field)
+ Either the host name of the database, or the complete URI the driver
+ expects (e.g. ``postgresql://user:pass@host:5432/db`` or
+ ``file::memory:``). A value that contains ``://`` or ``::``, or that
+ starts with ``/``, ``:`` or ``file:``, is passed to the driver unchanged.
+
+Login / Password / Port / Schema
+ Used when the Host field holds a host name: the hook then builds the URI
+ ``<dialect>://login:password@host:port/schema`` from these fields. The
+ extras never end up in that URI.
Extra (JSON)
A JSON object with the following recognized keys:
diff --git a/providers/apache/arrow/provider.yaml
b/providers/apache/arrow/provider.yaml
index 1325ef814d1..38415cbfdb0 100644
--- a/providers/apache/arrow/provider.yaml
+++ b/providers/apache/arrow/provider.yaml
@@ -54,8 +54,12 @@ connection-types:
hook-name: "ADBC Connection"
connection-type: adbc
ui-field-behaviour:
- hidden-fields:
- - port
- - schema
+ hidden-fields: []
relabeling:
- host: Connection URL
+ host: Host or URI
+ schema: Database
+ placeholders:
+ host: db.example.com, or postgresql://user:[email protected]:5432/db
+ port: "5432"
+ schema: db
+ extra: '{"dialect": "postgresql"}'
diff --git
a/providers/apache/arrow/src/airflow/providers/apache/arrow/get_provider_info.py
b/providers/apache/arrow/src/airflow/providers/apache/arrow/get_provider_info.py
index 1fe6cc5632c..172f685c434 100644
---
a/providers/apache/arrow/src/airflow/providers/apache/arrow/get_provider_info.py
+++
b/providers/apache/arrow/src/airflow/providers/apache/arrow/get_provider_info.py
@@ -47,8 +47,14 @@ def get_provider_info():
"hook-name": "ADBC Connection",
"connection-type": "adbc",
"ui-field-behaviour": {
- "hidden-fields": ["port", "schema"],
- "relabeling": {"host": "Connection URL"},
+ "hidden-fields": [],
+ "relabeling": {"host": "Host or URI", "schema":
"Database"},
+ "placeholders": {
+ "host": "db.example.com, or
postgresql://user:[email protected]:5432/db",
+ "port": "5432",
+ "schema": "db",
+ "extra": '{"dialect": "postgresql"}',
+ },
},
}
],
diff --git
a/providers/apache/arrow/src/airflow/providers/apache/arrow/hooks/adbc.py
b/providers/apache/arrow/src/airflow/providers/apache/arrow/hooks/adbc.py
index 84302941f10..0c3834be7e9 100644
--- a/providers/apache/arrow/src/airflow/providers/apache/arrow/hooks/adbc.py
+++ b/providers/apache/arrow/src/airflow/providers/apache/arrow/hooks/adbc.py
@@ -25,6 +25,7 @@ from contextlib import closing
from functools import cached_property, partial
from importlib import resources as importlib_resources
from typing import TYPE_CHECKING, Any
+from urllib.parse import quote
from adbc_driver_manager.dbapi import Connection, NotSupportedError, connect
from more_itertools import chunked
@@ -118,8 +119,14 @@ class AdbcHook(DbApiHook):
def get_ui_field_behaviour(cls) -> dict[str, Any]:
"""Get custom field behaviour."""
return {
- "hidden_fields": ["port", "schema"],
- "relabeling": {"host": "Connection URL"},
+ "hidden_fields": [],
+ "relabeling": {"host": "Host or URI", "schema": "Database"},
+ "placeholders": {
+ "host": "db.example.com, or
postgresql://user:[email protected]:5432/db",
+ "port": "5432",
+ "schema": "db",
+ "extra": '{"dialect": "postgresql"}',
+ },
}
@cached_property
@@ -149,14 +156,30 @@ class AdbcHook(DbApiHook):
@cached_property
def uri(self) -> str:
- host = self.connection.host
- if host and "::" in str(host):
- return str(host)
- uri = self.get_uri()
- return uri.replace(
- f"{self.conn_type.lower().replace('_', '-')}://",
- f"{self.dialect_name.lower().replace('_', '-')}://",
- )
+ """
+ Return the URI handed to the driver.
+
+ A host that already is a URI or a path
(``postgresql://user:pass@host:5432/db``,
+ ``file::memory:``, ``:memory:``, ``/data/app.db``) is passed through
unchanged. Otherwise the
+ URI is built as ``<dialect>://login:password@host:port/schema`` from
the connection's fields.
+ Airflow's own connection URI is of no use here: its query string
carries the extras, which
+ drivers such as libpq refuse as unknown parameters.
+ """
+ host = self.connection.host or ""
+ if "://" in host or "::" in host or host.startswith(("/", ":",
"file:")):
+ return host
+ uri = f"{self.dialect_name.lower().replace('_', '-')}://"
+ if self.connection.login:
+ uri += quote(self.connection.login, safe="")
+ if self.connection.password:
+ uri += f":{quote(self.connection.password, safe='')}"
+ uri += "@"
+ uri += host
+ if self.connection.port:
+ uri += f":{self.connection.port}"
+ if self.connection.schema:
+ uri += f"/{quote(self.connection.schema, safe='')}"
+ return uri
@cached_property
def driver(self) -> str:
diff --git a/providers/apache/arrow/tests/unit/apache/arrow/hooks/test_adbc.py
b/providers/apache/arrow/tests/unit/apache/arrow/hooks/test_adbc.py
index 32bb2f20e7f..f02082ca29f 100644
--- a/providers/apache/arrow/tests/unit/apache/arrow/hooks/test_adbc.py
+++ b/providers/apache/arrow/tests/unit/apache/arrow/hooks/test_adbc.py
@@ -377,6 +377,70 @@ class TestAdbcHook:
return AdbcHookWithRealGetConn()
+ @pytest.mark.parametrize(
+ ("connection", "expected"),
+ [
+ pytest.param(
+ Connection(
+ conn_id="adbc_default",
+ conn_type="adbc",
+ host="db.example.com",
+ login="user",
+ password="p@ss",
+ port=5432,
+ schema="db",
+ extra=json.dumps({"dialect": "postgresql"}),
+ ),
+ "postgresql://user:p%[email protected]:5432/db",
+ id="built-from-the-fields",
+ ),
+ pytest.param(
+ Connection(
+ conn_id="adbc_default",
+ conn_type="adbc",
+ host="db.example.com",
+ extra=json.dumps(
+ {"dialect": "postgresql", "conn_kwargs":
{"adbc.connection.read_only": "true"}}
+ ),
+ ),
+ "postgresql://db.example.com",
+ id="extras-stay-out-of-the-uri",
+ ),
+ pytest.param(
+ Connection(
+ conn_id="adbc_default",
+ conn_type="adbc",
+
host="postgresql://user:[email protected]:5432/db?sslmode=require",
+ extra=json.dumps({"dialect": "postgresql"}),
+ ),
+
"postgresql://user:[email protected]:5432/db?sslmode=require",
+ id="uri-in-the-host-field",
+ ),
+ pytest.param(
+ Connection(
+ conn_id="adbc_default",
+ conn_type="adbc",
+ host="file::memory:",
+ extra=json.dumps({"dialect": "sqlite"}),
+ ),
+ "file::memory:",
+ id="sqlite-in-memory",
+ ),
+ pytest.param(
+ Connection(
+ conn_id="adbc_default",
+ conn_type="adbc",
+ host="/data/app.db",
+ extra=json.dumps({"dialect": "sqlite"}),
+ ),
+ "/data/app.db",
+ id="sqlite-file",
+ ),
+ ],
+ )
+ def test_uri(self, connection, expected):
+ assert self._make_real_conn_hook(connection).uri == expected
+
@mock.patch("airflow.providers.apache.arrow.hooks.adbc.connect")
def test_get_conn_forwards_db_kwargs(self, mock_connect):
conn = Connection(