docs(ods): находки ревью — опыт с _load_ts, точность формулировок, рез повторов
Зачем: холодное ревью по двум линиям нашло дыру в следе опытов и три места, где текст утверждает не то, что построено. Что: - Опыт «_load_ts переносится из сырья» прогнан и записан: у двух тысяч событий метка совпала с меткой одной из доставок, случаев «метки нет среди доставок» ноль. Туда же — ответ про форму ключа ODS: вопрос раздела 11 спеки закрывался молча. - Дока хранилища говорила, что предикат собран из функций, не возвращающих NULL; построено иначе — обнуляемый разбор есть, но кончается IS NOT NULL. - Записана гарантия на JSONType: на не-JSON и пустой строке она отдаёт Null и не бросает, то есть годится в предикат. Раньше первый класс брака стоял на замере соседней функции. - ttl_only_drop_parts у таблицы ошибок назван в доке хранилища. - Комментарий матвью ужат: три вопроса строгого приёма пересказывали ADR 0005 целиком. Осталось то, чего по коду не видно, — запрет трогать arraySort и замер про ISO-8601. Убрано неверное «в полусотне строк» и упоминание имени таблицы хранилища в докстринге контракта генератора. Проверка: DDL применяется на живом кластере; make lint, typecheck, docs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,7 +191,10 @@ ClickHouse 26.3.17.56. Все четыре ответили так, как жд
|
|||||||
класс брака поэтому проверяет именно объект, а не валидность;
|
класс брака поэтому проверяет именно объект, а не валидность;
|
||||||
- `JSONExtractKeys('123')` возвращает пустой массив, и он же приходит от
|
- `JSONExtractKeys('123')` возвращает пустой массив, и он же приходит от
|
||||||
вовсе не-JSON. Значит скаляр проваливает и сверку ключей — отсюда
|
вовсе не-JSON. Значит скаляр проваливает и сверку ключей — отсюда
|
||||||
обязательный порядок классов;
|
обязательный порядок классов. Раз `isValidJSON` объект от скаляра не
|
||||||
|
отличает, первый класс держит `JSONType`: она возвращает `Object` у объекта,
|
||||||
|
`Int64` у скаляра `123` и `Null` у вовсе не-JSON и у пустой строки —
|
||||||
|
исключения не бросает ни в одном случае, то есть годится в предикат;
|
||||||
- `JSONAsString` на некорректном вводе падает, а не пропускает строку: код 117
|
- `JSONAsString` на некорректном вводе падает, а не пропускает строку: код 117
|
||||||
`INCORRECT_DATA`, «JSON object must begin with '{'». Падает и на скаляре
|
`INCORRECT_DATA`, «JSON object must begin with '{'». Падает и на скаляре
|
||||||
`123`. На этом стоит отказ от него в пользу `RawBLOB`;
|
`123`. На этом стоит отказ от него в пользу `RawBLOB`;
|
||||||
|
|||||||
@@ -219,9 +219,11 @@ Airflow она стоит — там это обычный `SETTINGS` у зап
|
|||||||
нахлёста.** Одна забирает годные строки в `ods.event_dist`, вторая — брак в
|
нахлёста.** Одна забирает годные строки в `ods.event_dist`, вторая — брак в
|
||||||
`ods.event_errors_dist`. Строка, подошедшая обеим, задвоится; не подошедшая ни
|
`ods.event_errors_dist`. Строка, подошедшая обеим, задвоится; не подошедшая ни
|
||||||
одной — исчезнет молча. Держится это формой: второе условие пишется буквальным
|
одной — исчезнет молча. Держится это формой: второе условие пишется буквальным
|
||||||
отрицанием первого, а сам предикат собирается только из функций, не возвращающих
|
отрицанием первого, а сам предикат NULL не возвращает ни в одной своей части, —
|
||||||
NULL, — иначе трёхзначная логика даст строку, которую не возьмёт ни `условие`,
|
иначе трёхзначная логика даст строку, которую не возьмёт ни `условие`, ни
|
||||||
ни `NOT условие`. Те же функции не должны и бросать исключений: упавшая матвью
|
`NOT условие`. Обнуляемый разбор в предикате поэтому есть, но заканчивается
|
||||||
|
`IS NOT NULL`, а сравнения дают 0 или 1. Функции предиката не должны и бросать
|
||||||
|
исключений: упавшая матвью
|
||||||
роняет вставку и останавливает потребление до починки
|
роняет вставку и останавливает потребление до починки
|
||||||
([ADR 0005](../adr/0005-event-ingestion.md)).
|
([ADR 0005](../adr/0005-event-ingestion.md)).
|
||||||
|
|
||||||
@@ -283,7 +285,10 @@ D0 и к реальному календарю не привязана; паке
|
|||||||
брака нет разобранных полей: шардируется `cityHash64` сырой строки — `ClientID` у
|
брака нет разобранных полей: шардируется `cityHash64` сырой строки — `ClientID` у
|
||||||
строки, которая не разобралась, взять неоткуда; нарезается по дню загрузки, как и
|
строки, которая не разобралась, взять неоткуда; нарезается по дню загрузки, как и
|
||||||
сырьё; живёт месяц. Дольше сырья — намеренно: если брак истекает вместе с ним,
|
сырьё; живёт месяц. Дольше сырья — намеренно: если брак истекает вместе с ним,
|
||||||
разбираться к моменту разбирательства будет уже нечем.
|
разбираться к моменту разбирательства будет уже нечем. Снятие — целыми кусками
|
||||||
|
(`ttl_only_drop_parts`), как у сырья и по той же причине: строки в партиции дня
|
||||||
|
загрузки разного возраста не более чем на сутки, и куску незачем переживать
|
||||||
|
срок из-за самой свежей строки.
|
||||||
|
|
||||||
Класс брака лежит в колонке `error_class` типа `LowCardinality(String)`. Без неё
|
Класс брака лежит в колонке `error_class` типа `LowCardinality(String)`. Без неё
|
||||||
в таблице копятся строки «что-то не так» без ответа на «что именно», а витрине
|
в таблице копятся строки «что-то не так» без ответа на «что именно», а витрине
|
||||||
@@ -386,7 +391,7 @@ ODS. Второе: матвью приёма создаётся последне
|
|||||||
таймауту; `CREATE ... IF NOT EXISTS` на существующем объекте не бросает.
|
таймауту; `CREATE ... IF NOT EXISTS` на существующем объекте не бросает.
|
||||||
|
|
||||||
**Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении
|
**Проверено на стенде.** Опыты прогнаны на живом кластере: пять при исполнении
|
||||||
#37 (четыре 5 августа 2026 года, пятый 6 августа) и два при исполнении #43
|
#37 (четыре 5 августа 2026 года, пятый 6 августа) и четыре при исполнении #43
|
||||||
(7 августа). Все подтвердили то, что здесь написано.
|
(7 августа). Все подтвердили то, что здесь написано.
|
||||||
|
|
||||||
- Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
- Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
||||||
@@ -403,6 +408,15 @@ ODS. Второе: матвью приёма создаётся последне
|
|||||||
`Distributed` кладёт блок в спул и сразу возвращает управление, так что на
|
`Distributed` кладёт блок в спул и сразу возвращает управление, так что на
|
||||||
сносе локальной ошибка всплыла бы фоном и утверждение показалось бы
|
сносе локальной ошибка всплыла бы фоном и утверждение показалось бы
|
||||||
опровергнутым.
|
опровергнутым.
|
||||||
|
- `_load_ts` в `ods.event` — это метка исходной строки сырья, а не время
|
||||||
|
разбора. Сверено по `WatchID` на двух тысячах событий модельного дня,
|
||||||
|
залитого дважды: у каждого события метка совпала с меткой одной из двух его
|
||||||
|
доставок, а после `FINAL` — с меткой поздней. Случаев «метки нет среди
|
||||||
|
доставок» ноль, то есть `now64()` в матвью разбора нет.
|
||||||
|
- Форма ключа `ods.event` принимается такой, как её задумала спека: выражение
|
||||||
|
`intHash32(ClientID)` стоит в ключе сортировки `ReplacingMergeTree`, а
|
||||||
|
`SAMPLE BY` — по тому же выражению. Вопрос стоял открытым в разделе 11
|
||||||
|
мастер-спеки; ответ — DDL применяется и таблица работает.
|
||||||
|
|
||||||
- `RawBLOB` даёт ровно одну строку на каждое непустое сообщение. Три сообщения
|
- `RawBLOB` даёт ровно одну строку на каждое непустое сообщение. Три сообщения
|
||||||
с ключами, поставленные в очередь до одного сброса продюсера, стали тремя
|
с ключами, поставленные в очередь до одного сброса продюсера, стали тремя
|
||||||
|
|||||||
@@ -585,10 +585,10 @@ v2 стартует пустым, поэтому объём ниже — это
|
|||||||
|
|
||||||
Список убывает по мере постройки: проверенное уходит отсюда, а ответ с датой
|
Список убывает по мере постройки: проверенное уходит отсюда, а ответ с датой
|
||||||
остаётся там, где на него опираются. Формат чтеца и форма виртуальной метки
|
остаётся там, где на него опираются. Формат чтеца и форма виртуальной метки
|
||||||
времени закрыты при исполнении #37, форма ключа ODS, поведение матвью над
|
времени закрыты при исполнении #37; форма ключа ODS и поведение матвью над
|
||||||
`Distributed` и запасной именованный кортеж — при исполнении #43; ответы — в
|
`Distributed` — при исполнении #43, ответы в [доке
|
||||||
[доке хранилища](../architecture/storage.md) и
|
хранилища](../architecture/storage.md), раздел «Что проверено»; запасной
|
||||||
[ADR 0005](../adr/0005-event-ingestion.md), разделы «Что проверено».
|
именованный кортеж — там же в [ADR 0005](../adr/0005-event-ingestion.md).
|
||||||
|
|
||||||
- Поведение соединения двух Distributed-таблиц и `distributed_product_mode` —
|
- Поведение соединения двух Distributed-таблиц и `distributed_product_mode` —
|
||||||
эмпирически на стенде (хвост #14).
|
эмпирически на стенде (хвост #14).
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
его валидацию и «описание выгрузки» в доках (`schema_doc`). Хранилище
|
его валидацию и «описание выгрузки» в доках (`schema_doc`). Хранилище
|
||||||
строится по описанию, а не по модулю; границу сторожит строгий приём на его
|
строится по описанию, а не по модулю; границу сторожит строгий приём на его
|
||||||
стороне — сверка набора ключей сообщения с контрактным списком, и
|
стороне — сверка набора ключей сообщения с контрактным списком, и
|
||||||
разошедшееся уходит в `ods.event_errors` (спека генератора, раздел 3).
|
разошедшееся уходит в таблицу ошибок (спека генератора, раздел 3).
|
||||||
|
|
||||||
Что несёт описатель колонки:
|
Что несёт описатель колонки:
|
||||||
|
|
||||||
|
|||||||
@@ -28,32 +28,14 @@
|
|||||||
|
|
||||||
-- Годное событие: строка, прошедшая строгий приём.
|
-- Годное событие: строка, прошедшая строгий приём.
|
||||||
--
|
--
|
||||||
-- Строгий приём — это три вопроса, и первые два держат весь контракт схемы.
|
-- Строгий приём — это три вопроса: объект ли это JSON, тот ли набор ключей,
|
||||||
|
-- разбираются ли в свой тип пять опорных колонок. Почему именно так — почему
|
||||||
|
-- объект, а не валидность; почему сверка ключей заменяет сорок семь проверок
|
||||||
|
-- на присутствие; почему опорных пять, а не сорок семь — ADR 0005, «Решение».
|
||||||
--
|
--
|
||||||
-- 1. Это вообще объект JSON? Проверяется именно объект, а не валидность:
|
-- arraySort ниже стоит с обеих сторон, и убирать его нельзя: без обёртки сорок
|
||||||
-- isValidJSON('123') возвращает единицу — скаляр тоже законный JSON
|
-- семь CamelCase-имён пришлось бы держать ровно в байтовом порядке, а сбой
|
||||||
-- (измерено на стенде 7 августа 2026 года).
|
-- порядка увёл бы в брак вообще всё, и молча (ADR 0005).
|
||||||
--
|
|
||||||
-- 2. Совпадает ли набор ключей с контрактным — все сорок семь имён, ни одного
|
|
||||||
-- лишнего. Одно это сравнение заменяет сорок семь проверок на присутствие
|
|
||||||
-- и ловит то, чего иначе не поймать вовсе: опечатку в имени поля (для
|
|
||||||
-- хранилища это одновременно пропавшее ожидаемое и появившееся лишнее),
|
|
||||||
-- молчаливое расширение контракта источником и любую подмену имени в
|
|
||||||
-- колонке-массиве. Обязательны все сорок семь: генератор шлёт их в каждом
|
|
||||||
-- событии, а «пусто» по контракту — пустое значение, а не отсутствие
|
|
||||||
-- ключа.
|
|
||||||
--
|
|
||||||
-- arraySort стоит с обеих сторон, и это не украшение. Без него сорок семь
|
|
||||||
-- CamelCase-имён пришлось бы выписать руками ровно в байтовом порядке —
|
|
||||||
-- ошибка, которая увела бы в брак вообще всё, и притом молча.
|
|
||||||
--
|
|
||||||
-- 3. Разбираются ли пять опорных колонок в свой тип. Не сорок семь, а пять:
|
|
||||||
-- идентификаторы события, визита и посетителя, дата партиции и метка
|
|
||||||
-- времени. Порча любой из них отравляет всё ниже по течению, тогда как
|
|
||||||
-- единственный производитель топика — свой генератор, сериализующий по
|
|
||||||
-- объявленным типам, и неверный тип может прийти только из руки. Сорок
|
|
||||||
-- семь проверок на NULL превратили бы матвью в простыню, не добавив
|
|
||||||
-- защиты. Присутствие остальных сорока двух держит вопрос 2.
|
|
||||||
--
|
--
|
||||||
-- Метку времени разбирает не JSONExtract, а parseDateTimeBestEffortOrNull, и
|
-- Метку времени разбирает не JSONExtract, а parseDateTimeBestEffortOrNull, и
|
||||||
-- это измеренная необходимость, а не вкус. На проводе UTCEventTime уезжает в
|
-- это измеренная необходимость, а не вкус. На проводе UTCEventTime уезжает в
|
||||||
@@ -155,8 +137,7 @@ WHERE is_object AND keys_match AND key_fields_parsed;
|
|||||||
-- Предикат повторён здесь дословно, и это выбор, а не безвыходность: назвать
|
-- Предикат повторён здесь дословно, и это выбор, а не безвыходность: назвать
|
||||||
-- его один раз на две матвью позволил бы CREATE FUNCTION. Отвергнуто — условие
|
-- его один раз на две матвью позволил бы CREATE FUNCTION. Отвергнуто — условие
|
||||||
-- разбора ушло бы за имя, в отдельный объект со своей жизнью, и слой перестал
|
-- разбора ушло бы за имя, в отдельный объект со своей жизнью, и слой перестал
|
||||||
-- бы читаться по своему же DDL. Расхождения двух копий сторожит соседство:
|
-- бы читаться по своему же DDL.
|
||||||
-- обе живут в одном файле, в полусотне строк друг от друга.
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_errors_mv ON CLUSTER clickstream_cluster
|
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_errors_mv ON CLUSTER clickstream_cluster
|
||||||
TO ods.event_errors_dist
|
TO ods.event_errors_dist
|
||||||
AS
|
AS
|
||||||
|
|||||||
Reference in New Issue
Block a user