Repository: kayak/pypika
Pull request #294 (2019-08-14 16:49:19)

clickhouse base type_conversion + search_string
Added part of functions to type conversion. Also a few search functions(_match, like, notLike, multiSearchAny, multiMatchAny_).
Tests added. Let me know if you have any questions.

Thank you.


Patch:
diff --git a/pypika/clickhouse/__init__.py b/pypika/clickhouse/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pypika/clickhouse/search_string.py b/pypika/clickhouse/search_string.py
new file mode 100644
index 0000000..58a4b34
--- /dev/null
+++ b/pypika/clickhouse/search_string.py
@@ -0,0 +1,97 @@
+import abc
+
+from pypika.terms import Function
+
+
+class _AbstractSearchString(Function, metaclass=abc.ABCMeta):
+    def __init__(self, name, pattern: str, alias: str = None):
+        super(_AbstractSearchString, self).__init__(
+            self.clickhouse_function(),
+            name,
+            alias=alias
+        )
+
+        self._pattern = pattern
+
+    @classmethod
+    @abc.abstractmethod
+    def clickhouse_function(cls) -> str:
+        pass
+
+    def get_sql(self, with_alias=False, with_namespace=False, quote_char=None, dialect=None,
+                **kwargs):
+        args = []
+        for p in self.args:
+            if hasattr(p, 'get_sql'):
+                args.append('toString("{arg}")'.format(arg=p.get_sql(with_alias=False, **kwargs)))
+            else:
+                args.append(str(p))
+
+        return '{name}({args},\'{pattern}\'){alias}'.format(
+            name=self.name,
+            args=','.join(args),
+            pattern=self._pattern,
+            alias=' ' + self.alias if self.alias else ''
+        )
+
+
+class Match(_AbstractSearchString):
+    @classmethod
+    def clickhouse_function(cls) -> str:
+        return 'match'
+
+
+class Like(_AbstractSearchString):
+    @classmethod
+    def clickhouse_function(cls) -> str:
+        return 'like'
+
+
+class NotLike(_AbstractSearchString):
+    @classmethod
+    def clickhouse_function(cls) -> str:
+        return 'notLike'
+
+
+class _AbstractMultiSearchString(Function, metaclass=abc.ABCMeta):
+    def __init__(self, name, patterns: list, alias: str = None):
+        super(_AbstractMultiSearchString, self).__init__(
+            self.clickhouse_function(),
+            name,
+            alias=alias
+        )
+
+        self._patterns = patterns
+
+    @classmethod
+    @abc.abstractmethod
+    def clickhouse_function(cls) -> str:
+        pass
+
+    def get_sql(self, with_alias=False, with_namespace=False, quote_char=None, dialect=None,
+                **kwargs):
+        args = []
+        for p in self.args:
+            if hasattr(p, 'get_sql'):
+                args.append('toString("{arg}")'.format(arg=p.get_sql(with_alias=False, **kwargs)))
+            else:
+                args.append(str(p))
+
+        return '{name}({args},[{patterns}]){alias}'.format(
+            name=self.name,
+            args=','.join(args),
+            patterns=','.join(["'%s'" % i for i in self._patterns]),
+            alias=' ' + self.alias if self.alias else ''
+        )
+
+
+class MultiSearchAny(_AbstractMultiSearchString):
+    @classmethod
+    def clickhouse_function(cls) -> str:
+        return 'multiSearchAny'
+
+
+class MultiMatchAny(_AbstractMultiSearchString):
+    @classmethod
+    def clickhouse_function(cls) -> str:
+        return 'multiMatchAny'
diff --git a/pypika/clickhouse/type_conversion.py b/pypika/clickhouse/type_conversion.py
new file mode 100644
index 0000000..70e9a17
--- /dev/null
+++ b/pypika/clickhouse/type_conversion.py
@@ -0,0 +1,84 @@
+from pypika.terms import Function, Field
+
+
+class ToString(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToString, self).__init__('toString', name, alias=alias)
+
+
+class ToFixedString(Function):
+    def __init__(self, field, length: int, alias: str = None, schema: str = None):
+        self._length = length
+        self._field = field
+        self.alias = alias
+        self.name = 'toFixedString'
+        self.schema = schema
+
+    def get_sql(self, with_alias=False, with_namespace=False, quote_char=None, dialect=None,
+                **kwargs):
+        return '{name}({field},{length}){alias}'.format(
+            name=self.name,
+            field=self._field if isinstance(self._field, Field) else "'%s'" % str(self._field),
+            length=self._length,
+            alias=' ' + self.alias if self.alias else ''
+        )
+
+
+class ToInt8(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToInt8, self).__init__('toInt8', name, alias=alias)
+
+
+class ToInt16(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToInt16, self).__init__('toInt16', name, alias=alias)
+
+
+class ToInt32(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToInt32, self).__init__('toInt32', name, alias=alias)
+
+
+class ToInt64(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToInt64, self).__init__('toInt64', name, alias=alias)
+
+
+class ToUInt8(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToUInt8, self).__init__('toUInt8', name, alias=alias)
+
+
+class ToUInt16(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToUInt16, self).__init__('toUInt16', name, alias=alias)
+
+
+class ToUInt32(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToUInt32, self).__init__('toUInt32', name, alias=alias)
+
+
+class ToUInt64(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToUInt64, self).__init__('toUInt64', name, alias=alias)
+
+
+class ToFloat32(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToFloat32, self).__init__('toFloat32', name, alias=alias)
+
+
+class ToFloat64(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToFloat64, self).__init__('toFloat64', name, alias=alias)
+
+
+class ToDate(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToDate, self).__init__('toDate', name, alias=alias)
+
+
+class ToDateTime(Function):
+    def __init__(self, name, alias: str = None):
+        super(ToDateTime, self).__init__('toDateTime', name, alias=alias)


Test patch:
diff --git a/pypika/tests/clickhouse/__init__.py b/pypika/tests/clickhouse/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pypika/tests/clickhouse/test_search_string.py b/pypika/tests/clickhouse/test_search_string.py
new file mode 100644
index 0000000..20e6de4
--- /dev/null
+++ b/pypika/tests/clickhouse/test_search_string.py
@@ -0,0 +1,33 @@
+import unittest
+
+from pypika import Field
+from pypika.clickhouse.search_string import Match, Like, NotLike, MultiSearchAny, MultiMatchAny
+
+
+class TestSearchString(unittest.TestCase):
+    def test_search_string(self):
+        search_types = (
+            (Match, "match(toString(\"name\"),'twheys')"),
+            (Like, "like(toString(\"name\"),'twheys')"),
+            (NotLike, "notLike(toString(\"name\"),'twheys')"),
+        )
+
+        for cls, expected in search_types:
+            self.assertEqual(
+                cls(Field('name'), 'twheys'),
+                expected
+            )
+
+
+class TestMultiSearch(unittest.TestCase):
+    def test_multi_search_string(self):
+        search_types = (
+            (MultiSearchAny, "multiSearchAny(toString(\"name\"),['sarah','connor'])"),
+            (MultiMatchAny, "multiMatchAny(toString(\"name\"),['sarah','connor'])"),
+        )
+
+        for cls, expected in search_types:
+            self.assertEqual(
+                cls(Field('name'), ['sarah', 'connor']).get_sql(),
+                expected
+            )
diff --git a/pypika/tests/clickhouse/test_type_conversion.py b/pypika/tests/clickhouse/test_type_conversion.py
new file mode 100644
index 0000000..77e298d
--- /dev/null
+++ b/pypika/tests/clickhouse/test_type_conversion.py
@@ -0,0 +1,81 @@
+import unittest
+
+from pypika import Field
+from pypika.clickhouse.type_conversion import (
+    ToString,
+    ToInt8,
+    ToInt16,
+    ToInt32,
+    ToInt64,
+    ToUInt8,
+    ToUInt16,
+    ToUInt32,
+    ToUInt64,
+    ToFloat32,
+    ToFloat64,
+    ToDate,
+    ToDateTime,
+    ToFixedString,
+)
+
+
+class TestBasicTypeConverters(unittest.TestCase):
+    def test_basic_types_field(self):
+        types_map = (
+            ('toString("field_name")', ToString,),
+            ('toInt8("field_name")', ToInt8,),
+            ('toInt16("field_name")', ToInt16,),
+            ('toInt32("field_name")', ToInt32,),
+            ('toInt64("field_name")', ToInt64,),
+            ('toUInt8("field_name")', ToUInt8,),
+            ('toUInt16("field_name")', ToUInt16,),
+            ('toUInt32("field_name")', ToUInt32,),
+            ('toUInt64("field_name")', ToUInt64,),
+            ('toFloat32("field_name")', ToFloat32,),
+            ('toFloat64("field_name")', ToFloat64,),
+            ('toFloat64("field_name")', ToFloat64,),
+            ('toDate("field_name")', ToDate,),
+            ('toDateTime("field_name")', ToDateTime,),
+        )
+
+        for expected, cls in types_map:
+            self.assertEqual(
+                cls(Field('field_name')),
+                expected
+            )
+
+    def test_basic_types_value(self):
+        types_map = (
+            ("toString('100')", ToString,),
+            ("toInt8('100')", ToInt8,),
+            ("toInt16('100')", ToInt16,),
+            ("toInt32('100')", ToInt32,),
+            ("toInt64('100')", ToInt64,),
+            ("toUInt8('100')", ToUInt8,),
+            ("toUInt16('100')", ToUInt16,),
+            ("toUInt32('100')", ToUInt32,),
+            ("toUInt64('100')", ToUInt64,),
+            ("toFloat32('100')", ToFloat32,),
+            ("toFloat64('100')", ToFloat64,),
+            ("toFloat64('100')", ToFloat64,),
+            ("toDate('100')", ToDate,),
+            ("toDateTime('100')", ToDateTime,),
+        )
+
+        for expected, cls in types_map:
+            self.assertEqual(cls('100'), expected)
+
+
+class TestToFixedString(unittest.TestCase):
+
+    def test_to_fixed_string_field(self):
+        self.assertEqual(
+            ToFixedString(Field('field_name'), 100).get_sql(),
+            'toFixedString("field_name",100)'
+        )
+
+    def test_to_fixed_string_value(self):
+        self.assertEqual(
+            ToFixedString('100', 100).get_sql(),
+            "toFixedString('100',100)"
+        )