docs(ingestion): упрощены объяснения источников и приема данных
Зачем: помочь начинающему инженеру проследить событие и версию заказа от генератора до хранилища, включая ошибки и повторные поступления. Что: обновлены вводная генератора, справочники приема и модели заказа, разделы хранилища и комментарии SQL и дага. Сохранены технические условия; исполняемый код, схемы и поведение стенда не изменены. Проверка: make lint, git diff --check, 142 локальные ссылки и справка CLI. SQL без комментариев и Python AST без строк документации совпали с исходными. Генерация данных и сброс состояния не выполнялись. Refs #250
This commit is contained in:
1 parent
83434102d5
commit
75f28fe99f
13 files changed
+663
-974
No files matched your search
+27
-46
@@ -1,24 +1,15 @@
|
||||
-- Разбор одного среза заказов: сначала годные версии, затем брак.
|
||||
-- Разбор сохраненной порции заказов: сначала годные версии, затем ошибки.
|
||||
-- Макрос Jinja подставляет одно условие годности в обе вставки.
|
||||
-- Вторая вставка берет его отрицание, чтобы каждая строка получила результат.
|
||||
-- Условие должно давать только 0 или 1 и не вызывать исключений на неверном raw.
|
||||
--
|
||||
-- Обе вставки должны делить один предикат. Локальный макрос Jinja
|
||||
-- разворачивает его в каждое выражение до того, как оператор разделит файл по
|
||||
-- точкам с запятой. Так контракт остаётся рядом с запросами и не становится
|
||||
-- третьим SQL-файлом, который нельзя исполнить самостоятельно.
|
||||
--
|
||||
-- Строгий приём проверяет форму провода и ничего сверх неё. Ключей одиннадцать,
|
||||
-- и проверяются все: десять скалярных значений прямо образуют типизированную
|
||||
-- строку заказа. Внутрь items приём не смотрит — содержимое позиций, переходы
|
||||
-- статуса и равенства сумм остаются ниже этой границы
|
||||
-- (docs/architecture/orders/ingestion.md, «Граница строгого приёма»).
|
||||
--
|
||||
-- У общего предиката два обязательных свойства. Он всегда даёт 0 или 1, не
|
||||
-- NULL: иначе строку не возьмут ни условие, ни его отрицание. И он не бросает
|
||||
-- исключений на произвольном raw: одна грязная строка не должна ронять весь
|
||||
-- переход, ради этого и существует таблица брака.
|
||||
-- Проверяются одиннадцать полей верхнего уровня. Внутри items поля не проверяются;
|
||||
-- позиции разбирает DDS, а равенства сумм проверяются отдельно.
|
||||
-- Точные правила — docs/architecture/orders/ingestion.md, «Граница строгого приёма».
|
||||
|
||||
{% macro order_wire_contract() %}
|
||||
WITH
|
||||
-- Каноническое время провода: UTC, ровно три знака долей секунды.
|
||||
-- Время источника: UTC, ровно три знака долей секунды.
|
||||
'yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'' AS ts_mask,
|
||||
JSONType(raw) = 'Object' AS is_object,
|
||||
arraySort(JSONExtractKeys(raw)) = arraySort([
|
||||
@@ -28,15 +19,14 @@ WITH
|
||||
]) AS keys_match,
|
||||
JSONType(raw, 'order_id') = 'String'
|
||||
AND JSONType(raw, 'status') = 'String'
|
||||
-- Целое у JSONType зовётся двумя именами, и нужны оба: с одним
|
||||
-- Int64 законный идентификатор за 2^63 уехал бы в брак.
|
||||
-- JSONType различает Int64 и UInt64; принимаются оба, если значение
|
||||
-- разбирается в UInt64, в том числе идентификаторы больше 2^63−1.
|
||||
AND JSONType(raw, 'user_id') IN ('Int64', 'UInt64')
|
||||
AND JSONExtract(raw, 'user_id', 'Nullable(UInt64)') IS NOT NULL
|
||||
AND JSONType(raw, 'items') = 'Array'
|
||||
-- Форму держит регулярка, реальность — разбор, и порознь они дырявы:
|
||||
-- маска берёт «2026-6-3T…» без ведущих нулей, а регулярка пропускает
|
||||
-- 30 февраля. Обе половины измерены — docs/architecture/storage.md,
|
||||
-- «Что проверено».
|
||||
-- Регулярное выражение проверяет точную форму, включая ведущие нули.
|
||||
-- Разбор проверяет само время: например, отвергает 30 февраля.
|
||||
-- Замеры функций — docs/architecture/storage.md, «Что проверено».
|
||||
AND arrayAll(k ->
|
||||
JSONType(raw, k) = 'String'
|
||||
AND match(JSONExtractString(raw, k),
|
||||
@@ -44,9 +34,8 @@ WITH
|
||||
AND parseDateTime64InJodaSyntaxOrNull(
|
||||
JSONExtractString(raw, k), ts_mask, 'UTC') IS NOT NULL,
|
||||
['created_at', 'updated_at'])
|
||||
-- Деньги — строка с ровно двумя знаками после точки. Регулярка держит
|
||||
-- форму, разбор — вместимость: у строки из двадцати девяток форма
|
||||
-- хороша, а в Decimal(18, 2) она не влезает.
|
||||
-- Сумма в рублях: строка с двумя знаками после точки, без минуса.
|
||||
-- После проверки формы проверяется возможность разбора в Decimal.
|
||||
AND arrayAll(k ->
|
||||
JSONType(raw, k) = 'String'
|
||||
AND match(JSONExtractString(raw, k), '^\\d+\\.\\d{2}$')
|
||||
@@ -60,11 +49,9 @@ WITH
|
||||
is_object AND keys_match AND fields_valid AS row_is_valid
|
||||
{% endmacro %}
|
||||
|
||||
-- Годные версии заказа. Срез читается по _load_id — тому же, что проставил
|
||||
-- забор: разбор идёт по неизменной порции, а не по «всему, что появилось».
|
||||
--
|
||||
-- Разобранные значения берёт assumeNotNull: обнуляемый разбор стоит за
|
||||
-- предикатом, который NULL уже отсёк, и приведение здесь не может упасть.
|
||||
-- Годные строки сохраненного среза по _load_id запуска приема.
|
||||
-- WHERE исключает строки с NULL в разобранных полях; assumeNotNull
|
||||
-- приводит оставшиеся значения к типам целевой таблицы.
|
||||
|
||||
INSERT INTO ods.order_dist
|
||||
{{ order_wire_contract() }}
|
||||
@@ -81,30 +68,25 @@ SELECT
|
||||
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'discount'), 2)) AS discount,
|
||||
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'delivery'), 2)) AS delivery,
|
||||
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'total'), 2)) AS total,
|
||||
-- items кладётся сырым фрагментом JSON, а не разобранной структурой.
|
||||
-- items сохраняется строкой JSON для разбора позиций в DDS.
|
||||
JSONExtractRaw(raw, 'items') AS items,
|
||||
toDate(assumeNotNull(parseDateTimeInJodaSyntaxOrNull(
|
||||
JSONExtractString(raw, 'snapshot_date'),
|
||||
'yyyy-MM-dd', 'UTC'))) AS snapshot_date,
|
||||
-- Метки запуска и прибытия переносятся как есть. Поставь здесь now64(3) —
|
||||
-- и _load_ts молча ответила бы на другой вопрос: «когда разобрали».
|
||||
-- _load_ts остается временем приема в STG; _load_id связывает с исходной порцией.
|
||||
_load_id,
|
||||
_load_ts
|
||||
FROM stg.orders_raw_dist
|
||||
WHERE _load_id = {load_id:String} AND row_is_valid
|
||||
-- Заводская двойка parallel_distributed_insert_select исполняет вставку из
|
||||
-- Distributed в Distributed локально на каждом шарде, минуя ключ шардирования
|
||||
-- цели, — так заказы и расползлись по обоим шардам. Ноль отдаёт раскладку
|
||||
-- инициатору, по ключу цели. Капкан целиком — docs/architecture/storage.md,
|
||||
-- «Раскладка по шардам».
|
||||
-- Ноль заставляет распределить результат по ключу цели. При значении 2
|
||||
-- вставка может сохранить раскладку STG, где ключ — raw, вместо order_id.
|
||||
-- Подробнее — docs/architecture/storage.md, «Раскладка по шардам».
|
||||
SETTINGS distributed_foreground_insert = 1,
|
||||
parallel_distributed_insert_select = 0;
|
||||
|
||||
-- Брак: тот же срез и буквальное отрицание того же предиката.
|
||||
--
|
||||
-- Классы перекрываются, поэтому проверяются по порядку, а в error_class идёт
|
||||
-- первый совпавший: скаляр проваливает и проверку на объект, и сверку ключей —
|
||||
-- без объявленного порядка он попал бы то в один класс, то в другой.
|
||||
-- Ошибки: тот же срез и отрицание условия годности.
|
||||
-- Класс определяется первым нарушенным условием: например, JSON-строка
|
||||
-- не проходит и проверку объекта, и проверку ключей, но получает not_an_object.
|
||||
|
||||
INSERT INTO ods.order_errors_dist
|
||||
{{ order_wire_contract() }}
|
||||
@@ -124,7 +106,6 @@ SELECT
|
||||
_load_ts
|
||||
FROM stg.orders_raw_dist
|
||||
WHERE _load_id = {load_id:String} AND NOT row_is_valid
|
||||
-- Тот же ноль: ключ брака сегодня совпадает с ключом STG, и локальная
|
||||
-- запись легла бы верно — но по совпадению, а не по контракту.
|
||||
-- Та же настройка: распределяем строки по ключу целевой таблицы ошибок.
|
||||
SETTINGS distributed_foreground_insert = 1,
|
||||
parallel_distributed_insert_select = 0;
|
||||
Reference in new issue
Block a user