Skip to content

Commit bc86aa7

Browse files
committed
feat(clickhouse): add dataframe external table support for memtables
1 parent 422c98d commit bc86aa7

File tree

4 files changed

+25
-16
lines changed

4 files changed

+25
-16
lines changed

ibis/backends/base/sql/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,7 @@ def execute(
186186

187187
# register all in memory tables if the backend supports cheap access
188188
# to them
189-
if self.compiler.cheap_in_memory_tables:
190-
for memtable in lin.traverse(_find_memtables, expr):
191-
self._register_in_memory_table(memtable)
189+
self._register_in_memory_tables(expr)
192190

193191
with self._safe_raw_sql(sql, **kwargs) as cursor:
194192
result = self.fetch_from_cursor(cursor, schema)
@@ -201,6 +199,11 @@ def execute(
201199
def _register_in_memory_table(self, table_op):
202200
raise NotImplementedError
203201

202+
def _register_in_memory_tables(self, expr):
203+
if self.compiler.cheap_in_memory_tables:
204+
for memtable in lin.traverse(_find_memtables, expr):
205+
self._register_in_memory_table(memtable)
206+
204207
@abc.abstractmethod
205208
def fetch_from_cursor(self, cursor, schema):
206209
"""Fetch data from cursor."""

ibis/backends/clickhouse/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any, Literal, Mapping
55

66
import pandas as pd
7+
import toolz
78
from clickhouse_driver.client import Client as _DriverClient
89
from pydantic import Field
910

@@ -38,6 +39,13 @@ class Options(ibis.config.BaseModel):
3839
description="Database to use for temporary objects.",
3940
)
4041

42+
def __init__(self, *args, external_tables=None, **kwargs):
43+
super().__init__(*args, **kwargs)
44+
self._external_tables = external_tables or {}
45+
46+
def _register_in_memory_table(self, table_op):
47+
self._external_tables[table_op.name] = table_op.data.to_frame()
48+
4149
def do_connect(
4250
self,
4351
host: str = "localhost",
@@ -49,6 +57,7 @@ def do_connect(
4957
compression: (
5058
Literal["lz4", "lz4hc", "quicklz", "zstd"] | bool
5159
) = _default_compression,
60+
external_tables=None,
5261
**kwargs: Any,
5362
):
5463
"""Create a ClickHouse client for use with Ibis.
@@ -92,6 +101,7 @@ def do_connect(
92101
compression=compression,
93102
**kwargs,
94103
)
104+
self._external_tables = external_tables or {}
95105

96106
@property
97107
def version(self) -> str:
@@ -145,7 +155,9 @@ def raw_sql(
145155
external_tables_list = []
146156
if external_tables is None:
147157
external_tables = {}
148-
for name, df in external_tables.items():
158+
for name, df in toolz.merge(
159+
self._external_tables, external_tables
160+
).items():
149161
if not isinstance(df, pd.DataFrame):
150162
raise TypeError(
151163
'External table is not an instance of pandas dataframe'

ibis/backends/clickhouse/compiler.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ class ClickhouseTableSetFormatter(TableSetFormatter):
7575

7676
_non_equijoin_supported = False
7777

78+
def _format_in_memory_table(self, op):
79+
# We register in memory tables as external tables because clickhouse
80+
# doesn't implement a generic VALUES statement
81+
return op.name
82+
7883

7984
class ClickhouseExprTranslator(ExprTranslator):
8085
_registry = operation_registry
@@ -118,6 +123,7 @@ def day_of_week_name(expr):
118123

119124

120125
class ClickhouseCompiler(Compiler):
126+
cheap_in_memory_tables = True
121127
translator_class = ClickhouseExprTranslator
122128
table_set_formatter_class = ClickhouseTableSetFormatter
123129
select_builder_class = ClickhouseSelectBuilder

ibis/backends/tests/test_client.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -601,10 +601,6 @@ def test_deprecated_path_argument(backend, tmp_path):
601601
),
602602
],
603603
)
604-
@pytest.mark.notyet(
605-
["clickhouse"],
606-
reason="ClickHouse doesn't support a VALUES construct",
607-
)
608604
@pytest.mark.notyet(
609605
["mysql", "sqlite"],
610606
reason="SQLAlchemy generates incorrect code for `VALUES` projections.",
@@ -616,10 +612,6 @@ def test_in_memory_table(backend, con, expr, expected):
616612
backend.assert_frame_equal(result, expected)
617613

618614

619-
@pytest.mark.notyet(
620-
["clickhouse"],
621-
reason="ClickHouse doesn't support a VALUES construct",
622-
)
623615
@pytest.mark.notyet(
624616
["mysql", "sqlite"],
625617
reason="SQLAlchemy generates incorrect code for `VALUES` projections.",
@@ -634,10 +626,6 @@ def test_filter_memory_table(backend, con):
634626
backend.assert_frame_equal(result, expected)
635627

636628

637-
@pytest.mark.notyet(
638-
["clickhouse"],
639-
reason="ClickHouse doesn't support a VALUES construct",
640-
)
641629
@pytest.mark.notyet(
642630
["mysql", "sqlite"],
643631
reason="SQLAlchemy generates incorrect code for `VALUES` projections.",

0 commit comments

Comments
 (0)