Предикаты
Какие сравнения даёт каждый тип колонки и почему наборы различаются.
Набор зависит от типа#
Предикат есть у дескриптора тогда, когда PostgreSQL определяет операцию для этого типа. Поэтому списки различаются, и поэтому разница — ошибка компиляции, а не рантайма.
Любая колонка#
Users.Email.Eq("a@example.com")
Users.Email.Ne("a@example.com")
Users.Email.In("a@example.com", "b@example.com")
Users.ID.In(ids...) // для готового срезаNotIn намеренно нет. orm.Not(Users.ID.In(...)) говорит то же самое и говорит один раз.
Упорядоченные колонки#
Любой тип, который PostgreSQL упорядочивает, — целые, дробные, текст, даты, uuid, inet, interval:
Users.CreatedAt.Gt(t)
Users.CreatedAt.Gte(t)
Users.CreatedAt.Lt(t)
Users.CreatedAt.Lte(t)
Users.CreatedAt.Between(from, to)
Users.CreatedAt.Asc()
Users.CreatedAt.Desc()У jsonb и bytea есть полный порядок для индексов, но сравнение двух таких значений не отвечает ни на чей вопрос, поэтому они остаются на равенстве.
Текстовые колонки#
Users.Email.Like("%@example.com")
Users.Email.ILike("%@EXAMPLE.com")
orm.Not(Users.Email.Like("%@spam.test"))
Users.Email.Like("admin%")
Users.Email.Like("%.org")
Users.Email.Like("%example%")Nullable-колонки#
Только у них есть эти методы, потому что на NOT NULL колонке они отвечали бы на невозможный вопрос:
Users.Bio.IsNull()
Users.Bio.IsNotNull()
Users.Bio.Eq("hello") // тоже доступно: это bio = 'hello'Массивы#
Вхождение в массив — это свободные функции, а не методы, и они дают
Predicate[Composed]. Не-nullable колонка поднимается через orm.Opt:
orm.ArrayContains(orm.Opt(Users.Tags), orm.Val([]string{"go"})) // @>
orm.ArrayContainedBy(orm.Opt(Users.Tags), orm.Val(all)) // <@
orm.ArrayOverlaps(orm.Opt(Users.Tags), orm.Val([]string{"a", "b"})) // &&JSONB#
Тоже свободные функции и по той же причине — с любой стороны может быть колонка или выражение. Весь набор — в разделе JSON и JSONB:
orm.JSONHasKey(orm.Opt(Users.Meta), "plan")
orm.JSONPathExists(orm.Opt(Users.Meta), "$.billing.tier")
orm.JSONPathText(orm.Opt(Users.Meta), "billing", "tier")Диапазоны#
Bookings.During.Overlaps(r)
Bookings.During.Contains(t)
Bookings.During.StrictlyLeftOf(other)
Bookings.During.Adjacent(other)Полнотекстовый поиск#
orm.Matches(Docs.Search, orm.PlainToTSQuery(orm.English, "postgres mapper"))
orm.TSRank(Docs.Search, query).Desc()Комбинирование#
orm.And(a, b, c)
orm.Or(a, b)
orm.Not(a)Они вкладываются, и компилятор удерживает их на одной сущности: orm.And, смешивающий Predicate[User] и Predicate[Order], не компилируется. Это не педантизм — такой предикат дал бы SQL, называющий таблицу, которой в запросе нет.
Сравнение двух колонок#
Сравнения «колонка с колонкой» — это свободные функции, а не методы, и они дают
Predicate[Composed], поэтому их место в составном запросе, а не в Where по
сущности:
orm.Compose(pool, shape).
From(Orders.Source()).
Where(orm.Gt(Orders.Total, Orders.Paid))Eq, Ne, Gt, Gte, Lt и Lte принимают два типизированных значения, и
обе стороны обязаны нести один тип значения. Справа может стоять выражение:
orm.Eq(Orders.Total, Orders.Net.AddCol(Orders.Tax))Арифметика на колонке — это метод: Add, Sub, Mul, Div со значением и
AddCol или SubCol с другой колонкой той же сущности.
Сырые фрагменты#
Когда типизированной формы нет:
db.Users.Query().Where(orm.Expr[User]("age(created_at) > interval ?", "1 year"))Expr принимает текст SQL намеренно. Значения, вставленные в него, — нет: каждый ? становится параметром привязки, а плейсхолдеры фрагмента проверяются против переданных аргументов.
Разобранные примеры#
Доска вакансий#
Три фильтра, читающиеся как три предложения, и один, которого в SQL нет, пока вы его не напишете.
// Удалённые вакансии этого месяца с зарплатой не ниже порога.
db.Postings.Query().Where(
Postings.Remote.Eq(true),
Postings.PostedAt.Gte(monthStart),
Postings.SalaryMin.Gte(60000),
)
// Всё, кроме заблокированных агентств.
db.Postings.Query().Where(orm.Not(Postings.CompanyID.In(blocked...)))
// Заголовок или описание — один OR, написанный один раз.
db.Postings.Query().Where(orm.Or(
Postings.Title.ILike("%golang%"),
Postings.Description.ILike("%golang%"),
))Очередь модерации#
Случаи с NULL — там живёт большинство ошибок в фильтрах:
// Ни разу не просмотрено: reviewed_at никогда не проставляли.
db.Comments.Query().Where(Comments.ReviewedAt.IsNull())
// Просмотрено и одобрено: проставлено, причина не записана.
db.Comments.Query().Where(
Comments.ReviewedAt.IsNotNull(),
Comments.RejectReason.IsNull(),
)
// Просмотрено и отклонено с причиной, которая не пустая строка.
db.Comments.Query().Where(
Comments.RejectReason.IsNotNull(),
orm.Not(Comments.RejectReason.Eq("")),
)У NOT NULL колонки нет IsNull, поэтому первые два по ошибке к ней не
применить.
Прайс-лист#
Сравнение двух колонок — это составной запрос, а не запрос по сущности:
// Всё, что сейчас продаётся ниже себестоимости.
orm.Compose(pool, shape).
From(Prices.Source()).
Where(orm.Lt(Prices.Retail, Prices.Cost)).
All(ctx)
// Маржа ниже порога, вычисленная, а не хранимая.
orm.Compose(pool, shape).
From(Prices.Source()).
Where(orm.Lt(Prices.Retail.SubCol(Prices.Cost), orm.Val(int32(500)))).
All(ctx)