Skip to content

Commit 4b61476

Browse files
authored
Merge branch 'xzkostyan:master' into master
2 parents bbcd884 + 0589171 commit 4b61476

8 files changed

Lines changed: 135 additions & 4 deletions

File tree

clickhouse_sqlalchemy/drivers/base.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
'FixedString': types.String,
5151
'Enum8': types.Enum8,
5252
'Enum16': types.Enum16,
53+
'Object(\'json\')': types.JSON,
5354
'_array': types.Array,
5455
'_nullable': types.Nullable,
5556
'_lowcardinality': types.LowCardinality,
@@ -135,6 +136,16 @@ class ClickHouseDialect(default.DefaultDialect):
135136

136137
inspector = ClickHouseInspector
137138

139+
def __init__(
140+
self,
141+
json_serializer=None,
142+
json_deserializer=None,
143+
**kwargs,
144+
):
145+
default.DefaultDialect.__init__(self, **kwargs)
146+
self._json_deserializer = json_deserializer
147+
self._json_serializer = json_serializer
148+
138149
def initialize(self, connection):
139150
super(ClickHouseDialect, self).initialize(connection)
140151

clickhouse_sqlalchemy/drivers/compilers/typecompiler.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ def visit_numeric(self, type_, **kw):
8787
def visit_boolean(self, type_, **kw):
8888
return 'Bool'
8989

90+
def visit_json(self, type_, **kw):
91+
return 'JSON'
92+
9093
def visit_nested(self, nested, **kwargs):
9194
ddl_compiler = self.dialect.ddl_compiler(self.dialect, None)
9295
cols_create = [
@@ -121,10 +124,26 @@ def visit_ipv6(self, type_, **kw):
121124
return 'IPv6'
122125

123126
def visit_tuple(self, type_, **kw):
124-
cols = (
125-
self.process(type_api.to_instance(nested_type), **kw)
127+
cols = []
128+
is_named_type = all([
129+
isinstance(nested_type, tuple) and len(nested_type) == 2
126130
for nested_type in type_.nested_types
127-
)
131+
])
132+
if is_named_type:
133+
for nested_type in type_.nested_types:
134+
name = nested_type[0]
135+
name_type = nested_type[1]
136+
inner_type = self.process(
137+
type_api.to_instance(name_type),
138+
**kw
139+
)
140+
cols.append(
141+
f'{name} {inner_type}')
142+
else:
143+
cols = (
144+
self.process(type_api.to_instance(nested_type), **kw)
145+
for nested_type in type_.nested_types
146+
)
128147
return 'Tuple(%s)' % ', '.join(cols)
129148

130149
def visit_map(self, type_, **kw):

clickhouse_sqlalchemy/drivers/native/connector.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ def _prepare(self, context=None):
131131
execute_kwargs = {
132132
'settings': settings,
133133
'external_tables': external_tables,
134-
'types_check': execution_options.get('types_check', False)
134+
'types_check': execution_options.get('types_check', False),
135+
'query_id': execution_options.get('query_id', None)
135136
}
136137

137138
return execute, execute_kwargs

clickhouse_sqlalchemy/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
'Decimal',
3232
'IPv4',
3333
'IPv6',
34+
'JSON',
3435
'Nested',
3536
'Tuple',
3637
'Map',
@@ -68,6 +69,7 @@
6869
from .common import Enum8
6970
from .common import Enum16
7071
from .common import Decimal
72+
from .common import JSON
7173
from .common import Tuple
7274
from .common import Map
7375
from .common import AggregateFunction

clickhouse_sqlalchemy/types/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class Boolean(types.Boolean, ClickHouseTypeEngine):
3030
pass
3131

3232

33+
class JSON(types.JSON, ClickHouseTypeEngine):
34+
__visit_name__ = 'json'
35+
36+
3337
class Array(ClickHouseTypeEngine):
3438
__visit_name__ = 'array'
3539

tests/drivers/native/test_cursor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import uuid
2+
13
from sqlalchemy import text
24

35
from tests.testcase import NativeSessionTestCase
@@ -47,3 +49,14 @@ def test_with_settings_in_execution_options(self):
4749
dict(rv.context.execution_options), {"settings": {"final": 1}}
4850
)
4951
self.assertEqual(len(rv.fetchall()), 1)
52+
53+
def test_set_query_id(self):
54+
query_id = str(uuid.uuid4())
55+
rv = self.session.execute(
56+
text(
57+
f"SELECT query_id "
58+
f"FROM system.processes "
59+
f"WHERE query_id = '{query_id}'"
60+
), execution_options={'query_id': query_id}
61+
)
62+
self.assertEqual(rv.fetchall()[0][0], query_id)

tests/test_ddl.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,26 @@ def test_create_table_tuple(self):
296296
'ENGINE = Memory'
297297
)
298298

299+
def test_create_table_named_tuple(self):
300+
table = Table(
301+
't1', self.metadata(),
302+
Column(
303+
'x',
304+
types.Tuple(
305+
('name', types.String),
306+
('value', types.Float32)
307+
)
308+
),
309+
engines.Memory()
310+
)
311+
312+
self.assertEqual(
313+
self.compile(CreateTable(table)),
314+
'CREATE TABLE t1 ('
315+
'x Tuple(name String, value Float32)) '
316+
'ENGINE = Memory'
317+
)
318+
299319
@require_server_version(21, 1, 3)
300320
def test_create_table_map(self):
301321
table = Table(

tests/types/test_json.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import json
2+
from sqlalchemy import Column, text, inspect, func
3+
from sqlalchemy.sql.ddl import CreateTable
4+
5+
from clickhouse_sqlalchemy import types, engines, Table
6+
from tests.testcase import BaseTestCase, CompilationTestCase
7+
from tests.util import class_name_func
8+
from parameterized import parameterized_class
9+
from tests.session import native_session
10+
11+
12+
class JSONCompilationTestCase(CompilationTestCase):
13+
def test_create_table(self):
14+
table = Table(
15+
'test', CompilationTestCase.metadata(),
16+
Column('x', types.JSON),
17+
engines.Memory()
18+
)
19+
20+
self.assertEqual(
21+
self.compile(CreateTable(table)),
22+
'CREATE TABLE test (x JSON) ENGINE = Memory'
23+
)
24+
25+
26+
@parameterized_class(
27+
[{'session': native_session}],
28+
class_name_func=class_name_func
29+
)
30+
class JSONTestCase(BaseTestCase):
31+
required_server_version = (22, 3, 2)
32+
33+
table = Table(
34+
'test', BaseTestCase.metadata(),
35+
Column('x', types.JSON),
36+
engines.Memory()
37+
)
38+
39+
def test_select_insert(self):
40+
data = {'k1': 1, 'k2': '2', 'k3': True}
41+
42+
self.table.drop(bind=self.session.bind, if_exists=True)
43+
try:
44+
# http session is unsupport
45+
self.session.execute(
46+
text('SET allow_experimental_object_type = 1;')
47+
)
48+
self.session.execute(text(self.compile(CreateTable(self.table))))
49+
self.session.execute(self.table.insert(), [{'x': data}])
50+
coltype = inspect(self.session.bind).get_columns('test')[0]['type']
51+
self.assertIsInstance(coltype, types.JSON)
52+
# https://clickhouse.com/docs/en/sql-reference/functions/json-functions#tojsonstring
53+
# The json type returns a tuple of values by default,
54+
# which needs to be converted to json using the
55+
# toJSONString function.
56+
res = self.session.query(
57+
func.toJSONString(self.table.c.x)
58+
).scalar()
59+
self.assertEqual(json.loads(res), data)
60+
finally:
61+
self.table.drop(bind=self.session.bind, if_exists=True)

0 commit comments

Comments
 (0)