# Живой слой баз данных: структура, связи, примеры, актуализация

> Что это: RAGRAF умеет индексировать **реальные базы** — имена, структуру, типы,
> связи, примеры строк — и держать этот слой свежим так же, как слой кода.
> Документ описывает настройку, что собирается, как читать ответы и где границы.
> Все числа измерены на машине владельца (MySQL 9.7.2 и PostgreSQL 18.6).

Автор: Михаил Дейнекин
Сайт: <https://Deynekin.com>
Почта: Mikhail@Deynekin.com

---

## 1. Зачем это, если схема уже есть из миграций

В проекте уже был слой схемы из **миграций**: `src/ragraf/schema_db.py` разбирает
SQL-файлы и пишет таблицы в граф (`db_table`, `db_column`, `db_fk`). Это картина
«как задумано».

Живой слой отвечает на другой вопрос — «как есть»:

| Вопрос | Миграции | Живая база |
|---|---|---|
| Какая структура задумана | да | — |
| Что реально лежит в этой базе сейчас | — | да |
| Сколько строк и какого размера | — | да |
| Какие есть индексы, ключи, представления | частично | да |
| Как выглядят данные (пример строки) | — | да |
| Домены значений (статусы, типы) | — | да |

**Расхождение между ними — отдельная находка.** Оно показывает, что миграции и
база разошлись: колонка есть в файле, но не применена, или наоборот.

---

## 2. Что собирается

### 2.1 Структура (факт из системных каталогов)

* **Базы**: имя, кодировка, сопоставление.
* **Таблицы**: вид (таблица или представление), движок, оценка числа строк, размер
  данных и индексов, комментарий.
* **Колонки**: тип с длиной и точностью, допустимость `NULL`, значение по
  умолчанию, признак ключа, комментарий, порядок.
* **Индексы** и уникальные ограничения — отдельно от первичного ключа.
* **Представления** и таблицы, от которых они зависят.
* **Внешние ключи** с правилами (`ON DELETE`/`ON UPDATE`).

### 2.2 Связи: факт и вывод — РАЗНЫЕ вещи

| Вид | Откуда | Достоверность | Как читать |
|---|---|---|---|
| `stated` | объявлено в базе внешним ключом | 1.0 | **факт** |
| `derived` | вывод по имени колонки (`user_id` → `users`) | 0.4 | **предположение** |

Зачем вывод вообще нужен: в живых базах внешние ключи часто не объявлены. Измерено
на `z_bsdocs_test`: **0 связей из внешних ключей на 86 таблиц**, а по именам
колонок нашлось **37**. Без вывода агент не видел связей вовсе.

Почему вывод нельзя выдавать за факт: связь, которой нет, ломает вывод о влиянии
правки. В проекте уже был ложный `full`, из-за которого клиент получал ошибку
вместо честного «недоступно». Поэтому вид связи всегда в ответе.

Разбор имён учитывает три формы написания (измерено, что первая редакция ловила
только `_id` и нашла **одну** колонку на 892 вместо 173):

| Имя колонки | Разбор | Найдёт |
|---|---|---|
| `user_id` | змеиный регистр | `users` |
| `UserID` | сокращение | `users` |
| `AuthorUserID` | составное | `users` (по последнему слову) |

Неоднозначность **не разрешается**: если кандидатов несколько, связь не создаётся.

### 2.3 Персональные данные: значения не собираются вовсе

Колонка признаётся персональными данными или секретом **до** сбора значений: она
исключается из списка полей запроса, а не фильтруется после выборки.

Признак определяется по имени, и имя нормализуется: `FirstName`, `first_name` и
`First-Name` — одно и то же поле. Измерено на живой базе: без нормализации
`FirstName` и `E-Mail` **не** распознавались, и имя человека попадало бы в индекс.

Что закрывается: имя, фамилия, отчество, почта, телефон, дата рождения, адрес,
логин, пароль, токен, ключ, паспорт, СНИЛС, ИНН, номер карты, контакты мессенджеров.

Что **не** закрывается (иначе правило стало бы бесполезным): `UserID`, `CompanyID`,
`CompanyName`, `Created`, `Amount`, `Status` — это идентификаторы, названия
организаций и служебные поля.

В примере на месте закрытой колонки стоит пометка **«скрыто: персональные данные»**,
а не пустота: пустое значение агент прочитает как «поля нет», и это неотличимо от
данных. Измерено на `bsdocs.users`: **16 колонок закрыто**, утечек нет.

### 2.4 Примеры строк

* берутся **только непустые** значения;
* если разные поля заполнены в разных строках, собирается **сборный пример**: по
  каждой колонке берётся первое непустое значение из просмотренных строк;
* из каждой таблицы читается ограниченное число строк (`LIMIT`), запрос с
  таймаутом, только `SELECT`;
* длина значения обрезается, общий размер примера ограничен;
* в ответе видно, **сколько строк просмотрено**;
* каждый ответ с примером несёт пометку: **снимок на момент индексации**.

### 2.5 Домены значений

Для колонок с малым числом различных значений (статусы, типы, флаги) собирается
словарь значений с частотами — до 20 значений, только для колонок с признаком «мало
различных». Это **не содержимое строк**, а словарь модели данных: безопаснее
примеров и полезнее для понимания схемы.

---

## 3. Настройка

Файл: `catalog/db_access.json` — **вне корпуса**, поэтому значение доступа не
попадает ни в индекс, ни в поисковую выдачу.

| Настройка | Смысл |
|---|---|
| `enabled` | включён ли сбор (по умолчанию **выключен**) |
| `dialect` | `mysql` или `postgresql` |
| `host`, `port` | адрес (по умолчанию `127.0.0.1:3306` / `:5432`) |
| `user`, `password` | доступ |
| `databases_include` / `databases_exclude` | какие базы брать |
| `samples_enabled` | собирать примеры (по умолчанию **выключено**) |
| `samples_max_rows`, `samples_max_bytes` | пределы примера |
| `max_tables`, `max_columns`, `timeout_seconds` | пределы обхода |

### 3.1 Переменные окружения важнее файла

| Переменная | Значение |
|---|---|
| `RAGRAF_DB_PASSWORD` | значение доступа |
| `RAGRAF_DB_HOST`, `RAGRAF_DB_PORT` | адрес |
| `RAGRAF_DB_USER` | пользователь |
| `RAGRAF_DB_DIALECT` | `mysql` / `postgresql` |

Значение доступа можно не хранить на диске вовсе. Проверено: при заданной
переменной источник доступа становится `environment`, и файл не используется.

### 3.2 Панель

`python tools/ragraf-admin.py` → раздел **«Базы данных»**:

| Действие | Что делает |
|---|---|
| Сохранить настройки | пишет `catalog/db_access.json` |
| **Проверить подключение** | реально открывает соединение и получает ответ сервера |
| **Проиндексировать структуру** | собирает и записывает в индекс |

Значение доступа **никогда не возвращается** панелью: показывается только признак
«задано» и источник. Проба не сохраняет настройки — это разные действия.

### 3.3 Командная строка

```powershell
# проверка подключения, ничего не собирается
python tools/ragraf.py -p config/ragraf.yaml db --probe-only

# сбор структуры и запись в индекс
python tools/ragraf.py -p config/ragraf.yaml db

# вместе с примерами строк, только PostgreSQL, выключить сбор после
python tools/ragraf.py -p config/ragraf.yaml db --dialect postgresql --samples
```

---

## 4. Актуализация: как у кода

### 4.1 Отпечаток структуры

Считается по структуре в детерминированном порядке: базы, таблицы, колонки с
типами, ключи, индексы, представления, связи. Записывается в `meta`
control-хранилища рядом с ревизией.

**Данные в отпечаток НЕ входят.** Изменилась строка в таблице — это не повод
пересобирать корпус: структура не изменилась, а примеры имеют собственную пометку
устаревания. Проверено: изменение оценки числа строк отпечаток не меняет.

Детерминированность — обязательное условие, а не мелочь: если отпечаток зависит от
порядка обхода каталогов, сторож свежести начнёт переиндексировать корпус
бесконечно — ровно так, как это описано для суммы корпуса. Проверено: две модели с
разным порядком добавления дают один отпечаток.

### 4.2 Сторож свежести

Проверка структуры подключается к сторожу **только когда сбор включён** и **только
после того, как сумма корпуса совпала** (если код уже разошёлся, лишняя проверка не
нужна). Три состояния различаются намеренно:

| Состояние | Вердикт | Что происходит |
|---|---|---|
| структура та же | `checked: true, changed: false` | ничего |
| структура изменилась | `checked: true, changed: true` | корпус признаётся устаревшим |
| сбор выключен | `checked: false` | ничего: сравнивать нечего |
| база недоступна | `error: true, checked: false` | **ничего**: код от недоступности СУБД не устарел |

Последняя строка — предохранитель от ложной реакции. Проверено: при неверном
значении доступа возвращается `error: true` и `changed: false`.

### 4.3 Конвейер индексации

При индексации корпуса слой баз собирается **до** стадии векторов: примеры попадают
в те же чанки, и вычислитель получает их одним проходом. Если сбор выключен, стадия
пропускается — и это видно в отчёте прогона отдельной стадией `databases` с причиной.

---

## 5. Операции

Шесть операций фасада и шесть инструментов MCP.

| Операция | Вопрос, на который отвечает |
|---|---|
| `db_schema` | что в базе: базы, таблицы, колонки, типы, индексы |
| `db_table` | одна таблица подробно: колонки, индексы, ключи, связи, скрытые поля |
| `db_relations` | связи таблицы: `stated` и `derived` **раздельно** |
| `db_sample` | собранный пример с происхождением и пометкой сокрытия |
| `db_drift` | что изменилось в структуре между ревизиями |
| `db_cross` | сверка «код ↔ база»: чего не хватает с обеих сторон |

У каждой операции есть инструмент MCP с тем же именем и префиксом: например
`ragraf_db_relations`.

### 5.1 Сверка «код ↔ база» — что она находит

* **`used_missing`** — имя таблицы встречается в коде, но таблицы нет в базе:
  устаревший код или неверное окружение;
* **`unused`** — таблица есть в базе, но код её не касается: кандидат на удаление.

Это статическая сверка по именам: она показывает, **куда смотреть**, а не что
удалять.

**Проверка осмысленности.** Если пересечение «имена таблиц в базе» и «имена в коде»
пусто, значит сравниваются код одного проекта и база другого — и тогда оба списка
ничего не доказывают. В этом случае в ответе стоит `plausible: false` и
предупреждение: измерено на корпусе RAGRAF, куда подключена база чужого проекта
(`z.batteryservice.ru`) — без предупреждения 83 имени в `unused` читались бы как
«код не использует 83 таблицы», то есть как ложная находка.

---

## 6. Границы и чего система не делает

| Границы | Почему |
|---|---|
| **Только чтение.** Записи, DDL и управления правами нет вовсе | индексация не должна уметь менять чужую базу |
| Драйверы СУБД не используются | проект держит нулевые зависимости; источник — штатный клиент (`mysql.exe`, `psql.exe`) |
| Сопоставление имён — соглашение, не факт | вывод по имени помечен `derived` и достоверностью 0.4 |
| Примеры — снимок, а не текущее состояние | строки меняются; в каждом ответе пометка |
| Схема из миграций и живая база — **разные источники** | расхождение между ними показывается, а не сглаживается |
| Классификация персональных данных — по имени | тип для этого бесполезен: телефон и номер заказа выглядят одинаково |
| Примеры по представлениям не собираются | у представления нет своих данных, а запрос может быть тяжёлым |

### 6.1 Если СУБД нет

Это не отказ, а состояние с причиной: `db --probe-only` печатает, что именно не
найдено (клиент, доступ, права), и что сделать. Пустая база — **нормальное
состояние**: измерено на живом PostgreSQL, где пользовательских таблиц ноль и это
не ошибка сбора.

---

## 7. Границы, подтверждённые замером

| Что | Замер |
|---|---|
| MySQL 9.7 игнорирует выбранный разделитель вывода | разбор определяет разделитель по факту |
| psql отдаёт вертикальную черту, а не табуляцию | то же |
| У PostgreSQL **база и схема — разные вещи** | схема читается из каталога, а не предполагается `public` |
| Раньше проверка искала только `_id` | нашла 1 колонку из 173 — вывод связей был почти пуст |
| Раньше проверка искала `first_name` | не ловила `FirstName` и `E-Mail` — имя попадало в пример |
| `rev_history` лежит в control-хранилище, не в графе | запрос к графу падал `no such table` |
| Раньше `db_cross` искал `from имя` без SQL-контекста | в «таблицы» попали `__future__` из импортов и `ack` — 73 ложные находки из 86 |
| Соединение связей по имени таблицы давало декартово произведение | 16 «фактов» вместо 4: таблица `contracts` есть в двух базах |
| `CREATE INDEX` по новой колонке падал до миграции | `no such column` при каждом запуске у уже созданной базы |
| `INSERT OR REPLACE` не заменял строки таблицы связей (нет первичного ключа) | старые связи оставались рядом с новыми |
| В PostgreSQL на машине 0 пользовательских таблиц | пустая база — нормальное состояние, а не сбой |

---

## 8. Проверки

```powershell
python tests/test_db_index.py        # доступ, структура, примеры, персональные, отпечаток
python tools/check_db_layer.py       # свойства слоя для чекера
python tools/ragraf.py -p config/ragraf.yaml db --probe-only
python tools/ragraf.py -p config/ragraf.yaml db
pwsh -NoLogo -NoProfile -NonInteractive -File tools/Invoke-ProjectDoctrine.ps1
```

`tests/test_db_index.py` работает на **живых** СУБД и не требует данных: пустая база
обрабатывается как нормальное состояние отдельным случаем.
