mirror of
https://github.com/Infinidat/infi.clickhouse_orm.git
synced 2024-11-22 00:56:34 +03:00
Functions WIP
This commit is contained in:
parent
db3dc70ebf
commit
19439e45ef
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue
Block a user