PostgreSQL: поля¶
Всё на этой странице находится в hare.dialects.postgresql — собственном пакете диалекта PostgreSQL — и
работает только с подключением к PostgreSQL. Если объявить любое из этих полей в модели, которая работает
с другим диалектом (SQLite, ClickHouse), при создании команд таблицы будет UnSupportedError, а не
команды, не имеющие смысла для этой базы. Все поля этой страницы, кроме ArrayField, импортируются из
hare.dialects.postgresql.fields; ArrayField — это hare.fields.ArrayField, который работает и в
ClickHouse, — см. Типы ClickHouse:
from hare.dialects.postgresql.fields import (
CitextField, FixedCharField, HStoreField, InetField, IntervalField, IntRangeField, LtreeField,
NaiveDatetimeField, NaiveTimeField, NativeEnumField, PostGISField, Range, TSVectorField,
# ... любое поле этой страницы
)
from hare.fields import ArrayField
ArrayField¶
Настоящий массив PostgreSQL. Тип элемента задаётся настоящим объектом поля, а не строкой с названием
типа:
Элемент None хранится как элемент SQL NULL при любом base_field ([1, None, 3] в массиве
IntField); каждый другой элемент проходит преобразование и проверку самого base_field. Для массива с
элементами None объявляйте base_field с null=True:
pydantic-схема типизирует элемент по нему (list[int] или
list[int | None]), а ClickHouse хранит элемент None только в Nullable.
Собственные операторы фильтра: contains, contained_by, overlap, len, а также пути к элементу
(keywords__0), срезу (keywords__0_2) или длине (keywords__len) — см.
Операторы фильтра и пути.
Операторы фильтра и пути¶
Тоже отдельный набор: exact, not, isnull, not_isnull, contains, contained_by, overlap,
len, item.
await Book.objects.filter(keywords__contains=["sea"])
await Book.objects.filter(keywords__overlap=["sea", "whaling"])
item сравнивает один элемент по позиции, отсчитываемой с 0 (как в Python); внутри она переводится в
индекс массива PostgreSQL, который отсчитывается с 1:
У вложенного массива (ArrayField(base_field=ArrayField(...))) len считает строки (первое измерение),
а item сравнивает строку целиком: matrix__item=(0, [1, 2]). Массив PostgreSQL прямоугольный:
подмассивы разной длины на одной глубине ([[1, 2], [3]]) или значение рядом с подмассивом
([[1, 2], None]) дают ValidationError до отправки. Массив пустых подмассивов ([[]], [[], []])
PostgreSQL сохраняет как пустой массив, и он читается как []; ClickHouse сохраняет его как есть.
Чтобы получить элемент как вычисляемое значение, а не фильтровать по нему, используйте
ArrayItem — позиция тоже отсчитывается с 0:
Путь через массив читает значение, которое принимает операторы своего типа:
| Часть пути | Что читает | Тип |
|---|---|---|
keywords__0 |
элемент на позиции, отсчитываемой с 0 (-1 — последний); NULL за пределами массива |
тип элемента |
keywords__0_2 |
элементы с 0 до 2, не включая 2 | тип массива |
keywords__len |
число элементов (строк у вложенного массива) — 0 у пустого массива |
целое число |
Части пути можно продолжать, в том числе через вложенные массивы (matrix__1__0, matrix__0__len), и те
же пути работают в F(), values()/values_list(), order_by(), group_by() и после связи
(books__keywords__0):
await Book.objects.filter(keywords__0__startswith="se")
await Book.objects.filter(keywords__len__gte=2)
await Book.objects.filter(keywords__0_2__contains=["whaling"])
await Book.objects.all().order_by("keywords__0").values_list("keywords__0", "keywords__len")
NaiveDatetimeField, NaiveTimeField, IntervalField, FixedCharField¶
class NaiveDatetimeField:
def __init__(self, **kwargs: Any) -> None: ...
class NaiveTimeField:
def __init__(self, **kwargs: Any) -> None: ...
class IntervalField:
def __init__(self, **kwargs: Any) -> None: ...
class FixedCharField:
def __init__(self, max_length: int, **kwargs: Any) -> None: ...
Типы PostgreSQL, которые не пишут собственные поля hare: DatetimeField и TimeField хранят момент
(timestamptz, timetz), TimeDeltaField — bigint микросекунд, CharField — varchar.
NaiveDatetimeField—timestamp: дата и время по часам без часового пояса, хранится как наивныйdatetime, что бы ни говорилuse_timezone. Принимает наивный datetime, дату (её полночь) или строку ISO 8601; datetime с поясом называет момент, а не время по часам, и даётValidationError. У поля те же части даты, что уDatetimeField(__year,__hour, ...), его__dateи__time, и оно принимаетExtract()/Trunc()иvalues("at__date")— всё читается с часов как есть, без пояса;Trunc(..., "time")даёт наивныйtime.F("at") ± timedeltaдаёт время по часам, аF("at") - F("at2")двух таких —timedelta; время по часам и момент (DatetimeField,datetimeс поясом) не смешиваются в арифметике, вCoalesce(),Case(),Greatest(),Least()иNullIf(), иupdate()не присваивает одно другому (update(at=F("moment"))) —FieldError: они различаются поясом. Наивная константаdatetimeрядом со временем по часам (Coalesce(F("at"), datetime(2026, 7, 5, 9, 15))) — это то же время по часам, в любом настроенном поясе; константа внутри другого выражения из одних констант (Coalesce(F("at"), Case(When(..., then=datetime(...))))) там — момент настроенного пояса, и рядом со временем по часам она отклоняется.NaiveTimeField—time: время суток по часам, хранится как наивныйtime; время сtzinfoдаётValidationError. У поля есть__hour,__minute,__second, и оно принимаетExtract()иTrunc()времени суток.IntervalField—interval, хранится какtimedelta. PostgreSQL хранит месяцы интервала отдельно от дней: интервал, записанный через SQL ('1 month'), читается как 30 дней на месяц и 365 дней на год.FixedCharField(max_length)—character(max_length): PostgreSQL дополняет текст пробелами доmax_length, поэтому он читается дополненным ("ab"изcharacter(4)читается как"ab ") и сравнивается без хвостовых пробелов (filter(code="ab")его находит). Проверяется какCharField.
inspectdb сопоставляет колонки timestamp, time, interval и character(n) с этими полями.
CitextField¶
Колонка CITEXT PostgreSQL — текст без учёта регистра: сравнение, индексы и сортировка работают без
UPPER()/LOWER() с любой стороны запроса. Нужно расширение citext; автоматическое создание миграций
само добавляет CreateExtension("citext") везде, где используется это поле, — объявлять
Meta.extensions не нужно (см. Операции миграций —
расширения).
class Reader(Model):
email = CitextField()
await Reader.objects.filter(email="Someone@Example.com").first() # совпадёт и с "someone@example.com"
__contains/__startswith/__endswith тоже не учитывают регистр: колонка сравнивается как citext, а
не приводится к VARCHAR.
Значение с нулевым байтом отклоняет сам PostgreSQL, как и у CharField/TextField и JSONField, —
hare поднимает на это ValidationError (при записи, обновлении и в фильтре), а не OperationalError.
NativeEnumField¶
from hare.dialects.postgresql.fields import NativeEnumField
def NativeEnumField(
enum_type: type[NativeEnumType],
*,
type_name: str | None = None,
type_schema: str | None = None,
**kwargs: Any,
) -> NativeEnumType: ...
Член перечисления в колонке собственного типа ENUM PostgreSQL —
CREATE TYPE loan_status AS ENUM ('lent', 'overdue', 'returned'). Читается и записывается как
CharEnumField (принимается член или его значение, возвращается член), а сама база отклоняет любую
другую метку и сортирует колонку в порядке перечисления, а не по алфавиту.
class LoanStatus(Enum):
LENT = "lent"
OVERDUE = "overdue"
RETURNED = "returned"
class Loan(Model):
status = NativeEnumField(LoanStatus)
previous_status = NativeEnumField(LoanStatus, null=True) # тот же тип
await Loan.objects.filter(status__in=[LoanStatus.LENT, "overdue"]).order_by("status") # lent, overdue, returned
- Тип. Его имя —
type_name, иначе имя класса перечисления в snake case (LoanStatus→loan_status) — идентификатор в нижнем регистре длиной до 63 символов. Метки — значения членов текстом (str(member.value), поэтому подходит иIntEnum), в порядке перечисления, каждая до 63 байт. Поля, называющие один тип в одной схеме, делят его; два таких поля с разными метками даютConfigurationError. Тип живёт в схемеtype_schema, иначе в схеме подключения по умолчанию, какой бы ни былаMeta.schemaмодели, — поэтому в двух схемах могут быть одноимённые типы со своими метками, каждый со своейtype_schema. Перечисление без членов, неверное имя или неверная схема даютConfigurationErrorпри объявлении поля. - Схема.
generate_schemas()создаёт типы до таблиц (safe=Trueоставляет существующий). Автоопределение миграций пишетCreateEnumTypeперед первой колонкой типа,AlterEnumType, когда меняются члены перечисления, иDropEnumTypeпосле того, как ушла его последняя колонка, — см. Операции миграций — типы ENUM. Схема, которую называетtype_schema, создаётся так же, какMeta.schemaмодели (CreateSchema). ЗаменаCharEnumFieldнаNativeEnumFieldили перенос колонки на другой тип (другиеtype_nameилиtype_schema) преобразует её на месте через текст (ALTER COLUMN ... TYPE ... USING). - Изменение членов. Новые члены, добавленные так, что старые сохраняют порядок, становятся новыми
метками на месте (
ALTER TYPE ... ADD VALUE) — добавленное так значение нельзя использовать в той же транзакции, поэтому записывайте такие строки в следующей миграции. Удалённый или переставленный член заменяет тип: колонки этого типа (и колонки-массивы) и их значения по умолчанию преобразуются в новый тип одной командой, поэтому ни одна строка не должна хранить удалённую метку — сначала обновите такие строки: миграция, нашедшая такую строку, бросаетFieldNarrowingDataLossErrorс именем таблицы и колонки, ничего не изменив. - Фильтры — общий набор: равенство,
__in,__isnullи сравнения в порядке перечисления. Метка вне перечисления даётValidationErrorдо выполнения запроса. - Только PostgreSQL (
features.supports_enum_types); на другой базе поле даётUnSupportedError.
Сетевые поля: InetField, CidrField, MacAddressField¶
from hare.dialects.postgresql.fields import CidrField, InetField, MacAddressField
class Host(Model):
address = InetField() # inet
subnet = CidrField(null=True) # cidr
mac = MacAddressField(null=True, unique=True) # macaddr
InetField— адрес узла IPv4 или IPv6 с длиной префикса его сети, если она есть. Читается какipaddress.IPv4Address/IPv6Addressили какIPv4Interface/IPv6Interface(адрес и сеть), когда префикс короче полной длины (10.0.1.7/16). Принимаются текст и объектыipaddress(адрес, интерфейс, сеть).CidrField— сеть IPv4 или IPv6, читается какipaddress.IPv4Network/IPv6Network; принимаются текст и объектыipaddress, адрес — как сеть из него одного (/32,/128). Сеть с установленными битами узла (10.0.0.1/8) даётValidationError, как и колонка её не принимает.MacAddressField— 6-байтовый MAC-адрес в любой привычной форме (08:00:2B:01:02:03,08-00-2b-01-02-03,0800.2b01.0203,08002b010203); читается и записывается как08:00:2b:01:02:03.- Значение не того вида даёт
ValidationErrorдо выполнения запроса — и при записи, и в фильтре.
Все три фильтруются по равенству, __not, __in/__not_in, __isnull и сравнениям (__gt, ...,
__range, в порядке базы); текстовых операторов у них нет. InetField и CidrField фильтруются ещё и
по подсетям — значение: адрес или сеть, текстом или объектом ipaddress, либо F() другой колонки:
| Оператор | В SQL | Истинно, когда адрес или сеть колонки |
|---|---|---|
__net_contained |
<< |
лежит внутри значения и не равна ему |
__net_contained_or_equal |
<<= |
лежит внутри значения или равна ему |
__net_contains |
>> |
содержит значение и не равна ему |
__net_contains_or_equal |
>>= |
содержит значение или равна ему |
__net_overlaps |
&& |
содержит значение или лежит внутри него |
await Host.objects.filter(address__net_contained="10.0.0.0/8")
await Host.objects.filter(subnet__net_contains=request_ip)
await Host.objects.filter(address__net_contained_or_equal=F("subnet"))
__family (4 или 6) и __masklen (длина префикса) читают часть значения как целое — в фильтрах,
values(), F() и order_by(): filter(address__family=6), values_list("subnet__masklen"). Только
PostgreSQL: на другой базе поля дают UnSupportedError.
HStoreField¶
Колонка hstore PostgreSQL — плоский набор пар «строковый ключ — строка или None», в Python это
dict. Нужно расширение hstore; автоматическое создание миграций добавляет CreateExtension("hstore")
везде, где используется это поле. Ключ или значение, которые не являются строкой, записываются как их
str() ({"n": 5} читается как {"n": "5"}); значение, которое не является dict, или нулевой байт
дают ValidationError.
class Edition(Model):
attributes = HStoreField(null=True)
await Edition.objects.filter(attributes__binding="hardcover")
await Edition.objects.filter(attributes__has_keys=["binding", "format"])
await Edition.objects.all().values_list("attributes__binding", "attributes__keys")
Операторы фильтра и пути — см. Операторы фильтра и пути.
Операторы фильтра и пути¶
exact, not, in, not_in, isnull, not_isnull сравнивают всё значение; contains/
contained_by принимают dict пар (@>/<@); has_key принимает ключ, has_keys/has_any_keys —
список ключей (?/?&/?|).
Любое другое имя после поля — это ключ: attributes__binding — его значение (текст, None, если ключа
нет), оно принимает текстовые операторы (attributes__binding__startswith="hard"). attributes__keys и
attributes__values — все ключи/значения в виде текстового массива, с операторами и путями ArrayField
(attributes__keys__contains=["format"], attributes__keys__len=3). Сегмент с именем любого оператора
(in, contains, gt, ...) — это оператор, а не ключ: attributes__in=[...] сравнивает всё значение, а
attributes__gt=... даёт FieldError, как на любом поле без __gt; такой ключ доступен через
contains/has_key. Те же пути работают в F(),
values(), order_by() и после связи.
await Edition.objects.filter(attributes__contains={"binding": "hardcover"})
await Edition.objects.filter(attributes__format__in=["A5", "B5"])
await Edition.objects.filter(attributes__keys__overlap=["format", "weight"])
TSVectorField¶
class TSVectorField:
def __init__(
self,
source_fields: Sequence[str] | str | None = None,
config: str | None = None,
weights: Sequence[str] | None = None,
stored: bool = True,
**kwargs: Any,
) -> None: ...
stored=True и заданных source_fields создаётся как GENERATED ALWAYS AS (...) STORED — значение
поддерживает сам PostgreSQL, код приложения для этого не нужен:
class Book(Model):
title = fields.CharField(max_length=300)
description = fields.TextField()
search_vector = TSVectorField(source_fields=("title", "description"), config="english")
Миграция, меняющая тип колонки поля-источника, удаляет колонку вектора перед изменением и добавляет её после — PostgreSQL не меняет колонку, которую читает генерируемая; векторы вычисляются заново.
LtreeField¶
Колонка ltree — место узла в дереве в виде пути из меток от корня (Top.Science.Astronomy), в Python —
str. Одна колонка отвечает на вопросы «все потомки», «все предки» и на поиск по шаблону одним условием
с индексом, без рекурсивного запроса. Нужно расширение ltree; автодетектор миграций добавляет
CreateExtension("ltree") везде, где используется поле.
from hare.dialects.postgresql.fields import LtreeField
from hare.dialects.postgresql.indexes import GistIndex
class Category(Model):
path = LtreeField(unique=True)
class Meta:
indexes = (GistIndex(fields=("path",)),) # обслуживает операторы дерева и шаблонов
Путь записывается текстом или списком/кортежем меток (["Top", "Science"] — это "Top.Science", [] и
"" — пустой путь), читается текстом. Метка состоит из букв, цифр, _ и - (дефису нужен
PostgreSQL 16) и не пуста; любой другой путь — "Top..Science", метка с пробелом или нелатинской буквой,
значение, которое не str и не список str, — даёт ValidationError до выполнения запроса.
Равенство, __not, __in/__not_in, __isnull и сравнения (__gt, ..., __range — порядок обхода
дерева в глубину, родитель перед детьми) работают как у любого поля; текстовых операторов нет. Операторы
дерева принимают путь (текстом, списком меток или F() другой колонки):
| Оператор | В SQL | Истинно, когда путь колонки |
|---|---|---|
__ancestor_of |
@> |
равен значению или является его предком |
__descendant_of |
<@ |
равен значению или является его потомком |
__matches |
~ |
подходит под шаблон lquery (str) |
__matches_any |
? |
подходит под один из шаблонов lquery (непустой список str) |
__matches_text |
@ |
подходит под запрос ltxtquery (str) |
await Category.objects.filter(path__descendant_of="Top.Science") # поддерево вместе с корнем
await Category.objects.filter(path__ancestor_of=category.path) # «хлебные крошки»
await Category.objects.filter(path__matches="*.Astronomy.*") # любой узел под Astronomy
await Category.objects.filter(path__matches="Top.*{1}") # дети Top
await Category.objects.filter(path__matches_text="Astro*% & !pictures@")
Шаблон lquery сопоставляет метки по одной: * — любое число меток, *{1} — ровно одна, Astro* —
метка, начинающаяся с Astro, a|b — любая из двух, !a — любая, кроме a; запрос ltxtquery
соединяет слова через &, | и !, где * — префикс, % — слово внутри метки, разделённое _, а @
— без учёта регистра. Полный синтаксис — в документации
ltree.
path__depth — число меток (целое) в фильтрах, values(), F() и order_by():
filter(path__depth=2). Subpath(field, offset, length=None) читает часть пути как путь — метки с
позиции offset (с 0, отрицательная — с конца), length штук или до конца. Позиция за пределами пути —
дело самого subpath() PostgreSQL: положительная даёт OperationalError; отрицательная за началом пути
отсчитывается с конца ещё раз (-4 у пути из трёх меток читается как -1), а за удвоенной длиной пути
даёт OperationalError:
from hare.dialects.postgresql.functions import Subpath
await Category.objects.annotate(section=Subpath("path", 1, 1)).values_list("section", flat=True)
Только PostgreSQL: на другой базе поле даёт UnSupportedError.
PostGISField¶
Хранит geography(Point,4326) — широту и долготу в WGS84 с точными расчётами расстояний на стороне базы.
Нужно расширение postgis; автоматическое создание миграций добавляет CreateExtension("postgis")
везде, где используется это поле. Значение в Python — кортеж (latitude, longitude). Другие типы
геометрий, SRID, плоские колонки, пространственные операторы и функции — см.
Геоданные (GIS).
class Store(Model):
location = PostGISField()
class Meta:
indexes = (GistIndex(fields=("location",)),)
Сразу есть оператор фильтра within_km:
Радиус — конечное число не меньше нуля (int, float или Decimal), точка проверяется так же, как
записываемое значение. Иначе — ValidationError до отправки запроса.
Для сортировки и фильтров по расстоянию сложнее простого радиуса используйте вычисляемые значения
STDistance/STDWithin (см. Функции PostgreSQL).
Поля диапазонов¶
Поля диапазонов — RangeField служит основой (напрямую не используется); значение в Python —
hare.dialects.postgresql.fields.Range или, для краткости, простой кортеж (lower, upper) (полуоткрытый
диапазон [)), или словарь его атрибутов ({"lower": 1, "upper": 10}) — форма, в которой его пишет
pydantic-схема модели (она проверяет границы как тип колонки), поэтому create(**schema.model_dump()) его
принимает:
@dataclass(frozen=True)
class Range(Generic[T]):
lower: T | None = None # None — без границы
upper: T | None = None # None — без границы
lower_inc: bool = True # нижняя граница включается ("[")
upper_inc: bool = False # верхняя граница включается ("]") — по умолчанию в PostgreSQL [)
is_empty: bool = False
Пустой диапазон и диапазон без границ
PostgreSQL приводит любой диапазон, в который не входит ни одно значение (например, с равными границами
при полуоткрытом [) по умолчанию), к своему значению 'empty'. При чтении оно даёт
lower=None, upper=None — то же самое, что диапазон вообще без границ, если не проверять ещё и
is_empty. Если записать прочитанный диапазон обратно, не сохранив is_empty, «ни одного значения»
молча превратится в «все значения».
Присвоенный диапазон (при создании объекта, в create() или присваивании атрибуту) сразу хранится в
памяти в том виде, в каком его возвращает PostgreSQL: границы приводятся к типу элемента поля (строка с
датой становится date, граница даты-времени подчиняется правилам DatetimeField — см. ниже),
дискретный диапазон (IntRangeField/BigIntRangeField/DateRangeField) переписывается в вид [)
(Range(1, 5, lower_inc=False, upper_inc=True) становится Range(2, 6)), сторона без границы считается
невключённой, а диапазон без значений становится пустым. Нижняя граница больше верхней даёт
ValidationError, как и граница целочисленного диапазона вне его типа или с дробной частью
(Range(1.9, 3), span__contains=1.5) или типа bool — так же её отклоняет IntField, — и граница NaN
у DecimalRangeField, которая не упорядочивается ни с какой другой.
Range в default поля — и список таких значений у мультидиапазона — записывается в миграцию вызовом,
который его строит (default=Range(1, 5)).
Миграция, которая меняет поле диапазона на диапазон другого типа (IntRangeField на BigIntRangeField
или DecimalRangeField, так же мультидиапазон или массив диапазонов), преобразует колонку через текст
каждого значения (USING span::text::int8range): PostgreSQL не приводит один тип диапазона к другому.
Граница, которую новый тип не вмещает, останавливает миграцию ошибкой PostgreSQL.
Граница DateTimeRangeField означает то же, что значение DatetimeField: граница без пояса — это время
на часах в настроенном поясе при use_timezone=True и в местном поясе системы при use_timezone=False,
а читаются границы в настроенном поясе (use_timezone=True) или как местное время без пояса
(use_timezone=False), поэтому during__contains=at согласуется с DatetimeField, хранящим то же
значение без пояса.
Значение infinity/-infinity (колонка DATE/TIMESTAMPTZ или граница диапазона) на обоих драйверах
читается как крайнее значение Python — date.max/date.min, datetime.max/datetime.min, а запись
такого значения обратно сохраняет конечный день или момент, которым оно является (см. Крайние
даты). Граница
DateTimeRangeField, прочитанная в бесконечности, при use_timezone=True содержит пояс UTC
(datetime.min.replace(tzinfo=UTC)), а при use_timezone=False — без пояса; граница datetime.max/
datetime.min без пояса считается тем же моментом UTC, каким бы ни был настроенный пояс.
| Поле | Тип PostgreSQL |
|---|---|
IntRangeField |
int4range |
BigIntRangeField |
int8range |
DecimalRangeField |
numrange |
DateRangeField |
daterange |
DateTimeRangeField |
tstzrange |
Операторы фильтра (contains, overlap, fully_lt, adjacent_to, ...) и пути к границе
(during__startswith) или признаку (during__isempty) — см. Операторы фильтра и пути.
Поля диапазонов хорошо сочетаются с ExclusionConstraint для расписаний без пересечений — см.
Ограничения.
Операторы фильтра и пути¶
exact, not, in, not_in, isnull, not_isnull, а также операторы диапазонов PostgreSQL — каждый
принимает Range (или кортеж (lower, upper)), а contains ещё и одно значение:
| Оператор | В SQL | Подходят строки, где диапазон |
|---|---|---|
contains |
@> |
содержит значение или весь диапазон |
contained_by |
<@ |
лежит внутри диапазона |
overlap |
&& |
имеет общее значение с диапазоном |
fully_lt |
<< |
лежит целиком ниже него |
fully_gt |
>> |
лежит целиком выше него |
not_lt |
&> |
не выходит ниже него |
not_gt |
&< |
не выходит выше него |
adjacent_to |
-|- |
касается его, не пересекаясь |
Путь читает часть диапазона с её собственными операторами: startswith/endswith — нижняя/верхняя
граница (типа элемента — date у DateRangeField, NULL без границы или у пустого диапазона), а
isempty, lower_inc, lower_inf, upper_inc, upper_inf — логические значения. Они так же работают
в F(), values(), order_by() и после связи:
await Booking.objects.filter(during__fully_lt=Range(start, end))
await Booking.objects.filter(during__startswith__year=2024)
await Booking.objects.filter(during__upper_inf=True)
await Booking.objects.all().order_by("during__startswith").values("id", "during__endswith")
Поля мультидиапазонов¶
Мультидиапазон хранит в одной колонке несколько непересекающихся диапазонов — занятые дни комнаты, часы работы библиотеки, свободные места в ряду:
from hare.dialects.postgresql.fields import DateMultiRangeField, IntMultiRangeField
class RoomSchedule(Model):
busy_days = DateMultiRangeField(null=True) # datemultirange
free_seats = IntMultiRangeField(default=list) # int4multirange
| Поле | Тип в PostgreSQL | Каждый диапазон — как |
|---|---|---|
IntMultiRangeField |
int4multirange |
IntRangeField |
BigIntMultiRangeField |
int8multirange |
BigIntRangeField |
DecimalMultiRangeField |
nummultirange |
DecimalRangeField |
DateMultiRangeField |
datemultirange |
DateRangeField |
DateTimeMultiRangeField |
tstzmultirange |
DateTimeRangeField |
Каждое из них — MultiRangeField со своими SQL_TYPE и RANGE_FIELD_CLASS (поле его диапазонов);
мультидиапазон своего поля диапазона наследует его так же.
Значение в Python — список Range (каждый можно дать и кортежем (lower, upper), и словарём его
атрибутов, как его пишет схема модели), [] — пустой мультидиапазон. Каждый диапазон принимается так,
как его принимает поле диапазона (границы приводятся к типу, дискретный диапазон становится [), граница
DateTimeMultiRangeField читается в настроенном часовом поясе), а список хранится так, как его хранит
PostgreSQL: пустые диапазоны отбрасываются, остальные сортируются по нижней границе, пересекающиеся и
касающиеся сливаются в один — в памяти сразу при присваивании, а не только после чтения:
schedule = RoomSchedule(free_seats=[(8, 9), (1, 3), (3, 5), Range(4, 4)])
schedule.free_seats # [Range(1, 5), Range(8, 9)]
Два диапазона, касающиеся в границе, сливаются, если её включает хотя бы один: [1, 2) и [2, 3)
становятся [1, 3), а [1, 2) и (2, 3) остаются раздельными (2 не входит ни в один). Значение, не
являющееся списком диапазонов, — в том числе один Range или кортеж (lower, upper) вместо списка —
даёт ValidationError с именем поля и позицией неверного диапазона (busy_days[1]: ...); так же —
диапазон, который не принимает поле диапазона.
Операторы фильтра и пути¶
Операторы поля диапазона (Операторы фильтра и пути) — exact, not, isnull,
not_isnull, contains, contained_by, overlap, fully_lt, fully_gt, not_lt, not_gt,
adjacent_to. Значение — список диапазонов или один Range/кортеж (lower, upper), который означает
мультидиапазон из него одного; contains принимает и одно значение типа элемента:
await RoomSchedule.objects.filter(busy_days__contains=date(2024, 1, 11))
await RoomSchedule.objects.filter(busy_days__overlap=[(start, end), (other_start, other_end)])
await RoomSchedule.objects.filter(free_seats__contains=(5, 7))
Пути поля диапазона читают мультидиапазон целиком: startswith/endswith — нижняя граница первого
диапазона и верхняя граница последнего, isempty, lower_inc, lower_inf, upper_inc, upper_inf —
его признаки; span — наименьший диапазон, вмещающий все (Range, в SQL — range_merge()):
values_list("busy_days__span"), filter(busy_days__startswith__gte=day).
RangeAgg(field) (RANGE_AGG в PostgreSQL) сливает колонку диапазонов каждой группы в мультидиапазон,
который читается через поле мультидиапазона этого поля диапазона — см. Функции:
await Booking.objects.values("room").annotate(busy=RangeAgg("during"))
# [{"room": 1, "busy": [Range(date(2024, 1, 1), date(2024, 1, 6)), ...]}, ...]
Мультидиапазонам нужен PostgreSQL 14 — самая старая версия, которую поддерживает hare; на другой базе
поля дают UnSupportedError.
Проверки значений¶
hare.dialects.postgresql.validators, для validators=[...] поля:
| Проверка | Что проверяет |
|---|---|
KeysValidator(keys, strict=False, message=None) |
в словаре HStoreField есть каждый ключ из keys — а при strict=True нет никаких других |
RangeMinValueValidator(limit_value, message=None) |
нижняя граница диапазона не меньше limit_value (сторона без границы не проходит) |
RangeMaxValueValidator(limit_value, message=None) |
верхняя граница диапазона не больше limit_value (сторона без границы не проходит) |
Пустой диапазон проходит обе проверки диапазона. Число элементов ArrayField проверяют
MaxLengthValidator/MinLengthValidator, которые принимают списки.