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 ### 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 ```python
# A random integer between 1 and 10 # A random integer between 1 and 10
F.rand() % 10 + 1 F.rand() % 10 + 1
@ -75,11 +75,12 @@ class Event(Model):
### Which functions are available? ### 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 ```python
expr = F("someFunctionName", arg1, arg2, ...) 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) [<< 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 functools import wraps
from inspect import signature, Parameter from inspect import signature, Parameter
from types import FunctionType from types import FunctionType
@ -168,6 +168,12 @@ class FunctionOperatorsMixin(object):
def __invert__(self): def __invert__(self):
return F._not(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): class FMeta(type):
@ -242,7 +248,7 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta):
def __repr__(self): def __repr__(self):
return self.to_sql() 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. Generates an SQL string for this function and its arguments.
For example if the function name is a symbol of a binary operator: 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): def _arg_to_sql(arg):
""" """
Converts a function argument to SQL string according to its type. 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. None, numbers, timezones, arrays/iterables.
""" """
from .fields import Field, StringField, DateTimeField, DateField 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) return "toDateTime(%s)" % DateTimeField().to_db_string(arg)
if isinstance(arg, date): if isinstance(arg, date):
return "toDate('%s')" % arg.isoformat() return "toDate('%s')" % arg.isoformat()
if isinstance(arg, timedelta):
return "toIntervalSecond(%d)" % int(arg.total_seconds())
if isinstance(arg, bool): if isinstance(arg, bool):
return str(int(arg)) return str(int(arg))
if isinstance(arg, tzinfo): if isinstance(arg, tzinfo):
@ -390,6 +398,18 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta):
def _not(a): def _not(a):
return F('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 # Functions for working with dates and times
@staticmethod @staticmethod
@ -628,6 +648,39 @@ class F(Cond, FunctionOperatorsMixin, metaclass=FMeta):
def subtractYears(d, n, timezone=NO_VALUE): def subtractYears(d, n, timezone=NO_VALUE):
return F('subtractYears', d, n, timezone) 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 # Type conversion functions
@staticmethod @staticmethod

View File

@ -104,6 +104,13 @@ class FuncsTestCase(TestCaseWithData):
self._test_qs(qs.exclude(birthday=F.today()), 100) self._test_qs(qs.exclude(birthday=F.today()), 100)
self._test_qs(qs.filter(birthday__between=['1970-01-01', 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): def test_comparison_operators(self):
one = F.plus(1, 0) one = F.plus(1, 0)
two = F.plus(1, 1) 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), 1546255353)
self._test_func(F.toRelativeSecondNum(dt, 'Europe/Athens'), 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.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.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.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)]) 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.subtractWeeks(dt, 3, 'Europe/Athens'))
self._test_func(F.subtractYears(d, 3)) self._test_func(F.subtractYears(d, 3))
self._test_func(F.subtractYears(dt, 3, 'Europe/Athens')) 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): 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): for f in (F.toUInt8, F.toUInt16, F.toUInt32, F.toUInt64, F.toInt8, F.toInt16, F.toInt32, F.toInt64, F.toFloat32, F.toFloat64):