diff --git a/docs/adr/0005-event-ingestion.md b/docs/adr/0005-event-ingestion.md index 5b32f36..435ace1 100644 --- a/docs/adr/0005-event-ingestion.md +++ b/docs/adr/0005-event-ingestion.md @@ -191,7 +191,10 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд класс брака поэтому проверяет именно объект, а не валидность; - `JSONExtractKeys('123')` возвращает пустой массив, и он же приходит от вовсе не-JSON. Значит скаляр проваливает и сверку ключей — отсюда - обязательный порядок классов; + обязательный порядок классов. Раз `isValidJSON` объект от скаляра не + отличает, первый класс держит `JSONType`: она возвращает `Object` у объекта, + `Int64` у скаляра `123` и `Null` у вовсе не-JSON и у пустой строки — + исключения не бросает ни в одном случае, то есть годится в предикат; - `JSONAsString` на некорректном вводе падает, а не пропускает строку: код 117 `INCORRECT_DATA`, «JSON object must begin with '{'». Падает и на скаляре `123`. На этом стоит отказ от него в пользу `RawBLOB`; diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 3ddb734..1391cc1 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -219,9 +219,11 @@ Airflow она стоит — там это обычный `SETTINGS` у зап нахлёста.** Одна забирает годные строки в `ods.event_dist`, вторая — брак в `ods.event_errors_dist`. Строка, подошедшая обеим, задвоится; не подошедшая ни одной — исчезнет молча. Держится это формой: второе условие пишется буквальным -отрицанием первого, а сам предикат собирается только из функций, не возвращающих -NULL, — иначе трёхзначная логика даст строку, которую не возьмёт ни `условие`, -ни `NOT условие`. Те же функции не должны и бросать исключений: упавшая матвью +отрицанием первого, а сам предикат NULL не возвращает ни в одной своей части, — +иначе трёхзначная логика даст строку, которую не возьмёт ни `условие`, ни +`NOT условие`. Обнуляемый разбор в предикате поэтому есть, но заканчивается +`IS NOT NULL`, а сравнения дают 0 или 1. Функции предиката не должны и бросать +исключений: упавшая матвью роняет вставку и останавливает потребление до починки ([ADR 0005](../adr/0005-event-ingestion.md)). @@ -283,7 +285,10 @@ D0 и к реальному календарю не привязана; паке брака нет разобранных полей: шардируется `cityHash64` сырой строки — `ClientID` у строки, которая не разобралась, взять неоткуда; нарезается по дню загрузки, как и сырьё; живёт месяц. Дольше сырья — намеренно: если брак истекает вместе с ним, -разбираться к моменту разбирательства будет уже нечем. +разбираться к моменту разбирательства будет уже нечем. Снятие — целыми кусками +(`ttl_only_drop_parts`), как у сырья и по той же причине: строки в партиции дня +загрузки разного возраста не более чем на сутки, и куску незачем переживать +срок из-за самой свежей строки. Класс брака лежит в колонке `error_class` типа `LowCardinality(String)`. Без неё в таблице копятся строки «что-то не так» без ответа на «что именно», а витрине @@ -386,7 +391,7 @@ ODS. Второе: матвью приёма создаётся последне таймауту; `CREATE ... IF NOT EXISTS` на существующем объекте не бросает. **Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении -#37 (четыре 5 августа 2026 года, пятый 6 августа) и два при исполнении #43 +#37 (четыре 5 августа 2026 года, пятый 6 августа) и четыре при исполнении #43 (7 августа). Все подтвердили то, что здесь написано. - Матвью с источником-`Distributed` срабатывает на вставку именно в эту @@ -403,6 +408,15 @@ ODS. Второе: матвью приёма создаётся последне `Distributed` кладёт блок в спул и сразу возвращает управление, так что на сносе локальной ошибка всплыла бы фоном и утверждение показалось бы опровергнутым. +- `_load_ts` в `ods.event` — это метка исходной строки сырья, а не время + разбора. Сверено по `WatchID` на двух тысячах событий модельного дня, + залитого дважды: у каждого события метка совпала с меткой одной из двух его + доставок, а после `FINAL` — с меткой поздней. Случаев «метки нет среди + доставок» ноль, то есть `now64()` в матвью разбора нет. +- Форма ключа `ods.event` принимается такой, как её задумала спека: выражение + `intHash32(ClientID)` стоит в ключе сортировки `ReplacingMergeTree`, а + `SAMPLE BY` — по тому же выражению. Вопрос стоял открытым в разделе 11 + мастер-спеки; ответ — DDL применяется и таблица работает. - `RawBLOB` даёт ровно одну строку на каждое непустое сообщение. Три сообщения с ключами, поставленные в очередь до одного сброса продюсера, стали тремя diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md index 0aa9420..5eca965 100644 --- a/docs/specs/2026-07-30-stand-v2-realism.md +++ b/docs/specs/2026-07-30-stand-v2-realism.md @@ -585,10 +585,10 @@ v2 стартует пустым, поэтому объём ниже — это Список убывает по мере постройки: проверенное уходит отсюда, а ответ с датой остаётся там, где на него опираются. Формат чтеца и форма виртуальной метки -времени закрыты при исполнении #37, форма ключа ODS, поведение матвью над -`Distributed` и запасной именованный кортеж — при исполнении #43; ответы — в -[доке хранилища](../architecture/storage.md) и -[ADR 0005](../adr/0005-event-ingestion.md), разделы «Что проверено». +времени закрыты при исполнении #37; форма ключа ODS и поведение матвью над +`Distributed` — при исполнении #43, ответы в [доке +хранилища](../architecture/storage.md), раздел «Что проверено»; запасной +именованный кортеж — там же в [ADR 0005](../adr/0005-event-ingestion.md). - Поведение соединения двух Distributed-таблиц и `distributed_product_mode` — эмпирически на стенде (хвост #14). diff --git a/generator/src/clickstream_generator/schema.py b/generator/src/clickstream_generator/schema.py index db942da..83e36a8 100644 --- a/generator/src/clickstream_generator/schema.py +++ b/generator/src/clickstream_generator/schema.py @@ -6,7 +6,7 @@ его валидацию и «описание выгрузки» в доках (`schema_doc`). Хранилище строится по описанию, а не по модулю; границу сторожит строгий приём на его стороне — сверка набора ключей сообщения с контрактным списком, и -разошедшееся уходит в `ods.event_errors` (спека генератора, раздел 3). +разошедшееся уходит в таблицу ошибок (спека генератора, раздел 3). Что несёт описатель колонки: diff --git a/sql/ddl/30-ods-views.sql b/sql/ddl/30-ods-views.sql index 454c56e..1548fce 100644 --- a/sql/ddl/30-ods-views.sql +++ b/sql/ddl/30-ods-views.sql @@ -28,32 +28,14 @@ -- Годное событие: строка, прошедшая строгий приём. -- --- Строгий приём — это три вопроса, и первые два держат весь контракт схемы. +-- Строгий приём — это три вопроса: объект ли это JSON, тот ли набор ключей, +-- разбираются ли в свой тип пять опорных колонок. Почему именно так — почему +-- объект, а не валидность; почему сверка ключей заменяет сорок семь проверок +-- на присутствие; почему опорных пять, а не сорок семь — ADR 0005, «Решение». -- --- 1. Это вообще объект JSON? Проверяется именно объект, а не валидность: --- isValidJSON('123') возвращает единицу — скаляр тоже законный JSON --- (измерено на стенде 7 августа 2026 года). --- --- 2. Совпадает ли набор ключей с контрактным — все сорок семь имён, ни одного --- лишнего. Одно это сравнение заменяет сорок семь проверок на присутствие --- и ловит то, чего иначе не поймать вовсе: опечатку в имени поля (для --- хранилища это одновременно пропавшее ожидаемое и появившееся лишнее), --- молчаливое расширение контракта источником и любую подмену имени в --- колонке-массиве. Обязательны все сорок семь: генератор шлёт их в каждом --- событии, а «пусто» по контракту — пустое значение, а не отсутствие --- ключа. --- --- arraySort стоит с обеих сторон, и это не украшение. Без него сорок семь --- CamelCase-имён пришлось бы выписать руками ровно в байтовом порядке — --- ошибка, которая увела бы в брак вообще всё, и притом молча. --- --- 3. Разбираются ли пять опорных колонок в свой тип. Не сорок семь, а пять: --- идентификаторы события, визита и посетителя, дата партиции и метка --- времени. Порча любой из них отравляет всё ниже по течению, тогда как --- единственный производитель топика — свой генератор, сериализующий по --- объявленным типам, и неверный тип может прийти только из руки. Сорок --- семь проверок на NULL превратили бы матвью в простыню, не добавив --- защиты. Присутствие остальных сорока двух держит вопрос 2. +-- arraySort ниже стоит с обеих сторон, и убирать его нельзя: без обёртки сорок +-- семь CamelCase-имён пришлось бы держать ровно в байтовом порядке, а сбой +-- порядка увёл бы в брак вообще всё, и молча (ADR 0005). -- -- Метку времени разбирает не JSONExtract, а parseDateTimeBestEffortOrNull, и -- это измеренная необходимость, а не вкус. На проводе UTCEventTime уезжает в @@ -155,8 +137,7 @@ WHERE is_object AND keys_match AND key_fields_parsed; -- Предикат повторён здесь дословно, и это выбор, а не безвыходность: назвать -- его один раз на две матвью позволил бы CREATE FUNCTION. Отвергнуто — условие -- разбора ушло бы за имя, в отдельный объект со своей жизнью, и слой перестал --- бы читаться по своему же DDL. Расхождения двух копий сторожит соседство: --- обе живут в одном файле, в полусотне строк друг от друга. +-- бы читаться по своему же DDL. CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_errors_mv ON CLUSTER clickstream_cluster TO ods.event_errors_dist AS