Functions WIP

This commit is contained in:
Itai Shirav 2020-04-14 06:24:37 +03:00
parent db3dc70ebf
commit 19439e45ef
3 changed files with 70 additions and 6 deletions

View File

@ -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)

View File

@ -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

View File

@ -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):