diff --git a/AGENTS.md b/AGENTS.md index bd05319..0856d13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,9 +12,9 @@ - `airflow/dags/` — DAG-файлы (напр. `bookings_to_gp_stage.py`). - `sql/` — DDL и SQL-скрипты. Разделены на слои: src/ (исходные системы), stg/ (стейджинг), ods/ (операционное хранилище), dds/ (детальное хранилище), dm/ (слой витрин). - *Правило ИИ:* DDL таблиц хранится строго рядом с объектом (напр. `sql/stg/bookings_ddl.sql`). -- `docs/` — документация. Подкаталоги: `design/` (дизайн, стандарты), `reference/` (справочники), `plans/` (активные планы), `archive/` (выполненные планы), `assignment/` (задание для студента). +- `docs/` — документация. Подкаталоги: `design/` (дизайн, стандарты), `reference/` (справочники), `assignment/` (задание для студента). - `docs/design/naming_conventions.md` — единый источник нейминга служебных и SCD-полей. *Правило ИИ: всегда сверяться при генерации DDL/SQL.* - - Нейминг файлов планов: `YYYY-MM-DD_краткое-описание.md` (дата — ISO 8601, `_` отделяет дату от описания, описание в kebab-case, без суффикса `-plan`). + - Полные дизайн-документы (PRD, assignment_design, планы, архив) — в ветке `solution`. - `tests/` — pytest-тесты (smoke-тесты DAG'ов и юнит-тесты). - `.env` — Настройки окружения (все секреты `GP_*`, `AIRFLOW_*` берем только отсюда). @@ -61,7 +61,6 @@ - Есть smoke‑тесты DAG‑структуры (`tests/test_dags_smoke.py`). - Smoke‑тесты DAG автоматически пропускаются, если Airflow не установлен в venv. - Для ручного прогона стенда см. `TESTING.md` (пошаговый чек‑лист для студентов). -- Для программной проверки DAG (без браузера) — см. `docs/agent-dag-testing.md`: CLI, REST API, проверка параллельности, запросы в Greenplum. ## Pull Requests - Conventional Commits: `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`. Пример: `feat(dags): load orders to Greenplum`. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index c19f08e..0000000 --- a/TODO.md +++ /dev/null @@ -1,141 +0,0 @@ -# TODO (maintainers / mentors) - -Этот файл собирает задачи по подготовке стенда к курсовой работе -и идеи по доработке, которые не критичны для текущих задач менти. - -Контекст и стратегия: [docs/design/PRD.md](docs/design/PRD.md). -Дизайн задания: [docs/design/assignment_design.md](docs/design/assignment_design.md). - ---- - -## Подготовка курсовой - -> Дедлайн: ~2-3 недели (первый студент может подойти к курсовой). - -### Этап 1. Вынос CSV-пайплайна - -**Инструмент:** Sonnet / Gemini / ChatGPT — механическая работа, перенос файлов. - -- [x] Перенести в [airflow-manual](https://github.com/dementev-dev/airflow-manual): - `csv_to_greenplum.py`, `csv_to_greenplum_dq.py`, `ddl_greenplum_base.py`, - `helpers/greenplum.py`, `sql/base/orders_ddl.sql`, связанные тесты -- [x] Убрать CSV-зависимости из docker-compose / .env (`CSV_DIR`, `CSV_ROWS`) -- [x] Обновить README (убрать упоминания CSV-пайплайна) - -### Этап 1.5. Полировка эталона - -**Инструмент:** Opus (глубокий анализ кода и контекста проекта) -+ ручное тестирование (make up, запуск DAG'ов, проверка данных). - -- [x] Протестировать полный ETL-цикл с нуля - (make up → bookings-init (восстановление из дампа) → STG → ODS → DDS → DM) -- [x] Прогнать инкремент (bookings-generate-day → повторный запуск DAG'ов) -- [x] Почистить код эталонного среза -- [x] Актуализировать README и документацию -- [x] Убедиться, что `make test` и `make lint` проходят -- [x] Проверить, что стенд поднимается на чистой машине - (проверено 2026-03-09: `cp .env.example .env` → `make up` → `make bookings-init` → - `make ddl-gp` → STG → ODS → DDS → DM — всё success) - -### Этап 2. ТЗ от аналитика - -**Инструмент:** Opus — нужно глубокое понимание предметной области, маппингов -между слоями, SCD-паттернов и педагогического контекста. - -> Делаем пока полный пайплайн работает — можно сверяться с реальными данными. - -- [x] Создать `docs/assignment/analyst_spec.md` -- [x] Для каждой таблицы-задания: имя, описание, поля, маппинг, - бизнес-правила, тип SCD, гранулярность, distribution key -- [x] Для dim_routes (SCD2): пошаговый алгоритм текстом, формула hashdiff -- [x] Рекомендуемый порядок выполнения - -### Этап 3. Валидационный DAG - -**Инструмент:** Sonnet (шаблонная работа, структура в assignment_design.md) -+ Opus для финальной вычитки. - -> Делаем и тестируем на полных данных, пока ничего не удалено. - -- [x] Создать `airflow/dags/bookings_validate.py` -- [x] Создать SQL-скрипты в `sql/validate/` -- [x] Таски по слоям: STG, ODS, DDS, DM -- [x] Дружелюбные сообщения об ошибках с подсказками -- [x] Протестировать на работающем стенде - -### Этап 4. Подготовка main и ветка solution - -**Инструмент:** Opus — раскладка по веткам, заглушки, ослабление DQ. - -> Финальный этап: всё готово и протестировано, теперь раскладываем по веткам. -> План: `docs/plans/2026-03-12_main-solution-split.md` - -- [ ] Смержить chore/bookings-etl → main -- [ ] Общие правки на main (документация, TODO.md) -- [ ] Создать ветку solution (снимок полного эталона) -- [ ] Main-only правки: заглушки, ослабление DQ, адаптация тестов -- [ ] Очистка docs на main (удалить внутренние документы) -- [ ] Верификация main (`make test`, `make lint`) -- [ ] Верификация solution (`make test`) - ---- - -## Прочее (бэклог) - -- [ ] Протестировать устойчивость `bookings-db` после остановки контейнеров: - - прогнать сценарии `make stop` -> `make up` и `make down` -> `make up`; - - зафиксировать, ломается ли генератор/данные в `bookings-db`; - - при необходимости добавить шаги восстановления и обновить документацию. - ---- - -## Выполнено - -- [x] Сделать REST API Airflow основным способом тестирования ETL вместо CLI-вызовов - через `docker compose exec ... airflow ...`: - - обновить `TESTING.md`, сместив фокус на REST API сценарии; - - оставить CLI как резервный вариант для локальной отладки; - - проверить, что шаги тестирования воспроизводимы без входа в контейнер Airflow. - -- [x] Собрать свой образ Airflow поверх `apache/airflow:2.9.2`: - - вынести установку Python‑зависимостей из runtime (`pip install ...` при старте контейнеров) - в отдельный `Dockerfile`; - - переключить `docker-compose.yml` на использование этого образа для `airflow-webserver`, - `airflow-scheduler` и `airflow-init`; - - обновить документацию (README/TESTING) под новую схему сборки. - -- [x] Переключить Airflow с `SequentialExecutor` (SequentialScheduler) на `LocalExecutor` - для docker‑стенда: - - проверить, какие параметры достаточно поменять в env/конфиге (`AIRFLOW__CORE__EXECUTOR`) - для образа `apache/airflow:2.9.2`; - - убедиться, что примерные DAG'и (`csv_to_greenplum`, `bookings_to_gp_stage`) ведут себя - предсказуемо в режиме параллельного исполнения; - - при необходимости скорректировать тесты и документацию (README/TESTING) с учётом нового executor'а. - -- [x] Разобрать и стабилизировать интеграцию с Greenplum/PXF: - - убедиться, что PXF в контейнере `greenplum` всегда корректно инициализируется - (нет ошибок вида `protocol "pxf" does not exist` при первом запуске `make ddl-gp`); - - при необходимости доработать init‑скрипты в `pxf/init/` и/или документацию, - чтобы порядок действий для ментей был однозначным и воспроизводимым; - - добавить краткий раздел в README/TESTING о типичных ошибках PXF/Greenplum и шагах по их устранению. - - диагностика текущего кейса: `docs/reference/pxf_bookings.md` (раздел «Известная проблема»). - -- [x] Разобраться с генератором demodb: - - после `make bookings-generate` таблица `bookings.bookings` остаётся пустой; - - патчи `bookings/patches/engine_jobs1_sync.patch` и `bookings/patches/install_drop_if_exists.patch` - падают при применении (hunk failed / garbage in patch); - - из‑за этого DAG `bookings_to_gp_stage` валится на проверках (источник пустой). - - детали: `docs/reference/bookings_db_issues.md` - -- [x] Добавить раздел «Благодарности» в `README.md`: - - явно поблагодарить Postgres Pro за демо‑БД bookings (репозиторий `postgrespro/demodb`); - - указать автора Docker‑сборки Greenplum (`woblerr/docker-greenplum`, образ `woblerr/greenplum`); - - при необходимости сослаться на соответствующие лицензии/README исходных проектов. - -- [x] Добавить в образ Airflow установку `psql`, чтобы тестировать загрузку CSV из CLI внутри контейнера (без root и дополнительных зависимостей на хосте). - -- [x] Денормализовать `dds.dim_routes` (добавить departure_city, arrival_city, airplane_model, total_seats): - - привести измерение в соответствие с принципом Кимбалла («самодостаточное измерение»); - - упростить `dm.route_performance` с 4-JOIN до 1-JOIN; - - обновить DAG-зависимости: airports+airplanes DQ → routes load; - - подробный план: `docs/archive/dim_routes_denormalization_plan.md`. diff --git a/docs/README.md b/docs/README.md index d3f79cd..85b1e57 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,26 +20,12 @@ - [Дизайн-документ ODS](design/bookings_ods_design.md) - [Дизайн-документ DDS](design/bookings_dds_design.md) - [Дизайн-документ DM](design/bookings_dm_design.md) -- [Архитектурные решения (ADR)](design/architecture_review.md) -- [PRD: стратегия курсовой](design/PRD.md) -- [Дизайн задания](design/assignment_design.md) + +> Полные дизайн-документы (PRD, assignment_design, архитектурные решения) — в ветке `solution`. ## Справочники (`reference/`) - [Как устроен Docker-стенд (образы, Connections, переменные окружения)](stack.md) -- [PXF в этом проекте (проектная реализация)](reference/pxf_bookings.md) -- [Про время/UTC в bookings](reference/bookings_tz.md) - [Известные проблемы bookings-db](reference/bookings_db_issues.md) -- [Бенчмарк генерации данных](reference/bookings_generation_benchmark.md) - [QA-план отладки пайплайна](reference/qa-plan.md) - [Порядок запуска DAG-ов](dag_execution_order.md) -- [End-to-end протокол тестирования](e2e-etl-test-protocol.md) -- [Тестирование DAG-ов через API](agent-dag-testing.md) - -## Планы (`plans/`) - -Активные планы работ. После выполнения переносятся в `archive/`. - -## Архив (`archive/`) - -Выполненные планы, закрытые ревью. Ссылки внутри файлов могут быть устаревшими. diff --git a/docs/agent-dag-testing.md b/docs/agent-dag-testing.md deleted file mode 100644 index 35fcb2b..0000000 --- a/docs/agent-dag-testing.md +++ /dev/null @@ -1,89 +0,0 @@ -# Тестирование DAG (гайд для AI-агентов) - -Этот документ описывает, как агенту взаимодействовать с Airflow (без UI) для точечной проверки и отладки DAG'ов в процессе разработки. - -## 0. Состояние среды (Предварительная проверка) - -Прежде чем тестировать DAG, убедитесь, что стек работает: -```bash -docker compose ps -``` -Если контейнеров нет, поднимите стек: `make up`. -Если исходная база данных пуста (например, после `make clean`), проинициализируйте её: `make bookings-init`. - ---- - -## 1. Быстрая проверка структуры графа (Локально) - -При любом изменении Python-кода DAG'а сначала проверьте, что он компилируется и структура графа корректна: -```bash -make test -``` -Это запустит smoke-тесты (`tests/test_dags_smoke.py`), которые проверят целостность всех DAG'ов без обращения к базе данных. - ---- - -## 2. Запуск конкретного DAG'а (REST API) - -Для тестирования загрузки данных запустите измененный DAG через REST API (базовый URL `http://localhost:8080/api/v1`, креды взять из `.venv`). - -**Шаг 2.1. Снять DAG с паузы (при старте стенда все DAG'и на паузе — этот шаг обязателен!):** -```bash -curl -s -X PATCH "http://localhost:8080/api/v1/dags/" \ - -u admin:admin -H "Content-Type: application/json" -d '{"is_paused": false}' -``` - -**Шаг 2.2. Запустить DAG:** -```bash -curl -s -X POST "http://localhost:8080/api/v1/dags//dagRuns" \ - -u admin:admin -H "Content-Type: application/json" -d '{}' | jq '{dag_run_id}' -``` - -**Шаг 2.3. Проверить статус выполнения:** -Используйте `dag_run_id` из предыдущего шага: -```bash -curl -s "http://localhost:8080/api/v1/dags//dagRuns/" \ - -u admin:admin | jq '{state}' -``` -Повторяйте запрос, пока `state` не станет `success` или `failed`. - ---- - -## 3. Отладка упавших задач - -Если DAG перешел в статус `failed`, найдите упавшую задачу: - -**Шаг 3.1. Получить статусы всех задач:** -```bash -curl -s "http://localhost:8080/api/v1/dags//dagRuns//taskInstances" \ - -u admin:admin | jq '.task_instances[] | {task_id, state}' -``` - -**Шаг 3.2. Посмотреть логи упавшей задачи (через CLI Airflow):** -REST API отдает логи сложно, поэтому для логов проще использовать `docker compose exec`: -```bash -docker compose exec airflow-webserver airflow tasks logs -``` - -*(Совет: ищите в логах слова `ERROR`, `Exception` или вывод SQL-ошибок от PostgresOperator).* - ---- - -## 4. Проверка результата в DWH (Greenplum) - -Успешное выполнение DAG'а (зеленый статус) не гарантирует, что данные загрузились правильно (например, если источник был пуст). Проверьте целевые таблицы напрямую: - -```bash -docker compose exec greenplum bash -lc "su - gpadmin -c \"/usr/local/greenplum-db/bin/psql -t -A -d gp_dwh -c 'SELECT COUNT(*) FROM <схема>.<таблица>;'\"" -``` -Убедитесь, что таблица содержит ожидаемое количество строк. - ---- - -## 5. Полный сквозной тест (E2E) - -Если вы вносили масштабные изменения, затрагивающие несколько слоев DWH, или меняли DDL таблиц, рекомендуется прогнать полный конвейер (STG -> ODS -> DDS -> DM): -```bash -make e2e-etl -``` -Этот скрипт сам запустит все нужные DAG'и в правильном порядке и проверит результаты. diff --git a/docs/archive/2026-01-18_bookings-stg-code-review.md b/docs/archive/2026-01-18_bookings-stg-code-review.md deleted file mode 100644 index 6fc2819..0000000 --- a/docs/archive/2026-01-18_bookings-stg-code-review.md +++ /dev/null @@ -1,149 +0,0 @@ -# Ревью решения (образец для студентов): `bookings-db` → `stg` в Greenplum - -Этот документ фиксирует рекомендации по улучшению учебного решения ETL (Airflow + Greenplum + PXF) -на основе ревью изменений ветки `chore/bookings-etl` (добавление полного STG слоя и пайплайна загрузки). - -Цель ревью — сделать решение **безоговорочно рекомендуемым** к изучению начинающими: -понятным, предсказуемым, с корректной терминологией и честными инженерными компромиссами. - ---- - -## 1) Сильные стороны решения (что уже хорошо и стоит сохранить) - -1. **Единый “шаблон” по таблицам** в `sql/stg/`: - - `{table}_ddl.sql` — создаёт `*_ext` и внутреннюю таблицу; - - `{table}_load.sql` — загружает данные; - - `{table}_dq.sql` — валидирует качество и останавливает пайплайн при проблемах. - - Это отличная учебная структура: студент быстро понимает, “где что лежит” и как добавлять новые таблицы. - -2. **DAG как оркестратор, SQL как логика**: - - `airflow/dags/bookings_to_gp_stage.py` и `airflow/dags/bookings_stg_ddl.py` используют `PostgresOperator` - и читают SQL с диска через `template_searchpath="/sql"`. - Это соответствует “канонической” модели: Airflow управляет шагами, а трансформации живут в SQL. - -3. **Понятные сообщения при падении DQ** (в большинстве скриптов): студенту легче дебажить. - ---- - -## 2) Критичные замечания (статус на текущий момент) - -### 2.1. Некорректные утверждения про MPP и co-location (статус: исправлено) - -В Greenplum производительность JOIN сильно зависит от распределения данных по сегментам. -Если ключ распределения двух таблиц совпадает с ключом JOIN — часто удаётся обойтись без перераспределения данных (motion). - -Раньше в некоторых DDL-комментариях обещалась co-location там, где её не будет. -Это педагогически опасно: студент запоминает неверную модель, а потом “не понимает”, почему запросы медленные. - -Что сделано: -- DDL-комментарии приведены к честной формулировке “ключ выбран так-то, но JOIN по другим ключам может требовать motion”. -- Исправлены места, где co-location заявлялась ошибочно (в т.ч. `routes`, `flights`, `segments`, `boarding_passes`). - -Файлы: `sql/stg/routes_ddl.sql`, `sql/stg/flights_ddl.sql`, `sql/stg/segments_ddl.sql`, `sql/stg/boarding_passes_ddl.sql`. - -### 2.2. DQ-проверки ссылочной целостности: “текущий батч” vs “вся история” (статус: исправлено) - -Часть DQ-скриптов проверяет наличие “родительских” записей в таблице **без фильтра `_load_id`**. -При append-only истории это может скрыть проблемы текущей загрузки: -родитель был загружен в прошлом батче → проверка пройдёт, даже если текущий батч родителя не загрузил. - -Что сделано: -- `routes_dq.sql`: проверка airports/airplanes стала батч-строгой (`_load_id = текущий батч`). -- `seats_dq.sql`: проверка airplanes стала батч-строгой (`_load_id = текущий батч`). -- `flights_dq.sql`: проверка routes стала батч-строгой (`_load_id = текущий батч`). - -Примечание (почему не везде `_load_id = текущий батч`): -- Если дочерняя таблица грузится инкрементом, то ссылки могут указывать на “исторические” записи, - загруженные в предыдущих батчах → для таких связей корректнее проверять “существует в STG вообще”. -- Для `boarding_passes` (full snapshot) ссылки на `tickets/segments` также проверяются по STG-истории, - потому что `tickets/segments` не перезагружаются полным снэпшотом каждый запуск. - -### 2.3. Smoke-тесты DAG’ов (статус: исправлено) - -Что сделано: -- Тесты усилены: теперь проверяются ключевые зависимости графа через `get_direct_relatives(upstream=False)` и “барьеры” через `get_flat_relatives(upstream=False)`. - -### 2.4. Документация по DAG (статус: синхронизировано) - -Что сделано: -- `docs/bookings_to_gp_stage.md` обновлён так, чтобы отражать текущий набор таблиц и шагов пайплайна. - ---- - -## 3) Рекомендации по качеству и читаемости (Clean Code для SQL и DAG) - -### 3.1. “Empty window” в инкременте: договориться о политике (fail vs skip) (статус: исправлено) - -Раньше поведение было разным: -- часть DQ-скриптов падала, если в окне инкремента 0 строк; -- `boarding_passes_dq.sql` делал `RAISE NOTICE` и `RETURN`. - -Обе стратегии допустимы, но в учебном решении важно выбрать одну и объяснить: -- **Fail** полезен, когда “ожидаем данные в каждом запуске” (например, учебный генератор должен добавлять день); -- **Skip** полезен, когда “окно может быть пустым и это нормально”. - -Выбранная политика для учебного стенда: -- Для инкрементальных таблиц (`bookings`, `tickets`, `flights`, `segments`) “пустое окно” **допустимо**: - DQ логирует `NOTICE` и завершает проверку, не падая. -- Для snapshot-справочников (`airports`, `airplanes`, `routes`, `seats`) пустой источник считаем ошибкой: - это почти всегда признак проблем с PXF/источником. - -### 3.2. Комментарии в `*_load.sql`: точнее формулировать “идемпотентность”, а не “дедупликацию источника” (статус: исправлено) - -Типовой паттерн: -```sql -WHERE NOT EXISTS ( - SELECT 1 FROM stg.table WHERE _load_id = '{{ run_id }}' AND key = ext.key -); -``` - -Это в первую очередь защита от повторного запуска того же таска в рамках одного `_load_id` (retry), -а не “лечение” дублей в источнике. - -Что сделано: -- В `sql/stg/*_load.sql` комментарии приведены к формулировке “идемпотентность при повторном запуске/ретрае”. - -### 3.3. Проверка составных ключей: избегать склейки строк (статус: исправлено) - -Паттерн вида `COUNT(DISTINCT col1 || '|' || col2)` теоретически может давать коллизии (если в данных встречается разделитель). -В учебном стенде риск небольшой, но как “эталон” лучше показывать более безопасный подход. - -Что сделано: -- Заменили склейку строк на `COUNT(DISTINCT md5(ROW(col1, col2)::text))` в DQ‑скриптах для составных ключей. - Такой подход сохраняет DV‑стиль и убирает неоднозначность разделителей. - ---- - -## 4) Практические примеры “как улучшить” - -### 4.1. Батч-строгая ссылочная целостность (пример подхода) - -Если таблицы грузятся как snapshot в рамках батча, проверки можно сделать батч-строгими: -“в текущем батче ссылки указывают на строки текущего батча”. - -Идея (пример для routes → airports): -```sql -LEFT JOIN stg.airports AS a - ON r.departure_airport = a.airport_code - AND a._load_id = v_batch_id -``` - -### 4.2. Smoke-тест реального графа (минимальный полезный уровень) - -Вместо “таски существуют” лучше проверять ключевые зависимости: -```python -tickets_dq = dag.get_task("check_tickets_dq") -airports_load = dag.get_task("load_airports_to_stg") -assert airports_load in tickets_dq.get_direct_relatives(upstream=False) -``` - ---- - -## 5) Чек-лист “готово как эталон” - -- [x] В DDL-комментариях нет неверных обещаний про co-location/уникальность ключей. -- [x] Для DQ определена и описана политика “0 строк”: где fail, где skip. -- [x] DQ ссылочной целостности не маскирует проблемы текущего батча (batch-строгие проверки там, где это уместно). -- [x] `docs/bookings_to_gp_stage.md` соответствует фактическому DAG. -- [x] Smoke-тесты проверяют хотя бы критические зависимости графа. diff --git a/docs/archive/2026-03-04_dim-routes-denormalization.md b/docs/archive/2026-03-04_dim-routes-denormalization.md deleted file mode 100644 index c72c6c8..0000000 --- a/docs/archive/2026-03-04_dim-routes-denormalization.md +++ /dev/null @@ -1,205 +0,0 @@ -# План: Денормализация dim_routes + упрощение route_performance - -> **Статус:** реализовано. - -## Контекст - -`dds.dim_routes` хранит только FK-ссылки (`departure_airport`, `arrival_airport`, `airplane_code`) без атрибутов (город, модель самолёта, кол-во мест). Из-за этого витрина `dm.route_performance` вынуждена делать 4-JOIN цепочку вместо одного JOIN к измерению. Это противоречит принципу Кимбалла (измерение должно быть «самодостаточным») и усложняет учебный материал. - -**Цель:** добавить в dim_routes 4 денормализованных колонки, упростить route_performance до 1 JOIN, показать студентам правильную Star Schema. - ---- - -## Файлы для изменения - -| # | Файл | Суть | -|---|---|---| -| 1 | `sql/dds/dim_routes_ddl.sql` | +4 колонки через ALTER TABLE | -| 2 | `sql/dds/dim_routes_load.sql` | JOIN к airports/airplanes при INSERT + refresh-шаг | -| 3 | `sql/dds/dim_routes_dq.sql` | NOT NULL проверка для новых колонок (current-срез) | -| 4 | `airflow/dags/bookings_to_gp_dds.py` | Зависимость: airports+airplanes DQ → routes load | -| 5 | `sql/dm/route_performance_load.sql` | Упрощение 4-JOIN → 1-JOIN | -| 6 | `tests/test_dags_smoke.py` | Новые assert для зависимости routes от airports/airplanes | -| 7 | `docs/internal/db_schema.md` | Обновить схему dim_routes | - ---- - -## Шаг 1. DDL — `sql/dds/dim_routes_ddl.sql` - -После существующего `COMMENT ON TABLE` добавить учебный комментарий и 4 ALTER TABLE: - -```sql --- Денормализация атрибутов из SCD1-измерений (dim_airports, dim_airplanes). --- --- Учебный комментарий (Kimball Star Schema): --- Измерение должно быть «самодостаточным»: один JOIN к dim_routes — --- и аналитик видит маршрут, города, модель самолёта и кол-во мест. --- --- Эти колонки НЕ участвуют в hashdiff. Версия SCD2 фиксирует изменения --- атрибутов маршрута (аэропорт, самолёт, расписание). Если изменится --- название города — обновим отдельным refresh-шагом, не создавая новую версию. -ALTER TABLE dds.dim_routes ADD COLUMN IF NOT EXISTS departure_city TEXT; -ALTER TABLE dds.dim_routes ADD COLUMN IF NOT EXISTS arrival_city TEXT; -ALTER TABLE dds.dim_routes ADD COLUMN IF NOT EXISTS airplane_model TEXT; -ALTER TABLE dds.dim_routes ADD COLUMN IF NOT EXISTS total_seats INTEGER; -``` - -Колонки nullable — `ALTER TABLE ADD COLUMN ... NOT NULL` без DEFAULT упадёт на существующих строках. DQ проверит NOT NULL для current-среза. - ---- - -## Шаг 2. Load — `sql/dds/dim_routes_load.sql` - -Структура: 3 фазы вместо 2. - -**Фаза 1 (без изменений):** temp table + hashdiff из ods.routes, UPDATE закрытие версий. Hashdiff **не включает** денормализованные атрибуты. - -**Фаза 2 (изменён INSERT):** при вставке новой версии — LEFT JOIN к dim_airports (×2) и dim_airplanes для заполнения departure_city, arrival_city, airplane_model, total_seats. - -Конкретно: в INSERT (Statement 2, строки 59-107) добавить: -- В список колонок: `departure_city, arrival_city, airplane_model, total_seats` -- В SELECT: `dep.city, arr.city, air.model, air.total_seats` -- LEFT JOIN к dim_airports и dim_airplanes (LEFT — чтобы не терять маршруты при отсутствии аэропорта; DQ поймает) - -**Фаза 3 (новая):** refresh денормализованных атрибутов для всех current-версий: - -```sql --- Фаза 3: Обновление денормализованных атрибутов (refresh). --- Нужна для SCD1-изменений в dim_airports/dim_airplanes (напр. переименование города). --- Обновляем ТОЛЬКО current-версии (valid_to IS NULL). -UPDATE dds.dim_routes AS d -SET departure_city = dep.city, - arrival_city = arr.city, - airplane_model = air.model, - total_seats = air.total_seats, - updated_at = now(), - _load_id = '{{ run_id }}', - _load_ts = now() -FROM dds.dim_airports AS dep, - dds.dim_airports AS arr, - dds.dim_airplanes AS air -WHERE d.valid_to IS NULL - AND dep.airport_bk = d.departure_airport - AND arr.airport_bk = d.arrival_airport - AND air.airplane_bk = d.airplane_code - AND ( - d.departure_city IS DISTINCT FROM dep.city - OR d.arrival_city IS DISTINCT FROM arr.city - OR d.airplane_model IS DISTINCT FROM air.model - OR d.total_seats IS DISTINCT FROM air.total_seats - ); -``` - -Этот же шаг при первом запуске заполнит колонки для существующих данных (backfill). - ---- - -## Шаг 3. DQ — `sql/dds/dim_routes_dq.sql` - -Перед финальным `RAISE NOTICE` добавить проверку: - -```sql --- Денормализованные поля не пустые в текущих (актуальных) версиях. --- Исторические версии могли быть загружены ДО добавления колонок — пропускаем. -SELECT COUNT(*) INTO v_null_count -FROM dds.dim_routes -WHERE valid_to IS NULL - AND (departure_city IS NULL OR arrival_city IS NULL - OR airplane_model IS NULL OR total_seats IS NULL); - -IF v_null_count <> 0 THEN - RAISE EXCEPTION - 'DQ FAILED: в current-срезе dim_routes найдены NULL денормализованные поля: %', - v_null_count; -END IF; -``` - ---- - -## Шаг 4. DAG — `airflow/dags/bookings_to_gp_dds.py` - -Убрать `load_dds_dim_routes` из параллельного fan-out, добавить зависимость от DQ airports и airplanes: - -```python -# Было: -dq_dds_dim_calendar >> [ - load_dds_dim_airports, - load_dds_dim_airplanes, - load_dds_dim_tariffs, - load_dds_dim_passengers, - load_dds_dim_routes, # параллельно -] - -# Стало: -dq_dds_dim_calendar >> [ - load_dds_dim_airports, - load_dds_dim_airplanes, - load_dds_dim_tariffs, - load_dds_dim_passengers, -] - -# dim_routes зависит от airports и airplanes (денормализация). -[dq_dds_dim_airports, dq_dds_dim_airplanes] >> load_dds_dim_routes -``` - -Транзитивная зависимость от calendar сохраняется через airports/airplanes. - ---- - -## Шаг 5. Витрина — `sql/dm/route_performance_load.sql` - -Упростить Шаг 3. Было 4 JOIN → станет 1 JOIN: - -```sql -FROM tmp_route_metrics m -JOIN dds.dim_routes r_curr - ON m.route_bk = r_curr.route_bk AND r_curr.valid_to IS NULL -``` - -SELECT использует `r_curr.departure_airport AS departure_airport_bk`, `r_curr.departure_city`, `r_curr.airplane_code AS airplane_bk`, `r_curr.airplane_model`, `r_curr.total_seats`. Добавить учебный комментарий: «Благодаря денормализации dim_routes — один JOIN вместо четырёх.» - ---- - -## Шаг 6. Тесты — `tests/test_dags_smoke.py` - -Добавить в `test_bookings_to_gp_dds_dag_structure`: - -```python -# dim_routes зависит от airports и airplanes (денормализация). -_assert_reachable(dag, "dq_dds_dim_airports", "load_dds_dim_routes") -_assert_reachable(dag, "dq_dds_dim_airplanes", "load_dds_dim_routes") -``` - ---- - -## Шаг 7. Документация — `docs/internal/db_schema.md` - -Обновить описание dim_routes: добавить 4 новые колонки, пометить «денормализовано из dim_airports/dim_airplanes». Обновить граф зависимостей DAG. - ---- - -## Что НЕ меняется - -- **fact_flight_sales_load.sql** — факт резолвит SK через свои JOIN-ы. Без изменений. -- **passenger_loyalty, monthly_overview** — используют только `route_bk` из dim_routes. -- **airport_traffic** — вообще не джойнит dim_routes. -- **hashdiff** — остаётся прежним (только атрибуты из ods.routes). - ---- - -## Верификация - -1. `make ddl-gp` — накатить DDL (ALTER TABLE добавит колонки) -2. Запустить DAG `bookings_to_gp_dds` — проверить: - - airports/airplanes грузятся ДО routes (Graph view) - - Фаза 3 (refresh) заполняет departure_city, arrival_city, airplane_model, total_seats - - DQ проходит без ошибок -3. Запустить DAG `bookings_to_gp_dm` — проверить: - - route_performance загружается с 1 JOIN - - Витрина содержит корректные города и модели самолётов -4. `make test` — smoke-тесты DAG проходят (включая новые assert'ы) -5. SQL-проверка: - ```sql - SELECT route_bk, departure_city, arrival_city, airplane_model, total_seats - FROM dds.dim_routes WHERE valid_to IS NULL LIMIT 5; - ``` diff --git a/docs/archive/2026-03-10_docs-restructuring.md b/docs/archive/2026-03-10_docs-restructuring.md deleted file mode 100644 index faa040d..0000000 --- a/docs/archive/2026-03-10_docs-restructuring.md +++ /dev/null @@ -1,220 +0,0 @@ -# План ревизии документации - -> Статус: **ЧЕРНОВИК v3** | Дата: 2026-03-10 -> Контекст: перед Этапом 2 (подготовка main) нужно навести порядок в docs/ - ---- - -## Проблемы - -- `docs/internal/` — свалка: дизайн-документы, планы, ревью, баг-трекеры, стандарты -- 3 архивных плана лежат рядом с живыми документами (неотличимы) -- 3 осиротевших документа (никто не ссылается) -- `educational-tasks.md` в корне — устарел (раздел 2.2 говорит «ODS/DDS/DM будут позже») -- 5 документов содержат устаревшие фрагменты -- `docs/README.md` не знает про несколько живых документов - ---- - -## Принципы - -- **Архив замораживается.** Файлы в `docs/archive/` не правим — ссылки внутри них - могут быть битыми, это ожидаемо. Они сохраняются как исторические артефакты. -- **Активные планы** живут в `docs/plans/`, после выполнения переезжают в `docs/archive/`. -- **Студенческий entry point** не должен исчезать: пока `analyst_spec.md` (Этап 3) - не создан, в `docs/assignment/` будет заглушка `README.md` со ссылкой на эталонный - срез для самостоятельного изучения. - ---- - -## Фаза 1. Структура каталогов - -Создать новые каталоги: - -``` -docs/design/ — дизайн-документы, стандарты, архитектура -docs/reference/ — техническая справка, баг-трекеры, бенчмарки -docs/plans/ — активные планы работ -docs/archive/ — выполненные планы, закрытые ревью -docs/assignment/ — заглушка README.md (подготовка для Этапа 3) -``` - ---- - -## Фаза 2. Перемещение файлов - -### В `docs/archive/` (4 файла) - -| Откуда | Файл | Причина | -|--------|-------|---------| -| корень | `educational-tasks.md` | Устарел, заменён `assignment_design.md` | -| `docs/internal/` | `stg_naming_unification_plan.md` | Выполнен | -| `docs/internal/` | `dim_routes_denormalization_plan.md` | Выполнен | -| `docs/internal/` | `bookings_stg_code_review.md` | Все замечания закрыты | - -### В `docs/plans/` (1 файл) - -| Откуда | Файл | Примечание | -|--------|-------|---------| -| `docs/internal/` | `docs_restructuring_plan.md` | Этот план — активный; после выполнения → `docs/archive/` | - -### В `docs/design/` (9 файлов из `docs/internal/`) - -| Файл | Роль | -|------|------| -| `PRD.md` | Стратегия продукта | -| `assignment_design.md` | Дизайн курсового задания | -| `naming_conventions.md` | Стандарт нейминга (единый источник) | -| `db_schema.md` | Схема БД всех слоёв DWH | -| `bookings_stg_design.md` | Дизайн STG-слоя | -| `bookings_ods_design.md` | Дизайн ODS-слоя | -| `bookings_dds_design.md` | Дизайн DDS-слоя | -| `bookings_dm_design.md` | Дизайн DM-слоя | -| `architecture_review.md` | Архитектурные решения (ADR) | - -### В `docs/reference/` (5 файлов из `docs/internal/`) - -| Файл | Роль | -|------|------| -| `pxf_bookings.md` | PXF: настройка, проблемы | -| `bookings_tz.md` | Источник bookings-db | -| `bookings_db_issues.md` | Известные проблемы bookings-db | -| `bookings_generation_benchmark.md` | Бенчмарк генерации | -| `qa-plan.md` | План отладки пайплайна | - -**Итог:** `docs/internal/` опустеет → удалить. - ---- - -## Фаза 3. Обновление перекрёстных ссылок - -### Категория A. Корневые и публичные файлы (ссылаются на `docs/internal/`) - -| Файл | Ссылок | Детали замен | -|------|--------|------| -| `AGENTS.md` | 2 | строки 15, 45: `docs/internal/naming_conventions.md` → `docs/design/naming_conventions.md` | -| `TODO.md` | 6 | строки 6, 7, 43: → `docs/design/`; строка 121: → `docs/reference/pxf_bookings.md`; строка 128: → `docs/reference/bookings_db_issues.md`; строка 141: → `docs/archive/dim_routes_denormalization_plan.md` | -| `docs/README.md` | 7 | строки 18-24: все `internal/*` → `design/*` или `reference/*` | -| `docs/bookings_to_gp_stage.md` | 2 | строка 151: → `reference/pxf_bookings.md`; строка 156: → `archive/bookings_stg_code_review.md` | -| `docs/stack.md` | 1 | строка 85: → `reference/pxf_bookings.md` | - -### Категория B. Файлы внутри `docs/design/` и `docs/reference/` - -Полный реестр ссылок с `docs/internal/` в файлах, которые переедут в `design/` или -`reference/`. Все требуют обновления — либо кросс-каталожные пути, либо display-тексты. - -**B1. Кросс-каталожные ссылки (ссылка ведёт в другой каталог — путь сломается):** - -| Файл (→ design/) | Строка | Ссылка | Новый путь | -|------|--------|--------|------| -| `db_schema.md` | 426 | `bookings_tz.md` (отн.) | `../reference/bookings_tz.md` | -| `db_schema.md` | 427 | `pxf_bookings.md` (отн.) | `../reference/pxf_bookings.md` | -| `db_schema.md` | 425 | `bookings_stg_code_review.md` (отн.) | `../archive/bookings_stg_code_review.md` | -| `bookings_stg_design.md` | 5 | `docs/internal/bookings_tz.md` | `../reference/bookings_tz.md` | -| `bookings_stg_design.md` | 130 | `docs/internal/bookings_tz.md` | `../reference/bookings_tz.md` | -| `bookings_stg_design.md` | 131 | `docs/internal/pxf_bookings.md` | `../reference/pxf_bookings.md` | - -| Файл (→ reference/) | Строка | Ссылка | Новый путь | -|------|--------|--------|------| -| `qa-plan.md` | 10 | `docs/internal/bookings_db_issues.md` | `bookings_db_issues.md` (тот же каталог) | - -**B2. Внутрикаталожные, но с полным путём `docs/internal/...` (путь не сломается -для ссылок с относительным target, но display-текст устареет):** - -| Файл (→ design/) | Строки | Что обновить | -|------|--------|------| -| `PRD.md` | 209 | `docs/internal/naming_conventions.md` → `docs/design/naming_conventions.md` | -| `bookings_ods_design.md` | 48 | display-текст `docs/internal/naming_conventions.md` → `docs/design/naming_conventions.md` | -| `bookings_dds_design.md` | 8 | `docs/internal/bookings_ods_design.md` → `docs/design/bookings_ods_design.md` | -| `bookings_dds_design.md` | 185 | display-текст `docs/internal/naming_conventions.md` → `docs/design/naming_conventions.md` | -| `bookings_dds_design.md` | 828-829 | `docs/internal/bookings_dds_design.md`, `docs/internal/db_schema.md` → `docs/design/...` | -| `bookings_dds_design.md` | 945 | `docs/internal/db_schema.md` → `docs/design/db_schema.md` | -| `bookings_dm_design.md` | 279 | `docs/internal/db_schema.md` → `docs/design/db_schema.md` | -| `bookings_dm_design.md` | 283, 291 | `docs/internal/bookings_dm_design.md` → `docs/design/bookings_dm_design.md` | -| `bookings_dm_design.md` | 395 | `docs/internal/naming_conventions.md` → `docs/design/naming_conventions.md` | -| `db_schema.md` | 26 | display-текст `docs/internal/naming_conventions.md` → `docs/design/naming_conventions.md` | -| `db_schema.md` | 421-423 | display-тексты `docs/internal/bookings_*_design.md` → `docs/design/...` | -| `architecture_review.md` | 70 | `docs/internal/bookings_dm_design.md` → `docs/design/bookings_dm_design.md` | -| `architecture_review.md` | 146 | `docs/internal/distribution_strategy.md` → **удалить путь** (файл не существует, оставить как текстовый backlog-пункт без ссылки) | - -### Категория C. Ссылки на `educational-tasks.md` - -| Файл | Действие | -|------|----------| -| `README.md` (корень) | Заменить ссылку на `educational-tasks.md` → `docs/assignment/` | -| `docs/README.md` | Заменить ссылку на `educational-tasks.md` → `assignment/` | - -### Категория D. Файлы в `docs/archive/` — НЕ ТРОГАЕМ - -Архивные файлы замораживаются. Ссылки внутри них могут быть битыми — это ожидаемо. - -### Комментарий в SQL - -`sql/dm/sales_report_ddl.sql` строка 52: `naming_conventions.md` (без пути) — -оставить как есть (комментарий, не ссылка; путь и так неточный). - ---- - -## Фаза 4. Актуализация содержания - -| Файл (новый путь) | Что сделать | -|------|-------------| -| `docs/design/architecture_review.md` | DM завершён (5/5 витрин), пометить выполненные P2; строка 146 — убрать путь к несуществующему `distribution_strategy.md`, оставить как текстовый backlog-пункт | -| `docs/design/db_schema.md` | Добавить DM-слой, убрать выполненный TODO | -| `TESTING.md` | Убрать артефакт «Docker-стенд не запускался» | -| `docs/dag_execution_order.md` | Добавить все 4 DDL DAG-а | -| `docs/reference/pxf_bookings.md` | Исправить нумерацию разделов (7→9→8→10 → последовательную) | - ---- - -## Фаза 5. Обновление индексов и заглушка assignment - -### `docs/README.md` - -Переписать структуру: разделы по каталогам (`design/`, `reference/`, `plans/`, -`archive/`, `assignment/`). Включить ранее пропущенные документы: `db_schema.md`, -`dag_execution_order.md`, `e2e-etl-test-protocol.md`, `agent-dag-testing.md`. - -### `README.md` (корень) - -Заменить ссылку на `educational-tasks.md` → `docs/assignment/`, проверить остальные. - -### `docs/assignment/README.md` (новый файл) - -Временная заглушка: -- Указание, что курсовые задания появятся в Этапе 3 -- Ссылка на эталонный срез (DAG-и STG→ODS→DDS→DM) для самостоятельного изучения -- Ссылка на `docs/design/assignment_design.md` для менторов - ---- - -## Фаза 6. Финальная проверка - -Шаги выполняются строго по порядку: - -1. [ ] `make test` — тесты проходят -2. [ ] `make lint` — стиль кода -3. [ ] Удалить пустой каталог `docs/internal/` -4. [ ] Перенести план из `docs/plans/` в `docs/archive/docs_restructuring_plan.md` -5. [ ] grep по `internal/` в живых .md файлах (`rg --glob '!docs/archive/*'`) — нет битых ссылок -6. [ ] grep по `educational-tasks` в живых .md файлах (`rg --glob '!docs/archive/*'`) — нет битых ссылок - ---- - -## Порядок выполнения - -Фазы 1→2→3 делаются вместе (иначе ссылки будут битыми). -Фаза 4 — независима, можно параллельно. -Фаза 5 — после всех перемещений. -Фаза 6 — в конце. - ---- - -## Что это даёт следующим этапам - -| Этап | Как помогает | -|------|-------------| -| **Этап 2** (подготовка main) | Чистая структура — понятно, что удалять, что оставлять | -| **Этап 3** (ТЗ от аналитика) | Готовый каталог `docs/assignment/` с заглушкой, место для `analyst_spec.md` | -| **Этап 4** (валидационный DAG) | `docs/reference/qa-plan.md` — рядом с другими справочными | -| **Этап 5** (ветка solution) | Архив отделён — не попадёт в ветку solution | diff --git a/docs/archive/2026-03-10_educational-tasks.md b/docs/archive/2026-03-10_educational-tasks.md deleted file mode 100644 index 1c78fd9..0000000 --- a/docs/archive/2026-03-10_educational-tasks.md +++ /dev/null @@ -1,91 +0,0 @@ -# Учебные задания по стенду - -Этот документ собирает в одном месте задания для менти. -Он разбит на блоки: от архитектуры Greenplum и демо‑БД bookings до реализации аналитических слоев DWH. - -Если вы только начинаете, выполняйте задания по порядку. - ---- - -## 1. Greenplum и модель данных (введение) - -В следующих заданиях мы будем опираться на демо‑БД bookings (Postgres) и слой STG в Greenplum. -На этом этапе достаточно бегло посмотреть на структуру и понять общую идею, детальная проработка пойдёт позже. - -### 1.1. Знакомство с демо‑БД bookings - -1. Прочитайте `bookings/README.md` — какие сервисы и команды относятся к демобазе. -2. Поднимите стенд и выполните: - - `make up` - - `make bookings-init` -3. Подключитесь к демобазе: - - `make bookings-psql` - - посмотрите таблицы в схеме `bookings` (например, `\dt bookings.*`). -4. Найдите таблицу `bookings.bookings` и посмотрите на её структуру: - - какие типы колонок используются; - - какие поля выглядят как ключи, даты, суммы. - -### 1.2. Знакомство с STG в Greenplum - -1. Прочитайте `sql/stg/bookings_ddl.sql` и краткое описание потока `docs/bookings_to_gp_stage.md` (если интересно — `docs/internal/bookings_stg_design.md`). -2. Ответьте себе на вопросы: - - чем внешняя таблица `stg.bookings_ext` отличается от внутренней `stg.bookings`; - - зачем нужны тех.колонки `event_ts`, `_load_ts`, `_load_id`; - - чем слой STG отличается от итоговых витрин (DDS/DM) с точки зрения моделирования. -3. Выполните `make ddl-gp`, затем зайдите в Greenplum (`make gp-psql`) и проверьте наличие схемы и таблиц: - - `\dn` и `\dt stg.*` - - `SELECT * FROM stg.bookings LIMIT 5;` (после запуска соответствующего DAG). - -### 1.3. Как генерируются учебные данные bookings - -1. Откройте файл `bookings/generate_next_day.sql` и ответьте себе на вопросы: - - с какой даты начинается генерация данных (посмотрите на GUC `bookings.start_date` и переменную `v_start_cfg`); - - сколько дней генерируется при первой установке (переменная `bookings.init_days`); - - что происходит, если таблица `bookings.bookings` уже не пустая. -2. В демобазе (`make bookings-psql`) выполните: - - `SELECT min(book_date), max(book_date) FROM bookings.bookings;` - - затем запустите `make bookings-generate-day` и повторите запрос — как изменился максимальный день? -3. Откройте `sql/src/bookings_generate_day_if_missing.sql` и обратите внимание, что: - - логическая дата запуска DAG (`{{ ds }}`) не влияет на выбор дня генерации; - - скрипт всегда смотрит на `max(book_date)` и добавляет **следующий** день (или несколько стартовых дней, если база пуста). -4. Сделайте вывод: генератор всегда «шагает» по датам вперёд от максимальной даты, поэтому: - - при `make bookings-init` вы получаете готовые данные из seed-дампа (при `make bookings-generate` генератор создаст `BOOKINGS_INIT_DAYS` дней начиная с `BOOKINGS_START_DATE`); - - при последующих вызовах (`make bookings-generate-day` или DAG) добавляется ровно один новый день. - ---- - -## 2. DAG bookings_to_gp_stage (заготовка заданий) - -Этот DAG показывает путь данных от демо‑БД bookings в Postgres до сырого слоя STG в Greenplum. -Сейчас он уже реализован как учебный пример, а в будущем вокруг него появятся отдельные задания по моделированию DWH. - -### 2.1. Что есть сейчас - -1. Откройте `airflow/dags/bookings_to_gp_stage.py`. -2. Найдите в коде ссылки на SQL‑файлы: - - `sql/src/bookings_generate_day_if_missing.sql` - - `sql/stg/bookings_load.sql` - - `sql/stg/bookings_dq.sql` -3. Соотнесите шаги DAG с документом `docs/bookings_to_gp_stage.md`: - - генерация учебного дня в `bookings.bookings`; - - загрузка инкремента в `stg.bookings`; - - проверка количества строк между источником и STG. -4. Обратите внимание, как в DAG используется логическая дата запуска: - - `{{ run_id }}` используется как `_load_id` — метка загрузки в таблице `stg.bookings` для конкретного запуска; - - сами даты данных (какие дни есть в `bookings.bookings`) определяются генератором по `max(book_date)`, а не по `ds`. - -На этом этапе достаточно понять общую цепочку. Детальные задания по переработке модели данных и построению ODS/DDS/DM слоёв будут добавлены позже. - -### 2.2. Идеи для будущих заданий (черновик) - -> Ниже — набросок задач, к которым мы вернёмся, когда базовые темы по Airflow будут освоены. - -Планируемые направления: - -- Спроектировать модель данных для основных сущностей демобазы bookings (рейсы, билеты, перелёты) в слоях ODS/DDS/DM. -- Реализовать слой ODS поверх STG, аккуратно работая с временными атрибутами и ключами. -- Построить витрины (DM) для типичных аналитических вопросов: загрузка рейсов, выручка по направлениям, динамика бронирований. -- Добавить DAG’и, которые используют `stg.bookings` как источник и строят следующие слои DWH. -- Расширить проверки качества данных для потоков bookings → STG → витрины. - -Когда будете готовы к этим темам, вернитесь к этому разделу — он станет основой для следующего «модуля» лабораторных заданий. diff --git a/docs/archive/2026-03-10_stg-naming-unification.md b/docs/archive/2026-03-10_stg-naming-unification.md deleted file mode 100644 index bca5fb9..0000000 --- a/docs/archive/2026-03-10_stg-naming-unification.md +++ /dev/null @@ -1,118 +0,0 @@ -# Унификация нейминга служебных полей в STG - -## Контекст - -В STG-слое используются legacy-имена служебных полей (`batch_id`, `load_dttm`, `src_created_at_ts`), а начиная с ODS — каноничные (`_load_id`, `_load_ts`, `event_ts`). Студент видит разные имена для одного понятия. Цель — привести STG к канону, убрав расхождение. - -> **Breaking change (dev-only).** Это ломающее переименование колонок. Миграционный шаг (ALTER TABLE … RENAME COLUMN) не предусмотрен. DDL-файлы используют `CREATE TABLE IF NOT EXISTS`, поэтому сами по себе они не пересоздадут существующие таблицы с новыми именами колонок. План предполагает заранее пересозданную среду (например, `make down && make up`) или ручной `DROP TABLE` / `DROP SCHEMA` перед `make ddl-gp`. Обратная совместимость не обеспечивается. - -## Маппинг - -| Legacy (STG сейчас) | Канон (ODS/DDS/DM) | -|---|---| -| `batch_id` | `_load_id` | -| `load_dttm` | `_load_ts` | -| `src_created_at_ts` | `event_ts` | - -### Оговорка про `event_ts` в snapshot-справочниках - -В транзакционных STG-таблицах (bookings, tickets, flights, segments, boarding_passes) поле `src_created_at_ts` действительно хранит время события из источника — переименование в `event_ts` семантически точно. - -В snapshot-справочниках (airports, airplanes, routes, seats) это поле заполняется `now()` при загрузке, т.е. по факту это ещё одно load-time, а не время события. Тем не менее мы сохраняем единое имя `event_ts` как **учебное упрощение** — ради консистентной структуры STG-таблиц. Это зафиксировано как осознанный trade-off: единообразие важнее семантической точности в справочниках. В `naming_conventions.md` нужно добавить соответствующую оговорку (раздел 4, «Time Rule»). - -## Что НЕ переименовываем - -- PL/pgSQL переменная `v_batch_id` — это локальная переменная, не колонка -- Python-функция `_resolve_stg_batch_id`, переменная `stg_batch_id`, task_id `resolve_stg_batch_id` — это Python/Airflow-идентификаторы -- XCom-ключи, ссылающиеся на task_id - -## Порядок выполнения - -### Шаг 1: STG DDL (9 файлов) - -`sql/stg/{bookings,tickets,flights,segments,airports,airplanes,routes,seats,boarding_passes}_ddl.sql` - -В каждом: `batch_id` → `_load_id`, `load_dttm` → `_load_ts`, `src_created_at_ts` → `event_ts`. - -### Шаг 2: STG Load (9 файлов) - -`sql/stg/{bookings,tickets,flights,segments,airports,airplanes,routes,seats,boarding_passes}_load.sql` - -INSERT-списки, SELECT, WHERE, комментарии — те же 3 замены. - -### Шаг 3: STG DQ (9 файлов) - -`sql/stg/{bookings,tickets,flights,segments,airports,airplanes,routes,seats,boarding_passes}_dq.sql` - -WHERE-условия (`batch_id = v_batch_id` → `_load_id = v_batch_id`), RAISE-сообщения, комментарии. - -### Шаг 4: ODS Load (9 файлов) - -Два подтипа — обрабатывать по-разному. - -#### 4a: Транзакционные таблицы (5 файлов) - -`sql/ods/{bookings,tickets,flights,segments,boarding_passes}_load.sql` - -SELECT из STG: `s.batch_id` → `s._load_id`, `s.load_dttm` → `s._load_ts`, `s.src_created_at_ts` → `s.event_ts`. Убрать лишние алиасы (`s.src_created_at_ts AS event_ts` → просто `s.event_ts`). - -#### 4b: Snapshot-справочники (4 файла) - -`sql/ods/{airports,airplanes,routes,seats}_load.sql` - -Здесь `event_ts` отсутствует в целевой ODS-таблице — менять только ссылки на STG-колонки: `s.batch_id` → `s._load_id`, `s.load_dttm` → `s._load_ts`, `s.src_created_at_ts` → `s.event_ts` (только в ORDER BY / WHERE, где они читают из STG). ODS-колонка `_load_ts` по-прежнему заполняется через `now()`, это не меняется. - -### Шаг 5: ODS DQ (9 файлов) - -`sql/ods/{bookings,tickets,flights,segments,airports,airplanes,routes,seats,boarding_passes}_dq.sql` - -`WHERE batch_id =` → `WHERE _load_id =`, RAISE-сообщения. - -### Шаг 6: DAG-файлы (2 файла) - -- `airflow/dags/bookings_to_gp_stage.py` — комментарий про `batch_id` -- `airflow/dags/bookings_to_gp_ods.py` — встроенный SQL-запрос резолвера: все `batch_id` как колонка → `_load_id`, `load_dttm` → `_load_ts`. Python-имена не трогаем. - -### Шаг 7: Тесты (3 файла) - -- `tests/test_ods_snapshot_integration.py` — inline DDL и INSERT в тестах -- `tests/test_dags_smoke.py` — комментарии -- `tests/test_ods_sql_contract.py` — docstring - -### Шаг 8: Документация (~15 файлов) - -- `docs/internal/naming_conventions.md` — убрать legacy-исключение (секция 5/STG), убрать переходный маппинг (секция 6), добавить оговорку про `event_ts` в snapshot-справочниках (секция 4) -- `docs/internal/db_schema.md` — описания STG-полей -- `docs/internal/bookings_stg_design.md` — дизайн STG -- `docs/internal/bookings_ods_design.md` — маппинг STG→ODS, SQL-примеры -- `docs/internal/qa-plan.md` — SQL-запросы проверок -- `docs/internal/architecture_review.md` — архитектурные заметки -- `docs/internal/bookings_stg_code_review.md` — код-ревью -- `docs/bookings_to_gp_stage.md` — описание STG DAG, примеры полей -- `docs/bookings_to_gp_ods.md` — описание ODS DAG -- `docs/dag_execution_order.md` — порядок выполнения DAG -- `educational-tasks.md` — учебные задания -- `README.md` — SQL-примеры в README -- `TESTING.md` — чек-лист тестирования - -### Шаг 9: Верификация - -```bash -# 1. Проверить SQL и Python — не должно быть колонок batch_id -# (допустимы только: v_batch_id, stg_batch_id, resolve_stg_batch_id) -grep -rn 'batch_id' sql/stg/ sql/ods/ airflow/dags/ tests/ --include='*.sql' --include='*.py' - -# 2. Ноль совпадений по старым именам в коде -grep -rn 'load_dttm' sql/ airflow/dags/ tests/ --include='*.sql' --include='*.py' -grep -rn 'src_created_at_ts' sql/ airflow/dags/ tests/ --include='*.sql' --include='*.py' - -# 3. Проверить документацию — не должно быть старых имён как актуальных -# (допустимы упоминания в историческом контексте) -grep -rn 'batch_id\|load_dttm\|src_created_at_ts' docs/ educational-tasks.md README.md TESTING.md - -# 4. Тесты и линтер -make test -make fmt && make lint -``` - -## Итого: ~55 файлов, ~3 механические замены в каждом diff --git a/docs/archive/2026-03-11_docs-etl-quality.md b/docs/archive/2026-03-11_docs-etl-quality.md deleted file mode 100644 index 51545da..0000000 --- a/docs/archive/2026-03-11_docs-etl-quality.md +++ /dev/null @@ -1,88 +0,0 @@ -# План: выравнивание качества документации ETL DAG'ов - -## Контекст - -Документ `bookings_to_gp_stage.md` — эталон: пошаговый разбор задач, ASCII-граф, -ссылки на SQL-файлы, описание edge cases. Три остальных документа -(`_ods`, `_dds`, `_dm`) значительно беднее. Цель — подтянуть их до того же уровня. - -## Единый шаблон секций (целевая структура) - -Каждый документ должен содержать: - -1. **Заголовок + вводный абзац** (что за DAG, какой слой, зачем) -2. **Что делает DAG** (буллеты, краткое описание) -3. **Что должно быть готово** (prerequisites) -4. **Как запустить** (UI + опциональные параметры) -5. **Граф зависимостей** (ASCII-диаграмма, не буллет-лист) -6. **Как это работает внутри (по шагам)** ← ГЛАВНОЕ ДОБАВЛЕНИЕ - - Пронумерованные шаги: task_id → SQL-файл → что делает → паттерн (SCD1/SCD2/rebuild/HWM) - - Учебные пояснения к нетривиальным паттернам -7. **Как проверить результат** (SQL-запросы) -8. **Типичные ошибки** (уже есть, оставляем) - -## Что именно добавить/исправить в каждом документе - -### A. `bookings_to_gp_ods.md` - -**Текущее состояние:** 95 строк, нет пошагового разбора, нет ASCII-графа, нет SQL-путей. - -| # | Что сделать | Детали | -|---|-------------|--------| -| A1 | ASCII-граф зависимостей | Заменить буллет-лист на диаграмму (как в stage). Показать параллельные ветки airports/airplanes, схождение на routes/seats, цепочку flights→segments→boarding_passes | -| A2 | Секция "Как это работает внутри" | 10 шагов: resolve_stg_batch_id (Python, INTERSECT-логика), затем 9 пар load→dq с указанием SQL-файлов | -| A3 | Пояснить паттерн SCD1 UPSERT | Кратко: TEMP TABLE → UPDATE (IS DISTINCT FROM) → INSERT. Одного абзаца достаточно, потом ссылка "паттерн одинаков для всех 9 таблиц" | -| A4 | Описать разницу snapshot vs HWM | Snapshot-справочники фильтруются по `stg_batch_id`; транзакционные таблицы — по HWM (`_load_ts`). Объяснить почему (чтобы не терять инкременты при повторных запусках STG) | -| A5 | Edge case: пустой батч | Для инкрементальных таблиц допустим; для snapshot — нет | - -**Ожидаемый объём:** ~140–160 строк. - -### B. `bookings_to_gp_dds.md` - -**Текущее состояние:** 73 строки — самый бедный документ. Нет пошаговости, нет ASCII-графа, SCD2 не объяснён. - -| # | Что сделать | Детали | -|---|-------------|--------| -| B1 | ASCII-граф зависимостей | calendar → параллельно 4 SCD1-измерения + dim_routes (после airports+airplanes) → fact (после всех dims) → summary | -| B2 | Секция "Как это работает внутри" | 8 шагов: calendar (rebuild), 4×SCD1-измерения, dim_routes (SCD2), fact_flight_sales, summary | -| B3 | Объяснить SCD2 для dim_routes | Учебный блок: hashdiff (MD5), закрытие старых версий, вставка новых, point-in-time valid_from. Это ключевой паттерн DDS — заслуживает 10–15 строк | -| B4 | Объяснить Phase 3 (денормализация) | Обновление SCD1-атрибутов (города, модель) во ВСЕХ версиях dim_routes. Зачем: чтобы не хранить устаревшие названия городов | -| B5 | Объяснить late-arriving dimensions | LEFT JOIN в fact_flight_sales: факт может прийти раньше справочника. 3–5 строк | -| B6 | Объяснить point-in-time join | Как факт привязывается к правильной версии SCD2-маршрута по дате рейса | -| B7 | Edge case: генерация SK | MAX() + ROW_NUMBER() безопасна только при concurrency=1 | - -**Ожидаемый объём:** ~160–180 строк. - -### C. `bookings_to_gp_dm.md` - -**Текущее состояние:** 80 строк. ASCII-граф есть (хорошо!), описание стратегий загрузки есть (хорошо!), но нет пошагового разбора с SQL-файлами. - -| # | Что сделать | Детали | -|---|-------------|--------| -| C1 | Секция "Как это работает внутри" | 5 шагов (по одному на витрину) + start_dm + finish_dm_summary. Для каждой витрины: task_id → SQL-файл → паттерн → зерно (grain) | -| C2 | Расширить описание sales_report | HWM-паттерн: какие даты пересчитываются, NULLIF-guard для boarding_rate, автоматическая "догонка" при первичной загрузке | -| C3 | Расширить описание route_performance | Почему Full Rebuild: AO Column (нет UPDATE/DELETE), таблица маленькая. 3-шаговый паттерн: TRUNCATE → агрегация по route_bk → JOIN с текущей версией SCD2 | -| C4 | Добавить grain для каждой витрины | sales_report: (flight_date, departure_airport_sk, arrival_airport_sk, tariff_sk); route_performance: route_bk; и т.д. | -| C5 | Добавить SQL-пути к задачам | Сейчас нигде не указаны пути к SQL-файлам | - -**Ожидаемый объём:** ~140–160 строк. - -### D. Мелкие правки в `bookings_to_gp_stage.md` (опционально) - -| # | Что сделать | Детали | -|---|-------------|--------| -| D1 | Убедиться в консистентности шаблона | Если в ходе работы над ODS/DDS/DM выработается чуть лучшая структура — привести stage к тому же формату (только структурные правки, контент не менять) | - -## Порядок работы - -1. **ODS** (средняя сложность, знакомый паттерн SCD1) -2. **DDS** (наибольшая учебная ценность — SCD2, late-arriving dims) -3. **DM** (наименьший объём правок — ASCII-граф уже есть) -4. **Stage** — только если нужна косметика для консистентности - -## Принципы - -- Не раздувать: целевой объём каждого документа — 140–180 строк (stage = 157). -- Учебная ценность > полнота: объяснять «почему», а не перечислять все колонки. -- SQL-пути обязательны — это главный навигационный инструмент для студента. -- Один паттерн объясняем один раз подробно, дальше ссылаемся: "паттерн аналогичен X". diff --git a/docs/archive/2026-03-11_routes-to-reference.md b/docs/archive/2026-03-11_routes-to-reference.md deleted file mode 100644 index cfd3e54..0000000 --- a/docs/archive/2026-03-11_routes-to-reference.md +++ /dev/null @@ -1,503 +0,0 @@ -# Plan: Перенос STG в эталон, ODS airplanes+seats — студенту - -> Версия: 4 (после третьего ревью ChatGPT 5.4) -> Дата: 2026-03-11 - -> **Статус выполнения (2026-03-12):** -> - Секция «Сейчас (на chore/bookings-etl)» — ✅ выполнена (п.1, п.6, п.7, п.9) -> - Секция «Этап 3 (валидационный DAG)» — ✅ выполнена (см. `docs/archive/2026-03-12_validation-dag.md`) -> - Секция «Этап 4 (подготовка main + solution)» — ⬜ не выполнена. -> Детальные спецификации кода (п.2–5) и документации (п.8, п.10–11) остаются -> актуальным справочником. Порядок выполнения и стратегия веток — -> в новом плане `docs/plans/2026-03-12_main-solution-split.md`. - -## Контекст и мотивация - -### Обнаруженная проблема - -При подготовке к Этапу 4 (раскладка по веткам main / solution) обнаружен **конфликт -между текущим дизайном БД** (`docs/design/db_schema.md`, `docs/design/bookings_dds_design.md`) -**и планом курсового задания** (`docs/design/assignment_design.md`). - -Суть конфликта: эталонный пайплайн (который должен работать «из коробки» на main-ветке -для студента) **неработоспособен**, если студент ещё не реализовал ни одного задания. -Три DDS-измерения (`dim_routes`, `dim_passengers`, `dim_airplanes`) отнесены к заданию -студента, но эталонная фактовая таблица `fact_flight_sales` зависит от них через -цепочку LEFT JOIN. Без этих измерений факт загружается с массовыми NULL в ключевых FK, -а DQ-проверки блокируют pipeline. Эталонные витрины (`sales_report`, `airport_traffic`) -становятся бесполезными. - -Проблема усугубляется архитектурой batch resolver ODS DAG и тестовыми контрактами, -которые требуют данных во **всех** snapshot-таблицах STG. - -Ниже — детальный разбор и выбранное решение. - -### Фактовая таблица: зависимость от студенческих измерений - -`fact_flight_sales` зависит от **всех 7 измерений** DDS, -включая 3 студенческих: `dim_routes`, `dim_passengers`, `dim_airplanes`. - -Критическая цепочка: аэропорты в факте разрешаются **через `dim_routes`**: - -``` -flt.route_no → dim_routes.route_bk → rte.departure_airport → dim_airports.airport_bk → airport_sk -``` - -Если `dim_routes` пуст (студент ещё не реализовал), то **NULL** получают не только -`route_sk`, но и оба `airport_sk` и `airplane_sk`. Эталонная витрина `sales_report` -становится бесполезной. - -Причина: `ods.flights` содержит только `route_no`, но не `departure_airport` / -`arrival_airport` / `aircraft_code`. Эти поля доступны только через таблицу `routes`. - -### Почему нельзя просто использовать DDL-заглушки - -Если `dim_routes` — пустая заглушка, все LEFT JOIN через неё возвращают NULL. -Факт загружается, но: - -- `departure_airport_sk = NULL` → `sales_report` не может группировать по аэропортам -- `arrival_airport_sk = NULL` → `airport_traffic` не может считать трафик -- UPDATE факта **не перезаписывает SK** (by design, строка 4-5 load-скрипта) → - NULL-и остаются навсегда до TRUNCATE + перезагрузки - -### Дополнительные архитектурные ограничения (из ревью) - -1. **Fact DQ блокирует загрузку** (`fact_flight_sales_dq.sql`): - - Строка 71: `passenger_sk IS NULL` → **hard fail** (100% NULL → exception) - - Строка 98: `route_sk / airplane_sk IS NULL` → при >1% строк → **exception** - - На main с пустыми студенческими dims — 100% NULL → pipeline падает - -2. **Batch resolver ODS DAG** (`bookings_to_gp_ods.py:59-77`): - - `INTERSECT` по `stg.airports`, `stg.airplanes`, `stg.routes`, `stg.seats` - - Если **любая** STG-таблица пуста — общего batch нет → весь ODS pipeline падает - - Контракт зафиксирован тестом `test_ods_sql_contract.py:30` - -### Выбранное решение - -**Весь STG-слой — эталонный** (все 9 таблиц загружаются reference-кодом). -Это гарантирует: -- Batch resolver всегда находит согласованный batch (все 4 snapshot-таблицы заполнены) -- DAG-зависимости и smoke-тесты STG не требуют изменений -- Никакого расхождения batch-контракта между main и solution - -**ODS: `airplanes` + `seats` остаются заданием студента** (TRUNCATE+INSERT практика). - -**DDS: `dim_airplanes`, `dim_passengers`, `dim_routes`** — задание студента (DDL-заглушки). - -**Fact lookup**: аэропорты через `ods.routes` (эталон); `airplane_sk` остаётся -point-in-time через `dim_routes` (NULL на main — допустимо). - -**Fact DQ**: на main ослабить проверки студенческих SK. - -### Что теряет студент (и почему это приемлемо) - -| Потеря | Компенсация | -|--------|-------------| -| STG-практика (PXF external tables) | Изучает эталонный STG-код; отдельный PXF-практикум в бэклоге (`assignment_design.md`, раздел 6) | -| `ods.routes` (TRUNCATE+INSERT) | Остаются `ods.airplanes` + `ods.seats` — тот же паттерн | - -Ключевые элементы задания **сохранены**: -- ODS: 2 таблицы (airplanes, seats) — практика TRUNCATE+INSERT -- DDS: `dim_airplanes` (SCD1), `dim_passengers` (SCD1), **`dim_routes` (SCD2)** — ключевой вызов -- DM: 4 витрины разной сложности (от простой к сложной) - -### Пересчёт факта после реализации студентом - -После реализации всех измерений студенту нужно: -1. Запустить загрузку DDS-измерений (dim_airplanes, dim_passengers, dim_routes) -2. `TRUNCATE dds.fact_flight_sales;` -3. Перезапустить загрузку факта → теперь все SK заполнены -4. Перезапустить DM-витрины - -Это стандартная практика при late-arriving dimensions и хорошая обучающая точка. - -### airport_traffic — студенческое задание - -`airport_traffic` архитектурно не зависит от студенческих dims (использует только -`calendar_sk`, `departure_airport_sk`, `arrival_airport_sk`). Но это **не значит**, -что витрина готова: SQL витрины пишет студент. Данные в факте есть (эталон), -студент пишет агрегирующий SQL. Статус: **задание студента** (как и остальные 3 DM). - ---- - -## Матрица зависимостей DM-витрин - -| DM витрина | calendar | airports | tariffs | routes | passengers | airplanes | Работает без студ. dims? | -|---|---|---|---|---|---|---|---| -| `sales_report` (эталон) | + | + | + | — | — | — | **ДА** | -| `airport_traffic` (студент) | + | + | — | — | — | — | Данные есть, SQL пишет студент | -| `route_performance` (студент) | + | — | — | **+** | — | — | Нет | -| `monthly_overview` (студент) | + | — | — | **+** | **+** | **+** | Нет | -| `passenger_loyalty` (студент) | + | — | + | **+** | **+** | — | Нет | - ---- - -## Изменения (код) - -### 1. `sql/dds/fact_flight_sales_load.sql` — lookup аэропортов через ods.routes - -**Ветка:** chore/bookings-etl (сейчас) + main (Этап 4) - -**Текущее состояние** (строки 51-62): -```sql -LEFT JOIN dds.dim_routes AS rte - ON rte.route_bk = flt.route_no - AND flt.scheduled_departure::DATE >= rte.valid_from - AND (rte.valid_to IS NULL OR flt.scheduled_departure::DATE < rte.valid_to) -... -LEFT JOIN dds.dim_airports AS dep - ON dep.airport_bk = rte.departure_airport -- через dim_routes! -LEFT JOIN dds.dim_airports AS arr - ON arr.airport_bk = rte.arrival_airport -- через dim_routes! -LEFT JOIN dds.dim_airplanes AS ap - ON ap.airplane_bk = rte.airplane_code -- через dim_routes! -``` - -**После изменения:** -```sql --- Учебный комментарий: Airport lookup через ods.routes (эталонный справочник), --- а не через dds.dim_routes (студенческое задание SCD2). --- Это архитектурное решение: эталонный пайплайн работает независимо от студенческого кода. --- ROW_NUMBER по validity DESC: выбираем актуальную версию расписания маршрута. --- Аэропорты вылета/прилёта одинаковы во всех версиях одного route_no. -LEFT JOIN ( - SELECT route_no, departure_airport, arrival_airport - FROM ( - SELECT route_no, departure_airport, arrival_airport, - ROW_NUMBER() OVER (PARTITION BY route_no ORDER BY validity DESC) AS rn - FROM ods.routes - ) ranked - WHERE rn = 1 -) AS ods_rte ON ods_rte.route_no = flt.route_no -LEFT JOIN dds.dim_routes AS rte - ON rte.route_bk = flt.route_no - AND flt.scheduled_departure::DATE >= rte.valid_from - AND (rte.valid_to IS NULL OR flt.scheduled_departure::DATE < rte.valid_to) -... -LEFT JOIN dds.dim_airports AS dep - ON dep.airport_bk = ods_rte.departure_airport -- через ods.routes (эталон) -LEFT JOIN dds.dim_airports AS arr - ON arr.airport_bk = ods_rte.arrival_airport -- через ods.routes (эталон) -LEFT JOIN dds.dim_airplanes AS ap - ON ap.airplane_bk = rte.airplane_code -- через dim_routes (point-in-time, как прежде) -``` - -**Почему аэропорты через ods.routes, а airplane через dim_routes:** - -- **Аэропорты**: `departure_airport` и `arrival_airport` одинаковы во всех версиях - одного `route_no` (маршрут SVO→LED всегда SVO→LED). Безопасно брать из ODS. - Это даёт эталонным витринам (`sales_report`, `airport_traffic`) корректные `airport_sk` - даже без студенческого `dim_routes`. - -- **Самолёт**: `airplane_code` теоретически может отличаться между версиями маршрута. - Текущий контракт DDS (`bookings_dds_design.md:499`) фиксирует, что `airplane_sk` - приходит из **той же point-in-time версии** маршрута, что и `route_sk`. - Менять эту семантику — изменение модели данных, а не стабилизация. - На main `airplane_sk` будет NULL (dim_routes — заглушка). Это допустимо: - ни `sales_report`, ни `airport_traffic` не используют `airplane_sk`. - -### 2. `sql/dds/fact_flight_sales_dq.sql` — ослабить проверки студенческих SK (только main) - -**Ветка:** только main (Этап 4). На solution-ветке полные проверки сохраняются. - -Изменения: - -- **Строки 65-75** (`passenger_sk IS NULL`): заменить `RAISE EXCEPTION` на `RAISE NOTICE`. - На main `dim_passengers` — заглушка, 100% строк будут с `passenger_sk IS NULL`. - Любой порог (даже >1%) даст exception. **Только логирование, без блокировки.** - -- **Строки 89-108** (route-related FK): разделить на два блока: - - **Блок A — эталонные FK (проверка с порогом 1%, как calendar_sk):** - ```sql - -- departure_airport_sk и arrival_airport_sk заполняются через ods.routes (эталон). - -- NULL здесь — аномалия данных (пропущен маршрут в ODS), а не отсутствие студенческого кода. - -- Порог 1% — защита от единичных аномалий источника (аналогично calendar_sk). - SELECT COUNT(*) INTO v_null_airport - FROM dds.fact_flight_sales - WHERE departure_airport_sk IS NULL OR arrival_airport_sk IS NULL; - - IF v_null_airport > 0 THEN - IF v_null_airport * 100.0 / NULLIF(v_row_count, 0) > 1.0 THEN - RAISE EXCEPTION 'DQ FAILED: NULL airport_sk: % (>1%%)', v_null_airport; - ELSE - RAISE NOTICE 'DQ WARNING: NULL airport_sk: % (<=1%%, допустимо)', v_null_airport; - END IF; - END IF; - ``` - - **Блок B — студенческие FK (только логирование, RAISE NOTICE):** - ```sql - -- route_sk и airplane_sk будут NULL, пока студент не реализует dim_routes. - -- Не блокируем pipeline. - SELECT COUNT(*) INTO v_null_student - FROM dds.fact_flight_sales - WHERE route_sk IS NULL OR airplane_sk IS NULL; - - IF v_null_student > 0 THEN - RAISE NOTICE 'DQ INFO: студенческие SK (route/airplane) NULL: %. ' - 'После реализации dim_routes: TRUNCATE fact → перезагрузка.', - v_null_student; - END IF; - ``` - -Итоговое правило на main: -- `departure_airport_sk`, `arrival_airport_sk` — **проверка с порогом 1%** (эталон, через ods.routes; >1% → EXCEPTION, <=1% → NOTICE) -- `passenger_sk`, `route_sk`, `airplane_sk` — **только RAISE NOTICE** (студенческие заглушки, 100% NULL допустимо) - -```sql --- Учебный комментарий: route_sk, airplane_sk и passenger_sk будут NULL, --- пока вы не реализуете соответствующие DDS-измерения. --- После реализации: TRUNCATE dds.fact_flight_sales → перезагрузка → все SK заполнены. --- Полную версию DQ (с блокировкой) см. в ветке solution. -``` - -### 3. ODS Routes DQ: убрать RI-проверку airplane_code (только main) - -**Ветка:** только main (Этап 4). На solution-ветке полные проверки сохраняются. - -> **Почему только ODS, не STG?** Весь STG — эталон, `stg.airplanes` всегда заполнен, -> `stg/routes_dq.sql` RI-проверка к `stg.airplanes` проходит штатно. Менять STG не нужно. -> На уровне ODS `ods.airplanes` — студенческая заглушка (пустая), поэтому -> `ods/routes_dq.sql` RI-проверка к `ods.airplanes` упадёт. - -**Файл:** -- `sql/ods/routes_dq.sql` — удалить блок строк 143-156 (RI к `ods.airplanes`) - -Добавить комментарий: -```sql --- Учебный комментарий: проверка RI airplane_code → ods.airplanes не выполняется, --- т.к. таблица ods.airplanes реализуется студентом. После реализации — раскомментируйте --- (см. ветку solution для полной версии). -``` - -### 4. ODS DAG-зависимость: убрать airplanes → routes (только main) - -**Ветка:** только main (Этап 4) - -> **Почему только ODS, не STG?** STG DAG не меняется: `stg.airplanes` — эталон, -> барьер `check_airplanes_dq >> load_routes_to_stg` работает штатно. -> На уровне ODS `dq_ods_airplanes` — заглушка (`SELECT 1;`), формально проходит, -> но зависимость вводит в заблуждение. Убираем для ясности. - -`airflow/dags/bookings_to_gp_ods.py` (строка 266): -```python -# Было: -[dq_ods_airports, dq_ods_airplanes] >> load_ods_routes -# Стало: -dq_ods_airports >> load_ods_routes -``` - -### 5. Заглушки для студенческих файлов (только main) - -**Ветка:** только main (Этап 4) - -#### STG-слой: весь код остаётся (эталон), заглушки не нужны - -Все 9 STG-таблиц загружаются эталонным кодом. Batch resolver работает без изменений. - -#### ODS-слой: заглушки для airplanes + seats - -| Файл | main (студент) | solution | -|------|----------------|----------| -| `sql/ods/airplanes_ddl.sql` | Полный DDL (таблица нужна) | Тот же | -| `sql/ods/airplanes_load.sql` | Заглушка: `SELECT 1; -- TODO` | Полная реализация | -| `sql/ods/airplanes_dq.sql` | Заглушка: `SELECT 1;` | Полная реализация | -| `sql/ods/seats_ddl.sql` | Полный DDL | Тот же | -| `sql/ods/seats_load.sql` | Заглушка | Полная реализация | -| `sql/ods/seats_dq.sql` | Заглушка | Полная реализация | - -#### DDS-слой: заглушки для 3 измерений - -| Файл | main (студент) | solution | -|------|----------------|----------| -| `sql/dds/dim_routes_ddl.sql` | Полный DDL (нужен для LEFT JOIN) | Тот же | -| `sql/dds/dim_routes_load.sql` | Заглушка: `SELECT 1; -- TODO: реализуйте SCD2` | Полная реализация | -| `sql/dds/dim_routes_dq.sql` | Заглушка: `SELECT 1;` | Полная реализация | -| `sql/dds/dim_passengers_ddl.sql` | Полный DDL | Тот же | -| `sql/dds/dim_passengers_load.sql` | Заглушка | Полная реализация | -| `sql/dds/dim_passengers_dq.sql` | Заглушка | Полная реализация | -| `sql/dds/dim_airplanes_ddl.sql` | Полный DDL | Тот же | -| `sql/dds/dim_airplanes_load.sql` | Заглушка | Полная реализация | -| `sql/dds/dim_airplanes_dq.sql` | Заглушка | Полная реализация | - -#### DM-слой: заглушки для 4 витрин - -Аналогично: DDL остаётся, load/dq заменяются заглушками. -Файлы: `airport_traffic`, `route_performance`, `monthly_overview`, `passenger_loyalty`. - ---- - -## Изменения (документация) - -### 6. `docs/assignment/analyst_spec.md` - -- **Удалить** разделы 1.1-1.3 (все STG-задания) — весь STG теперь эталон -- **Удалить** разделы 2.3 (`ods.routes`) — routes теперь эталон -- **Обновить** рекомендуемый порядок: начинается с ODS (airplanes, seats) -- **Добавить** мотивационную секцию «Почему STG уже реализован»: - - Эталонный пайплайн требует согласованного batch по всем snapshot-таблицам - - Без данных в STG невозможна загрузка ODS и далее по цепочке - - Студент изучает эталонный STG-код как образец -- **Добавить** секцию «Пересчёт факта после реализации измерений»: - - Инструкция: TRUNCATE fact + re-run после реализации всех DDS-измерений - - Педагогическая ценность: late-arriving dimensions, dependency management - -### 7. `docs/design/assignment_design.md` - -- **Обновить** таблицу «Эталонные таблицы»: добавить весь STG + ods.routes -- **Обновить** таблицу «Задание студенту»: убрать STG целиком, ODS оставить airplanes+seats -- **Обновить** рекомендуемый порядок: начинается с ODS -- **Обновить** структуру валидационного DAG (раздел 4): убрать `validate_stg` секцию - -### 8. `docs/design/bookings_dds_design.md` - -- **Строки 463, 468** (SQL-скелет факта): обновить JOIN-блок — - `dep.airport_bk` и `arr.airport_bk` теперь через `ods_rte`, а не через `rte`; - `ap.airplane_bk` остаётся через `rte` (point-in-time) -- **Строка 499-501** (текстовое пояснение): обновить — - `departure_airport_sk` и `arrival_airport_sk` берутся из `ods.routes` (эталон); - `airplane_sk` остаётся point-in-time через `dim_routes` -- **Строка 558** (passenger_sk = 0): убрать утверждение о нулевой толерантности — - на main `passenger_sk` будет 100% NULL (dim_passengers — заглушка) -- **Строка 559** (DQ текстовое описание): обновить — на main student SK - (`passenger_sk`, `route_sk`, `airplane_sk`) не блокируют pipeline; - `airport_sk` проверяются с порогом 1% -- **Строка 741** (DQ SQL-пример): обновить встроенный SQL — разделить route-related - блок на эталонный (airport_sk, порог 1%) и студенческий (route_sk, airplane_sk, NOTICE) - -### 9. `docs/design/db_schema.md` - -- Пометить весь STG и `ods.routes` как эталонные (не студенческие) -- Строка 3: «Все слои реализованы» — уточнить, что на main ODS airplanes/seats, - DDS student dims и DM student vitrines — заглушки -- Строка 253: DQ-контракт «SQL-скрипты с RAISE EXCEPTION» — добавить оговорку, - что на main студенческие DQ-скрипты являются заглушками - -### 10. Тесты - -- `tests/test_dags_smoke.py` строки 242-243: - На main убрать assert барьера `dq_ods_airplanes → dq_ods_routes` (ODS routes - не зависит от ODS airplanes на main). STG-барьеры (строки 123-124) **не трогаем**: - STG целиком эталонный, барьер `check_airplanes_dq → check_routes_dq` работает. -- `tests/test_ods_sql_contract.py` строка 6: - На main изменить `SNAPSHOT_ENTITIES` — исключить студенческие ODS: - ```python - # Было: - SNAPSHOT_ENTITIES = ("airports", "airplanes", "routes", "seats") - # Стало (main): - SNAPSHOT_ENTITIES = ("airports", "routes") - ``` - Тесты `test_snapshot_load_scripts_use_truncate` (строка 13) и - `test_snapshot_dq_checks_extra_keys` (строка 22) проверяют паттерны TRUNCATE+INSERT - и `v_extra_keys_count` в SQL-файлах. Заглушки `SELECT 1;` не содержат этих паттернов → - тесты упадут для airplanes/seats. Исключение из `SNAPSHOT_ENTITIES` решает проблему. - Batch resolver тест (строка 30) проверяет DAG-код (не SQL-файлы) — без изменений. - -### 11. Прочие документы - -- `docs/design/PRD.md` — обновить чеклист (раздел 10) -- `docs/design/bookings_ods_design.md` — комплексное обновление: - - Строка 29, 515, 534: пометить `airplanes` и `seats` как студенческие - (на main — заглушки; STG-уровень — эталон, ODS load/dq — студент) - - Строки 521, 523: скорректировать общий DQ-контракт — на main `airplanes_dq.sql` - и `seats_dq.sql` являются заглушками, а не полноценными RAISE EXCEPTION скриптами - - Строка 620: обновить DAG-граф — routes больше не зависит от airplanes на ODS - - Строка 634: убрать зависимость routes от airplanes в описании FK -- `docs/bookings_to_gp_ods.md` — обновить описание -- `docs/bookings_to_gp_dds.md`: - - Строки 111-113: убрать гарантию отсутствия NULL SK в штатном режиме — - на main student SK (`passenger_sk`, `route_sk`, `airplane_sk`) будут NULL - - Строка 116: обновить DQ-семантику — student SK не блокируют (NOTICE); - airport_sk — порог 1% - - Строка 126: обновить описание lookup (airports через ods.routes) -- `docs/reference/qa-plan.md`: - - Строка 80: «ODS: все 9 таблиц не пустые» — на main `ods.airplanes` и `ods.seats` - будут пустыми by design (студенческие заглушки). Уточнить формулировку. -- `docs/e2e-etl-test-protocol.md` — обновить контекст - ---- - -## Порядок выполнения - -### Сейчас (на chore/bookings-etl) - -1. **Код**: изменить `fact_flight_sales_load.sql` (lookup через ods.routes) — п.1 -2. **Документация**: обновить `analyst_spec.md` — п.6 -3. **Документация**: обновить `assignment_design.md` — п.7 -4. **Документация**: обновить `db_schema.md` — п.9 -5. **Тестирование**: `make test` + ручная проверка SQL -6. **Коммит** - -### Этап 3 (валидационный DAG) - -- Проектировать с учётом DDL-заглушек -- `validate_stg` секцию **не делать** (STG — эталон) -- `check_dim_routes_scd2` — активный тест (мутация ODS → проверка → откат) - -### Этап 4 (подготовка main + solution) - -- Fact DQ: ослабить проверки student SK (main only) — п.2 -- Routes DQ: убрать airplane RI (main only) — п.3 -- DAG-зависимости: убрать airplanes → routes (main only) — п.4 -- Заглушки: ODS (airplanes, seats) + DDS (3 dims) + DM (4 vitrines) — п.5 -- Тесты: обновить smoke-тесты (main only) — п.10 -- Документация: bookings_dds_design, PRD, прочее — п.8, п.11 - ---- - -## Верификация - -### После п.1 (fact_flight_sales fix) - -```bash -make test # smoke-тесты DAG -``` - -Ручная проверка (при поднятом стенде): - -> **Важно:** UPDATE факта не перезаписывает dimension SK (by design, строка 4-5). -> Уже загруженные строки сохранят старые значения `airport_sk` (через dim_routes). -> Для полной проверки новой логики нужен **TRUNCATE + перезагрузка**: - -```sql -TRUNCATE dds.fact_flight_sales; --- Перезапустить DDS DAG (или только task load_dds_fact_flight_sales) - --- Проверка: airport_sk заполнены (через ods.routes), route_sk тоже (dim_routes заполнен на этой ветке) -SELECT - COUNT(*) AS total, - COUNT(departure_airport_sk) AS has_dep_sk, - COUNT(arrival_airport_sk) AS has_arr_sk, - COUNT(route_sk) AS has_route_sk -FROM dds.fact_flight_sales; -``` - -### После Этапа 4 (main branch) - -1. `make up` → `make ddl-gp` → запустить STG+ODS+DDS DAG-и -2. Проверить: `sales_report` содержит данные с корректными аэропортами -3. Проверить: студенческие ODS/DDS-таблицы пусты (заглушки) -4. Fact DQ проходит (student SK = NULL, но DQ не блокирует) -5. Реализовать один студенческий dim → TRUNCATE fact → re-run → проверить SK - ---- - -## Ключевые файлы - -| Файл | Роль | Ветка | -|------|------|-------| -| `sql/dds/fact_flight_sales_load.sql` | Главное изменение: airport lookup через ods.routes | обе | -| `sql/dds/fact_flight_sales_dq.sql` | Student SK: EXCEPTION → NOTICE | main only | -| `sql/ods/routes_dq.sql` | Убрать RI airplane_code → ods.airplanes | main only | -| `airflow/dags/bookings_to_gp_ods.py` | DAG: убрать airplanes → routes | main only | -| `tests/test_ods_sql_contract.py` | SNAPSHOT_ENTITIES: убрать airplanes, seats | main only | -| `tests/test_dags_smoke.py` | ODS-барьер airplanes → routes | main only | -| `docs/assignment/analyst_spec.md` | Убрать STG-задания, убрать ods.routes | обе | -| `docs/design/assignment_design.md` | Обновить таблицы эталон/задание | обе | -| `docs/design/bookings_dds_design.md` | Обновить описание fact lookup | обе | -| `docs/design/bookings_ods_design.md` | Убрать зависимость routes от airplanes | main only | -| `docs/design/db_schema.md` | Пометить STG + ods.routes как эталон | обе | - -> **STG не трогаем**: `sql/stg/routes_dq.sql`, `bookings_to_gp_stage.py`, -> STG smoke-тесты — без изменений. Весь STG эталонный, зависимости работают штатно. diff --git a/docs/archive/2026-03-12_validation-dag.md b/docs/archive/2026-03-12_validation-dag.md deleted file mode 100644 index f1e8f5f..0000000 --- a/docs/archive/2026-03-12_validation-dag.md +++ /dev/null @@ -1,999 +0,0 @@ -# План: Валидационный DAG `bookings_validate` - -## Контекст и мотивация - -### Проблема - -Студент реализует 9 объектов (2 ODS, 3 DDS-измерения, 4 DM-витрины) и **не имеет -автоматической обратной связи** — правильно ли работает его код. Сейчас единственный -способ проверки — ручные SQL-запросы и визуальный контроль данных в таблицах. - -Это приводит к типичным ошибкам, которые студент не замечает: - -| Ошибка | Где проявляется | Когда обнаруживается | -|--------|-----------------|----------------------| -| NULL в PK | ODS/DDS | Только при построении витрины (неожиданные NULL в агрегатах) | -| Дубли по BK | DDS dim_passengers | Факт раздувается, витрины считают неверно | -| SCD2 не закрывает версии | DDS dim_routes | `valid_to` всегда NULL → point-in-time JOIN перестаёт работать | -| «Дыры» в SCD2-интервалах | DDS dim_routes | `sales_report` теряет строки за «пустые» даты | -| TRUNCATE+INSERT не вытащил все строки | ODS airplanes/seats | DDS-измерение неполное → NULL в факте | -| Витрина пуста при непустом источнике | DM | Видно сразу, но причина непонятна | - -Без валидационного DAG студент узнаёт о проблеме **через 2-3 слоя** — на этапе DM-витрин, -где отладка многократно сложнее. - -### Решение - -**Отдельный DAG `bookings_validate`** — набор проверок, сгруппированных по слоям. -Студент запускает его вручную (trigger в Airflow UI) после реализации заданий. -Каждый таск — одна проверка, с дружелюбным сообщением об ошибке и подсказкой, -что делать дальше. - -### Педагогическая ценность - -1. **Практика чтения логов Airflow** — студент учится находить ошибки в логах тасков -2. **Инкрементальная обратная связь** — можно запускать после каждого шага, не дожидаясь - реализации всех заданий -3. **Паттерн DQ в пайплайне** — студент видит, как устроены production-проверки качества данных -4. **Активный тест SCD2** — единственный способ убедиться, что SCD2-логика действительно - работает (справочник `routes` в демо-базе статичен) - -### Дизайн-решения - -| Решение | Обоснование | -|---------|-------------| -| Отдельный DAG (не часть основного пайплайна) | Запускается по желанию, не блокирует основную загрузку | -| `schedule=None` (ручной триггер) | Студент запускает, когда готов проверить свой код | -| `PostgresOperator` + SQL-скрипты | Единый паттерн с основными DAG-ами | -| SQL в `sql/validate/` | Отдельный каталог — валидация не смешивается с DQ пайплайна | -| `DO $$ ... RAISE EXCEPTION ... $$` | Тот же паттерн, что в эталонных DQ-скриптах | -| TaskGroup по слоям | Студент видит, на каком слое проблема | -| Проверки «существует + не пуста» для DM | Минимально необходимо; бизнес-инварианты студент добавит сам в DQ витрин | - ---- - -## Структура DAG - -``` -bookings_validate -├── validate_ods (TaskGroup) -│ ├── check_ods_airplanes_rowcount -│ ├── check_ods_seats_rowcount -│ ├── check_ods_no_dup_bk -│ └── check_ods_no_null_pks -├── validate_dds (TaskGroup) -│ ├── check_dim_airplanes_exists -│ ├── check_dim_passengers_exists -│ ├── check_dim_passengers_no_dup_bk -│ ├── check_dim_routes_exists -│ ├── check_dim_routes_scd2 ← активный тест! -│ └── check_dim_routes_no_gaps -└── validate_dm (TaskGroup) - ├── check_airport_traffic_exists - ├── check_route_performance_exists - ├── check_monthly_overview_exists - └── check_passenger_loyalty_exists -``` - -### Зависимости между группами - -Нет жёстких зависимостей. Все три группы запускаются параллельно — студент может -реализовать только ODS и увидеть зелёные чеки для `validate_ods`, пока DDS/DM ещё -красные. Это даёт инкрементальную обратную связь. - -Внутри каждой группы таски также параллельны (независимые проверки). - ---- - -## Детализация проверок - -### ODS: `check_ods_airplanes_rowcount` - -**Файл:** `sql/validate/ods_airplanes_rowcount.sql` - -**Логика:** ODS загружает один конкретный snapshot-батч из STG (по `_load_id`). -STG — append-only: содержит историю всех загрузок. Поэтому сравнивать ODS -со всей STG некорректно — нужно проверять точное множество BK для того батча, -который ODS фактически загрузил. - -Определяем батч: `_load_id` из `ods.airplanes` (единственный, т.к. TRUNCATE+INSERT). -Затем проверяем, что множество BK в ODS = множество BK в STG для этого батча. - -```sql -DO $$ -DECLARE - v_batch_count BIGINT; - v_batch TEXT; - v_ods_count BIGINT; - v_missing_in_ods BIGINT; - v_extra_in_ods BIGINT; -BEGIN - -- ODS пуста? - SELECT COUNT(*) INTO v_ods_count FROM ods.airplanes; - IF v_ods_count = 0 THEN - RAISE EXCEPTION 'FAILED: ods.airplanes пуста. Реализуйте загрузку: sql/ods/airplanes_load.sql'; - END IF; - - -- Инвариант: ODS после TRUNCATE+INSERT содержит ровно один _load_id - SELECT COUNT(DISTINCT _load_id) INTO v_batch_count FROM ods.airplanes; - IF v_batch_count <> 1 THEN - RAISE EXCEPTION 'FAILED: ods.airplanes содержит % разных _load_id (ожидается 1 после TRUNCATE+INSERT). Проверьте, что load начинается с TRUNCATE.', v_batch_count; - END IF; - - -- Определяем батч, из которого загружена ODS - SELECT DISTINCT _load_id INTO v_batch FROM ods.airplanes; - - -- BK есть в STG-батче, но нет в ODS (потеряны при загрузке) - SELECT COUNT(*) INTO v_missing_in_ods - FROM ( - SELECT DISTINCT airplane_code FROM stg.airplanes WHERE _load_id = v_batch - ) AS stg_bk - WHERE NOT EXISTS ( - SELECT 1 FROM ods.airplanes AS o WHERE o.airplane_code = stg_bk.airplane_code - ); - - IF v_missing_in_ods > 0 THEN - RAISE EXCEPTION 'FAILED: % самолётов из STG-батча (%) отсутствуют в ods.airplanes. Проверьте логику TRUNCATE+INSERT.', v_missing_in_ods, v_batch; - END IF; - - -- BK есть в ODS, но нет в STG-батче (откуда взялись?) - SELECT COUNT(*) INTO v_extra_in_ods - FROM ( - SELECT DISTINCT airplane_code FROM ods.airplanes - ) AS ods_bk - WHERE NOT EXISTS ( - SELECT 1 FROM stg.airplanes AS s WHERE s._load_id = v_batch AND s.airplane_code = ods_bk.airplane_code - ); - - IF v_extra_in_ods > 0 THEN - RAISE EXCEPTION 'FAILED: % самолётов в ods.airplanes отсутствуют в STG-батче (%). Возможно, TRUNCATE не выполнился перед INSERT.', v_extra_in_ods, v_batch; - END IF; - - RAISE NOTICE 'PASSED: ods.airplanes содержит % самолётов, множество BK = STG-батч %', v_ods_count, v_batch; -END $$; -``` - -### ODS: `check_ods_seats_rowcount` - -**Файл:** `sql/validate/ods_seats_rowcount.sql` - -Аналогичная проверка по составному ключу `(airplane_code, seat_no)` — -точное множество BK из STG-батча (по `_load_id` из `ods.seats`). - -### ODS: `check_ods_no_dup_bk` - -**Файл:** `sql/validate/ods_no_dup_bk.sql` - -**Логика:** Проверить, что в ODS нет дублей по бизнес-ключу. Типичная ошибка -студента — INSERT без предшествующего TRUNCATE, или TRUNCATE забыт при повторном -запуске. Дубли по BK в ODS каскадно ломают DDS (лишние строки в измерениях). - -```sql -DO $$ -DECLARE - v_dup_airplanes BIGINT; - v_dup_seats BIGINT; -BEGIN - -- airplanes: BK = airplane_code - SELECT COUNT(*) INTO v_dup_airplanes - FROM ( - SELECT airplane_code FROM ods.airplanes - GROUP BY airplane_code HAVING COUNT(*) > 1 - ) AS d; - - IF v_dup_airplanes > 0 THEN - RAISE EXCEPTION 'FAILED: ods.airplanes содержит % дублирующихся airplane_code. Проверьте, что load начинается с TRUNCATE.', v_dup_airplanes; - END IF; - - -- seats: BK = (airplane_code, seat_no) - SELECT COUNT(*) INTO v_dup_seats - FROM ( - SELECT airplane_code, seat_no FROM ods.seats - GROUP BY airplane_code, seat_no HAVING COUNT(*) > 1 - ) AS d; - - IF v_dup_seats > 0 THEN - RAISE EXCEPTION 'FAILED: ods.seats содержит % дублирующихся (airplane_code, seat_no). Проверьте, что load начинается с TRUNCATE.', v_dup_seats; - END IF; - - RAISE NOTICE 'PASSED: Нет дублей по BK в ods.airplanes и ods.seats'; -END $$; -``` - -### ODS: `check_ods_no_null_pks` - -**Файл:** `sql/validate/ods_no_null_pks.sql` - -**Логика:** Проверить, что PK-поля не содержат NULL в обеих студенческих ODS-таблицах. - -```sql -DO $$ -DECLARE - v_null_airplanes BIGINT; - v_null_seats BIGINT; -BEGIN - -- airplanes: PK = airplane_code - SELECT COUNT(*) INTO v_null_airplanes - FROM ods.airplanes WHERE airplane_code IS NULL; - - IF v_null_airplanes > 0 THEN - RAISE EXCEPTION 'FAILED: ods.airplanes содержит % строк с NULL в airplane_code.', v_null_airplanes; - END IF; - - -- seats: PK = (airplane_code, seat_no) - SELECT COUNT(*) INTO v_null_seats - FROM ods.seats WHERE airplane_code IS NULL OR seat_no IS NULL; - - IF v_null_seats > 0 THEN - RAISE EXCEPTION 'FAILED: ods.seats содержит % строк с NULL в PK (airplane_code, seat_no).', v_null_seats; - END IF; - - RAISE NOTICE 'PASSED: PK не содержат NULL в ods.airplanes и ods.seats'; -END $$; -``` - ---- - -### DDS: `check_dim_airplanes_exists` - -**Файл:** `sql/validate/dim_airplanes_exists.sql` - -**Логика:** -1. Таблица не пуста -2. Нет дублей по `airplane_bk` -3. Покрытие ODS: все `airplane_code` из `ods.airplanes` есть в `dds.dim_airplanes` - -```sql -DO $$ -DECLARE - v_count BIGINT; - v_dup BIGINT; - v_missing BIGINT; -BEGIN - SELECT COUNT(*) INTO v_count FROM dds.dim_airplanes; - IF v_count = 0 THEN - RAISE EXCEPTION 'FAILED: dds.dim_airplanes пуста. Реализуйте загрузку: sql/dds/dim_airplanes_load.sql'; - END IF; - - SELECT COUNT(*) - COUNT(DISTINCT airplane_bk) INTO v_dup FROM dds.dim_airplanes; - IF v_dup > 0 THEN - RAISE EXCEPTION 'FAILED: dds.dim_airplanes содержит % дублей по airplane_bk. Проверьте SCD1-логику (UPSERT).', v_dup; - END IF; - - SELECT COUNT(*) INTO v_missing - FROM (SELECT DISTINCT airplane_code FROM ods.airplanes) AS s - WHERE NOT EXISTS ( - SELECT 1 FROM dds.dim_airplanes AS d WHERE d.airplane_bk = s.airplane_code - ); - IF v_missing > 0 THEN - RAISE EXCEPTION 'FAILED: % самолётов из ods.airplanes отсутствуют в dds.dim_airplanes.', v_missing; - END IF; - - RAISE NOTICE 'PASSED: dds.dim_airplanes содержит % строк, покрывает все BK из ODS', v_count; -END $$; -``` - -### DDS: `check_dim_passengers_exists` - -**Файл:** `sql/validate/dim_passengers_exists.sql` - -Аналогичная структура: не пуста + нет дублей по `passenger_id` + покрытие -`ods.tickets` (все уникальные `passenger_id` из тикетов есть в измерении). - -### DDS: `check_dim_passengers_no_dup_bk` - -**Файл:** `sql/validate/dim_passengers_no_dup_bk.sql` - -Отдельная проверка дублей `passenger_id` — типичная ошибка студентов при SCD1: -INSERT без проверки EXISTS создаёт дубли. Выделена в отдельный таск для ясности -сообщения об ошибке. - -**Примечание:** Эту проверку можно объединить с `check_dim_passengers_exists`. -Отдельный таск оправдан, если хотим дать студенту более точную диагностику. -Решение — на усмотрение реализатора. - -### DDS: `check_dim_routes_exists` - -**Файл:** `sql/validate/dim_routes_exists.sql` - -Таблица не пуста + покрытие ODS (`route_no`) + **не более одной текущей версии -на `route_bk`** (инвариант SCD2: `COUNT(*) WHERE valid_to IS NULL` <= 1 для каждого BK). - -Типичная ошибка студента — INSERT без проверки `NOT EXISTS ... AND hashdiff = ...`, -что создаёт дубли текущей версии. Активный SCD2-тест проверяет только один маршрут, -эта проверка ловит проблему глобально. - -```sql -DO $$ -DECLARE - v_count BIGINT; - v_missing BIGINT; - v_multi_current BIGINT; -BEGIN - SELECT COUNT(*) INTO v_count FROM dds.dim_routes; - IF v_count = 0 THEN - RAISE EXCEPTION 'FAILED: dds.dim_routes пуста. Реализуйте загрузку: sql/dds/dim_routes_load.sql'; - END IF; - - -- Покрытие ODS - SELECT COUNT(*) INTO v_missing - FROM (SELECT DISTINCT route_no FROM ods.routes) AS s - WHERE NOT EXISTS ( - SELECT 1 FROM dds.dim_routes AS d WHERE d.route_bk = s.route_no - ); - IF v_missing > 0 THEN - RAISE EXCEPTION 'FAILED: % маршрутов из ods.routes отсутствуют в dds.dim_routes.', v_missing; - END IF; - - -- Инвариант SCD2: не более одной текущей версии на route_bk - SELECT COUNT(*) INTO v_multi_current - FROM ( - SELECT route_bk FROM dds.dim_routes - WHERE valid_to IS NULL - GROUP BY route_bk HAVING COUNT(*) > 1 - ) AS d; - - IF v_multi_current > 0 THEN - RAISE EXCEPTION E'FAILED: % маршрутов имеют более одной текущей версии (valid_to IS NULL).\n' - 'Подсказка: при INSERT новой версии проверяйте NOT EXISTS ... AND hashdiff = ...\n' - 'чтобы не создавать дубликат, если hashdiff не изменился.', v_multi_current; - END IF; - - RAISE NOTICE 'PASSED: dds.dim_routes содержит % строк, покрывает ODS, по одной текущей версии на маршрут', v_count; -END $$; -``` - -### DDS: `check_dim_routes_scd2` (АКТИВНЫЙ ТЕСТ) - -**Файл:** `sql/validate/dim_routes_scd2.sql` - -Это ключевая проверка плана. Справочник `bookings.routes` в демо-базе **статичен** — -маршруты не меняются между запусками генератора. При обычном прогоне пайплайна -студент **никогда не увидит**, как SCD2 закрывает старую версию и создаёт новую. - -**Алгоритм активного теста:** - -``` -1. Бэкап: CREATE TABLE _validate_bk_ods_routes AS SELECT * FROM ods.routes; - CREATE TABLE _validate_bk_dim_routes AS SELECT * FROM dds.dim_routes; - (обычные таблицы — TEMP не сохраняются между тасками Airflow) - -2. Мутация: Выбрать один маршрут из ods.routes (WHERE departure_time IS NOT NULL LIMIT 1). - Сохранить его route_no в служебную таблицу _validate_scd2_target. - Обновить в ods.routes его departure_time на +1 час. - -3. Запуск студенческого кода: PostgresOperator(sql="dds/dim_routes_load.sql") - -4. Проверки (все 5): - a) Ровно 2 версии тестового маршрута (было 1, стало 2) - b) Старая версия закрыта: valid_to IS NOT NULL - c) Ровно 1 открытая версия: valid_to IS NULL - d) hashdiff старой ≠ hashdiff новой (мутация отразилась в хеше) - e) Нет «дыры»: старая.valid_to = новая.valid_from - -5. Откат (с проверкой существования бэкапов через to_regclass): - IF _validate_bk_ods_routes exists: TRUNCATE ods.routes + INSERT FROM backup; - IF _validate_bk_dim_routes exists: TRUNCATE dds.dim_routes + INSERT FROM backup; - DROP TABLE IF EXISTS _validate_bk_*, _validate_scd2_target; -``` - -**Реализация:** Этот таск **не может быть простым PostgresOperator** с одним SQL, -потому что шаг 3 — это вызов студенческого SQL-скрипта *внутри* теста. Варианты: - -**Выбранный подход: цепочка тасков.** -- Jinja-шаблоны работают из коробки -- Каждый шаг прозрачен в Airflow UI -- `trigger_rule="all_done"` на restore гарантирует откат -- Студент видит в UI, на каком именно шаге проблема - -### DDS: `check_dim_routes_no_gaps` - -**Файл:** `sql/validate/dim_routes_no_gaps.sql` - -**Логика:** Для каждого `route_bk` с несколькими версиями проверить, что -`valid_to` предыдущей версии = `valid_from` следующей (полуоткрытый интервал -`[valid_from, valid_to)` без «дыр»). - -**Важно:** SCD2-загрузка поддерживает «исчезнувшие» маршруты (`dim_routes_load.sql`, -Statement 1.1): если маршрут пропал из ODS, его текущая версия закрывается -(`valid_to = CURRENT_DATE`), но новая **не вставляется**. Это корректное поведение — -у такого маршрута `valid_to IS NOT NULL` и `next_valid_from IS NULL`. Проверка -должна считать «дырой» только случаи, когда следующая версия **существует**, -но `valid_to ≠ next_valid_from`. - -```sql -DO $$ -DECLARE - v_gaps BIGINT; -BEGIN - SELECT COUNT(*) INTO v_gaps - FROM ( - SELECT route_bk, valid_to, - LEAD(valid_from) OVER (PARTITION BY route_bk ORDER BY valid_from) AS next_valid_from - FROM dds.dim_routes - ) AS t - WHERE valid_to IS NOT NULL - AND next_valid_from IS NOT NULL -- следующая версия существует (не «исчезнувший» маршрут) - AND valid_to <> next_valid_from; - - IF v_gaps > 0 THEN - RAISE EXCEPTION 'FAILED: В dds.dim_routes найдено % «дыр» между версиями SCD2. valid_to старой версии должен совпадать с valid_from новой.', v_gaps; - END IF; - - RAISE NOTICE 'PASSED: Нет «дыр» в SCD2-версиях dim_routes'; -END $$; -``` - ---- - -### DM: `check_{vitrine}_exists` (4 таска) - -**Файлы:** -- `sql/validate/airport_traffic_exists.sql` -- `sql/validate/route_performance_exists.sql` -- `sql/validate/monthly_overview_exists.sql` -- `sql/validate/passenger_loyalty_exists.sql` - -**Логика:** Минимальная проверка — таблица не пуста + нет NULL в ключевых полях. - -Пример для `airport_traffic`: - -```sql -DO $$ -DECLARE - v_count BIGINT; - v_null_pk BIGINT; -BEGIN - SELECT COUNT(*) INTO v_count FROM dm.airport_traffic; - IF v_count = 0 THEN - RAISE EXCEPTION 'FAILED: dm.airport_traffic пуста. Реализуйте загрузку: sql/dm/airport_traffic_load.sql'; - END IF; - - SELECT COUNT(*) INTO v_null_pk - FROM dm.airport_traffic - WHERE traffic_date IS NULL OR airport_sk IS NULL; - - IF v_null_pk > 0 THEN - RAISE EXCEPTION 'FAILED: dm.airport_traffic содержит % строк с NULL в ключе (traffic_date, airport_sk).', v_null_pk; - END IF; - - RAISE NOTICE 'PASSED: dm.airport_traffic содержит % строк', v_count; -END $$; -``` - -Для `route_performance` ключ — `route_bk`, для `monthly_overview` — `(year_actual, month_actual, airplane_sk)`, -для `passenger_loyalty` — `passenger_sk`. - ---- - -## Файлы для создания - -### SQL-скрипты (`sql/validate/`) - -| # | Файл | Описание | -|---|------|----------| -| 1 | `ods_airplanes_rowcount.sql` | BK coverage: ODS vs STG-батч | -| 2 | `ods_seats_rowcount.sql` | BK coverage: ODS vs STG-батч | -| 3 | `ods_no_dup_bk.sql` | Нет дублей по BK в ODS (airplanes + seats) | -| 4 | `ods_no_null_pks.sql` | NULL в PK обеих ODS-таблиц | -| 5 | `dim_airplanes_exists.sql` | Не пуста + нет дублей BK + покрытие ODS | -| 6 | `dim_passengers_exists.sql` | Не пуста + нет дублей BK + покрытие ODS | -| 7 | `dim_passengers_no_dup_bk.sql` | Дубли `passenger_id` (отдельная диагностика) | -| 8 | `dim_routes_exists.sql` | Не пуста + покрытие ODS + не более 1 текущей версии на BK | -| 9 | `dim_routes_scd2_backup.sql` | Бэкап ods.routes + dds.dim_routes | -| 10 | `dim_routes_scd2_mutate.sql` | Мутация тестового маршрута + сохранение route_no | -| 11 | `dim_routes_scd2_check.sql` | 5 проверок SCD2 после загрузки | -| 12 | `dim_routes_scd2_restore.sql` | Безопасный откат данных из бэкапа | -| 13 | `dim_routes_no_gaps.sql` | Проверка SCD2-интервалов (без «исчезнувших») | -| 14 | `airport_traffic_exists.sql` | Не пуста + NULL в ключе | -| 15 | `route_performance_exists.sql` | Не пуста + NULL в ключе | -| 16 | `monthly_overview_exists.sql` | Не пуста + NULL в ключе | -| 17 | `passenger_loyalty_exists.sql` | Не пуста + NULL в ключе | - -### DAG-файл - -| # | Файл | Описание | -|---|------|----------| -| 18 | `airflow/dags/bookings_validate.py` | DAG с TaskGroup по слоям | - -### Тест - -| # | Файл | Описание | -|---|------|----------| -| 19 | Дополнение `tests/test_dags_smoke.py` | Smoke-тест структуры DAG | - ---- - -## DAG-файл: `bookings_validate.py` - -```python -""" -Валидационный DAG: самопроверка студенческих заданий. - -Запускается вручную в Airflow UI после реализации заданий. -Таски сгруппированы по слоям — студент видит, где именно проблема. -""" - -from datetime import timedelta -from logging import getLogger - -import pendulum -from airflow import DAG -from airflow.providers.postgres.operators.postgres import PostgresOperator -from airflow.utils.task_group import TaskGroup - -GREENPLUM_CONN_ID = "greenplum_conn" -log = getLogger(__name__) - -default_args = { - "owner": "airflow", - "retries": 0, # Без ретраев — студент должен увидеть ошибку сразу - "retry_delay": timedelta(seconds=10), -} - -with DAG( - dag_id="bookings_validate", - start_date=pendulum.datetime(2017, 1, 1, tz="UTC"), - schedule=None, # Только ручной запуск - catchup=False, - max_active_runs=1, - template_searchpath="/sql", - default_args=default_args, - tags=["demo", "bookings", "greenplum", "validate"], - description="Валидация студенческих заданий: ODS, DDS, DM", -) as dag: - - with TaskGroup("validate_ods") as validate_ods: - check_ods_airplanes = PostgresOperator( - task_id="check_ods_airplanes_rowcount", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/ods_airplanes_rowcount.sql", - ) - check_ods_seats = PostgresOperator( - task_id="check_ods_seats_rowcount", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/ods_seats_rowcount.sql", - ) - check_ods_dup_bk = PostgresOperator( - task_id="check_ods_no_dup_bk", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/ods_no_dup_bk.sql", - ) - check_ods_pks = PostgresOperator( - task_id="check_ods_no_null_pks", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/ods_no_null_pks.sql", - ) - - with TaskGroup("validate_dds") as validate_dds: - check_dim_airplanes = PostgresOperator( - task_id="check_dim_airplanes_exists", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_airplanes_exists.sql", - ) - check_dim_passengers = PostgresOperator( - task_id="check_dim_passengers_exists", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_passengers_exists.sql", - ) - check_dim_passengers_dup = PostgresOperator( - task_id="check_dim_passengers_no_dup_bk", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_passengers_no_dup_bk.sql", - ) - check_dim_routes = PostgresOperator( - task_id="check_dim_routes_exists", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_routes_exists.sql", - ) - - # SCD2 активный тест: цепочка backup → mutate → load → check → restore - scd2_backup = PostgresOperator( - task_id="scd2_backup", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_routes_scd2_backup.sql", - ) - scd2_mutate = PostgresOperator( - task_id="scd2_mutate", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_routes_scd2_mutate.sql", - ) - scd2_run_load = PostgresOperator( - task_id="scd2_run_student_load", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="dds/dim_routes_load.sql", # студенческий load-скрипт! - ) - scd2_check = PostgresOperator( - task_id="scd2_check", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_routes_scd2_check.sql", - ) - scd2_restore = PostgresOperator( - task_id="scd2_restore", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_routes_scd2_restore.sql", - trigger_rule="all_done", # Откат ВСЕГДА, даже если check упал - ) - - check_dim_routes_gaps = PostgresOperator( - task_id="check_dim_routes_no_gaps", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/dim_routes_no_gaps.sql", - ) - - # SCD2 цепочка - scd2_backup >> scd2_mutate >> scd2_run_load >> scd2_check >> scd2_restore - - # no_gaps запускается после restore (на чистых данных) - scd2_restore >> check_dim_routes_gaps - - with TaskGroup("validate_dm") as validate_dm: - check_airport_traffic = PostgresOperator( - task_id="check_airport_traffic_exists", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/airport_traffic_exists.sql", - ) - check_route_performance = PostgresOperator( - task_id="check_route_performance_exists", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/route_performance_exists.sql", - ) - check_monthly_overview = PostgresOperator( - task_id="check_monthly_overview_exists", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/monthly_overview_exists.sql", - ) - check_passenger_loyalty = PostgresOperator( - task_id="check_passenger_loyalty_exists", - postgres_conn_id=GREENPLUM_CONN_ID, - sql="validate/passenger_loyalty_exists.sql", - ) -``` - ---- - -## Активный тест SCD2: детализация SQL - -### `dim_routes_scd2_backup.sql` - -```sql --- Бэкап текущего состояния перед тестом SCD2. --- Используем обычные таблицы (не TEMP) — между тасками Airflow --- TEMP-таблицы не сохраняются (каждый таск = отдельная транзакция). - -DROP TABLE IF EXISTS _validate_bk_ods_routes; -CREATE TABLE _validate_bk_ods_routes AS SELECT * FROM ods.routes; - -DROP TABLE IF EXISTS _validate_bk_dim_routes; -CREATE TABLE _validate_bk_dim_routes AS SELECT * FROM dds.dim_routes; - --- Cleanup служебной таблицы от предыдущего запуска (на случай если restore не доехал) -DROP TABLE IF EXISTS _validate_scd2_target; -``` - -### `dim_routes_scd2_mutate.sql` - -```sql --- Мутация: сдвигаем departure_time у одного маршрута на 1 час. --- Это должно изменить hashdiff → SCD2 должен закрыть старую версию. --- --- Сохраняем route_no тестового маршрута в служебную таблицу _validate_scd2_target, --- чтобы check-скрипт точно знал, какой маршрут проверять (а не угадывал по побочным эффектам). - -DO $$ -DECLARE - v_route TEXT; - v_old_time TIME; -BEGIN - -- Берём первый маршрут, у которого departure_time заполнен - SELECT route_no, departure_time - INTO v_route, v_old_time - FROM ods.routes - WHERE departure_time IS NOT NULL - ORDER BY route_no - LIMIT 1; - - IF v_route IS NULL THEN - RAISE EXCEPTION 'FAILED: ods.routes пуста или нет маршрутов с departure_time. Загрузите STG→ODS перед проверкой.'; - END IF; - - -- Запоминаем тестовый маршрут в служебную таблицу - DROP TABLE IF EXISTS _validate_scd2_target; - CREATE TABLE _validate_scd2_target AS - SELECT v_route AS route_no; - - -- Сдвигаем время на 1 час у всех записей этого маршрута - UPDATE ods.routes - SET departure_time = departure_time + INTERVAL '1 hour' - WHERE route_no = v_route; - - RAISE NOTICE 'SCD2 TEST: маршрут % — departure_time сдвинут с % на %', - v_route, v_old_time, v_old_time + INTERVAL '1 hour'; -END $$; -``` - -### `dim_routes_scd2_check.sql` - -```sql --- Проверяем, что SCD2-логика студента сработала корректно. --- Читаем route_no тестового маршрута из служебной таблицы _validate_scd2_target --- (создана на шаге mutate), а не угадываем по побочным эффектам. -DO $$ -DECLARE - v_route TEXT; - v_version_count BIGINT; - v_closed_count BIGINT; - v_open_count BIGINT; - v_old_hash TEXT; - v_new_hash TEXT; - v_gap_count BIGINT; -BEGIN - -- Читаем тестовый маршрут из служебной таблицы - SELECT route_no INTO v_route FROM _validate_scd2_target LIMIT 1; - - IF v_route IS NULL THEN - RAISE EXCEPTION 'FAILED: служебная таблица _validate_scd2_target пуста. Шаг mutate не выполнился?'; - END IF; - - -- Если dim_routes пуста — load не запустился - IF NOT EXISTS (SELECT 1 FROM dds.dim_routes WHERE route_bk = v_route) THEN - RAISE EXCEPTION E'FAILED: dds.dim_routes не содержит маршрут % после запуска load.\n' - 'Проверьте sql/dds/dim_routes_load.sql.', v_route; - END IF; - - -- Проверка a: Ровно 2 версии тестового маршрута (было 1, стало 2 после мутации) - SELECT COUNT(*) INTO v_version_count - FROM dds.dim_routes WHERE route_bk = v_route; - - IF v_version_count < 2 THEN - RAISE EXCEPTION E'FAILED: Маршрут % — найдена % версия (ожидается 2: старая закрытая + новая открытая).\n' - 'SCD2 должен был создать новую версию после изменения departure_time.', v_route, v_version_count; - END IF; - - IF v_version_count > 2 THEN - RAISE EXCEPTION E'FAILED: Маршрут % — найдено % версий (ожидается 2).\n' - 'Возможно, load создаёт лишние дубликаты. Проверьте условие NOT EXISTS при INSERT.', v_route, v_version_count; - END IF; - - -- Проверка b: Старая версия закрыта (valid_to IS NOT NULL) - SELECT COUNT(*) INTO v_closed_count - FROM dds.dim_routes WHERE route_bk = v_route AND valid_to IS NOT NULL; - - IF v_closed_count = 0 THEN - RAISE EXCEPTION E'FAILED: Маршрут % — SCD2 не закрыл старую версию (valid_to IS NULL у всех версий).\n' - 'Подсказка: hashdiff изменился (departure_time сдвинут на 1 час),\n' - 'но ваш load-скрипт не обнаружил это изменение.\n' - 'Проверьте:\n' - ' 1. Формулу hashdiff — включает ли она departure_time?\n' - ' 2. Логику сравнения hashdiff (UPDATE ... SET valid_to = CURRENT_DATE WHERE hashdiff <> новый_hashdiff)', v_route; - END IF; - - -- Проверка c: Новая версия открыта (valid_to IS NULL) - SELECT COUNT(*) INTO v_open_count - FROM dds.dim_routes WHERE route_bk = v_route AND valid_to IS NULL; - - IF v_open_count <> 1 THEN - RAISE EXCEPTION E'FAILED: Маршрут % — ожидается ровно 1 открытая версия (valid_to IS NULL), найдено %.\n' - 'Подсказка: SCD2 должен вставить новую строку с valid_to = NULL.', v_route, v_open_count; - END IF; - - -- Проверка d: hashdiff старой ≠ hashdiff новой (мутация действительно отразилась) - SELECT hashdiff INTO v_old_hash - FROM dds.dim_routes WHERE route_bk = v_route AND valid_to IS NOT NULL - ORDER BY valid_from DESC LIMIT 1; - - SELECT hashdiff INTO v_new_hash - FROM dds.dim_routes WHERE route_bk = v_route AND valid_to IS NULL; - - IF v_old_hash = v_new_hash THEN - RAISE EXCEPTION E'FAILED: Маршрут % — hashdiff старой и новой версий совпадают.\n' - 'Мутация сдвинула departure_time на 1 час, но hashdiff не изменился.\n' - 'Проверьте, что departure_time входит в формулу hashdiff.', v_route; - END IF; - - -- Проверка e: Нет «дыры» между valid_to старой и valid_from новой - SELECT COUNT(*) INTO v_gap_count - FROM dds.dim_routes AS old_v - JOIN dds.dim_routes AS new_v - ON old_v.route_bk = new_v.route_bk - WHERE old_v.route_bk = v_route - AND old_v.valid_to IS NOT NULL - AND new_v.valid_to IS NULL - AND old_v.valid_to <> new_v.valid_from; - - IF v_gap_count > 0 THEN - RAISE EXCEPTION E'FAILED: Маршрут % — «дыра» между версиями:\n' - 'valid_to старой ≠ valid_from новой.\n' - 'Подсказка: полуоткрытый интервал [valid_from, valid_to).\n' - 'valid_from новой версии должен = valid_to старой (обычно CURRENT_DATE).', v_route; - END IF; - - RAISE NOTICE 'PASSED: SCD2 корректен для маршрута %. 2 версии, hashdiff различаются, «дыр» нет.', v_route; -END $$; -``` - -### `dim_routes_scd2_restore.sql` - -```sql --- Откат данных после теста SCD2. --- Выполняется ВСЕГДА (trigger_rule="all_done"), даже если check упал. --- --- Безопасность: если backup-шаг не создал таблицы (сбой на backup), --- откат НЕ трогает live-данные — просто чистит служебные таблицы. --- Это гарантирует, что restore никогда не сломает ods.routes / dds.dim_routes. - -DO $$ -DECLARE - v_has_ods_backup BOOLEAN; - v_has_dim_backup BOOLEAN; -BEGIN - -- Проверяем существование backup-таблиц через to_regclass - -- (ищет по search_path — совпадает с тем, как CREATE TABLE их создал) - v_has_ods_backup := to_regclass('_validate_bk_ods_routes') IS NOT NULL; - v_has_dim_backup := to_regclass('_validate_bk_dim_routes') IS NOT NULL; - - -- Восстанавливаем ods.routes только если бэкап существует - IF v_has_ods_backup THEN - TRUNCATE ods.routes; - INSERT INTO ods.routes SELECT * FROM _validate_bk_ods_routes; - RAISE NOTICE 'RESTORE: ods.routes восстановлена из бэкапа'; - ELSE - RAISE NOTICE 'RESTORE: бэкап ods.routes не найден — пропускаем (backup-шаг не завершился?)'; - END IF; - - -- Восстанавливаем dds.dim_routes только если бэкап существует - IF v_has_dim_backup THEN - TRUNCATE dds.dim_routes; - INSERT INTO dds.dim_routes SELECT * FROM _validate_bk_dim_routes; - RAISE NOTICE 'RESTORE: dds.dim_routes восстановлена из бэкапа'; - ELSE - RAISE NOTICE 'RESTORE: бэкап dds.dim_routes не найден — пропускаем'; - END IF; -END $$; - --- Cleanup служебных таблиц (безусловно, IF EXISTS) -DROP TABLE IF EXISTS _validate_bk_ods_routes; -DROP TABLE IF EXISTS _validate_bk_dim_routes; -DROP TABLE IF EXISTS _validate_scd2_target; - --- Учебный комментарий: Мы восстанавливаем данные из бэкапа, чтобы тест --- не оставлял «мусорных» версий в dim_routes. Это стандартный паттерн --- для интеграционных тестов: setup → act → assert → teardown. --- IF EXISTS проверки гарантируют, что restore безопасен при любом сценарии сбоя. -``` - ---- - -## Smoke-тест DAG (дополнение `test_dags_smoke.py`) - -Добавить новый тест-класс: - -```python -class TestBookingsValidate: - """Smoke-тесты DAG bookings_validate.""" - - def test_dag_loads(self): - dag = _load_dag("airflow.dags.bookings_validate") - assert dag is not None - - def test_expected_tasks(self): - dag = _load_dag("airflow.dags.bookings_validate") - expected = { - # ODS - "validate_ods.check_ods_airplanes_rowcount", - "validate_ods.check_ods_seats_rowcount", - "validate_ods.check_ods_no_dup_bk", - "validate_ods.check_ods_no_null_pks", - # DDS - "validate_dds.check_dim_airplanes_exists", - "validate_dds.check_dim_passengers_exists", - "validate_dds.check_dim_passengers_no_dup_bk", - "validate_dds.check_dim_routes_exists", - "validate_dds.scd2_backup", - "validate_dds.scd2_mutate", - "validate_dds.scd2_run_student_load", - "validate_dds.scd2_check", - "validate_dds.scd2_restore", - "validate_dds.check_dim_routes_no_gaps", - # DM - "validate_dm.check_airport_traffic_exists", - "validate_dm.check_route_performance_exists", - "validate_dm.check_monthly_overview_exists", - "validate_dm.check_passenger_loyalty_exists", - } - assert expected.issubset(dag.task_dict.keys()) - - def test_scd2_chain(self): - """SCD2 цепочка backup → mutate → load → check → restore.""" - dag = _load_dag("airflow.dags.bookings_validate") - _assert_direct_edge(dag, "validate_dds.scd2_backup", "validate_dds.scd2_mutate") - _assert_direct_edge(dag, "validate_dds.scd2_mutate", "validate_dds.scd2_run_student_load") - _assert_direct_edge(dag, "validate_dds.scd2_run_student_load", "validate_dds.scd2_check") - _assert_direct_edge(dag, "validate_dds.scd2_check", "validate_dds.scd2_restore") - - def test_no_gaps_after_restore(self): - """no_gaps должен выполняться после restore (на чистых данных).""" - dag = _load_dag("airflow.dags.bookings_validate") - _assert_direct_edge(dag, "validate_dds.scd2_restore", "validate_dds.check_dim_routes_no_gaps") - - def test_restore_trigger_rule(self): - """restore должен выполняться всегда (all_done), даже если check упал.""" - dag = _load_dag("airflow.dags.bookings_validate") - restore_task = dag.task_dict["validate_dds.scd2_restore"] - assert restore_task.trigger_rule == "all_done" -``` - ---- - -## Порядок реализации - -1. Создать каталог `sql/validate/` -2. Написать SQL-скрипты (17 файлов) — начать с простых (ODS, DM), затем DDS, затем SCD2 -3. Написать DAG `bookings_validate.py` -4. Дополнить `tests/test_dags_smoke.py` -5. `make test` — smoke-тесты проходят -6. Ручная проверка на стенде (если поднят): - - Trigger DAG в Airflow UI - - Все ODS/DDS/DM зелёные (на solution-ветке) - - SCD2 активный тест: backup → mutate → load → check (зелёный) → restore -7. Коммит - ---- - -## Верификация - -### Автоматическая - -```bash -make test # smoke-тест DAG-структуры -``` - -### Ручная (на стенде) - -1. `make up && make ddl-gp` → запустить STG → ODS → DDS → DM -2. Trigger `bookings_validate` в Airflow UI -3. Проверить: все таски зелёные -4. Проверить SCD2: в логах `scd2_check` видно `PASSED: SCD2 корректен для маршрута ...` -5. Проверить restore: `ods.routes` и `dds.dim_routes` не изменились после теста - -### Сценарий «студент ещё не реализовал» - -На main-ветке (с DDL-заглушками): -- ODS-таски: FAILED (таблицы пусты) → дружелюбное сообщение -- DDS-таски: FAILED → сообщение «Реализуйте загрузку» -- DM-таски: FAILED → сообщение «Реализуйте загрузку» -- SCD2-тест: `scd2_run_student_load` пройдёт (заглушка `SELECT 1;` — валидный SQL), - но `scd2_check` упадёт (dim_routes не обновилась, версий < 2) - → `scd2_restore` всё равно выполнится (`trigger_rule="all_done"`) - ---- - -## Открытые вопросы - -1. **`dim_passengers_no_dup_bk` — отдельный таск или объединить с `exists`?** - Отдельный таск даёт точнее диагностику, но увеличивает число тасков. - Рекомендация: объединить проверки в один файл `dim_passengers_exists.sql` - (как сделано для `dim_airplanes_exists`). - -2. **Нужна ли проверка `total_seats` в `dim_airplanes`?** - `total_seats` вычисляется агрегацией из `ods.seats`. Если студент забудет - этот JOIN — поле будет NULL. Можно добавить `SELECT COUNT(*) WHERE total_seats IS NULL`. - Рекомендация: добавить в `dim_airplanes_exists.sql`. - -3. **Бэкап SCD2: обычные таблицы vs TEMP?** - Каждый таск Airflow — отдельная транзакция → TEMP-таблицы не сохраняются. - Используем обычные таблицы с префиксом `_validate_bk_`. Риск: если DAG упадёт - между backup и restore, таблицы останутся. Cleanup: `scd2_restore` делает - `DROP TABLE IF EXISTS`, поэтому при следующем запуске проблем не будет. - ---- - -## Ключевые файлы - -| Файл | Роль | -|------|------| -| `airflow/dags/bookings_validate.py` | DAG: TaskGroup по слоям | -| `sql/validate/*.sql` | 17 SQL-скриптов проверок | -| `sql/dds/dim_routes_load.sql` | Студенческий load (вызывается из SCD2-теста) | -| `tests/test_dags_smoke.py` | Smoke-тесты DAG-структуры | -| `docs/design/assignment_design.md` | Дизайн (секция 4 — источник требований) | diff --git a/docs/archive/architecture_review.md b/docs/archive/architecture_review.md deleted file mode 100644 index 39fd69b..0000000 --- a/docs/archive/architecture_review.md +++ /dev/null @@ -1,304 +0,0 @@ -# Ревью архитектуры слоёв DWH: оценка учебной ценности - -> Дата: 2026-03-01 (ревью), 2026-03-11 (закрытие) -> Статус: **завершён** — P0/P1/P2 выполнены, P3 отложены (покрыты другими документами) -> Контекст: оценка текущей конструкции слоёв с точки зрения учебных целей - -## Context - -Стенд — курсовая работа и эталон для менти-джунов. Они понесут эти паттерны на свою первую работу. Оцениваем по двум осям: **production-ready** (чтобы не стыдно было показать на собеседовании) и **KISS** (чтобы джун не утонул в сложности). - -Текущее состояние: 95 SQL-файлов, 5 слоёв (STG→ODS→DDS→DM), 9 DAG-ов, полная Star Schema с SCD2, DQ на каждом шаге. Реализована 1 из 5 витрин DM. - ---- - -## СИЛЬНЫЕ СТОРОНЫ (что уже отлично) - -### 1. Паттерн load → DQ на каждом шаге — эталонный -Каждая сущность в каждом слое имеет тройку файлов `_ddl.sql` / `_load.sql` / `_dq.sql`. DAG-и обеспечивают порядок load→dq→next. Smoke-тесты проверяют рёбра графа. Студенты усвоят: **DQ — не опция, а часть пайплайна**. - -### 2. UPSERT через UPDATE + INSERT — production-grade для Greenplum -Не DELETE+INSERT (дорого на AO-таблицах), не MERGE (нет в GP6). `IS DISTINCT FROM` для null-safe сравнения — деталь, которую даже опытные инженеры забывают. - -### 3. Антипаттерн-обучение в DM (distribution by date) -Комментарий в `dm/sales_report_ddl.sql` объясняет **почему** нельзя распределять по дате, с конкретными причинами (Load Skew, Processing Skew). Это «почему нет» — именно то, что не дают учебники. - -### 4. SCD2 в dim_routes — полный и корректный -Все три кейса: закрытие изменённых версий (hashdiff), закрытие исчезнувших маршрутов, вставка новых версий с правильной логикой valid_from. DQ проверяет пересечение интервалов. Готовый reference implementation. - -### 5. Point-in-time lookup в факте — ключевой навык -```sql -LEFT JOIN dds.dim_routes AS rte - ON rte.route_bk = flt.route_no - AND flt.scheduled_departure::DATE >= rte.valid_from - AND (rte.valid_to IS NULL OR flt.scheduled_departure::DATE < rte.valid_to) -``` -Многие продакшн-DWH ошибаются, присоединяя только текущую версию. - -### 6. HWM-инкрементальность в DM — самовосстанавливающийся пайплайн -`MAX(_load_ts)` + TEMP TABLE для однократной агрегации — канон MPP. Пайплайн сам «догоняет» пропущенные дни. - -### 7. DAG-графы корректно отражают зависимости данных -Параллельность airports/airplanes, gates на routes (нужны оба), факт после всех измерений. Smoke-тесты проверяют и наличие, и **отсутствие** рёбер (параллельность). - -### 8. ANALYZE после каждой загрузки -GP-специфичная best practice, которую забывают даже опытные команды. - -### 9. Идемпотентные STG-загрузки -`NOT EXISTS (... WHERE _load_id = '{{ run_id }}')` — простой, корректный, понятный паттерн для retry-safe загрузок. - ---- - -## ЗАМЕЧАНИЯ И ЗАДАЧИ ДЛЯ ДОРАБОТКИ - -### P0: Фактическая ошибка (исправить до показа студентам) - -- [x] **ODS batch resolver теряет данные при двух STG-запусках подряд** - - Сценарий: STG run_1 загружает день N, STG run_2 загружает день N+1, затем ODS запускается - - `_resolve_stg_batch_id()` выбирает только последний согласованный batch (`run_2`) - - Все ODS load-скрипты фильтруют `WHERE _load_id = 'run_2'` → данные `run_1` навсегда пропущены - - **Справочники** (airports, airplanes, routes, seats): проблемы нет — full snapshot, `run_2` содержит всё - - **Транзакционные таблицы** (bookings, tickets, flights, segments, boarding_passes): **потеря данных** — инкрементальные записи `run_1` никогда не попадут в ODS - - Корень проблемы: batch resolver проектировался для согласованности справочников (INTERSECT), но тот же single-batch фильтр применяется к транзакционным таблицам, где нужны **все необработанные** batch-и - - **Нужно**: разделить логику — для транзакционных таблиц загружать все batch-и с `_load_ts > MAX(_load_ts в ODS)` (аналог HWM из DM), для справочников — по-прежнему последний согласованный - - Файлы: `airflow/dags/bookings_to_gp_ods.py`, `sql/ods/bookings_load.sql`, `sql/ods/tickets_load.sql`, `sql/ods/flights_load.sql`, `sql/ods/segments_load.sql`, `sql/ods/boarding_passes_load.sql` - -- [x] **Противоречие в distribution key для airport_traffic** - - `bookings_dm_design.md` (строка 182): `DISTRIBUTED BY (traffic_date)` - - `sales_report_ddl.sql`: явно объясняет, почему distribution by date — антипаттерн - - **Нужно**: исправить на `DISTRIBUTED BY (airport_sk)` в дизайн-документе - - Файл: `docs/design/bookings_dm_design.md` - -### P1: Высокий эффект, минимум усилий (комментарии и документация) - -- [x] **Нет объяснения «почему не SERIAL» в генерации SK** - - `MAX(sk) + ROW_NUMBER()` корректен для GP, но студент на PostgreSQL/Snowflake будет использовать `IDENTITY`/`SEQUENCE` - - **Нужно**: 4-строчный комментарий в `sql/dds/dim_airports_load.sql` - -- [x] **Факт без суррогатного ключа — не объяснено «почему»** - - Натуральный (ticket_no, flight_id) как grain — правильное Kimball-моделирование - - **Нужно**: комментарий в `sql/dds/fact_flight_sales_ddl.sql` - -- [x] **Нет упоминания cross-DAG зависимостей** - - STG, ODS, DDS, DM — отдельные DAG-и с `schedule=None`, студент может не понять порядок - - **Нужно**: комментарий в docstring каждого DAG или `docs/dag_execution_order.md` - -- [x] **Late-arriving dimensions не упомянуты** - - Факт делает LEFT JOIN → `passenger_sk = NULL` при опоздании; нет механизма исправления - - **Нужно**: комментарий в `sql/dds/fact_flight_sales_load.sql` у LEFT JOIN-ов - -- [x] **Batch resolver недообъяснён** - - `_resolve_stg_batch_id` с INTERSECT по 4 таблицам — нет комментария **зачем** нужна согласованность - - **Нужно**: комментарий в `airflow/dags/bookings_to_gp_ods.py` перед SQL-запросом - -- [x] **`helpers/greenplum.py`** — удалён вместе с CSV-пайплайном (перенесён в airflow-manual) - -### P2: Средние усилия, заметное улучшение качества - -- [x] **Явный storage type для всех таблиц + AO где возможно** ✅ РЕШЕНИЕ ПРИНЯТО - - 18 из 28 таблиц имели неявный heap (нет `WITH`) — теперь выбор сделан явно - - **Целевая раскладка по storage:** - - **AO Row + zstd**: `dds.dim_calendar` (узкая таблица, column-store не даёт выигрыша) - - **AO Row + zstd**: ODS snapshot-справочники (`airports`, `airplanes`, `routes`, `seats`) - — перевести загрузку с UPSERT на TRUNCATE+INSERT (честнее для full snapshot семантики) - - **AO Row + zstd**: `dds.dim_tariffs` (только INSERT, нет UPDATE) - - **AO Row + zstd**: `dm.route_performance` (full rebuild, по дизайну) - - **Heap (явный)**: ODS транзакционные (`bookings`, `tickets`, `flights`, `segments`, - `boarding_passes`) — row-level UPDATE при SCD1 UPSERT - - **Heap (явный)**: DDS измерения с UPDATE (`dim_airports`, `dim_airplanes`, - `dim_passengers`, `dim_routes`) и `fact_flight_sales` - - **Heap (явный)**: DM витрины с UPSERT (`sales_report` и будущие HWM-витрины) - - К каждой таблице добавить комментарий, объясняющий выбор storage type - - Файлы: все `*_ddl.sql` в ods/, dds/, dm/ + переписать 4 ODS snapshot load-скрипта - - См. ADR-3 - -- [x] **Дублирование hashdiff CTE в dim_routes_load.sql** - - md5(COALESCE(...)) повторяется в Statement 1 и Statement 2, ROW_NUMBER() — 3 раза - - **Решение**: вынесено в CREATE TEMP TABLE tmp_routes_src ON COMMIT DROP ✅ ВЫПОЛНЕНО - - Файл: `sql/dds/dim_routes_load.sql` - -- [x] **Несогласованность нейминга STG vs ODS+** ✅ ВЫПОЛНЕНО - - STG: `batch_id`, `load_dttm`, `src_created_at_ts` → переименованы в канон `_load_id`, `_load_ts`, `event_ts` - - Единый словарь во всех слоях снижает когнитивную нагрузку - - Секция 6 «Переходный маппинг» удалена из `naming_conventions.md` как неактуальная - - Файлы: 27 STG SQL + ODS load-скрипты + `naming_conventions.md` + тесты - -- [x] **Дублирование CTE в ODS load-скриптах** - - `WITH src AS (...)` копируется 2-3 раза в каждом из 9 ODS load-файлов - - **Решение**: TEMP TABLE для самых сложных (airports, flights, routes); простые — оставить - - Файлы: `sql/ods/airports_load.sql`, `sql/ods/flights_load.sql`, `sql/ods/routes_load.sql` - - *Заметка*: Для всех транзакционных таблиц ODS внедрен паттерн TEMP TABLE для надежной работы HWM. - -- [x] **DM слой спроектирован** - - 5 витрин: `sales_report`, `route_performance`, `passenger_loyalty`, `airport_traffic`, `monthly_overview` - - `sales_report`, `route_performance` — эталонные реализации; `passenger_loyalty`, `airport_traffic`, `monthly_overview` — задания для студентов - - `route_performance` — full rebuild + AO Column Store - -### P3: Отложено (покрыто другими документами) - -- [~] **Крутая лестница сложности ODS→DDS** - - Покрыто: `docs/assignment/analyst_spec.md` содержит рекомендуемый порядок выполнения - (STG → ODS → DDS SCD1 → DDS SCD2 → DM от простого к сложному) - -- [~] **DQ без переиспользуемых функций** - - Отложено: переусложнение для учебного стенда (AGENTS.md: KISS) - -- [~] **Нет документа по стратегии distribution** - - Покрыто: подсказки по DK есть в `analyst_spec.md` и комментариях к каждому DDL - -- [~] **Отсутствующие паттерны** (комментарии/заметки) - - Покрыто частично: partitioning и exchange partition описаны в ADR-2 (этот документ). - SCD3/6 и data lineage — вне скоупа курсовой - ---- - -## ПРИНЯТЫЕ АРХИТЕКТУРНЫЕ РЕШЕНИЯ - -### ADR-1: Города, страны, модели самолётов — атрибуты измерений, не отдельные справочники - -**Рассматривалось**: выделить `dim_city`, `dim_country`, `dim_airplane_model` как отдельные -измерения со своими суррогатными ключами. - -**Решение**: оставить `city`, `country` как атрибуты `dim_airports`, а `model` — как атрибут -`dim_airplanes`. Не создавать отдельные справочники. - -**Обоснование**: -1. **Star vs Snowflake.** Kimball-методология рекомендует «wide and flat» измерения. - Вынос атрибутов в подтаблицы превращает star schema в snowflake — добавляет 2-3 JOIN-а - в каждый запрос к факту без аналитического выигрыша. Для учебного стенда star schema — - правильный эталон. -2. **Нет самостоятельной сущности в домене.** Город — JSON-атрибут аэропорта в источнике - (`airport_name::json->>'ru'`). У него нет своего бизнес-ключа, жизненного цикла, - независимых атрибутов. Модель самолёта — аналогично. -3. **Когнитивная нагрузка.** Лестница ODS→DDS уже крутая (6 измерений + 1 факт + SCD2). - Добавление 2-3 измерений усложнит стенд без пропорционального обучающего эффекта. - -**Когда отдельное измерение оправдано** (для справки студентам): -- Город имеет собственные атрибуты из другого источника (население, регион, координаты) - → `dim_geography` как outrigger-измерение -- Модель самолёта имеет независимые характеристики (производитель, сертификация, конфигурации) - → `dim_aircraft_type` -- В Data Vault — `hub_city` / `hub_country` как самостоятельные бизнес-объекты (другая парадигма) - -### ADR-2: Heap + UPSERT вместо AO + партиционирование + exchange partition - -**Контекст**: Greenplum широко распространён в РФ — на него активно мигрировали при -импортозамещении с Teradata и Exadata. Именно поэтому GP выбран для курсовой: опыт работы -с ним будет напрямую релевантен первой работе студента. Тем важнее, чтобы студенты понимали, -как устроены реальные GP-хранилища, даже если стенд использует упрощённый подход. - -**Рассматривалось**: использовать production-паттерн крупных GP-хранилищ: -- AO Column Store (сжатие zlib/zstd, векторное чтение, колоночное хранение) -- Range-партиционирование по дате (`PARTITION BY RANGE (flight_date)`) -- Обновление через замену партиций (`ALTER TABLE EXCHANGE PARTITION`) или - `DELETE + INSERT` в рамках одной партиции вместо row-level UPDATE - -**Решение**: партиционирование не применяем (учебные объёмы). Для storage — -дифференцированный подход: heap для таблиц с UPDATE, AO для иммутабельных -(см. ADR-3 с полной раскладкой). - -**Обоснование**: -1. **Универсальность паттерна.** UPSERT через UPDATE + INSERT работает в PostgreSQL, - Snowflake, BigQuery, Redshift — везде. Exchange partition — GP-специфика - (`ALTER TABLE ... EXCHANGE PARTITION FOR (...) WITH TABLE tmp_...`). - Студент, освоив UPSERT, сможет применить его на любой платформе. -2. **Объём данных.** На учебных ~100K строк партиционирование не даёт partition pruning - эффекта, зато утраивает DDL (стратегия, sub-partitions, retention policy). - Выигрыш нулевой, когнитивная нагрузка — существенная. -3. **Простота ментальной модели.** «Вот строка, она обновилась» понятнее, чем «вот партиция, - она заменилась целиком». Второй паттерн требует понимания storage engine, что выходит - за рамки первого курса DWH. - -**Что студенту важно знать про реальный GP** (для менти): - -На продакшн-хранилищах с десятками и сотнями миллионов строк подход меняется принципиально: - -| Аспект | Стенд (учебный) | Продакшн (реальный GP) | -|--------|-----------------|----------------------| -| Хранение фактов | Heap (row-oriented) | AO Column Store (сжатие, колонки) | -| Партиционирование | Нет | Range по дате (день/месяц) | -| Обновление | Row-level UPDATE | Exchange partition или DELETE+INSERT в партиции | -| Причина | UPDATE на AO «раздувает» таблицу (помечает строки deleted, дописывает новые) | | -| Когда переходить | > 10M строк, или когда VACUUM не справляется | | - -Типичный production-паттерн загрузки факта по дням: -```sql --- 1. Собрать новую партицию во временную таблицу -CREATE TABLE tmp_fact_20170102 (LIKE dds.fact_flight_sales) - WITH (appendonly=true, orientation=column, compresstype=zstd); -INSERT INTO tmp_fact_20170102 SELECT ... FROM ods... WHERE flight_date = '2017-01-02'; - --- 2. Атомарно заменить партицию (без DELETE, без UPDATE) -ALTER TABLE dds.fact_flight_sales - EXCHANGE PARTITION FOR ('2017-01-02') WITH TABLE tmp_fact_20170102; - --- 3. Удалить временную таблицу (теперь в ней старые данные) -DROP TABLE tmp_fact_20170102; -``` - -Преимущества exchange partition: -- Нет row-level UPDATE → нет bloat, не нужен VACUUM -- AO Column Store даёт 5-10x сжатие и быстрые аналитические скана -- Partition pruning: запрос `WHERE flight_date = '2017-01-02'` читает только одну партицию -- Атомарность: EXCHANGE — одна DDL-команда, нет окна неконсистентности - -### ADR-3: Явный storage type для каждой таблицы + AO где нет UPDATE - -**Проблема**: 18 из 28 таблиц в ODS/DDS/DM создаются без `WITH`-клаузы. GP по умолчанию -создаёт heap, но студент не видит осознанного выбора — таблица «просто создаётся». -В учебном стенде каждое решение должно быть видимым и объяснённым. - -**Решение**: добавить явный `WITH (...)` ко всем таблицам. Где row-level UPDATE не нужен — -перевести на AO (Row или Column) с компрессией. - -**Целевая раскладка storage по таблицам:** - -| Storage | Таблицы | Почему | -|---------|---------|--------| -| **AO Column** zstd | `dds.dim_calendar` | Write-once (generate_series), никогда не обновляется. Колоночное хранение идеально для аналитических скан. | -| **AO Column** zstd | `dm.route_performance` | Full rebuild (TRUNCATE+INSERT), чисто аналитические чтения. | -| **AO Row** zstd | STG: все 9 таблиц | Уже реализовано. Append-only, иммутабельные батчи. Примечание: используем **zstd (level 1)** вместо zlib, так как он обеспечивает более высокую скорость декомпрессии и лучшее сжатие в современных GP-кластерах (6.0+). | -| **AO Row** zstd | ODS snapshot: `airports`, `airplanes`, `routes`, `seats` | Полный snapshot каждый раз. Перевести загрузку с UPSERT на TRUNCATE+INSERT — честнее для семантики «текущий срез». | -| **AO Row** zstd | `dds.dim_tariffs` | Только INSERT новых тарифов, UPDATE не используется. | -| **Heap** (явный) | ODS транзакционные: `bookings`, `tickets`, `flights`, `segments`, `boarding_passes` | Row-level UPDATE при SCD1 UPSERT. Heap обязателен. | -| **Heap** (явный) | DDS измерения с UPDATE: `dim_airports`, `dim_airplanes`, `dim_passengers`, `dim_routes` | SCD1/SCD2 UPSERT с row-level UPDATE. | -| **Heap** (явный) | `dds.fact_flight_sales` | UPDATE (is_boarded, seat_no меняются). | -| **Heap** (явный) | DM витрины с UPSERT: `sales_report` и будущие HWM-витрины | Row-level UPDATE при инкрементальном UPSERT. | - -**Учебная ценность**: студенты видят на практике три storage-стратегии в одном проекте: -1. AO Column — для иммутабельных аналитических таблиц (dim_calendar, route_performance) -2. AO Row — для append-only данных и snapshot-справочников (STG, ODS refs, dim_tariffs) -3. Heap — для таблиц с row-level UPDATE (ODS транзакции, DDS dims с UPSERT, факт, DM) - -И понимают **почему** выбор именно такой: UPDATE на AO = bloat + необходимость VACUUM. - ---- - -## ЧТО ОСТАВИТЬ КАК ЕСТЬ - -| Аспект | Почему не трогаем | -|--------|-------------------| -| Факт без SK | Правильное моделирование, нужен только комментарий (P1) | -| ODS DAG с 20 задачами | Не перегружает — параллельная структура наглядна на графе | -| 5 витрин DM | Правильное количество, каждая учит своему паттерну | -| PL/pgSQL DQ | Достаточно для учебного проекта, фреймворк — перебор | -| MAX+ROW_NUMBER для SK | Корректно для GP, нужен только комментарий (P1) | - ---- - -## Сводка по трудозатратам - -Все задачи P0–P2 выполнены. P3 отложены (покрыты другими документами). - -| Приоритет | Действие | Статус | -|-----------|----------|--------| -| ~~P0~~ | ODS batch resolver: разделить логику для справочников и транзакций | ✅ | -| ~~P0~~ | Исправить distribution key в airport_traffic | ✅ | -| ~~P1~~ | Добавить 7 точечных комментариев | ✅ | -| ~~P2~~ | Явный storage type + AO где нет UPDATE (ADR-3) | ✅ | -| ~~P2~~ | Рефакторинг hashdiff → TEMP TABLE | ✅ | -| ~~P2~~ | Переименовать STG поля в канон | ✅ | -| ~~P2~~ | TEMP TABLE для сложных ODS load-ов | ✅ | -| ~~P2~~ | Реализовать DM слой (5 витрин) | ✅ | -| P3 | Маршрут изучения DDS | Покрыто: `analyst_spec.md` | -| P3 | DQ-функции, distribution strategy, паттерны | Отложено | diff --git a/docs/assignment/README.md b/docs/assignment/README.md index 561bd40..9ab1b02 100644 --- a/docs/assignment/README.md +++ b/docs/assignment/README.md @@ -12,8 +12,9 @@ - SQL-скрипты: `sql/stg/`, `sql/ods/`, `sql/dds/`, `sql/dm/` - DAG-файлы: `airflow/dags/` -- [Порядок запуска DAG-ов](../reference/dag_execution_order.md) +- [Порядок запуска DAG-ов](../dag_execution_order.md) ## Для менторов -Дизайн заданий и педагогическая логика: [docs/design/assignment_design.md](../design/assignment_design.md). +Дизайн заданий и педагогическая логика — в ветке `solution` +(`docs/design/assignment_design.md`). diff --git a/docs/bookings_to_gp_dds.md b/docs/bookings_to_gp_dds.md index 6ff4808..5312103 100644 --- a/docs/bookings_to_gp_dds.md +++ b/docs/bookings_to_gp_dds.md @@ -108,20 +108,19 @@ load_dds_dim_calendar → dq_dds_dim_calendar Три учебных приёма в этом скрипте: **Защитные LEFT JOIN (defensive coding).** -Все JOIN-ы с измерениями — `LEFT JOIN`. DAG гарантирует, что все измерения загружены -и прошли DQ **до** старта факта (жёсткие зависимости в графе). Поэтому в штатном режиме -NULL SK не возникают. LEFT JOIN здесь — защита от data quality аномалий (например, если -в `ods.routes` появится маршрут с несуществующим аэропортом). +Все JOIN-ы с измерениями — `LEFT JOIN`. На ветке `solution` все измерения заполнены, +и NULL SK не возникают в штатном режиме. На ветке `main` студенческие измерения +(`dim_passengers`, `dim_routes`, `dim_airplanes`) — заглушки, поэтому соответствующие +SK будут NULL до реализации студентом. -DQ-проверки факта отражают эту логику: -- `passenger_sk` и `tariff_sk` — **запрещены** NULL целиком (0 строк); -- route-related FK (`route_sk`, `airport_sk`, `airplane_sk`) и `calendar_sk` — - допускается до **1%** NULL (NOTICE-предупреждение), при превышении — EXCEPTION. +DQ-проверки факта на main: +- `tariff_sk` — **запрещён** NULL (EXCEPTION); +- `departure_airport_sk`, `arrival_airport_sk` (через `ods.routes`, эталон) — порог **1%** NULL (EXCEPTION); +- `passenger_sk`, `route_sk`, `airplane_sk` (студенческие) — только **NOTICE** (100% NULL допустимо); +- `calendar_sk` — порог **1%** NULL. -> Это **не** паттерн late-arriving dimensions (опаздывающих измерений) в классическом -> понимании: backfill NULL SK при повторном запуске не реализован. -> В боевых системах для этого используют «строку-заглушку» (unknown member, SK = 0) -> и отдельный процесс backfill. +> После реализации всех измерений: `TRUNCATE dds.fact_flight_sales` → перезагрузка → +> все SK заполнены. Полную версию DQ см. в ветке `solution`. **Два пути lookup для аэропортов и маршрутов.** Аэропорты (`departure_airport_sk`, `arrival_airport_sk`) разрешаются через `ods.routes` → diff --git a/docs/bookings_to_gp_stage.md b/docs/bookings_to_gp_stage.md index 9cbb3e2..6d1e5a1 100644 --- a/docs/bookings_to_gp_stage.md +++ b/docs/bookings_to_gp_stage.md @@ -148,9 +148,8 @@ LIMIT 10; - `database "demo" does not exist`: демо‑БД не установлена → выполните `make bookings-init`. - Ошибки про `stg.*`/`stg.*_ext`: не применён DDL → запустите `bookings_stg_ddl` или `make ddl-gp`. - Ошибки PXF (`protocol "pxf" does not exist`, connection refused): перезапустите `greenplum` и повторите DDL. - Для технических деталей см. `docs/reference/pxf_bookings.md`. + Для технических деталей PXF см. ветку `solution` (`docs/reference/pxf_bookings.md`). ## Рекомендации по качеству решения -Ревью решения и список улучшений, которые делают пайплайн более “эталонным” для обучения: -`docs/archive/bookings_stg_code_review.md`. +Ревью решения и список улучшений — в ветке `solution` (`docs/archive/`). diff --git a/docs/design/PRD.md b/docs/design/PRD.md deleted file mode 100644 index 3a84e70..0000000 --- a/docs/design/PRD.md +++ /dev/null @@ -1,244 +0,0 @@ -# PRD: Greenplum Bookings DWH - -> Курсовая работа для курса [DE Roadmap](https://github.com/dementev-dev/de-roadmap). -> Статус: **ЧЕРНОВИК v0.1** | Дата: 2026-03-08 - ---- - -## 1. Видение продукта - -**Greenplum Bookings DWH** — учебный стенд, на котором студент самостоятельно строит -end-to-end ETL-пайплайн: от базы-источника до аналитических витрин. - -Стенд имитирует реальную рабочую задачу Data-инженера: -- Есть «боевая» система-источник (bookings-db), в которой каждый день появляются - новые данные — как в жизни, без ограниченного объёма. -- Есть DWH на Greenplum с классическими слоями (STG → ODS → DDS → DM). -- Есть Airflow, оркестрирующий загрузку. -- Есть ТЗ от «аналитика» с описанием ожидаемых таблиц и маппингов. - -Студент получает **частично реализованный пайплайн** (эталонный вертикальный срез) -и **дореализует остальное** по ТЗ — SQL-скрипты и таски в DAG. - -### Почему именно bookings? - -Домен бронирования авиабилетов выбран не ради предметной области, а благодаря -генератору данных: каждый вызов `make bookings-generate-day` создаёт новый день -с реалистичным объёмом. Это даёт бесконечный поток инкрементальных данных — -как в настоящей production-системе. - ---- - -## 2. Целевая аудитория и пререквизиты - -**Кто:** студенты курса DE Roadmap, дошедшие до раздела «Курсовая работа». - -**Что уже умеют** (к моменту старта): -- Git: ветки, PR, merge, GitFlow -- SQL: JOIN, CTE, оконные функции, планы запросов, моделирование (3NF, звезда, SCD) -- Python: скрипты, pandas, базовое ООП -- Docker: запуск контейнеров, логи, docker-compose -- Airflow: понятие DAG, операторы, зависимости, UI, логи -- Greenplum: распределение по сегментам, skew, EXPLAIN, отличие от Postgres - -**Уровень:** уверенный джун, готовящийся к первым собеседованиям. - ---- - -## 3. Учебные результаты (Learning Outcomes) - -После выполнения курсовой студент умеет: - -1. **Проектировать и реализовывать ETL-пайплайн** по слоям DWH - (STG → ODS → DDS → DM) на реальном стеке Airflow + Greenplum. -2. **Читать ТЗ от аналитика** (маппинги, описания таблиц) и превращать его - в работающий SQL + DAG. -3. **Писать идемпотентные загрузки** с инкрементальностью (HWM, _load_id, - delete+insert), понимая, почему в Greenplum не используется MERGE. -4. **Реализовывать SCD1/SCD2** и объяснять, когда что применяется. -5. **Настраивать и проверять Data Quality** — понимает, зачем DQ-проверки - и как их встроить в пайплайн. -6. **Работать с Greenplum** как с MPP: выбирать distribution key, - понимать heap vs AO, читать планы запросов. -7. **Оформить проект как портфолио** — репозиторий пригоден для упаковки - в резюме как реальный опыт работы с Airflow и Greenplum. - ---- - -## 4. Скоуп - -### В скоупе (In Scope) - -| Компонент | Описание | -|-----------------------|-------------------------------------------------------------| -| Источник данных | bookings-db (Postgres) с генератором дней | -| DWH | Greenplum, 4 слоя: STG, ODS, DDS, DM | -| Оркестрация | Apache Airflow (PostgresOperator + SQL-файлы) | -| Федеративный доступ | PXF (чтение из Postgres в Greenplum) | -| Инфраструктура | Docker Compose (полный стенд в одной команде) | -| Data Quality | DQ-проверки, встроенные в DAG | -| Документация | README, ТЗ, design docs, naming conventions | - -### Вне скоупа (Out of Scope) - -| Что | Почему | -|-----------------------|-------------------------------------------------------------| -| Kafka / стриминг | Отдельный стенд в курсе | -| BI-инструменты | Фокус на ETL, не на визуализации | -| CI/CD | Избыточно для курсовой | -| Spark / Trino / dbt | Отдельные стенды в курсе | -| Второй источник | Усложнение без пропорциональной учебной ценности | -| CSV-пайплайн | Вынести в [airflow-manual](https://github.com/dementev-dev/airflow-manual) | -| Облачная инфраструктура | Всё локально, через Docker | - ---- - -## 5. Архитектура стенда - -### Сервисы (Docker Compose) - -``` -bookings-db (Postgres 16) ──PXF──> Greenplum 6.27 - ├── stg.* (стейджинг) - ├── ods.* (операционное хранилище) - ├── dds.* (детальное хранилище) - └── dm.* (витрины) - -pgmeta (Postgres 16) ─────────────> Airflow (webserver + scheduler) -``` - -### Слои DWH - -| Слой | Назначение | Паттерн загрузки | Кол-во таблиц | -|------|-----------------------------------|---------------------------|---------------| -| STG | Зеркало источника | TRUNCATE + INSERT (batch) | 9 | -| ODS | Нормализованное хранилище | SCD1 UPSERT | 9 | -| DDS | Измерения + факты (Kimball) | SCD1/SCD2 + fact load | 7 (6D + 1F) | -| DM | Аналитические витрины | HWM-инкремент | 5 | - -### Сущности - -| STG / ODS | DDS | DM | -|----------------------|--------------------------|-----------------------| -| bookings | dim_airports | airport_traffic | -| tickets | dim_airplanes | monthly_overview | -| airports | dim_passengers | passenger_loyalty | -| airplanes | dim_routes (SCD2) | route_performance | -| routes | dim_calendar | sales_report | -| seats | dim_tariffs | | -| flights | fact_flight_sales | | -| segments | | | -| boarding_passes | | | - ---- - -## 6. Педагогическая модель - -Подробности — в [assignment_design.md](assignment_design.md). - -### Принцип: «Эталонный срез + ТЗ» - -Студент получает репозиторий, в котором: - -1. **Эталонный вертикальный срез** — полностью реализованная цепочка - `sales_report` и все её источники вниз по слоям (STG → ODS → DDS → DM). -2. **ТЗ от аналитика** — описание остальных таблиц - (analyst_spec.md — будет создан на Этапе 3, см. TODO.md). -3. **Частично готовый DAG** — студент добавляет свои таски по аналогии. -4. **Валидационный DAG** — студент запускает для самоконтроля. - -### Что делает студент - -- Пишет DDL, SQL-загрузки, DQ-проверки для назначенных таблиц -- Добавляет таски в существующий DAG -- Проверяет результат через валидационный DAG и запросы в Greenplum - -### Что студент НЕ делает - -- Не поднимает инфраструктуру с нуля (Docker Compose дан) -- Не пишет DAG с нуля (шаблон дан) -- Не настраивает Airflow Connections (преднастроены) -- Не работает с PXF-конфигурацией (настроен) - ---- - -## 7. Ветки и workflow - -``` -main (стартовое состояние) - ├── Эталонный срез: реализованные таблицы + DAG - ├── ТЗ от аналитика - ├── Инфраструктура (Docker, Make, PXF) - ├── Заглушки / TODO-маркеры для студенческих заданий - └── Валидационный DAG для самоконтроля - -solution (полное решение) - └── Все таблицы реализованы — эталон для самопроверки - и подсказка, если студент застрял -``` - -### Workflow студента - -1. Форкает репозиторий -2. Читает README и ТЗ -3. `make up` — поднимает стенд -4. `make bookings-init` — инициализирует источник -5. Запускает DDL-DAG'и (эталонные таблицы создаются) -6. Запускает ETL-DAG'и — эталонный срез работает -7. Реализует задания из ТЗ (SQL + таски в DAG) -8. Проверяет себя через валидационный DAG -9. `make bookings-generate-day` — генерирует новый день, проверяет - инкрементальность -10. Защищает работу перед ментором - ---- - -## 8. Критерии приёмки курсовой - -### Для студента (самопроверка) - -- [ ] Стенд поднимается (`make up`) без ошибок -- [ ] Все DAG'и проходят без failed-тасков -- [ ] Данные доезжают от STG до DM -- [ ] Валидационный DAG проходит на всех реализованных таблицах -- [ ] После `make bookings-generate-day` + повторного запуска DAG - данные корректно доливаются (инкрементальность работает) - -### Для ментора (ревью + защита) - -- [ ] Код соответствует naming conventions (`docs/design/naming_conventions.md`) -- [ ] SQL идемпотентен (повторный запуск не ломает данные) -- [ ] Distribution keys выбраны осмысленно -- [ ] Студент может объяснить: почему delete+insert, а не MERGE; - разницу SCD1/SCD2; что такое HWM; как работает _load_id -- [ ] Код оформлен для портфолио (чистый Git-history, README) - ---- - -## 9. Ограничения и риски - -| Риск / ограничение | Митигация | -|--------------------------------------------|-------------------------------------------------| -| Стенд тяжёлый (~8-16 GB RAM) | Указать минимальные требования; не утяжелять | -| bookings-db генерирует данные медленно | Не добавлять нагрузку; задокументировать ожидание| -| Студент может застрять надолго | Ветка `solution` как подсказка; еженедельные встречи | -| Greenplum 6.x — устаревающая версия | Для учебных целей достаточно; паттерны переносимы | -| PXF нестабилен при холодном старте | Задокументировано в README; healthcheck настроен | - -### Требования к машине студента - -- 2-4 CPU, 8-16 GB RAM, 25-40 GB диска -- Linux / WSL2 / macOS -- Docker + Docker Compose - ---- - -## 10. План работ - -План с чекбоксами и рекомендациями по инструментам: [TODO.md](../../TODO.md). - ---- - -## 11. Открытые вопросы - -1. **Название** — рабочее: «Greenplum Bookings DWH». Финализировать. diff --git a/docs/design/assignment_design.md b/docs/design/assignment_design.md deleted file mode 100644 index 13d18db..0000000 --- a/docs/design/assignment_design.md +++ /dev/null @@ -1,173 +0,0 @@ -# Дизайн курсового задания - -> Тактические решения по нарезке задания, порядку выполнения и самопроверке. -> Стратегию и контекст см. в [PRD.md](PRD.md). - ---- - -## 1. Эталонный срез: витрина `sales_report` - -Эталоном выбрана витрина `dm.sales_report` и вся её цепочка вниз по слоям. - -**Почему `sales_report`:** -- Покрывает SCD1 (airports, tariffs), HWM-инкремент, fact load -- Богатая денормализация — хороший образец для подражания -- Средняя сложность — не пугает, но и не тривиальна - -### Эталонные таблицы (даны студенту) - -| Слой | Таблицы | -|------|-----------------------------------------------------------------| -| DM | `sales_report` | -| DDS | `fact_flight_sales`, `dim_airports` (SCD1), `dim_tariffs` (SCD1), `dim_calendar` | -| ODS | `bookings`, `tickets`, `segments`, `flights`, `boarding_passes`, `airports`, `routes` | -| STG | весь слой: `bookings`, `tickets`, `segments`, `flights`, `boarding_passes`, `airports`, `airplanes`, `seats`, `routes` | - -### Задание студенту - -| Слой | Таблицы | Что нового для студента | -|------|-------------------------------------------------------------------|--------------------------------------------------| -| ODS | `airplanes`, `seats` | Практика TRUNCATE+INSERT по аналогии с эталоном | -| DDS | `dim_airplanes` (SCD1), `dim_passengers` (SCD1), `dim_routes` (SCD2) | **SCD2 — ключевой вызов курсовой** | -| DM | `airport_traffic`, `monthly_overview`, `route_performance`, `passenger_loyalty` | Разная сложность (от простой к сложной) | - ---- - -## 2. Рекомендуемый порядок выполнения - -Студенту рекомендуется (но не обязательно) двигаться в таком порядке: - -1. **ODS** (airplanes, seats) — практика TRUNCATE+INSERT -2. **DDS** dim_airplanes, dim_passengers (SCD1) — новые измерения -3. **DDS** dim_routes (**SCD2**) — ключевой вызов -4. **DM** airport_traffic — простая витрина, похожа на sales_report -5. **DM** route_performance — TRUNCATE+INSERT, SCD2-агрегация по BK -6. **DM** monthly_overview — двухуровневая агрегация -7. **DM** passenger_loyalty — самая сложная, пересчёт истории - -Порядок выстроен от простого к сложному. Каждый шаг опирается на опыт -предыдущего. - ---- - -## 3. SCD2: подход «рецепт без готового SQL» - -Реализация `dim_routes` (SCD2) — ключевой вызов курсовой. Студент делает это -самостоятельно, но ТЗ содержит пошаговую подсказку: - -1. Алгоритм SCD2 текстом (без SQL): - - Вычисли `hashdiff` по набору атрибутов (атрибуты перечислены в ТЗ) - - Найди строки, у которых `hashdiff` изменился - - Закрой старую версию (`valid_to = текущая_дата`) - - Вставь новую версию (`valid_from = текущая_дата`, `valid_to = NULL`) -2. Формула hashdiff: `md5(concat_ws('|', field1, field2, ...))` -3. Ссылка на `naming_conventions.md` (поля `valid_from`, `valid_to`, `hashdiff`) -4. Напоминание: полуоткрытый интервал `[valid_from, valid_to)` -5. Если застрял — ветка `solution` - -Самостоятельная реализация — ключ к запоминанию. SCD2 — обязательный вопрос -на собеседованиях DE, и студент должен уметь объяснить его на основе -собственного опыта. - ---- - -## 4. Валидационный DAG (`bookings_validate`) - -Отдельный DAG для самопроверки студента. Запускается вручную в Airflow UI -после реализации заданий. Таски сгруппированы по слоям — студент видит, -где именно проблема. Дополнительный бонус — практика чтения логов Airflow. - -### Примерная структура тасков - -``` -bookings_validate -├── validate_ods -│ ├── check_ods_airplanes_rowcount (ODS >= STG по кол-ву уникальных BK) -│ ├── check_ods_seats_rowcount -│ └── check_ods_no_null_pks (PK not null) -├── validate_dds -│ ├── check_dim_airplanes_exists -│ ├── check_dim_passengers_exists -│ ├── check_dim_routes_scd2 (valid_from/valid_to корректны) -│ └── check_dim_routes_no_gaps (нет «дыр» в версиях SCD2) -└── validate_dm - ├── check_airport_traffic_exists - ├── check_monthly_overview_exists - ├── check_route_performance_exists - └── check_passenger_loyalty_exists -``` - -### Реализация - -- `PostgresOperator` + SQL-скрипты в `sql/validate/` -- Каждый SQL-скрипт выполняет SELECT и бросает исключение (через - `DO $$ ... RAISE EXCEPTION ... $$`), если проверка не пройдена -- Сообщения об ошибках — дружелюбные, с подсказкой что делать дальше - -### Ключевые проверки - -- Таблицы существуют и содержат данные -- PK не содержат NULL -- SCD2: `valid_to IS NULL` для текущих версий, нет перекрытий интервалов -- Кросс-слойная консистентность (row count ODS vs STG) -- DM-витрины содержат данные за загруженные дни - -### Активная проверка SCD2 (`check_dim_routes_scd2`) - -Справочник `bookings.routes` в демо-базе статичен — маршруты не меняются -между запусками генератора. Поэтому при обычном прогоне пайплайна студент -никогда не увидит, как SCD2 закрывает старую версию и создаёт новую. - -Чтобы проверить корректность реализации, таск `check_dim_routes_scd2` -должен быть **активным** (не только читать, но и тестировать загрузку): - -1. Сохранить текущее состояние `ods.routes` и `dds.dim_routes` (temp-таблицы). -2. Вставить в `ods.routes` тестовый маршрут с изменённым атрибутом - (например, `scheduled_time` → `departure_time` сдвинут на 1 час). -3. Вызвать студенческий SQL загрузки `dim_routes` (`sql/dds/dim_routes_load.sql`). -4. Проверить результат: - - Старая версия маршрута закрыта (`valid_to IS NOT NULL`). - - Новая версия открыта (`valid_to IS NULL`, `hashdiff` отличается). - - Нет «дыр» между `valid_to` старой и `valid_from` новой версии. -5. Откатить изменения: восстановить `ods.routes` и `dds.dim_routes` - из сохранённых temp-таблиц. - -Это единственный способ гарантировать, что SCD2 работает, без мутации -источника (что сломало бы генератор `continue()`). - ---- - -## 5. Формат ТЗ от аналитика - -Файл: `docs/assignment/analyst_spec.md` (или несколько файлов по слоям). - -Для каждой таблицы-задания документ содержит: - -- **Имя таблицы** и целевая схема (stg / ods / dds / dm) -- **Описание** — что хранит таблица, бизнес-смысл -- **Список полей** с типами и описанием -- **Маппинг источников** — откуда берётся каждое поле -- **Бизнес-правила и фильтры** (если есть) -- **Тип историзации** (SCD1 / SCD2 / snapshot / append) -- **Гранулярность** (одна строка = ?) -- **Distribution key** (подсказка или задание на выбор) - -Формат — приближен к реальным ТЗ, которые студент встретит на работе. - ---- - -## 6. Бэклог: идеи для будущих итераций - -### PXF-практикум (замена STG-заданий) - -После переноса всего STG-слоя в эталон (см. `docs/plans/2026-03-11_routes-to-reference.md`) -студенты не практикуются в создании PXF external tables и загрузке сырых данных. -Это важный навык для DE — нужно компенсировать отдельным заданием. - -Варианты: -- **Новый источник данных**: подключить CSV/JSON-файл (например, справочник городов - или курсов валют) через PXF и загрузить в отдельную STG-таблицу -- **Мини-задание «подключи внешний справочник»**: студент создаёт PXF external table - для существующей таблицы источника и сравнивает результат с эталонной STG -- **Лабораторная работа**: отдельное упражнение на создание external table + - сравнение форматов (TEXT vs CUSTOM), профилей PXF, обработку типов diff --git a/docs/design/bookings_dds_design.md b/docs/design/bookings_dds_design.md index 7fa97d5..04d25ed 100644 --- a/docs/design/bookings_dds_design.md +++ b/docs/design/bookings_dds_design.md @@ -569,8 +569,9 @@ UPDATE факта обновляет только **мутабельные по 1. Таблица не пуста 2. Нет дублей по зерну `(ticket_no, flight_id)` 3. Количество строк = `COUNT(*)` из `ods.segments` -4. **Обязательные FK**: `passenger_sk IS NULL` = 0, `tariff_sk IS NULL` = 0 -5. **FK маршрута**: NULL в любом из `route_sk`, `departure_airport_sk`, `arrival_airport_sk`, `airplane_sk` — допустимо при аномалиях, считаем и логируем (`RAISE NOTICE`); фейлим если > 1% строк +4. **Обязательные FK**: `tariff_sk IS NULL` = 0. На main `passenger_sk` — NOTICE (dim_passengers — заглушка); на solution — EXCEPTION. +5. **Эталонные FK аэропортов**: `departure_airport_sk`, `arrival_airport_sk` (через `ods.routes`) — порог 1%, EXCEPTION. + **Студенческие FK**: `route_sk`, `airplane_sk` — на main NOTICE (dim_routes — заглушка, 100% NULL допустимо); на solution — порог 1%, EXCEPTION. 6. **Calendar**: `calendar_sk IS NULL` — допустимо если `scheduled_departure IS NULL` в ODS; считаем и логируем (`RAISE NOTICE`); фейлим если > 1% строк 7. Обязательные поля: `book_ref`, `ticket_no`, `flight_id`, `is_boarded` не NULL diff --git a/docs/design/bookings_ods_design.md b/docs/design/bookings_ods_design.md index 139382c..f77df92 100644 --- a/docs/design/bookings_ods_design.md +++ b/docs/design/bookings_ods_design.md @@ -512,7 +512,9 @@ ANALYZE ods.bookings; ### 6.4. Поведение при пустом батче - для инкрементальных таблиц (`bookings`, `tickets`, `flights`, `segments`, `boarding_passes`) пустой батч допустим; -- для snapshot-справочников (`airports`, `airplanes`, `routes`, `seats`) пустой батч считаем ошибкой. +- для snapshot-справочников (`airports`, `routes`) пустой батч считаем ошибкой. + На main `airplanes` и `seats` — студенческие заглушки (load/dq = `SELECT 1;`), + таблицы будут пустыми до реализации студентом. --- @@ -531,7 +533,8 @@ ANALYZE ods.bookings; 3. **Обязательные поля** не `NULL`/не пустые. -4. **Батч не пустой** для snapshot-справочников (`airports`, `airplanes`, `routes`, `seats`): если STG-батч оказался пустым — это ошибка (источник недоступен или PXF не работает). +4. **Батч не пустой** для эталонных snapshot-справочников (`airports`, `routes`): если STG-батч оказался пустым — это ошибка (источник недоступен или PXF не работает). + На main `airplanes_dq.sql` и `seats_dq.sql` — студенческие заглушки. 5. **Ссылочная целостность** в ODS: - `tickets.book_ref -> bookings.book_ref` @@ -617,7 +620,8 @@ tests/test_dags_smoke.py (+ smoke для 2 новых DAG) resolve_stg_batch_id ├-> load_ods_bookings -> dq_ods_bookings -> load_ods_tickets -> dq_ods_tickets ──────────────────┐ ├-> load_ods_airports -> dq_ods_airports ─┐ │ - ├-> load_ods_airplanes -> dq_ods_airplanes ─┼-> load_ods_routes -> dq_ods_routes │ + ├-> load_ods_airplanes -> dq_ods_airplanes ─┐ + │ ├-> load_ods_routes -> dq_ods_routes │ └-> └-> load_ods_seats -> dq_ods_seats │ │ dq_ods_routes -> load_ods_flights -> dq_ods_flights │ @@ -631,7 +635,8 @@ resolve_stg_batch_id Зависимости (по FK): - `tickets` после `bookings` (FK: `book_ref`); -- `routes` после `airports` и `airplanes` (FK: `departure_airport`, `arrival_airport`, `airplane_code`); +- `routes` после `airports` (FK: `departure_airport`, `arrival_airport`). + На ветке `solution` также зависит от `airplanes` (FK: `airplane_code`); - `seats` после `airplanes` (FK: `airplane_code`); - `flights` после `routes` (FK: `route_no`); - `segments` после `flights` и `tickets` (FK: `flight_id`, `ticket_no`); diff --git a/docs/design/bookings_stg_design.md b/docs/design/bookings_stg_design.md index 72be507..237c95e 100644 --- a/docs/design/bookings_stg_design.md +++ b/docs/design/bookings_stg_design.md @@ -2,7 +2,7 @@ ## 1. Цель и общий контур -- Источник: Postgres в контейнере `bookings-db`, база `demo`, таблица `bookings.bookings` (см. [`docs/reference/bookings_tz.md`](../reference/bookings_tz.md)). +- Источник: Postgres в контейнере `bookings-db`, база `demo`, таблица `bookings.bookings`. - Цель: показываем путь данных от операционной БД до сырого слоя DWH в Greenplum. - В этом документе описываем часть `src (bookings-db) → STG (Greenplum)`. STG — входной слой; далее данные обрабатываются в ODS → DDS → DM (см. соответствующие design-документы). @@ -127,8 +127,7 @@ DDL определён в `sql/stg/bookings_ddl.sql` и подключается ## 5. Связь с остальными документами -- [`docs/reference/bookings_tz.md`](../reference/bookings_tz.md) — как готовится и генерируется источник `bookings-db`. -- [`docs/reference/pxf_bookings.md`](../reference/pxf_bookings.md) — детали настройки PXF и внешней таблицы для чтения из `bookings-db`. +- Детали настройки PXF и часовых поясов — в ветке `solution` (`docs/reference/`). - `sql/stg/bookings_ddl.sql` — DDL для схемы `stg` и таблиц `stg.bookings_ext` / `stg.bookings` (подключается из `sql/ddl_gp.sql` и применяется через `make ddl-gp`). Дальнейшая обработка данных описана в design-документах ODS/DDS/DM (см. раздел 5). diff --git a/docs/design/db_schema.md b/docs/design/db_schema.md index 9e3d34d..6ed8f58 100644 --- a/docs/design/db_schema.md +++ b/docs/design/db_schema.md @@ -310,6 +310,5 @@ Degenerate keys: `book_ref`, `ticket_no`, `flight_id`, `book_date`, `seat_no`. - [`bookings_dds_design.md`](bookings_dds_design.md) — дизайн DDS (Star Schema, SCD2) - [`bookings_dm_design.md`](bookings_dm_design.md) — дизайн DM (5 витрин) - [`naming_conventions.md`](naming_conventions.md) — нейминг полей -- [`../reference/bookings_tz.md`](../reference/bookings_tz.md) — часовые пояса -- [`../reference/pxf_bookings.md`](../reference/pxf_bookings.md) — настройка PXF +- Часовые пояса и настройка PXF — в ветке `solution` (`docs/reference/`) - [`../assignment/analyst_spec.md`](../assignment/analyst_spec.md) — ТЗ от аналитика (курсовое задание) diff --git a/docs/e2e-etl-test-protocol.md b/docs/e2e-etl-test-protocol.md deleted file mode 100644 index 582684f..0000000 --- a/docs/e2e-etl-test-protocol.md +++ /dev/null @@ -1,184 +0,0 @@ -# Протокол сквозного (E2E) тестирования ETL - -Этот документ описывает процедуру полной проверки цепочки ETL: `STG -> ODS -> DDS -> DM`. -Цель теста — убедиться в корректности инкрементальной загрузки, работы паттерна `Temporary Table` и механизмов `HWM`. - -**Управление DAG'ами — только через Airflow UI или REST API.** -Не используйте CLI-команды внутри контейнера (`docker exec ... airflow dags ...`) — они могут молча не сработать (например, `unpause` не снимает паузу). REST API работает надёжно и приближает опыт к реальной боевой эксплуатации. - ---- - -## 1. Подготовка окружения - -Убедитесь, что все сервисы запущены и генератор инициализирован. - -```bash -make up -make bookings-init -``` - -**Доступ к API:** -В `docker-compose.yml` включена базовая аутентификация (`basic_auth`). -Для curl-запросов используйте учетные данные из вашего `.env` файла (переменные `AIRFLOW_USER` и `AIRFLOW_PASSWORD`, по умолчанию `admin:admin`). - -- **UI:** `http://localhost:8080` -- **API Endpoint:** `http://localhost:8080/api/v1` - ---- - -## 2. Очистка данных (Reset) - -Перед началом теста необходимо полностью очистить все слои DWH. - -```bash -make dwh-truncate -``` -*(Если вы меняли DDL, лучше полностью пересоздать схемы: `make gp-psql -c "DROP SCHEMA IF EXISTS ods CASCADE; DROP SCHEMA IF EXISTS dds CASCADE; DROP SCHEMA IF EXISTS dm CASCADE; CREATE SCHEMA ods; CREATE SCHEMA dds; CREATE SCHEMA dm;"`)* - ---- - -## 3. Этап 1: Создание схем и Загрузка за первый день (Initial Load) - -Рекомендуется запускать слои последовательно, дожидаясь завершения предыдущего. - -### Создание DDL -Откройте **Airflow UI** (`http://localhost:8080`) и нажмите кнопку **▶ Play -> Trigger DAG** для DDL-дагов: -1. `bookings_stg_ddl` -2. `bookings_ods_ddl` -3. `bookings_dds_ddl` -4. `bookings_dm_ddl` - -### Запуск пайплайна (Day 1) -После успешного создания таблиц, запустите DAG загрузки `bookings_to_gp_stage`. - -**Через UI:** откройте http://localhost:8080, **снимите DAG с паузы** (переключатель слева от названия) и нажмите **▶ Play -> Trigger DAG**. - -**Через REST API (рекомендуется для автоматизации и агентов):** - -**Важно:** при старте стенда все DAG-и находятся на паузе. Каждый DAG перед первым запуском нужно «разморозить» (unpause), иначе запуск зависнет в статусе `queued`. Для каждого DAG требуется два шага: **1) unpause, 2) trigger**. - -```bash -# 1. Снятие с паузы (обязательно перед первым запуском!) -curl -s -X PATCH "http://localhost:8080/api/v1/dags/bookings_to_gp_stage" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \ --H "Content-Type: application/json" \ --d '{"is_paused": false}' - -# 2. Запуск -curl -s -X POST "http://localhost:8080/api/v1/dags/bookings_to_gp_stage/dagRuns" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \ --H "Content-Type: application/json" \ --d '{}' -``` - -### Проверка статуса -Дождитесь, пока DAG перейдет в статус `success` (следите в UI или опрашивайте через API): -```bash -curl -s "http://localhost:8080/api/v1/dags/bookings_to_gp_stage/dagRuns?order_by=-execution_date&limit=1" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" | python3 -c "import sys,json; print(json.load(sys.stdin)['dag_runs'][0]['state'])" -``` - -### Запуск остальных слоёв -Поочерёдно запускайте каждый следующий слой **по той же схеме: unpause + trigger** (замените ``): -1. `bookings_to_gp_ods` -2. `bookings_to_gp_dds` -3. `bookings_to_gp_dm` - -```bash -# Повторите для каждого DAG из списка выше -DAG_ID=bookings_to_gp_ods - -# 1. Unpause -curl -s -X PATCH "http://localhost:8080/api/v1/dags/${DAG_ID}" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \ --H "Content-Type: application/json" \ --d '{"is_paused": false}' - -# 2. Trigger -curl -s -X POST "http://localhost:8080/api/v1/dags/${DAG_ID}/dagRuns" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \ --H "Content-Type: application/json" \ --d '{}' -``` - ---- - -## 4. Этап 2: Проверка инкремента - -Эмулируйте появление данных за второй день и проверьте дозагрузку. DAG слоя STG автоматически сгенерирует новый день в базе-источнике перед загрузкой. - -```bash -# Повторный запуск цепочки ETL (DAG STG сам сгенерирует новый день) -# Снова нажмите "Trigger DAG" в UI для каждого слоя (STG -> ODS -> DDS -> DM). -``` - ---- - -## 5. Цикл отладки: Логи и Перезапуск (Clear) - -Если DAG упал, **не нужно пересоздавать стенд с нуля**. Airflow позволяет исправить код и перезапустить только упавшие задачи. - -Для выполнения команд ниже вам понадобится **Run ID** упавшего запуска. Его можно скопировать из UI (вкладка *Graph* -> кликнуть на фон сетки -> вкладка *Details* -> `Run ID`) или получить последним API-запросом: - -```bash -# Получить Run ID последнего запуска ODS -curl -s "http://localhost:8080/api/v1/dags/bookings_to_gp_ods/dagRuns?order_by=-execution_date&limit=1" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" | grep -o '"dag_run_id": "[^"]*"' -``` - -### Чтение логов через API -Подставьте ваш `` (например, `manual__2026-03-03T...`) и имя упавшей таски: -```bash -curl -s "http://localhost:8080/api/v1/dags/bookings_to_gp_ods/dagRuns//taskInstances//logs/1" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" -``` - -### Перезапуск задачи (Clear) -1. Прочитайте ошибку в логах. -2. Исправьте SQL-файл локально на хосте. -3. Очистите состояние упавших задач (`only_failed: true`) в конкретном запуске, передав ваш ``: - -```bash -curl -s -X POST "http://localhost:8080/api/v1/dags/bookings_to_gp_ods/clearTaskInstances" \ ---user "${AIRFLOW_USER}:${AIRFLOW_PASSWORD}" \ --H "Content-Type: application/json" \ --d '{ - "only_failed": true, - "reset_dag_runs": true, - "dag_run_id": "" -}' -``` -После этого планировщик подхватит обновленный SQL-код и продолжит выполнение DAG с точки падения. Вы также можете сделать это в UI: клик по упавшей задаче -> кнопка **Clear**. - ---- - -## 6. Финальная верификация (Критерии успеха) - -Выполните SQL-запрос для сверки данных: - -```bash -make gp-psql -c " -SELECT 'STG' as layer, COUNT(*) FROM stg.bookings -UNION ALL -SELECT 'ODS' as layer, COUNT(*) FROM ods.bookings -UNION ALL -SELECT 'DDS' as layer, COUNT(*) FROM dds.fact_flight_sales -UNION ALL -SELECT 'DM ' as layer, COUNT(*) FROM dm.sales_report; -" -``` - -**Критерии корректности:** -1. **Инкремент STG**: Количество строк в `stg.bookings` после Этапа 2 больше, чем после Этапа 1. -2. **Инкремент ODS**: Количество строк в `ods.bookings` выросло. В ODS нет дублей (`SELECT book_ref FROM ods.bookings GROUP BY book_ref HAVING COUNT(*) > 1` должен вернуть 0 строк). -3. **ODS Справочники**: Количество строк в `ods.airports` и `ods.routes` не должно меняться между днями (работает паттерн TRUNCATE+INSERT полного снимка). -4. **HWM в DM**: Витрина `sales_report` содержит данные за оба дня. Значение `COUNT(*)` после Этапа 2 выросло. -5. **Lineage**: Поля `_load_id` и `_load_ts` во всех слоях содержат метки соответствующих запусков (`manual__...`). - ---- - -## 7. Зафиксированный опыт (Типичные ошибки) - -- **Рассинхронизация DDL и Load скриптов**: Частая причина падения ODS/DDS — несовпадение имен колонок (например, `amount` vs `segment_amount`) или типов данных (например, `INTEGER[]` vs `TEXT`) между схемой таблицы и запросом загрузки. Внимательно читайте логи задачи. -- **Работа с массивами**: При генерации `hashdiff` в Greenplum/PostgreSQL нельзя использовать пустую строку `''` в `COALESCE` для массива. Массив нужно предварительно привести к тексту: `COALESCE(days_of_week::TEXT, '')`. -- **Кавычки в psql**: При выполнении ручных проверок через `psql -c "..."` помните, что строковые литералы должны оборачиваться в **одинарные кавычки** (`'text'`), а двойные кавычки (`"text"`) интерпретируются как идентификаторы колонок. diff --git a/docs/plans/2026-03-12_main-solution-split.md b/docs/plans/2026-03-12_main-solution-split.md deleted file mode 100644 index 911332c..0000000 --- a/docs/plans/2026-03-12_main-solution-split.md +++ /dev/null @@ -1,342 +0,0 @@ -# Plan: Подготовка main + ветка solution (Этап 4) - -> Дата: 2026-03-12 | Версия: 4 (после третьего ревью Codex) -> Ветка: chore/bookings-etl → main → solution -> Спецификации кода: `docs/archive/2026-03-11_routes-to-reference.md` (п.2–5, п.8, п.10–11) - ---- - -## 1. Мотивация - -### Зачем раскладка по веткам - -Сейчас весь код (эталон + студенческие задания) живёт в ветке `chore/bookings-etl`. -Студент должен получить **частично реализованный пайплайн**: эталонный вертикальный -срез работает «из коробки», а студенческие файлы — заглушки (`SELECT 1; -- TODO`). -Полная реализация доступна в ветке `solution` для самопроверки. - -### Почему именно такой порядок - -**Рассмотренные варианты:** - -| # | Схема | Проблема | -|---|-------|----------| -| A | Создать solution от bookings-etl, потом на main — cherry-pick + заглушки | Грязные cherry-pick'и, main временно «не main» | -| B | Мерж в main, заглушки на main, solution = cherry-pick эталона обратно | main в какой-то момент содержит полный код, потом — заглушки; solution — через cherry-pick | -| **C** | **Мерж в main → общие правки → ветка solution (снимок) → main-only правки** | **Выбран** | - -**Почему вариант C:** - -- **main всегда остаётся main.** Нет момента, когда main «подменяется» другой веткой. - Любые CI/CD, ссылки, клонирования — работают непрерывно. -- **Простая линейная история.** Мерж → общие правки → бранч → main-only коммиты. - Никаких cherry-pick'ов. -- **solution — архивный снимок.** Не ожидается активная разработка. Студент сверяется - с ним, а не мержит. -- **Общие правки до ветвления.** Документация, актуальная для обеих веток, - обновляется *до* создания solution. Solution получает её автоматически. - -### Стратегия синхронизации веток - -После раскладки `main` (заглушки) и `solution` (полный код) расходятся. - -**Главный принцип: solution — source of truth.** - -Все изменения начинаются на solution (или feature-ветке от solution), -затем портируются в main. Направление потока: **solution → main**. - -Файлы делятся на три категории: - -| Категория | Примеры | Правило | -|-----------|---------|---------| -| **Общий код** | STG SQL, docker, Makefile, инфраструктура | Фиксим на solution, cherry-pick в main | -| **Общая документация** | README, TESTING, stack.md, naming_conventions | Фиксим на solution, cherry-pick в main | -| **Branch-specific** | заглушки load/dq, fact_flight_sales_dq (ослабленный), routes_dq (без RI), ODS DAG (без airplanes→routes), branch-specific формулировки в design docs | Фиксим на нужной ветке. Конфликтов нет — содержимое файлов разное | - -**Исключение:** main-only правка (опечатка в заглушке, битая ссылка -в main-only документе) — правим прямо на main, solution не трогаем. - -**Главное правило: никогда не мержим main → solution целиком.** - -> Детальный протокол синхронизации — открытый вопрос, см. секцию 3. - ---- - -## 2. Порядок выполнения - -### Шаг 1. PR chore/bookings-etl → main - -- Создать PR, ревью -- Мерж (squash или обычный — на усмотрение) -- После мержа: `git checkout main && git pull` - -### Шаг 2. Общие правки на main (до ветвления solution) - -> **Bootstrap-исключение:** стратегия синхронизации (секция 1) определяет -> solution как source of truth с потоком solution → main. Но при первичной -> раскладке solution ещё не существует — общие правки делаются на main, -> и solution наследует их при ветвлении (шаг 3). После создания solution -> действует штатный протокол. - -Эти изменения отражают код, уже изменённый на bookings-etl -(fact_flight_sales_load.sql использует ods.routes). Документация должна -соответствовать коду на **обеих** ветках. - -#### 2.1. Документация (общая для обеих веток) - -- `docs/design/bookings_dds_design.md` — обновить описание fact lookup - (airports через ods.routes, airplane через dim_routes point-in-time) -- `docs/bookings_to_gp_dds.md` — обновить описание DDS lookup -- `docs/bookings_to_gp_ods.md` — обновить описание ODS -- `docs/design/db_schema.md` — пометить STG + ods.routes как эталон - -#### 2.2. TODO.md — переписать - -Текущий текст Этапа 4 в TODO.md описывает устаревшую стратегию -(удалить routes из STG, убирать `\i` из ddl_gp.sql). Нужно обновить, -чтобы на solution осталась корректная история: - -- Отметить этапы 2, 3 выполненными (✅) -- **Переписать** текст этапа 4 (не просто поставить галочку) — привести - в соответствие с фактической стратегией (данный план) -- Отметить этап 4 выполненным после завершения - -### Шаг 3. Создать ветку solution - -```bash -git checkout main -git checkout -b solution -git push -u origin solution -``` - -Solution получает: полный эталонный код + актуальную общую документацию + -все design docs, plans, archive, TODO.md — полный контекст для мейнтейнера. - -### Шаг 4. Main-only правки (заглушки, ослабление DQ) - -Работаем на main (или на feature-ветке → PR в main). - -#### 4.1. Код: fact_flight_sales_dq.sql — ослабить student SK - -Спецификация: архивный план, п.2. - -- `passenger_sk IS NULL` → `RAISE NOTICE` (было `RAISE EXCEPTION`) -- Разделить route-related блок: airport_sk (порог 1%, EXCEPTION) vs - route_sk/airplane_sk (NOTICE only) -- Добавить учебный комментарий - -#### 4.2. Код: routes_dq.sql — закомментировать RI airplane - -Спецификация: архивный план, п.3. - -- **Закомментировать** (не удалять) блок RI `airplane_code → ods.airplanes` -- Добавить комментарий: - -```sql --- Проверка RI airplane_code → ods.airplanes закомментирована, --- т.к. таблица ods.airplanes реализуется студентом. --- После реализации — раскомментируйте этот блок. --- Полную версию см. в ветке solution. -``` - -#### 4.3. Код: ODS DAG — убрать зависимость airplanes → routes - -Спецификация: архивный план, п.4. - -- `[dq_ods_airports, dq_ods_airplanes] >> load_ods_routes` - → `dq_ods_airports >> load_ods_routes` - -#### 4.4. Заглушки: студенческие файлы - -Спецификация: архивный план, п.5. - -| Слой | Файлы (load + dq) | DDL | -|------|--------------------|-----| -| ODS | airplanes, seats | Оставить (таблица нужна) | -| DDS | dim_routes, dim_passengers, dim_airplanes | Оставить (нужен для LEFT JOIN) | -| DM | airport_traffic, route_performance, monthly_overview, passenger_loyalty | Оставить | - -Формат заглушки load: -```sql --- TODO: реализуйте загрузку (см. ТЗ в docs/assignment/analyst_spec.md) --- Эталонную реализацию можно найти в ветке solution. -SELECT 1; -``` - -Формат заглушки dq: -```sql --- TODO: реализуйте проверки качества данных --- Эталонную реализацию можно найти в ветке solution. -SELECT 1; -``` - -#### 4.5. Тесты - -Спецификация: архивный план, п.10. - -- `test_dags_smoke.py`: убрать assert барьера `dq_ods_airplanes → dq_ods_routes` -- `test_ods_sql_contract.py`: `SNAPSHOT_ENTITIES = ("airports", "routes")` - (убрать airplanes, seats — их load/dq теперь заглушки) - -#### 4.6. Документация (main-specific) - -- `docs/design/bookings_ods_design.md` — пометить airplanes/seats как студенческие, - убрать зависимость routes от airplanes, скорректировать DQ-контракт -- `docs/design/bookings_dds_design.md` — обновить DQ-описание: - student SK не блокируют (NOTICE), airport_sk — порог 1% -- `docs/bookings_to_gp_dds.md` — обновить: student SK будут NULL на main -- `docs/reference/qa-plan.md` — уточнить: ods.airplanes и ods.seats - пусты by design на main - -### Шаг 5. Очистка docs на main (после всех правок) - -> Этот шаг выполняется **после** шагов 2–4, потому что: -> - solution уже создан (шаг 3) и сохранил все файлы -> - план и архивный справочник были доступны во время работы (шаг 4) - -#### 5.1. Удалить с main (остаётся на solution) - -| Файл/каталог | Почему не нужен студенту | -|--------------|------------------------| -| `docs/plans/` (весь каталог) | Планы разработки — внутренняя кухня | -| `docs/archive/` (весь каталог) | Архив планов — внутренняя кухня | -| `docs/design/PRD.md` | Продуктовые требования — внутренний документ | -| `docs/design/assignment_design.md` | Мета-дизайн задания (для авторов курса, не для студентов) | -| `docs/reference/bookings_generation_benchmark.md` | Бенчмарки генерации — отладочная информация | -| `docs/reference/bookings_tz.md` | Заметки о таймзонах — отладочная информация | -| `docs/reference/pxf_bookings.md` | PXF-конфигурация — внутренняя отладка | -| `docs/agent-dag-testing.md` | Инструкция для AI-агентов по тестированию | -| `docs/e2e-etl-test-protocol.md` | E2E-протокол — внутреннее тестирование | -| `TODO.md` | Таск-лист мейнтейнера | - -#### 5.2. Что остаётся на main (полезно студенту) - -| Файл | Зачем студенту | -|------|---------------| -| `README.md` | Установка, запуск, структура проекта | -| `TESTING.md` | Как проверять свою работу | -| `AGENTS.md`, `CLAUDE.md`, `GEMINI.md` | Если студент использует AI-помощников | -| `docs/README.md` | Навигация по документации | -| `docs/stack.md` | Стек технологий — контекст | -| `docs/dag_execution_order.md` | Порядок запуска DAG'ов | -| `docs/bookings_to_gp_stage.md` | Описание STG — эталонный код для изучения | -| `docs/bookings_to_gp_ods.md` | Описание ODS | -| `docs/bookings_to_gp_dds.md` | Описание DDS | -| `docs/bookings_to_gp_dm.md` | Описание DM | -| `docs/design/naming_conventions.md` | Нейминг полей — нужен для DDL/SQL | -| `docs/design/db_schema.md` | Схема БД — справочник | -| `docs/design/bookings_stg_design.md` | Дизайн STG — эталон для изучения | -| `docs/design/bookings_ods_design.md` | Дизайн ODS — нужен для задания | -| `docs/design/bookings_dds_design.md` | Дизайн DDS — нужен для задания | -| `docs/design/bookings_dm_design.md` | Дизайн DM — нужен для задания | -| `docs/assignment/` | Задание (analyst_spec.md) | -| `docs/reference/qa-plan.md` | QA-чеклист — полезен для самопроверки | -| `docs/reference/bookings_db_issues.md` | Известные проблемы — чтобы студент не тратил время на отладку | - -#### 5.3. AGENTS.md — адаптировать для main - -`AGENTS.md` содержит карту проекта и ссылки, которые побьются после cleanup. -Нужно обновить, а не просто добавить одну строку: - -- **Карта проекта (секция 2):** убрать упоминания `docs/plans/`, `docs/archive/`. - Добавить: «Полные дизайн-документы (PRD, assignment_design, планы) — в ветке solution» -- **Тестирование:** убрать ссылку на `docs/agent-dag-testing.md` (файл удалён) -- **Прочие ссылки:** проверить, что все пути в AGENTS.md ведут на существующие файлы - -#### 5.4. Link audit — полная проверка ссылок - -Проверить **все** ссылки во **всех** оставшихся на main markdown-файлах, -а не только ссылки на удалённые файлы. В документации уже есть битые ссылки -(напр. `docs/README.md` → `design/architecture_review.md`, файл в архиве). - -**Метод:** - -```bash -# 1. Найти все markdown-ссылки в оставшихся файлах -rg -o '\[.*?\]\([^)]+\.md[^)]*\)' docs/ README.md TESTING.md AGENTS.md - -# 2. Проверить существование каждого целевого файла -# 3. Починить: удалить ссылку, обновить путь, или заменить на «см. ветку solution» -``` - -Известные проблемы (минимум): - -- `docs/README.md` — ссылки на plans/, archive/, PRD, assignment_design, architecture_review -- `docs/assignment/README.md` — возможные ссылки на assignment_design -- `docs/design/bookings_stg_design.md` — ссылки на внутренние reference -- `docs/bookings_to_gp_stage.md` — ссылки на reference -- `docs/stack.md` — ссылки на reference -- `docs/design/db_schema.md` — ссылки на PRD, assignment_design -- `README.md` — ссылки на TODO.md, PRD -- `AGENTS.md` — покрыто шагом 5.3 - -### Шаг 6. Верификация main - -```bash -make test # smoke-тесты и контракты -make lint # линтинг -``` - -При поднятом стенде: -1. `make up` → `make ddl-gp` -2. Запустить STG → ODS → DDS → DM DAG'и -3. Проверить: `sales_report` содержит данные с корректными аэропортами -4. Проверить: студенческие ODS/DDS-таблицы пусты -5. Fact DQ проходит (student SK = NULL, но DQ не блокирует) - -### Шаг 7. Верификация solution - -```bash -git checkout solution -make test -``` - -Убедиться, что полный пайплайн работает без изменений (это снимок -проверенного chore/bookings-etl + общие doc-правки). - ---- - -## 3. Открытый вопрос: протокол синхронизации веток - -> Не блокирует Этап 4. Доработать после раскладки, когда появится конкретный -> опыт (например, при первом PXF-задании). - -### Предварительное решение: solution как source of truth - -Все изменения (новые задания, баг-фиксы, доработки) начинаются на solution -или на feature-ветке от solution. Main — производная. - -``` -solution ← feat/new-task (разработка + тесты) - ↓ merge -solution (полный эталон, всегда рабочий) - ↓ порт -main (cherry-pick общего + заглушки) -``` - -**Поток по умолчанию:** solution → main (одно направление). - -**Исключение (редко):** main-only правка (опечатка в заглушке, битая ссылка -в main-only документе) — правим прямо на main, solution не трогаем. - -### Что нужно доработать - -- Как именно выглядит «порт в main»: cherry-pick, ручной перенос, чеклист? -- Что делать при конфликтах cherry-pick? -- Нужен ли реестр known-different файлов (заглушки vs реализации) для - автоматической проверки синхронизации? -- Нужен ли периодический `git diff main..solution -- ` для - обнаружения расхождений? - ---- - -## 4. Ключевые файлы - -Полная таблица с ролями и ветками: архивный план, секция «Ключевые файлы». - -Дополнительно (docs cleanup, шаг 5.1): - -| Действие | Файлы | -|----------|-------| -| Удалить с main | `docs/plans/*`, `docs/archive/*`, `docs/design/PRD.md`, `docs/design/assignment_design.md`, `docs/reference/bookings_generation_benchmark.md`, `docs/reference/bookings_tz.md`, `docs/reference/pxf_bookings.md`, `docs/agent-dag-testing.md`, `docs/e2e-etl-test-protocol.md`, `TODO.md` | -| Обновить на main | `AGENTS.md` (ссылка на solution), все .md с битыми ссылками (link audit) | diff --git a/docs/plans/README.md b/docs/plans/README.md deleted file mode 100644 index 4098055..0000000 --- a/docs/plans/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Планы работ - -Активные планы. После выполнения переносятся в [`archive/`](../archive/). diff --git a/docs/reference/bookings_generation_benchmark.md b/docs/reference/bookings_generation_benchmark.md deleted file mode 100644 index e5bbd3d..0000000 --- a/docs/reference/bookings_generation_benchmark.md +++ /dev/null @@ -1,75 +0,0 @@ -# Бенчмарк генерации bookings: параллельность и тюнинг PostgreSQL - -> Дата: 2026-03-09 -> Железо: Intel Core i7-11800H @ 2.30GHz (8 ядер / 16 потоков), игровой ноутбук -> Среда: WSL2, Docker Desktop, PostgreSQL 16 в контейнере -> Данные: seed-дамп 60 дней (~500k bookings, ~180k boarding_passes, ~1.2M tickets) - -## Контекст - -Генератор demodb (`postgrespro/demodb`) использует очередь событий `gen.events` с обработкой через `process_queue()`. При `jobs>1` воркеры запускаются через `dblink_send_query` и конкурируют за очередь через `SELECT ... FOR UPDATE SKIP LOCKED`. - -Задача: найти оптимальную конфигурацию для `make bookings-generate-day` (инкремент +1 день). - -## Методика - -- Между экспериментами: `make bookings-init BOOKINGS_JOBS=N` (восстановление из seed-дампа, ~18 сек). -- Замер: `time make bookings-generate-day BOOKINGS_JOBS=N`. -- Контроль: `max(book_date)` должен сдвинуться на +1 день. -- Тюнинг PG: `ALTER SYSTEM SET` + `docker compose restart bookings-db`. - -Параметры тюнинга PostgreSQL: -``` -shared_buffers = 512MB (дефолт: 128MB) -work_mem = 64MB (дефолт: 4MB) -maintenance_work_mem = 256MB (дефолт: 64MB) -effective_cache_size = 1GB (дефолт: 4GB) -wal_buffers = 16MB (дефолт: -1, авто) -checkpoint_completion_target = 0.9 (дефолт: 0.9) -random_page_cost = 1.1 (дефолт: 4.0) -``` - -## Результаты - -| # | Тюнинг PG | sync_commit | Jobs | Время | vs baseline | -|---|-----------|-------------|------|--------|--------------| -| 1 | нет | on | 1 | 2m 48s | **baseline** | -| 2 | нет | on | 2 | 8m 36s | 3× хуже | -| 3 | да | on | 1 | 3m 01s | шум | -| 4 | да | on | 2 | 8m 42s | 3× хуже | -| 5 | да | off | 1 | 2m 50s | шум | -| 6 | да | off | 2 | 8m 39s | 3× хуже | - -Повторный инкремент (прогретый кэш): 5m 03s (jobs=2), не замерялся (jobs=1). - -## Выводы - -### 1. jobs=1 оптимален для инкрементов - -Для генерации +1 дня параллельность через dblink **контрпродуктивна**: -- Два воркера конкурируют за одну очередь `gen.events` через `FOR UPDATE SKIP LOCKED` — это row-level lock contention. -- Overhead: dblink-соединения, синхронизация через `gen.stat_jobs`, polling через `dblink_is_busy()`. -- На WSL2 дополнительно: виртуализированный I/O не масштабируется при параллельных записях. - -При генерации с нуля (`make bookings-generate`, десятки тысяч событий) jobs>1 **может** давать прирост, но не тестировалось в этом бенчмарке. - -### 2. Тюнинг PostgreSQL не влияет - -Увеличение shared_buffers в 4 раза, work_mem в 16 раз и т.д. не дало измеримого эффекта. Узкое место — не буферы и не I/O, а сама логика генератора: последовательная обработка событий с COMMIT после каждого. - -### 3. synchronous_commit=off не влияет - -Генератор делает COMMIT после каждого события (~сотни раз за день). Ожидалось, что `synchronous_commit=off` ускорит запись WAL. Эффект не обнаружен — вероятно, WAL-буферы и так справляются при одном потоке. - -### 4. VACUUM-ивенты — отдельная проблема - -Генератор вставляет в очередь событие `VACUUM` каждую неделю модельного времени. Обработчик запускает `VACUUM ANALYZE` всей базы через `dblink_exec`. При 500k+ строках это занимает минуты. Решение: `DELETE FROM gen.events WHERE type = 'VACUUM'` перед `continue()` в инкрементальных скриптах. - -## Итоговая конфигурация - -``` -BOOKINGS_JOBS=1 # синхронно, без dblink — ~3 мин на +1 день -BOOKINGS_INIT_DAYS=60 # seed-дамп покрывает ~34 дня бронирований + boarding_passes -``` - -Тюнинг PostgreSQL не требуется для текущих объёмов данных. diff --git a/docs/reference/bookings_tz.md b/docs/reference/bookings_tz.md deleted file mode 100644 index fe0c41a..0000000 --- a/docs/reference/bookings_tz.md +++ /dev/null @@ -1,29 +0,0 @@ -# Временное ТЗ по блоку bookings (для текущей разработки) - -_Внутренний файл для наставника: поясняет, как устроен источник `bookings-db` и генерация данных. Студентам обычно не нужен._ - -- Контейнер `bookings-db` — отдельный сервис Postgres из `docker-compose.yml`, база по умолчанию `demo` (из upstream demodb), без переименований. -- Доступ снаружи не блокируем (порт `5434` по умолчанию), чтобы позже читать через PXF и подключаться из Greenplum. -- Инициализация: два способа: - - `make bookings-init` (рекомендуется): быстрое восстановление из seed-дампа (~18 сек). - - `make bookings-generate` (для разработчиков): полная генерация с нуля — клонирует demodb с закреплённым коммитом, накладывает патчи (`engine`: `jobs=1` синхронно + `busy()` игнорирует свой pid; `install.sql`: `DROP DATABASE IF EXISTS`, `connstr` без хардкода), ждёт `pg_isready`, ставит `gen.connstr` и GUC `bookings.start_date/init_days/jobs`, затем запускает `/bookings/generate_next_day.sql` через `psql -f`. Значения по умолчанию: стартовая дата 2017-01-01, `init_days=60`, `jobs=2`. -- Генерация следующего дня: `make bookings-generate-day` прогоняет тот же SQL (читает GUC, вызывает `generate/continue`, ждёт `busy()`, закрывает dblink). При `jobs=1` всё синхронно, без dblink. -- Исходники demodb: клонируем по требованию с фиксированным хешем, кладём в `bookings/demodb/` (в `.gitignore`), патчи лежат в `bookings/patches/` и применяются автоматически при `make bookings-generate`. -- Документация: в README описаны команды (`bookings-init`, `bookings-generate`, проверка данных, генерация дня), параметры `.env`; настройка PXF/ETL — следующий этап. - -## Текущее состояние -- `make bookings-init` — быстрое восстановление из seed-дампа (~18 сек), рекомендуется для студентов. -- `make bookings-generate` — полная генерация с нуля: автоматически применяет патчи (`engine_jobs1_sync.patch`, `install_drop_if_exists.patch`), ждёт готовности Postgres через `pg_isready`, запускает `install.sql`, выставляет `gen.connstr`/GUC и вызывает `generate_next_day.sql` через `psql -f`. -- Дефолты: `BOOKINGS_START_DATE=2017-01-01`, `BOOKINGS_INIT_DAYS=60`, `BOOKINGS_JOBS=2`. При `jobs=1` генерация идёт синхронно без dblink, `busy()` не учитывает текущую сессию. -- `.env.example`/README обновлены под новые дефолты; каталог `bookings/demodb/` в `.gitignore`. -- Патчи лежат в `bookings/patches/` и накладываются при `bookings-clone-demodb`. - -## Текущее состояние тестов/проблем -- Чистый прогон `make bookings-init` (восстановление из seed-дампа) проходит за ~18 секунд. -- Чистый прогон `make bookings-generate` (после `docker compose down -v` и удаления `bookings/demodb`) проходит за ~1,5 минуты: база ставится, `busy()` → `f`, `bookings.bookings` от `2017-01-01 00:00:18` до `2017-01-01 23:59:59`. -- Ранее зависание на `busy()` при `jobs=1` лечится патчем: `process_queue` теперь синхронный, а `busy()` игнорирует текущий backend. -- Данных пока только на 1 день по умолчанию, чтобы генерация не занимала много времени. - -## Идеи/следующие шаги -- Если понадобится больше дней — увеличивать `BOOKINGS_INIT_DAYS`, но помнить, что генерация может идти долго; контролировать через `SELECT busy();`. -- Следующий этап — PXF/ETL в Greenplum; текущая задача — лишь подготовить источник bookings. diff --git a/docs/reference/pxf_bookings.md b/docs/reference/pxf_bookings.md deleted file mode 100644 index 702b637..0000000 --- a/docs/reference/pxf_bookings.md +++ /dev/null @@ -1,201 +0,0 @@ -# PXF для bookings в учебном стенде (актуально) - -Этот документ описывает **текущую реализацию** PXF в проекте: чтение данных из демо‑БД -`bookings` (Postgres, сервис `bookings-db`) в Greenplum через JDBC. - -## 1. Что должно работать - -- В Greenplum доступны внешние таблицы: - - `public.ext_bookings_bookings` (создаётся `make ddl-gp`); - - `stg.bookings_ext` (создаётся DAG `bookings_stg_ddl`). -- PXF должен быть готов **после каждого старта** контейнера `greenplum`. - -## 2. Почему мы делаем свой образ Greenplum - -Изначально PXF‑скрипты/конфиги монтировались в контейнер как bind‑mount `:ro`. -Базовый entrypoint образа Greenplum пытается делать `chown` файлов в -`/docker-entrypoint-initdb.d/`, из‑за чего контейнер иногда падал с ошибкой: - -`chown: changing ownership ... Read-only file system` - -Снять `:ro` тоже нежелательно — можно получить проблемы с правами на файлах хоста -(файл становится `root`, IDE перестаёт сохранять, появляются лишние изменения в git). - -Решение для учебного стенда: - -- собрать **свой образ** Greenplum (`Dockerfile.greenplum`); -- «вшить» в образ seed‑файлы и скрипты PXF; -- на каждом старте контейнера идемпотентно докладывать файлы в `PXF_BASE`, - который живёт на persistent volume. - -## 3. Где что хранится - -**Внутри образа (immutable):** - -- seed для PXF: `/opt/pxf-seed/` (JDBC‑JAR и `servers/bookings-db/jdbc-site.xml`); -- скрипты: - - `/opt/pxf-scripts/ensure_pxf_bookings.sh` (подготовка `PXF_BASE`); - - `/start_greenplum_with_pxf.sh` (startup wrapper). - -**На persistent volume (переживает рестарты):** - -- `PXF_BASE` по умолчанию: `${GREENPLUM_DATA_DIRECTORY}/pxf` → в нашем compose это - `/data/pxf` на томе `greenplum_data`. - -Важно: так как `PXF_BASE` лежит на томе, обновления seed‑файлов из нового образа -**не перезатирают** файлы в `PXF_BASE` автоматически (это сделано намеренно, чтобы -не ломать ручные правки студентов). - -## 4. Что происходит при старте контейнера `greenplum` - -1) Docker запускает контейнер с базовым entrypoint образа и командой -`/start_greenplum_with_pxf.sh` (она задана в `Dockerfile.greenplum` как `CMD`). - -2) `/start_greenplum_with_pxf.sh` выполняет подготовку PXF: - -- запускает ensure‑скрипт `/opt/pxf-scripts/ensure_pxf_bookings.sh`; -- параллельно пытается выполнить `CREATE EXTENSION IF NOT EXISTS pxf` - в базе `${GP_DB}` (по умолчанию `gp_dwh`), когда Greenplum начинает принимать - подключения. - -3) Затем управление передаётся оригинальному старту Greenplum: `exec /start_gpdb.sh`. - -4) Healthcheck сервиса `greenplum` ждёт и готовность Greenplum, и то, что PXF уже -запущен (`pxf cluster status`). Это нужно, чтобы Airflow не стартовал раньше PXF. - -## 5. Управляющие переменные окружения - -Все переменные можно задать в `.env` (см. `.env.example`): - -- `PXF_SEED_OVERWRITE=1` — принудительно перезаписать seed‑файлы из образа в `PXF_BASE` - (обычно нужно после правок в каталоге `pxf/`). -- `PXF_SYNC_ON_START=1` — выполнять `pxf cluster sync` при старте контейнера - (делает старт чуть дольше, но гарантирует актуальные конфиги на хостах кластера). - -## 6. Быстрая ручная проверка - -1) Дождаться `healthy` у `greenplum`: - -`docker compose ps` - -2) Проверить статус PXF (PXF CLI запускается только под пользователем `gpadmin`): - -`docker compose exec greenplum bash -lc "su - gpadmin -c '/usr/local/pxf/bin/pxf cluster status'"` - -3) После применения DDL (`make ddl-gp`) проверить чтение через PXF: - -- `make gp-psql` -- `SELECT COUNT(*) FROM public.ext_bookings_bookings;` - -## 7. Типовые ошибки - -- `protocol "pxf" does not exist` - - причина: не создано расширение `pxf` в базе Greenplum; - - решение: перезапустить `greenplum` (скрипт сделает `CREATE EXTENSION IF NOT EXISTS pxf`) - или выполнить вручную `CREATE EXTENSION pxf;`. -- `Connection refused` к порту `5888` - - причина: PXF не поднялся/не успел подняться; - - решение: проверить `pxf cluster status`, посмотреть логи PXF в `/data/pxf/logs`, - перезапустить сервис `greenplum`. -- PXF «не подхватывает» изменения конфигов - - причина: файлы уже лежат в `PXF_BASE` на томе, а seed из образа по умолчанию не перетирает их; - - решение: `make build` + restart `greenplum` + (при необходимости) `PXF_SEED_OVERWRITE=1`. - -## 8. Известная проблема: `protocol "pxf" does not exist` на «холодном старте» (исправлено) - -Раньше (воспроизводилось в `./scripts/e2e_smoke.sh`) при первом `make ddl-gp` можно было получить: - -`ERROR: protocol "pxf" does not exist` - -### Почему так происходило - -В базовом `/start_gpdb.sh` из образа Greenplum создание расширения `pxf` связано с проверкой -файла `${PXF_BASE}/conf/pxf-env.sh`: - -- если `pxf-env.sh` **отсутствует**, скрипт выполняет `pxf cluster prepare/register` и затем - `CREATE EXTENSION IF NOT EXISTS pxf`; -- если `pxf-env.sh` **уже существует**, этот блок **пропускается**, и расширение может не появиться. - -При этом наш ensure‑скрипт `pxf/init/10_pxf_bookings.sh` копировал `pxf-env.sh` в `${PXF_BASE}` -ещё до запуска Greenplum, из‑за чего базовый скрипт считал PXF “уже настроенным” и -пропускал создание расширения. - -### Что изменили - -- создание `extension pxf` вынесено в `start_greenplum_with_pxf.sh` и обёрнуто ретраями; -- `pxf-env.sh` по‑прежнему копируется в `PXF_BASE`, чтобы `/start_gpdb.sh` не пытался выполнять - `pxf cluster prepare` на непустом `PXF_BASE`; -- healthcheck `greenplum` ждёт не только PXF, но и наличие `extension pxf`. -- добавлен экспорт `PGPASSWORD` для `pxf cluster start`, чтобы `docker compose stop/start` - не ломал запуск из‑за `password authentication failed` для `gpadmin`. - -### Если ошибка всё ещё возникает - -1) Пересоберите образ и перезапустите контейнер `greenplum`: -`make build && make down && make up` - -2) Проверьте наличие extension: -`docker compose exec greenplum bash -lc "su - gpadmin -c '/usr/local/greenplum-db/bin/psql -d gp_dwh -t -A -c \"SELECT extname FROM pg_extension WHERE extname = ''pxf'';\"'"` - -## 9. Связанные файлы - -- `Dockerfile.greenplum` -- `docker-compose.yml` (сервис `greenplum`: `build`, `hostname`, env, healthcheck) -- `pxf/init/10_pxf_bookings.sh` (ensure‑логика) -- `pxf/init/start_greenplum_with_pxf.sh` (старт контейнера) -- `docs/stack.md` (раздел «Greenplum + PXF: свой образ») - -## 10. Известная проблема: после `docker compose stop/start` Greenplum может упасть (auth для PXF) - -### Симптом - -После `docker compose stop`, затем `docker compose start` контейнер `greenplum` иногда уходит в `Exited (1)`. -В логах видно, что GPDB поднялся, но упал на старте PXF: - -- `INFO - pxf cluster start` -- `ERROR: Could not connect to GPDB` -- `FATAL: password authentication failed for user "gpadmin"` - -### Текущее понимание причины (почему это “иногда”) - -1) При старте GPDB образ `woblerr/greenplum` генерирует/дописывает `pg_hba.conf` на persistent volume. -2) В `pg_hba.conf` присутствует trust‑правило для **конкретного IP** контейнера в docker‑сети - (пример из диагностики: `host all gpadmin 172.21.0.2/32 trust`). -3) После `docker compose stop/start` Docker может выдать контейнеру **другой IP** (например, `172.21.0.3`). - Тогда trust‑правило больше не подходит, и подключение начинает идти по `md5`. -4) `pxf cluster start` подключается к GPDB по TCP на `host=gpdbsne` (hostname контейнера), - то есть попадает именно в `pg_hba.conf` (а не в local‑auth). -5) В результате при “не совпавшем IP” получаем `md5` + пароль (возможно пустой/не тот) → падение на `28P01`. - -Эта проблема выглядит флапающей, потому что IP после `stop/start` иногда совпадает с захардкоженным trust‑/32, -а иногда нет. - -### Как подтвердить при следующем воспроизведении - -1) Посмотреть логи `greenplum`: -`docker compose logs --tail=200 greenplum` - -2) Найти реальный IP клиента в master‑логах GPDB (на томе): -`Password does not match ...` обычно содержит адрес вида `172.21.0.X`. - -3) Сравнить его с trust‑строкой в `pg_hba.conf` на томе: -`/data/master/gpseg-1/pg_hba.conf` - -Если IP в ошибке (например, `172.21.0.3`) **не** совпадает с trust‑/32 (например, `172.21.0.2/32`) — -это почти наверняка корень падения. - -### Что с этим делать дальше (варианты решения, без реализации здесь) - -Основная цель — убрать зависимость от “случайного IP после stop/start”: - -- заставить `pxf cluster start` подключаться к GPDB через `127.0.0.1` (тогда работает существующий trust на localhost); -- или перестать добавлять в `pg_hba.conf` trust на конкретный `172.21.0.2/32` и заменить на более стабильное правило - (например, на подсеть docker‑сети или на `samehost`); -- или закрепить IP контейнера в compose (static IP), чтобы он не “плавал”; -- или отказаться от `stop/start` в пользу сценария, который не меняет сетевое окружение (но это хуже для UX студентов). - -### Что реализовано - -- В `pxf/init/start_greenplum_with_pxf.sh` добавлен шаг, который на каждом старте - обеспечивает в `pg_hba.conf` trust‑правило `host all gpadmin samehost trust` - (вставка перед `host all all 0.0.0.0/0 md5`), и делает `pg_ctl reload`, если GPDB уже запущен. diff --git a/docs/reference/qa-plan.md b/docs/reference/qa-plan.md index d20678d..1b9d5ad 100644 --- a/docs/reference/qa-plan.md +++ b/docs/reference/qa-plan.md @@ -77,7 +77,7 @@ UNION ALL SELECT 'stg.seats', COUNT(*) FROM stg.seats UNION ALL SELECT 'stg.boarding_passes', COUNT(*) FROM stg.boarding_passes ORDER BY 1; --- ODS: все 9 таблиц не пустые +-- ODS: эталонные таблицы не пустые (на main ods.airplanes и ods.seats пусты by design) SELECT 'ods.bookings' AS tbl, COUNT(*) FROM ods.bookings UNION ALL SELECT 'ods.tickets', COUNT(*) FROM ods.tickets UNION ALL SELECT 'ods.segments', COUNT(*) FROM ods.segments diff --git a/docs/stack.md b/docs/stack.md index 0f5a51b..2ccdfb4 100644 --- a/docs/stack.md +++ b/docs/stack.md @@ -82,7 +82,7 @@ docker compose ps # проверить health - `PXF_SEED_OVERWRITE=1` — перезаписать конфиги при старте; - `PXF_SYNC_ON_START=1` — выполнить `pxf cluster sync` при старте. -Подробнее: `docs/reference/pxf_bookings.md`. +Подробнее: см. ветку `solution` (`docs/reference/pxf_bookings.md`). ## Airflow: свой образ