test(e2e): протокол сквозного тестирования и инструменты очистки DWH
- Зачем: - формализация проверки всей цепочки ETL (STG -> ODS -> DDS -> DM). - Что: - создан docs/e2e-etl-test-protocol.md и sql/truncate_gp.sql. - в Makefile добавлена команда dwh-truncate. - start_date во всех DAG изменен на 2017-01-01. - Проверка: - выполнение make dwh-truncate и прогон DAG.
This commit is contained in:
@@ -36,6 +36,9 @@ logs:
|
|||||||
gp-psql:
|
gp-psql:
|
||||||
docker compose -f docker-compose.yml exec greenplum bash -c "su - gpadmin -c '/usr/local/greenplum-db/bin/psql -p 5432 -d gp_dwh'"
|
docker compose -f docker-compose.yml exec greenplum bash -c "su - gpadmin -c '/usr/local/greenplum-db/bin/psql -p 5432 -d gp_dwh'"
|
||||||
|
|
||||||
|
dwh-truncate:
|
||||||
|
docker compose -f docker-compose.yml exec greenplum bash -c "su - gpadmin -c 'cd /sql && /usr/local/greenplum-db/bin/psql -d gp_dwh -f truncate_gp.sql'"
|
||||||
|
|
||||||
ddl-gp:
|
ddl-gp:
|
||||||
docker compose -f docker-compose.yml exec greenplum bash -c "su - gpadmin -c 'cd /sql && /usr/local/greenplum-db/bin/psql -d gp_dwh -f ddl_gp.sql'"
|
docker compose -f docker-compose.yml exec greenplum bash -c "su - gpadmin -c 'cd /sql && /usr/local/greenplum-db/bin/psql -d gp_dwh -f ddl_gp.sql'"
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(secon
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_dds_ddl",
|
dag_id="bookings_dds_ddl",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
template_searchpath="/sql",
|
template_searchpath="/sql",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(secon
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_dm_ddl",
|
dag_id="bookings_dm_ddl",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
template_searchpath="/sql",
|
template_searchpath="/sql",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(secon
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_ods_ddl",
|
dag_id="bookings_ods_ddl",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
template_searchpath="/sql",
|
template_searchpath="/sql",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(secon
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_stg_ddl",
|
dag_id="bookings_stg_ddl",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
template_searchpath="/sql",
|
template_searchpath="/sql",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def _finish_summary() -> None:
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_to_gp_dds",
|
dag_id="bookings_to_gp_dds",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def _finish_summary() -> None:
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_to_gp_dm",
|
dag_id="bookings_to_gp_dm",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ def _finish_summary() -> None:
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_to_gp_ods",
|
dag_id="bookings_to_gp_ods",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def _finish_summary() -> None:
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="bookings_to_gp_stage",
|
dag_id="bookings_to_gp_stage",
|
||||||
start_date=pendulum.datetime(2024, 1, 1, tz="UTC"),
|
start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(secon
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="csv_to_greenplum",
|
dag_id="csv_to_greenplum",
|
||||||
start_date=datetime(2024, 1, 1),
|
start_date=datetime(2017, 1, 1),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
default_args=default_args,
|
default_args=default_args,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(secon
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="csv_to_greenplum_dq",
|
dag_id="csv_to_greenplum_dq",
|
||||||
start_date=datetime(2024, 1, 1),
|
start_date=datetime(2017, 1, 1),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
default_args=default_args,
|
default_args=default_args,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(secon
|
|||||||
|
|
||||||
with DAG(
|
with DAG(
|
||||||
dag_id="orders_base_ddl",
|
dag_id="orders_base_ddl",
|
||||||
start_date=datetime(2024, 1, 1),
|
start_date=datetime(2017, 1, 1),
|
||||||
schedule=None,
|
schedule=None,
|
||||||
catchup=False,
|
catchup=False,
|
||||||
template_searchpath="/sql",
|
template_searchpath="/sql",
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Протокол сквозного (E2E) тестирования ETL
|
||||||
|
|
||||||
|
Этот документ описывает процедуру полной проверки цепочки ETL: `STG -> ODS -> DDS -> DM`.
|
||||||
|
Цель теста — убедиться в корректности инкрементальной загрузки, работы паттерна `Temporary Table` и механизмов `HWM`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Подготовка окружения
|
||||||
|
|
||||||
|
Убедитесь, что все сервисы запущены и DDL применен.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make up
|
||||||
|
make bookings-init
|
||||||
|
make ddl-gp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Важно: Настройка дат
|
||||||
|
Для корректного тестирования исторических данных из `demodb` (начинаются с 2017 года), убедитесь, что в DAG-файлах `start_date` установлен в `2017-01-01`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Очистка данных (Reset)
|
||||||
|
|
||||||
|
Перед началом теста необходимо полностью очистить все слои DWH.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dwh-truncate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Этап 1: Загрузка за первый день (2017-01-01)
|
||||||
|
|
||||||
|
Выполните последовательный запуск всех DAG для первой порции данных.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Загрузка в STG (создает первый батч в источнике)
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_stage 2017-01-01
|
||||||
|
|
||||||
|
# Загрузка в ODS (Initial Load)
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_ods 2017-01-01
|
||||||
|
|
||||||
|
# Загрузка в DDS (Initial Load)
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_dds 2017-01-01
|
||||||
|
|
||||||
|
# Загрузка в DM (Initial Load витрины)
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_dm 2017-01-01
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ожидаемые результаты (Day 1)
|
||||||
|
Проверьте наполнение таблиц:
|
||||||
|
- `stg.bookings` и `ods.bookings` должны иметь одинаковое количество строк (>0).
|
||||||
|
- `dm.sales_report` должна содержать агрегированные данные за первый день.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Этап 2: Проверка инкремента (2017-01-02)
|
||||||
|
|
||||||
|
Эмулируйте появление данных за второй день и проверьте дозагрузку.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Генерация данных за 2-й день в базе-источнике
|
||||||
|
make bookings-generate-day
|
||||||
|
|
||||||
|
# Повторный запуск цепочки ETL
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_stage 2017-01-02
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_ods 2017-01-02
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_dds 2017-01-02
|
||||||
|
docker compose exec airflow-scheduler airflow dags test bookings_to_gp_dm 2017-01-02
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Финальная верификация (Критерии успеха)
|
||||||
|
|
||||||
|
Выполните 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 == ODS**: Количество строк в `stg.bookings` и `ods.bookings` совпадает (т.к. это SCD1 UPSERT).
|
||||||
|
2. **Инкремент STG**: Количество строк в `stg.bookings` после Day 2 больше, чем после Day 1.
|
||||||
|
3. **Инкремент ODS (Temporary Table)**: В ODS нет дублей. `SELECT book_ref FROM ods.bookings GROUP BY book_ref HAVING COUNT(*) > 1` должен вернуть 0 строк.
|
||||||
|
4. **HWM в DM**: Витрина `sales_report` содержит данные за оба дня. Значение `COUNT(*)` после Day 2 должно вырасти по сравнению с Day 1.
|
||||||
|
5. **Lineage**: Поля `_load_id` и `_load_ts` во всех слоях содержат метки соответствующих запусков.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
- **Пустые таблицы**: Проверьте, что в `bookings-db` есть данные (`SELECT COUNT(*) FROM bookings.bookings`). Если 0 — сделайте `make bookings-init`.
|
||||||
|
- **Пропуски в ODS**: Убедитесь, что `stg_batch_id` в ODS корректно вычисляется (задача `resolve_stg_batch_id`).
|
||||||
|
- **Дубли в DDS**: Проверьте логику генерации SK в `dds/*_load.sql`.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- Скрипт для полной очистки всех слоев DWH (STG, ODS, DDS, DM).
|
||||||
|
-- Используется для сброса состояния перед проведением E2E-тестов.
|
||||||
|
|
||||||
|
-- 1. Слой STG (Сырые данные)
|
||||||
|
TRUNCATE stg.bookings,
|
||||||
|
stg.tickets,
|
||||||
|
stg.airports,
|
||||||
|
stg.airplanes,
|
||||||
|
stg.routes,
|
||||||
|
stg.seats,
|
||||||
|
stg.flights,
|
||||||
|
stg.segments,
|
||||||
|
stg.boarding_passes;
|
||||||
|
|
||||||
|
-- 2. Слой ODS (Текущее состояние, SCD1)
|
||||||
|
TRUNCATE ods.bookings,
|
||||||
|
ods.tickets,
|
||||||
|
ods.airports,
|
||||||
|
ods.airplanes,
|
||||||
|
ods.routes,
|
||||||
|
ods.seats,
|
||||||
|
ods.flights,
|
||||||
|
ods.segments,
|
||||||
|
ods.boarding_passes;
|
||||||
|
|
||||||
|
-- 3. Слой DDS (Схема "Звезда", SCD1/SCD2)
|
||||||
|
TRUNCATE dds.dim_calendar,
|
||||||
|
dds.dim_airports,
|
||||||
|
dds.dim_airplanes,
|
||||||
|
dds.dim_tariffs,
|
||||||
|
dds.dim_passengers,
|
||||||
|
dds.dim_routes,
|
||||||
|
dds.fact_flight_sales;
|
||||||
|
|
||||||
|
-- 4. Слой DM (Витрины данных)
|
||||||
|
TRUNCATE dm.sales_report;
|
||||||
Reference in New Issue
Block a user