diff --git a/docs/expressions.md b/docs/expressions.md index a9b6838..bad4cf0 100644 --- a/docs/expressions.md +++ b/docs/expressions.md @@ -30,7 +30,7 @@ toDayOfWeek(today()) ### Operators -ORM expressions support Python's standard arithmetic operators, so you can compose expressions using `+`, `-`, `*`, `/` and `%`. For example: +ORM expressions support Python's standard arithmetic operators, so you can compose expressions using `+`, `-`, `*`, `/`, `//` and `%`. For example: ```python # A random integer between 1 and 10 F.rand() % 10 + 1 @@ -75,11 +75,12 @@ class Event(Model): ### Which functions are available? -ClickHouse has many hundreds of functions, and new ones often get added. If you encounter a function that the database supports but is not available in the `F` class, please report this via a GitHub issue. You can still use the function by providing its name: +ClickHouse has many hundreds of functions, and new ones often get added. Many, but not all of them, are already covered by the ORM. If you encounter a function that the database supports but is not available in the `F` class, please report this via a GitHub issue. You can still use the function by providing its name: ```python expr = F("someFunctionName", arg1, arg2, ...) ``` +Note that higher-order database functions (those that use lambda expressions) are not supported. --- [<< Models and Databases](models_and_databases.md) | [Table of Contents](toc.md) | [Querysets >>](querysets.md) \ No newline at end of file diff --git a/src/infi/clickhouse_orm/funcs.py b/src/infi/clickhouse_orm/funcs.py index 227f12f..a5bda56 100644 --- a/src/infi/clickhouse_orm/funcs.py +++ b/src/infi/clickhouse_orm/funcs.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, tzinfo +from datetime import date, datetime, tzinfo, timedelta from functools import wraps from inspect import signature, Parameter from types import FunctionType @@ -168,6 +168,12 @@ class FunctionOperatorsMixin(object): def __invert__(self): return F._not(self) + def isIn(self, others): + return F._in(self, others) + + def isNotIn(self, others): + return F._notIn(self, others) + class FMeta(type): @@ -242,7 +248,7 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta): def __repr__(self): return self.to_sql() - def to_sql(self, *args): # FIXME why *args ? + def to_sql(self, *args): """ Generates an SQL string for this function and its arguments. For example if the function name is a symbol of a binary operator: @@ -263,7 +269,7 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta): def _arg_to_sql(arg): """ Converts a function argument to SQL string according to its type. - Supports functions, model fields, strings, dates, datetimes, booleans, + Supports functions, model fields, strings, dates, datetimes, timedeltas, booleans, None, numbers, timezones, arrays/iterables. """ from .fields import Field, StringField, DateTimeField, DateField @@ -277,6 +283,8 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta): return "toDateTime(%s)" % DateTimeField().to_db_string(arg) if isinstance(arg, date): return "toDate('%s')" % arg.isoformat() + if isinstance(arg, timedelta): + return "toIntervalSecond(%d)" % int(arg.total_seconds()) if isinstance(arg, bool): return str(int(arg)) if isinstance(arg, tzinfo): @@ -390,6 +398,18 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta): def _not(a): return F('not', a) + # in / not in + + @staticmethod + @binary_operator + def _in(a, b): + return F('IN', a, b) + + @staticmethod + @binary_operator + def _notIn(a, b): + return F('NOT IN', a, b) + # Functions for working with dates and times @staticmethod @@ -628,6 +648,39 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta): def subtractYears(d, n, timezone=NO_VALUE): return F('subtractYears', d, n, timezone) + @staticmethod + def toIntervalSecond(number): + return F('toIntervalSecond', number) + + @staticmethod + def toIntervalMinute(number): + return F('toIntervalMinute', number) + + @staticmethod + def toIntervalHour(number): + return F('toIntervalHour', number) + + @staticmethod + def toIntervalDay(number): + return F('toIntervalDay', number) + + @staticmethod + def toIntervalWeek(number): + return F('toIntervalWeek', number) + + @staticmethod + def toIntervalMonth(number): + return F('toIntervalMonth', number) + + @staticmethod + def toIntervalQuarter(number): + return F('toIntervalQuarter', number) + + @staticmethod + def toIntervalYear(number): + return F('toIntervalYear', number) + + # Type conversion functions @staticmethod diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 4820368..8ec8b27 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -104,6 +104,13 @@ class FuncsTestCase(TestCaseWithData): self._test_qs(qs.exclude(birthday=F.today()), 100) self._test_qs(qs.filter(birthday__between=['1970-01-01', F.today()]), 100) + def test_in_and_not_in(self): + qs = Person.objects_in(self.database) + self._test_qs(qs.filter(Person.first_name.isIn(['Ciaran', 'Elton'])), 4) + self._test_qs(qs.filter(~Person.first_name.isIn(['Ciaran', 'Elton'])), 96) + self._test_qs(qs.filter(Person.first_name.isNotIn(['Ciaran', 'Elton'])), 96) + self._test_qs(qs.exclude(Person.first_name.isIn(['Ciaran', 'Elton'])), 96) + def test_comparison_operators(self): one = F.plus(1, 0) two = F.plus(1, 1) @@ -247,7 +254,7 @@ class FuncsTestCase(TestCaseWithData): self._test_func(F.toRelativeSecondNum(dt), 1546255353) self._test_func(F.toRelativeSecondNum(dt, 'Europe/Athens'), 1546255353) self._test_func(F.now(), datetime.utcnow().replace(tzinfo=pytz.utc, microsecond=0)) # FIXME this may fail if the timing is just right - self._test_func(F.today(), date.today()) + self._test_func(F.today(), date.today()) # FIXME this may fail if the timing is just right self._test_func(F.yesterday(), date.today() - timedelta(days=1)) self._test_func(F.timeSlot(dt), datetime(2018, 12, 31, 11, 0, 0, tzinfo=pytz.utc)) self._test_func(F.timeSlots(dt, 300), [datetime(2018, 12, 31, 11, 0, 0, tzinfo=pytz.utc)]) @@ -285,6 +292,9 @@ class FuncsTestCase(TestCaseWithData): self._test_func(F.subtractWeeks(dt, 3, 'Europe/Athens')) self._test_func(F.subtractYears(d, 3)) self._test_func(F.subtractYears(dt, 3, 'Europe/Athens')) + self._test_func(F.now() + F.toIntervalSecond(3) + F.toIntervalMinute(3) + F.toIntervalHour(3) + F.toIntervalDay(3)) + self._test_func(F.now() + F.toIntervalWeek(3) + F.toIntervalMonth(3) + F.toIntervalQuarter(3) + F.toIntervalYear(3)) + self._test_func(F.now() + F.toIntervalSecond(3000) - F.toIntervalDay(3000) == F.now() + timedelta(seconds=3000, days=-3000)) def test_type_conversion_functions(self): for f in (F.toUInt8, F.toUInt16, F.toUInt32, F.toUInt64, F.toInt8, F.toInt16, F.toInt32, F.toInt64, F.toFloat32, F.toFloat64):