diff --git a/pytest.ini b/pytest.ini index 3e2de51b8b6..8dc00c0c5be 100644 --- a/pytest.ini +++ b/pytest.ini @@ -28,7 +28,7 @@ asyncio_mode = auto filterwarnings = ignore always::sqlalchemy.exc.RemovedIn20Warning -# error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning + error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning # error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning # error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning # error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning diff --git a/scripts/benchmark_migration.py b/scripts/benchmark_migration.py index 8da9a68609e..f2b9e258cdc 100644 --- a/scripts/benchmark_migration.py +++ b/scripts/benchmark_migration.py @@ -30,7 +30,7 @@ from flask import current_app from flask_appbuilder import Model from flask_migrate import downgrade, upgrade from progress.bar import ChargingBar -from sqlalchemy import create_engine, inspect +from sqlalchemy import create_engine, inspect, text from sqlalchemy.ext.automap import automap_base from superset import db @@ -154,7 +154,7 @@ def main( # noqa: C901 print(f"Migration goes from {down_revision} to {revision}") current_revision = db.engine.execute( - "SELECT version_num FROM alembic_version" + text("SELECT version_num FROM alembic_version") ).scalar() print(f"Current version of the DB is {current_revision}") diff --git a/superset/db_engine_specs/databricks.py b/superset/db_engine_specs/databricks.py index 3bc9c0a258a..1393608468e 100644 --- a/superset/db_engine_specs/databricks.py +++ b/superset/db_engine_specs/databricks.py @@ -24,7 +24,7 @@ from apispec.ext.marshmallow import MarshmallowPlugin from flask_babel import gettext as __ from marshmallow import fields, Schema from marshmallow.validate import Range -from sqlalchemy import types +from sqlalchemy import text, types from sqlalchemy.engine.default import DefaultDialect from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL @@ -554,11 +554,11 @@ class DatabricksNativeEngineSpec(DatabricksDynamicBaseEngineSpec): return default_catalog with database.get_sqla_engine() as engine: - catalogs = {catalog for (catalog,) in engine.execute("SHOW CATALOGS")} + catalogs = {catalog for (catalog,) in engine.execute(text("SHOW CATALOGS"))} if len(catalogs) == 1: return catalogs.pop() - return engine.execute("SELECT current_catalog()").scalar() + return engine.execute(text("SELECT current_catalog()")).scalar() @classmethod def get_prequeries( @@ -582,7 +582,7 @@ class DatabricksNativeEngineSpec(DatabricksDynamicBaseEngineSpec): database: Database, inspector: Inspector, ) -> set[str]: - return {catalog for (catalog,) in inspector.bind.execute("SHOW CATALOGS")} + return {catalog for (catalog,) in inspector.bind.execute(text("SHOW CATALOGS"))} class DatabricksPythonConnectorEngineSpec(DatabricksDynamicBaseEngineSpec): @@ -746,7 +746,7 @@ class DatabricksPythonConnectorEngineSpec(DatabricksDynamicBaseEngineSpec): database: Database, inspector: Inspector, ) -> set[str]: - return {catalog for (catalog,) in inspector.bind.execute("SHOW CATALOGS")} + return {catalog for (catalog,) in inspector.bind.execute(text("SHOW CATALOGS"))} @classmethod def adjust_engine_params( diff --git a/superset/db_engine_specs/doris.py b/superset/db_engine_specs/doris.py index ffb451015c7..27a69bdf183 100644 --- a/superset/db_engine_specs/doris.py +++ b/superset/db_engine_specs/doris.py @@ -21,7 +21,7 @@ from typing import Any, Optional from urllib import parse from flask_babel import gettext as __ -from sqlalchemy import Float, Integer, Numeric, String, TEXT, types +from sqlalchemy import Float, Integer, Numeric, String, TEXT, text, types from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL from sqlalchemy.sql.type_api import TypeEngine @@ -326,7 +326,7 @@ class DorisEngineSpec(MySQLEngineSpec): CatalogId, CatalogName, Type, IsCurrent, CreateTime, LastUpdateTime, Comment We need to extract just the CatalogName column. """ - result = inspector.bind.execute("SHOW CATALOGS") + result = inspector.bind.execute(text("SHOW CATALOGS")) return {row.CatalogName for row in result} @classmethod diff --git a/superset/db_engine_specs/duckdb.py b/superset/db_engine_specs/duckdb.py index 8f19b838d26..b9b2616c1c9 100644 --- a/superset/db_engine_specs/duckdb.py +++ b/superset/db_engine_specs/duckdb.py @@ -27,7 +27,7 @@ from apispec.ext.marshmallow import MarshmallowPlugin from flask import current_app as app from flask_babel import gettext as __ from marshmallow import fields, Schema -from sqlalchemy import types +from sqlalchemy import text, types from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL @@ -473,6 +473,6 @@ class MotherDuckEngineSpec(DuckDBEngineSpec): return { catalog for (catalog,) in inspector.bind.execute( - "SELECT alias FROM MD_ALL_DATABASES() WHERE is_attached;" + text("SELECT alias FROM MD_ALL_DATABASES() WHERE is_attached;") ) } diff --git a/superset/db_engine_specs/gsheets.py b/superset/db_engine_specs/gsheets.py index d7748b7fe33..9dc85bc56c3 100644 --- a/superset/db_engine_specs/gsheets.py +++ b/superset/db_engine_specs/gsheets.py @@ -32,6 +32,7 @@ from marshmallow.exceptions import ValidationError from requests import Session from shillelagh.adapters.api.gsheets.lib import SCOPES from shillelagh.exceptions import UnauthenticatedError +from sqlalchemy import text from sqlalchemy.engine import create_engine from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL @@ -428,7 +429,7 @@ class GSheetsEngineSpec(ShillelaghEngineSpec): try: url = url.replace('"', '""') - results = conn.execute(f'SELECT * FROM "{url}" LIMIT 1') # noqa: S608 + results = conn.execute(text(f'SELECT * FROM "{url}" LIMIT 1')) # noqa: S608 results.fetchall() except Exception: # pylint: disable=broad-except errors.append( diff --git a/superset/db_engine_specs/hive.py b/superset/db_engine_specs/hive.py index 3dffab83e9b..3d89e122314 100644 --- a/superset/db_engine_specs/hive.py +++ b/superset/db_engine_specs/hive.py @@ -234,7 +234,7 @@ class HiveEngineSpec(PrestoEngineSpec): catalog=table.catalog, schema=table.schema, ) as engine: - engine.execute(f"DROP TABLE IF EXISTS {str(table)}") + engine.execute(text(f"DROP TABLE IF EXISTS {str(table)}")) def _get_hive_type(dtype: np.dtype[Any]) -> str: hive_type_by_dtype = { diff --git a/superset/db_engine_specs/impala.py b/superset/db_engine_specs/impala.py index ed129d853e4..086839a0b77 100644 --- a/superset/db_engine_specs/impala.py +++ b/superset/db_engine_specs/impala.py @@ -25,7 +25,7 @@ from typing import Any, Optional, TYPE_CHECKING import requests from flask import current_app as app -from sqlalchemy import types +from sqlalchemy import text, types from sqlalchemy.engine.reflection import Inspector from superset import db @@ -98,7 +98,7 @@ class ImpalaEngineSpec(BaseEngineSpec): def get_schema_names(cls, inspector: Inspector) -> set[str]: return { row[0] - for row in inspector.engine.execute("SHOW SCHEMAS") + for row in inspector.engine.execute(text("SHOW SCHEMAS")) if not row[0].startswith("_") } diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py index 34b24faebd8..6bb4ce5cd49 100644 --- a/superset/db_engine_specs/postgres.py +++ b/superset/db_engine_specs/postgres.py @@ -24,7 +24,7 @@ from re import Pattern from typing import Any, Callable, Optional, TYPE_CHECKING from flask_babel import gettext as __ -from sqlalchemy import types +from sqlalchemy import text, types from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, JSON from sqlalchemy.dialects.postgresql.base import PGInspector from sqlalchemy.engine.reflection import Inspector @@ -785,10 +785,10 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec): return { catalog for (catalog,) in inspector.bind.execute( - """ + text(""" SELECT datname FROM pg_database WHERE datistemplate = false; - """ + """) ) } diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index 878b5cd5f45..4f4d2eb19f5 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -33,7 +33,7 @@ import pandas as pd from flask import current_app as app from flask_babel import gettext as __, lazy_gettext as _ from packaging.version import Version -from sqlalchemy import Column, literal_column, types +from sqlalchemy import Column, literal_column, text, types from sqlalchemy.engine.interfaces import Dialect from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.result import Row as ResultRow @@ -326,7 +326,7 @@ class PrestoBaseEngineSpec(BaseEngineSpec, metaclass=ABCMeta): """ Get all catalogs. """ - return {catalog for (catalog,) in inspector.bind.execute("SHOW CATALOGS")} + return {catalog for (catalog,) in inspector.bind.execute(text("SHOW CATALOGS"))} @classmethod def adjust_engine_params( @@ -703,7 +703,9 @@ class PrestoBaseEngineSpec(BaseEngineSpec, metaclass=ABCMeta): :return: list of column objects """ full_table_name = cls.quote_table(table, inspector.engine.dialect) - return inspector.bind.execute(f"SHOW COLUMNS FROM {full_table_name}").fetchall() + return inspector.bind.execute( + text(f"SHOW COLUMNS FROM {full_table_name}") + ).fetchall() @classmethod def _create_column_info( diff --git a/superset/db_engine_specs/snowflake.py b/superset/db_engine_specs/snowflake.py index e92e98dc9d4..1723d9b5963 100644 --- a/superset/db_engine_specs/snowflake.py +++ b/superset/db_engine_specs/snowflake.py @@ -30,7 +30,7 @@ from cryptography.hazmat.primitives import serialization from flask import current_app as app from flask_babel import gettext as __ from marshmallow import fields, Schema -from sqlalchemy import types +from sqlalchemy import text, types from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL @@ -268,7 +268,7 @@ class SnowflakeEngineSpec(PostgresBaseEngineSpec): return { catalog for (catalog,) in inspector.bind.execute( - "SELECT DATABASE_NAME from information_schema.databases" + text("SELECT DATABASE_NAME from information_schema.databases") ) } diff --git a/superset/db_engine_specs/starrocks.py b/superset/db_engine_specs/starrocks.py index 3fddf613658..35278a4688b 100644 --- a/superset/db_engine_specs/starrocks.py +++ b/superset/db_engine_specs/starrocks.py @@ -22,7 +22,7 @@ from typing import Any from urllib import parse from flask_babel import gettext as __ -from sqlalchemy import Float, Integer, Numeric, types +from sqlalchemy import Float, Integer, Numeric, text, types from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL from sqlalchemy.sql.type_api import TypeEngine @@ -344,7 +344,7 @@ class StarRocksEngineSpec(MySQLEngineSpec): The command returns columns: Catalog, Type, Comment """ try: - result = inspector.bind.execute("SHOW CATALOGS") + result = inspector.bind.execute(text("SHOW CATALOGS")) catalogs = set() for row in result: @@ -375,7 +375,7 @@ class StarRocksEngineSpec(MySQLEngineSpec): (e.g., "catalog." sets the context to that catalog). """ try: - result = inspector.bind.execute("SHOW DATABASES") + result = inspector.bind.execute(text("SHOW DATABASES")) return {row[0] for row in result} except Exception as ex: # pylint: disable=broad-except logger.exception("Error fetching schema names from SHOW DATABASES: %s", ex) diff --git a/superset/extensions/metadb.py b/superset/extensions/metadb.py index 6a7a43dc4cd..99a3e82a6fb 100644 --- a/superset/extensions/metadb.py +++ b/superset/extensions/metadb.py @@ -82,7 +82,7 @@ class SupersetAPSWDialect(APSWDialect): >>> engine = create_engine('superset://') >>> conn = engine.connect() - >>> results = conn.execute('SELECT * FROM "examples.birth_names"') + >>> results = conn.execute(text('SELECT * FROM "examples.birth_names"')) Queries can also join data across different Superset databases. diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index a0c17d78e1f..9d77774c57b 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -35,6 +35,7 @@ from flask_appbuilder.utils.base import get_safe_redirect from flask_babel import lazy_gettext as _, refresh from flask_compress import Compress from flask_session import Session +from sqlalchemy import text from werkzeug.middleware.proxy_fix import ProxyFix from superset.commands.database.exceptions import DatabaseInvalidError @@ -809,7 +810,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods try: with self.superset_app.app_context(): # Simple connection test - db.engine.execute("SELECT 1") + db.engine.execute(text("SELECT 1")) except Exception: db_uri = self.database_uri diff --git a/superset/migrations/shared/utils.py b/superset/migrations/shared/utils.py index f91f3d1a45e..74ccd982002 100644 --- a/superset/migrations/shared/utils.py +++ b/superset/migrations/shared/utils.py @@ -130,7 +130,9 @@ def assign_uuids( for dialect, sql in uuid_by_dialect.items(): if isinstance(bind.dialect, dialect): op.execute( - f"UPDATE {dialect().identifier_preparer.quote(table_name)} SET uuid = {sql}" # noqa: S608, E501 + text( + f"UPDATE {dialect().identifier_preparer.quote(table_name)} SET uuid = {sql}" # noqa: S608, E501 + ) ) print(f"Done. Assigned {count} uuids in {time.time() - start_time:.3f}s.\n") return diff --git a/superset/migrations/versions/2018-03-30_14-00_d6ffdf31bdd4_add_published_column_to_dashboards.py b/superset/migrations/versions/2018-03-30_14-00_d6ffdf31bdd4_add_published_column_to_dashboards.py index 2113c6c13a9..56e6cb86d93 100644 --- a/superset/migrations/versions/2018-03-30_14-00_d6ffdf31bdd4_add_published_column_to_dashboards.py +++ b/superset/migrations/versions/2018-03-30_14-00_d6ffdf31bdd4_add_published_column_to_dashboards.py @@ -33,7 +33,7 @@ from alembic import op # noqa: E402 def upgrade(): with op.batch_alter_table("dashboards") as batch_op: batch_op.add_column(sa.Column("published", sa.Boolean(), nullable=True)) - op.execute("UPDATE dashboards SET published='1'") + op.execute(sa.text("UPDATE dashboards SET published='1'")) def downgrade(): diff --git a/superset/migrations/versions/2022-04-01_14-38_a9422eeaae74_new_dataset_models_take_2.py b/superset/migrations/versions/2022-04-01_14-38_a9422eeaae74_new_dataset_models_take_2.py index 1af34b20beb..ae4986f026d 100644 --- a/superset/migrations/versions/2022-04-01_14-38_a9422eeaae74_new_dataset_models_take_2.py +++ b/superset/migrations/versions/2022-04-01_14-38_a9422eeaae74_new_dataset_models_take_2.py @@ -29,7 +29,7 @@ from uuid import uuid4 import sqlalchemy as sa from alembic import op -from sqlalchemy import select +from sqlalchemy import select, text from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.orm import backref, relationship, Session @@ -874,14 +874,14 @@ new_tables: sa.Table = [ def reset_postgres_id_sequence(table: str) -> None: """Reset PostgreSQL sequence ID for a table's id column.""" op.execute( - """ + text(""" SELECT setval( pg_get_serial_sequence(:table, 'id'), COALESCE(max(id) + 1, 1), false ) FROM :table; - """, + """), {"table": table}, ) diff --git a/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py b/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py index ff67fc5f2b9..66b12da3459 100644 --- a/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py +++ b/superset/migrations/versions/2022-07-07_13-00_c747c78868b6_migrating_legacy_treemap.py @@ -23,6 +23,7 @@ Create Date: 2022-06-30 22:04:17.686635 """ from alembic import op +from sqlalchemy import text from sqlalchemy.dialects.mysql.base import MySQLDialect from superset import db @@ -41,8 +42,8 @@ def upgrade(): # which may significantly increase the size of these fields. if isinstance(bind.dialect, MySQLDialect): # If the columns are already MEDIUMTEXT, this is a no-op - op.execute("ALTER TABLE slices MODIFY params MEDIUMTEXT") - op.execute("ALTER TABLE slices MODIFY query_context MEDIUMTEXT") + op.execute(text("ALTER TABLE slices MODIFY params MEDIUMTEXT")) + op.execute(text("ALTER TABLE slices MODIFY query_context MEDIUMTEXT")) session = db.Session(bind=bind) MigrateTreeMap.upgrade(session) diff --git a/superset/migrations/versions/2022-07-19_15-16_a39867932713_query_context_to_mediumtext.py b/superset/migrations/versions/2022-07-19_15-16_a39867932713_query_context_to_mediumtext.py index 39651ccecdd..a354065b16a 100644 --- a/superset/migrations/versions/2022-07-19_15-16_a39867932713_query_context_to_mediumtext.py +++ b/superset/migrations/versions/2022-07-19_15-16_a39867932713_query_context_to_mediumtext.py @@ -23,6 +23,7 @@ Create Date: 2022-07-19 15:16:06.091961 """ from alembic import op +from sqlalchemy import text from sqlalchemy.dialects.mysql.base import MySQLDialect # revision identifiers, used by Alembic. @@ -33,8 +34,8 @@ down_revision = "06e1e70058c7" def upgrade(): if isinstance(op.get_bind().dialect, MySQLDialect): # If the columns are already MEDIUMTEXT, this is a no-op - op.execute("ALTER TABLE slices MODIFY params MEDIUMTEXT") - op.execute("ALTER TABLE slices MODIFY query_context MEDIUMTEXT") + op.execute(text("ALTER TABLE slices MODIFY params MEDIUMTEXT")) + op.execute(text("ALTER TABLE slices MODIFY query_context MEDIUMTEXT")) def downgrade(): diff --git a/superset/migrations/versions/2023-03-29_20-30_07f9a902af1b_drop_postgres_enum_constrains_for_tags.py b/superset/migrations/versions/2023-03-29_20-30_07f9a902af1b_drop_postgres_enum_constrains_for_tags.py index f308e866728..6de792de0d4 100644 --- a/superset/migrations/versions/2023-03-29_20-30_07f9a902af1b_drop_postgres_enum_constrains_for_tags.py +++ b/superset/migrations/versions/2023-03-29_20-30_07f9a902af1b_drop_postgres_enum_constrains_for_tags.py @@ -27,6 +27,7 @@ revision = "07f9a902af1b" down_revision = "b5ea9d343307" from alembic import op # noqa: E402 +from sqlalchemy import text # noqa: E402 from sqlalchemy.dialects import postgresql # noqa: E402 @@ -34,11 +35,11 @@ def upgrade(): conn = op.get_bind() if isinstance(conn.dialect, postgresql.dialect): conn.execute( - 'ALTER TABLE "tagged_object" ALTER COLUMN "object_type" TYPE VARCHAR' + text('ALTER TABLE "tagged_object" ALTER COLUMN "object_type" TYPE VARCHAR') ) - conn.execute('ALTER TABLE "tag" ALTER COLUMN "type" TYPE VARCHAR') - conn.execute("DROP TYPE IF EXISTS objecttypes") - conn.execute("DROP TYPE IF EXISTS tagtypes") + conn.execute(text('ALTER TABLE "tag" ALTER COLUMN "type" TYPE VARCHAR')) + conn.execute(text("DROP TYPE IF EXISTS objecttypes")) + conn.execute(text("DROP TYPE IF EXISTS tagtypes")) def downgrade(): diff --git a/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py b/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py index e80102b02d7..5a33ba0c390 100644 --- a/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py +++ b/superset/migrations/versions/2023-06-08_10-22_4c5da39be729_migrate_treemap_chart.py @@ -23,6 +23,7 @@ Create Date: 2023-06-08 10:22:23.192064 """ from alembic import op +from sqlalchemy import text from sqlalchemy.dialects.mysql.base import MySQLDialect from superset import db @@ -41,8 +42,8 @@ def upgrade(): # which may significantly increase the size of these fields. if isinstance(bind.dialect, MySQLDialect): # If the columns are already MEDIUMTEXT, this is a no-op - op.execute("ALTER TABLE slices MODIFY params MEDIUMTEXT") - op.execute("ALTER TABLE slices MODIFY query_context MEDIUMTEXT") + op.execute(text("ALTER TABLE slices MODIFY params MEDIUMTEXT")) + op.execute(text("ALTER TABLE slices MODIFY query_context MEDIUMTEXT")) session = db.Session(bind=bind) MigrateTreeMap.upgrade(session) diff --git a/superset/utils/encrypt.py b/superset/utils/encrypt.py index b788566228a..67b2747e4de 100644 --- a/superset/utils/encrypt.py +++ b/superset/utils/encrypt.py @@ -302,7 +302,7 @@ class SecretsMigrator: table_name: str, ) -> Row: cols = ",".join(pk_columns + column_names) - return conn.execute(f"SELECT {cols} FROM {table_name}") # noqa: S608 + return conn.execute(text(f"SELECT {cols} FROM {table_name}")) # noqa: S608 def _target_type(self, encrypted_type: EncryptedType) -> EncryptedType: """The EncryptedType to re-encrypt a value *into*. diff --git a/superset/utils/mock_data.py b/superset/utils/mock_data.py index 58647dffa3e..346522480f2 100644 --- a/superset/utils/mock_data.py +++ b/superset/utils/mock_data.py @@ -28,7 +28,7 @@ from uuid import uuid4 import sqlalchemy.sql.sqltypes import sqlalchemy_utils from flask_appbuilder import Model -from sqlalchemy import Column, inspect, MetaData, Table as DBTable +from sqlalchemy import Column, inspect, MetaData, Table as DBTable, text from sqlalchemy.dialects import postgresql from sqlalchemy.sql import func from sqlalchemy.sql.visitors import VisitableType @@ -284,7 +284,9 @@ def add_sample_rows(model: type[Model], count: int) -> Iterator[Model]: def get_valid_foreign_key(column: Column) -> Any: foreign_key = list(column.foreign_keys)[0] table_name, column_name = foreign_key.target_fullname.split(".", 1) - return db.engine.execute(f"SELECT {column_name} FROM {table_name} LIMIT 1").scalar() # noqa: S608 + return db.engine.execute( + text(f"SELECT {column_name} FROM {table_name} LIMIT 1") # noqa: S608 + ).scalar() def generate_value(column: Column) -> Any: diff --git a/tests/common/assert_utils.py b/tests/common/assert_utils.py new file mode 100644 index 00000000000..1a1f05f6d0f --- /dev/null +++ b/tests/common/assert_utils.py @@ -0,0 +1,59 @@ +# 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. +from unittest import mock + + +def assert_any_call_with_text( + m: mock.Mock, + q: str, +): + """ + Assert the mock has been called with the specified query. + + Compares by value when an SQLAlchemy text object is passed to the mock. + """ + assert any( + hasattr(call_args_it[0][0], "text") and call_args_it[0][0].text == q + for call_args_it in m.call_args_list + ) + + +def assert_called_once_with_text( + m: mock.Mock, + q: str, +): + """ + Assert that the mock was called exactly once and that call was with the specified + arguments. + + Compares by value when an SQLAlchemy text object is passed to the mock. + """ + m.assert_called_once() + assert m.call_args[0][0].text == q + + +def assert_called_with_text( + m: mock.Mock, + q: str, +): + """ + This method is a convenient way of asserting that the last call has been made in a + particular way. + + Compares by value when an SQLAlchemy text object is passed to the mock. + """ + assert m.call_args[0][0].text == q diff --git a/tests/example_data/data_loading/pandas/pandas_data_loader.py b/tests/example_data/data_loading/pandas/pandas_data_loader.py index 8dfbd21f6a2..af3416b60d9 100644 --- a/tests/example_data/data_loading/pandas/pandas_data_loader.py +++ b/tests/example_data/data_loading/pandas/pandas_data_loader.py @@ -20,6 +20,7 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING from pandas import DataFrame +from sqlalchemy import text from sqlalchemy.inspection import inspect from tests.common.logger_utils import log @@ -74,7 +75,7 @@ class PandasDataLoader(DataLoader): return None def remove_table(self, table_name: str) -> None: - self._db_engine.execute(f"DROP TABLE IF EXISTS {table_name}") + self._db_engine.execute(text(f"DROP TABLE IF EXISTS {table_name}")) class TableToDfConvertor(ABC): diff --git a/tests/integration_tests/celery_tests.py b/tests/integration_tests/celery_tests.py index 7d17019b1d3..15807dd9575 100644 --- a/tests/integration_tests/celery_tests.py +++ b/tests/integration_tests/celery_tests.py @@ -28,6 +28,7 @@ from tests.integration_tests.fixtures.birth_names_dashboard import ( ) import pytest +from sqlalchemy import text import flask # noqa: F401 from flask import current_app, has_app_context # noqa: F401 @@ -120,7 +121,7 @@ def drop_table_if_exists(table_name: str, table_type: CTASMethod) -> None: sql = f"DROP {table_type.name} IF EXISTS {table_name}" database = get_example_database() with database.get_sqla_engine() as engine: - engine.execute(sql) + engine.execute(text(sql)) def quote_f(value: Optional[str]): @@ -531,7 +532,7 @@ def test_teardown_with_app_context(): def delete_tmp_view_or_table(name: str, ctas_method: CTASMethod): - db.get_engine().execute(f"DROP {ctas_method.name} IF EXISTS {name}") + db.get_engine().execute(text(f"DROP {ctas_method.name} IF EXISTS {name}")) def wait_for_success(result): diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 4f6ce10b0f9..2dde51433c5 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -26,6 +26,7 @@ from unittest.mock import patch import pytest from flask.ctx import AppContext from flask_appbuilder.security.sqla import models as ab_models +from sqlalchemy import text from sqlalchemy.engine import Engine from superset import db, security_manager @@ -146,14 +147,14 @@ def setup_sample_data() -> Any: def drop_from_schema(engine: Engine, schema_name: str): - schemas = engine.execute(f"SHOW SCHEMAS").fetchall() # noqa: F541 + schemas = engine.execute(text("SHOW SCHEMAS")).fetchall() # noqa: F541 if schema_name not in [s[0] for s in schemas]: # schema doesn't exist return - tables_or_views = engine.execute(f"SHOW TABLES in {schema_name}").fetchall() + tables_or_views = engine.execute(text(f"SHOW TABLES in {schema_name}")).fetchall() for tv in tables_or_views: - engine.execute(f"DROP TABLE IF EXISTS {schema_name}.{tv[0]}") - engine.execute(f"DROP VIEW IF EXISTS {schema_name}.{tv[0]}") + engine.execute(text(f"DROP TABLE IF EXISTS {schema_name}.{tv[0]}")) + engine.execute(text(f"DROP VIEW IF EXISTS {schema_name}.{tv[0]}")) @pytest.fixture(scope="session") @@ -213,12 +214,12 @@ def setup_presto_if_needed(): database = get_example_database() with database.get_sqla_engine() as engine: drop_from_schema(engine, CTAS_SCHEMA_NAME) - engine.execute(f"DROP SCHEMA IF EXISTS {CTAS_SCHEMA_NAME}") - engine.execute(f"CREATE SCHEMA {CTAS_SCHEMA_NAME}") + engine.execute(text(f"DROP SCHEMA IF EXISTS {CTAS_SCHEMA_NAME}")) + engine.execute(text(f"CREATE SCHEMA {CTAS_SCHEMA_NAME}")) drop_from_schema(engine, ADMIN_SCHEMA_NAME) - engine.execute(f"DROP SCHEMA IF EXISTS {ADMIN_SCHEMA_NAME}") - engine.execute(f"CREATE SCHEMA {ADMIN_SCHEMA_NAME}") + engine.execute(text(f"DROP SCHEMA IF EXISTS {ADMIN_SCHEMA_NAME}")) + engine.execute(text(f"CREATE SCHEMA {ADMIN_SCHEMA_NAME}")) def with_feature_flags(**mock_feature_flags): @@ -356,7 +357,7 @@ def physical_dataset(): quoter = get_identifier_quoter(engine.name) # sqlite can only execute one statement at a time engine.execute( - f""" + text(f""" CREATE TABLE IF NOT EXISTS physical_dataset( col1 INTEGER, col2 VARCHAR(255), @@ -366,10 +367,10 @@ def physical_dataset(): col6 TIMESTAMP DEFAULT '1970-01-01 00:00:01', {quoter("time column with spaces")} TIMESTAMP DEFAULT '1970-01-01 00:00:01' ); - """ + """) ) engine.execute( - """ + text(""" INSERT INTO physical_dataset values (0, 'a', 1.0, NULL, '2000-01-01 00:00:00', '2002-01-03 00:00:00', '2002-01-03 00:00:00'), (1, 'b', 1.1, NULL, '2000-01-02 00:00:00', '2002-02-04 00:00:00', '2002-02-04 00:00:00'), @@ -381,7 +382,7 @@ def physical_dataset(): (7, 'h', 1.7, NULL, '2000-01-08 00:00:00', '2002-08-18 00:00:00', '2002-08-18 00:00:00'), (8, 'i', 1.8, NULL, '2000-01-09 00:00:00', '2002-09-20 00:00:00', '2002-09-20 00:00:00'), (9, 'j', 1.9, NULL, '2000-01-10 00:00:00', '2002-10-22 00:00:00', '2002-10-22 00:00:00'); - """ # noqa: E501 + """) # noqa: E501 ) dataset = SqlaTable( @@ -407,9 +408,9 @@ def physical_dataset(): yield dataset engine.execute( - """ + text(""" DROP TABLE physical_dataset; - """ + """) ) dataset = db.session.query(SqlaTable).filter_by(table_name="physical_dataset").all() for ds in dataset: diff --git a/tests/integration_tests/databases/commands/upload_test.py b/tests/integration_tests/databases/commands/upload_test.py index 95e21960605..abb7120fec9 100644 --- a/tests/integration_tests/databases/commands/upload_test.py +++ b/tests/integration_tests/databases/commands/upload_test.py @@ -19,6 +19,7 @@ from __future__ import annotations import pytest from flask.ctx import AppContext +from sqlalchemy import text from superset import db, security_manager from superset.commands.database.exceptions import ( @@ -78,8 +79,8 @@ def _setup_csv_upload(allowed_schemas: list[str] | None = None): upload_db = get_upload_db() with upload_db.get_sqla_engine() as engine: - engine.execute(f"DROP TABLE IF EXISTS {CSV_UPLOAD_TABLE}") - engine.execute(f"DROP TABLE IF EXISTS {CSV_UPLOAD_TABLE_W_SCHEMA}") + engine.execute(text(f"DROP TABLE IF EXISTS {CSV_UPLOAD_TABLE}")) + engine.execute(text(f"DROP TABLE IF EXISTS {CSV_UPLOAD_TABLE_W_SCHEMA}")) db.session.delete(upload_db) db.session.commit() @@ -112,7 +113,7 @@ def test_csv_upload_with_nulls(): CSVReader({"null_values": ["N/A", "None"]}), ).run() with upload_database.get_sqla_engine() as engine: - data = engine.execute(f"SELECT * from {CSV_UPLOAD_TABLE}").fetchall() # noqa: S608 + data = engine.execute(text(f"SELECT * from {CSV_UPLOAD_TABLE}")).fetchall() # noqa: S608 assert data == [ ("name1", None, "city1", "1-1-1980"), ("name2", 29, None, "1-1-1981"), @@ -156,7 +157,7 @@ def test_csv_upload_with_index(): CSVReader({"dataframe_index": True, "index_label": "id"}), ).run() with upload_database.get_sqla_engine() as engine: - data = engine.execute(f"SELECT * from {CSV_UPLOAD_TABLE}").fetchall() # noqa: S608 + data = engine.execute(text(f"SELECT * from {CSV_UPLOAD_TABLE}")).fetchall() # noqa: S608 assert data == [ (0, "name1", 30, "city1", "1-1-1980"), (1, "name2", 29, "city2", "1-1-1981"), @@ -165,7 +166,7 @@ def test_csv_upload_with_index(): # assert column names assert [ # noqa: C416 col - for col in engine.execute(f"SELECT * from {CSV_UPLOAD_TABLE}").keys() # noqa: S608 + for col in engine.execute(text(f"SELECT * from {CSV_UPLOAD_TABLE}")).keys() # noqa: S608 ] == [ "id", "Name", diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index 66e851422a1..c5eae19943c 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -27,7 +27,7 @@ import pytest import rison import yaml from freezegun import freeze_time -from sqlalchemy import inspect +from sqlalchemy import inspect, text from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import joinedload from sqlalchemy.sql import func @@ -896,7 +896,7 @@ class TestDatasetApi(SupersetTestCase): example_db = get_example_database() with example_db.get_sqla_engine() as engine: engine.execute( - f"CREATE TABLE {CTAS_SCHEMA_NAME}.birth_names AS SELECT 2 as two" + text(f"CREATE TABLE {CTAS_SCHEMA_NAME}.birth_names AS SELECT 2 as two") ) self.login(ADMIN_USERNAME) @@ -916,7 +916,7 @@ class TestDatasetApi(SupersetTestCase): rv = self.client.delete(uri) assert rv.status_code == 200 with example_db.get_sqla_engine() as engine: - engine.execute(f"DROP TABLE {CTAS_SCHEMA_NAME}.birth_names") + engine.execute(text(f"DROP TABLE {CTAS_SCHEMA_NAME}.birth_names")) def test_create_dataset_validate_database(self): """ @@ -2956,8 +2956,10 @@ class TestDatasetApi(SupersetTestCase): examples_db = get_example_database() with examples_db.get_sqla_engine() as engine: - engine.execute("DROP TABLE IF EXISTS test_create_sqla_table_api") - engine.execute("CREATE TABLE test_create_sqla_table_api AS SELECT 2 as col") + engine.execute(text("DROP TABLE IF EXISTS test_create_sqla_table_api")) + engine.execute( + text("CREATE TABLE test_create_sqla_table_api AS SELECT 2 as col") + ) rv = self.client.post( "api/v1/dataset/get_or_create/", @@ -2981,7 +2983,7 @@ class TestDatasetApi(SupersetTestCase): self.items_to_delete = [table] with examples_db.get_sqla_engine() as engine: - engine.execute("DROP TABLE test_create_sqla_table_api") + engine.execute(text("DROP TABLE test_create_sqla_table_api")) def test_get_or_create_dataset_disambiguates_by_schema(self): """ diff --git a/tests/integration_tests/datasets/commands_tests.py b/tests/integration_tests/datasets/commands_tests.py index dd3f8c3acb6..6fa623d4781 100644 --- a/tests/integration_tests/datasets/commands_tests.py +++ b/tests/integration_tests/datasets/commands_tests.py @@ -20,6 +20,7 @@ from unittest.mock import patch import pytest import yaml +from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError from superset import db, security_manager @@ -617,9 +618,9 @@ class TestCreateDatasetCommand(SupersetTestCase): def test_create_dataset_command(self): examples_db = get_example_database() with examples_db.get_sqla_engine() as engine: - engine.execute("DROP TABLE IF EXISTS test_create_dataset_command") + engine.execute(text("DROP TABLE IF EXISTS test_create_dataset_command")) engine.execute( - "CREATE TABLE test_create_dataset_command AS SELECT 2 as col" + text("CREATE TABLE test_create_dataset_command AS SELECT 2 as col") ) with override_user(security_manager.find_user("admin")): @@ -640,7 +641,7 @@ class TestCreateDatasetCommand(SupersetTestCase): db.session.delete(table) db.session.commit() with examples_db.get_sqla_engine() as engine: - engine.execute("DROP TABLE test_create_dataset_command") + engine.execute(text("DROP TABLE test_create_dataset_command")) db.session.commit() def test_create_dataset_command_not_allowed(self): diff --git a/tests/integration_tests/datasource_tests.py b/tests/integration_tests/datasource_tests.py index b9da228ea3c..ec75ce1f0c9 100644 --- a/tests/integration_tests/datasource_tests.py +++ b/tests/integration_tests/datasource_tests.py @@ -23,6 +23,7 @@ from unittest import mock import pytest import rison from flask import current_app +from sqlalchemy import text from superset import db, security_manager as sm from superset.commands.dataset.exceptions import DatasetNotFoundError @@ -63,15 +64,26 @@ def create_test_table_context(database: Database): with database.get_sqla_engine() as engine: engine.execute( - f"CREATE TABLE IF NOT EXISTS {full_table_name} AS SELECT 1 as first, 2 as second" # noqa: E501 + text(f""" + CREATE TABLE IF NOT EXISTS {full_table_name} AS + SELECT 1 as first, 2 as second + """) + ) + engine.execute( + text(f""" + INSERT INTO {full_table_name} (first, second) VALUES (1, 2) + """) # noqa: S608 + ) + engine.execute( + text(f""" + INSERT INTO {full_table_name} (first, second) VALUES (3, 4) + """) # noqa: S608 ) - engine.execute(f"INSERT INTO {full_table_name} (first, second) VALUES (1, 2)") # noqa: S608 - engine.execute(f"INSERT INTO {full_table_name} (first, second) VALUES (3, 4)") # noqa: S608 yield db.session with database.get_sqla_engine() as engine: - engine.execute(f"DROP TABLE {full_table_name}") + engine.execute(text(f"DROP TABLE {full_table_name}")) @contextmanager diff --git a/tests/integration_tests/db_engine_specs/hive_tests.py b/tests/integration_tests/db_engine_specs/hive_tests.py index 3211889519b..1ab1738ebf7 100644 --- a/tests/integration_tests/db_engine_specs/hive_tests.py +++ b/tests/integration_tests/db_engine_specs/hive_tests.py @@ -25,6 +25,7 @@ from sqlalchemy.sql import select from superset.db_engine_specs.hive import HiveEngineSpec, upload_to_s3 from superset.exceptions import SupersetException from superset.sql.parse import Table +from tests.common.assert_utils import assert_any_call_with_text from tests.integration_tests.base_tests import SupersetTestCase from tests.integration_tests.test_app import app @@ -198,7 +199,10 @@ def test_df_to_sql_if_exists_replace(mock_upload_to_s3, mock_g): {"if_exists": "replace", "header": 1, "na_values": "mock", "sep": "mock"}, ) - mock_execute.assert_any_call(f"DROP TABLE IF EXISTS {table_name}") + assert_any_call_with_text( + mock_execute, + f"DROP TABLE IF EXISTS {table_name}", + ) app.config = config @@ -226,7 +230,9 @@ def test_df_to_sql_if_exists_replace_with_schema(mock_upload_to_s3, mock_g): {"if_exists": "replace", "header": 1, "na_values": "mock", "sep": "mock"}, ) - mock_execute.assert_any_call(f"DROP TABLE IF EXISTS {schema}.{table_name}") + assert_any_call_with_text( + mock_execute, f"DROP TABLE IF EXISTS {schema}.{table_name}" + ) app.config = config diff --git a/tests/integration_tests/db_engine_specs/presto_tests.py b/tests/integration_tests/db_engine_specs/presto_tests.py index df12ca52412..10d9067f75b 100644 --- a/tests/integration_tests/db_engine_specs/presto_tests.py +++ b/tests/integration_tests/db_engine_specs/presto_tests.py @@ -29,6 +29,7 @@ from superset.db_engine_specs.presto import PrestoEngineSpec from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.sql.parse import Table from superset.utils.database import get_example_database +from tests.common.assert_utils import assert_called_once_with_text from tests.integration_tests.base_tests import SupersetTestCase @@ -836,8 +837,9 @@ class TestPrestoDbEngineSpec(SupersetTestCase): table_name = "table_name" result = PrestoEngineSpec._show_columns(inspector, Table(table_name)) assert result == ["a", "b"] - inspector.bind.execute.assert_called_once_with( - f'SHOW COLUMNS FROM "{table_name}"' + assert_called_once_with_text( + inspector.bind.execute, + f'SHOW COLUMNS FROM "{table_name}"', ) def test_show_columns_with_schema(self): @@ -853,8 +855,8 @@ class TestPrestoDbEngineSpec(SupersetTestCase): schema = "schema" result = PrestoEngineSpec._show_columns(inspector, Table(table_name, schema)) assert result == ["a", "b"] - inspector.bind.execute.assert_called_once_with( - f'SHOW COLUMNS FROM "{schema}"."{table_name}"' + assert_called_once_with_text( + inspector.bind.execute, f'SHOW COLUMNS FROM "{schema}"."{table_name}"' ) def test_is_column_name_quoted(self): diff --git a/tests/integration_tests/fixtures/energy_dashboard.py b/tests/integration_tests/fixtures/energy_dashboard.py index 2407ec01ed0..82f75d9805a 100644 --- a/tests/integration_tests/fixtures/energy_dashboard.py +++ b/tests/integration_tests/fixtures/energy_dashboard.py @@ -18,7 +18,7 @@ import random import pandas as pd import pytest -from sqlalchemy import column, Float, String +from sqlalchemy import column, Float, String, text from superset import db from superset.connectors.sqla.models import SqlaTable, SqlMetric @@ -53,7 +53,7 @@ def load_energy_table_data(): yield with app.app_context(): with get_example_database().get_sqla_engine() as engine: - engine.execute("DROP TABLE IF EXISTS energy_usage") + engine.execute(text("DROP TABLE IF EXISTS energy_usage")) @pytest.fixture diff --git a/tests/integration_tests/fixtures/unicode_dashboard.py b/tests/integration_tests/fixtures/unicode_dashboard.py index 2f18c402b8b..885ff500f80 100644 --- a/tests/integration_tests/fixtures/unicode_dashboard.py +++ b/tests/integration_tests/fixtures/unicode_dashboard.py @@ -16,7 +16,7 @@ # under the License. import pandas as pd import pytest -from sqlalchemy import String +from sqlalchemy import String, text from superset import db from superset.connectors.sqla.models import SqlaTable @@ -52,7 +52,7 @@ def load_unicode_data(): yield with app.app_context(): with get_example_database().get_sqla_engine() as engine: - engine.execute("DROP TABLE IF EXISTS unicode_test") + engine.execute(text("DROP TABLE IF EXISTS unicode_test")) @pytest.fixture diff --git a/tests/integration_tests/fixtures/world_bank_dashboard.py b/tests/integration_tests/fixtures/world_bank_dashboard.py index 983c0fba27b..eed27d995fd 100644 --- a/tests/integration_tests/fixtures/world_bank_dashboard.py +++ b/tests/integration_tests/fixtures/world_bank_dashboard.py @@ -22,7 +22,7 @@ from typing import Any import pandas as pd import pytest from pandas import DataFrame -from sqlalchemy import DateTime, String +from sqlalchemy import DateTime, String, text from superset import db from superset.connectors.sqla.models import SqlaTable @@ -67,7 +67,7 @@ def load_world_bank_data(): yield with app.app_context(): with get_example_database().get_sqla_engine() as engine: - engine.execute("DROP TABLE IF EXISTS wb_health_population") + engine.execute(text("DROP TABLE IF EXISTS wb_health_population")) @pytest.fixture diff --git a/tests/integration_tests/reports/commands_tests.py b/tests/integration_tests/reports/commands_tests.py index ae1c855d85a..e9c2f37a3c8 100644 --- a/tests/integration_tests/reports/commands_tests.py +++ b/tests/integration_tests/reports/commands_tests.py @@ -35,7 +35,7 @@ from slack_sdk.errors import ( SlackRequestError, SlackTokenRotationError, ) -from sqlalchemy.sql import func +from sqlalchemy.sql import func, text from superset import db from superset.commands.report.exceptions import ( @@ -172,14 +172,17 @@ def assert_log(state: str, error_message: Optional[str] = None): def create_test_table_context(database: Database): with database.get_sqla_engine() as engine: engine.execute( - "CREATE TABLE IF NOT EXISTS test_table AS SELECT 1 as first, 2 as second" + text(""" + CREATE TABLE IF NOT EXISTS test_table AS + SELECT 1 as first, 2 as second + """) ) - engine.execute("INSERT INTO test_table (first, second) VALUES (1, 2)") - engine.execute("INSERT INTO test_table (first, second) VALUES (3, 4)") + engine.execute(text("INSERT INTO test_table (first, second) VALUES (1, 2)")) + engine.execute(text("INSERT INTO test_table (first, second) VALUES (3, 4)")) yield db.session with database.get_sqla_engine() as engine: - engine.execute("DROP TABLE test_table") + engine.execute(text("DROP TABLE test_table")) @pytest.fixture diff --git a/tests/integration_tests/sqllab_tests.py b/tests/integration_tests/sqllab_tests.py index 950879027c7..0f01abfe6ae 100644 --- a/tests/integration_tests/sqllab_tests.py +++ b/tests/integration_tests/sqllab_tests.py @@ -22,6 +22,7 @@ from textwrap import dedent import pytest from celery.exceptions import SoftTimeLimitExceeded from parameterized import parameterized +from sqlalchemy import text from unittest import mock import rison @@ -184,10 +185,10 @@ class TestSqlLab(SupersetTestCase): examples_db = get_example_database() with examples_db.get_sqla_engine() as engine: data = engine.execute( - f"SELECT * FROM admin_database.{tmp_table_name}" # noqa: S608 + text(f"SELECT * FROM admin_database.{tmp_table_name}") # noqa: S608 ).fetchall() names_count = engine.execute( - f"SELECT COUNT(*) FROM birth_names" # noqa: F541, S608 + text(f"SELECT COUNT(*) FROM birth_names") # noqa: F541, S608 ).first() assert names_count[0] == len( data @@ -195,7 +196,7 @@ class TestSqlLab(SupersetTestCase): # cleanup engine.execute( - f"DROP {ctas_method.name} admin_database.{tmp_table_name}" + text(f"DROP {ctas_method.name} admin_database.{tmp_table_name}") ) examples_db.allow_ctas = old_allow_ctas db.session.commit() @@ -294,7 +295,10 @@ class TestSqlLab(SupersetTestCase): with examples_db.get_sqla_engine() as engine: engine.execute( - f"CREATE TABLE IF NOT EXISTS {CTAS_SCHEMA_NAME}.test_table AS SELECT 1 as c1, 2 as c2" # noqa: E501 + text(f""" + CREATE TABLE IF NOT EXISTS {CTAS_SCHEMA_NAME}.test_table AS + SELECT 1 as c1, 2 as c2 + """) # noqa: E501 ) # SQL Lab raw query access requires datasource_access on a registered @@ -314,7 +318,7 @@ class TestSqlLab(SupersetTestCase): db.session.query(Query).delete() with get_example_database().get_sqla_engine() as engine: - engine.execute(f"DROP TABLE IF EXISTS {CTAS_SCHEMA_NAME}.test_table") + engine.execute(text(f"DROP TABLE IF EXISTS {CTAS_SCHEMA_NAME}.test_table")) db.session.commit() def test_alias_duplicate(self): diff --git a/tests/unit_tests/db_engine_specs/test_doris.py b/tests/unit_tests/db_engine_specs/test_doris.py index 9e1e9e9f2e7..b0c44b3d478 100644 --- a/tests/unit_tests/db_engine_specs/test_doris.py +++ b/tests/unit_tests/db_engine_specs/test_doris.py @@ -36,6 +36,7 @@ from superset.db_engine_specs.doris import ( TINYINT, ) from superset.utils.core import GenericDataType +from tests.common.assert_utils import assert_called_once_with_text from tests.unit_tests.db_engine_specs.utils import assert_column_spec @@ -271,7 +272,10 @@ def test_get_catalog_names( catalogs = DorisEngineSpec.get_catalog_names(database, inspector) # Verify the SQL query - inspector.bind.execute.assert_called_once_with("SHOW CATALOGS") + assert_called_once_with_text( + inspector.bind.execute, + "SHOW CATALOGS", + ) # Verify the returned catalog names assert catalogs == expected_result diff --git a/tests/unit_tests/db_engine_specs/test_sqlite.py b/tests/unit_tests/db_engine_specs/test_sqlite.py index 485ec293a5f..7efb84e81b9 100644 --- a/tests/unit_tests/db_engine_specs/test_sqlite.py +++ b/tests/unit_tests/db_engine_specs/test_sqlite.py @@ -19,6 +19,7 @@ from datetime import datetime from typing import Optional import pytest +from sqlalchemy import text from sqlalchemy.engine import create_engine from superset.constants import TimeGrain @@ -121,11 +122,11 @@ def test_time_grain_expressions(dttm: str, grain: str, expected: str) -> None: engine = create_engine("sqlite://") connection = engine.connect() - connection.execute("CREATE TABLE t (dttm DATETIME)") - connection.execute("INSERT INTO t VALUES (?)", dttm) + connection.execute(text("CREATE TABLE t (dttm DATETIME)")) + connection.execute(text("INSERT INTO t VALUES (:dttm)"), {"dttm": dttm}) # pylint: disable=protected-access expression = SqliteEngineSpec._time_grain_expressions[grain].format(col="dttm") sql = f"SELECT {expression} FROM t" # noqa: S608 - result = connection.execute(sql).scalar() + result = connection.execute(text(sql)).scalar() assert result == expected diff --git a/tests/unit_tests/db_engine_specs/test_trino.py b/tests/unit_tests/db_engine_specs/test_trino.py index 7ad6cf67975..5f8f058ba2a 100644 --- a/tests/unit_tests/db_engine_specs/test_trino.py +++ b/tests/unit_tests/db_engine_specs/test_trino.py @@ -53,6 +53,7 @@ from superset.superset_typing import ( ) from superset.utils import json from superset.utils.core import GenericDataType +from tests.common.assert_utils import assert_called_with_text from tests.unit_tests.db_engine_specs.utils import ( assert_column_spec, assert_convert_dttm, @@ -571,7 +572,10 @@ def test_get_columns_error(mocker: MockerFixture): _assert_columns_equal(actual, expected) - mock_inspector.bind.execute.assert_called_with('SHOW COLUMNS FROM schema."table"') + assert_called_with_text( + mock_inspector.bind.execute, + 'SHOW COLUMNS FROM schema."table"', + ) def test_get_columns_expand_rows(mocker: MockerFixture): diff --git a/tests/unit_tests/extensions/test_sqlalchemy.py b/tests/unit_tests/extensions/test_sqlalchemy.py index 06252a648e8..4778813f98d 100644 --- a/tests/unit_tests/extensions/test_sqlalchemy.py +++ b/tests/unit_tests/extensions/test_sqlalchemy.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING import pytest from pytest_mock import MockerFixture +from sqlalchemy import text from sqlalchemy.engine import create_engine from sqlalchemy.exc import ProgrammingError from sqlalchemy.orm.session import Session @@ -63,13 +64,15 @@ def database1(session: Session) -> Iterator["Database"]: def table1(session: Session, database1: "Database") -> Iterator[None]: with database1.get_sqla_engine() as engine: conn = engine.connect() - conn.execute("CREATE TABLE table1 (a INTEGER NOT NULL PRIMARY KEY, b INTEGER)") - conn.execute("INSERT INTO table1 (a, b) VALUES (1, 10), (2, 20)") + conn.execute( + text("CREATE TABLE table1 (a INTEGER NOT NULL PRIMARY KEY, b INTEGER)") + ) + conn.execute(text("INSERT INTO table1 (a, b) VALUES (1, 10), (2, 20)")) db.session.commit() yield - conn.execute("DROP TABLE table1") + conn.execute(text("DROP TABLE table1")) db.session.commit() @@ -97,13 +100,15 @@ def database2(session: Session) -> Iterator["Database"]: def table2(session: Session, database2: "Database") -> Iterator[None]: with database2.get_sqla_engine() as engine: conn = engine.connect() - conn.execute("CREATE TABLE table2 (a INTEGER NOT NULL PRIMARY KEY, b TEXT)") - conn.execute("INSERT INTO table2 (a, b) VALUES (1, 'ten'), (2, 'twenty')") + conn.execute( + text("CREATE TABLE table2 (a INTEGER NOT NULL PRIMARY KEY, b TEXT)") + ) + conn.execute(text("INSERT INTO table2 (a, b) VALUES (1, 'ten'), (2, 'twenty')")) db.session.commit() yield - conn.execute("DROP TABLE table2") + conn.execute(text("DROP TABLE table2")) db.session.commit() @@ -133,7 +138,7 @@ def test_superset(mocker: MockerFixture, app_context: None, table1: None) -> Non pytest.skip(f"Superset dialect not available: {e}") conn = engine.connect() - results = conn.execute('SELECT * FROM "database1.table1"') + results = conn.execute(text('SELECT * FROM "database1.table1"')) assert list(results) == [(1, 10), (2, 20)] @@ -174,7 +179,7 @@ def test_superset_limit(mocker: MockerFixture, app_context: None, table1: None) pytest.skip(f"Superset dialect not available: {e}") conn = engine.connect() - results = conn.execute('SELECT * FROM "database1.table1"') + results = conn.execute(text('SELECT * FROM "database1.table1"')) assert list(results) == [(1, 10)] @@ -210,12 +215,12 @@ def test_superset_joins( conn = engine.connect() results = conn.execute( - """ + text(""" SELECT t1.b, t2.b FROM "database1.table1" AS t1 JOIN "database2.table2" AS t2 ON t1.a = t2.a - """ + """) ) assert list(results) == [(10, "ten"), (20, "twenty")] @@ -254,18 +259,20 @@ def test_dml( conn = engine.connect() - conn.execute('INSERT INTO "database1.table1" (a, b) VALUES (3, 30)') - results = conn.execute('SELECT * FROM "database1.table1"') + conn.execute(text('INSERT INTO "database1.table1" (a, b) VALUES (3, 30)')) + results = conn.execute(text('SELECT * FROM "database1.table1"')) assert list(results) == [(1, 10), (2, 20), (3, 30)] - conn.execute('UPDATE "database1.table1" SET b=35 WHERE a=3') - results = conn.execute('SELECT * FROM "database1.table1"') + conn.execute(text('UPDATE "database1.table1" SET b=35 WHERE a=3')) + results = conn.execute(text('SELECT * FROM "database1.table1"')) assert list(results) == [(1, 10), (2, 20), (3, 35)] - conn.execute('DELETE FROM "database1.table1" WHERE b>20') - results = conn.execute('SELECT * FROM "database1.table1"') + conn.execute(text('DELETE FROM "database1.table1" WHERE b>20')) + results = conn.execute(text('SELECT * FROM "database1.table1"')) assert list(results) == [(1, 10), (2, 20)] with pytest.raises(ProgrammingError) as excinfo: - conn.execute("""INSERT INTO "database2.table2" (a, b) VALUES (3, 'thirty')""") + conn.execute( + text("""INSERT INTO "database2.table2" (a, b) VALUES (3, 'thirty')""") + ) assert str(excinfo.value).strip() == ( "(shillelagh.exceptions.ProgrammingError) DML not enabled in database " '"database2"\n[SQL: INSERT INTO "database2.table2" (a, b) ' @@ -319,7 +326,7 @@ def test_security_manager( conn = engine.connect() with pytest.raises(SupersetSecurityException) as excinfo: - conn.execute('SELECT * FROM "database1.table1"') + conn.execute(text('SELECT * FROM "database1.table1"')) assert str(excinfo.value) == ( "You need access to the following tables: `table1`,\n " "`all_database_access` or `all_datasource_access` permission" @@ -353,11 +360,11 @@ def test_allowed_dbs(mocker: MockerFixture, app_context: None, table1: None) -> conn = engine.connect() - results = conn.execute('SELECT * FROM "database1.table1"') + results = conn.execute(text('SELECT * FROM "database1.table1"')) assert list(results) == [(1, 10), (2, 20)] with pytest.raises(ProgrammingError) as excinfo: - conn.execute('SELECT * FROM "database2.table2"') + conn.execute(text('SELECT * FROM "database2.table2"')) assert str(excinfo.value) == ( """ (shillelagh.exceptions.ProgrammingError) Unsupported table: database2.table2