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

vatsrahul1001 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 08e428ca8ab Fix db migrate failure to Airflow 2.9.2 under PyMySQL 
driver (#70235)
08e428ca8ab is described below

commit 08e428ca8ab19975cb502972eb9c66eac3fd7a72
Author: Aaryan Mahajan <[email protected]>
AuthorDate: Mon Sep 7 13:59:32 2026 +0530

    Fix db migrate failure to Airflow 2.9.2 under PyMySQL driver (#70235)
    
    * Fix db migrate failure to Airflow 2.9.2 under PyMySQL driver
    
    Upgrading through revision 686269002441 raised
    pymysql.err.ProgrammingError because the migration used MySQL's
    PREPARE/EXECUTE/DEALLOCATE PREPARE dynamic SQL to conditionally drop
    unique constraints, which PyMySQL's driver does not support
    (see https://github.com/PyMySQL/PyMySQL/issues/202). Replaced with a
    plain lookup of existing constraints from
    information_schema.TABLE_CONSTRAINTS followed by conditional
    ALTER TABLE ... DROP INDEX statements, which works with any MySQL
    driver.
    
    Verified against a real MySQL/PyMySQL backend: downgrading to
    bff083ad727d and re-running db migrate now completes without error.
    
    closes: #43690
    
    * Add newsfragment for MySQL PyMySQL migration fix
    
    * Fix offline migration for 2.9.2 ORM-consistency revision
    
    The PyMySQL online-mode fix in this migration queries
    information_schema.TABLE_CONSTRAINTS via conn.execute() to decide which
    legacy unique constraints to drop. In Alembic's offline (--sql) mode
    there is no live connection, so conn.execute() returns None and
    iterating over it raises TypeError, breaking the CI "Test offline
    migration" step. Guard the query-then-conditionally-drop logic behind
    context.is_offline_mode(), falling back to the original dynamic
    prepare/execute/deallocate SQL text for the offline branch, since that
    text is only ever emitted as script output there rather than executed
    directly.
    
    * Address review feedback on MySQL PyMySQL migration fix
    
    Bind the table name as a query parameter in the information_schema
    lookup instead of interpolating it into the SQL string, since it's a
    value comparison rather than an identifier — matches Airflow's usual
    parameter-binding convention even though every call site currently
    passes a hardcoded literal. Also correct the docstring: PyMySQL can run
    a bare PREPARE statement fine, what it actually can't do is send more
    than one statement per cursor.execute() call, because it never enables
    CLIENT.MULTI_STATEMENTS.
    
    * Fixed the PR comments
---
 ...nconsistency_between_ORM_and_migration_files.py | 132 ++++++++-------------
 1 file changed, 51 insertions(+), 81 deletions(-)

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 64531a957b3..26d2e86176c 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
@@ -28,7 +28,7 @@ Create Date: 2024-04-15 14:19:49.913797
 from __future__ import annotations
 
 import sqlalchemy as sa
-from alembic import op
+from alembic import context, op
 from sqlalchemy import literal
 
 # revision identifiers, used by Alembic.
@@ -39,40 +39,59 @@ depends_on = None
 airflow_version = "2.9.2"
 
 
-def upgrade():
-    """Apply Update missing constraints."""
-    conn = op.get_bind()
-    if conn.dialect.name == "mysql":
-        # TODO: Rewrite these queries to use alembic when lowest MYSQL version 
supports IF EXISTS
-        conn.execute(
-            sa.text("""
-        set @var=if((SELECT true FROM information_schema.TABLE_CONSTRAINTS 
WHERE
-            CONSTRAINT_SCHEMA = DATABASE() AND
-            TABLE_NAME        = 'connection' AND
-            CONSTRAINT_NAME   = 'unique_conn_id' AND
-            CONSTRAINT_TYPE   = 'UNIQUE') = true,'ALTER TABLE connection
-            DROP INDEX unique_conn_id','select 1');
+def _mysql_drop_unique_constraint_if_exists(conn, table: str, index_name: str) 
-> None:
+    """
+    Drop a MySQL unique constraint only if it is actually present.
 
-        prepare stmt from @var;
-        execute stmt;
-        deallocate prepare stmt;
-        """)
-        )
-        # Dropping the below and recreating cause there's no IF NOT EXISTS in 
mysql
+    MySQL has no ``DROP INDEX IF EXISTS``, and PyMySQL does not enable
+    ``CLIENT.MULTI_STATEMENTS``, so it cannot run the
+    ``prepare``/``execute``/``deallocate prepare`` script in a single
+    ``cursor.execute()`` call, so the existence check and the drop are issued 
as two
+    separate single statements. In offline (``--sql``) mode there is no live 
connection
+    to query information_schema against, so the guarded dynamic SQL is emitted 
as literal
+    script text instead, to be run later through a real SQL client that 
supports
+    multi-statement scripts.
+    """
+    if context.is_offline_mode():
         conn.execute(
-            sa.text("""
+            sa.text(f"""
                 set @var=if((SELECT true FROM 
information_schema.TABLE_CONSTRAINTS WHERE
                     CONSTRAINT_SCHEMA = DATABASE() AND
-                    TABLE_NAME        = 'connection' AND
-                    CONSTRAINT_NAME   = 'connection_conn_id_uq' AND
-                    CONSTRAINT_TYPE   = 'UNIQUE') = true,'ALTER TABLE 
connection
-                    DROP INDEX connection_conn_id_uq','select 1');
+                    TABLE_NAME        = '{table}' AND
+                    CONSTRAINT_NAME   = '{index_name}' AND
+                    CONSTRAINT_TYPE   = 'UNIQUE') = true,'ALTER TABLE {table}
+                    DROP INDEX {index_name}','select 1');
 
                 prepare stmt from @var;
                 execute stmt;
                 deallocate prepare stmt;
                 """)
         )
+        return
+    existing_indexes = {
+        row[0]
+        for row in conn.execute(
+            sa.text("""
+                SELECT CONSTRAINT_NAME FROM 
information_schema.TABLE_CONSTRAINTS
+                WHERE CONSTRAINT_SCHEMA = DATABASE()
+                AND TABLE_NAME = :table
+                AND CONSTRAINT_TYPE = 'UNIQUE'
+            """),
+            {"table": table},
+        )
+    }
+    if index_name in existing_indexes:
+        conn.execute(sa.text(f"ALTER TABLE {table} DROP INDEX {index_name}"))
+
+
+def upgrade():
+    """Apply Update missing constraints."""
+    conn = op.get_bind()
+    if conn.dialect.name == "mysql":
+        # TODO: Rewrite these queries to use alembic when lowest MYSQL version 
supports IF EXISTS
+        # Dropping the below and recreating cause there's no IF NOT EXISTS in 
mysql
+        for index_name in ("unique_conn_id", "connection_conn_id_uq"):
+            _mysql_drop_unique_constraint_if_exists(conn, "connection", 
index_name)
     elif conn.dialect.name == "sqlite":
         # SQLite does not support DROP CONSTRAINT
         # We have to recreate the table without the constraint
@@ -121,63 +140,14 @@ def upgrade():
         batch_op.drop_constraint("task_reschedule_dr_fkey", type_="foreignkey")
 
     if conn.dialect.name == "mysql":
-        conn.execute(
-            sa.text("""
-                        set @var=if((SELECT true FROM 
information_schema.TABLE_CONSTRAINTS WHERE
-                            CONSTRAINT_SCHEMA = DATABASE() AND
-                            TABLE_NAME        = 'dag_run' AND
-                            CONSTRAINT_NAME   = 
'dag_run_dag_id_execution_date_uq' AND
-                            CONSTRAINT_TYPE   = 'UNIQUE') = true,'ALTER TABLE 
dag_run
-                            DROP INDEX 
dag_run_dag_id_execution_date_uq','select 1');
-
-                        prepare stmt from @var;
-                        execute stmt;
-                        deallocate prepare stmt;
-                        """)
-        )
-        conn.execute(
-            sa.text("""
-                        set @var=if((SELECT true FROM 
information_schema.TABLE_CONSTRAINTS WHERE
-                            CONSTRAINT_SCHEMA = DATABASE() AND
-                            TABLE_NAME        = 'dag_run' AND
-                            CONSTRAINT_NAME   = 'dag_run_dag_id_run_id_uq' AND
-                            CONSTRAINT_TYPE   = 'UNIQUE') = true,'ALTER TABLE 
dag_run
-                            DROP INDEX dag_run_dag_id_run_id_uq','select 1');
-
-                        prepare stmt from @var;
-                        execute stmt;
-                        deallocate prepare stmt;
-                        """)
-        )
         # below we drop and recreate the constraints because there's no IF NOT 
EXISTS
-        conn.execute(
-            sa.text("""
-                                set @var=if((SELECT true FROM 
information_schema.TABLE_CONSTRAINTS WHERE
-                                    CONSTRAINT_SCHEMA = DATABASE() AND
-                                    TABLE_NAME        = 'dag_run' AND
-                                    CONSTRAINT_NAME   = 
'dag_run_dag_id_execution_date_key' AND
-                                    CONSTRAINT_TYPE   = 'UNIQUE') = 
true,'ALTER TABLE dag_run
-                                    DROP INDEX 
dag_run_dag_id_execution_date_key','select 1');
-
-                                prepare stmt from @var;
-                                execute stmt;
-                                deallocate prepare stmt;
-                                """)
-        )
-        conn.execute(
-            sa.text("""
-                            set @var=if((SELECT true FROM 
information_schema.TABLE_CONSTRAINTS WHERE
-                                CONSTRAINT_SCHEMA = DATABASE() AND
-                                TABLE_NAME        = 'dag_run' AND
-                                CONSTRAINT_NAME   = 
'dag_run_dag_id_run_id_key' AND
-                                CONSTRAINT_TYPE   = 'UNIQUE') = true,'ALTER 
TABLE dag_run
-                                DROP INDEX dag_run_dag_id_run_id_key','select 
1');
-
-                            prepare stmt from @var;
-                            execute stmt;
-                            deallocate prepare stmt;
-                            """)
-        )
+        for index_name in (
+            "dag_run_dag_id_execution_date_uq",
+            "dag_run_dag_id_run_id_uq",
+            "dag_run_dag_id_execution_date_key",
+            "dag_run_dag_id_run_id_key",
+        ):
+            _mysql_drop_unique_constraint_if_exists(conn, "dag_run", 
index_name)
         with op.batch_alter_table("callback_request", schema=None) as batch_op:
             batch_op.alter_column(
                 "processor_subdir",

Reply via email to