Перейти к содержанию

Происхождение данных на уровне колонок

Provisa отслеживает происхождение данных на уровне колонок статически — оно вычисляется на основе SQL-определений и контрактов команд, без выполнения запроса. Доступны два представления: DAG для отдельного выражения и граф происхождения (provenance graph) на уровне всей федерации, охватывающий все зарегистрированные представления и материализованные представления (MV).

Обозреватель происхождения

Перейдите на страницу Lineage в UI (/lineage). Вставьте SQL-выражение и нажмите Build statement graph, чтобы увидеть его DAG на уровне колонок. Нажмите Federation graph, чтобы загрузить граф происхождения по всем MV в реестре. [tool-verified: LineagePage.tsx:28-119]

DAG уровня выражения (REQ-1160)

Каждая именованная выходная колонка в вашем 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 соединяет все объявленные входы со всеми выходами). [tool-verified: _splice_commands in graph.py:223-242]
  • e.embedding и e.geo — узлы command — граница enrich_grpc_set.
  • geo_u — узел derived, произведённый SQL-функцией UPPER.

Граница команды не непрозрачна. Поскольку enrich_grpc_set объявляет свои входные колонки (id, region) и выходные колонки (id, embedding, geo), движок происхождения непрерывно сшивает (splice) taint-closure от объявленных колонок исходного отношения к каждому выходу. [tool-verified: _splice_commands and _input_relation in graph.py:245-271]

Типы узлов и визуальные обозначения

[tool-verified: LineageDag.tsx:25-29, KIND_COLOR constants; LineagePage.tsx:21-26 LEGEND]

Тип узла Цвет Значение
source Зелёный Колонка базовой таблицы
derived Синий Произведена SQL-выражением (функция, оператор, CTE)
command Фиолетовый Выходная колонка зарегистрированной команды

Дополнительные кольца на узле:

  • Оранжевое кольцо — итоговая выходная колонка выражения.
  • Двойная рамка — отношение колонки является материализованным представлением (снимок MV/CTAS).
  • Красное кольцо — участник цикла, классифицированного как ошибка.
  • Жёлтое кольцо — участник цикла, классифицированного как цикл обратной связи (feedback loop).

[tool-verified: LineageDag.tsx:88-103 Cytoscape style selectors]

Именованные преобразования на рёбрах

Каждое ребро несёт исходное SQL-выражение, производящее целевую колонку, а также список именованных операций: SQL-функции (sql_function), арифметические/логические операторы (operator), зарегистрированные команды (command), простые ссылки на колонку (identity) и литералы (constant). [tool-verified: TransformOp and name_transform in graph.py:36-145]

Ребро от вызова команды отображается в UI как фиолетовая пунктирная линия. [tool-verified: LineageDag.tsx:122-124]

Граф на уровне всей федерации (REQ-1161)

Граф федерации объединяет происхождение каждого зарегистрированного MV на уровне выражения в один граф происхождения. Идентичность узла — relation.column: выходная колонка представления и ссылка на ту же колонку из другого представления схлопываются в один узел. Результат — единый DAG от колонок базовых источников до каждого производного набора данных на платформе. [tool-verified: build_federation_graph in merge.py:205-229 and qualify_outputs in graph.py:275-299]

Используйте focus, direction и depth, чтобы ограничить область просмотра на уровне федерации без пересчёта графа. [tool-verified: slice_graph in merge.py:160-189]

Циклы (REQ-1161)

Циклы описываются, а не отклоняются. Движок происхождения обнаруживает каждый направленный цикл и классифицирует его. [tool-verified: Cycle.classification property in merge.py:43-46]

Классификация Цвет рамки Значение
feedback Жёлтый Цикл проходит через материализованный узел — легитимный, отложенный во времени цикл обратной связи. Снимок MV — это граница версии, которая делает его корректно определённым.
error Красный На цикле нет границы материализации — циклическое определение без стабильного порядка вычисления. Вероятно, ошибка проектирования.

[tool-verified: LineagePage.tsx:83-98 cycle alert rendering; merge.py:38-48]

Цикл 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"
}

[tool-verified: lineage_graph endpoint at lineage_router.py:45-54, LineageGraphRequest model at lineage_router.py:29-31]

Форма ответа [tool-verified: LineageGraph.to_dict in graph.py:82-105]:

{
  "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 не удаётся разобрать. [tool-verified: lineage_router.py:51-54]

GET /admin/lineage/federation

Возвращает объединённый граф происхождения по всем MV в реестре.

GET /admin/lineage/federation
GET /admin/lineage/federation?focus=orders.id&direction=downstream&depth=3

[tool-verified: federation_graph endpoint at lineage_router.py:73-98]

Параметры запроса [tool-verified: function signature at lineage_router.py:73-76]:

Параметр Значения По умолчанию Эффект
focus id узла Ограничить ответ подграфом вокруг этого узла
direction upstream | downstream | both both В каком направлении обходить граф от focus
depth целое число без ограничений Максимальное расстояние в переходах от focus

Ответ имеет ту же форму, что и граф выражения, с добавленным полем cycles [tool-verified: MergedGraph.to_dict in merge.py:60-64]:

{
  "nodes": [...],
  "edges": [...],
  "outputs": [...],
  "cycles": [
    {
      "nodes": ["orders.region", "enriched_orders.region"],
      "has_materialization_boundary": true,
      "classification": "feedback"
    }
  ]
}

Что сломает переименование или удаление колонки (REQ-1484)

У колонки есть два имени, и каждое хранится в своём наборе артефактов.

Внешнее имя (exposed name) — то, что показывают поверхности SQL и GraphQL: table_columns.alias, с откатом на значение по умолчанию в snake_case, если алиас не задан [tool-verified: computed_sql_alias at schema_helpers.py:317]. Представления, материализованные представления, выражения метрик, предикаты RLS, контракты DQ, гранулярности metric-view и ключи строк MV — всё это написано относительно этого имени, поэтому переименование алиаса ломает их так же надёжно, как и удаление колонки.

Физическое имя (physical name) — это table_columns.column_name, идентичность, которая переживает полную замену набора колонок при upsert таблицы. Связи (relationships), привязки глоссария, назначения тегов, колонка водяного знака (watermark) и пресеты колонок хранят именно это имя, поэтому они ломаются только при удалении колонки.

columnDependents сообщает об обоих случаях. Зависимые представления и MV ниже по потоку получаются срезом графа федерации по внешнему имени колонки; артефакты, которые этот граф не охватывает, получаются прямым сканированием реестра [tool-verified: graph_dependents in provisa/lineage/dependents.py, registry scans in provisa/api/admin/column_dependents.py].

query {
  columnDependents(tableId: "42", renamed: ["order_total"], removed: ["legacy_code"]) {
    columnName
    dependents { kind name detail breaksOn }
  }
}

breaksOn равно rename для ссылки по внешнему имени и remove для ссылки по физическому имени, так что вызывающая сторона может понять, на какую половину правки реагирует каждый артефакт.

Задавайте этот вопрос до сохранения. Переименованная колонка находится по внешнему имени, которое она всё ещё несёт в реестре; как только алиас применён, старое имя исчезает, и запрос ничего не находит.

Страница Tables автоматически выполняет этот запрос, когда незафиксированная правка меняет алиас или сужает набор колонок, и выводит список найденного [tool-verified: diffEditedColumns in provisa-ui/src/pages/tables/columnDiff.ts, dialog in TablesPage.tsx]. Предупреждение носит рекомендательный характер: оно называет затронутые артефакты, а решение принимает администратор. Оно не блокирует сохранение, потому что не все потребители массива данных достижимы — внешняя панель мониторинга или клиентское приложение, которое запрашивает колонку по имени, находится за пределами осведомлённости реестра. По той же причине сканирование произвольного SQL-текста сопоставляет колонку как токен идентификатора, а не разрешает область видимости, что может назвать артефакт, который на деле колонку не использует. Избыточное сообщение — безопасное направление для предупреждения.

Использование lineage для управления контрактами команд

Поскольку taint-closure соединяет каждую объявленную входную колонку с каждой объявленной выходной колонкой, широта этого замыкания целиком зависит от того, что вы объявите.

Рассмотрим команду, которая принимает полную таблицу orders (id, region, amount, customer_id, discount, notes, ...) и возвращает embedding. Если входной контракт перечисляет все эти колонки, каждая нижестоящая колонка, использующая embedding, будет показывать происхождение от них всех. Это точно, но бесполезно — трудно понять, что на самом деле имело значение.

Объявите только id и text (колонки, которые модель embedding действительно читает), и конус происхождения сужается до этих двух исходных колонок. Вывод остаётся корректным и становится точным.

Механику объявления узкого входного контракта см. в разделе Commands.