Происхождение данных на уровне колонок¶
Provisa отслеживает происхождение данных на уровне столбцов статически. Он вычисляет его по определениям SQL и контрактам команд, так что ничего не нужно запускать. Доступны два представления: DAG для каждого оператора и граф происхождения по всей федерации, охватывающий каждое зарегистрированное представление и материализованное представление (MV).
Обозреватель происхождения¶
Перейдите на страницу Lineage в UI (/lineage). Вставьте SQL-выражение и нажмите Build
statement graph, чтобы увидеть его DAG на уровне колонок. Нажмите Federation graph, чтобы
загрузить граф происхождения по всем MV в реестре.
DAG уровня выражения¶
Каждая именованная выходная колонка в вашем SQL становится узлом. Построитель прослеживает её назад через все CTE, подзапросы, join и встроенные вызовы команд до исходных колонок, строя направленный граф от исходных входов к итоговым выходам.
Разобранный пример¶
SELECT o.id, e.embedding, upper(e.geo) AS geo_u
FROM orders o
JOIN enrich_grpc_set('main.public.orders') e ON o.id = e.id
Это выражение производит три выходные колонки. Граф для geo_u выглядит так:
orders.geo ──[enrich_grpc_set(...)]──► e.geo ──[UPPER]──► geo_u
orders.id ─╮ (taint closure)
orders.region ─╯
orders.id,orders.regionиorders.geo— узлы source (узкий входной контрактenrich_grpc_setобъявляетidиregion; полное замыкание taint-closure соединяет все объявленные входы со всеми выходами).e.embeddingиe.geoявляются узлами command на границеenrich_grpc_set.geo_u— узел derived, произведённый SQL-функциейUPPER.
Граница команды не непрозрачна. Поскольку enrich_grpc_set объявляет свои входные колонки
(id, region) и выходные колонки (id, embedding, geo), движок происхождения непрерывно
сращивает taint-замыкание от объявленных колонок исходного отношения к каждому выходу.
Виды узлов и визуальные подсказки¶
| Вид узла | Цвет | Значение |
|---|---|---|
source |
Зелёный | Колонка базовой таблицы |
derived |
Синий | Произведена SQL-выражением (функция, оператор, CTE) |
command |
Фиолетовый | Выходная колонка зарегистрированной команды |
Дополнительные кольца на узле:
- Оранжевое кольцо — итоговая выходная колонка выражения.
- Двойная рамка — отношение колонки является материализованным представлением (снимок MV/CTAS).
- Красное кольцо — участник цикла, классифицированного как ошибка.
- Жёлтое кольцо — участник цикла, классифицированного как контур обратной связи.
Именованные преобразования на рёбрах¶
Каждое ребро несёт исходное SQL-выражение, производящее целевую колонку, плюс список именованных
операций: SQL-функции (sql_function), арифметические и логические операторы (operator),
зарегистрированные команды (command), голые ссылки на колонки (identity) и литералы
(constant).
Ребро от вызова команды отрисовывается в UI пунктирной фиолетовой линией.
Граф на уровне всей федерации¶
Граф федерации объединяет происхождение каждой зарегистрированной MV по операторам в один граф происхождения. Идентичность узла задаётся как relation.column, поэтому выходной столбец одного представления и входная ссылка другого представления на этот столбец сливаются в один узел. В результате получается единый DAG от столбцов базовых источников до каждого производного набора данных платформы.
Используйте focus, direction и depth, чтобы сузить представление в масштабе федерации без
пересчёта графа.
Циклы¶
Движок происхождения обнаруживает каждый направленный цикл и классифицирует его. Цикл описывается и никогда не отклоняется.
| Классификация | Цвет рамки | Значение |
|---|---|---|
feedback |
Жёлтый | Цикл проходит через материализованный узел — легальный контур обратной связи с задержкой во времени. Снимок MV — это граница версий, которая делает его определённым. |
error |
Красный | На петле нет границы материализации — циклическое определение без устойчивого порядка вычисления. Вероятно, ошибка проектирования. |
Цикл feedback является допустимым шаблоном. MV обогащения, возвращающая производный столбец в собственное исходное отношение, работает, пока один узел петли материализован, потому что снимок разделяет две половины во времени. Цикл error требует решения оператора. Обычно он означает, что два представления ссылаются друг на друга без снимка между ними.
API¶
Обе конечные точки статические. Они читают определения и контракты и никогда не читают данные.
POST /admin/lineage/graph¶
Возвращает DAG на уровне колонок для одного SQL-выражения.
POST /admin/lineage/graph
Content-Type: application/json
{
"sql": "SELECT o.id, e.embedding FROM orders o JOIN enrich_grpc_set('main.public.orders') e ON o.id = e.id",
"dialect": "postgres"
}
Форма ответа:
{
"nodes": [
{"id": "orders.id", "column": "id", "relation": "orders", "kind": "source", "materialized": false}
],
"edges": [
{
"source": "orders.id",
"target": "e.id",
"transform": "enrich_grpc_set(...)",
"ops": [{"name": "enrich_grpc_set", "kind": "command"}]
}
],
"outputs": ["id", "embedding"]
}
Возвращает HTTP 422, когда SQL не удаётся разобрать.
GET /admin/lineage/federation¶
Возвращает объединённый граф происхождения по всем MV в реестре.
GET /admin/lineage/federation
GET /admin/lineage/federation?focus=orders.id&direction=downstream&depth=3
Параметры запроса:
| Параметр | Значения | По умолчанию | Действие |
|---|---|---|---|
focus |
Идентификатор узла | — | Сузить ответ до подграфа вокруг этого узла |
direction |
upstream | downstream | both |
both |
В каком направлении обходить от focus |
depth |
целое число | без ограничения | Максимальное расстояние в переходах от focus |
Форма ответа та же, что и у графа выражения, с добавленным полем cycles
:
{
"nodes": [...],
"edges": [...],
"outputs": [...],
"cycles": [
{
"nodes": ["orders.region", "enriched_orders.region"],
"has_materialization_boundary": true,
"classification": "feedback"
}
]
}
Из чего построено каждое обновление¶
Каждое обновление материализованного представления записывает, из чего оно построено: версию определения представления, версию его входных данных и то, как эта версия установлена, а также исходные столбцы и выражение каждого выходного столбца. Операционное представление "ops"."mv_refresh_lineage" возвращает одну строку на обновление:
| Столбец | Содержит |
|---|---|
mv_id |
Обновлённое материализованное представление |
target_table |
Таблица, в которую записало обновление |
definition_version |
Версия определения представления |
input_version, input_version_kind |
Версия его входных данных: снимок Iceberg, водяной знак, токен свежести или время обновления |
trace_id |
Трассировка обновления в "ops"."traces" |
refreshed_at |
Когда оно выполнялось |
column_derivations |
Каждый выходной столбец с его исходными столбцами и выражением, в виде JSON |
SELECT refreshed_at, definition_version, input_version, input_version_kind
FROM "ops"."mv_refresh_lineage"
WHERE mv_id = 'view-view_exposure'
ORDER BY refreshed_at
Представление читает записи обновлений в "ops"."traces" и отвечает на каждом движке, который их хранит. В Trino это таблицы Iceberg каталога otel. В DuckDB сервер записывает собственные записи трассировки в виде файлов Parquet в своей папке телеметрии (PROVISA_TELEMETRY_DIR, по умолчанию ~/.provisa/telemetry), по файлу на пакет экспорта в папке на каждый день. Плановое уплотнение телеметрии (otel.compact_cron) объединяет мелкие файлы каждого дня в файлы до 64 МБ. Каждая организация читает только свои обновления, а записи хранятся бессрочно. Span'ы записываются только пока включён SDK OpenTelemetry (OTEL_SDK_DISABLED не задан или false).
Представление каким оно было и каким было записано¶
Представление, объявленное битемпоральным, хранит каждую версию, которая у него была: каждое обновление дописывает, и ни одна версия не перезаписывается, в любом хранилище. Обычное чтение возвращает текущее состояние. Два заголовка запроса читают его прошлое:
X-Provisa-As-Ofчитает представление на момент времени или за интервал по его часам: по собственному времени изменения в источнике, если представление объявляетbitemporal_source_time, иначе по времени загрузки в Provisa.X-Provisa-As-Recordedчитает его таким, каким Provisa записал его на момент времени. Версии, записанные позже, не учитываются, поэтому исправление, записанное позднее, не меняет воспроизведение прошлого.
На 30 сентября по записям на 1 октября даёт исходное значение; по записям на 5 октября даёт исправление. Запись аудита каждого запроса хранит оба времени (as_of, as_recorded), так что отчёт можно воспроизвести таким, каким он был получен впервые. Представления с историей описывает объявление битемпорального представления и каждый интерфейс, принимающий эти два значения.
Что сломало бы переименование или удаление колонки¶
У колонки два имени, и каждое хранится своим набором артефактов.
Отображаемое имя — то, что показывают поверхности SQL и GraphQL: table_columns.alias, а при
отсутствии псевдонима — значение по умолчанию в snake_case. Представления, материализованные представления, выражения метрик,
предикаты RLS, контракты DQ, гранулярности представлений-метрик и ключи строк MV — всё это написано
против этого имени, поэтому переименование псевдонима ломает их так же надёжно, как и удаление
колонки.
Физическое имя — это table_columns.column_name, идентичность, переживающая сплошную замену
колонок при upsert таблицы. Связи, привязки глоссария, назначения тегов, колонка
водяного знака и пресеты колонок хранят именно его, поэтому они ломаются только тогда, когда колонка
удаляется.
columnDependents сообщает об обоих. Нижестоящие представления и MV берутся из среза графа
федерации по отображаемому имени колонки; артефакты, которые граф не покрывает, берутся из прямого
сканирования реестра.
query {
columnDependents(tableId: "42", renamed: ["order_total"], removed: ["legacy_code"]) {
columnName
dependents { kind name detail breaksOn }
}
}
breaksOn равно rename для ссылки на отображаемое имя и remove для ссылки на физическое, так
что вызывающая сторона может понять, на какую половину правки реагирует каждый артефакт.
Спрашивайте об этом до сохранения. Переименованная колонка находится по отображаемому имени, которое она ещё несёт в реестре; как только псевдоним записан, старое имя исчезло и запрос ничего не находит.
Страница таблиц запускает запрос автоматически, когда ожидающая правка меняет псевдоним или сокращает набор столбцов, и перечисляет найденное. Предупреждение носит рекомендательный характер. Оно называет затронутые артефакты, а решение принимает администратор. Оно никогда не блокирует сохранение, потому что до всех потребителей не дотянуться: внешний дашборд или клиентское приложение, обращающееся к столбцу по имени, реестру неизвестны. По той же причине поиск по свободному тексту SQL сопоставляет столбец как токен идентификатора, не разрешая область видимости, и может назвать артефакт, который на деле столбец не использует. Для предупреждения избыточность является безопасным направлением.
Использование происхождения для управления контрактами команд¶
Поскольку taint-замыкание соединяет каждую объявленную входную колонку с каждой объявленной выходной, широта этого замыкания целиком зависит от того, что вы объявите.
Возьмём команду, которая принимает полную таблицу заказов (id, region, amount, customer_id, discount, notes, ...) и возвращает embedding. Если входной контракт перечисляет все эти столбцы, каждый последующий столбец, использующий эмбеддинг, показывает происхождение от всех них. Это точно, но бесполезно: не понять, что действительно имело значение.
Объявите только id и text (колонки, которые модель embedding действительно читает), и конус
происхождения сузится до этих двух исходных колонок. Вывод при этом остаётся и корректным, и точным.
О механике объявления узкого входного контракта см. Команды.