clickhouse-orm 3.0.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clickhouse_orm/__init__.py +14 -0
- clickhouse_orm/database.py +457 -0
- clickhouse_orm/engines.py +346 -0
- clickhouse_orm/fields.py +665 -0
- clickhouse_orm/funcs.py +1841 -0
- clickhouse_orm/migrations.py +287 -0
- clickhouse_orm/models.py +617 -0
- clickhouse_orm/query.py +701 -0
- clickhouse_orm/system_models.py +170 -0
- clickhouse_orm/utils.py +176 -0
- clickhouse_orm-3.0.1.dist-info/METADATA +90 -0
- clickhouse_orm-3.0.1.dist-info/RECORD +14 -0
- clickhouse_orm-3.0.1.dist-info/WHEEL +5 -0
- clickhouse_orm-3.0.1.dist-info/licenses/LICENSE +27 -0
clickhouse_orm/fields.py
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from calendar import timegm
|
|
5
|
+
from decimal import Decimal, localcontext
|
|
6
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import pytz
|
|
11
|
+
from pytz import BaseTzInfo
|
|
12
|
+
|
|
13
|
+
from .funcs import F, FunctionOperatorsMixin
|
|
14
|
+
from .utils import comma_join, escape, get_subclass_names, parse_array, string_or_func
|
|
15
|
+
|
|
16
|
+
logger = getLogger("clickhouse_orm")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Field(FunctionOperatorsMixin):
|
|
20
|
+
"""
|
|
21
|
+
Abstract base class for all field types.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name = None # this is set by the parent model
|
|
25
|
+
parent = None # this is set by the parent model
|
|
26
|
+
creation_counter = 0 # used for keeping the model fields ordered
|
|
27
|
+
class_default = 0 # should be overridden by concrete subclasses
|
|
28
|
+
db_type = None # should be overridden by concrete subclasses
|
|
29
|
+
|
|
30
|
+
def __init__(self, default=None, alias=None, materialized=None, readonly=None, codec=None):
|
|
31
|
+
assert [default, alias, materialized].count(None) >= 2, (
|
|
32
|
+
"Only one of default, alias and materialized parameters can be given"
|
|
33
|
+
)
|
|
34
|
+
assert alias is None or isinstance(alias, F) or isinstance(alias, str) and alias != "", (
|
|
35
|
+
"Alias parameter must be a string or function object, if given"
|
|
36
|
+
)
|
|
37
|
+
assert (
|
|
38
|
+
materialized is None or isinstance(materialized, F) or isinstance(materialized, str) and materialized != ""
|
|
39
|
+
), "Materialized parameter must be a string or function object, if given"
|
|
40
|
+
assert readonly is None or type(readonly) is bool, "readonly parameter must be bool if given"
|
|
41
|
+
assert codec is None or isinstance(codec, str) and codec != "", "Codec field must be string, if given"
|
|
42
|
+
if alias:
|
|
43
|
+
assert codec is None, "Codec cannot be used for alias fields"
|
|
44
|
+
|
|
45
|
+
self.creation_counter = Field.creation_counter
|
|
46
|
+
Field.creation_counter += 1
|
|
47
|
+
self.default = self.class_default if default is None else default
|
|
48
|
+
self.alias = alias
|
|
49
|
+
self.materialized = materialized
|
|
50
|
+
self.readonly = bool(self.alias or self.materialized or readonly)
|
|
51
|
+
self.codec = codec
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
return self.name
|
|
55
|
+
|
|
56
|
+
def __repr__(self):
|
|
57
|
+
return "<%s>" % self.__class__.__name__
|
|
58
|
+
|
|
59
|
+
def to_python(self, value, timezone_in_use):
|
|
60
|
+
"""
|
|
61
|
+
Converts the input value into the expected Python data type, raising ValueError if the
|
|
62
|
+
data can't be converted. Returns the converted value. Subclasses should override this.
|
|
63
|
+
The timezone_in_use parameter should be consulted when parsing datetime fields.
|
|
64
|
+
"""
|
|
65
|
+
return value # pragma: no cover
|
|
66
|
+
|
|
67
|
+
def validate(self, value):
|
|
68
|
+
"""
|
|
69
|
+
Called after to_python to validate that the value is suitable for the field's database type.
|
|
70
|
+
Subclasses should override this.
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def _range_check(self, value, min_value, max_value):
|
|
75
|
+
"""
|
|
76
|
+
Utility method to check that the given value is between min_value and max_value.
|
|
77
|
+
"""
|
|
78
|
+
if value < min_value or value > max_value:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"%s out of range - %s is not between %s and %s" % (self.__class__.__name__, value, min_value, max_value)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def to_db_string(self, value, quote=True):
|
|
84
|
+
"""
|
|
85
|
+
Returns the field's value prepared for writing to the database.
|
|
86
|
+
When quote is true, strings are surrounded by single quotes.
|
|
87
|
+
"""
|
|
88
|
+
return escape(value, quote)
|
|
89
|
+
|
|
90
|
+
def get_sql(self, with_default_expression=True, db=None):
|
|
91
|
+
"""
|
|
92
|
+
Returns an SQL expression describing the field (e.g. for CREATE TABLE).
|
|
93
|
+
|
|
94
|
+
- `with_default_expression`: If True, adds default value to sql.
|
|
95
|
+
It doesn't affect fields with alias and materialized values.
|
|
96
|
+
- `db`: Database, used for checking supported features.
|
|
97
|
+
"""
|
|
98
|
+
sql = self.db_type
|
|
99
|
+
args = self.get_db_type_args()
|
|
100
|
+
if args:
|
|
101
|
+
sql += "(%s)" % comma_join(args)
|
|
102
|
+
if with_default_expression:
|
|
103
|
+
sql += self._extra_params(db)
|
|
104
|
+
return sql
|
|
105
|
+
|
|
106
|
+
def get_db_type_args(self):
|
|
107
|
+
"""Returns field type arguments"""
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
def _extra_params(self, db):
|
|
111
|
+
sql = ""
|
|
112
|
+
if self.alias:
|
|
113
|
+
sql += " ALIAS %s" % string_or_func(self.alias)
|
|
114
|
+
elif self.materialized:
|
|
115
|
+
sql += " MATERIALIZED %s" % string_or_func(self.materialized)
|
|
116
|
+
elif isinstance(self.default, F):
|
|
117
|
+
sql += " DEFAULT %s" % self.default.to_sql()
|
|
118
|
+
elif self.default:
|
|
119
|
+
default = self.to_db_string(self.default)
|
|
120
|
+
sql += " DEFAULT %s" % default
|
|
121
|
+
if self.codec and db and db.has_codec_support and not self.alias:
|
|
122
|
+
sql += " CODEC(%s)" % self.codec
|
|
123
|
+
return sql
|
|
124
|
+
|
|
125
|
+
def isinstance(self, types):
|
|
126
|
+
"""
|
|
127
|
+
Checks if the instance if one of the types provided or if any of the inner_field child is one of the types
|
|
128
|
+
provided, returns True if field or any inner_field is one of ths provided, False otherwise
|
|
129
|
+
|
|
130
|
+
- `types`: Iterable of types to check inclusion of instance
|
|
131
|
+
|
|
132
|
+
Returns: Boolean
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(self, types):
|
|
135
|
+
return True
|
|
136
|
+
inner_field = getattr(self, "inner_field", None)
|
|
137
|
+
while inner_field:
|
|
138
|
+
if isinstance(inner_field, types):
|
|
139
|
+
return True
|
|
140
|
+
inner_field = getattr(inner_field, "inner_field", None)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class StringField(Field):
|
|
145
|
+
class_default = ""
|
|
146
|
+
db_type = "String"
|
|
147
|
+
|
|
148
|
+
def to_python(self, value, timezone_in_use):
|
|
149
|
+
if isinstance(value, str):
|
|
150
|
+
return value
|
|
151
|
+
if isinstance(value, bytes):
|
|
152
|
+
return value.decode("utf-8")
|
|
153
|
+
raise ValueError("Invalid value for %s: %r" % (self.__class__.__name__, value))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FixedStringField(StringField):
|
|
157
|
+
def __init__(self, length, default=None, alias=None, materialized=None, readonly=None):
|
|
158
|
+
self._length = length
|
|
159
|
+
self.db_type = "FixedString(%d)" % length
|
|
160
|
+
super().__init__(default, alias, materialized, readonly)
|
|
161
|
+
|
|
162
|
+
def to_python(self, value, timezone_in_use):
|
|
163
|
+
value = super().to_python(value, timezone_in_use)
|
|
164
|
+
return value.rstrip("\0")
|
|
165
|
+
|
|
166
|
+
def validate(self, value):
|
|
167
|
+
if isinstance(value, str):
|
|
168
|
+
value = value.encode("utf-8")
|
|
169
|
+
if len(value) > self._length:
|
|
170
|
+
raise ValueError("Value of %d bytes is too long for FixedStringField(%d)" % (len(value), self._length))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class DateField(Field):
|
|
174
|
+
min_value = datetime.date(1970, 1, 1)
|
|
175
|
+
max_value = datetime.date(2105, 12, 31)
|
|
176
|
+
class_default = min_value
|
|
177
|
+
db_type = "Date"
|
|
178
|
+
|
|
179
|
+
def to_python(self, value, timezone_in_use):
|
|
180
|
+
if isinstance(value, datetime.datetime):
|
|
181
|
+
return value.astimezone(pytz.utc).date() if value.tzinfo else value.date()
|
|
182
|
+
if isinstance(value, datetime.date):
|
|
183
|
+
return value
|
|
184
|
+
if isinstance(value, int):
|
|
185
|
+
return DateField.class_default + datetime.timedelta(days=value)
|
|
186
|
+
if isinstance(value, str):
|
|
187
|
+
if value == "0000-00-00":
|
|
188
|
+
return DateField.min_value
|
|
189
|
+
return datetime.datetime.strptime(value, "%Y-%m-%d").date()
|
|
190
|
+
raise ValueError("Invalid value for %s - %r" % (self.__class__.__name__, value))
|
|
191
|
+
|
|
192
|
+
def validate(self, value):
|
|
193
|
+
self._range_check(value, DateField.min_value, DateField.max_value)
|
|
194
|
+
|
|
195
|
+
def to_db_string(self, value, quote=True):
|
|
196
|
+
return escape(value.isoformat(), quote)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class DateTimeField(Field):
|
|
200
|
+
class_default = datetime.datetime.fromtimestamp(0, pytz.utc)
|
|
201
|
+
db_type = "DateTime"
|
|
202
|
+
|
|
203
|
+
def __init__(self, default=None, alias=None, materialized=None, readonly=None, codec=None, timezone=None):
|
|
204
|
+
super().__init__(default, alias, materialized, readonly, codec)
|
|
205
|
+
# assert not timezone, 'Temporarily field timezone is not supported'
|
|
206
|
+
if timezone:
|
|
207
|
+
timezone = timezone if isinstance(timezone, BaseTzInfo) else pytz.timezone(timezone)
|
|
208
|
+
self.timezone = timezone
|
|
209
|
+
|
|
210
|
+
def get_db_type_args(self):
|
|
211
|
+
args = []
|
|
212
|
+
if self.timezone:
|
|
213
|
+
args.append(escape(self.timezone.zone))
|
|
214
|
+
return args
|
|
215
|
+
|
|
216
|
+
def to_python(self, value, timezone_in_use):
|
|
217
|
+
if isinstance(value, datetime.datetime):
|
|
218
|
+
return value if value.tzinfo else value.replace(tzinfo=pytz.utc)
|
|
219
|
+
if isinstance(value, datetime.date):
|
|
220
|
+
return datetime.datetime(value.year, value.month, value.day, tzinfo=pytz.utc)
|
|
221
|
+
if isinstance(value, int):
|
|
222
|
+
return datetime.datetime.utcfromtimestamp(value).replace(tzinfo=pytz.utc)
|
|
223
|
+
if isinstance(value, str):
|
|
224
|
+
if value == "0000-00-00 00:00:00":
|
|
225
|
+
return self.class_default
|
|
226
|
+
if len(value) == 10:
|
|
227
|
+
try:
|
|
228
|
+
value = int(value)
|
|
229
|
+
return datetime.datetime.utcfromtimestamp(value).replace(tzinfo=pytz.utc)
|
|
230
|
+
except ValueError:
|
|
231
|
+
pass
|
|
232
|
+
# left the date naive in case of no tzinfo set
|
|
233
|
+
dt = datetime.datetime.fromisoformat(value)
|
|
234
|
+
|
|
235
|
+
# convert naive to aware
|
|
236
|
+
if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
|
|
237
|
+
dt = timezone_in_use.localize(dt)
|
|
238
|
+
return dt
|
|
239
|
+
raise ValueError("Invalid value for %s - %r" % (self.__class__.__name__, value))
|
|
240
|
+
|
|
241
|
+
def to_db_string(self, value, quote=True):
|
|
242
|
+
return escape("%010d" % timegm(value.utctimetuple()), quote)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class DateTime64Field(DateTimeField):
|
|
246
|
+
db_type = "DateTime64"
|
|
247
|
+
|
|
248
|
+
def __init__(
|
|
249
|
+
self, default=None, alias=None, materialized=None, readonly=None, codec=None, timezone=None, precision=6
|
|
250
|
+
):
|
|
251
|
+
super().__init__(default, alias, materialized, readonly, codec, timezone)
|
|
252
|
+
assert precision is None or isinstance(precision, int), "Precision must be int type"
|
|
253
|
+
self.precision = precision
|
|
254
|
+
|
|
255
|
+
def get_db_type_args(self):
|
|
256
|
+
args = [str(self.precision)]
|
|
257
|
+
if self.timezone:
|
|
258
|
+
args.append(escape(self.timezone.zone))
|
|
259
|
+
return args
|
|
260
|
+
|
|
261
|
+
def to_db_string(self, value, quote=True):
|
|
262
|
+
"""
|
|
263
|
+
Returns the field's value prepared for writing to the database
|
|
264
|
+
|
|
265
|
+
Returns string in 0000000000.000000 format, where remainder digits count is equal to precision
|
|
266
|
+
"""
|
|
267
|
+
return escape(
|
|
268
|
+
"{timestamp:0{width}.{precision}f}".format(
|
|
269
|
+
timestamp=value.timestamp(), width=11 + self.precision, precision=self.precision
|
|
270
|
+
),
|
|
271
|
+
quote,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def to_python(self, value, timezone_in_use):
|
|
275
|
+
try:
|
|
276
|
+
return super().to_python(value, timezone_in_use)
|
|
277
|
+
except ValueError:
|
|
278
|
+
if isinstance(value, (int, float)):
|
|
279
|
+
return datetime.datetime.utcfromtimestamp(value).replace(tzinfo=pytz.utc)
|
|
280
|
+
if isinstance(value, str):
|
|
281
|
+
left_part = value.split(".")[0]
|
|
282
|
+
if left_part == "0000-00-00 00:00:00":
|
|
283
|
+
return self.class_default
|
|
284
|
+
if len(left_part) == 10:
|
|
285
|
+
try:
|
|
286
|
+
value = float(value)
|
|
287
|
+
return datetime.datetime.utcfromtimestamp(value).replace(tzinfo=pytz.utc)
|
|
288
|
+
except ValueError:
|
|
289
|
+
pass
|
|
290
|
+
raise
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class BaseIntField(Field):
|
|
294
|
+
"""
|
|
295
|
+
Abstract base class for all integer-type fields.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def to_python(self, value, timezone_in_use):
|
|
299
|
+
try:
|
|
300
|
+
return int(value)
|
|
301
|
+
except Exception:
|
|
302
|
+
raise ValueError("Invalid value for %s - %r" % (self.__class__.__name__, value))
|
|
303
|
+
|
|
304
|
+
def to_db_string(self, value, quote=True):
|
|
305
|
+
# There's no need to call escape since numbers do not contain
|
|
306
|
+
# special characters, and never need quoting
|
|
307
|
+
return str(value)
|
|
308
|
+
|
|
309
|
+
def validate(self, value):
|
|
310
|
+
self._range_check(value, self.min_value, self.max_value)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class UInt8Field(BaseIntField):
|
|
314
|
+
min_value = 0
|
|
315
|
+
max_value = 2**8 - 1
|
|
316
|
+
db_type = "UInt8"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class UInt16Field(BaseIntField):
|
|
320
|
+
min_value = 0
|
|
321
|
+
max_value = 2**16 - 1
|
|
322
|
+
db_type = "UInt16"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class UInt32Field(BaseIntField):
|
|
326
|
+
min_value = 0
|
|
327
|
+
max_value = 2**32 - 1
|
|
328
|
+
db_type = "UInt32"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class UInt64Field(BaseIntField):
|
|
332
|
+
min_value = 0
|
|
333
|
+
max_value = 2**64 - 1
|
|
334
|
+
db_type = "UInt64"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class Int8Field(BaseIntField):
|
|
338
|
+
min_value = -(2**7)
|
|
339
|
+
max_value = 2**7 - 1
|
|
340
|
+
db_type = "Int8"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class Int16Field(BaseIntField):
|
|
344
|
+
min_value = -(2**15)
|
|
345
|
+
max_value = 2**15 - 1
|
|
346
|
+
db_type = "Int16"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class Int32Field(BaseIntField):
|
|
350
|
+
min_value = -(2**31)
|
|
351
|
+
max_value = 2**31 - 1
|
|
352
|
+
db_type = "Int32"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class Int64Field(BaseIntField):
|
|
356
|
+
min_value = -(2**63)
|
|
357
|
+
max_value = 2**63 - 1
|
|
358
|
+
db_type = "Int64"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class BaseFloatField(Field):
|
|
362
|
+
"""
|
|
363
|
+
Abstract base class for all float-type fields.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def to_python(self, value, timezone_in_use):
|
|
367
|
+
try:
|
|
368
|
+
return float(value)
|
|
369
|
+
except Exception:
|
|
370
|
+
raise ValueError("Invalid value for %s - %r" % (self.__class__.__name__, value))
|
|
371
|
+
|
|
372
|
+
def to_db_string(self, value, quote=True):
|
|
373
|
+
# There's no need to call escape since numbers do not contain
|
|
374
|
+
# special characters, and never need quoting
|
|
375
|
+
return str(value)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class Float32Field(BaseFloatField):
|
|
379
|
+
db_type = "Float32"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class Float64Field(BaseFloatField):
|
|
383
|
+
db_type = "Float64"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class DecimalField(Field):
|
|
387
|
+
"""
|
|
388
|
+
Base class for all decimal fields. Can also be used directly.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
def __init__(self, precision, scale, default=None, alias=None, materialized=None, readonly=None):
|
|
392
|
+
assert 1 <= precision <= 38, "Precision must be between 1 and 38"
|
|
393
|
+
assert 0 <= scale <= precision, "Scale must be between 0 and the given precision"
|
|
394
|
+
self.precision = precision
|
|
395
|
+
self.scale = scale
|
|
396
|
+
self.db_type = "Decimal(%d,%d)" % (self.precision, self.scale)
|
|
397
|
+
with localcontext() as ctx:
|
|
398
|
+
ctx.prec = 38
|
|
399
|
+
self.exp = Decimal(10) ** -self.scale # for rounding to the required scale
|
|
400
|
+
self.max_value = Decimal(10 ** (self.precision - self.scale)) - self.exp
|
|
401
|
+
self.min_value = -self.max_value
|
|
402
|
+
super().__init__(default, alias, materialized, readonly)
|
|
403
|
+
|
|
404
|
+
def to_python(self, value, timezone_in_use):
|
|
405
|
+
if not isinstance(value, Decimal):
|
|
406
|
+
try:
|
|
407
|
+
value = Decimal(value)
|
|
408
|
+
except Exception:
|
|
409
|
+
raise ValueError("Invalid value for %s - %r" % (self.__class__.__name__, value))
|
|
410
|
+
if not value.is_finite():
|
|
411
|
+
raise ValueError("Non-finite value for %s - %r" % (self.__class__.__name__, value))
|
|
412
|
+
return self._round(value)
|
|
413
|
+
|
|
414
|
+
def to_db_string(self, value, quote=True):
|
|
415
|
+
# There's no need to call escape since numbers do not contain
|
|
416
|
+
# special characters, and never need quoting
|
|
417
|
+
return str(value)
|
|
418
|
+
|
|
419
|
+
def _round(self, value):
|
|
420
|
+
return value.quantize(self.exp)
|
|
421
|
+
|
|
422
|
+
def validate(self, value):
|
|
423
|
+
self._range_check(value, self.min_value, self.max_value)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class Decimal32Field(DecimalField):
|
|
427
|
+
def __init__(self, scale, default=None, alias=None, materialized=None, readonly=None):
|
|
428
|
+
super().__init__(9, scale, default, alias, materialized, readonly)
|
|
429
|
+
self.db_type = "Decimal32(%d)" % scale
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class Decimal64Field(DecimalField):
|
|
433
|
+
def __init__(self, scale, default=None, alias=None, materialized=None, readonly=None):
|
|
434
|
+
super().__init__(18, scale, default, alias, materialized, readonly)
|
|
435
|
+
self.db_type = "Decimal64(%d)" % scale
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class Decimal128Field(DecimalField):
|
|
439
|
+
def __init__(self, scale, default=None, alias=None, materialized=None, readonly=None):
|
|
440
|
+
super().__init__(38, scale, default, alias, materialized, readonly)
|
|
441
|
+
self.db_type = "Decimal128(%d)" % scale
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class BaseEnumField(Field):
|
|
445
|
+
"""
|
|
446
|
+
Abstract base class for all enum-type fields.
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
def __init__(self, enum_cls, default=None, alias=None, materialized=None, readonly=None, codec=None):
|
|
450
|
+
self.enum_cls = enum_cls
|
|
451
|
+
if default is None:
|
|
452
|
+
default = list(enum_cls)[0]
|
|
453
|
+
super().__init__(default, alias, materialized, readonly, codec)
|
|
454
|
+
|
|
455
|
+
def to_python(self, value, timezone_in_use):
|
|
456
|
+
if isinstance(value, self.enum_cls):
|
|
457
|
+
return value
|
|
458
|
+
try:
|
|
459
|
+
if isinstance(value, str):
|
|
460
|
+
try:
|
|
461
|
+
return self.enum_cls[value]
|
|
462
|
+
except Exception:
|
|
463
|
+
return self.enum_cls(value)
|
|
464
|
+
if isinstance(value, bytes):
|
|
465
|
+
decoded = value.decode("utf-8")
|
|
466
|
+
try:
|
|
467
|
+
return self.enum_cls[decoded]
|
|
468
|
+
except Exception:
|
|
469
|
+
return self.enum_cls(decoded)
|
|
470
|
+
if isinstance(value, int):
|
|
471
|
+
return self.enum_cls(value)
|
|
472
|
+
except (KeyError, ValueError):
|
|
473
|
+
pass
|
|
474
|
+
raise ValueError("Invalid value for %s: %r" % (self.enum_cls.__name__, value))
|
|
475
|
+
|
|
476
|
+
def to_db_string(self, value, quote=True):
|
|
477
|
+
return escape(value.name, quote)
|
|
478
|
+
|
|
479
|
+
def get_db_type_args(self):
|
|
480
|
+
return ["%s = %d" % (escape(item.name), item.value) for item in self.enum_cls]
|
|
481
|
+
|
|
482
|
+
@classmethod
|
|
483
|
+
def create_ad_hoc_field(cls, db_type):
|
|
484
|
+
"""
|
|
485
|
+
Give an SQL column description such as "Enum8('apple' = 1, 'banana' = 2, 'orange' = 3)"
|
|
486
|
+
this method returns a matching enum field.
|
|
487
|
+
"""
|
|
488
|
+
import re
|
|
489
|
+
from enum import Enum
|
|
490
|
+
|
|
491
|
+
members = {}
|
|
492
|
+
for match in re.finditer(r"'([\w ]+)' = (-?\d+)", db_type):
|
|
493
|
+
members[match.group(1)] = int(match.group(2))
|
|
494
|
+
enum_cls = Enum("AdHocEnum", members)
|
|
495
|
+
field_class = Enum8Field if db_type.startswith("Enum8") else Enum16Field
|
|
496
|
+
return field_class(enum_cls)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class Enum8Field(BaseEnumField):
|
|
500
|
+
db_type = "Enum8"
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class Enum16Field(BaseEnumField):
|
|
504
|
+
db_type = "Enum16"
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class ArrayField(Field):
|
|
508
|
+
class_default = []
|
|
509
|
+
|
|
510
|
+
def __init__(self, inner_field, default=None, alias=None, materialized=None, readonly=None, codec=None):
|
|
511
|
+
assert isinstance(inner_field, Field), "The first argument of ArrayField must be a Field instance"
|
|
512
|
+
assert not isinstance(inner_field, ArrayField), "Multidimensional array fields are not supported by the ORM"
|
|
513
|
+
self.inner_field = inner_field
|
|
514
|
+
super().__init__(default, alias, materialized, readonly, codec)
|
|
515
|
+
|
|
516
|
+
def to_python(self, value, timezone_in_use):
|
|
517
|
+
if isinstance(value, str):
|
|
518
|
+
value = parse_array(value)
|
|
519
|
+
elif isinstance(value, bytes):
|
|
520
|
+
value = parse_array(value.decode("utf-8"))
|
|
521
|
+
elif not isinstance(value, (list, tuple)):
|
|
522
|
+
raise ValueError("ArrayField expects list or tuple, not %s" % type(value))
|
|
523
|
+
return [self.inner_field.to_python(v, timezone_in_use) for v in value]
|
|
524
|
+
|
|
525
|
+
def validate(self, value):
|
|
526
|
+
for v in value:
|
|
527
|
+
self.inner_field.validate(v)
|
|
528
|
+
|
|
529
|
+
def to_db_string(self, value, quote=True):
|
|
530
|
+
array = [self.inner_field.to_db_string(v, quote=True) for v in value]
|
|
531
|
+
return "[" + comma_join(array) + "]"
|
|
532
|
+
|
|
533
|
+
def get_sql(self, with_default_expression=True, db=None):
|
|
534
|
+
sql = "Array(%s)" % self.inner_field.get_sql(with_default_expression=False, db=db)
|
|
535
|
+
if with_default_expression and self.codec and db and db.has_codec_support:
|
|
536
|
+
sql += " CODEC(%s)" % self.codec
|
|
537
|
+
return sql
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class UUIDField(Field):
|
|
541
|
+
class_default = UUID(int=0)
|
|
542
|
+
db_type = "UUID"
|
|
543
|
+
|
|
544
|
+
def to_python(self, value, timezone_in_use):
|
|
545
|
+
if isinstance(value, UUID):
|
|
546
|
+
return value
|
|
547
|
+
elif isinstance(value, bytes):
|
|
548
|
+
return UUID(bytes=value)
|
|
549
|
+
elif isinstance(value, str):
|
|
550
|
+
return UUID(value)
|
|
551
|
+
elif isinstance(value, int):
|
|
552
|
+
return UUID(int=value)
|
|
553
|
+
elif isinstance(value, tuple):
|
|
554
|
+
return UUID(fields=value)
|
|
555
|
+
else:
|
|
556
|
+
raise ValueError("Invalid value for UUIDField: %r" % value)
|
|
557
|
+
|
|
558
|
+
def to_db_string(self, value, quote=True):
|
|
559
|
+
return escape(str(value), quote)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class IPv4Field(Field):
|
|
563
|
+
class_default = 0
|
|
564
|
+
db_type = "IPv4"
|
|
565
|
+
|
|
566
|
+
def to_python(self, value, timezone_in_use):
|
|
567
|
+
if isinstance(value, IPv4Address):
|
|
568
|
+
return value
|
|
569
|
+
elif isinstance(value, (bytes, str, int)):
|
|
570
|
+
return IPv4Address(value)
|
|
571
|
+
else:
|
|
572
|
+
raise ValueError("Invalid value for IPv4Address: %r" % value)
|
|
573
|
+
|
|
574
|
+
def to_db_string(self, value, quote=True):
|
|
575
|
+
return escape(str(value), quote)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class IPv6Field(Field):
|
|
579
|
+
class_default = 0
|
|
580
|
+
db_type = "IPv6"
|
|
581
|
+
|
|
582
|
+
def to_python(self, value, timezone_in_use):
|
|
583
|
+
if isinstance(value, IPv6Address):
|
|
584
|
+
return value
|
|
585
|
+
elif isinstance(value, (bytes, str, int)):
|
|
586
|
+
return IPv6Address(value)
|
|
587
|
+
else:
|
|
588
|
+
raise ValueError("Invalid value for IPv6Address: %r" % value)
|
|
589
|
+
|
|
590
|
+
def to_db_string(self, value, quote=True):
|
|
591
|
+
return escape(str(value), quote)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class NullableField(Field):
|
|
595
|
+
class_default = None
|
|
596
|
+
|
|
597
|
+
def __init__(self, inner_field, default=None, alias=None, materialized=None, extra_null_values=None, codec=None):
|
|
598
|
+
assert isinstance(inner_field, Field), (
|
|
599
|
+
f"The first argument of NullableField must be a Field instance. Not: {inner_field}"
|
|
600
|
+
)
|
|
601
|
+
self.inner_field = inner_field
|
|
602
|
+
self._null_values = [None]
|
|
603
|
+
if extra_null_values:
|
|
604
|
+
self._null_values.extend(extra_null_values)
|
|
605
|
+
super().__init__(default, alias, materialized, readonly=None, codec=codec)
|
|
606
|
+
|
|
607
|
+
def to_python(self, value, timezone_in_use):
|
|
608
|
+
if value == "\\N" or value in self._null_values:
|
|
609
|
+
return None
|
|
610
|
+
return self.inner_field.to_python(value, timezone_in_use)
|
|
611
|
+
|
|
612
|
+
def validate(self, value):
|
|
613
|
+
value in self._null_values or self.inner_field.validate(value)
|
|
614
|
+
|
|
615
|
+
def to_db_string(self, value, quote=True):
|
|
616
|
+
if value in self._null_values:
|
|
617
|
+
return "\\N"
|
|
618
|
+
return self.inner_field.to_db_string(value, quote=quote)
|
|
619
|
+
|
|
620
|
+
def get_sql(self, with_default_expression=True, db=None):
|
|
621
|
+
sql = "Nullable(%s)" % self.inner_field.get_sql(with_default_expression=False, db=db)
|
|
622
|
+
if with_default_expression:
|
|
623
|
+
sql += self._extra_params(db)
|
|
624
|
+
return sql
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
class LowCardinalityField(Field):
|
|
628
|
+
def __init__(self, inner_field, default=None, alias=None, materialized=None, readonly=None, codec=None):
|
|
629
|
+
assert isinstance(inner_field, Field), (
|
|
630
|
+
f"The first argument of LowCardinalityField must be a Field instance. Not: {inner_field}"
|
|
631
|
+
)
|
|
632
|
+
assert not isinstance(inner_field, LowCardinalityField), (
|
|
633
|
+
"LowCardinality inner fields are not supported by the ORM"
|
|
634
|
+
)
|
|
635
|
+
assert not isinstance(inner_field, ArrayField), (
|
|
636
|
+
"Array field inside LowCardinality are not supported by the ORM. Use Array(LowCardinality) instead"
|
|
637
|
+
)
|
|
638
|
+
self.inner_field = inner_field
|
|
639
|
+
self.class_default = self.inner_field.class_default
|
|
640
|
+
super().__init__(default, alias, materialized, readonly, codec)
|
|
641
|
+
|
|
642
|
+
def to_python(self, value, timezone_in_use):
|
|
643
|
+
return self.inner_field.to_python(value, timezone_in_use)
|
|
644
|
+
|
|
645
|
+
def validate(self, value):
|
|
646
|
+
self.inner_field.validate(value)
|
|
647
|
+
|
|
648
|
+
def to_db_string(self, value, quote=True):
|
|
649
|
+
return self.inner_field.to_db_string(value, quote=quote)
|
|
650
|
+
|
|
651
|
+
def get_sql(self, with_default_expression=True, db=None):
|
|
652
|
+
if db and db.has_low_cardinality_support:
|
|
653
|
+
sql = "LowCardinality(%s)" % self.inner_field.get_sql(with_default_expression=False)
|
|
654
|
+
else:
|
|
655
|
+
sql = self.inner_field.get_sql(with_default_expression=False)
|
|
656
|
+
logger.warning(
|
|
657
|
+
f"LowCardinalityField not supported on clickhouse-server version < 19.0 using {self.inner_field.__class__.__name__} as fallback"
|
|
658
|
+
)
|
|
659
|
+
if with_default_expression:
|
|
660
|
+
sql += self._extra_params(db)
|
|
661
|
+
return sql
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# Expose only relevant classes in import *
|
|
665
|
+
__all__ = get_subclass_names(locals(), Field)
|