Skip to content

Commit

Permalink
Handle special enum values #333
Browse files Browse the repository at this point in the history
  • Loading branch information
xzkostyan committed Aug 8, 2024
1 parent b57c569 commit d4bdbe5
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 24 deletions.
2 changes: 1 addition & 1 deletion clickhouse_driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .dbapi import connect


VERSION = (0, 2, 8)
VERSION = (0, 2, 9)
__version__ = '.'.join(str(x) for x in VERSION)

__all__ = ['Client', 'connect']
44 changes: 27 additions & 17 deletions clickhouse_driver/columns/enumcolumn.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
from enum import Enum
from collections import OrderedDict

from .. import errors
from .intcolumn import IntColumn

invalid_names_for_python_enum = frozenset(['mro', ''])


class EnumColumn(IntColumn):
py_types = (Enum, int, str)

def __init__(self, enum_cls, **kwargs):
self.enum_cls = enum_cls
def __init__(self, name_by_value, value_by_name, **kwargs):
self.name_by_value = name_by_value
self.value_by_name = value_by_name
super(EnumColumn, self).__init__(**kwargs)

def before_write_items(self, items, nulls_map=None):
null_value = self.null_value

enum_cls = self.enum_cls
name_by_value = self.name_by_value
value_by_name = self.value_by_name

for i, item in enumerate(items):
if nulls_map and nulls_map[i]:
Expand All @@ -26,29 +30,29 @@ def before_write_items(self, items, nulls_map=None):
# Check real enum value
try:
if isinstance(source_value, str):
items[i] = enum_cls[source_value].value
items[i] = value_by_name[source_value]
else:
items[i] = enum_cls(source_value).value
items[i] = value_by_name[name_by_value[source_value]]
except (ValueError, KeyError):
choices = ', '.join(
"'{}' = {}".format(x.name.replace("'", r"\'"), x.value)
for x in enum_cls
"'{}' = {}".format(name.replace("'", r"\'"), value)
for name, value in value_by_name.items()
)
enum_str = '{}({})'.format(enum_cls.__name__, choices)
enum_str = '{}({})'.format(self.ch_type, choices)

raise errors.LogicalError(
"Unknown element '{}' for type {}"
.format(source_value, enum_str)
)

def after_read_items(self, items, nulls_map=None):
enum_cls = self.enum_cls
name_by_value = self.name_by_value

if nulls_map is None:
return tuple(enum_cls(item).name for item in items)
return tuple(name_by_value[item] for item in items)
else:
return tuple(
(None if is_null else enum_cls(items[i]).name)
(None if is_null else name_by_value[items[i]])
for i, is_null in enumerate(nulls_map)
)

Expand All @@ -73,11 +77,13 @@ def create_enum_column(spec, column_options):
params = spec[7:-1]
cls = Enum16Column

return cls(Enum(cls.ch_type, _parse_options(params)), **column_options)
name_by_value, value_by_name = _parse_options(params)

return cls(name_by_value, value_by_name, **column_options)


def _parse_options(option_string):
options = dict()
name_by_value, value_by_name = {}, OrderedDict()
after_name = False
escaped = False
quote_character = None
Expand All @@ -93,7 +99,9 @@ def _parse_options(option_string):
if ch in (' ', '='):
pass
elif ch == ',':
options[name] = int(value)
value = int(value)
name_by_value[value] = name
value_by_name[name] = value
after_name = False
name = ''
value = '' # reset before collecting new option
Expand All @@ -114,6 +122,8 @@ def _parse_options(option_string):
quote_character = ch

if after_name:
options.setdefault(name, int(value)) # append word after last comma
value = int(value)
name_by_value[value] = name
value_by_name[name] = value

return options
return name_by_value, value_by_name
8 changes: 2 additions & 6 deletions docs/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,8 @@ SELECT type: :class:`str`.
>>> client.execute('SELECT * FROM test')
[('foo',), ('bar',), ('foo',)]
Currently clickhouse-driver can't handle empty enum value due to Python's `Enum` mechanics.
Enum member name must be not empty. See `issue`_ and `workaround`_.

.. _issue: https://github.com/mymarilyn/clickhouse-driver/issues/48
.. _workaround: https://github.com/mymarilyn/clickhouse-driver/issues/48#issuecomment-412480613

*Starting from version 0.2.9* clickhouse-driver can handle special enum values:
``mro`` and ``''``.

Array(T)
--------
Expand Down
24 changes: 24 additions & 0 deletions tests/columns/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,27 @@ def test_nullable(self):
(None, ), ('hello', ), (None, ), ('world', ),
]
)

def test_invalid_python_names(self):
data = [(1, ), (2, ), (3, )]
with self.create_table("a Enum8('mro' = 1, '' = 2, 'test' = 3)"):
self.client.execute(
'INSERT INTO test (a) VALUES', data
)

query = 'SELECT * FROM test'
inserted = self.emit_cli(query)
self.assertEqual(
inserted, (
'mro\n'
'\n'
'test\n'
)
)

inserted = self.client.execute(query)
self.assertEqual(
inserted, [
('mro', ), ('', ), ('test', )
]
)

0 comments on commit d4bdbe5

Please sign in to comment.