Indexes¶
The indexes a model declares in Meta.indexes: Index, PartialIndex and the index types of a dialect.
Migrations create, change and drop them with the model.
from hare.ddl import Index, PartialIndex, RawSQLTerm
from hare.dialects.postgresql.indexes import GinIndex # and the other PostgreSQL index types
from hare.query.expressions import Q
from hare.query.functions import Lower
class Book(Model):
id = fields.IntField(primary_key=True)
title = fields.CharField(max_length=300)
isbn = fields.CharField(max_length=13)
published_at = fields.DateField(null=True)
class Meta:
indexes = (
Index(fields=("-published_at", "title")),
Index(Lower("title"), name="book_title_lower"),
PartialIndex(fields=("isbn",), condition=Q(published_at__isnull=False), unique=True),
)
Index¶
class Index:
def __init__(
self,
*expressions: Term | Expression | Ordering,
fields: tuple[str, ...] | list[str] | None = None,
name: str | None = None,
opclasses: tuple[str, ...] | list[str] | None = None,
unique: bool = False,
include: tuple[str, ...] | list[str] | None = None,
) -> None: ...
An index has keys of fields or of expressions — one of them, not both, or ConfigurationError.
Meta.indexes also takes a plain tuple of field names — an index over their columns, its name generated,
as Index(fields=...) gives.
Key order and NULL placement¶
A key is ascending unless its field name starts with -: Index(fields=("-published_at", "title")) is
(published_at DESC, title); a relation of several key columns takes its direction on each of them —
("-owner", "title") is (owner_a DESC, owner_b DESC, title). For an explicit NULL placement, spell a key
as an ordering —
Index(F("rating").desc(nulls_last=True), name="book_rating_nulls_last"),
Index(F("rating").asc(nulls_first=True), F("price"), name=...); F("a")/F("a").desc() keys with no
NULL placement are the same index as fields=("a",)/("-a",). SQLite indexes can't place NULLs (they
keep them first ascending, last descending), and ClickHouse's keep no key direction at all: a model
declaring such an index raises ConfigurationError when it is bound to its connection (Hare.init()),
before makemigrations writes a migration no migrate could apply. So does an expression reading a
related model (Coalesce(F("owner"), 0), Lower("owner__name")): an index reads its table's own
columns — name the relation's column, Coalesce(F("owner_id"), 0).
Expression keys¶
A key may be an expression, with a direction and a NULL placement as above:
Index(Lower("title").desc(nulls_last=True), name="book_title_lower_desc"). A collated key is an
expression too: Index(Collate("title", "C"), name="book_title_c"). Each database writes an expression
key in its own SQL — Index(Trunc("published_at", "month")), Index(Extract("published_at", "year")),
Index(Cast("price", IntField())), Index(F("details__edition")) too; an unnamed one is named after its
keys, the same name on every database.
A key whose SQL needs one of the functions hare installs on its own SQLite connections (a truncated or
extracted date part, a cast) raises UnSupportedError when the index is created on SQLite: the database
keeps an index up to date without hare, and any other client writing the table couldn't (see In
constraints and indexes).
A UniqueConstraint takes field names; an expression is unique through
Index(<expression>, unique=True).
Non-key columns: include¶
include names fields stored in the index as non-key columns (PostgreSQL INCLUDE), so a query reading
only them and the key columns is answered from the index alone; SQLite has no such columns and creates
the index without them. Btree, GiST and SP-GiST indexes take it —
GinIndex/BrinIndex/HashIndex/BloomIndex/IvfflatIndex/HnswIndex raise UnSupportedError for
it.
Operator classes: opclasses¶
opclasses gives each field of fields its operator class, so it needs fields and one class per field
(ConfigurationError otherwise) — e.g. an index serving LIKE by a prefix on PostgreSQL:
PartialIndex¶
class PartialIndex(Index):
def __init__(
self,
*expressions: Term | Expression | Ordering,
fields: tuple[str, ...] | list[str] | None = None,
name: str | None = None,
# condition: Q - a condition over the model's fields; RawSQLTerm - a raw WHERE predicate
condition: Q | RawSQLTerm | None = None,
opclasses: tuple[str, ...] | list[str] | None = None,
unique: bool = False,
include: tuple[str, ...] | list[str] | None = None,
) -> None: ...
An index of the rows matching condition. A Q condition reads the model's own fields, as a
constraint's does — see Constraints. A RawSQLTerm condition
is written as the WHERE clause as it is — for what a Q can't say (a function call, a
database-specific operator):
A database without partial indexes (Features.supports_partial_indexes — ClickHouse) gets the index of
every row: it serves the queries the partial one serves, and hare drift takes it for the declared
index. A unique index or UniqueConstraint refuses the model there when it is bound, and a migration
adding one before its first statement — ClickHouse keeps nothing unique; see ClickHouse
models.
Index types of a dialect¶
PostgreSQL has a family of index types in hare.dialects.postgresql.indexes —
GinIndex/GistIndex/BrinIndex/BloomIndex/HashIndex/SpGistIndex — all subclasses of
PartialIndex with the same constructor, so every one of them takes condition= too. Only a plain index
can be unique: unique=True on an index type raises UnSupportedError. See PostgreSQL index
types.
Index names¶
An index without name= gets one generated from its table and columns — plus, when set, its access
method, operator classes, include columns, storage parameters and condition, so several unnamed indexes
on the same columns (a GiST and an SP-GiST one, two HNSW ones with different operator classes, a plain
and a partial one) each get their own name. A plain btree index's name depends on its table and columns
only.
A model whose indexes would share a name raises ConfigurationError as it is defined: the same unnamed
index declared twice, or one name= given to two indexes — or to an index and a unique or exclusion
constraint, which the database keeps as indexes too. So does a unique index next to a UniqueConstraint
over the same fields with the same condition (or none) — one rule declared twice; with different
conditions (PartialIndex(fields=["isbn"], condition=Q(in_print=True), unique=True) and
UniqueConstraint(fields=["isbn"], condition=Q(archived=False))) they are two rules and both are kept.