This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new d12c9a0083d [v3-4-test] Fix log template sync failing when a custom 
SQL Alchemy schema is configured (#70793) (#75194)
d12c9a0083d is described below

commit d12c9a0083d70f0cc613f91be9ceb8ca2edd0fe0
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Oct 20 15:06:30 2026 +0530

    [v3-4-test] Fix log template sync failing when a custom SQL Alchemy schema 
is configured (#70793) (#75194)
    
    * Fix log template sync failing when a custom SQL Alchemy schema is 
configured
    
    When sql_alchemy_schema is set, reflect_tables() derived the schema to
    reflect a mapped class's table under by splitting its bare __tablename__
    on a dot, which discards the schema entirely for classes like
    LogTemplate whose table name has no dot. synchronize_log_template()
    then looked up the reflected table by that same bare name, which also
    does not match how SQLAlchemy keys schema-qualified tables in the
    reflected metadata. Together these left the log_template table
    unrecognized under a custom schema, causing triggering a Dag to fail
    with a TypeError.
    
    Derive the schema from the mapped class's own configured table instead
    of parsing its name, and look the reflected table up by its schema
    -qualified key.
    
    * Add newsfragment for log template custom schema fix
    
    * Remove newsfragment for log template custom schema fix
    
    Not needed for this change per review feedback.
    
    * Use inspect().local_table instead of getattr for the mapped class's schema
    
    getattr(tbl, "__table__") worked around __table__ not being declared on
    MappedClassProtocol, but SQLAlchemy's inspect() already exposes the same
    table through a properly typed Mapper.local_table, so the getattr and its
    explanatory comment are no longer needed.
    (cherry picked from commit 6aee277d4a9bbf518d9c6359f0d0ab468b4da898)
    
    Co-authored-by: Aaryan Mahajan <[email protected]>
    Co-authored-by: Rahul Vats <[email protected]>
---
 airflow-core/src/airflow/utils/db.py     | 14 +++++----
 airflow-core/tests/unit/utils/test_db.py | 50 +++++++++++++++++++++++++++++++-
 2 files changed, 58 insertions(+), 6 deletions(-)

diff --git a/airflow-core/src/airflow/utils/db.py 
b/airflow-core/src/airflow/utils/db.py
index 5f0f2d245cc..538231bf54c 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -1019,7 +1019,7 @@ def synchronize_log_template(*, session: Session = 
NEW_SESSION) -> None:
     from airflow.models.tasklog import LogTemplate
 
     metadata = reflect_tables([LogTemplate], session)
-    log_template_table: Table | None = 
metadata.tables.get(LogTemplate.__tablename__)
+    log_template_table: Table | None = 
metadata.tables.get(LogTemplate.__table__.key)
 
     if log_template_table is None:
         log.info("Log template table does not exist (added in 2.3.0); skipping 
log template sync.")
@@ -1104,11 +1104,15 @@ def reflect_tables(tables: Sequence[MappedClassProtocol 
| str] | None, session,
     else:
         for tbl in tables:
             try:
-                table_name = tbl if isinstance(tbl, str) else tbl.__tablename__
                 tbl_schema: str | None
-                tbl_schema, sep, name = table_name.partition(".")
-                if not sep:
-                    tbl_schema, name = None, table_name
+                if isinstance(tbl, str):
+                    tbl_schema, sep, name = tbl.partition(".")
+                    if not sep:
+                        tbl_schema, name = None, tbl
+                else:
+                    # Preserve the mapped class's own configured schema; see 
PR #70793.
+                    name = tbl.__tablename__
+                    tbl_schema = inspect(tbl, raiseerr=True).local_table.schema
                 metadata.reflect(
                     bind=bind, schema=tbl_schema, only=[name], 
extend_existing=True, resolve_fks=False
                 )
diff --git a/airflow-core/tests/unit/utils/test_db.py 
b/airflow-core/tests/unit/utils/test_db.py
index 83a1e653045..6af67dec69a 100644
--- a/airflow-core/tests/unit/utils/test_db.py
+++ b/airflow-core/tests/unit/utils/test_db.py
@@ -30,7 +30,8 @@ from alembic.config import Config
 from alembic.migration import MigrationContext
 from alembic.runtime.environment import EnvironmentContext
 from alembic.script import ScriptDirectory
-from sqlalchemy import Column, Integer, MetaData, Table, delete, select
+from sqlalchemy import Column, Integer, MetaData, String, Table, delete, select
+from sqlalchemy.orm import declarative_base
 
 from airflow import settings
 from airflow.models import Base as airflow_base
@@ -48,7 +49,9 @@ from airflow.utils.db import (
     downgrade,
     get_default_connections,
     initdb,
+    reflect_tables,
     resetdb,
+    synchronize_log_template,
     upgradedb,
 )
 from airflow.utils.db_manager import RunDBManager
@@ -453,6 +456,51 @@ class TestDb:
         assert bool(lss) is False
 
 
+class TestReflectTablesSchemaQualified:
+    def test_reflect_tables_uses_mapped_class_configured_schema(self, mocker):
+        metadata = MetaData()
+        table = Table(
+            "scoped_model", metadata, Column("id", Integer, primary_key=True), 
schema="custom_schema"
+        )
+
+        class _ScopedModel(declarative_base(metadata=metadata)):
+            __tablename__ = "scoped_model"
+            __table__ = table
+
+        mock_reflect = mocker.patch.object(MetaData, "reflect")
+        session = mocker.MagicMock()
+
+        reflect_tables([_ScopedModel], session)
+
+        mock_reflect.assert_called_once_with(
+            bind=session.bind,
+            schema="custom_schema",
+            only=["scoped_model"],
+            extend_existing=True,
+            resolve_fks=False,
+        )
+
+    def test_synchronize_log_template_finds_table_in_configured_schema(self, 
mocker):
+        from airflow.models.tasklog import LogTemplate
+
+        metadata = MetaData()
+        Table(
+            LogTemplate.__tablename__,
+            metadata,
+            Column("id", Integer, primary_key=True),
+            Column("filename", String(2000)),
+            Column("elasticsearch_id", String(2000)),
+            schema="custom_schema",
+        )
+        mocker.patch.object(LogTemplate.__table__, "schema", "custom_schema")
+        mocker.patch("airflow.utils.db.reflect_tables", return_value=metadata)
+        session = mocker.MagicMock()
+
+        synchronize_log_template(session=session)
+
+        session.execute.assert_called()
+
+
 class TestAutocommitEngineForMySQL:
     """Test the AutocommitEngineForMySQL context manager."""
 

Reply via email to