Skip to content

Commit

Permalink
Fixes ClickHouse#159 - quoting and escaping string literals inside lists
Browse files Browse the repository at this point in the history
  • Loading branch information
ewjoachim committed Apr 3, 2023
1 parent b1c78f8 commit 5f43508
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ in a future release. Starting with 0.5.9 the driver now requests ClickHouse pro
The secondary effect of the `send_progress` argument -- to set `wait_end_of_query=1` -- is now handled automatically based
on whether the query is streaming or not.

## [next release]
### Fix quoting and escaping of array literals in server parameters
See [#159](https://github.com/ClickHouse/clickhouse-connect/issues/159)

## 0.5.18, 2023-03-30
### Performance Improvement
- The server timezone will not be applied (and Python datetime types will be timezone naive) if the client and server timezones match
Expand Down
22 changes: 16 additions & 6 deletions clickhouse_connect/driver/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,10 @@ def bind_query(query: str, parameters: Optional[Union[Sequence, Dict[str, Any]]]


def format_str(value: str):
return f"'{''.join(f'{BS}{c}' if c in must_escape else c for c in value)}'"
return f"'{escape_str(value)}'"

def escape_str(value: str):
return ''.join(f'{BS}{c}' if c in must_escape else c for c in value)


# pylint: disable=too-many-return-statements
Expand Down Expand Up @@ -431,33 +434,40 @@ def format_query_value(value: Any, server_tz: tzinfo = pytz.UTC):
return str(value)


def format_bind_value(value: Any, server_tz: tzinfo = pytz.UTC):
def format_bind_value(value: Any, server_tz: tzinfo = pytz.UTC, top_level: bool = True):
"""
Format Python values in a ClickHouse query
:param value: Python object
:param server_tz: Server timezone for adjusting datetime values
:return: Literal string for python value
"""
recurse = lambda x: format_bind_value(x, server_tz=server_tz, top_level=False)
if value is None:
return 'NULL'
if isinstance(value, str):
if not top_level:
# At the top levels, strings must not be surrounded by quotes
return format_str(value)
else:
return escape_str(value)
if isinstance(value, datetime):
if value.tzinfo is None and server_tz != local_tz:
value = value.replace(tzinfo=server_tz)
return value.strftime('%Y-%m-%d %H:%M:%S')
if isinstance(value, date):
return value.isoformat()
if isinstance(value, list):
return f"[{', '.join(format_bind_value(x, server_tz) for x in value)}]"
return f"[{', '.join(recurse(x) for x in value)}]"
if isinstance(value, tuple):
return f"({', '.join(format_bind_value(x, server_tz) for x in value)})"
return f"({', '.join(recurse(x) for x in value)})"
if isinstance(value, dict):
if common.get_setting('dict_parameter_format') == 'json':
return any_to_json(value).decode()
pairs = [format_bind_value(k, server_tz) + ':' + format_bind_value(v, server_tz)
pairs = [recurse(k) + ':' + recurse(v)
for k, v in value.items()]
return f"{{{', '.join(pairs)}}}"
if isinstance(value, Enum):
return format_bind_value(value.value, server_tz)
return recurse(value.value)
return str(value)


Expand Down
20 changes: 19 additions & 1 deletion tests/unit_tests/test_driver/test_parameters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import datetime

from clickhouse_connect.driver.query import finalize_query
import pytest

from clickhouse_connect.driver.query import finalize_query, format_bind_value


def test_finalize():
Expand All @@ -14,3 +16,19 @@ def test_finalize():
parameters = [hash_id, timestamp]
query = finalize_query('SELECT hash_id FROM db.mytable WHERE hash_id = %s AND dt = %s', parameters)
assert query == expected


@pytest.mark.parametrize("value, expected", [
("a", "a"),
("a'", r"a\'"),
("'a'", r"\'a\'"),
("''a'", r"\'\'a\'"),
([], "[]"),
([1], "[1]"),
(["a"], "['a']"),
(["a'"], r"['a\'']"),
([["a"]], "[['a']]"),
])
def test_format_bind_value(value, expected):
assert format_bind_value(value) == expected

0 comments on commit 5f43508

Please sign in to comment.