botruntime
Данные

Таблицы

Схема таблицы, чтение, фильтры и безопасное обновление строк.

Таблица хранит структурированные строки одного бота. Схема объявляется в коде, а данные доступны через типизированный клиент и HTTP API.

Используйте таблицы для заказов, каталогов и других наборов записей. Для контекста одного пользователя или диалога используйте состояние.

Объявите таблицу

src/tables/orders.ts
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 integerid
rowVersionположительное safe integerversion
createdAtRFC 3339created_at
updatedAtRFC 3339updated_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.

Дальше

On this page