From e10918307cb6f750c26b1ac0e2052c0ab96bfb06 Mon Sep 17 00:00:00 2001 From: Joao Amaral <7281460+joaopamaral@users.noreply.github.com> Date: Thu, 23 Apr 2026 06:01:20 +0700 Subject: [PATCH] fix(db): Add MariaDB DDL fix for `NOCYCLE` syntax (#37582) --- superset/extensions/__init__.py | 10 +++++ superset/utils/database.py | 22 +++++++++- tests/unit_tests/utils/test_database.py | 53 +++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/utils/test_database.py diff --git a/superset/extensions/__init__.py b/superset/extensions/__init__.py index 628af40cd62..e704a2a4048 100644 --- a/superset/extensions/__init__.py +++ b/superset/extensions/__init__.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. import json +import logging import os from typing import Any, Callable, Optional @@ -45,11 +46,20 @@ from superset.extensions.ssh import SSHManagerFactory from superset.extensions.stats_logger import BaseStatsLoggerManager from superset.security.manager import SupersetSecurityManager from superset.utils.cache_manager import CacheManager +from superset.utils.database import apply_mariadb_ddl_fix from superset.utils.encrypt import EncryptedFieldFactory from superset.utils.feature_flag_manager import FeatureFlagManager from superset.utils.machine_auth import MachineAuthProviderFactory from superset.utils.profiler import SupersetProfiler +# Apply MariaDB DDL fix early in the import chain +try: + apply_mariadb_ddl_fix() +except Exception as ex: + logging.exception( + "Applying MariaDB DDL fix failed; continuing without patch: %s", ex + ) + class ResultsBackendManager: def __init__(self) -> None: diff --git a/superset/utils/database.py b/superset/utils/database.py index cc16eaacb01..cd23dfff5a1 100644 --- a/superset/utils/database.py +++ b/superset/utils/database.py @@ -17,9 +17,10 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING from flask import current_app as app +from sqlalchemy.sql import compiler from superset.constants import EXAMPLES_DB_UUID @@ -84,3 +85,22 @@ def remove_database(database: Database) -> None: db.session.delete(database) db.session.flush() + + +def apply_mariadb_ddl_fix() -> None: + """ + Fix MariaDB "NO CYCLE" syntax issue - MariaDB uses "NOCYCLE" (no space). + + This fix will be included in SQLAlchemy v2.1.0. + See: https://github.com/sqlalchemy/sqlalchemy/blob/rel_2_1_0b1/lib/sqlalchemy/dialects/mysql/_mariadb_shim.py + """ + original_visit_create_sequence = compiler.DDLCompiler.visit_create_sequence + + def patched_visit_create_sequence(self: Any, create: Any, **kw: Any) -> str: + text = original_visit_create_sequence(self, create, **kw) + dialect_name = getattr(self.dialect, "name", "") or "" + if "mariadb" in dialect_name.lower(): + return text.replace("NO CYCLE", "NOCYCLE") + return text + + compiler.DDLCompiler.visit_create_sequence = patched_visit_create_sequence diff --git a/tests/unit_tests/utils/test_database.py b/tests/unit_tests/utils/test_database.py new file mode 100644 index 00000000000..22bea17765e --- /dev/null +++ b/tests/unit_tests/utils/test_database.py @@ -0,0 +1,53 @@ +# 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. +"""Tests for superset.utils.database module.""" + +import pytest +from sqlalchemy import Sequence +from sqlalchemy.dialects import mysql, postgresql +from sqlalchemy.schema import CreateSequence +from sqlalchemy.sql.compiler import DDLCompiler + +from superset.utils.database import apply_mariadb_ddl_fix + + +@pytest.fixture(scope="module", autouse=True) +def setup_mariadb_ddl_fix(): + """Apply MariaDB DDL fix once per module before tests run.""" + apply_mariadb_ddl_fix() + + +def test_mariadb_nocycle_fix_applied(): + """Test that 'NO CYCLE' is replaced with 'NOCYCLE' for MariaDB dialect.""" + dialect = mysql.dialect() + dialect.name = "mariadb" + ddl_compiler = DDLCompiler(dialect, None) + seq = Sequence("test_seq", cycle=False) + + result = ddl_compiler.visit_create_sequence(CreateSequence(seq)) + assert "NOCYCLE" in result + assert "NO CYCLE" not in result + + +def test_nocycle_fix_not_applied_for_postgresql(): + """Test that 'NO CYCLE' is NOT replaced for PostgreSQL dialect.""" + dialect = postgresql.dialect() + compiler = DDLCompiler(dialect, None) + seq = Sequence("test_seq", cycle=False) + + result = compiler.visit_create_sequence(CreateSequence(seq)) + assert "NO CYCLE" in result