Files
airflow-greenplum/tests/test_ods_snapshot_integration.py
T
ddadmin 5972bcc2d8 refactor(all): унифицированы служебные поля STG — переход на канонический нейминг
- Зачем:
  - STG-слой использовал legacy-имена (batch_id, load_dttm, src_created_at_ts),
    тогда как ODS/DDS/DM уже работали с каноном (_load_id, _load_ts, event_ts).
    Студент видел разные имена для одного понятия — это убрано.
- Что:
  - переименованы колонки в 9 STG DDL: batch_id→_load_id, load_dttm→_load_ts,
    src_created_at_ts→event_ts; добавлен NOT NULL для _load_id во всех таблицах.
  - обновлены 9 STG Load, 9 STG DQ, 9 ODS Load, 9 ODS DQ (INSERT/SELECT/WHERE).
  - обновлены DAG-файлы bookings_to_gp_stage.py и bookings_to_gp_ods.py
    (встроенный SQL резолвера, комментарии; Python-идентификаторы не тронуты).
  - обновлены тесты и ~15 документов (naming_conventions, PRD, db_schema,
    design-docs, qa-plan, README, TESTING и др.).
- Проверка:
  - grep -rn 'load_dttm\|src_created_at_ts' sql/ airflow/ tests/ — 0 совпадений.
  - make test — 4 passed.
  - e2e-etl: day1 прошёл полностью, day2 стартовал без ошибок.
2026-03-10 00:07:08 +03:00

152 lines
5.1 KiB
Python

from __future__ import annotations
import os
import subprocess
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
RUN_ODS_INTEGRATION = os.getenv("RUN_ODS_INTEGRATION") == "1"
BATCH_TOKEN = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'
pytestmark = pytest.mark.skipif(
not RUN_ODS_INTEGRATION,
reason="Set RUN_ODS_INTEGRATION=1 to run ODS integration tests",
)
def _psql(sql: str) -> str:
"""Выполняет SQL в Greenplum-контейнере и возвращает stdout psql."""
docker_bin = os.getenv("DOCKER_BIN", "docker")
cmd = [
docker_bin,
"compose",
"-f",
"docker-compose.yml",
"exec",
"-T",
"greenplum",
"bash",
"-lc",
"su - gpadmin -c '/usr/local/greenplum-db/bin/psql -v ON_ERROR_STOP=1 -d gp_dwh -At -f -'",
]
result = subprocess.run(
cmd,
cwd=PROJECT_ROOT,
input=sql,
text=True,
capture_output=True,
check=True,
)
return result.stdout
def _render_airports_sql(
path: str, batch_id: str, stg_table: str, ods_table: str
) -> str:
sql = (PROJECT_ROOT / path).read_text(encoding="utf-8")
sql = sql.replace(BATCH_TOKEN, batch_id)
sql = sql.replace("stg.airports", stg_table)
sql = sql.replace("ods.airports", ods_table)
return sql
def test_snapshot_airports_contract_upsert_delete_and_dq() -> None:
"""
Интеграционный тест контракта snapshot-таблицы:
- UPSERT обновляет и вставляет;
- DELETE синхронизирует current state по выбранному батчу;
- DQ проходит на корректном состоянии.
"""
stg_table = "public.it_stg_airports_ods"
ods_table = "public.it_ods_airports_ods"
setup_sql = f"""
DROP TABLE IF EXISTS {stg_table};
DROP TABLE IF EXISTS {ods_table};
CREATE TABLE {stg_table} (
airport_code TEXT,
airport_name TEXT,
city TEXT,
country TEXT,
coordinates TEXT,
timezone TEXT,
event_ts TIMESTAMP,
_load_ts TIMESTAMP,
_load_id TEXT
);
CREATE TABLE {ods_table} (
airport_code TEXT NOT NULL,
airport_name TEXT NOT NULL,
city TEXT NOT NULL,
country TEXT NOT NULL,
coordinates TEXT,
timezone TEXT NOT NULL,
_load_id TEXT NOT NULL,
_load_ts TIMESTAMP NOT NULL DEFAULT now()
);
"""
_psql(setup_sql)
try:
_psql(
f"""
INSERT INTO {stg_table} (
airport_code, airport_name, city, country, coordinates, timezone,
event_ts, _load_ts, _load_id
) VALUES
('AAA', '{{"en": "Airport A", "ru": "Аэропорт A"}}', '{{"en": "City A", "ru": "Город A"}}', '{{"en": "Country A", "ru": "Страна A"}}', '(0,0)', 'UTC', now(), now(), 'batch_1'),
('BBB', '{{"en": "Airport B", "ru": "Аэропорт B"}}', '{{"en": "City B", "ru": "Город B"}}', '{{"en": "Country B", "ru": "Страна B"}}', '(1,1)', 'UTC', now(), now(), 'batch_1');
"""
)
_psql(
_render_airports_sql(
"sql/ods/airports_load.sql", "batch_1", stg_table, ods_table
)
)
codes_batch_1 = _psql(
f"SELECT COALESCE(string_agg(airport_code, ',' ORDER BY airport_code), '') FROM {ods_table};"
).strip()
assert codes_batch_1 == "AAA,BBB"
_psql(
f"""
INSERT INTO {stg_table} (
airport_code, airport_name, city, country, coordinates, timezone,
event_ts, _load_ts, _load_id
) VALUES
('AAA', '{{"en": "Airport A v2", "ru": "Аэропорт A v2"}}', '{{"en": "City A", "ru": "Город A"}}', '{{"en": "Country A", "ru": "Страна A"}}', '(0,0)', 'UTC', now(), now(), 'batch_2'),
('CCC', '{{"en": "Airport C", "ru": "Аэропорт C"}}', '{{"en": "City C", "ru": "Город C"}}', '{{"en": "Country C", "ru": "Страна C"}}', '(2,2)', 'UTC', now(), now(), 'batch_2');
"""
)
_psql(
_render_airports_sql(
"sql/ods/airports_load.sql", "batch_2", stg_table, ods_table
)
)
codes_batch_2 = _psql(
f"SELECT COALESCE(string_agg(airport_code, ',' ORDER BY airport_code), '') FROM {ods_table};"
).strip()
assert codes_batch_2 == "AAA,CCC"
# ODS теперь содержит русские названия (извлечены из JSON)
airport_a_name = _psql(
f"SELECT airport_name FROM {ods_table} WHERE airport_code = 'AAA';"
).strip()
assert airport_a_name == "Аэропорт A v2"
_psql(
_render_airports_sql(
"sql/ods/airports_dq.sql", "batch_2", stg_table, ods_table
)
)
finally:
_psql(f"DROP TABLE IF EXISTS {stg_table}; DROP TABLE IF EXISTS {ods_table};")