From 5efb8338372cb99f96f269e13f2d6b2ec1b4702e Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 28 Feb 2026 17:35:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(sql):=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20JSON-=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=20=D0=B2=20ODS=20=D0=B4=D0=BB=D1=8F=20airpor?= =?UTF-8?q?ts=20=D0=B8=20airplanes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - источник хранит мультиязычные названия как JSON ({"en": "...", "ru": "..."}), - для упрощения downstream-логики (DDS/DM) нужны чистые строки на одном языке. - Что: - добавлен парсинг JSON с извлечением поля 'ru' в sql/ods/airports_load.sql (airport_name, city, country). - добавлен парсинг JSON с извлечением поля 'ru' в sql/ods/airplanes_load.sql (model). - обновлена документация docs/internal/bookings_ods_design.md с примечаниями о нормализации. - обновлены тестовые данные в tests/test_ods_snapshot_integration.py для соответствия JSON-формату STG. - Проверка: - uv run make test (15 passed). - SELECT airport_name FROM ods.airports → "Аль-Баха" (вместо JSON). --- docs/internal/bookings_ods_design.md | 19 +++++++++++++++---- sql/ods/airplanes_load.sql | 8 ++++++-- sql/ods/airports_load.sql | 16 ++++++++++------ tests/test_ods_snapshot_integration.py | 11 ++++++----- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/docs/internal/bookings_ods_design.md b/docs/internal/bookings_ods_design.md index b3c77c6..d6960c5 100644 --- a/docs/internal/bookings_ods_design.md +++ b/docs/internal/bookings_ods_design.md @@ -96,9 +96,9 @@ ODS в учебном проекте — это: #### `ods.airports` ```sql airport_code TEXT NOT NULL -airport_name TEXT NOT NULL -city TEXT NOT NULL -country TEXT NOT NULL +airport_name TEXT NOT NULL -- русское название, извлечено из JSON (поле 'ru') +city TEXT NOT NULL -- русское название, извлечено из JSON (поле 'ru') +country TEXT NOT NULL -- русское название, извлечено из JSON (поле 'ru') coordinates TEXT timezone TEXT NOT NULL _load_id TEXT NOT NULL @@ -106,10 +106,17 @@ _load_ts TIMESTAMP NOT NULL DEFAULT now() DISTRIBUTED BY (airport_code) ``` +> **Примечание о нормализации JSON**: Источник (`bookings.airports_data`) хранит +> мультиязычные поля (`airport_name`, `city`, `country`) как JSON-объекты вида +> `{"en": "...", "ru": "..."}`. На уровне ODS выполняется нормализация: +> извлекается только русский вариант (`->>'ru'`). Это упрощает downstream-логику +> (DDS/DM не нужно знать о внутренней структуре JSON) и соответствует принципу +> "типизированные данные" на уровне ODS. + #### `ods.airplanes` ```sql airplane_code TEXT NOT NULL -model TEXT NOT NULL +model TEXT NOT NULL -- русское название модели, извлечено из JSON (поле 'ru') range_km INTEGER speed_kmh INTEGER _load_id TEXT NOT NULL @@ -117,6 +124,10 @@ _load_ts TIMESTAMP NOT NULL DEFAULT now() DISTRIBUTED BY (airplane_code) ``` +> **Примечание о нормализации JSON**: Источник (`bookings.aircrafts_data`) хранит +> поле `model` как JSON-объект `{"en": "...", "ru": "..."}`. На уровне ODS +> извлекается только русский вариант (`->>'ru'`) для консистентности с `airports`. + #### `ods.routes` ```sql route_no TEXT NOT NULL diff --git a/sql/ods/airplanes_load.sql b/sql/ods/airplanes_load.sql index 99a0a12..78ecae2 100644 --- a/sql/ods/airplanes_load.sql +++ b/sql/ods/airplanes_load.sql @@ -1,10 +1,13 @@ -- Загрузка ODS по airplanes: SCD1 (UPDATE изменившихся + INSERT новых). -- Statement 1: UPDATE существующих строк. +-- Нормализация JSON: извлекаем русское название модели из поля с мультиязычностью. +-- Почему: источник хранит переводы как {"en": "...", "ru": "..."}, +-- в ODS оставляем только один язык для упрощения downstream-логики. WITH src AS ( SELECT s.airplane_code, - s.model, + s.model::json->>'ru' AS model, NULLIF(s.range, '')::INTEGER AS range_km, NULLIF(s.speed, '')::INTEGER AS speed_kmh, ROW_NUMBER() OVER ( @@ -30,10 +33,11 @@ WHERE s.rn = 1 ); -- Statement 2: INSERT новых строк. +-- Нормализация JSON: извлекаем русское название модели из поля с мультиязычностью. WITH src AS ( SELECT s.airplane_code, - s.model, + s.model::json->>'ru' AS model, NULLIF(s.range, '')::INTEGER AS range_km, NULLIF(s.speed, '')::INTEGER AS speed_kmh, ROW_NUMBER() OVER ( diff --git a/sql/ods/airports_load.sql b/sql/ods/airports_load.sql index 8a62893..e4a9c97 100644 --- a/sql/ods/airports_load.sql +++ b/sql/ods/airports_load.sql @@ -1,12 +1,15 @@ -- Загрузка ODS по airports: SCD1 (UPDATE изменившихся + INSERT новых). -- Statement 1: UPDATE существующих строк. +-- Нормализация JSON: извлекаем русские названия из полей с мультиязычностью. +-- Почему: источник хранит переводы как {"en": "...", "ru": "..."}, +-- в ODS оставляем только один язык для упрощения downstream-логики. WITH src AS ( SELECT s.airport_code, - s.airport_name, - s.city, - s.country, + s.airport_name::json->>'ru' AS airport_name, + s.city::json->>'ru' AS city, + s.country::json->>'ru' AS country, s.coordinates, s.timezone, ROW_NUMBER() OVER ( @@ -36,12 +39,13 @@ WHERE s.rn = 1 ); -- Statement 2: INSERT новых строк. +-- Нормализация JSON: извлекаем русские названия из полей с мультиязычностью. WITH src AS ( SELECT s.airport_code, - s.airport_name, - s.city, - s.country, + s.airport_name::json->>'ru' AS airport_name, + s.city::json->>'ru' AS city, + s.country::json->>'ru' AS country, s.coordinates, s.timezone, ROW_NUMBER() OVER ( diff --git a/tests/test_ods_snapshot_integration.py b/tests/test_ods_snapshot_integration.py index 3c227f6..f0ca294 100644 --- a/tests/test_ods_snapshot_integration.py +++ b/tests/test_ods_snapshot_integration.py @@ -98,8 +98,8 @@ def test_snapshot_airports_contract_upsert_delete_and_dq() -> None: airport_code, airport_name, city, country, coordinates, timezone, src_created_at_ts, load_dttm, batch_id ) VALUES - ('AAA', 'Airport A', 'City A', 'Country A', '(0,0)', 'UTC', now(), now(), 'batch_1'), - ('BBB', 'Airport B', 'City B', 'Country B', '(1,1)', 'UTC', now(), now(), 'batch_1'); + ('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'); """ ) @@ -120,8 +120,8 @@ def test_snapshot_airports_contract_upsert_delete_and_dq() -> None: airport_code, airport_name, city, country, coordinates, timezone, src_created_at_ts, load_dttm, batch_id ) VALUES - ('AAA', 'Airport A v2', 'City A', 'Country A', '(0,0)', 'UTC', now(), now(), 'batch_2'), - ('CCC', 'Airport C', 'City C', 'Country C', '(2,2)', 'UTC', now(), now(), 'batch_2'); + ('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'); """ ) @@ -136,10 +136,11 @@ def test_snapshot_airports_contract_upsert_delete_and_dq() -> None: ).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 == "Airport A v2" + assert airport_a_name == "Аэропорт A v2" _psql( _render_airports_sql(