Перейти к содержанию

Свои функции и выражения

Проект добавляет SQL-функцию, которой нет в hare, подклассом Function, а всё остальное, что может вычислять запрос, — подклассом Expression; затем оба работают в annotate(), filter(), order_by(), агрегатах и update(), как встроенные.

Своя функция

Каждый класс из раздела Функции базы данных — подкласс Function (или, для агрегата, Aggregate), оба из hare.query.expressions. Свою функцию пишите так же, если встроенных не хватает для нужной функции базы. Функции вида NAME(arguments...) достаточно имени SQL в function_name; аргументы — поле (имя, F() или другое выражение), а за ним значения, переданные после него:

from hare.query.expressions import F, Function

class JsonSet(Function):
    function_name = "JSON_SET"

Функция со своей формой SQL (EXTRACT(part FROM value), x::type) оставляет function_name пустым и задаёт в database_function подкласс hare.sql.terms.Function, который строит эту форму. Если какая-то база пишет функцию иначе, её диалект регистрирует отрисовщик для этого имени (см. Функции и операторы фильтра).

Работает и как значение атрибута, присвоенное перед .save(), и прямо в .update() — в обоих случаях это настоящее выражение на стороне SQL, а не значение, вычисленное в Python:

book.metadata = JsonSet(F("metadata"), "$.edition", 2)
await book.save()

await Book.objects.filter(pk=book.pk).update(metadata=JsonSet(F("metadata"), "$.edition", 3))

Подкласс Function наследует то, как функция описывает свой план запроса: её класс, поле и тип каждого литерального аргумента (сами значения подставляются в каждом запросе). Собственный атрибут, который записывается в текст SQL, а не подставляется как значение, объявляется в plan_parts после унаследованных частей — как PlanPartType.KEY: тогда два вызова с разными его значениями никогда не делят один план. Атрибут, не объявленный в plan_parts, не попадает в ключ: два вызова, которые различаются только им, получат один план, а с ним — один текст SQL.

from typing import ClassVar

from hare.query.expressions import Function
from hare.query.plans.description.declared_plan_parts import DeclaredPlanParts
from hare.query.plans.enums import PlanPartType
from hare.sql.terms import Function as SqlFunction, LiteralValue


class Prefix(Function):
    plan_parts: ClassVar[DeclaredPlanParts] = (*Function.plan_parts, ("length", PlanPartType.KEY))

    def __init__(self, field: str, length: int) -> None:
        super().__init__(field)
        self.length = int(length)

    def get_function_term(self, term, *default_values):
        return SqlFunction("SUBSTR", term, LiteralValue("1"), LiteralValue(str(self.length)))

get_function_term(term, *default_values) строит SQL-терм вызова из термов аргументов — переопределите его, как здесь, когда функция пишет свой SQL сама. Подкласс Function, который не задаёт ни function_name, ни database_function и не переопределяет get_function_term(), не называет никакой SQL-функции: запрос с ним даёт ConfigurationError до отправки SQL.

Своё выражение

Подкласс Expression, который не является Function (его собственный get_result() строит SQL-терм), тоже говорит, как запрос с ним хранит план: он объявляет в plan_parts, как каждый его атрибут входит в план, и описание генерируется из этого объявления; либо он объявляет plannable = False. Класс, который не делает ни того ни другого, отклоняется уже при определении (TypeError), поэтому новое выражение никогда случайно не воспользуется планом, построенным для другого.

Каждая часть — атрибут (или метод, для типов *_METHOD) и его PlanPartType, в порядке ключа:

Тип части Атрибут — это
KEY то, что пишется в текст SQL как есть: оператор, имя поля или вычисляемого значения, параметр
KEYS последовательность, которая пишется в текст SQL: имена полей, сортировки
KEY_METHOD метод, возвращающий то, что пишется в текст SQL
LITERAL литерал, подставляемый параметром (как его подставляет Value)
ARGUMENT / ARGUMENTS аргумент (их последовательность): выражение — своим описанием, None — как NULL, терм hare.sql — текстом и значениями, любой другой литерал — типом, а сам он подставляется; литерал-список или кортеж плана не хранит
ENCODED_ARGUMENT аргумент, литерал которого построение кодирует в одно подставляемое значение (точка — её текстом, вектор): список или кортеж подставляется целиком
ARGUMENT_METHOD метод, дающий аргумент, описанный как ARGUMENT
VALUE_METHOD метод, дающий значение, подставляемое параметром
PARAMETERS параметры ValueWrapper, которые объект сделал при создании (у RawSQL)
FIELD / FIELDS то, что читает функция (их последовательность): выражение, имя поля или вычисляемого значения, терм hare.sql
EXPRESSION / EXPRESSIONS выражение или условие (их последовательность) своим описанием, None — отсутствие
EXPRESSION_METHOD метод, дающий выражение или условие
JOIN_CONDITION_METHOD метод, дающий условие, которое построение вплетает в JOIN: его значения подставляются в каждый JOIN, куда оно вплетено, и никуда, если таких нет
FILTERS именованные фильтры условия
QUERY / QUERY_METHOD запрос, встроенный в выражение (метод, дающий его)
PLAN_KEY объект, который сам даёт свой ключ плана (get_plan_key())
KEEPS_PLAN_METHOD метод, говорящий, хранит ли объект план вообще: False — не хранит
NONE описан через другую часть (метод, который его читает) или вовсе не часть текста SQL: кэш, служебное

Каждый атрибут класса классифицирован: слот класса со __slots__, не названный ни одной частью, отклоняется при определении класса, а запуск pytest с --verify-plans проверяет атрибуты каждого объекта, который описывает. Базовый класс перечисляет служебные атрибуты всех своих объектов в unplanned_attributes (у Expression — запись аргументов его конструктора).

Построение разрешает каждый аргумент так, как его описывает часть, — ExpressionArguments.get_result(self, attribute, value, expression_context) для ARGUMENT (binds_whole=True для ENCODED_ARGUMENT, encoder= сначала переводит литерал в подставляемую форму), ExpressionArguments.get_field_result(...) для FIELD: выражение разрешается в свой результат, терм SQL записывает свои значения, а литерал подставляется параметром и, пока запрос записывает свой план, записывается под атрибутом, где лежит. План подставляет значение во все ссылки, записанные под ним, сколько бы раз построение его ни разрешало.

from typing import ClassVar

from hare.query.expressions import Expression, ExpressionContext, ExpressionResult
from hare.query.expressions.value_references.expression_arguments import ExpressionArguments
from hare.query.plans.description.declared_plan_parts import DeclaredPlanParts
from hare.query.plans.enums import PlanPartType


class Clamp(Expression):
    plan_parts: ClassVar[DeclaredPlanParts] = (
        ("field", PlanPartType.FIELD),
        ("low", PlanPartType.ARGUMENT),
        ("high", PlanPartType.ARGUMENT),
    )

    def __init__(self, field: str, low: int, high: int) -> None:
        self.field, self.low, self.high = field, low, high

    def get_result(self, expression_context: ExpressionContext) -> ExpressionResult:
        field = ExpressionArguments.get_field_result(self, "field", self.field, expression_context)
        low = ExpressionArguments.get_result(self, "low", self.low, expression_context)
        high = ExpressionArguments.get_result(self, "high", self.high, expression_context)
        ...  # LEAST(GREATEST(field.term, low.term), high.term)


class Unplanned(Expression):
    plannable = False  # запрос с ним каждый раз строится полностью

    def get_result(self, expression_context: ExpressionContext) -> ExpressionResult: ...

Класс, описание которого не покрывает ни один тип части, пишет get_plan_description(context) сам и возвращает PlanDescription(structure, values, origins), None — когда экземпляр не хранит план; пока запрос записывает свой план (PlanOrigins.records), origins перечисляет, откуда взято каждое значение, — PlanOrigins.get_value_origin(self, attribute), — а иначе это None.

Выражение, текст SQL которого вообще не берёт значений (PI(), текущий момент), наследует hare.query.expressions.ConstantExpression — он описывает его одним только классом.