This is an automated email from the ASF dual-hosted git repository.
shahar1 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 45a93a1e1fb Make psycopg (v3) the default synchronous Postgres driver
in airflow-core (#69469)
45a93a1e1fb is described below
commit 45a93a1e1fbadcdf46648fe7658b61ea55fff9d3
Author: Dev-iL <[email protected]>
AuthorDate: Wed Jul 22 21:08:47 2026 +0300
Make psycopg (v3) the default synchronous Postgres driver in airflow-core
(#69469)
---
.../logging-monitoring/logging-tasks.rst | 2 +-
.../modules_management.rst | 2 +-
airflow-core/docs/core-concepts/multi-team.rst | 8 ++--
.../docs/howto/docker-compose/docker-compose.yaml | 4 +-
airflow-core/docs/howto/set-up-database.rst | 26 +++++++++++-
airflow-core/newsfragments/69469.improvement.rst | 1 +
airflow-core/src/airflow/configuration.py | 12 ++++--
...nconsistency_between_ORM_and_migration_files.py | 2 +-
...0_replace_deadline_inline_callback_with_fkey.py | 6 +--
airflow-core/src/airflow/models/serialized_dag.py | 6 ++-
airflow-core/src/airflow/settings.py | 7 ++--
.../unit/api_fastapi/common/test_exceptions.py | 20 ++++++++-
airflow-core/tests/unit/core/test_configuration.py | 38 +++++++++++++++--
.../tests/unit/core/test_sqlalchemy_config.py | 24 +++++++++++
airflow-e2e-tests/docker/openlineage-compat-db.yml | 48 ++++++++++++++++++++++
.../tests/airflow_e2e_tests/conftest.py | 9 +++-
.../tests/airflow_e2e_tests/constants.py | 5 +++
chart/values.yaml | 6 ++-
scripts/ci/docker-compose/backend-postgres.yml | 4 +-
task-sdk-integration-tests/docker-compose.yaml | 2 +-
20 files changed, 198 insertions(+), 34 deletions(-)
diff --git
a/airflow-core/docs/administration-and-deployment/logging-monitoring/logging-tasks.rst
b/airflow-core/docs/administration-and-deployment/logging-monitoring/logging-tasks.rst
index 3f2a70e49c9..e947fb5c9d8 100644
---
a/airflow-core/docs/administration-and-deployment/logging-monitoring/logging-tasks.rst
+++
b/airflow-core/docs/administration-and-deployment/logging-monitoring/logging-tasks.rst
@@ -150,7 +150,7 @@ the example below.
version | 2.9.0.dev0
executor | LocalExecutor
task_logging_handler |
airflow.utils.log.file_task_handler.FileTaskHandler
- sql_alchemy_conn |
postgresql+psycopg2://postgres:airflow@postgres/airflow
+ sql_alchemy_conn |
postgresql+psycopg://postgres:airflow@postgres/airflow
dags_folder | /files/dags
plugins_folder | /root/airflow/plugins
base_log_folder | /root/airflow/logs
diff --git
a/airflow-core/docs/administration-and-deployment/modules_management.rst
b/airflow-core/docs/administration-and-deployment/modules_management.rst
index 4ea73ea6fd7..62f3430da22 100644
--- a/airflow-core/docs/administration-and-deployment/modules_management.rst
+++ b/airflow-core/docs/administration-and-deployment/modules_management.rst
@@ -293,7 +293,7 @@ Below is the sample output of the ``airflow info`` command:
Config info
executor | LocalExecutor
task_logging_handler | airflow.utils.log.file_task_handler.FileTaskHandler
- sql_alchemy_conn |
postgresql+psycopg2://postgres:airflow@postgres/airflow
+ sql_alchemy_conn |
postgresql+psycopg://postgres:airflow@postgres/airflow
dags_folder | /files/dags
plugins_folder | /root/airflow/plugins
base_log_folder | /root/airflow/logs
diff --git a/airflow-core/docs/core-concepts/multi-team.rst
b/airflow-core/docs/core-concepts/multi-team.rst
index 6a9d8a2ec63..b45b0379b7f 100644
--- a/airflow-core/docs/core-concepts/multi-team.rst
+++ b/airflow-core/docs/core-concepts/multi-team.rst
@@ -486,7 +486,7 @@ is uppercase.
export AIRFLOW__TEAM_B___CELERY__BROKER_URL="redis://team-b-redis:6379/0"
# team_b's Celery result backend
- export
AIRFLOW__TEAM_B___CELERY__RESULT_BACKEND="db+postgresql+psycopg2://team-b-db/celery_results"
+ export
AIRFLOW__TEAM_B___CELERY__RESULT_BACKEND="db+postgresql+psycopg://team-b-db/celery_results"
Via Config File
"""""""""""""""
@@ -499,17 +499,17 @@ name followed by an equals sign:
# Global celery settings (used by the global executor, NOT as a fallback
for teams)
[celery]
broker_url = redis://default-redis:6379/0
- result_backend = db+postgresql+psycopg2://default-db/celery_results
+ result_backend = db+postgresql+psycopg://default-db/celery_results
# team_a overrides
[team_a=celery]
broker_url = redis://team-a-redis:6379/0
- result_backend = db+postgresql+psycopg2://team-a-db/celery_results
+ result_backend = db+postgresql+psycopg://team-a-db/celery_results
# team_b overrides
[team_b=celery]
broker_url = redis://team-b-redis:6379/0
- result_backend = db+postgresql+psycopg2://team-b-db/celery_results
+ result_backend = db+postgresql+psycopg://team-b-db/celery_results
Dag Bundle to Team Association
------------------------------
diff --git a/airflow-core/docs/howto/docker-compose/docker-compose.yaml
b/airflow-core/docs/howto/docker-compose/docker-compose.yaml
index 6a8891f3f58..b68f1afd3cc 100644
--- a/airflow-core/docs/howto/docker-compose/docker-compose.yaml
+++ b/airflow-core/docs/howto/docker-compose/docker-compose.yaml
@@ -57,8 +57,8 @@ x-airflow-common:
&airflow-common-env
AIRFLOW__CORE__EXECUTOR: CeleryExecutor
AIRFLOW__CORE__AUTH_MANAGER:
airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN:
postgresql+psycopg2://airflow:airflow@postgres/airflow
- AIRFLOW__CELERY__RESULT_BACKEND:
db+postgresql+psycopg2://airflow:airflow@postgres/airflow
+ AIRFLOW__DATABASE__SQL_ALCHEMY_CONN:
postgresql+psycopg://airflow:airflow@postgres/airflow
+ AIRFLOW__CELERY__RESULT_BACKEND:
db+postgresql+psycopg://airflow:airflow@postgres/airflow
AIRFLOW__CELERY__BROKER_URL: redis://:@redis:6379/0
AIRFLOW__CORE__FERNET_KEY: ${FERNET_KEY}
AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true'
diff --git a/airflow-core/docs/howto/set-up-database.rst
b/airflow-core/docs/howto/set-up-database.rst
index ec9604f2ccb..03e0a7a627d 100644
--- a/airflow-core/docs/howto/set-up-database.rst
+++ b/airflow-core/docs/howto/set-up-database.rst
@@ -197,12 +197,34 @@ in the Postgres documentation to learn more.
Details in the `SQLAlchemy Changelog
<https://docs.sqlalchemy.org/en/20/changelog/changelog_14.html#change-3687655465c25a39b968b4f5f6e9170b>`_.
-We recommend specifying a driver, such as ``psycopg2``, explicitly in your
SQLAlchemy connection string:
+We recommend specifying a driver, such as ``psycopg``, explicitly in your
SQLAlchemy connection string:
.. code-block:: text
postgresql+<driver>://<user>:<password>@<host>/<db>
+Sync SQLAlchemy engine and the sync driver
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``psycopg`` (psycopg3) is the default synchronous driver for the metadata
database. When ``sql_alchemy_conn`` omits a driver
+(a bare ``postgresql://`` scheme) or still uses the legacy ``postgres://`` /
``postgres+psycopg2://`` schemes, Airflow
+rewrites it to:
+
+.. code-block:: text
+
+ postgresql+psycopg://<user>:<password>@<host>/<db>
+
+If you need the old ``psycopg2`` driver, install the
``apache-airflow-providers-postgres[psycopg2]`` extra and set the
+connection string explicitly:
+
+.. code-block:: ini
+
+ [database]
+ sql_alchemy_conn = postgresql+psycopg2://<user>:<password>@<host>/<db>
+
+An explicit ``postgresql+psycopg2://`` URL is never rewritten. If ``psycopg2``
is not installed while such a URL is
+configured, Airflow fails loudly at connection time rather than silently
falling back to a different driver.
+
Async SQLAlchemy engine and the async driver
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -387,7 +409,7 @@ For instance, you can specify a database schema where
Airflow will create its re
.. code-block:: bash
- export
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN="postgresql+psycopg2://postgres@localhost:5432/my_database?options=-csearch_path%3Dairflow"
+ export
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN="postgresql+psycopg://postgres@localhost:5432/my_database?options=-csearch_path%3Dairflow"
export AIRFLOW__DATABASE__SQL_ALCHEMY_SCHEMA="airflow"
Note the ``search_path`` at the end of the ``SQL_ALCHEMY_CONN`` database URL.
diff --git a/airflow-core/newsfragments/69469.improvement.rst
b/airflow-core/newsfragments/69469.improvement.rst
new file mode 100644
index 00000000000..2260dc1287d
--- /dev/null
+++ b/airflow-core/newsfragments/69469.improvement.rst
@@ -0,0 +1 @@
+``psycopg`` (psycopg3) is now the default synchronous Postgres driver for the
metadata database, mirroring the existing async default; a bare
``postgresql://`` or legacy ``postgres://``/``postgres+psycopg2://``
``sql_alchemy_conn`` is now rewritten to ``postgresql+psycopg://`` instead of
``postgresql+psycopg2://``, while an explicit ``postgresql+psycopg2://``
connection string is left untouched (install the
``apache-airflow-providers-postgres[psycopg2]`` extra to keep using psycopg2).
diff --git a/airflow-core/src/airflow/configuration.py
b/airflow-core/src/airflow/configuration.py
index d43eb715d32..4eb92054cea 100644
--- a/airflow-core/src/airflow/configuration.py
+++ b/airflow-core/src/airflow/configuration.py
@@ -28,6 +28,7 @@ import warnings
from base64 import b64encode
from collections.abc import Callable
from configparser import ConfigParser
+from importlib.util import find_spec
from inspect import ismodule
from io import StringIO
from re import Pattern
@@ -372,13 +373,18 @@ class AirflowConfigParser(_SharedAirflowConfigParser):
Upgrade SQL schemas.
As of SQLAlchemy 1.4, schemes `postgres+psycopg2` and `postgres`
- must be replaced with `postgresql+psycopg2`. The bare `postgresql`
- scheme is also upgraded to make the psycopg2 driver explicit.
+ must be replaced with `postgresql+psycopg` if the psycopg (v3) driver
+ is installed, or `postgresql+psycopg2` otherwise. The bare `postgresql`
+ scheme is upgraded the same way to make the driver explicit.
"""
section, key = "database", "sql_alchemy_conn"
old_value = self.get(section, key, _extra_stacklevel=1)
bad_schemes = ["postgres+psycopg2", "postgres", "postgresql"]
- good_scheme = "postgresql+psycopg2"
+ # The provider-side hooks (common.sql / postgres / amazon) also gate
psycopg (v3) on an
+ # ``_is_sqlalchemy_2()`` check, because they support Airflow 2.11 on
SQLAlchemy 1.4, which has
+ # no native ``postgresql+psycopg`` dialect. airflow-core pins
``sqlalchemy>=2.0``, so that
+ # dialect is always present when the package is importable —
``find_spec`` alone suffices here.
+ good_scheme = "postgresql+psycopg" if find_spec("psycopg") is not None
else "postgresql+psycopg2"
parsed = urlsplit(old_value)
if parsed.scheme in bad_schemes:
warnings.warn(
diff --git
a/airflow-core/src/airflow/migrations/versions/0017_2_9_2_fix_inconsistency_between_ORM_and_migration_files.py
b/airflow-core/src/airflow/migrations/versions/0017_2_9_2_fix_inconsistency_between_ORM_and_migration_files.py
index fa24916df6f..64531a957b3 100644
---
a/airflow-core/src/airflow/migrations/versions/0017_2_9_2_fix_inconsistency_between_ORM_and_migration_files.py
+++
b/airflow-core/src/airflow/migrations/versions/0017_2_9_2_fix_inconsistency_between_ORM_and_migration_files.py
@@ -110,7 +110,7 @@ def upgrade():
batch_op.create_unique_constraint(batch_op.f("connection_conn_id_uq"),
["conn_id"])
max_cons = sa.table("dag", sa.column("max_consecutive_failed_dag_runs"))
-
op.execute(max_cons.update().values(max_consecutive_failed_dag_runs=literal("0")))
+
op.execute(max_cons.update().values(max_consecutive_failed_dag_runs=literal(0)))
with op.batch_alter_table("dag") as batch_op:
batch_op.alter_column("max_consecutive_failed_dag_runs",
existing_type=sa.Integer(), nullable=False)
diff --git
a/airflow-core/src/airflow/migrations/versions/0094_3_2_0_replace_deadline_inline_callback_with_fkey.py
b/airflow-core/src/airflow/migrations/versions/0094_3_2_0_replace_deadline_inline_callback_with_fkey.py
index 0cab3d57c25..bf1f6651092 100644
---
a/airflow-core/src/airflow/migrations/versions/0094_3_2_0_replace_deadline_inline_callback_with_fkey.py
+++
b/airflow-core/src/airflow/migrations/versions/0094_3_2_0_replace_deadline_inline_callback_with_fkey.py
@@ -92,7 +92,7 @@ def _upgrade_postgresql(conn, batch_size):
'__var', json_build_object(
'path', b.cb_path,
'kwargs', b.cb_kwargs,
- 'prefix', :prefix,
+ 'prefix', CAST(:prefix AS text),
'dag_id', b.dag_id
),
'__type', 'dict'
@@ -325,7 +325,7 @@ def _downgrade_postgresql(conn, batch_size):
'path', c.data::jsonb->'__var'->>'path',
'kwargs', c.data::jsonb->'__var'->'kwargs'
),
- '__classname__', :classname,
+ '__classname__', CAST(:classname AS text),
'__version__', 0
)::json,
callback_state = CASE
@@ -360,7 +360,7 @@ def _downgrade_postgresql(conn, batch_size):
'path', c.data::jsonb->'__var'->>'path',
'kwargs', c.data::jsonb->'__var'->'kwargs'
),
- '__classname__', :classname,
+ '__classname__', CAST(:classname AS text),
'__version__', 0
)::json,
callback_state = CASE
diff --git a/airflow-core/src/airflow/models/serialized_dag.py
b/airflow-core/src/airflow/models/serialized_dag.py
index fbff450a993..9dda74d42b5 100644
--- a/airflow-core/src/airflow/models/serialized_dag.py
+++ b/airflow-core/src/airflow/models/serialized_dag.py
@@ -28,7 +28,7 @@ from uuid import UUID
import uuid6
from sqlalchemy import JSON, ForeignKey, LargeBinary, String, Uuid, exists,
select, tuple_, update
-from sqlalchemy.dialects.postgresql import JSONB
+from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, backref, foreign, mapped_column,
relationship
from sqlalchemy.sql.expression import func, literal
@@ -955,7 +955,9 @@ class SerializedDagModel(Base):
elif dialect == "postgresql":
# Use #> operator which works for both JSON and JSONB types
# Returns the JSON sub-object at the specified path
- data_col_to_select =
cls._data.op("#>")(literal('{"dag","dag_dependencies"}'))
+ data_col_to_select = cls._data.op("#>")(
+ literal(["dag", "dag_dependencies"], type_=ARRAY(String))
+ )
load_json = lambda x: x
else:
data_col_to_select = func.json_extract_path(cls._data, "dag",
"dag_dependencies")
diff --git a/airflow-core/src/airflow/settings.py
b/airflow-core/src/airflow/settings.py
index 54150dd8bfd..0a0e1c66197 100644
--- a/airflow-core/src/airflow/settings.py
+++ b/airflow-core/src/airflow/settings.py
@@ -524,15 +524,16 @@ def _is_sqlite_in_memory(url: str) -> bool:
def prepare_engine_args(disable_connection_pool=False, pool_class=None):
"""Prepare SQLAlchemy engine args."""
+ use_psycopg2_tuning = SQL_ALCHEMY_CONN.startswith("postgresql+psycopg2")
DEFAULT_ENGINE_ARGS: dict[str, dict[str, Any]] = {
"postgresql": (
{
"insertmanyvalues_page_size": 10000,
}
| (
- {}
- if _USE_PSYCOPG3
- else {"executemany_mode": "values_plus_batch",
"executemany_batch_page_size": 2000}
+ {"executemany_mode": "values_plus_batch",
"executemany_batch_page_size": 2000}
+ if use_psycopg2_tuning
+ else {}
)
)
}
diff --git a/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py
b/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py
index c38cbabb5e1..def8a0abd30 100644
--- a/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py
+++ b/airflow-core/tests/unit/api_fastapi/common/test_exceptions.py
@@ -277,7 +277,22 @@ class TestUniqueConstraintErrorHandler:
exeinfo_response_error.value.detail.pop("message", None) # type:
ignore[attr-defined]
assert exeinfo_response_error.value.status_code ==
expected_exception.status_code
- assert exeinfo_response_error.value.detail == expected_exception.detail
+
+ # The exact rendered SQL (e.g. explicit driver-added type casts) and
trailing whitespace in
+ # the driver's own error text are driver-specific rendering details,
not meaningful content —
+ # match on the statement's target table and normalize trailing
newlines instead of an exact
+ # string compare.
+ response_detail = exeinfo_response_error.value.detail
+ expected_detail = expected_exception.detail
+ actual_statement = response_detail.pop("statement", None) # type:
ignore[attr-defined]
+ expected_statement = expected_detail.pop("statement", None)
+ response_detail["orig_error"] =
response_detail["orig_error"].rstrip("\n") # type: ignore[index]
+ expected_detail["orig_error"] =
expected_detail["orig_error"].rstrip("\n")
+
+ assert response_detail == expected_detail
+ assert expected_statement is not None
+ assert actual_statement is not None
+ assert f"INSERT INTO {expected_statement.split()[2]}" in
actual_statement
@patch("airflow.api_fastapi.common.exceptions.get_random_string",
return_value=MOCKED_ID)
@conf_vars({("api", "expose_stacktrace"): "False"})
@@ -391,6 +406,9 @@ class TestUniqueConstraintErrorHandler:
# Removes the stacktrace from response to remove during comparison.
response_detail.pop("message", None) # type: ignore[attr-defined]
+ # Trailing whitespace in the driver's own error text is a
driver-specific rendering detail.
+ response_detail["orig_error"] =
response_detail["orig_error"].rstrip("\n") # type: ignore[index]
+ expected_detail["orig_error"] =
expected_detail["orig_error"].rstrip("\n")
assert response_detail == expected_detail
assert "INSERT INTO dag_run" in actual_statement
assert exeinfo_response_error.value.detail == expected_exception.detail
diff --git a/airflow-core/tests/unit/core/test_configuration.py
b/airflow-core/tests/unit/core/test_configuration.py
index fc44c398e8e..11d1809fe9f 100644
--- a/airflow-core/tests/unit/core/test_configuration.py
+++ b/airflow-core/tests/unit/core/test_configuration.py
@@ -1124,16 +1124,16 @@ class TestConf:
("input_scheme", "expected_scheme"),
[
pytest.param(
- "postgres://user:pass@host/db",
"postgresql+psycopg2://user:pass@host/db", id="postgres"
+ "postgres://user:pass@host/db",
"postgresql+psycopg://user:pass@host/db", id="postgres"
),
pytest.param(
"postgres+psycopg2://user:pass@host/db",
- "postgresql+psycopg2://user:pass@host/db",
+ "postgresql+psycopg://user:pass@host/db",
id="postgres+psycopg2",
),
pytest.param(
"postgresql://user:pass@host/db",
- "postgresql+psycopg2://user:pass@host/db",
+ "postgresql+psycopg://user:pass@host/db",
id="postgresql-bare",
),
pytest.param(
@@ -1145,7 +1145,37 @@ class TestConf:
],
)
@mock.patch.dict("os.environ", {}, clear=False)
- def test_upgrade_postgres_metastore_conn(self, input_scheme,
expected_scheme):
+ @mock.patch("airflow.configuration.find_spec", return_value=object())
+ def test_upgrade_postgres_metastore_conn(self, mock_find_spec,
input_scheme, expected_scheme):
+ """Assumes psycopg (v3) is installed; see the sibling
``_without_psycopg3`` test for the
+ fallback, since whether psycopg3 is actually present varies across CI
environments
+ (e.g. a lowest-dependencies job that doesn't install it)."""
+ os.environ["AIRFLOW__DATABASE__SQL_ALCHEMY_CONN"] = input_scheme
+ test_conf = AirflowConfigParser()
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", FutureWarning)
+ test_conf._upgrade_postgres_metastore_conn()
+ assert test_conf.get("database", "sql_alchemy_conn") == expected_scheme
+
+ @pytest.mark.parametrize(
+ ("input_scheme", "expected_scheme"),
+ [
+ pytest.param(
+ "postgres://user:pass@host/db",
"postgresql+psycopg2://user:pass@host/db", id="postgres"
+ ),
+ pytest.param(
+ "postgresql+psycopg2://user:pass@host/db",
+ "postgresql+psycopg2://user:pass@host/db",
+ id="postgresql+psycopg2-noop",
+ ),
+ ],
+ )
+ @mock.patch.dict("os.environ", {}, clear=False)
+ @mock.patch("airflow.configuration.find_spec", return_value=None)
+ def test_upgrade_postgres_metastore_conn_without_psycopg3(
+ self, mock_find_spec, input_scheme, expected_scheme
+ ):
+ """Falls back to psycopg2 when the psycopg (v3) package isn't
installed."""
os.environ["AIRFLOW__DATABASE__SQL_ALCHEMY_CONN"] = input_scheme
test_conf = AirflowConfigParser()
with warnings.catch_warnings():
diff --git a/airflow-core/tests/unit/core/test_sqlalchemy_config.py
b/airflow-core/tests/unit/core/test_sqlalchemy_config.py
index db211bab2c5..ab364dd9a7b 100644
--- a/airflow-core/tests/unit/core/test_sqlalchemy_config.py
+++ b/airflow-core/tests/unit/core/test_sqlalchemy_config.py
@@ -121,6 +121,30 @@ class TestSqlAlchemySettings:
assert "pool_recycle" not in engine_args
assert "pool_pre_ping" not in engine_args
+ @pytest.mark.parametrize(
+ ("conn_str", "use_psycopg3", "expect_psycopg2_tuning"),
+ [
+ ("postgresql+psycopg2://user:pass@host/db", True, True),
+ ("postgresql+psycopg2://user:pass@host/db", False, True),
+ ("postgresql+psycopg://user:pass@host/db", True, False),
+ ("postgresql+psycopg://user:pass@host/db", False, False),
+ ],
+ )
+ def test_prepare_engine_args_psycopg2_tuning_follows_configured_scheme(
+ self, conn_str, use_psycopg3, expect_psycopg2_tuning, monkeypatch
+ ):
+ """executemany tuning must follow the configured scheme, not whether
psycopg happens to be importable."""
+ monkeypatch.setattr(settings, "SQL_ALCHEMY_CONN", conn_str)
+ monkeypatch.setattr(settings, "_USE_PSYCOPG3", use_psycopg3)
+ engine_args = settings.prepare_engine_args()
+ assert engine_args["insertmanyvalues_page_size"] == 10000
+ if expect_psycopg2_tuning:
+ assert engine_args["executemany_mode"] == "values_plus_batch"
+ assert engine_args["executemany_batch_page_size"] == 2000
+ else:
+ assert "executemany_mode" not in engine_args
+ assert "executemany_batch_page_size" not in engine_args
+
@patch("airflow.settings.setup_event_handlers")
@patch("airflow.settings.scoped_session")
@patch("airflow.settings.sessionmaker")
diff --git a/airflow-e2e-tests/docker/openlineage-compat-db.yml
b/airflow-e2e-tests/docker/openlineage-compat-db.yml
new file mode 100644
index 00000000000..01c9e68e2fd
--- /dev/null
+++ b/airflow-e2e-tests/docker/openlineage-compat-db.yml
@@ -0,0 +1,48 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Docker Compose override for the openlineage E2E compat matrix
(``--airflow-version``).
+#
+# docker-compose.yaml points sql_alchemy_conn/result_backend at the
``psycopg`` (v3) driver, matching
+# the current default. The compat matrix runs unmodified released
``apache/airflow:<version>`` images
+# for versions that only ever bundled psycopg2 by default (no psycopg3
provider dependency existed
+# yet), and does not reinstall the postgres provider on top of them — so the
psycopg3 dialect can't
+# load there. Pin the explicit psycopg2 scheme these images actually support.
+---
+x-airflow-common-compat-db-env: &airflow-common-compat-db-env
+ AIRFLOW__DATABASE__SQL_ALCHEMY_CONN:
postgresql+psycopg2://airflow:airflow@postgres/airflow
+ AIRFLOW__CELERY__RESULT_BACKEND:
db+postgresql+psycopg2://airflow:airflow@postgres/airflow
+
+services:
+ airflow-apiserver:
+ environment:
+ <<: *airflow-common-compat-db-env
+ airflow-scheduler:
+ environment:
+ <<: *airflow-common-compat-db-env
+ airflow-dag-processor:
+ environment:
+ <<: *airflow-common-compat-db-env
+ airflow-triggerer:
+ environment:
+ <<: *airflow-common-compat-db-env
+ airflow-worker:
+ environment:
+ <<: *airflow-common-compat-db-env
+ airflow-init:
+ environment:
+ <<: *airflow-common-compat-db-env
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
index b97add595fd..5b45974f053 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
@@ -56,6 +56,7 @@ from airflow_e2e_tests.constants import (
LOCALSTACK_PATH,
LOGS_FOLDER,
NODE_IMAGE,
+ OPENLINEAGE_COMPAT_DB_COMPOSE_PATH,
OPENLINEAGE_COMPOSE_PATH,
OPENSEARCH_PATH,
PROVIDERS_MOUNT_CONTAINER_PATH,
@@ -630,7 +631,7 @@ def _setup_go_sdk_integration(dot_env_file, tmp_dir):
os.environ["ENV_FILE_PATH"] = str(dot_env_file)
-def _setup_openlineage_integration(dot_env_file, tmp_dir):
+def _setup_openlineage_integration(dot_env_file, tmp_dir, compose_file_names):
"""Set up the openlineage E2E test mode.
The OpenLineage system-test DAGs are the single source of truth;
``prepare_dags`` copies them
@@ -644,6 +645,10 @@ def _setup_openlineage_integration(dot_env_file, tmp_dir):
prepare_dags(tmp_dir / "dags")
copyfile(OPENLINEAGE_COMPOSE_PATH, tmp_dir / "openlineage.yml")
+ if os.environ.get("E2E_TARGET_AIRFLOW_VERSION", "").strip():
+ copyfile(OPENLINEAGE_COMPAT_DB_COMPOSE_PATH, tmp_dir /
"openlineage-compat-db.yml")
+ compose_file_names.append("openlineage-compat-db.yml")
+
def _build_ts_sdk_example_bundle(*, native=False):
build_commands = (
@@ -784,7 +789,7 @@ def spin_up_airflow_environment(tmp_path_factory:
pytest.TempPathFactory):
_setup_go_sdk_integration(dot_env_file, tmp_dir)
elif E2E_TEST_MODE == "openlineage":
compose_file_names.append("openlineage.yml")
- _setup_openlineage_integration(dot_env_file, tmp_dir)
+ _setup_openlineage_integration(dot_env_file, tmp_dir,
compose_file_names)
elif E2E_TEST_MODE == "ts_sdk":
compose_file_names.append("ts.yml")
_setup_ts_sdk_integration(dot_env_file, tmp_dir)
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
index e389a8bc6b9..cbba1dde33f 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
@@ -53,6 +53,11 @@ KAFKA_DIR_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" /
"docker" / "kafka"
# OpenLineage E2E test paths. The DAGs are sourced from the provider system
tests at runtime by
# openlineage_tests/prepare_dags.py; the overlay carries the
OpenLineage-specific env + dag_doc mount.
OPENLINEAGE_COMPOSE_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker"
/ "openlineage.yml"
+# Pins the metadata-DB driver back to psycopg2 for the compat matrix
(--airflow-version), whose
+# released base images predate the psycopg3 default; see
openlineage-compat-db.yml for details.
+OPENLINEAGE_COMPAT_DB_COMPOSE_PATH = (
+ AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" /
"openlineage-compat-db.yml"
+)
# CI sets this (the same switch the lang-SDK k8s job uses) to build the
lang-SDK
# artifacts with the host toolchain instead of ephemeral toolchain containers.
diff --git a/chart/values.yaml b/chart/values.yaml
index 06330ed41ed..a1c057239d8 100644
--- a/chart/values.yaml
+++ b/chart/values.yaml
@@ -445,12 +445,14 @@ data:
# connection: base64_encoded_connection_string
#
# The 'connection' key is base64-encoded SQLAlchemy connection string, e.g.:
- # postgresql+psycopg2://airflow:password@postgres/airflow
+ # postgresql+psycopg://airflow:password@postgres/airflow (Airflow
>=3.4.0)
+ # postgresql+psycopg2://airflow:password@postgres/airflow (Airflow
<3.4.0, or with the [psycopg2] extra)
metadataSecretName: ~
# If not set, falls back to metadataSecretName. The secret must contain
'connection' key which is
# a base64-encoded connection string, e.g.:
- # postgresql+psycopg2://user:password@host/db
+ # postgresql+psycopg://user:password@host/db (Airflow >=3.4.0)
+ # postgresql+psycopg2://user:password@host/db (Airflow <3.4.0, or with
the [psycopg2] extra)
resultBackendSecretName: ~
brokerUrlSecretName: ~
diff --git a/scripts/ci/docker-compose/backend-postgres.yml
b/scripts/ci/docker-compose/backend-postgres.yml
index d2971bc1e62..b1a64f16a01 100644
--- a/scripts/ci/docker-compose/backend-postgres.yml
+++ b/scripts/ci/docker-compose/backend-postgres.yml
@@ -19,8 +19,8 @@ services:
airflow:
environment:
- BACKEND=postgres
- -
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://postgres:airflow@postgres/airflow
- -
AIRFLOW__CELERY__RESULT_BACKEND=db+postgresql+psycopg2://postgres:airflow@postgres/airflow
+ -
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql://postgres:airflow@postgres/airflow
+ -
AIRFLOW__CELERY__RESULT_BACKEND=db+postgresql+psycopg://postgres:airflow@postgres/airflow
depends_on:
postgres:
condition: service_healthy
diff --git a/task-sdk-integration-tests/docker-compose.yaml
b/task-sdk-integration-tests/docker-compose.yaml
index 82143e98b5e..b71de00d27e 100644
--- a/task-sdk-integration-tests/docker-compose.yaml
+++ b/task-sdk-integration-tests/docker-compose.yaml
@@ -27,7 +27,7 @@ x-airflow-common:
# yamllint disable rule:line-length
AIRFLOW__CORE__AUTH_MANAGER:
'airflow.api_fastapi.auth.managers.simple.simple_auth_manager.SimpleAuthManager'
AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_ALL_ADMINS: 'true'
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN:
postgresql+psycopg2://airflow:airflow@postgres/airflow
+ AIRFLOW__DATABASE__SQL_ALCHEMY_CONN:
postgresql+psycopg://airflow:airflow@postgres/airflow
AIRFLOW__CORE__FERNET_KEY: ${FERNET_KEY}
AIRFLOW__CORE__LOAD_EXAMPLES: 'false'
AIRFLOW__CORE__DAGS_FOLDER: '/opt/airflow/dags'