# Восстановите прием справочника из источника
Хранилище получает справочник аэропортов из Postgres. Названия и другие
атрибуты в источнике могут меняться, поэтому загрузка должна сохранять
состояние справочника на момент приема данных.
Вы разберете готовый участок `airports` и напишете его заново: создадите
внешнюю таблицу PXF и загрузку полного снимка в STG. Затем испытаете ее
на повторном запуске и изменении источника. Полученный снимок понадобится
следующему слою — ODS.
## Задача
Ваш участок на [карте](../design/architecture-map.html#node=stg.airports):
`bookings.airports_data → stg.airports_ext → stg.airports`.
| Что написать | Где |
|---|---|
| DDL внешней таблицы `stg.airports_ext` | [airports_ddl.sql](../../sql/stg/airports_ddl.sql) |
| Загрузку полного снимка во внутреннюю `stg.airports` | [airports_load.sql](../../sql/stg/airports_load.sql) |
Внутренняя таблица, JDBC-сервер PXF, DAG и проверки качества уже готовы.
Рядом есть пример для другого справочника:
[DDL airplanes](../../sql/stg/airplanes_ddl.sql) и
[его загрузка](../../sql/stg/airplanes_load.sql). Используйте его, учитывая
состав полей и ключ аэропорта.
Требования к вашей загрузке:
- Источник — `bookings.airports_data` в базе `demo`, ключ — `airport_code`.
- Все шесть полей (`airport_code`, `airport_name`, `city`, `country`,
`coordinates`, `timezone`) попадают в STG без потерь и преобразований.
Внешняя и внутренняя таблицы используют TEXT; JSON и координаты
пока хранятся как текст.
- Строки одного снимка получают `_load_id = '{{ run_id }}'`,
`_load_ts = now()` и `event_ts = now()`. Точного времени изменения
аэропорта в источнике нет, поэтому `event_ts` — время получения снимка.
- Новый запуск сохраняет новый полный снимок, оставляя предыдущие.
Повторное выполнение загрузки в том же запуске на неизменном источнике
не меняет бизнес-данные и не создает дубли ключей внутри снимка.
## Подготовка
Нужен свой стенд после [быстрого старта и знакомства с моделью](README.md#подготовка).
Во время работы не запускайте другие загрузки: они могут изменить данные
посреди опыта. Если запущен `bookings_validate`, дождитесь его завершения.
Сначала освободите участок для своей реализации. Команды ниже удаляют
внешнюю таблицу и **всю историю только `stg.airports`**. Это нужно, чтобы
данные быстрого старта не скрыли отсутствие вашей загрузки.
Подготовка файлов и таблиц
Сохраните свои SQL-файлы в Git или отдельной копии. В `airports_ddl.sql`
удалите блок от `DROP EXTERNAL TABLE` до конца `CREATE EXTERNAL TABLE`;
DDL схемы и внутренней таблицы оставьте. Содержимое `airports_load.sql`
замените на `SELECT 1;`. DAG и DQ менять не нужно.
В Greenplum (`make gp-psql`):
```sql
DROP EXTERNAL TABLE IF EXISTS stg.airports_ext;
TRUNCATE TABLE stg.airports;
SELECT COUNT(*) FROM stg.airports; -- 0
```
Остальные справочники и текущий ODS сохраняются. Пока участок
не восстановлен, ODS запускать не нужно.
## Соберите загрузку
Начните с внешней таблицы. Добейтесь, чтобы запрос к `stg.airports_ext`
возвращал те же поля и значения, что запрос к `bookings.airports_data`
в Postgres. Для начала достаточно рассмотреть несколько аэропортов.
Ориентиры для PXF и применение DDL
В `LOCATION` укажите таблицу источника, `PROFILE=JDBC` и `SERVER=bookings-db`.
`SERVER` — имя готового [каталога конфигурации PXF](../../pxf/servers/bookings-db/jdbc-site.xml).
Формат чтения — `FORMAT 'CUSTOM' (formatter='pxfwritable_import')`.
Логин и пароль в DDL не нужны. Если меняли учетные данные источника,
согласуйте их с PXF по [справочнику стенда](../stack.md).
Примените свой файл из `make gp-psql`:
```sql
\set ON_ERROR_STOP on
\i /sql/stg/airports_ddl.sql
SELECT * FROM stg.airports_ext ORDER BY airport_code LIMIT 3;
```
В Postgres (`make bookings-psql`) выполните такой же запрос к
`bookings.airports_data`. Сверьте состав и порядок полей с DDL.
Если чтение не работает, начните с сообщения об ошибке и
[диагностики STG](../bookings_to_gp_stage.md#если-загрузка-не-прошла).
Теперь напишите загрузку снимка. Продумайте, как ваш SQL отличит
повторное выполнение от нового запуска и сохранит историю.
Выполнять загрузку будет готовый DAG `lab_pxf_airports`.
Получите первый снимок и сравните его с источником готовой сверкой.
Она проверяет ключи, все бизнес-поля и служебные метки. Зеленые задачи
в Airflow показывают, что код выполнился; сверка помогает обнаружить
ошибку в самих данных.
Первый запуск в Airflow и сверка с Postgres
Для сверки нужен `uv` с Python 3.11 из
[окружения разработчика](../stack.md#локальное-окружение-разработчика-uv).
В терминале из корня репозитория сохраните исходный справочник:
```bash
mkdir -p data/lab9
uv run --no-project scripts/check_airports_snapshot.py export data/lab9/airports-before.json
```
Скрипт читает Postgres напрямую, минуя вашу внешнюю таблицу, и не перезаписывает
существующий файл. Эта копия позволит сравнить первый снимок с источником
даже после изменения названия аэропорта.
Откройте Airflow UI по адресу из `.env` (`AIRFLOW_WEB_PORT`), войдите с
`AIRFLOW_USER` и `AIRFLOW_PASSWORD`. Включите `lab_pxf_airports`, нажмите
**Trigger DAG** и дождитесь успеха всех задач. В **Grid** выберите запуск;
его **Run ID** подставьте в команду:
```bash
first_run='вставьте Run ID первого запуска'
uv run --no-project scripts/check_airports_snapshot.py check "$first_run" data/lab9/airports-before.json
```
Если задача упала, откройте ее **Log**. SQL-файлы доступны контейнеру
из рабочей папки: после правки пересобирать образ не нужно.
`{{ run_id }}` подставляет Airflow, поэтому файл загрузки не следует
выполнять напрямую через `psql`.
## Испытайте загрузку
### Повторное выполнение
Представьте, что после сбоя пришлось повторить уже выполненную задачу.
Что произойдет с данными при повторном выполнении вашего SQL? За счет
чего в одном снимке не появится вторая копия каждого аэропорта?
Повторите загрузку **в том же запуске** и снова выполните сверку первого
снимка. При ошибке исправьте SQL и повторите опыт на том же Run ID.
Как повторить задачу, сохранив Run ID
В запуске первого снимка выберите `load_airports_to_stg` → **Clear task**.
Снимите **Upstream**, **Downstream**, **Past**, **Future**, **Recursive**
и **Only Failed**, если включены. В **Affected Tasks** должна остаться
только эта задача этого запуска. Нажмите **Clear**, дождитесь успеха,
затем так же повторите `check_airports_dq`.
Новый **Trigger DAG** создал бы другой Run ID. Здесь нужно повторное
выполнение с прежним `_load_id`, чтобы испытать защиту от дублей.
Если загрузка уже записала дубли или неверные значения, исправление SQL
само их не удалит.
Удалите из `stg.airports` строки только этого `_load_id`, затем повторите
исправленную загрузку и DQ в том же запуске. После успешной сверки повторите
их еще раз: теперь вы проверяете защиту от дублей на заполненном снимке.
### Изменение в источнике
Теперь аэропорт переименовали. До новой загрузки предположите, где будет
видно новое название: в Postgres, во внешней таблице, в первом снимке STG?
Измените русское название одного аэропорта, сохранив ключ и остальные
языки. Для опыта ниже есть готовый SQL, который также сохраняет исходное
название для восстановления.
Изменить название в Postgres
В `make bookings-psql`:
```sql
\set ON_ERROR_STOP on
CREATE TABLE public.lab9_airport_original AS
SELECT airport_code, airport_name
FROM bookings.airports_data ORDER BY airport_code LIMIT 1;
UPDATE bookings.airports_data AS a
SET airport_name = jsonb_set(
b.airport_name, '{ru}', to_jsonb((b.airport_name->>'ru') || ' [lab9]'), false
)
FROM public.lab9_airport_original AS b
WHERE a.airport_code = b.airport_code
RETURNING a.airport_code, b.airport_name AS original, a.airport_name AS changed;
```
Запрос изменяет один аэропорт. Если таблица копии уже существует после
предыдущего опыта, сначала [восстановите источник](#завершение-работы),
сверьте результат и удалите таблицу копии. Затем можно начать новый опыт.
Найдите этот аэропорт в `stg.airports_ext` и `stg.airports` обычными
SELECT-запросами. Сопоставьте наблюдение со своим предположением.
Запустите `lab_pxf_airports` заново и рассмотрите оба снимка в STG.
Какое название хранится в каждом? Почему одного сравнения количества
строк недостаточно, чтобы заметить переименование?
Сверьте новый снимок с измененным источником, а первый — с исходным.
Это обнаружит, например, загрузку старых данных под новым Run ID.
Сверить оба снимка
В том же терминале, где задан `first_run`:
```bash
uv run --no-project scripts/check_airports_snapshot.py export data/lab9/airports-after.json
next_run='вставьте Run ID нового запуска'
uv run --no-project scripts/check_airports_snapshot.py check "$next_run" data/lab9/airports-after.json
uv run --no-project scripts/check_airports_snapshot.py check "$first_run" data/lab9/airports-before.json
```
Если терминал закрывался, значения Run ID можно снова взять из Airflow.
## Передайте снимок в ODS
Запустите `bookings_to_gp_ods` без параметров `conf`. Найдите измененный
аэропорт в `ods.airports`: название должно быть новым, а `_load_id` —
совпадать с последним запуском STG. Так вы увидите, что следующий слой
использовал результат вашей загрузки.
ODS выбирает общий батч четырех справочников: `airports`, `airplanes`,
`routes`, `seats`. Поэтому готовый `lab_pxf_airports` загружает все четыре,
используя те же SQL, что основной STG-DAG. Генератор новых дней и
транзакционные таблицы в этом опыте не участвуют.
Проверить выбор снимка и данные ODS
В Log задачи `resolve_stg_batch_id` строка `Используем stg_batch_id=...`
показывает выбранный Run ID. Он должен совпасть с `next_run`.
После завершения DAG выполните:
```bash
uv run --no-project scripts/check_airports_snapshot.py check "$next_run" data/lab9/airports-after.json --layer ods
```
Сверка учитывает преобразование JSON в русские названия и требует нужный
`_load_id` у всех строк ODS. Если выбран старый батч, посмотрите, завершились
ли все загрузки и DQ последнего запуска `lab_pxf_airports`.
## Завершение работы
Верните исходное название и получите еще один общий снимок через
`lab_pxf_airports`. Проведите его по цепочке
`bookings_to_gp_ods` → `bookings_to_gp_dds` → `bookings_to_gp_dm`.
В `ods.airports` и `dds.dim_airports` должно вернуться исходное название.
Восстановить источник и сверить результат
В `make bookings-psql`:
```sql
UPDATE bookings.airports_data AS a
SET airport_name = b.airport_name
FROM public.lab9_airport_original AS b
WHERE a.airport_code = b.airport_code
RETURNING a.airport_code, a.airport_name;
```
После нового запуска `lab_pxf_airports` и ODS сравните восстановленный
снимок с исходной копией:
```bash
restored_run='вставьте Run ID запуска STG после восстановления'
uv run --no-project scripts/check_airports_snapshot.py check "$restored_run" data/lab9/airports-before.json
uv run --no-project scripts/check_airports_snapshot.py check "$restored_run" data/lab9/airports-before.json --layer ods
```
Повтор UPDATE восстановления безопасен. После успешной сверки выполните
в Postgres `DROP TABLE public.lab9_airport_original;`.
Если прервались на изменении источника,
продолжите с этого восстановления и нового запуска цепочки.
Для повторения всей лабораторной возьмите новые имена контрольных файлов.
При полной пересборке сохраните SQL-файлы: `make clean` удаляет тома.
Затем снова пройдите быстрый старт со своим решением.
Снимки в STG остаются историей опыта: первоначальный, с измененным названием
и восстановленный. Последний общий снимок и текущий ODS снова содержат
исходные значения; транзакционные данные остались от быстрого старта.
Результат работы — ваши два SQL-файла и объяснение их поведения: откуда
берется каждый снимок, что защищает его от дублей при повторе и как
изменение источника попадает в следующую загрузку.
Дальше — [загрузки `ods.airplanes` и `ods.seats`](README.md#первая-загрузка-ods).
Они используют уже подготовленный снимок, поэтому заново запускать STG
для начала следующей работы не нужно. Там вы будете преобразовывать поля
полученных данных и заменять текущее состояние в ODS.