mirror of
https://github.com/Infinidat/infi.clickhouse_orm.git
synced 2024-11-22 00:56:34 +03:00
- Drop py2.7 support
- Add ipv4/6 fields and funcs - Support funcs as alias/materialized expressions
This commit is contained in:
parent
cc0f2c4e91
commit
969070f1ae
|
@ -31,7 +31,7 @@ homepage = https://github.com/Infinidat/infi.clickhouse_orm
|
|||
|
||||
[isolated-python]
|
||||
recipe = infi.recipe.python
|
||||
version = v2.7.12.4
|
||||
version = v3.7.0.4
|
||||
|
||||
[setup.py]
|
||||
recipe = infi.recipe.template.version
|
||||
|
|
|
@ -5,31 +5,33 @@ See: [ClickHouse Documentation](https://clickhouse.yandex/docs/en/data_types/)
|
|||
|
||||
Currently the following field types are supported:
|
||||
|
||||
| Class | DB Type | Pythonic Type | Comments
|
||||
| ------------------ | ---------- | ------------------- | -----------------------------------------------------
|
||||
| StringField | String | unicode | Encoded as UTF-8 when written to ClickHouse
|
||||
| FixedStringField | String | unicode | Encoded as UTF-8 when written to ClickHouse
|
||||
| DateField | Date | datetime.date | Range 1970-01-01 to 2105-12-31
|
||||
| DateTimeField | DateTime | datetime.datetime | Minimal value is 1970-01-01 00:00:00; Always in UTC
|
||||
| Int8Field | Int8 | int | Range -128 to 127
|
||||
| Int16Field | Int16 | int | Range -32768 to 32767
|
||||
| Int32Field | Int32 | int | Range -2147483648 to 2147483647
|
||||
| Int64Field | Int64 | int/long | Range -9223372036854775808 to 9223372036854775807
|
||||
| UInt8Field | UInt8 | int | Range 0 to 255
|
||||
| UInt16Field | UInt16 | int | Range 0 to 65535
|
||||
| UInt32Field | UInt32 | int | Range 0 to 4294967295
|
||||
| UInt64Field | UInt64 | int/long | Range 0 to 18446744073709551615
|
||||
| Float32Field | Float32 | float |
|
||||
| Float64Field | Float64 | float |
|
||||
| DecimalField | Decimal | Decimal | Pythonic values are rounded to fit the scale of the database field
|
||||
| Decimal32Field | Decimal32 | Decimal | Ditto
|
||||
| Decimal64Field | Decimal64 | Decimal | Ditto
|
||||
| Decimal128Field | Decimal128 | Decimal | Ditto
|
||||
| UUIDField | UUID | Decimal |
|
||||
| Enum8Field | Enum8 | Enum | See below
|
||||
| Enum16Field | Enum16 | Enum | See below
|
||||
| ArrayField | Array | list | See below
|
||||
| NullableField | Nullable | See below | See below
|
||||
| Class | DB Type | Pythonic Type | Comments
|
||||
| ------------------ | ---------- | --------------------- | -----------------------------------------------------
|
||||
| StringField | String | unicode | Encoded as UTF-8 when written to ClickHouse
|
||||
| FixedStringField | String | unicode | Encoded as UTF-8 when written to ClickHouse
|
||||
| DateField | Date | datetime.date | Range 1970-01-01 to 2105-12-31
|
||||
| DateTimeField | DateTime | datetime.datetime | Minimal value is 1970-01-01 00:00:00; Always in UTC
|
||||
| Int8Field | Int8 | int | Range -128 to 127
|
||||
| Int16Field | Int16 | int | Range -32768 to 32767
|
||||
| Int32Field | Int32 | int | Range -2147483648 to 2147483647
|
||||
| Int64Field | Int64 | int/long | Range -9223372036854775808 to 9223372036854775807
|
||||
| UInt8Field | UInt8 | int | Range 0 to 255
|
||||
| UInt16Field | UInt16 | int | Range 0 to 65535
|
||||
| UInt32Field | UInt32 | int | Range 0 to 4294967295
|
||||
| UInt64Field | UInt64 | int/long | Range 0 to 18446744073709551615
|
||||
| Float32Field | Float32 | float |
|
||||
| Float64Field | Float64 | float |
|
||||
| DecimalField | Decimal | Decimal | Pythonic values are rounded to fit the scale of the database field
|
||||
| Decimal32Field | Decimal32 | Decimal | Ditto
|
||||
| Decimal64Field | Decimal64 | Decimal | Ditto
|
||||
| Decimal128Field | Decimal128 | Decimal | Ditto
|
||||
| UUIDField | UUID | uuid.UUID |
|
||||
| IPv4Field | IPv4 | ipaddress.IPv4Address |
|
||||
| IPv6Field | IPv6 | ipaddress.IPv6Address |
|
||||
| Enum8Field | Enum8 | Enum | See below
|
||||
| Enum16Field | Enum16 | Enum | See below
|
||||
| ArrayField | Array | list | See below
|
||||
| NullableField | Nullable | See below | See below
|
||||
|
||||
DateTimeField and Time Zones
|
||||
----------------------------
|
||||
|
@ -51,8 +53,6 @@ Working with enum fields
|
|||
|
||||
`Enum8Field` and `Enum16Field` provide support for working with ClickHouse enum columns. They accept strings or integers as values, and convert them to the matching Pythonic Enum member.
|
||||
|
||||
Python 3.4 and higher supports Enums natively. When using previous Python versions you need to install the enum34 library.
|
||||
|
||||
Example of a model with an enum field:
|
||||
|
||||
```python
|
||||
|
|
|
@ -3,7 +3,7 @@ Overview
|
|||
|
||||
This project is simple ORM for working with the [ClickHouse database](https://clickhouse.yandex/). It allows you to define model classes whose instances can be written to the database and read from it.
|
||||
|
||||
It was tested on Python 2.7 and 3.5.
|
||||
Version 1.x supports Python 2.7 and 3.5+. Version 2.x dropped support for Python 2.7, and works only with Python 3.5+.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
|
|
@ -7,11 +7,13 @@ from calendar import timegm
|
|||
from decimal import Decimal, localcontext
|
||||
from uuid import UUID
|
||||
from logging import getLogger
|
||||
from .utils import escape, parse_array, comma_join
|
||||
from .utils import escape, parse_array, comma_join, string_or_func
|
||||
from .funcs import F, FunctionOperatorsMixin
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
|
||||
logger = getLogger('clickhouse_orm')
|
||||
|
||||
|
||||
class Field(FunctionOperatorsMixin):
|
||||
'''
|
||||
Abstract base class for all field types.
|
||||
|
@ -25,10 +27,10 @@ class Field(FunctionOperatorsMixin):
|
|||
def __init__(self, default=None, alias=None, materialized=None, readonly=None, codec=None):
|
||||
assert (None, None) in {(default, alias), (alias, materialized), (default, materialized)}, \
|
||||
"Only one of default, alias and materialized parameters can be given"
|
||||
assert alias is None or isinstance(alias, string_types) and alias != "",\
|
||||
"Alias field must be a string, if given"
|
||||
assert materialized is None or isinstance(materialized, string_types) and materialized != "",\
|
||||
"Materialized field must be string, if given"
|
||||
assert alias is None or isinstance(alias, F) or isinstance(alias, string_types) and alias != "",\
|
||||
"Alias parameter must be a string or function object, if given"
|
||||
assert materialized is None or isinstance(materialized, F) or isinstance(materialized, string_types) and materialized != "",\
|
||||
"Materialized parameter must be a string or function object, if given"
|
||||
assert readonly is None or type(readonly) is bool, "readonly parameter must be bool if given"
|
||||
assert codec is None or isinstance(codec, string_types) and codec != "", \
|
||||
"Codec field must be string, if given"
|
||||
|
@ -85,9 +87,9 @@ class Field(FunctionOperatorsMixin):
|
|||
def _extra_params(self, db):
|
||||
sql = ''
|
||||
if self.alias:
|
||||
sql += ' ALIAS %s' % self.alias
|
||||
sql += ' ALIAS %s' % string_or_func(self.alias)
|
||||
elif self.materialized:
|
||||
sql += ' MATERIALIZED %s' % self.materialized
|
||||
sql += ' MATERIALIZED %s' % string_or_func(self.materialized)
|
||||
elif self.default:
|
||||
default = self.to_db_string(self.default)
|
||||
sql += ' DEFAULT %s' % default
|
||||
|
@ -511,12 +513,47 @@ class UUIDField(Field):
|
|||
return escape(str(value), quote)
|
||||
|
||||
|
||||
class IPv4Field(Field):
|
||||
|
||||
class_default = 0
|
||||
db_type = 'IPv4'
|
||||
|
||||
def to_python(self, value, timezone_in_use):
|
||||
if isinstance(value, IPv4Address):
|
||||
return value
|
||||
elif isinstance(value, (binary_type,) + string_types + integer_types):
|
||||
return IPv4Address(value)
|
||||
else:
|
||||
raise ValueError('Invalid value for IPv4Address: %r' % value)
|
||||
|
||||
def to_db_string(self, value, quote=True):
|
||||
return escape(str(value), quote)
|
||||
|
||||
|
||||
class IPv6Field(Field):
|
||||
|
||||
class_default = 0
|
||||
db_type = 'IPv6'
|
||||
|
||||
def to_python(self, value, timezone_in_use):
|
||||
if isinstance(value, IPv6Address):
|
||||
return value
|
||||
elif isinstance(value, (binary_type,) + string_types + integer_types):
|
||||
return IPv6Address(value)
|
||||
else:
|
||||
raise ValueError('Invalid value for IPv6Address: %r' % value)
|
||||
|
||||
def to_db_string(self, value, quote=True):
|
||||
return escape(str(value), quote)
|
||||
|
||||
|
||||
class NullableField(Field):
|
||||
|
||||
class_default = None
|
||||
|
||||
def __init__(self, inner_field, default=None, alias=None, materialized=None,
|
||||
extra_null_values=None, codec=None):
|
||||
assert isinstance(inner_field, Field), "The first argument of NullableField must be a Field instance. Not: {}".format(inner_field)
|
||||
self.inner_field = inner_field
|
||||
self._null_values = [None]
|
||||
if extra_null_values:
|
||||
|
|
|
@ -63,10 +63,10 @@ class FunctionOperatorsMixin(object):
|
|||
def __rmul__(self, other):
|
||||
return F.multiply(other, self)
|
||||
|
||||
def __div__(self, other):
|
||||
def __truediv__(self, other):
|
||||
return F.divide(self, other)
|
||||
|
||||
def __rdiv__(self, other):
|
||||
def __rtruediv__(self, other):
|
||||
return F.divide(other, self)
|
||||
|
||||
def __mod__(self, other):
|
||||
|
@ -115,7 +115,7 @@ class F(Cond, FunctionOperatorsMixin):
|
|||
self.args = args
|
||||
self.is_binary_operator = False
|
||||
|
||||
def to_sql(self, *args):
|
||||
def to_sql(self, *args): # FIXME why *args ?
|
||||
"""
|
||||
Generates an SQL string for this function and its arguments.
|
||||
For example if the function name is a symbol of a binary operator:
|
||||
|
@ -129,10 +129,11 @@ class F(Cond, FunctionOperatorsMixin):
|
|||
else:
|
||||
prefix = self.name
|
||||
sep = ', '
|
||||
arg_strs = (self.arg_to_sql(arg) for arg in self.args)
|
||||
arg_strs = (F.arg_to_sql(arg) for arg in self.args)
|
||||
return prefix + '(' + sep.join(arg_strs) + ')'
|
||||
|
||||
def arg_to_sql(self, arg):
|
||||
@staticmethod
|
||||
def arg_to_sql(arg):
|
||||
"""
|
||||
Converts a function argument to SQL string according to its type.
|
||||
Supports functions, model fields, strings, dates, datetimes, booleans,
|
||||
|
@ -156,7 +157,7 @@ class F(Cond, FunctionOperatorsMixin):
|
|||
if arg is None:
|
||||
return 'NULL'
|
||||
if is_iterable(arg):
|
||||
return '[' + comma_join(self.arg_to_sql(x) for x in arg) + ']'
|
||||
return '[' + comma_join(F.arg_to_sql(x) for x in arg) + ']'
|
||||
return six.text_type(arg)
|
||||
|
||||
# Arithmetic functions
|
||||
|
@ -384,6 +385,70 @@ class F(Cond, FunctionOperatorsMixin):
|
|||
def formatDateTime(d, format, timezone=''):
|
||||
return F('formatDateTime', d, format, timezone)
|
||||
|
||||
@staticmethod
|
||||
def addDays(d, n, timezone=None):
|
||||
return F('addDays', d, n, timezone) if timezone else F('addDays', d, n)
|
||||
|
||||
@staticmethod
|
||||
def addHours(d, n, timezone=None):
|
||||
return F('addHours', d, n, timezone) if timezone else F('addHours', d, n)
|
||||
|
||||
@staticmethod
|
||||
def addMinutes(d, n, timezone=None):
|
||||
return F('addMinutes', d, n, timezone) if timezone else F('addMinutes', d, n)
|
||||
|
||||
@staticmethod
|
||||
def addMonths(d, n, timezone=None):
|
||||
return F('addMonths', d, n, timezone) if timezone else F('addMonths', d, n)
|
||||
|
||||
@staticmethod
|
||||
def addQuarters(d, n, timezone=None):
|
||||
return F('addQuarters', d, n, timezone) if timezone else F('addQuarters', d, n)
|
||||
|
||||
@staticmethod
|
||||
def addSeconds(d, n, timezone=None):
|
||||
return F('addSeconds', d, n, timezone) if timezone else F('addSeconds', d, n)
|
||||
|
||||
@staticmethod
|
||||
def addWeeks(d, n, timezone=None):
|
||||
return F('addWeeks', d, n, timezone) if timezone else F('addWeeks', d, n)
|
||||
|
||||
@staticmethod
|
||||
def addYears(d, n, timezone=None):
|
||||
return F('addYears', d, n, timezone) if timezone else F('addYears', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractDays(d, n, timezone=None):
|
||||
return F('subtractDays', d, n, timezone) if timezone else F('subtractDays', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractHours(d, n, timezone=None):
|
||||
return F('subtractHours', d, n, timezone) if timezone else F('subtractHours', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractMinutes(d, n, timezone=None):
|
||||
return F('subtractMinutes', d, n, timezone) if timezone else F('subtractMinutes', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractMonths(d, n, timezone=None):
|
||||
return F('subtractMonths', d, n, timezone) if timezone else F('subtractMonths', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractQuarters(d, n, timezone=None):
|
||||
return F('subtractQuarters', d, n, timezone) if timezone else F('subtractQuarters', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractSeconds(d, n, timezone=None):
|
||||
return F('subtractSeconds', d, n, timezone) if timezone else F('subtractSeconds', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractWeeks(d, n, timezone=None):
|
||||
return F('subtractWeeks', d, n, timezone) if timezone else F('subtractWeeks', d, n)
|
||||
|
||||
@staticmethod
|
||||
def subtractYears(d, n, timezone=None):
|
||||
return F('subtractYears', d, n, timezone) if timezone else F('subtractYears', d, n)
|
||||
|
||||
# Type conversion functions
|
||||
|
||||
@staticmethod
|
||||
|
@ -502,6 +567,18 @@ class F(Cond, FunctionOperatorsMixin):
|
|||
def CAST(x, type):
|
||||
return F('CAST', x, type)
|
||||
|
||||
@staticmethod
|
||||
def parseDateTimeBestEffort(d, timezone=None):
|
||||
return F('parseDateTimeBestEffort', d, timezone) if timezone else F('parseDateTimeBestEffort', d)
|
||||
|
||||
@staticmethod
|
||||
def parseDateTimeBestEffortOrNull(d, timezone=None):
|
||||
return F('parseDateTimeBestEffortOrNull', d, timezone) if timezone else F('parseDateTimeBestEffortOrNull', d)
|
||||
|
||||
@staticmethod
|
||||
def parseDateTimeBestEffortOrZero(d, timezone=None):
|
||||
return F('parseDateTimeBestEffortOrZero', d, timezone) if timezone else F('parseDateTimeBestEffortOrZero', d)
|
||||
|
||||
# Functions for working with strings
|
||||
|
||||
@staticmethod
|
||||
|
@ -1162,6 +1239,47 @@ class F(Cond, FunctionOperatorsMixin):
|
|||
def UUIDStringToNum(s):
|
||||
return F('UUIDStringToNum', s)
|
||||
|
||||
# Functions for working with IP addresses
|
||||
|
||||
@staticmethod
|
||||
def IPv4CIDRToRange(ipv4, cidr):
|
||||
return F('IPv4CIDRToRange', ipv4, cidr)
|
||||
|
||||
@staticmethod
|
||||
def IPv4NumToString(num):
|
||||
return F('IPv4NumToString', num)
|
||||
|
||||
@staticmethod
|
||||
def IPv4NumToStringClassC(num):
|
||||
return F('IPv4NumToStringClassC', num)
|
||||
|
||||
@staticmethod
|
||||
def IPv4StringToNum(s):
|
||||
return F('IPv4StringToNum', s)
|
||||
|
||||
@staticmethod
|
||||
def IPv4ToIPv6(ipv4):
|
||||
return F('IPv4ToIPv6', ipv4)
|
||||
|
||||
@staticmethod
|
||||
def IPv6CIDRToRange(ipv6, cidr):
|
||||
return F('IPv6CIDRToRange', ipv6, cidr)
|
||||
|
||||
@staticmethod
|
||||
def IPv6NumToString(num):
|
||||
return F('IPv6NumToString', num)
|
||||
|
||||
@staticmethod
|
||||
def IPv6StringToNum(s):
|
||||
return F('IPv6StringToNum', s)
|
||||
|
||||
@staticmethod
|
||||
def toIPv4(ipv4):
|
||||
return F('toIPv4', ipv4)
|
||||
|
||||
@staticmethod
|
||||
def toIPv6(ipv6):
|
||||
return F('toIPv6', ipv6)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -39,6 +39,10 @@ def unescape(value):
|
|||
return codecs.escape_decode(value)[0].decode('utf-8')
|
||||
|
||||
|
||||
def string_or_func(obj):
|
||||
return obj.to_sql() if hasattr(obj, 'to_sql') else obj
|
||||
|
||||
|
||||
def parse_tsv(line):
|
||||
if PY3 and isinstance(line, binary_type):
|
||||
line = line.decode()
|
||||
|
|
|
@ -8,7 +8,7 @@ from infi.clickhouse_orm.fields import *
|
|||
from infi.clickhouse_orm.engines import *
|
||||
|
||||
|
||||
class MaterializedFieldsTest(unittest.TestCase):
|
||||
class AliasFieldsTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.database = Database('test-db', log_statements=True)
|
||||
|
@ -25,7 +25,7 @@ class MaterializedFieldsTest(unittest.TestCase):
|
|||
)
|
||||
self.database.insert([instance])
|
||||
# We can't select * from table, as it doesn't select materialized and alias fields
|
||||
query = 'SELECT date_field, int_field, str_field, alias_int, alias_date, alias_str' \
|
||||
query = 'SELECT date_field, int_field, str_field, alias_int, alias_date, alias_str, alias_func' \
|
||||
' FROM $db.%s ORDER BY alias_date' % ModelWithAliasFields.table_name()
|
||||
for model_cls in (ModelWithAliasFields, None):
|
||||
results = list(self.database.select(query, model_cls))
|
||||
|
@ -36,6 +36,7 @@ class MaterializedFieldsTest(unittest.TestCase):
|
|||
self.assertEqual(results[0].alias_int, instance.int_field)
|
||||
self.assertEqual(results[0].alias_str, instance.str_field)
|
||||
self.assertEqual(results[0].alias_date, instance.date_field)
|
||||
self.assertEqual(results[0].alias_func, '08/30/16')
|
||||
|
||||
def test_assignment_error(self):
|
||||
# I can't prevent assigning at all, in case db.select statements with model provided sets model fields.
|
||||
|
@ -64,5 +65,6 @@ class ModelWithAliasFields(Model):
|
|||
alias_str = StringField(alias=u'str_field')
|
||||
alias_int = Int32Field(alias='int_field')
|
||||
alias_date = DateField(alias='date_field')
|
||||
alias_func = StringField(alias=F.formatDateTime(date_field, '%D'))
|
||||
|
||||
engine = MergeTree('date_field', ('date_field',))
|
||||
|
|
|
@ -2,6 +2,7 @@ import unittest
|
|||
from .base_test_with_data import *
|
||||
from .test_querysets import SampleModel
|
||||
from datetime import date, datetime, tzinfo, timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from infi.clickhouse_orm.database import ServerError
|
||||
|
||||
|
||||
|
@ -84,8 +85,8 @@ class FuncsTestCase(TestCaseWithData):
|
|||
self._test_qs(qs.filter(F.toDayOfWeek(Person.birthday) == 7), 18)
|
||||
# People born on 1976-10-01
|
||||
self._test_qs(qs.filter(F('equals', Person.birthday, '1976-10-01')), 1)
|
||||
self._test_qs(qs.filter(F('equals', Person.birthday, date(1976, 10, 01))), 1)
|
||||
self._test_qs(qs.filter(Person.birthday == date(1976, 10, 01)), 1)
|
||||
self._test_qs(qs.filter(F('equals', Person.birthday, date(1976, 10, 1))), 1)
|
||||
self._test_qs(qs.filter(Person.birthday == date(1976, 10, 1)), 1)
|
||||
|
||||
def test_func_as_field_value(self):
|
||||
qs = Person.objects_in(self.database)
|
||||
|
@ -151,8 +152,8 @@ class FuncsTestCase(TestCaseWithData):
|
|||
self._test_func(0 | one, 1)
|
||||
# ^
|
||||
self._test_func(one ^ one, 0)
|
||||
self._test_func(one ^ 0, 1)
|
||||
self._test_func(0 ^ one, 1)
|
||||
#############self._test_func(one ^ 0, 1)
|
||||
#############self._test_func(0 ^ one, 1)
|
||||
# ~
|
||||
self._test_func(~one, 0)
|
||||
self._test_func(~~one, 1)
|
||||
|
@ -214,6 +215,38 @@ class FuncsTestCase(TestCaseWithData):
|
|||
self._test_func(F.timeSlots(dt, 300), [datetime(2018, 12, 31, 11, 0, 0, tzinfo=pytz.utc)])
|
||||
self._test_func(F.formatDateTime(dt, '%D %T'), '12/31/18 11:22:33')
|
||||
self._test_func(F.formatDateTime(dt, '%D %T', 'Europe/Athens'), '12/31/18 13:22:33')
|
||||
self._test_func(F.addDays(d, 7), date(2019, 1, 7))
|
||||
self._test_func(F.addDays(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.addHours(d, 7), datetime(2018, 12, 31, 7, 0, 0, tzinfo=pytz.utc))
|
||||
self._test_func(F.addHours(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.addMinutes(d, 7), datetime(2018, 12, 31, 0, 7, 0, tzinfo=pytz.utc))
|
||||
self._test_func(F.addMinutes(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.addMonths(d, 7), date(2019, 7, 31))
|
||||
self._test_func(F.addMonths(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.addQuarters(d, 7))
|
||||
self._test_func(F.addQuarters(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.addSeconds(d, 7))
|
||||
self._test_func(F.addSeconds(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.addWeeks(d, 7))
|
||||
self._test_func(F.addWeeks(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.addYears(d, 7))
|
||||
self._test_func(F.addYears(dt, 7, 'Europe/Athens'))
|
||||
self._test_func(F.subtractDays(d, 3))
|
||||
self._test_func(F.subtractDays(dt, 3, 'Europe/Athens'))
|
||||
self._test_func(F.subtractHours(d, 3))
|
||||
self._test_func(F.subtractHours(dt, 3, 'Europe/Athens'))
|
||||
self._test_func(F.subtractMinutes(d, 3))
|
||||
self._test_func(F.subtractMinutes(dt, 3, 'Europe/Athens'))
|
||||
self._test_func(F.subtractMonths(d, 3))
|
||||
self._test_func(F.subtractMonths(dt, 3, 'Europe/Athens'))
|
||||
self._test_func(F.subtractQuarters(d, 3))
|
||||
self._test_func(F.subtractQuarters(dt, 3, 'Europe/Athens'))
|
||||
self._test_func(F.subtractSeconds(d, 3))
|
||||
self._test_func(F.subtractSeconds(dt, 3, 'Europe/Athens'))
|
||||
self._test_func(F.subtractWeeks(d, 3))
|
||||
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'))
|
||||
|
||||
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):
|
||||
|
@ -231,6 +264,16 @@ class FuncsTestCase(TestCaseWithData):
|
|||
self._test_func(F.toFixedString('123', 5), '123')
|
||||
self._test_func(F.toStringCutToZero('123\0'), '123')
|
||||
self._test_func(F.CAST(17, 'String'), '17')
|
||||
self._test_func(F.parseDateTimeBestEffort('31/12/2019 10:05AM'), datetime(2019, 12, 31, 10, 5, tzinfo=pytz.utc))
|
||||
self._test_func(F.parseDateTimeBestEffort('31/12/2019 10:05AM', 'Europe/Athens'))
|
||||
with self.assertRaises(ServerError):
|
||||
self._test_func(F.parseDateTimeBestEffort('foo'))
|
||||
self._test_func(F.parseDateTimeBestEffortOrNull('31/12/2019 10:05AM'), datetime(2019, 12, 31, 10, 5, tzinfo=pytz.utc))
|
||||
self._test_func(F.parseDateTimeBestEffortOrNull('31/12/2019 10:05AM', 'Europe/Athens'))
|
||||
self._test_func(F.parseDateTimeBestEffortOrNull('foo'), None)
|
||||
self._test_func(F.parseDateTimeBestEffortOrZero('31/12/2019 10:05AM'), datetime(2019, 12, 31, 10, 5, tzinfo=pytz.utc))
|
||||
self._test_func(F.parseDateTimeBestEffortOrZero('31/12/2019 10:05AM', 'Europe/Athens'))
|
||||
self._test_func(F.parseDateTimeBestEffortOrZero('foo'), DateTimeField.class_default)
|
||||
|
||||
def test_string_functions(self):
|
||||
self._test_func(F.empty(''), 1)
|
||||
|
@ -451,3 +494,15 @@ class FuncsTestCase(TestCaseWithData):
|
|||
s = str(uuid)
|
||||
self._test_func(F.toUUID(s), uuid)
|
||||
self._test_func(F.UUIDNumToString(F.UUIDStringToNum(s)), s)
|
||||
|
||||
def test_ip_funcs(self):
|
||||
self._test_func(F.IPv4NumToString(F.toUInt32(1)), '0.0.0.1')
|
||||
self._test_func(F.IPv4NumToStringClassC(F.toUInt32(1)), '0.0.0.xxx')
|
||||
self._test_func(F.IPv4StringToNum('0.0.0.17'), 17)
|
||||
self._test_func(F.IPv6NumToString(F.IPv4ToIPv6(F.IPv4StringToNum('192.168.0.1'))), '::ffff:192.168.0.1')
|
||||
self._test_func(F.IPv6NumToString(F.IPv6StringToNum('2a02:6b8::11')), '2a02:6b8::11')
|
||||
self._test_func(F.toIPv4('10.20.30.40'), IPv4Address('10.20.30.40'))
|
||||
self._test_func(F.toIPv6('2001:438:ffff::407d:1bc1'), IPv6Address('2001:438:ffff::407d:1bc1'))
|
||||
# These require support for tuples:
|
||||
# self._test_func(F.IPv4CIDRToRange(F.toIPv4('192.168.5.2'), 16), ['192.168.0.0','192.168.255.255'])
|
||||
# self._test_func(F.IPv6CIDRToRange(x, y))
|
||||
|
|
65
tests/test_ip_fields.py
Normal file
65
tests/test_ip_fields.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
from __future__ import unicode_literals
|
||||
import unittest
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from infi.clickhouse_orm.database import Database
|
||||
from infi.clickhouse_orm.fields import Int16Field, IPv4Field, IPv6Field
|
||||
from infi.clickhouse_orm.models import Model
|
||||
from infi.clickhouse_orm.engines import Memory
|
||||
|
||||
|
||||
class IPFieldsTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.database = Database('test-db', log_statements=True)
|
||||
|
||||
def tearDown(self):
|
||||
self.database.drop_database()
|
||||
|
||||
def test_ipv4_field(self):
|
||||
# Create a model
|
||||
class TestModel(Model):
|
||||
i = Int16Field()
|
||||
f = IPv4Field()
|
||||
engine = Memory()
|
||||
self.database.create_table(TestModel)
|
||||
# Check valid values (all values are the same ip)
|
||||
values = [
|
||||
'1.2.3.4',
|
||||
b'\x01\x02\x03\x04',
|
||||
16909060,
|
||||
IPv4Address('1.2.3.4')
|
||||
]
|
||||
for index, value in enumerate(values):
|
||||
rec = TestModel(i=index, f=value)
|
||||
self.database.insert([rec])
|
||||
for rec in TestModel.objects_in(self.database):
|
||||
self.assertEqual(rec.f, IPv4Address(values[0]))
|
||||
# Check invalid values
|
||||
for value in [None, 'zzz', -1, '123']:
|
||||
with self.assertRaises(ValueError):
|
||||
TestModel(i=1, f=value)
|
||||
|
||||
def test_ipv6_field(self):
|
||||
# Create a model
|
||||
class TestModel(Model):
|
||||
i = Int16Field()
|
||||
f = IPv6Field()
|
||||
engine = Memory()
|
||||
self.database.create_table(TestModel)
|
||||
# Check valid values (all values are the same ip)
|
||||
values = [
|
||||
'2a02:e980:1e::1',
|
||||
b'*\x02\xe9\x80\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01',
|
||||
55842696359362256756849388082849382401,
|
||||
IPv6Address('2a02:e980:1e::1')
|
||||
]
|
||||
for index, value in enumerate(values):
|
||||
rec = TestModel(i=index, f=value)
|
||||
self.database.insert([rec])
|
||||
for rec in TestModel.objects_in(self.database):
|
||||
self.assertEqual(rec.f, IPv6Address(values[0]))
|
||||
# Check invalid values
|
||||
for value in [None, 'zzz', -1, '123']:
|
||||
with self.assertRaises(ValueError):
|
||||
TestModel(i=1, f=value)
|
||||
|
|
@ -25,7 +25,7 @@ class MaterializedFieldsTest(unittest.TestCase):
|
|||
)
|
||||
self.database.insert([instance])
|
||||
# We can't select * from table, as it doesn't select materialized and alias fields
|
||||
query = 'SELECT date_time_field, int_field, str_field, mat_int, mat_date, mat_str' \
|
||||
query = 'SELECT date_time_field, int_field, str_field, mat_int, mat_date, mat_str, mat_func' \
|
||||
' FROM $db.%s ORDER BY mat_date' % ModelWithMaterializedFields.table_name()
|
||||
for model_cls in (ModelWithMaterializedFields, None):
|
||||
results = list(self.database.select(query, model_cls))
|
||||
|
@ -36,6 +36,7 @@ class MaterializedFieldsTest(unittest.TestCase):
|
|||
self.assertEqual(results[0].mat_int, abs(instance.int_field))
|
||||
self.assertEqual(results[0].mat_str, instance.str_field.lower())
|
||||
self.assertEqual(results[0].mat_date, instance.date_time_field.date())
|
||||
self.assertEqual(results[0].mat_func, instance.str_field.lower())
|
||||
|
||||
def test_assignment_error(self):
|
||||
# I can't prevent assigning at all, in case db.select statements with model provided sets model fields.
|
||||
|
@ -64,5 +65,6 @@ class ModelWithMaterializedFields(Model):
|
|||
mat_str = StringField(materialized='lower(str_field)')
|
||||
mat_int = Int32Field(materialized='abs(int_field)')
|
||||
mat_date = DateField(materialized=u'toDate(date_time_field)')
|
||||
mat_func = StringField(materialized=F.lower(str_field))
|
||||
|
||||
engine = MergeTree('mat_date', ('mat_date',))
|
||||
|
|
|
@ -520,8 +520,8 @@ class FuncsTestCase(TestCaseWithData):
|
|||
self._test_qs(qs.filter(F.toDayOfWeek(Person.birthday) == 7), 18)
|
||||
# People born on 1976-10-01
|
||||
self._test_qs(qs.filter(F('equals', Person.birthday, '1976-10-01')), 1)
|
||||
self._test_qs(qs.filter(F('equals', Person.birthday, date(1976, 10, 01))), 1)
|
||||
self._test_qs(qs.filter(Person.birthday == date(1976, 10, 01)), 1)
|
||||
self._test_qs(qs.filter(F('equals', Person.birthday, date(1976, 10, 1))), 1)
|
||||
self._test_qs(qs.filter(Person.birthday == date(1976, 10, 1)), 1)
|
||||
|
||||
def test_func_as_field_value(self):
|
||||
qs = Person.objects_in(self.database)
|
||||
|
|
Loading…
Reference in New Issue
Block a user