ClickHouse: types¶
Every field of hare has a ClickHouse column type; hare.dialects.clickhouse.fields adds the types only
ClickHouse has, and the containers of hare.fields hold any field, each other too, to any depth.
hare inspectdb writes every one of them back as its field, and hare drift compares them as whole
trees.
Fields and their columns¶
| Field | Column |
|---|---|
SmallIntField, IntField, BigIntField |
Int16, Int32, Int64 |
Int8Field, UInt8Field, UInt16Field, UInt32Field, UInt64Field, UInt128Field, UInt256Field, Int128Field, Int256Field |
Int8, UInt8 ... Int256 — hare.dialects.clickhouse.fields, each a ClickhouseIntegerField taking only the values its type holds; Int8Field is the sign column of a CollapsingMergeTree |
FloatField, Float32Field |
Float64, Float32 |
DecimalField(max_digits, decimal_places) |
Decimal(P, S) |
BooleanField |
Bool |
CharField, TextField |
String |
FixedStringField(length) |
FixedString(length) |
DatetimeField |
DateTime64(6, 'UTC') — microseconds, stored in UTC |
DateField |
Date32 |
ClickhouseDatetimeField(precision=None, timezone="UTC") |
DateTime('UTC') — whole seconds; DateTime64(precision, 'UTC') with a precision |
ClickhouseDateField |
Date |
TimeField |
String — the ISO time of day, ordered and compared as that text (10:00+03:00 after 08:00+00:00); to order moments, keep them in a DatetimeField |
TimeDeltaField |
Int64 — microseconds |
UUIDField |
UUID — ordered and compared in ClickHouse's own order, which takes the lower 64 bits first: a uuid7 key doesn't sort by time here, so order by a date column |
BinaryField |
String of the bytes' lowercase hex |
JSONField |
JSON, String over clickhouse-driver — see JSON |
CharEnumField, IntEnumField |
Enum8/Enum16 — see Enums |
IPAddressField, IPv4AddressField |
IPv6 (an IPv4 address mapped into it, read back as IPv4; ordered and compared in ClickHouse's own IPv6 order), IPv4 |
ArrayField, MapField, TupleField, NestedField |
Array, Map, Tuple, Array(Tuple(...)) — see Containers |
LowCardinalityField(field) |
LowCardinality(T) |
VariantField([...]), DynamicField() |
Variant(...), Dynamic |
GeometryField |
Point, LineString, Polygon, MultiLineString, MultiPolygon — see Geo |
A nullable field's column is Nullable(...) — a leaf's alone: an array, a map and a tuple are never
Nullable (see Containers). An integer column wraps around on overflow, so hare checks
the ranges of the integer and decimal values it writes itself; an unsigned 64-bit value above 2⁶³ and
the 128- and 256-bit integers are Python int. ClickHouse's integer arithmetic is its own: a result
past its type's range wraps around, in a query that reads it (annotate(next=F("total") + 1)) and in
a value an update() writes (update(total=F("total") + 1) stores 2147483647 + 1 as -2147483648
in an Int32) — see Integer arithmetic. A remainder of
integers (F("count") % -2)
is ClickHouse's own modulo(), and so is a division or a remainder by zero (inf, nan or the server's
error for integers); hare checks neither.
ClickHouse reads a number literal as the narrowest type holding it, and an unsigned one shares no type
with a negative one. So hare writes an integer beyond Int64 through a wide type
(toInt128('18446744073709551615')), the values of the rows of a bulk_update() and of a VALUES list
as their column's type, and a literal branch of a Case (or Coalesce) of an integer result as that
type (accurateCast; a literal the type doesn't hold stays as it is, read as a type holding it). A UUID
literal is read as a UUID (toUUID),
and a dict branch of a Case of a map result as that map — a Case mixing literals and a column of the
type works as on PostgreSQL.
Float32Fieldreads a value back as the shortest decimal that is the same float32 —0.1, not the0.10000000149011612the drivers give; a value beyond its range, or so close to zero it would be stored as 0 (1e-50), raisesValidationError— a float32 holds neither. A sum, an average and arithmetic of its values are theFloat64ClickHouse computes them in and read as aFloatField; a statistic (StdDev,Variance) stays a float32.FixedStringField(length)holds a text of at mostlengthbytes in UTF-8, padded with zero bytes and read back without them; a text holding a zero byte of its own raisesValidationError, as the server would read it as the text's end.ClickhouseDatetimeFieldkeeps a moment in ClickHouse's ownDateTime(whole seconds, 1970 to 2106) or, withprecision(0 to 9 digits after the second),DateTime64(precision)(1900 to 2299), in the column'stimezone— an IANA name,Nonefor the server's own; it reads and writes likeDatetimeField, whose lookups (__year,__hour, ...) it has. A written moment is rounded to the column's precision, a tie away from zero, as aDecimalFieldrounds to its places; one outside aDateTimecolumn's range raisesValidationError.ClickhouseDateFieldkeeps a day in aDate(1970-01-01 to 2149-06-06), a day outside it raisingValidationError. Both are inhare.dialects.clickhouse.fields;inspectdbmaps these columns onto them. A cast to aClickhouseDatetimeField(Cast(value, ClickhouseDatetimeField(precision=3))) converts to its own type — its precision and zone.
Enums¶
A CharEnumField or an IntEnumField is an Enum8/Enum16 of labels and numbers. ClickHouse keeps the
numbers and reads the labels off the type, so a label keeps its number for good:
- an
IntEnumField's labels are its members' names, numbered by their values; - a
CharEnumField's labels are its stored values, numbered from a checksum of the label — a member added, removed or moved changes no other label's number. The server would compare and sort the column by those numbers, so hare reads it as its label there (toString(status)):order_by(),__gtandMin/Maxfollow the text, as on PostgreSQL.
A migration changes the members with MODIFY COLUMN. Removing a member checks first that no row holds it
— the server refuses to drop a label in use.
An Enum16 column takes no arithmetic, average or math function, so an IntEnumField is read by its
number there (toInt16(level)): F("level") + 1, Avg("level") and Abs("level") work on the number.
Max/Min keep the member. An update() setting an IntEnumField from an expression
(update(level=F("tiny"))) refuses a number no member has — the server's mutation would write it into the
column, which the drivers then read as None or fail on.
Containers¶
from hare import fields
class Delivery(Model):
tags = fields.ArrayField(fields.CharField(max_length=20))
prices = fields.MapField(fields.CharField(max_length=3), fields.DecimalField(max_digits=10, decimal_places=2))
point = fields.TupleField({"lon": fields.FloatField(), "lat": fields.FloatField()})
items = fields.NestedField({"isbn": fields.CharField(max_length=20), "quantity": fields.IntField()})
history = fields.ArrayField(
fields.MapField(fields.CharField(max_length=10), fields.ArrayField(fields.IntField(null=True)))
)
| Field | Value | Paths | Lookups |
|---|---|---|---|
ArrayField(field) |
a list | tags__0 an element (from 0, negative from the end), tags__0_2 a slice, tags__len |
__contains, __contained_by, __overlap, __len, __item=(index, value) |
MapField(key_field, value_field) |
a dict | prices__eur the value of a key (the value type's default for a missing key; a segment named like a lookup — prices__in — is that lookup, which a map refuses as an array does), prices__keys, prices__values, prices__len |
__has_key, __has_keys, __has_any_keys, __len |
TupleField([...]) / TupleField({...}) |
a tuple / a dict by name | point__0, point__lat |
those of the element |
NestedField({...}) |
a list of dicts | items__0 a row, items__isbn the column of every row, items__len |
the array lookups |
Each value inside a container is written and read by the field holding it — a decimal, a moment, a UUID,
an enum at any depth — and a path of any length reads through them: history__0__open__1__gt=3 is the
second number under the key open of the first map. A lookup at the end of a path is the one of the
field there.
NULL. An array, a map and a tuple are neverNullablein ClickHouse, sonull=Trueon one — at the top or inside another container — has no column: the model raisesUnSupportedErrorwhen it is bound.Nonegiven to the model (Shipment(tags=None),create(tags=None)) raisesQueryError, as on any field withoutnull=True, and a container left unset or set toNoneafterwards raisesValidationErroron every write — the server would store an empty array or a tuple of zeros instead.null=Trueon a scalar field inside makes that elementNullable.Min/Maxof an array, a map or a tuple over no rows give its empty value (an empty array or map, a tuple of zeros), notNone.Lag/Leadof a container take adefaultof its type: without one — past the partition's edge the value is NULL, which a container has no form of — and forNthValuethey raiseUnSupportedErrorbefore any SQL. ADynamicor aVariantholds NULL itself and takes all three. AGeneratedFieldwith a containeroutput_fieldhas the same behavior, including empty aggregates.- Through a relation. A related row that isn't there — a parent with no children, an unset foreign
key — has its container and geometry columns read as their empty value, not
None: invalues(),F()andorder_by(). A filter and an aggregate through the relation still tell it from a row: a filter holds for the missing row only as__isnull=True(or__isnull=Falsenegated), andCount,Min,MaxandArrayAggleave it out. - Order. An array, a tuple and a map order and compare as ClickHouse orders them, element by
element; a
CharEnumFieldvalue inside by its label, which hare stores so, inorder_by(),Min/Maxand a filter through a path (statuses__0__gt). - Map keys are integers, texts,
FixedString, UUIDs, dates, moments and enums (alsoLowCardinalityof text or UUIDs) holding noNULL— another key field raisesConfigurationErrorwhen the field is declared. A path names a key by its value as the key field reads it:counts__onlineis the value under the enum member"online". - Errors name the path to the value they are about:
tags[2],prices['eur'],point.1. - A migration compares the whole tree of the field and changes any level with
MODIFY COLUMN; a narrowing anywhere in it checks the data first.
ArrayField runs on PostgreSQL too — a nested ArrayField(ArrayField(...)) is a multidimensional array
there; MapField, TupleField and NestedField are ClickHouse's alone.
LowCardinalityField¶
LowCardinalityField(fields.CharField(max_length=2)) stores the values as a dictionary of the distinct
ones — for a column with few of them (a country, a status). Its values, lookups and paths are those of
the field it wraps; null=True on it is LowCardinality(Nullable(T)). It wraps text, a
FixedStringField or a UUIDField: ClickHouse keeps no LowCardinality of an enum, a decimal or a
moment, and one of a number, a date or an address only under a server setting, so another field raises
ConfigurationError when its column type is built. It wraps no container.
VariantField and DynamicField¶
from hare.dialects.clickhouse.fields import DynamicField, VariantField
reading = VariantField([fields.IntField(), fields.CharField(max_length=20)], null=True)
value = DynamicField(null=True)
A VariantField holds a value of one of its fields' types. A value is taken by the first field whose
type it is exactly, else by the first it is an instance of, else by the first converting it; a value read
is given back by the field of its ClickHouse type — so give fields read as distinct Python types. A
filter matches the values of the filter value's field only, and reading__0__gt=3 reads the values of
the field at that position, NULL for the others.
A DynamicField holds a value of any type, kept with the type its Python value has (an int as Int64, a text
as String, a list as an Array) and read back as it was written — a moment in the configured time zone, as
a DatetimeField reads it (a naive one written is the UTC wall clock). max_types caps the types kept in
columns of their own. A filter matches the values of the filter value's type only (value=5 doesn't match
"5"); a path named by a type reads the values of that type — value__String__startswith="a",
value__Int64__gt=3.
ClickHouse orders no rows by a Variant or Dynamic column — order_by() on one raises
UnSupportedError; order by a path of it. The column holds NULL itself: its fields take no
null=True.
Geo¶
A GeometryField of hare.gis is a Point, LineString, Polygon, MultiLineString or
MultiPolygon column, x and y only, with no SRID — a value read takes the field's, and a geometry of
another SRID in a write, a filter or an equality raises (ClickHouse transforms none). A point is a tuple,
and a line and an area are arrays: none of them is Nullable, so a geometry field with null=True
raises UnSupportedError when its model is bound, and a None written raises ValidationError. An empty
geometry is ClickHouse's own value, and a function of it gives ClickHouse's own answer or error. A
polygon's rings are stored in the orientation ClickHouse's functions read — the outer ring
clockwise — so a polygon given the other way round reads back with its rings reversed.
hare.gis |
ClickHouse |
|---|---|
A point in an area (__intersects, __within, __contains, __covers, __covered_by, __disjoint) |
pointInPolygon — a point on the border counts as in the area on some of its sides only (the left and the bottom of a square, not the right and the top): test a point near a border by distance |
| Relations of two areas | polygonsIntersect*, polygonsWithin*, polygonsEquals* |
__intersects, __equals, __disjoint of two points |
the coordinates compared — a point meets only the same point |
A relation or a Distance of a line |
UnSupportedError — ClickHouse has no test of it |
Area, Perimeter, ConvexHull, Intersection, Union, SymmetricDifference of areas |
polygonArea*, polygonPerimeter*, polygonConvexHullCartesian, polygons* |
Distance of points |
L2Distance; geoDistance in meters for a geography |
__dwithin |
the distance compared with the one given |
Distance of areas |
polygonsDistance* |
AsText |
wkt |
A geography (GeometryField(geography=True)) is measured on a sphere of WGS 84's mean radius with the
*Spherical functions — meters and square meters, a little off PostGIS's ellipsoid over long distances;
a function ClickHouse has only on the plane (ConvexHull, polygonsEquals) raises UnSupportedError
for it, as does anything else ClickHouse has no function of.