Таблицы
Схема таблицы, чтение, фильтры и безопасное обновление строк.
Таблица хранит структурированные строки одного бота. Схема объявляется в коде, а данные доступны через типизированный клиент и HTTP API.
Используйте таблицы для заказов, каталогов и других наборов записей. Для контекста одного пользователя или диалога используйте состояние.
Объявите таблицу
import { Table, z } from '@holocronlab/botruntime-runtime'
export const OrdersTable = new Table({
name: 'OrdersTable',
description: 'Заказы и их текущий статус',
keyColumn: {
name: 'orderNumber',
unique: true,
},
columns: {
orderNumber: z.string().describe('Номер заказа'),
customerId: z.string().describe('ID покупателя'),
status: z.enum(['new', 'paid', 'shipped', 'delivered']),
total: z.string().describe('Сумма заказа, decimal-строка'),
expectedAt: z.string().optional().describe('Дата доставки YYYY-MM-DD'),
},
})Имя должно оканчиваться на Table. Схема синхронизируется при deploy.
Удаление колонки или смена типа считаются destructive change и требуют явного
подтверждения CLI.
Таблица может объявить до 64 пользовательских колонок. Системные поля id,
rowVersion, createdAt и updatedAt в этот лимит не входят.
unique: true создаёт реальный частичный UNIQUE index в PostgreSQL. Контракт
включается только для явно объявленной таблицы и не меняет старые таблицы.
Пока контракт действует, значение ключа — идентичность строки: для смены
удалите строку и создайте новую.
Найдите строки
const { rows } = await OrdersTable.findRows({
filter: { customerId, status: { $in: ['paid', 'shipped'] } },
orderBy: 'createdAt',
orderDirection: 'desc',
limit: 20,
})Основные операторы фильтра:
| Оператор | Пример |
|---|---|
$eq, $ne | { status: { $ne: 'delivered' } } |
$gt, $gte, $lt, $lte | { priority: { $gte: 3 } } |
$in, $nin | { status: { $in: ['paid', 'shipped'] } } |
$exists | { expectedAt: { $exists: true } } |
$regex | { orderNumber: { $regex: '^ORD-' } } |
$size | { items: { $size: 2 } } |
Объединяйте условия через $and, $or и $not. Неизвестный оператор
возвращает 400; платформа не игнорирует часть фильтра.
Системные поля используют закрытый registry:
| Поле | Тип фильтра | Физическая колонка |
|---|---|---|
id | положительное safe integer | id |
rowVersion | положительное safe integer | version |
createdAt | RFC 3339 | created_at |
updatedAt | RFC 3339 | updated_at |
Для них разрешены только $eq, $ne, $gt, $gte, $lt, $lte, $in и
$nin. Эти поля работают одинаково в find, count и delete-by-filter.
Сортировка принимает все четыре имени; для стабильной пагинации сервер
автоматически добавляет id в том же направлении. row_id оставлен только
как устаревший alias сортировки и не принимается в filter.
Создайте строки
await OrdersTable.createRows({
rows: [
{
orderNumber: 'ORD-1042',
customerId: 'customer-7',
status: 'paid',
total: '2490.00',
},
],
})Денежные значения храните decimal-строками. Календарные даты храните в формате
YYYY-MM-DD, если время и часовой пояс не нужны.
Обновите строку
const current = await OrdersTable.getRow({ id: 42 })
await OrdersTable.updateRows({
rows: [
{
id: current.id,
rowVersion: current.rowVersion,
status: 'shipped',
},
],
})rowVersion защищает от потерянного обновления. Если строку уже изменил другой
процесс, сервер вернёт конфликт. Перечитайте строку и примените решение к новой
версии.
undefined означает «не менять поле», а null очищает nullable-колонку.
Upsert
await OrdersTable.upsertRows({
keyColumn: 'orderNumber',
rows: [
{
orderNumber: 'ORD-1042',
customerId: 'customer-7',
status: 'shipped',
total: '2490.00',
},
],
})Upsert подходит для синхронизации по стабильному внешнему ключу. Укажите
keyColumn в декларации или в вызове.
Зарезервируйте уникальный ключ
const { row, created } = await OrdersTable.reserveKey({
idempotencyKey: `order-created:${eventId}`,
row: {
orderNumber: 'ORD-1042',
customerId: 'customer-7',
status: 'paid',
total: '2490.00',
},
})Конкурентные вызовы с одним ключом получают одну и ту же полную
строку-победителя. Ровно один новый запрос получает created: true; остальные
получают created: false без фиктивного UPDATE. Повтор с тем же
idempotencyKey возвращает точный исходный результат, включая created.
Измените несколько таблиц атомарно
import { tables } from '@holocronlab/botruntime-runtime'
const result = await tables.atomic({
idempotencyKey: `order-paid:${eventId}`,
operations: [
{
id: 'order',
op: 'reserveKey',
table: OrdersTable,
row: {
orderNumber: 'ORD-1042',
customerId: 'customer-7',
status: 'paid',
total: '2490.00',
},
},
{
op: 'createRows',
table: AuditTable,
rows: [{
orderId: tables.reference<number>('order', '/row/id'),
event: 'paid',
}],
},
] as const,
})Batch содержит от 1 до 50 операций и выполняется одним HTTP-запросом в одной
READ COMMITTED транзакции. Поддерживаются reserveKey, createRows,
updateRows, upsertRows и deleteRows. Ссылка может указывать только на
результат более ранней именованной операции и использует RFC 6901. Ошибка
любого элемента откатывает весь batch; metadata.operationIndex указывает
проблемную операцию. Тот же idempotencyKey безопасно повторяет точный batch.
Удалите строки
await OrdersTable.deleteRows({
filter: { status: 'delivered' },
})Сначала проверьте тот же фильтр через findRows. Для массового удаления
добавьте прикладной лимит или отдельное подтверждение в вашем коде.
HTTP API
Стабильные операции для таблиц генерируются из OpenAPI: Tables API.