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