Типизированный ODS: ods.event, строгий приём и таблица ошибок #43
Notifications
Due Date
No due date set.
Blocks
Depends on
#42 Зерновой мир: мини-манифест и заливка при make up
ddmitry/clickstream-data-platform
#37 DDL-бутстрап, топик hits и приём сырья в STG обеими нодами
ddmitry/clickstream-data-platform
#41 Сериализатор, приёмники и проигрыватель с CLI
ddmitry/clickstream-data-platform
Reference: ddmitry/clickstream-data-platform#43
Reference in New Issue
Block a user
Part of #4.
Цель
Типизированный слой событий:
ods.eventсо строгим приёмом, ошибкиразбора — в
ods.event_errors. DDL пишется по рендеренному «описаниювыгрузки» из #36 — по правилу data contract: хранилище строится по
документации источника, как в бою.
Всё, что нужно, уже стоит: сырьё, топик и чтец — из #37, настоящие события
в топике — из #41. Тикет достраивает последний этаж цепочки
Kafka → STG → ODS.
Механика приёма решена до начала — docs/adr/0005-event-ingestion.md, имена
объектов — docs/adr/0006-object-naming.md, конвенции и свойства приёма —
docs/architecture/storage.md.
Откуда берутся события для проверок
Проигрыватель из #41 уже есть, поэтому фикстуру не выдумывать. Проверки
идут на настоящих событиях генератора: управляемую пачку даёт проигрыватель
ограниченным прогоном (способ решается в #41 и описан в спеке, раздел 9).
Пачка обязана нести события, которых в ODS ещё нет. Повтор схлопнет
ReplacingMergeTree, и сумма «события плюс ошибки» не сойдётся с числомотправленного — опыт покраснеет без поломки. Отсюда и выбор дня: пачка
играет день за границей оси мира. Он заведомо не пересекается ни с зерновым
миром, ни с тем, что менти прожил дагом, а его строки лежат в собственной
партиции
EventDate— той самой, которую опыт за собой и сносит.Порченые сообщения делаются мутацией настоящего события, и берётся оно
из файла. У проигрывателя #41 есть файловый приёмник, заведённый как раз
для проверок: срез дня пишется в файл, три строки из него портятся — опечатка
в имени поля, скаляр вместо объекта, битое значение
UTCEventTime, — и втопик уходит всё сразу, целая пачка и порченые вместе.
Брать образец из доехавшего сырья было бы естественнее, но дороже: пришлось
бы ждать доезда дважды. Приём асинхронный — Kafka-движок копит блок и
отдаёт его либо по размеру, либо по
stream_flush_interval_ms, умолчание7,5 секунды (сверено по документации ClickHouse через Context7, 6 августа
2026 года). Файл снимает второе ожидание: одна отправка, одно ожидание,
дальше все утверждения по уже доехавшему.
Что выяснилось при #37 — учесть здесь
RawBLOBихчитает, двигает офсет и молча не порождает строки; замер и довод — в доке
хранилища. Для счётной сверки это значит: пустых сообщений в пачке быть не
должно, а если счёт разошёлся — сначала посмотреть на дыру в
kafka_offset,а не искать ошибку в разборе.
docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic hits.sleepнаугад. Образцыограниченного цикла есть в
scripts/stand-services.sh— ожидание удалениятопика и ожидание состояния запуска DAG. В
scripts/clickhouse-smoke.shсвоего цикла нет, там только
query_with_timeout— обёрткаtimeout.schema.py) — гонять сPYTHONDONTWRITEBYTECODE=1. Откат внутри секундыоставляет байт-код мутанта, и проверка зеленеет на сломанном коде.
Первым делом: четыре
SELECTНа них стоят порядок классов брака, сам выбор формата чтеца и запасной путь
свёртки разбора, а документация поведение на не-объекте не описывает:
isValidJSON('123')— ожидается единица: скаляр тоже законный JSON;JSONExtractKeys('123')— ожидается пустой массив;JSONAsStringна некорректном вводе — падает или пропускает;JSONExtractсNullable-членами — еюсворачиваются 47 вызовов в один, если разбор окажется дорогим.
Список — из ADR 0005, раздел «Что проверено», последние три пункта; четвёртый
стоит ещё и в разделе 11 мастер-спеки. Результаты записать в ADR 0005, туда же.
Если ответ разойдётся с ожиданием — это вопрос владельцу, а не починка на
месте: на этих
SELECTстоит постановка, а не деталь реализации.Что войдёт
ods.event_rep/ods.event_dist: ReplacingMergeTree по_load_ts,PARTITION BY EventDate, ключ по разделу 1.3 мастер-спеки (точную формуORDER BY/SAMPLE BYпроверить на стенде — раздел 11), шардированиеcityHash64(ClientID)._load_tsпереносится из STG как есть, неставится заново: колонка отвечает на «когда строка приехала в хранилище».
Работа у версии ровно одна — схлопнуть повтор доставки.
ods.event_errors_rep/_dist: сырой текст, метаданные доставки(
kafka_topic,kafka_partition,kafka_offset,kafka_timestamp,consumer_host), метка загрузки_load_tsи класс бракаerror_class(
LowCardinality(String)). Движок —ReplicatedMergeTreeбез заменыверсий: схлопывать брак не по чему.
ORDER BY (error_class, kafka_partition, kafka_offset), шардированиеcityHash64(raw), нарезка подню загрузки, срок жизни месяц — дольше сырья, иначе разбираться будет уже
нечем.
20-ods-tables.sql(таблицы) и30-ods-views.sql(матвью). Номера некосметика: матвью приёма из #37 лежит в
40-stg-views.sqlи обязанасоздаваться последней — она включает чтение топика, и до появления разбора
всё доехавшее минует ODS молча.
stg.hits_raw_dist: одна наполняет событие, вторая — ошибки.Условие второй — буквальное отрицание первой, а предикат собирается только из
функций, которые не возвращают NULL и не бросают исключений. Первое —
чтобы трёхзначная логика не съела строку между двумя матвью. Второе — потому
что упавшая матвью роняет вставку и останавливает потребление до починки:
это единственное, что сейчас держит правило «грязные записи не валят
пайплайн». Приведение
Nullableк необнуляемому типу и любую арифметикудержать за предикатом, который NULL уже отсёк.
arraySort(JSONExtractKeys(raw))с контрактным списком, завёрнутым в тот жеarraySort: 47 CamelCase-имён, выписанных руками ровно в байтовом порядке, —ошибка, которая увела бы в брак вообще всё. Обязательны все 47 полей:
генератор шлёт их все, «пусто» по контракту — пустое значение, а не
отсутствие ключа. Тип держит
Nullable-разбор пяти опорных колонок —WatchID,VisitID,ClientID,EventDate,UTCEventTime; остальные 42достаются обычными типами.
error_classпервымсовпавшим:
not_an_object,keyset_mismatch,key_field_unparsed. Порядокобязателен, потому что классы пересекаются: скаляр проваливает и проверку на
объект, и сверку ключей.
Опечатка внутри
JSONExtractдаётумолчание типа молча — пустой массив, ноль, пустую строку, — и это верно не
только для двенадцати массивов, но и для сорока двух обычных колонок. Ловится
это тем, что источники у двух сторон разные: выражения сверки собираются
из контракта, выражения матвью пишутся руками по описанию выгрузки. Опечатка
в одном месте не повторится в другом. Собрать выражения из контракта — разовая
работа при исполнении (
cd generator && uv run python -c ...поschema.COLUMNS), в репозиторий для этого не кладётся ничего: постоянныймодуль обслуживал бы опыт, который проходит один раз.
scripts/clickhouse-smoke.shвscripts/check-clickhouse.sh— отдельным первым коммитом PR, одним
git mv. Имя досталось от старой целиsmoke-cluster(#58). Тем же коммитом едутMakefileи таблица вdocs/architecture/testing.md, иначе в истории останется коммит со сломанной
целью; абзац-объяснение из testing.md уходит туда же.
Sign—колонка формата без механики (раздел 1.1); партиции по дням — осознанное
отступление от месячных у Метрики (раздел 1.3); вырождение ключа сортировки —
CounterIDиEventDateконстанты, реальная сортировка по посетителю исобытию (раздел 1.3). Четвёртый — от этого тикета:
_load_tsпереносится изсырья и служит колонкой версии, работа у которой ровно одна — схлопнуть
повтор доставки.
Критерии приёмки
Постоянных проверок этот тикет не добавляет ни одной (решение владельца
7 августа 2026 года). Все три утверждения ниже проверяются разовыми опытами
при исполнении, и след у них — запись в теле PR, как у «настоящего дня».
Правило, его довод и требование прибираться за собой — в
docs/architecture/testing.md, раздел «Интеграционная проверка постоянной целью
не становится»; здесь оно не переписывается.
Постоянным сторожем цепочки остаются счётчики #42 против мини-манифеста.
Что при этом честно теряется — тихая подмена значения в одной колонке:
понадобится сторож и на неё — дом у неё в паспорте манифеста (пара агрегатов
по детерминированному миру), а не в проверке, вкладывающей данные.
SELECTиз «первым делом» прогнаны, результаты записаныв ADR 0005.
в
*_errorsс классомkeyset_mismatch, а не молчаливые нули (именаCamelCase регистрозависимы). Скаляр —
not_an_object, а неkeyset_mismatch: приоритет классов работает. Битое значениеUTCEventTime—key_field_unparsed:Nullable-разбор опорных колонокработает. Одна проверка, три сообщения.
получили N строк сырья и N в сумме событий и ошибок. Пачка играет день
за границей оси мира, и её партиция сносится по окончании опыта.
Пачку обязательно
обрамить: считать не по всей таблице, а по диапазону
_load_tsэтогопрогона. Офсеты для обрамления ODS не годятся — метаданные доставки живут
у
stg.*_raw, аods.eventих не несёт (раздел 7 спеки); по офсетамможно считать только сырьё. Счёт по ODS — через
FINAL: голыйcount()по
ReplacingMergeTreeзависит от числа прошедших мержей и запрещёнразделом 6 спеки. Счёт — только по Distributed-таблицам.
Осторожно с сочетанием фильтра и
FINAL: документация ClickHouseговорит, что PREWHERE исполняется до
FINALи даёт перекос, если полефильтра не входит в
ORDER BY(сверено через Context7 6 августа2026 года).
_load_tsтуда не входит и не может — она колонка версии,а версия в ключе сортировки запрещена разделом 1.3 спеки. Значит
обрамление по
_load_tsподFINALнадо либо считать подзапросом,либо ставить
apply_prewhere_after_final, и выбранный способ назватьв PR.
текстом по всем 47 колонкам; выражения сверки собраны из контракта, а не
списаны с матвью. Пару «сырьё и событие» брать внутри окна хранения
сырья: за трое суток строка в STG истекает, и сравнивать становится
не с чем.
исполнении, и это записано в теле PR: что сломали и что опыт сказал.
Правило — docs/architecture/testing.md, «Проверка, которая не умеет
краснеть, бесполезна»: утверждение, ни разу не видевшее красного,
ничего не утверждает, даже разовое.
PR, а не постоянная проверка. Прогон нужен новый: матвью задним
числом не досыпают, и день, залитый при исполнении #41, в ODS сам не
появится — он останется только в сырье. Это не лишняя работа, а
свойство матвью, и заодно первая сцена урока про переобработку.
День в полсотни тысяч событий в повторяемую цель не ставится.
Проверить: день доезжает до
ods.event_dist; счёт послеFINALсходится с числомсгенерированных событий;
*_errorsпуст на честном прогоне; повторнаязаливка того же дня счёт в ODS не меняет. Три утверждения переехали
сюда из #41 при развороте порядка: они про хранилище, а не про
сериализатор.
ровно два утверждения, и оба закрываются здесь:
упавшая матвью останавливает потребление до починки — проверить
опытом, удалив у матвью её цель, и цель эта — распределённая
ods.event_dist. Удаление локальнойods.event_repопыт не ставит:вставка в
Distributedкладёт блок в спул и сразу возвращает управление(дока хранилища, «Приём»), ошибка всплывёт фоном, и утверждение
покажется опровергнутым. На этом опыте висит правило
«грязные записи не валят пайплайн»;
матвью с источником-
Distributedсрабатывает на вставку именно враспределённую таблицу — обе матвью этого тикета стоят над
stg.hits_raw_dist, так что утверждение подтверждается самим фактомдоезда; записать это явно, а не считать закрытым молча.
_load_tsвods.eventравен_load_tsисходной строки сырья, а невремени разбора — проверено разово при исполнении, сравнением по
WatchID, и записано в «Что проверено». Постоянной проверки не заводить:она сторожила бы исполнителя, а менти учит комментарий в DDL и абзац в
доке хранилища.
проверено». Contract-тест снят, и это правка документов и кода.
Места собрать грепом, а не по памяти: перечень в постановке был
неполон и неточен — на 7 августа 2026 года упоминаний в документах
шесть, и ADR 0005 упоминает один раз, а не два. Не были названы три,
и два из них видны первому читателю: спека генератора, строка 37
(«Целевая картина одним взглядом») и строка 286 — обещание, что
«каждый
make upбесплатно прогоняет весь конвейер и contract-тест нанастоящих данных». В коде мест три, все в
generator/src/clickstream_generator/schema.py: обещание сторожа вдокстринге модуля, «от этой записи зависит будущий contract-тест» в
пункте про
clickhouse_typeи сам довод этого пункта — требование писатьтип «ровно в той записи, в какой его вернёт
system.columns» держалось наснимаемой сверке. Правило остаётся (параметры входят в имя типа целиком,
без сокращений), но довод у него другой: по этой записи человек пишет DDL.
Механизм остаётся один — строгий приём, — и рядом с
ним сверка разобранного события, которой в спеке нет вовсе: она
появилась позже. Довод снятия: сверка объявлений ловила сверх строгого
приёма только смену типа, а сверка разобранного события ловит её же и
на живых данных.
пополняется — тикет их не добавляет; правится там только строка про
переименованный скрипт.
Границы
ADR 0005/0006 — не переоткрывать.
dds.*и витрины — этап 4.там же и он единственный. Постоянного модуля, печатающего контракт, тикет не
заводит: выражения сверки собираются разовым запуском при исполнении.
make check-clickhouseне заводить ни одной.Наблюдения, которые просятся проверкой, но учат лабой, — пошардовая
раскладка (сырьё и событие ложатся на разные шарды,
docs/architecture/storage.md), форма ключей и срок жизни в
system.tables. Это материал лаб и содержание DDL, а не приёмки.tests/smoke-guards.shи цельmake smoke-guardsсрезаны в #56 вместе с практикой. Доказуемость поломкинесут форма проверки и разовый показ красного в теле PR.
Сначала прочитать
Оговорка к разделу «Цена решения»: утверждение, что опечатка в имени
скалярного поля уводит строки в брак и видна сразу, верно только для пяти
опорных колонок с
Nullable-разбором; для остальных сорока двух она так жемолчалива, как у массивов. В том же абзаце устарела и соседняя строка —
«эти двенадцать колонок сторожит smoke»: цель переименована в
check-clickhouse, а сторож массивов заменён сверкой по всем 47 колонкам.Поправить тем же PR обе.
приёма, инвариант двух матвью, раздел «Что проверено».
makeи куда кладётся новая проверка.
и интерфейс проигрывателя в разделе 9.
schema_doc.pyрядом — образец потребителя.sql/ddl/— DDL из #37; туда же кладутся файлы 20 и 30, службаclickhouse-initподхватывает их по порядку имён сама.scripts/check-clickhouse.sh первым коммитом PR; проверок в него не
добавляется.
Проверка
make up && make check-clickhouse— цель должна остаться такой жебыстрой и с тем же числом проверок, что до тикета
Постановка переписана после грилинга и двух холодных ревью (2026-08-03/04).
Что изменилось по существу:
— механизм строгого приёма другой: input_format_skip_unknown_fields при чтении байтами беспредметен. Присутствие держит сверка набора ключей (одно сравнение JSONExtractKeys с контрактным списком), тип — Nullable-разбор пяти опорных колонок, а не всех 47;
— служебная колонка зовётся _load_ts, не _ingested_at;
— имена объектов на суффиксе вида (ADR 0006): ods.event_rep/_dist, ods.event_errors_rep/_dist;
— у таблицы ошибок собственные ключи: шардирование cityHash64(raw), нарезка по дню загрузки, срок жизни месяц. У брака нет ни ClientID, ни EventDate;
— матвью две, и их условия обязаны делить поток без зазора и нахлёста: второе пишется отрицанием первого, предикат — без NULL;
— добавлен сторож колонок-массивов в smoke: contract-тест до выражений матвью не дотягивается, а опечатка в имени массива даёт пустой массив тихо;
— сверка «сырьё = события + ошибки» разовая, на управляемой пачке: постоянной она была бы красной в норме, сроки хранения слоёв разные.
Механика и доводы — docs/adr/0005-event-ingestion.md, конвенции — docs/architecture/storage.md.
Переписано после холодного ревью и сверки утверждений о ClickHouse с
документацией. Против прежней постановки изменилось вот что.
У таблицы ошибок появился класс брака — колонка
error_classиобязательный порядок проверки. Классы пересекаются: сообщение, не являющееся
объектом JSON, проваливает заодно и сверку набора ключей, потому что
JSONExtractKeysот скаляра даёт пустой массив. Там же названы движок и ключсортировки таблицы.
Сверка ключей завёрнута в
arraySortс обеих сторон. Иначе сорок семьCamelCase-имён пришлось бы выписать руками ровно в байтовом порядке — ошибка,
которая увела бы в брак вообще всё.
_load_tsпереносится из STG, а не ставится заново при разборе: колонкаотвечает на «когда строка приехала в хранилище». Работа у версии
ReplacingMergeTreeодна — схлопнуть повтор доставки.Счёт в smoke идёт через
FINAL. Голыйcount()поReplacingMergeTreeзависит от числа прошедших мержей, и раздел 6 спеки его прямо запрещает.
К предикату добавлено требование не бросать исключений. Упавшая матвью
роняет вставку и останавливает потребление до починки — это единственное, что
сейчас держит правило репозитория про грязные записи.
Появился шаг «первым делом» — три
SELECT, на которых стоит порядокклассов брака и сам выбор формата чтеца. Документация поведение на не-объекте
не описывает, и до прогона это предположения.
Файлы теперь
20-ods-tables.sqlи30-ods-views.sql; нумерация связана с #37.Вторая правка за день, по итогам холодного ревью тикетов как наряда на работу.
Главное: откуда берутся события. #41 с сериализатором и приёмником идёт
после этого тикета, а smoke здесь стоит на реальных событиях. Исполнитель
выписал бы событие из 47 полей руками — шестая копия контракта там, где
ADR 0005 насчитал пять, и вдобавок общая опечатка прошла бы зелёной сразу через
фикстуру и через контрольный список ключей в матвью. Теперь тикет требует
выводить фикстуру из
generator/src/clickstream_generator/schema.py.Форма дат на проводе зафиксирована — ISO-8601, записана в спеку генератора,
раздел 4. Раньше её не было нигде: ни в описании выгрузки (там типы ClickHouse
и numpy), ни в спеке генератора. Этот тикет форму пинит как первый потребитель,
#41 соблюдает.
Счётная сверка обрамлена. «N сообщений — N строк» по всей таблице красна со
второго прогона по построению: в сырье уже лежат сообщения приёмки #37, сырьё
копит строку за прогон, а ODS схлопывает повторы. Теперь считать по диапазону
_load_tsили по офсетам прогона, и рецепт проверки идёт сmake clean.Сторожа сторожей получили дом. Критерии «падает при рукотворном
расхождении» оформляются мутационными сторожами в
tests/smoke-guards.sh, гдетакая практика уже заведена. Разовое «сломал руками, посмотрел, вернул» следа
не оставляет и при ревью непроверяемо.
Из критерия про опечатку убрано «и на массиве» — это ровно та же
единственная сверка набора ключей, отдельного пути кода нет. Настоящий риск
массивов закрывает отдельный сторож, и он уже отдельным критерием.
Добавлены критерии, наблюдающие форму таблиц. Раньше ни один не смотрел на
то, что тикет же и назначает: исполнитель мог написать
now64()вместопереноса
_load_tsиз сырья и пройти приёмку — contract-тест видит толькоsystem.columns.Третья правка, и повод у неё внешний: тикет отстал от репозитория на день.
Последняя редакция была 5 августа вечером, а шестого в main приехало четыре
вещи, которые его задевают, — шлагбаум учебной ценности в AGENTS.md (12:37),
срез проверок стенда #56 вместе с
tests/smoke-guards.shи цельюmake smoke-guards(13:06), деление целей наsmoke/check-clickhouse/check-services#58 (14:29) и срез разбора Bash изconfig-test#59 (16:59).Главное: критерии прогнаны через шлагбаум. Их было четырнадцать, и почти
все — проверки; это ровно тот механизм, который #56 назвал причиной
разрастания. Осталось одиннадцать пунктов, из них проверок в
make check-clickhouse— четыре вместо восьми: contract-тест, строгий приём,инвариант слоёв, сторож массивов. Список объявлен закрытым прямо в критериях.
Под нож пошли три критерия.
Мутационные сторожа — дома у них больше нет, и снесён он по принципу, а не
по недосмотру. Критерий просил воскресить практику, срезанную накануне, и
просил именно критерием приёмки. Вместо него — форма проверки, которая не
умеет позеленеть вхолостую (сверка обоюдная, пустой список — ошибка; строка
сначала найдена, потом прочитана), плюс разовый показ красного с записью
в тело PR.
_load_tsперенесён, а неnow64()— фраза «чему научится менти» нескладывается: единственный способ сломать это свойство — переписать матвью,
то есть проверка сторожит исполнителя. Тикет мотив и называл прямо: «иначе
now64()в матвью пройдёт приёмку незамеченным». Свойство осталось критерием,но как разовый опыт с записью в «Что проверено». Самый спорный из трёх срезов:
проверка дешёвая. Но подмена на
now64()версиюReplacingMergeTreeнеломает — повтор доставки схлопнется и так, — значит урок словесный, и несут
его комментарий в DDL и абзац в доке.
Ключ шардирования и TTL в
system.tables— проверка утверждает, чтонаписанное написано, читая тот же DDL через другое окно. Ценно тут другое, и
это лаба, а не проверка: сырьё и разобранное из него событие ложатся на разные
шарды, потому что ключи у слоёв разные. Раздел 6 спеки прямо относит
пошардовые наблюдения к лабам. Оба наблюдения записаны в «Границы», чтобы не
вернулись самотёком.
Слиты две пары: «событие доезжает до
ods.event_dist» поглощён инвариантомслоёв (сошёлся счёт — доезд доказан), а
not_an_objectиkeyset_mismatchстали одной проверкой с двумя сообщениями.
Закрыты три вопроса, висевшие в постановке.
Где живёт код фикстуры и contract-теста — в пакете генератора, рядом со
schema_doc. Контракт уже кормит трёх потребителей, стенд становитсячетвёртым, и docstring
schema.pyэтого потребителя прямо ждёт. Вgenerator/tests/нельзя:make testобязан работать без стенда. Граница«генератор не трогать» переформулирована — не трогать логику, сериализатор и
приёмник это #41; читающий контракт модуль частью генератора не является.
Что делать с проверочными событиями —
EventDateфикстуры2000-01-01, внемодельного календаря.
ods.eventнарезан поEventDateи хранит всё, апроверка гоняется повторно; дата вне календаря даёт отдельную партицию, строки
видно глазами, ни в один срез модельного дня они не попадают и снимаются одной
командой, если помешают. Уборки в проверке поэтому нет.
CounterID— тот же,что у мира: второе значение сломало бы учебный комментарий про вырождение
ключа.
Переименование скрипта — да, в этом PR, отдельным первым коммитом одним
git mv, до содержательных правок. Иначеgitв одном коммите перестанетвидеть переименование: файл сильно правится. Абзац-объяснение в
testing.mdуходит вместе с ним.Починены мёртвые ссылки.
make smoke-cluster→make check-clickhouse.wait_for_local_tableкак образец опроса — файла нет, готового цикла вclickhouse-smoke.shтоже (там толькоquery_with_timeout, обёрткаtimeout), про это сказано прямо. Приёмuv runизconfig-test.shнегодился: там
uv run --no-project, окружения проекта он не даёт иclickstream_generatorиз него не импортируется — правильный образецcd generator && uv run, как уmake docs.Три
SELECTстали четырьмя. Тикет расщепил второй пункт списка ADR 0005на два и потерял форму именованного кортежа в
JSONExtractсNullable-членами — а она стоит ещё и в разделе 11 мастер-спеки какоткрытая. Теперь список сходится с ADR.
Рецепт закрытия «упавшая матвью останавливает потребление» уточнён.
Прежний пример — создать матвью на несуществующую цель — может и не пройти:
случай в документации ClickHouse не описан, сверено через Context7 6 августа.
Рабочий путь назван: создать матвью нормально, удалить цель, посмотреть,
встало ли потребление, восстановить.
Правило «фикстура выводится из контракта» получило недостающую половину.
schema.pyнесёт имена и типы, но не значения — правила «тип ClickHouse →значение на проводе» нет нигде, и вывести его придётся этим тикетом. Раньше
шаг подавался как бесплатный.
Четвёртая правка. Тикет переставлен после #41 и заметно упростился;
заодно в него легли находки холодного ревью.
Перестановка. Прежний порядок заставлял этот тикет выдумывать событие —
своим модулем, печатающим JSON, вторым местом сериализации против правила
раздела 4 спеки. Теперь события даёт проигрыватель, и всё, что вокруг
фикстуры наросло, ушло: правило «тип ClickHouse → значение на проводе»,
дата фикстуры вне модельного календаря, форма на проводе, конфликт с
единственным сериализатором. Модуль в пакете генератора остался, но
похудел до одной работы — печатать контракт машинно; событий он не собирает.
Порченые сообщения теперь делаются мутацией настоящего события, а не
собираются с нуля. Это и честнее: так ломается настоящая выгрузка.
Из #41 приехали три утверждения — день доезжает до
ods.eventчерез обеноды,
*_errorsпуст на честном прогоне, повтор не меняет счёт последедупа. Они про хранилище, а не про сериализатор. Оформлены разовой
приёмкой в теле PR, а не постоянной проверкой: день в полсотни тысяч
событий в повторяемую цель не ставится.
Холодное ревью, линия дефектов. Снят хвост «именно
clean» — онпротиворечил обрамлению пачки, которое сам же и отменяет. Обрамление ODS
офсетами убрано: метаданные доставки живут у
stg.*_raw,ods.eventих ненесёт (раздел 7 спеки), а положи их туда — покраснеет договор со схемой.
Рецепт опыта с упавшей матвью теперь называет объект: удалять надо
распределённую
ods.event_dist, а не локальную; на локальной вставка вDistributedвернёт управление сразу, и утверждение показалось быопровергнутым — а на нём стоит правило «грязные записи не валят пайплайн».
Ссылка на образец цикла опроса исправлена: циклы есть в
scripts/stand-services.sh, я зря написал, что их нет. Названы_load_tsв таблице ошибок, перенумерация проверок в скрипте и то, что
Makefileедет тем же коммитом, что и переименование скрипта.
Холодное ревью, линия уместности. Сторож массивов заменён на сверку
разобранного события по всем 47 колонкам. Довод ревьюера сильнее моего:
тихо пустой массив ничем не отличается от тихого нуля в остальных сорока
двух колонках — опечатка внутри
JSONExtractдаёт умолчание типа везде.Ловится это тем, что источники у двух сторон разные: выражения сверки
собираются из контракта, выражения матвью пишутся руками по описанию
выгрузки.
Побочно вскрылась неточность в ADR 0005: там сказано, что опечатка в имени
скалярного поля уводит строки в брак и видна сразу. Верно только для пяти
опорных колонок с
Nullable-разбором. Поправить тем же PR — записанов «сначала прочитать».
Добавлен третий класс брака в проверку строгого приёма:
key_field_unparsedбыл объявлен и нигде не показан, а он самый жизненный из трёх. Сказано, что
отправка одна на все проверки — иначе трижды платим ожиданием сброса буфера
Kafka. Добавлен четвёртый учебный комментарий в DDL, про
_load_tsкакколонку версии: постоянную проверку на это мы срезали, значит урок целиком
на комментарии. И сказано, что делать, если четыре
SELECTответят не так,как ждёт постановка, — это вопрос владельцу, а не починка на месте.
Про цену
make check-clickhouse: замер в карте целей перестанет быть верным,и его надо перезамерить. Порога по времени в репозитории нет и заводить его
не следует — цена там замеренное число с датой, а не назначенный предел.
Типизированный ODS: ods.event, строгий приём и contract-тестto Типизированный ODS: ods.event, строгий приём и таблица ошибокContract-тест снят. Решение владельца после разбора: сверка
system.columnsсо списком из контракта ловила сверх строгого приёма ровно одно — смену типа
колонки. А смену типа ловит и сверка разобранного события, причём лучше: не на
объявлениях, а на живых данных. Сверх соседки сверка объявлений не давала
ничего.
Всё, что действительно ломается — поле пропало, появилось, переименовано, —
ловит строгий приём, и ловит громко: события уходят в
*_errorsвсе доединого. Это часть продукта, а не проверка.
Понятие data contract на стенде остаётся видимым дважды: контракт есть
артефактом (
schema.pyи собранное из него описание выгрузки) и его соблюдениевидно работающим (
*_errorsс классом брака). Убрана церемония вокругэкспоната, а не экспонат.
Довод «в бою так пишут» проверки не спас: в бою источник и хранилище живут в
разных репозиториях с разными релизами, оттуда такие сверки и растут. Здесь оба
списка правятся одним коммитом. И форма в бою другая — реестр схем у Kafka или
тесты на данные, а не рукописное сличение с питоновским модулем.
Проверок в
make check-clickhouseосталось три: строгий приём, инвариант слоёв,сверка разобранного события. Модуль в пакете генератора остался — он нужен
третьей проверке, чтобы собрать выражения сличения из контракта, а не списать
их с матвью.
Название тикета поправлено. Правка документов — мандатом в критерии: мастер-спека
1.4 и спека генератора 3 называют contract-тест одним из двух механизмов границы,
ADR 0005 упоминает дважды, карта проверок зовёт «договором со схемой событий».
Заодно снялся вопрос о термине: сверять контракт с объявлениями больше нечего.
Ранее в тот же заход по холодному ревью: снято «через обе ноды» из критерия про
настоящий день (в ODS нет метаданных доставки); записана ловушка PREWHERE до
FINALпри обрамлении по_load_ts; сказано, что матвью задним числом недосыпают и прогон после создания ODS нужен новый; восстановлена зависимость
#43 → #42, которую разворот порядка порвал.
Три проверки становятся разовыми опытами. Решение владельца 7 августа
2026 года, по итогам холодного ревью связки #41/#42/#43.
Довод. Все три не спрашивают стенд, а кормят его: строгий приём вкладывает
в топик три порченых сообщения, инвариант слоёв — целую пачку, а сверка
разобранного события требует, чтобы у одного события разом нашлись строка в
сырье и строка в ODS. Последняя вдобавок хрупка по устройству: сырьё живёт трое
суток, и на стенде, поднятом неделю назад, она покраснела бы не от поломки.
Главное — вложенное остаётся. У ODS срока хранения нет, а
make check-clickhouseгоняют не задумываясь, семь секунд. После каждого прогона в мире менти
оседали бы события, которых мир не рождал, — и неотличимые от настоящих:
та же схема, тот же генератор. Менти считает конверсию и молча считает вместе
с ними. Интеграционная проверка — не часть регулярной жизни стенда, максимум
тема урока.
Что стережёт цепочку вместо них. Счётчики #42 против мини-манифеста: они
считают настоящий мир, ничего не вкладывают и ловят ту же поломку, ради которой
стоял инвариант слоёв, — сломалась матвью разбора, события поехали в брак, счёт
разошёлся. Причём на четырёхстах тысячах строк, а не на пачке из десятка.
Теряется честно одно: тихая подмена значения в одной колонке. Понадобится
сторож и на неё — дом у неё в паспорте манифеста, парой агрегатов по
детерминированному миру, а не в проверке, вкладывающей данные.
Уборка обязательна. Управляемая пачка играет день за границей оси мира:
её строки ложатся в собственную партицию
EventDate, и партиция сносится поокончании опыта. Партиция как единица уборки — тот же механизм, на котором
стоит переобработка дня X. Порченые сообщения остаются в
*_errors: тамнарезка по дню загрузки, снос задел бы настоящие ошибки того же дня, а брак —
журнал, а не данные мира.
Попутно отпали два пункта: сквозная перенумерация проверок в скрипте
(их остаётся восемь) и расширение
ensure_stand_runningна Kafka — брокер былнужен только этим проверкам. Переименование
clickhouse-smoke.shвcheck-clickhouse.shостаётся: это гигиена имени, а не проверка.Из холодного ревью сюда же легли две правки документов. Список мест, где
спека упоминает снимаемый contract-тест, был неполон и неточен: упоминаний
шесть, ADR 0005 упоминает один раз, а не два, и не были названы три — включая
строку 37 спеки генератора («Целевая картина одним взглядом») и строку 286, где
обещано, что «каждый
make upбесплатно прогоняет весь конвейер иcontract-тест на настоящих данных». Теперь велено собирать грепом, а не по
памяти. Второе: README сейчас говорит, что
uvнужен только проверкам безстенда, а этот тикет заводит модуль генератора, который зовут при опытах на
стенде.
Само правило вынесено в
docs/architecture/testing.md— чтобы решалось покарте, а не заново в каждом тикете.
Проход на вычитание, 7 августа 2026 года. Тикет переписывался пять раз, и
каждая правка прибавляла; этот проход имел право только резать. Из тела ушло
восемь пунктов, ни одного утверждения о продукте среди них нет.
Модуль в пакете генератора, печатающий контракт машинно, — срезан. Он
попал сюда 6 августа с мотивом «нужен третьей проверке, чтобы собрать выражения
сличения из контракта». 7 августа третья проверка стала разовым опытом, а
модуль остался по инерции — постоянный код в репозитории ради работы, которая
проходит один раз. Свойство «источники у двух сторон разные» живёт в самом
действии, а не в закоммиченном файле: выражения собираются при исполнении
разовым
cd generator && uv run python -c ...поschema.COLUMNS. Учить ментимодулю нечему —
docs/formats/clickstream-event.mdуже печатает все 47 колонокс типами ClickHouse в порядке контракта, и собирает его
schema_doc.py.Следом ушли три пункта, стоявшие только на нём: критерий про README и
uv(без модуля утверждение README остаётся верным), строка
make lint/typecheck/testв разделе «Проверка» (тикет больше не трогает Python) и половина границыпро генератор.
Преамбула критериев ужата до двух абзацев. Довод «почему разовые опыты», что
стережёт вместо них и требование прибираться партицией 7 августа переехали в
docs/architecture/testing.md— ровно затем, чтобы решаться по карте, а незаново в каждом тикете. В тикете осталась ссылка и та единственная фраза,
которой в карте нет: что честно теряется тихая подмена значения в одной колонке.
Обоснование отдельного коммита под переименование скрипта — срезано. Оно
опиралось на факт, которого больше нет: «этот же PR файл сильно правит, и в
одном коммите
gitперестанет видеть переименование». После 7 августа PR неправит
clickhouse-smoke.shвообще. Само переименование осталось.Пункт «нумерация проверок не меняется,
ensure_stand_runningнерасширяется» — срезан. Это то же самое, что сказано в комментарии от
7 августа, и запрет на то, чего исполнитель нынешнего тикета не начнёт: в этот
скрипт он не заходит.
Мелкие: абзац про ключи в консольном продюсере (объяснён и тут же снят своей же
фразой «для этого тикета ключи не нужны»), оговорка про матвью, созданную на
несуществующую цель (рецепт опыта уже выбран), замер про пустое сообщение
до одной строки (полностью он лежит в доке хранилища), раздел «Проверка» до
одной команды (четыре пункта из шести дословно повторяли критерии приёмки).
Contract-тест: охват критерия расширен с документов на код. Мест в
документах действительно шесть, как и было сказано. Но в
generator/src/clickstream_generator/schema.pyживут ещё три, и греп подокументам их не покажет: обещание «границу будет сторожить contract-тест» в
докстринге модуля, «от этой записи зависит будущий contract-тест» в пункте про
clickhouse_typeи сам довод этого пункта — требование писать тип «ровно в тойзаписи, в какой его вернёт
system.columns» держалось на снимаемой сверке.Правило остаётся, довод у него теперь другой: по этой записи человек пишет DDL.
Три находки не пережили перепроверки и в тело не пошли.
Слияние опытов «строгий приём» и «инвариант слоёв» в один: отправка у них
действительно общая, но утверждения разные, и красное у них показывается
разными поломками. Слияние сэкономило бы абзац прозы ценой размытия двух
утверждений в одно.
Рез строки про
PYTHONDONTWRITEBYTECODE: довод был «после реза модуля тикет нетрогает Python». Самый короткий способ показать красным сверку разобранного
события — поменять имя в
schema.py, чтобы выражение сличения читало не тотключ. Это Python, и ловушка с устаревшим
.pycтам ровно та.Рез перечня мест с contract-тестом как «устаревшего»: перечень точен, устарел
был мой счёт.
Что не трогалось вовсе: обе таблицы ODS и обе матвью с их условиями,
строгий приём и три класса брака, четыре разведочных
SELECT, сверкаразобранного события, опыт с упавшей матвью, четыре учебных комментария в DDL,
критерий про настоящий день.
Тело было 331 строку, стало 293.
Влито PR #62. Одно отклонение от буквы, чтобы оно не потерялось: критерий про карту целей велел править в
docs/architecture/testing.md«только строку про переименованный скрипт», а правок вышло две — вместе с ней ушёл абзац «Ось: кого спрашивают», где договор со схемой событий стоял примером выбора дома для проверки. Без contract-теста пример перестал существовать. Постоянными проверками карта не пополнялась, как и велено.