Форма записи слепка на проводе #81
Notifications
Due Date
No due date set.
Blocks
#80 Брак в слепке: куда уходит и по каким классам
ddmitry/clickstream-data-platform
#84 Вычитание: какая механика этапа 3 не окупает себя эффектом
ddmitry/clickstream-data-platform
Reference: ddmitry/clickstream-data-platform#81
Reference in New Issue
Block a user
Part of #69.
Вопрос
Чем деньги, вложенный JSON позиций и даты выглядят в сообщении топика
orders?Мастер-спека (раздел 2) назвала запись слепка со стороны хранилища: деньги —
Decimal(18,2),items—Stringс вложенным JSON,created_at/updated_at—DateTime,snapshot_date—Date. Чем та же запись выглядит на проводе,не решено ни одним документом.
Цена ошибки известна заранее. Положит генератор в JSON число
1299.9— хранилищеполучит Decimal через Float, то есть ровно через то, от чего предостерегает урок
класса C (Float64 против Decimal, мастер-спека, раздел 4). С датами репозиторий
уже наступал на соседнюю граблю:
JSONExtractвозвращал NULL на строке ссуффиксом
Z, и вылезло это на исполнении #43, а не на бумаге (спека генератора,раздел 4).
Решается вместе: форма денег, форма дат, экранирование вложенного JSON позиций и
то, каким сериализатором слепок собирается — канонический сериализатор события у
генератора уже есть, и второго заводить не хочется.
Блокирует #80: он эту запись разбирает и делит на годное и брак.
Решение
Учебный результат: менти видит на одной записи границы между бизнес-временем,
аудитом источника, датой слепка и временем загрузки, не разбирая ради этого
двойной JSON или универсальный сериализатор.
Запись на проводе
items[].price, — строки с ровно двумя знаками послеточки. Это сохраняет десятичную запись источника и не проводит сумму через
JSON-число/Float. Разница с кликстримом намеренна: там значения
purchaseRevenueприезжают JSON-числами и разбираются какArray(Float64),а бэкенд передаёт деньги строками для точного
Decimal.items— обычный массив JSON, не строка с JSON внутри. В ODS колонка остаётсяString: пакетный приём извлекает массив как сырой фрагмент черезJSONExtractRaw(raw, 'items'), а ODS → DDS разбирает его один раз.sku,qty,price. Это даёт воспроизводимые байты без сортировки ключей.Время
created_atиupdated_at— время аудита строки по часам базы источника:создание и последнее изменение. Это не время покупки и не время загрузки в
ClickHouse. Поля приезжают в сообщении бэкенда, которое в стенде формирует
заказная часть генератора; ClickHouse добавляет рядом собственную
_load_ts.Оба поля имеют настоящую миллисекундную точность и одну узкую форму
RFC 3339 в UTC:
В ClickHouse им соответствует
DateTime64(3, 'UTC');updated_atостаётсяверсией для
ReplacingMergeTree. Сериализатор всегда выводит три цифры долейсекунды, включая
.000для честного значения на границе секунды. Секунднуюмодель нельзя выдавать за миллисекундную простым дополнением нулей. Последующие
версии одного заказа имеют монотонный
updated_at.Бизнес-время покупки остаётся в
purchase.UTCEventTimeи связывается с заказомпо
purchaseID = order_id. Новогоordered_atне добавляем: отдельногопотребителя у него нет.
toDate(created_at)в описании DDS следует называтьднём создания строки источника, а не днём бизнес-события.
snapshot_date— дата завершившегося модельного дня, состояние которого снятона исходящей границе суток. Форма —
YYYY-MM-DD. Она вычисляется один раз на выгрузку и повторяется во всех еёзаписях; на следующем модельном дне меняется. Это не глобальная константа и не
UTC-дата отправки. Константами мира остаются D0 (
ORIGIN), пояс модельногокалендаря и окно K = 7. На старте оси прогон дня 0 ничего не отправляет за
несуществующий день −1; первый слепок дня 0 уезжает прогоном дня 1.
Для следующего тикета: точный nullable-разбор
parseDateTime64InJodaSyntaxOrNullс шаблоном на три миллисекундных знакапроверен на проектном ClickHouse 26.3.17.56.
toDecimal64OrNull(..., 2)сам посебе не проверяет правило «ровно два знака». Считать ли формы
1299.9и1299.900браком при приёме, решает #80; эта резолюция отдельного лексическоговалидатора не требует.
Сериализация
Оставляем одну границу рождения байтов — существующий модуль
serialize.py.Для слепка нужна явная функция рядом с
events(...): она собирает один словарьс вложенным списком
itemsи один раз вызываетorjson.dumpsна запись.Классы кодеков, реестр схем, общий универсальный сериализатор и отдельный
dumpsдляitemsне добавляем.Отклонено
через Float.
items— требует двойной сериализации и второго разбора.SNAPSHOT_DATE— смешивает настройку мира со значением пачки.ordered_atи универсальный слой кодеков — сложность без текущегоучебного потребителя.
Исследование и ссылки на первичные источники:
docs/research/2026-08-16-order-snapshot-wire-format.md.