## Получить схему базы

`GET /v1/infra/servers/:id/db/schema`

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

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

## Параметры

| Параметр | Тип | Обяз. | По умолч. | Описание |
|----------|-----|:-----:|-----------|----------|
| `id` (path) | string (UUID) | да | — | ID приложения с `kind` равным `GALAXY_APP`. Список: [`GET /v1/infra/servers`](/docs/infra/servers/list) |
| `file` (query) | string | нет | `data.defaultFile` из [`GET /db`](/docs/infra/database/info) | Путь к файлу базы относительно `/data`, до 1024 байт в UTF-8. Список файлов — `data.files[].file` в [`GET /db`](/docs/infra/database/info). Абсолютный путь, сегменты `.` и `..`, символьные ссылки не принимаются |

Другие параметры строки запроса дают `400 VALIDATION_ERROR`.

## Примеры

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

```bash
curl "https://vibecode.bitrix24.tech/v1/infra/servers/APP_ID/db/schema" \
  -H "X-Api-Key: YOUR_API_KEY"
```

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

```bash
curl "https://vibecode.bitrix24.tech/v1/infra/servers/APP_ID/db/schema" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

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

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${appId}/db/schema`,
  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
)
const { success, data, error } = await res.json()
if (!success) throw new Error(error.code)
for (const t of data.tables) {
  console.log(t.name, t.type, t.editability.editable ? 'правится' : t.editability.reason)
}
```

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

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${appId}/db/schema`,
  {
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
const { success, data, error } = await res.json()
if (!success) throw new Error(error.code)
console.log(data.tables.map(t => t.name))
```

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

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true` при успешном ответе |
| `data.file` | string | Путь прочитанного файла относительно `/data` |
| `data.tables` | array | Таблицы и представления, по алфавиту. Служебные таблицы SQLite с именем на `sqlite_` не входят |
| `data.tables[].name` | string | Имя таблицы или представления. Передаётся в `:table` при [чтении строк](/docs/infra/database/rows) |
| `data.tables[].type` | string | `table` — таблица, `view` — представление |
| `data.tables[].columns` | array | Колонки в порядке объявления, включая скрытые и вычисляемые |
| `data.tables[].columns[].name` | string | Имя колонки |
| `data.tables[].columns[].type` | string | Тип колонки так, как он объявлен в схеме. Пустая строка, если тип не объявлен |
| `data.tables[].columns[].notNull` | boolean | Объявлено ли ограничение `NOT NULL` |
| `data.tables[].columns[].defaultValue` | string \| null | Выражение значения по умолчанию текстом, например `CURRENT_TIMESTAMP` или `1`. `null`, если значения по умолчанию нет |
| `data.tables[].columns[].primaryKeyPosition` | integer | Номер колонки в первичном ключе, начиная с `1`. `0` — колонка не входит в ключ |
| `data.tables[].columns[].hidden` | integer | `0` — обычная колонка, `1` — скрытая колонка виртуальной таблицы, `2` — вычисляемая колонка `VIRTUAL`, `3` — вычисляемая колонка `STORED` |
| `data.tables[].indexes` | array | Индексы таблицы |
| `data.tables[].indexes[].name` | string | Имя индекса |
| `data.tables[].indexes[].unique` | boolean | Уникальный ли индекс |
| `data.tables[].indexes[].origin` | string | Откуда индекс: `c` — создан командой `CREATE INDEX`, `u` — ограничением `UNIQUE`, `pk` — первичным ключом |
| `data.tables[].indexes[].partial` | boolean | Частичный ли индекс, то есть с условием `WHERE` |
| `data.tables[].indexes[].columns` | array | Колонки индекса по порядку. `null` на месте выражения, если индекс построен по выражению |
| `data.tables[].primaryKey` | array | Колонки первичного ключа по порядку. Пустой массив, если ключ не объявлен |
| `data.tables[].editability.editable` | boolean | Можно ли добавлять, изменять и удалять строки через API |
| `data.tables[].editability.keyKind` | string \| null | Чем адресуется строка: `pk` — первичным ключом, `rowid` — встроенным номером строки SQLite. `null`, если правка недоступна |
| `data.tables[].editability.keyColumns` | array | Колонки, из которых строится ключ строки. Для `rowid` — одна колонка `rowid`, `_rowid_` или `oid`. Пустой массив, если правка недоступна |
| `data.tables[].editability.reason` | string \| null | Почему правка недоступна: `view` — представление, `virtual_table` — виртуальная таблица, `no_stable_key` — нет первичного ключа, а встроенный номер строки недоступен: таблица объявлена `WITHOUT ROWID` или имена `rowid`, `_rowid_` и `oid` заняты колонками. `null`, если правка доступна |
| `data.tables[].rowCountEstimate` | integer \| null | Оценка числа строк из статистики SQLite. `null`, если статистики нет |
| `data.truncated` | boolean | `true`, если в базе больше 500 таблиц и представлений или схема приблизилась к 1 МиБ. Тогда в ответе только часть таблиц |

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

```json
{
  "success": true,
  "data": {
    "file": "app.db",
    "tables": [
      {
        "name": "active_customers",
        "type": "view",
        "columns": [
          { "name": "id", "type": "INTEGER", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "name", "type": "TEXT", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 }
        ],
        "indexes": [],
        "primaryKey": [],
        "editability": {
          "editable": false,
          "keyKind": null,
          "keyColumns": [],
          "reason": "view"
        },
        "rowCountEstimate": null
      },
      {
        "name": "customers",
        "type": "table",
        "columns": [
          { "name": "id", "type": "INTEGER", "notNull": false, "defaultValue": null, "primaryKeyPosition": 1, "hidden": 0 },
          { "name": "name", "type": "TEXT", "notNull": true, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "email", "type": "TEXT", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "created_at", "type": "TEXT", "notNull": false, "defaultValue": "CURRENT_TIMESTAMP", "primaryKeyPosition": 0, "hidden": 0 }
        ],
        "indexes": [
          { "name": "idx_customers_email", "unique": false, "origin": "c", "partial": false, "columns": ["email"] }
        ],
        "primaryKey": ["id"],
        "editability": {
          "editable": true,
          "keyKind": "pk",
          "keyColumns": ["id"],
          "reason": null
        },
        "rowCountEstimate": null
      },
      {
        "name": "order_items",
        "type": "table",
        "columns": [
          { "name": "order_id", "type": "INTEGER", "notNull": true, "defaultValue": null, "primaryKeyPosition": 1, "hidden": 0 },
          { "name": "line", "type": "INTEGER", "notNull": true, "defaultValue": null, "primaryKeyPosition": 2, "hidden": 0 },
          { "name": "sku", "type": "TEXT", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "qty", "type": "INTEGER", "notNull": false, "defaultValue": "1", "primaryKeyPosition": 0, "hidden": 0 }
        ],
        "indexes": [
          { "name": "sqlite_autoindex_order_items_1", "unique": true, "origin": "pk", "partial": false, "columns": ["order_id", "line"] }
        ],
        "primaryKey": ["order_id", "line"],
        "editability": {
          "editable": true,
          "keyKind": "pk",
          "keyColumns": ["order_id", "line"],
          "reason": null
        },
        "rowCountEstimate": null
      }
    ],
    "truncated": false
  }
}
```

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

409 — работающее приложение закрыло соединение с базой в режиме WAL:

```json
{
  "success": false,
  "error": {
    "code": "APP_DB_WAL_CLOSED",
    "message": "A running application must keep its database connection open for reads and copies. A sleeping application can be read and copied with the connection closed.",
    "details": {
      "source": "sqlite",
      "hint": "A running application must keep its database connection open for reads and copies. A sleeping application can be read and copied with the connection closed."
    }
  }
}
```

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

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Передан неизвестный параметр или `file` пустой либо длиннее 1024 байт |
| 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` |
| 422 | `APP_DB_CORRUPT` | Проверка целостности базы не пройдена |
| 422 | `APP_DB_QUERY_TIMEOUT` | Чтение схемы не уложилось в 5 секунд |
| 422 | `APP_DB_SQL_ERROR` | В таблице больше 256 колонок или больше 256 индексов, `error.details.reason` равен `other` |
| 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).

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

- **Число строк не считается.** `rowCountEstimate` берётся из таблицы статистики `sqlite_stat1`, которую заполняет команда `ANALYZE` в самом приложении. Подсчёт строк по таблице вызов не выполняет, поэтому без статистики приходит `null`, а оценка отстаёт от данных на время с последнего `ANALYZE`.

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

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