Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions superset/db_engine_specs/bigquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import logging
import re
import urllib
from collections.abc import Callable
from datetime import datetime
from re import Pattern
from typing import Any, TYPE_CHECKING, TypedDict
Expand All @@ -32,6 +33,7 @@
from marshmallow.exceptions import ValidationError
from sqlalchemy import column, func, types
from sqlalchemy.engine.base import Engine
from sqlalchemy.engine.default import DefaultDialect
from sqlalchemy.engine.interfaces import Dialect
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.engine.url import URL
Expand Down Expand Up @@ -120,6 +122,51 @@ class BigQueryParametersType(TypedDict):
query: dict[str, Any]


class BigQueryStringType(types.TypeDecorator):
"""Custom string type for BigQuery that uses backslash escaping for apostrophes.

BigQuery does not support double-apostrophe escaping ('O''Hara'). Instead,
it requires backslash escaping ('O\\'Hara'). SQLAlchemy's default literal
processor uses double-apostrophe escaping, which causes syntax errors when
filter values contain apostrophes.

See: https://github.com/apache/superset/issues/35857
"""

impl = types.String
cache_ok = True

def literal_processor(self, dialect: DefaultDialect) -> Callable[[Any], str]:
def process(value: Any) -> str:
raw = str(value)
escaped = raw.replace("\\", "\\\\").replace("'", "\\'")
return f"'{escaped}'"

return process


def _monkeypatch_bigquery_dialect() -> None:
"""Monkeypatch the BigQuery dialect to escape apostrophes with backslash.

The sqlalchemy-bigquery dialect incorrectly escapes single quotes by
doubling them ('O''Hara') instead of using backslash escaping ('O\\'Hara').
This causes BigQuery to throw a syntax error when filter values contain
apostrophes.

This follows the same pattern used for the Databricks dialect fix in
superset/db_engine_specs/databricks.py.
"""
try:
from sqlalchemy_bigquery import BigQueryDialect

BigQueryDialect.colspecs[types.String] = BigQueryStringType
except ImportError:
pass


_monkeypatch_bigquery_dialect()


class BigQueryEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-methods
"""Engine spec for Google's BigQuery

Expand Down
36 changes: 36 additions & 0 deletions tests/unit_tests/jinja_context_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,42 @@ def test_where_in() -> None:
assert where_in(["O'Malley's"]) == "('O''Malley''s')"


def test_where_in_bigquery_apostrophe() -> None:
"""
Test that BigQuery dialect uses backslash escaping for apostrophes
instead of double-apostrophe escaping, which causes syntax errors.

See: https://github.com/apache/superset/issues/35857
"""
try:
# Import the Superset BigQuery engine spec so its dialect monkeypatch is applied
from sqlalchemy_bigquery import BigQueryDialect

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is non-deterministic: the BigQuery dialect monkeypatch is applied when superset/db_engine_specs/bigquery.py is imported, but this test only imports sqlalchemy_bigquery.BigQueryDialect. If the BigQuery engine spec module hasn’t been imported earlier in the test run, WhereInMacro(BigQueryDialect()) will still use the unpatched colspecs and this assertion will fail. Import the Superset BigQuery engine spec (or call the patch helper) in the test before instantiating BigQueryDialect so the test doesn’t depend on global import order.

Suggested change
from sqlalchemy_bigquery import BigQueryDialect
from sqlalchemy_bigquery import BigQueryDialect
# Import the Superset BigQuery engine spec so its dialect monkeypatch is applied
from superset.db_engine_specs import bigquery as _bigquery_engine_spec # noqa: F401

Copilot uses AI. Check for mistakes.

from superset.db_engine_specs import (
bigquery as _bigquery_engine_spec, # noqa: F401
)

where_in = WhereInMacro(BigQueryDialect())
result = where_in(["Armando's"])
assert result is not None
# BigQuery requires backslash escaping, not double-apostrophe
assert "''" not in result, (
f"BigQuery should use backslash escaping, got double-apostrophe: {result}"
)
assert "\\'" in result, (
f"BigQuery should escape apostrophes with backslash: {result}"
)

# Verify literal backslashes are also escaped (not just apostrophes)
backslash_result = where_in(["C:\\path"])
assert backslash_result is not None
assert "\\\\" in backslash_result, (
f"BigQuery should escape backslashes: {backslash_result}"
)
except ImportError:
pytest.skip("sqlalchemy-bigquery not installed")


def test_where_in_empty_list() -> None:
"""
Test the ``where_in`` Jinja2 filter when it receives an
Expand Down
Loading