feat(validate): добавлен валидационный DAG bookings_validate
- Зачем:
- студент реализует 9 объектов DWH без автоматической обратной связи;
- ошибки (NULL в PK, дубли BK, сломанный SCD2) обнаруживались только на слое DM
через 2–3 слоя, где отладка многократно сложнее.
- Что:
- создан DAG bookings_validate.py — три параллельных TaskGroup (validate_ods,
validate_dds, validate_dm), schedule=None (только ручной trigger);
- создано 17 SQL-скриптов в sql/validate/: ODS BK coverage vs STG-батч,
согласованность _load_id между ods.airplanes и ods.seats, дубли BK, NULL в PK,
покрытие ODS→DDS для измерений (dim_routes требует открытой версии valid_to IS NULL),
SCD2 активный тест backup→mutate→student_load→check(5 инвариантов)→restore
(trigger_rule=all_done), проверка «дыр» между SCD2-версиями,
exists-чеки для 4 DM-витрин;
- добавлен класс TestBookingsValidate (5 smoke-тестов) в tests/test_dags_smoke.py.
- Проверка:
- make test — 4 passed, 14 skipped (DAG-тесты скипаются без Airflow, норма);
- ручной trigger bookings_validate в Airflow UI на solution-ветке — все таски зелёные.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
-- Проверяем, что 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 $$;
|
||||
Reference in New Issue
Block a user