This is an automated email from the ASF dual-hosted git repository.
ash pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-0-test by this push:
new 5c0781193e8 [v3-0-test] Allow downgrading to 2.11 from 3.x (#54371)
(#54399)
5c0781193e8 is described below
commit 5c0781193e86a78cef460ccf2b8492a023dd1fcc
Author: Ash Berlin-Taylor <[email protected]>
AuthorDate: Wed Aug 13 10:01:09 2025 +0100
[v3-0-test] Allow downgrading to 2.11 from 3.x (#54371) (#54399)
---
.../src/airflow/cli/commands/db_command.py | 30 ++++++++++++++--------
airflow-core/src/airflow/utils/db.py | 28 ++++++++++----------
airflow-core/src/airflow/utils/db_manager.py | 6 -----
.../tests/unit/cli/commands/test_db_command.py | 16 ++++++++++++
airflow-core/tests/unit/utils/test_db.py | 14 ----------
airflow-core/tests/unit/utils/test_db_manager.py | 22 ++--------------
devel-common/src/tests_common/test_utils/db.py | 22 ++++++++++++++--
.../providers/fab/auth_manager/models/db.py | 5 ++++
8 files changed, 77 insertions(+), 66 deletions(-)
diff --git a/airflow-core/src/airflow/cli/commands/db_command.py
b/airflow-core/src/airflow/cli/commands/db_command.py
index 5a4f70cc604..f267f598b02 100644
--- a/airflow-core/src/airflow/cli/commands/db_command.py
+++ b/airflow-core/src/airflow/cli/commands/db_command.py
@@ -50,29 +50,39 @@ def resetdb(args):
db.resetdb(skip_init=args.skip_init)
-def _get_version_revision(
- version: str, recursion_limit: int = 10, revision_heads_map: dict[str,
str] | None = None
-) -> str | None:
+def _get_version_revision(version: str, revision_heads_map: dict[str, str] |
None = None) -> str | None:
"""
- Recursively search for the revision of the given version in
revision_heads_map.
+ Search for the revision of the given version in revision_heads_map.
This searches given revision_heads_map for the revision of the given
version, recursively
searching for the previous version if the given version is not found.
+
+ ``revision_heads_map`` must already be sorted in the dict in ascending
order for this function to work. No
+ checks are made that this is true
"""
if revision_heads_map is None:
revision_heads_map = _REVISION_HEADS_MAP
+ # Exact match found, we can just return it
if version in revision_heads_map:
return revision_heads_map[version]
+
try:
- major, minor, patch = map(int, version.split("."))
+ wanted = tuple(map(int, version.split(".")))
except ValueError:
return None
- new_version = f"{major}.{minor}.{patch - 1}"
- recursion_limit -= 1
- if recursion_limit <= 0:
- # Prevent infinite recursion as I can't imagine 10 successive versions
without migration
+
+ # Else, we walk backwards in the revision map until we find a version that
is < the target
+ for revision, head in reversed(revision_heads_map.items()):
+ try:
+ current = tuple(map(int, revision.split(".")))
+ except ValueError:
+ log.debug("Unable to parse HEAD revision", exc_info=True)
+ return None
+
+ if current < wanted:
+ return head
+ else:
return None
- return _get_version_revision(new_version, recursion_limit)
def run_db_migrate_command(args, command, revision_heads_map: dict[str, str]):
diff --git a/airflow-core/src/airflow/utils/db.py
b/airflow-core/src/airflow/utils/db.py
index f091f0868b0..a0dcb5fdf03 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -1211,21 +1211,21 @@ def downgrade(*, to_revision, from_revision=None,
show_sql_only=False, session:
config = _get_alembic_config()
# Check if downgrade is less than 3.0.0 and requires that `ab_user` fab
table is present
if _revision_greater(config, _REVISION_HEADS_MAP["2.10.3"], to_revision):
- unitest_mode = conf.getboolean("core", "unit_test_mode")
- if unitest_mode:
- try:
- from airflow.providers.fab.auth_manager.models.db import
FABDBManager
-
- dbm = FABDBManager(session)
- dbm.initdb()
- except ImportError:
- log.warning("Import error occurred while importing
FABDBManager. Skipping the check.")
- return
- if not inspect(settings.engine).has_table("ab_user") and not
unitest_mode:
- raise AirflowException(
- "Downgrade to revision less than 3.0.0 requires that `ab_user`
table is present. "
- "Please add FabDBManager to [core] external_db_managers and
run fab migrations before proceeding"
+ try:
+ from airflow.providers.fab.auth_manager.models.db import
FABDBManager
+ except ImportError:
+ # Raise the error with a new message
+ raise RuntimeError(
+ "Import error occurred while importing FABDBManager. We need
that to exist before we can "
+ "downgrade to <3.0.0"
)
+ dbm = FABDBManager(session)
+ if hasattr(dbm, "reset_to_2_x"):
+ dbm.reset_to_2_x()
+ else:
+ # Older version before we added that function, it only has a
single migration so we can just
+ # created
+ dbm.create_db_from_orm()
with create_global_lock(session=session, lock=DBLocks.MIGRATIONS):
if show_sql_only:
log.warning("Generating sql scripts for manual migration.")
diff --git a/airflow-core/src/airflow/utils/db_manager.py
b/airflow-core/src/airflow/utils/db_manager.py
index 6483c446f01..a11c4cdad25 100644
--- a/airflow-core/src/airflow/utils/db_manager.py
+++ b/airflow-core/src/airflow/utils/db_manager.py
@@ -213,12 +213,6 @@ class RunDBManager(LoggingMixin):
m = manager(session)
m.upgradedb()
- def downgrade(self, session):
- """Downgrade the external database managers."""
- for manager in self._managers:
- m = manager(session)
- m.downgrade()
-
def drop_tables(self, session, connection):
"""Drop the external database managers."""
for manager in self._managers:
diff --git a/airflow-core/tests/unit/cli/commands/test_db_command.py
b/airflow-core/tests/unit/cli/commands/test_db_command.py
index e6d1b9bd6d9..b9f5e0f04d0 100644
--- a/airflow-core/tests/unit/cli/commands/test_db_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_db_command.py
@@ -624,3 +624,19 @@ class TestCLIDBClean:
)
db_command.drop_archived(args)
mock_drop_archived_records.assert_called_once_with(table_names=None,
needs_confirm=expected)
+
+
+def test_get_version_revision():
+ heads: dict[str, str] = {
+ "2.10.0": "22ed7efa9da2",
+ "2.10.3": "5f2621c13b39",
+ "3.0.0": "29ce7909c52b",
+ "3.0.3": "fe199e1abd77",
+ "3.1.0": "808787349f22",
+ }
+
+ assert db_command._get_version_revision("3.1.0", heads) == "808787349f22"
+ assert db_command._get_version_revision("3.1.1", heads) == "808787349f22"
+ assert db_command._get_version_revision("2.11.1", heads) == "5f2621c13b39"
+ assert db_command._get_version_revision("2.10.1", heads) == "22ed7efa9da2"
+ assert db_command._get_version_revision("2.0.0", heads) is None
diff --git a/airflow-core/tests/unit/utils/test_db.py
b/airflow-core/tests/unit/utils/test_db.py
index cf692750253..b17c523ce27 100644
--- a/airflow-core/tests/unit/utils/test_db.py
+++ b/airflow-core/tests/unit/utils/test_db.py
@@ -34,12 +34,10 @@ from alembic.runtime.environment import EnvironmentContext
from alembic.script import ScriptDirectory
from sqlalchemy import Column, Integer, MetaData, Table, select
-from airflow.exceptions import AirflowException
from airflow.models import Base as airflow_base
from airflow.providers.fab.auth_manager.models.db import FABDBManager
from airflow.settings import engine
from airflow.utils.db import (
- _REVISION_HEADS_MAP,
LazySelectSequence,
_get_alembic_config,
check_migrations,
@@ -293,15 +291,3 @@ class TestDb:
lss = LazySelectSequence.from_select(select(t.c.id), order_by=[],
session=MockSession())
assert bool(lss) is False
-
- @conf_vars({("core", "unit_test_mode"): "False"})
- @mock.patch("airflow.utils.db.inspect")
- def test_downgrade_raises_if_lower_than_v3_0_0_and_no_ab_user(self,
mock_inspect):
- mock_inspect.return_value.has_table.return_value = False
- msg = (
- "Downgrade to revision less than 3.0.0 requires that `ab_user`
table is present. "
- "Please add FabDBManager to [core] external_db_managers and run
fab migrations before "
- "proceeding"
- )
- with pytest.raises(AirflowException, match=re.escape(msg)):
- downgrade(to_revision=_REVISION_HEADS_MAP["2.7.0"])
diff --git a/airflow-core/tests/unit/utils/test_db_manager.py
b/airflow-core/tests/unit/utils/test_db_manager.py
index 075e87c0cbe..501fa02ec38 100644
--- a/airflow-core/tests/unit/utils/test_db_manager.py
+++ b/airflow-core/tests/unit/utils/test_db_manager.py
@@ -23,7 +23,7 @@ from sqlalchemy import Table
from airflow.exceptions import AirflowException
from airflow.models import Base
-from airflow.utils.db import downgrade, initdb
+from airflow.utils.db import initdb
from airflow.utils.db_manager import BaseDBManager, RunDBManager
from tests_common.test_utils.config import conf_vars
@@ -58,27 +58,12 @@ class TestRunDBManager:
run_db_manager.validate()
metadata._remove_table("dag_run", None)
- @mock.patch.object(RunDBManager, "downgrade")
@mock.patch.object(RunDBManager, "upgradedb")
@mock.patch.object(RunDBManager, "initdb")
- def test_init_db_calls_rundbmanager(self, mock_initdb, mock_upgrade_db,
mock_downgrade_db, session):
+ def test_init_db_calls_rundbmanager(self, mock_initdb, mock_upgrade_db,
session):
initdb(session=session)
mock_initdb.assert_called()
mock_initdb.assert_called_once_with(session)
- mock_downgrade_db.assert_not_called()
-
- @mock.patch.object(RunDBManager, "downgrade")
- @mock.patch.object(RunDBManager, "upgradedb")
- @mock.patch.object(RunDBManager, "initdb")
- @mock.patch("alembic.command")
- def test_downgrade_dont_call_rundbmanager(
- self, mock_alembic_command, mock_initdb, mock_upgrade_db,
mock_downgrade_db, session
- ):
- downgrade(to_revision="base")
- mock_alembic_command.downgrade.assert_called_once_with(mock.ANY,
revision="base", sql=False)
- mock_upgrade_db.assert_not_called()
- mock_initdb.assert_not_called()
- mock_downgrade_db.assert_not_called()
@conf_vars(
{("database", "external_db_managers"):
"airflow.providers.fab.auth_manager.models.db.FABDBManager"}
@@ -94,9 +79,6 @@ class TestRunDBManager:
# upgradedb
ext_db.upgradedb(session=session)
fabdb_manager.upgradedb.assert_called_once()
- # downgrade
- ext_db.downgrade(session=session)
- mock_fabdb_manager.return_value.downgrade.assert_called_once()
connection = mock.MagicMock()
ext_db.drop_tables(session, connection)
mock_fabdb_manager.return_value.drop_tables.assert_called_once_with(connection)
diff --git a/devel-common/src/tests_common/test_utils/db.py
b/devel-common/src/tests_common/test_utils/db.py
index 4ab88d1c75e..8b3b85c33b6 100644
--- a/devel-common/src/tests_common/test_utils/db.py
+++ b/devel-common/src/tests_common/test_utils/db.py
@@ -17,6 +17,7 @@
# under the License.
from __future__ import annotations
+import os
from typing import TYPE_CHECKING
from airflow.configuration import conf
@@ -92,9 +93,26 @@ def initial_db_init():
from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS
db.resetdb()
+
if AIRFLOW_V_3_0_PLUS:
- db.downgrade(to_revision="5f2621c13b39")
- db.upgradedb(to_revision="head")
+ try:
+ from airflow.providers.fab.auth_manager.models.db import
FABDBManager
+ except ModuleNotFoundError:
+ # Reasons it might fail: we're in a provider bundle without FAB,
or we're on a version of Python
+ # where FAB isn't yet supported
+ pass
+ else:
+ if os.getenv("TEST_GROUP") != "providers":
+ # If we loaded the provider, and we're running core (or
running via breeze where TEST_GROUP
+ # isn't specified) run the downgrade+upgrade to ensure
migrations are in sync with Model
+ # classes
+ db.downgrade(to_revision="5f2621c13b39")
+ db.upgradedb(to_revision="head")
+ else:
+ # Just create the tables so they are there
+ with create_session() as session:
+ FABDBManager(session).create_db_from_orm()
+ session.commit()
else:
from flask import Flask
diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/models/db.py
b/providers/fab/src/airflow/providers/fab/auth_manager/models/db.py
index 8ffb6cfeab8..1e055085ba1 100644
--- a/providers/fab/src/airflow/providers/fab/auth_manager/models/db.py
+++ b/providers/fab/src/airflow/providers/fab/auth_manager/models/db.py
@@ -58,6 +58,11 @@ class FABDBManager(BaseDBManager):
super().create_db_from_orm()
_get_flask_db(settings.SQL_ALCHEMY_CONN).create_all()
+ def reset_to_2_x(self):
+ self.create_db_from_orm()
+ # And ensure it's at the oldest version
+ self.downgrade(_REVISION_HEADS_MAP["1.4.0"])
+
def upgradedb(self, to_revision=None, from_revision=None,
show_sql_only=False):
"""Upgrade the database."""
if from_revision and not show_sql_only: