Files
connect/tests/unit_tests/test_sqlalchemy/test_percent_encoding.py
Joe SpadolaandGitHub 55dcd7db4c Replace pylint with ruff for linting and formatting (#703)
* migrate from pylint to ruff

* ruff format/lint switch over

* add formatting commit

* update changelog
2026-04-02 16:09:59 -07:00

105 lines
3.5 KiB
Python

"""
Tests for correct handling of percent signs (%) in SQL queries through the
SQLAlchemy compilation pipeline.
See https://github.com/ClickHouse/clickhouse-connect/issues/297
The pyformat paramstyle (PEP 249) requires that literal % be doubled to %%
during compilation, and then unescaped by the DBAPI cursor. This must work
correctly for both parameterized and non-parameterized queries.
"""
from unittest.mock import Mock
from sqlalchemy import text
from clickhouse_connect import dbapi
from clickhouse_connect.cc_sqlalchemy.dialect import ClickHouseDialect
from clickhouse_connect.dbapi.cursor import Cursor
def _make_dialect():
"""Create a dialect that mirrors real create_engine() behavior."""
return ClickHouseDialect(dbapi=dbapi)
def _make_cursor():
"""Create a Cursor backed by a mock client."""
client = Mock()
query_result = Mock()
query_result.result_set = []
query_result.column_names = ["formatted"]
query_result.column_types = [Mock(name="String")]
query_result.summary = {}
client.query.return_value = query_result
return Cursor(client), client
def test_percent_in_non_parameterized_query():
"""Literal % in a non-parameterized text() query must survive compilation
and reach the driver as a single %."""
dialect = _make_dialect()
stmt = text("SELECT formatDateTime(toDate('2010-01-04'), '%g')")
compiled = stmt.compile(dialect=dialect)
compiled_sql = compiled.string
# SQLAlchemy should have doubled the % for pyformat
assert "%%g" in compiled_sql
# Now simulate what the dialect's do_execute_no_params does:
# cursor.execute(compiled_sql) -- no parameters
cursor, client = _make_cursor()
cursor.execute(compiled_sql)
actual_query = client.query.call_args[0][0]
assert "%g" in actual_query
assert "%%g" not in actual_query
def test_percent_in_parameterized_query():
"""Literal % in a parameterized text() query must survive both compilation
and parameter substitution."""
dialect = _make_dialect()
stmt = text("SELECT formatDateTime(toDate(:d), '%g')")
compiled = stmt.compile(dialect=dialect)
compiled_sql = compiled.string
# Should have %(d)s for the bind param and %%g for the literal %
assert "%(d)s" in compiled_sql
assert "%%g" in compiled_sql
# Simulate do_execute with parameters (as SQLAlchemy would call it)
cursor, client = _make_cursor()
cursor.execute(compiled_sql, {"d": "2010-01-04"})
# Parameters are passed through; finalize_query in the driver handles
# both %(d)s substitution and %% -> % unescaping via Python's % operator
actual_query = client.query.call_args[0][0]
actual_params = client.query.call_args[0][1]
assert actual_query == compiled_sql
assert actual_params == {"d": "2010-01-04"}
def test_format_datetime_full_pattern():
"""A realistic formatDateTime pattern with many % format specifiers."""
dialect = _make_dialect()
stmt = text("SELECT formatDateTime(now(), '%Y-%m-%d %H:%M:%S')")
compiled = stmt.compile(dialect=dialect)
cursor, client = _make_cursor()
cursor.execute(compiled.string)
actual_query = client.query.call_args[0][0]
assert actual_query == "SELECT formatDateTime(now(), '%Y-%m-%d %H:%M:%S')"
def test_preparer_double_percents_enabled():
"""Verify that the ClickHouse dialect keeps _double_percents = True
(the default for pyformat paramstyle), rather than disabling it."""
dialect = _make_dialect()
preparer = dialect.preparer(dialect)
assert preparer._double_percents is True