## Выполнить SQL на запись

`POST /v1/infra/servers/:id/db/execute`

Выполняет SQL-выражения на запись в SQLite-базе приложения одной транзакцией: изменяет строки, создаёт и меняет таблицы, индексы и представления.

Перед каждым вызовом платформа сохраняет отдельную защитную копию базы — см. [Защитные копии перед записью](/docs/infra/database#защитные-копии-перед-записью). Условия доступа — [Когда база доступна](/docs/infra/database#когда-база-доступна).

## Параметры

| Параметр | Тип | Обяз. | Описание |
|----------|-----|:-----:|----------|
| `id` (path) | string (UUID) | да | ID приложения с `kind` равным `GALAXY_APP`. Список: [`GET /v1/infra/servers`](/docs/infra/servers/list) |

## Поля запроса (body)

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `file` | string | нет | Путь к файлу базы относительно тома `/data`, до 1024 байт UTF-8. Абсолютный путь, сегменты `.` и `..` не принимаются. По умолчанию — значение `VIBE_DB_PATH` контейнера приложения, а без него `app.db`. Список файлов: [`GET /db`](/docs/infra/database/info) |
| `sql` | string | да | От 1 до 16384 байт UTF-8. До 64 выражений, разделённых `;`. Выражения выполняются по порядку |
| `parameters` | array | нет | Значения для заполнителей `?`, до 256 штук. Элемент — `null`, строка до 16384 байт UTF-8 или конечное число. Целое число — не больше 2⁵³−1 по модулю, точное 64-битное целое передайте литералом в `sql`. Логические значения не принимаются — передайте `1` или `0`. Допустимы, только когда в `sql` одно выражение. По умолчанию `[]` |

Неизвестное поле тела отклоняется с `400 VALIDATION_ERROR`.

В `sql` отклоняются с `422 APP_DB_SQL_ERROR` и причиной `not_permitted`: управление транзакциями и точками сохранения (`BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`), `ATTACH` и `DETACH`, `PRAGMA`, создание и удаление виртуальных таблиц, обращение к схемам кроме `main`, включая временные таблицы, функции `load_extension`, `readfile`, `writefile` и `fts3_tokenizer`. `VACUUM` тоже не выполняется: вызов идёт внутри транзакции, и выражение отклоняется с `422 APP_DB_SQL_ERROR`.

## Примеры

### curl — личный ключ

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/APP_ID/db/execute" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sql":"UPDATE notes SET body = ? WHERE id = ?","parameters":["updated",1]}'
```

### curl — OAuth-приложение

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/APP_ID/db/execute" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql":"UPDATE notes SET body = ? WHERE id = ?","parameters":["updated",1]}'
```

### JavaScript — личный ключ

```javascript
const url = `https://vibecode.bitrix24.tech/v1/infra/servers/${appId}/db/execute`
const headers = { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }
const body = JSON.stringify({ sql: 'UPDATE notes SET body = ? WHERE id = ?', parameters: ['updated', 1] })

// Повторяется только 409 APP_DB_BACKUP_NOT_READY: в этом ответе SQL ещё не выполнялся
const deadline = Date.now() + 180_000
let result
while (Date.now() < deadline) {
  const res = await fetch(url, { method: 'POST', headers, body })
  result = await res.json()
  if (res.status !== 409 || result.error?.code !== 'APP_DB_BACKUP_NOT_READY') break
  const seconds = Number(res.headers.get('Retry-After')) || 1
  await new Promise(resolve => setTimeout(resolve, Math.max(seconds, 3) * 1000))
}
if (!result.success) throw new Error(result.error.code)
console.log(result.data.rowCount, result.data.backupId)
```

### JavaScript — OAuth-приложение

```javascript
const url = `https://vibecode.bitrix24.tech/v1/infra/servers/${appId}/db/execute`
const headers = {
  'X-Api-Key': 'YOUR_APP_KEY',
  'Authorization': 'Bearer USER_SESSION_TOKEN',
  'Content-Type': 'application/json',
}
const body = JSON.stringify({ sql: 'UPDATE notes SET body = ? WHERE id = ?', parameters: ['updated', 1] })

const deadline = Date.now() + 180_000
let result
while (Date.now() < deadline) {
  const res = await fetch(url, { method: 'POST', headers, body })
  result = await res.json()
  if (res.status !== 409 || result.error?.code !== 'APP_DB_BACKUP_NOT_READY') break
  const seconds = Number(res.headers.get('Retry-After')) || 1
  await new Promise(resolve => setTimeout(resolve, Math.max(seconds, 3) * 1000))
}
if (!result.success) throw new Error(result.error.code)
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true`, если транзакция зафиксирована |
| `data.file` | string | Файл базы относительно тома `/data`, к которому применён SQL |
| `data.rowCount` | integer | Число строк, изменённых выражениями `INSERT`, `UPDATE` и `DELETE` напрямую. Изменения, сделанные триггерами, не учитываются. Выражение, которое меняет схему базы, например `CREATE TABLE … AS SELECT`, в счёт не входит |
| `data.statementCount` | integer | Число выполненных выражений, от 1 до 64 |
| `data.durationMs` | number | Время выполнения SQL на хосте в миллисекундах, с дробной частью |
| `data.backupId` | string | ID защитной копии, сохранённой перед этим вызовом. Список копий: [`GET /db/backups`](/docs/infra/database/backups) |

## Пример ответа

```json
{
  "success": true,
  "data": {
    "file": "app.db",
    "rowCount": 1,
    "durationMs": 0.933,
    "statementCount": 1,
    "backupId": "d5e1b7a9-0f42-4c8d-9a3e-6b21c4f80e57"
  }
}
```

## Пример ответа при ошибке

422 — выражение `PRAGMA journal_mode=DELETE` запрещено, транзакция откачена:

```json
{
  "success": false,
  "error": {
    "code": "APP_DB_SQL_ERROR",
    "message": "SQL operation is not permitted",
    "details": {
      "reason": "not_permitted"
    }
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Тело не прошло проверку: неизвестное поле, пустой или длиннее 16384 байт `sql`, некорректный `file`, больше 256 элементов или логическое значение в `parameters` |
| 400 | `APP_DB_INVALID_FILE` | Файл не является базой SQLite, путь к нему проходит через символическую ссылку, или значение `VIBE_DB_PATH` контейнера не указывает внутрь `/data` |
| 400 | `APP_DB_UNSUPPORTED_PLACEMENT` | Приложение размещено не в Галактике |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Ключ не опознан |
| 402 | `BILLING_FROZEN` | Счёт портала заморожен |
| 402 | `BILLING_INSUFFICIENT` | Баланса портала не хватает на хранение защитной копии |
| 403 | `INFRA_SCOPE_REQUIRED` | У ключа нет скоупа `vibe:infra` |
| 403 | `APP_DB_DISABLED` | Работа с базой приложения не включена для портала |
| 403 | `APP_DB_READONLY_KEY` | Ключ в режиме только чтения |
| 404 | `NOT_FOUND` | Приложение не найдено, удалено или недоступно этому ключу |
| 404 | `APP_DB_NOT_FOUND` | У приложения нет тома `/data` или файла базы: приложение её ещё не создало |
| 409 | `APP_DB_BACKUP_NOT_READY` | Защитная копия ещё готовится, SQL не выполнялся. Повторите запрос после паузы из заголовка `Retry-After` |
| 409 | `APP_DB_BUSY` | Идёт деплой, перенос или восстановление приложения, хост занят другой операцией, база занята записью самого приложения, или пробуждение приложения либо хоста галактики запрещено. SQL не выполнен. Повторите после паузы из `Retry-After` |
| 409 | `APP_DB_WAL_CLOSED` | Защитная копия не создана: работающее приложение закрыло соединение с базой в режиме журнала упреждающей записи (WAL). SQL не отправлялся, заголовка `Retry-After` нет. Настройте приложение так, чтобы соединение оставалось открытым |
| 413 | `PAYLOAD_TOO_LARGE` | Тело больше 64 КиБ, или полный вход служебного процесса на хосте больше 96 КиБ |
| 422 | `APP_DB_SQL_ERROR` | SQL не выполнен, транзакция откачена. Причина в `details.reason`: `syntax_error`, `not_permitted`, `no_such_table`, `no_such_column`, `type_mismatch`, `constraint_failed` или `other`. Причина `other` приходит и на параметры при нескольких выражениях, и на больше 64 выражений |
| 422 | `APP_DB_QUERY_TIMEOUT` | Выполнение заняло больше 30 секунд, транзакция откачена |
| 422 | `APP_DB_CORRUPT` | Проверка целостности базы перед записью не прошла, либо защитная копия не создана из-за повреждения базы |
| 422 | `APP_DB_BACKUP_TIMEOUT` | Защитная копия не создана за отведённое время, SQL не отправлялся |
| 422 | `APP_DB_BACKUP_INVALID` | Защитная копия не прошла проверку, SQL не отправлялся |
| 429 | `RATE_LIMITED` | Превышен лимит запросов на запись для портала — [Лимиты](/docs/infra/database#лимиты). Пауза — в заголовке `Retry-After` |
| 503 | `APP_DB_PRE_CHANGE_FAILED` | Защитная копия не создана по другой причине, SQL не отправлялся |
| 503 | `APP_DB_WRITE_OUTCOME_UNKNOWN` | SQL отправлен, но результат не получен, `details.retryable` равен `false`. Изменение могло сохраниться. Ключа идемпотентности нет, поэтому не повторяйте запрос автоматически: сначала прочитайте данные через [`POST /db/query`](/docs/infra/database/query) |
| 503 | `APP_DB_TOOL_UNAVAILABLE` | Хост не подготовлен или служебный канал к нему недоступен. Готовность хоста — [`GET /app-db/prepare`](/docs/infra/database/prepare-status) |
| 503 | `user_self_deletion_pending` | Владелец ключа удаляет свой аккаунт на портале |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

- **Первый вызов только готовит защитную копию.** Пока свежей копии для этого файла нет, вызов ставит её создание в очередь и отвечает `409 APP_DB_BACKUP_NOT_READY` с `details.backupId`, `details.retryAfterSeconds` и заголовком `Retry-After`. Повторяйте тот же запрос: когда копия готова, повтор выполняет SQL и возвращает в `data.backupId` тот же ID. Готовая копия ждёт повтора 10 минут, позже следующий вызов готовит новую. Устройство защитных копий — [Защитные копии перед записью](/docs/infra/database#защитные-копии-перед-записью).
- **Повторы расходуют лимит записи.** Каждый повтор после `409` учитывается в лимите запросов на запись ([Лимиты](/docs/infra/database#лимиты)), поэтому держите паузу не короче трёх секунд, даже когда `Retry-After` равен `1`.
- **Отказ копии приходит один раз.** Если защитная копия не создалась, ближайший вызов получает причину с `details.retryable: false`, а следующий вызов уже ставит в очередь новую копию.
- **Весь вызов — одна транзакция.** Ошибка любого выражения или превышение 30 секунд откатывает все выражения вызова. Строки, которые возвращают `SELECT` и `RETURNING`, читаются до конца и отбрасываются: в ответ они не попадают. SQL и значения параметров в журнал аудита не записываются.
- **Внешние ключи проверяются всегда.** На время вызова платформа включает проверку внешних ключей, даже если само приложение её не включает. Нарушение ссылочной целостности откатывает вызов с причиной `constraint_failed`.
- **Контейнер приложения не запускается.** SQL выполняет служебный контейнер на хосте с тем же томом `/data`.
- **Инструмент для AI-агентов повторяет только неготовую копию.** Инструмент `manage_app_db` из MCP-сервера Вайбкод с `action: "execute"` принимает `serverId`, `file`, `sql`, `parameters` и сам повторяет только `409 APP_DB_BACKUP_NOT_READY`. Общий срок ожидания — 180 секунд, пауза — по `Retry-After`, но не короче трёх секунд. Сетевую ошибку и `APP_DB_WRITE_OUTCOME_UNKNOWN` инструмент не повторяет. Отмена ожидания не доказывает, что уже отправленная запись откатилась.

## Смотрите также

- [Выполнить SQL-запрос на чтение](/docs/infra/database/query)
- [Получить список копий базы](/docs/infra/database/backups)
- [Восстановить базу из копии](/docs/infra/database/restore)
- [Получить статус подготовки хоста](/docs/infra/database/prepare-status)
- [База данных приложения](/docs/infra/database)
