## Выполнить SQL-запрос на чтение

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

Выполняет один SQL-запрос на чтение к SQLite-базе приложения и возвращает колонки и строки результата. Подходит для выборок с условиями, соединениями и агрегатами, которых нет у чтения строк одной таблицы.

Условия доступа к базе и лимиты запросов — [Когда база доступна](/docs/infra/database#когда-база-доступна) и [Лимиты](/docs/infra/database#лимиты). Изменить данные — [Выполнить SQL на запись](/docs/infra/database/execute).

## Параметры

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

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

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `sql` | string | да | Одна SQL-инструкция на чтение, до 16 384 байт в UTF-8. Значения подставляются через позиционные маркеры `?` |
| `parameters` | array | нет | Значения для маркеров `?` по порядку, до 256 штук. Каждое — строка до 16 384 символов, конечное число или `null`. Логические значения не принимаются, вместо них передавайте `1` и `0`. По умолчанию `[]` |
| `file` | string | нет | Путь к файлу базы относительно `/data`, до 1024 байт в UTF-8. Список файлов — `data.files[].file` в [`GET /db`](/docs/infra/database/info). По умолчанию — `data.defaultFile` оттуда же |

Другие поля тела дают `400 VALIDATION_ERROR`. Тело — до 64 КиБ.

**Пагинация.** Курсора и номера страницы у эндпоинта нет: при `data.truncated: true` строки сверх лимита отбрасываются. Остаток забирается следующим запросом: добавьте в `sql` устойчивую сортировку `ORDER BY` и `LIMIT` с `OFFSET` либо условие по ключу последней полученной строки, например `WHERE id > ?`.

## Примеры

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

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/APP_ID/db/query" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sql": "SELECT id, name, email FROM customers WHERE id > ? ORDER BY id",
    "parameters": [1]
  }'
```

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

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/APP_ID/db/query" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sql": "SELECT id, name, email FROM customers WHERE id > ? ORDER BY id",
    "parameters": [1]
  }'
```

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

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${appId}/db/query`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      sql: 'SELECT id, name, email FROM customers WHERE id > ? ORDER BY id',
      parameters: [1],
    }),
  }
)
const { success, data, error } = await res.json()
if (!success) throw new Error(`${error.code}: ${error.details?.reason ?? error.message}`)
// Строки — позиционные массивы в порядке data.columns
const rows = data.rows.map(r => Object.fromEntries(data.columns.map((c, i) => [c, r[i]])))
console.log(rows, data.truncated ? 'результат обрезан' : '')
```

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

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${appId}/db/query`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      sql: 'SELECT id, name, email FROM customers WHERE id > ? ORDER BY id',
      parameters: [1],
    }),
  }
)
const { success, data, error } = await res.json()
if (!success) throw new Error(error.code)
console.log(data.columns, data.rows)
```

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

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true` при успешном ответе |
| `data.file` | string | Путь прочитанного файла относительно `/data` |
| `data.columns` | array | Имена колонок результата по порядку. Одинаковые имена, например после соединения таблиц, остаются отдельными элементами |
| `data.rows` | array | Строки результата, до 500. Каждая строка — массив значений в порядке `data.columns` |
| `data.rows[][]` | string \| number \| null \| object | Значение ячейки. Текст — строкой, числа — числом, SQL `NULL` — `null`. Значения, которые нельзя передать числом или строкой JSON, приходят объектом с полем `type` — см. три строки ниже |
| `data.rows[][]` с `type: "blob"` | object | Двоичные данные, поле `base64` — содержимое в кодировке Base64 |
| `data.rows[][]` с `type: "integer"` | object | Целое число больше 9 007 199 254 740 991 по модулю, поле `value` — число строкой |
| `data.rows[][]` с `type: "float"` | object | Бесконечность или не-число, поле `value` — `Infinity`, `-Infinity` или `NaN` |
| `data.rowCount` | integer | Число строк в `data.rows` |
| `data.truncated` | boolean | `true`, если результат обрезан: строк больше 500 или ответ приблизился к 1 МиБ. Строки сверх лимита не возвращаются |
| `data.durationMs` | number | Время чтения на хосте в миллисекундах |

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

```json
{
  "success": true,
  "data": {
    "file": "app.db",
    "columns": ["id", "name", "email"],
    "rows": [
      [2, "Customer B", "b@example.com"],
      [3, "Customer C", null]
    ],
    "rowCount": 2,
    "truncated": false,
    "durationMs": 0.75
  }
}
```

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

422 — запрос пытается изменить данные:

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

Ответ на SQL-ошибку не содержит ни текста запроса, ни значений, ни исходного сообщения SQLite — причину называет только `error.details.reason`.

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Нет `sql`, передано неизвестное поле, `parameters` длиннее 256 элементов или содержит недопустимое значение, `sql` или `file` превышает допустимую длину |
| 400 | `APP_DB_INVALID_FILE` | `file` — абсолютный путь, содержит `.` или `..`, ведёт через символьную ссылку или указывает не на базу SQLite |
| 400 | `APP_DB_UNSUPPORTED_PLACEMENT` | Приложение размещено не в Галактике |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Ключ не опознан |
| 402 | `BILLING_FROZEN` | Хост галактики спит, а баланс заморожен — разбудить хост нельзя |
| 403 | `INFRA_SCOPE_REQUIRED` | У ключа нет скоупа `vibe:infra` |
| 403 | `APP_DB_DISABLED` | Работа с базой не включена для портала |
| 403 | `SERVER_WAKE_BLOCKED` | Хост галактики спит, и пробуждение для него запрещено |
| 404 | `NOT_FOUND` | Приложение не существует, удалено или недоступно этому ключу |
| 404 | `APP_DB_NOT_FOUND` | Нет тома `/data` или файла базы — приложение ещё не создало базу |
| 409 | `APP_DB_WAL_CLOSED` | База в режиме WAL, а файлов `-wal` и `-shm` нет: работающее приложение закрыло соединение. Заголовка `Retry-After` нет |
| 409 | `APP_DB_BUSY` | Хост занят другой операцией, не подключился за 10 секунд или база заблокирована записью приложения. Ответ несёт заголовок `Retry-After` |
| 413 | `PAYLOAD_TOO_LARGE` | Тело запроса больше 64 КиБ, или запрос вместе со служебными данными превышает 96 КиБ |
| 422 | `APP_DB_SQL_ERROR` | `error.details.reason` равен `not_permitted`: инструкция пытается изменить данные или схему, управлять транзакцией, подключить другую базу, выполнить `PRAGMA` или загрузить расширение, вызвать `readfile` или `writefile` |
| 422 | `APP_DB_SQL_ERROR` | `error.details.reason` равен `syntax_error`: ошибка синтаксиса SQL |
| 422 | `APP_DB_SQL_ERROR` | `error.details.reason` равен `no_such_table`: таблицы из запроса нет в базе |
| 422 | `APP_DB_SQL_ERROR` | `error.details.reason` равен `no_such_column`: колонки из запроса нет в таблице |
| 422 | `APP_DB_SQL_ERROR` | `error.details.reason` равен `type_mismatch`: значение несовместимо с типом |
| 422 | `APP_DB_SQL_ERROR` | `error.details.reason` равен `other`: прочие ошибки выполнения, в том числе несколько инструкций в одном `sql` |
| 422 | `APP_DB_QUERY_TIMEOUT` | Запрос не уложился в 5 секунд |
| 422 | `APP_DB_CORRUPT` | Проверка целостности базы не пройдена |
| 429 | `RATE_LIMITED` | Превышен лимит запросов на чтение для портала — [Лимиты](/docs/infra/database#лимиты). Пауза — в заголовке `Retry-After` |
| 503 | `APP_DB_TOOL_UNAVAILABLE` | Хост не подготовлен к чтению базы или временно недоступен. Ответ несёт заголовок `Retry-After`. Подготовка — [Подготовить хост](/docs/infra/database/prepare) |
| 503 | `user_self_deletion_pending` | Владелец ключа удаляет свой аккаунт на этом портале |

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

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

- **Изменить базу запрос не может.** Соединение открывается только на чтение, и кроме проверки инструкции на сервере SQLite сам отклоняет запись. Поэтому, в отличие от SQL на запись, `POST /db/query` не создаёт защитную копию перед выполнением.
- **SQL и данные не сохраняются в журнале аудита.** В журнал попадают операция, число строк и длительность. Текст `sql`, `parameters` и строки результата там не хранятся.

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

- [Получить схему базы](/docs/infra/database/schema)
- [Получить строки таблицы](/docs/infra/database/rows)
- [Выполнить SQL на запись](/docs/infra/database/execute)
- [База данных приложения](/docs/infra/database)
