Custom fields¶
A field of your own subclasses hare.fields.Field.
What a field overrides¶
What you typically override:
field_type— the Python type of the field's values, a class attribute:field_type = int.SQL_TYPE— a string, or a@property.to_db_value(self, value, instance)/from_db_value(self, value)— custom (de)serialization;to_lookup_value(self, value, instance)converts a filter's value (to_db_value()by default).to_db_value()converts the value and leavesinstanceas it is: what a write sets on the object itself (anauto_nowstamp, a blind index) is set before the values are converted.to_python(self, value)— normalizes a value freshly assigned viaModel(**kwargs); only needed when a fresh value should get the same treatmentfrom_db_valuegives a value read from the database (see below) — leave it alone for an asymmetricto_db_value/from_db_valuepair (encryption, masking, ...).get_json_value(self, value)— the value's JSON form, what an outbox event of a captured change holds of it. The default writes JSON's own values and the table of OutboxJsonEncoding, elements of lists and mappings too; a field whose values have no form there (an object of its own) returns one its consumers read — it raisesTypeErrorotherwise, and the write fails instead of the event.get_computed_field(self, computation)— the field a result computed from the field's values is read as;computationis aValueComputation(hare.fields.enums:SUM,AVERAGE,STATISTIC,ARITHMETIC). The field itself by default; a field the database computes with in a wider type returns a field of that type — ClickHouse'sFloat32Fieldgives aFloatFieldfor a sum, an average and arithmetic.get_column_type_options(self)— what besides its class sets the field's column type, as a tuple: a cast to the field (Cast(value, field)) keeps its plan per these options.()by default; ClickHouse'sClickhouseDatetimeFieldgives its precision and zone.deconstruct()— returns(path, args, kwargs): what a migration file writes and the autodetector compares. Extend it if your field takes constructor args beyond the base ones.migration_import_path— a stable dotted path, if the field's module might move.
from hare.fields import Field
class MoneyField(Field[int]):
"""Stores an integer amount of cents."""
field_type = int
SQL_TYPE = "BIGINT"
def to_db_value(self, value, instance):
self.validate(value)
return None if value is None else int(value)
def from_db_value(self, value):
return None if value is None else int(value)
Freshly assigned values: to_python¶
Model(**kwargs)/Model.objects.create(**kwargs) construction runs each keyword value through
to_python(self, value), not from_db_value() — a value read from the database and a value the caller
just typed in are different things, even though they often need the same handling. The default
implementation returns the value unchanged, which is safe for any field whose
to_db_value/from_db_value pair isn't a faithful, idempotent encode/decode of an already-Python value.
Override to_python() (typically to just call self.from_db_value(value), the same conversion a value
read from the database gets) when a freshly assigned value genuinely needs that same normalization — e.g.
parsing a bare ISO string into a real datetime.
Don't override it (keep the default) when to_db_value/from_db_value are asymmetric — an
encoder/decoder pair that isn't its own inverse when applied to an already-decoded value, as with
encryption or masking. Running from_db_value (the decode half) on a value that never went through
to_db_value would silently corrupt it in memory, and a subsequent save() would then encode the
already-corrupted value — for an encryption field, that means writing plaintext to the database instead
of ciphertext.
class EncryptedField(Field[str]):
field_type = str
SQL_TYPE = "TEXT"
def to_db_value(self, value, instance):
self.validate(value)
return None if value is None else encrypt(value)
def from_db_value(self, value):
return None if value is None else decrypt(value)
# No to_python() override - the default (return the value as-is) is correct:
# Model(secret="hello") must keep "hello" in memory, not decrypt() a value that was never
# encrypted in the first place.
Per-dialect storage¶
Each dialect has a type registry mapping a field class to how it stores it: a column type, the DDL of a
database-generated primary key and a cast wrapped around the column wherever it is compared or ordered. A
field class uses the mapping of its nearest registered base class, so MoneyField above takes BIGINT
everywhere unless a dialect registers something else for it:
from hare.dialects.base.types import TypeMapping
from hare.dialects.dialect_registry import DialectRegistry
DialectRegistry.get_dialect("sqlite").types.register(MoneyField, TypeMapping(column_type="INTEGER"))
A field that sets COLUMN_TYPE_FROM_DIALECT = True exists wherever a dialect's types give it a column
type (TypeMapping.column_type, registered for its class or a class between it and the one setting
that) and nowhere else; a model of it bound to a connection of another dialect raises UnSupportedError
(Hare.init()), as does a field whose declaration the dialect's type refuses (a ClickHouse
PointField(null=True): a point holds no NULL there). A field of one database is such a field, its type
registered by that database's dialect — it may still name the type itself:
class LtreeField(Field[str]):
SQL_TYPE = "ltree"
COLUMN_TYPE_FROM_DIALECT = True
DialectRegistry.get_dialect("postgresql").types.register(
LtreeField, TypeMapping(column_type=lambda field: field.SQL_TYPE, extension="ltree")
)
A field several dialects store, each in a type of its own, names none of them — GeometryField and
VectorField are such fields. A dialect built on
another one's types takes such a field away with an empty mapping:
types.register(GeometryField, TypeMapping()).
A TypeMapping leaves to the field whatever it doesn't set:
| Attribute | What it sets |
|---|---|
column_type |
The column type, or a (field) -> str building it. |
generated_sql |
The DDL of a database-generated primary key column. |
function_cast |
A (field, term) -> term wrapping the column wherever it is compared, ordered or copied. |
to_db |
A (field, value, instance) -> value in place of the field's to_db_value(). |
to_db_refuses_only |
to_db only refuses a value — one the field's native_write_check leaves to it — and writes any other as the field does: the write accelerator writes every other value itself (SQLite's FloatField refuses NaN this way). |
to_lookup |
A (field, value, instance) -> value in place of the field's to_lookup_value(). |
to_python |
A (field, value) -> value in place of the field's from_db_value(). |
json_term |
A (field, term) -> term giving the text the value is written into a JSON object as — for a value stored in a form JSON can't hold (a UUID in 16 bytes). |
naive_datetime_is_utc |
The field's own from_db_value(), with a naive datetime from the driver read as a UTC instant. |
extension |
The database extension the column type needs — created wherever a field of the class is used (postgis for a GeometryField, vector for a VectorField on PostgreSQL). |
inserted_by_select |
An INSERT writing the column writes its rows as a SELECT of them, not as VALUES — for a value written as an expression the dialect's VALUES doesn't read; or a (field) -> bool deciding it per field. |
field.get_column_type(dialect), field.get_generated_sql(dialect) and
field.get_function_cast(dialect) read the result; a field computing them from its own configuration
overrides these methods.
Lookups, value paths and generated columns¶
A field whose value isn't a plain scalar tells hare how it is filtered by overriding these methods of
Field — hare's own array, range and hstore fields are built this way, so a third-party field gets the
same treatment without any change to hare:
| Method / attribute | What it gives |
|---|---|
get_lookups() |
The field's lookups by suffix ("" is plain equality), each a FieldLookup like a register_lookup() one — the generic set by default; a field with a set of its own returns it instead, and a filter with a suffix outside it raises FieldError. register_lookup() lookups are added to it. |
get_path_transform(segment) |
How a path segment after the field's name reads inside its value — (function building the term from the field's term, field of the value read), or None. Filters (tags__0__startswith), F(), values() and order_by() follow it. |
get_lookup_value_description(lookup) |
(LookupValueShape, type) of a lookup's value where it isn't a value of the field's type (an array's contains takes a list of elements), for get_lookup_info(); None otherwise. |
get_like_text_function() |
How the value becomes text for contains/icontains/startswith/... and the pattern lookups; None casts it to VARCHAR. |
get_generated_from_field_names() / with_renamed_generated_from_field(old, new) |
The model's other fields a generated column is computed from, by name — a migration refuses to remove such a field while this one reads it, and a rename renames it here. |
holds_container_value |
True for an array, range or JSON-like value — an annotation of such a field takes its own lookups. |
holds_intervals |
True for a column of the database's interval type — a duration added to a moment as the database adds it, not as a count of microseconds. |
import operator
from functools import partial
from hare.fields import CharField
from hare.query.filters import FieldLookup
from hare.sql.terms import Function
class CodeField(CharField):
def get_lookups(self):
def starts_with(term, value):
return term.like(f"{value}%")
return {"": FieldLookup(operator.eq), "prefix": FieldLookup(starts_with)}
def get_path_transform(self, segment):
if segment == "upper":
return partial(Function, "UPPER"), self
return None
A dialect adds a path segment to field classes it doesn't define itself — PostgreSQL's name__unaccent
on text fields — with FieldClass.register_transform(segment, get_transform, required_extension=...) —
get_transform(field) returns the same pair as get_path_transform() — from its Dialect.install(),
which runs once when the dialect is registered.