Column-Level Lineage¶
Provisa עוקב אחר lineage של נתונים ברמת העמודה באופן סטטי. הוא מחשב אותו מהגדרות SQL ומחוזי פקודות, כך ששום דבר לא צריך לרוץ. יש לכם שתי תצוגות: DAG לכל משפט, וגרף מקור ברמת הפדרציה כולה שמשתרע על כל view ו-view ממומש (MV) רשומים.
סייר ה-Data Lineage¶
פתחו את Lineage בממשק המשתמש (/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; סגור הזיהום המלא מחבר את כל הקלטים המוצהרים לכל הפלטים). -
e.embeddingו-e.geoהם צמתי command בגבול ה-enrich_grpc_set. -
geo_uהוא צומת derived המיוצר על ידי פונקציית ה-SQLUPPER.
גבול הפקודה אינו אטום. משום ש-enrich_grpc_set מצהיר על עמודות הקלט שלו
(id, region) ועל עמודות הפלט שלו (id, embedding, geo), מנוע ה-Data Lineage משחיל את
סגור הזיהום ברציפות מהעמודות המוצהרות של יחס המקור אל כל פלט.
סוגי צמתים ורמזים חזותיים¶
| סוג צומת | צבע | משמעות |
|---|---|---|
source |
ירוק | עמודה בטבלת בסיס |
derived |
כחול | מיוצרת על ידי ביטוי SQL (פונקציה, אופרטור, CTE) |
command |
סגול | עמודת פלט מפקודה רשומה |
טבעות נוספות על צומת:
- טבעת כתומה — עמודת פלט סופית של המשפט.
- מסגרת כפולה — היחס של העמודה הוא Materialized View (תצלום MV/CTAS).
- טבעת אדומה — חבר במעגל המסווג כשגיאה.
- טבעת צהובה — חבר במעגל המסווג כלולאת משוב.
טרנספורמציות בעלות שם על קשתות¶
כל קשת נושאת את ביטוי ה-SQL הגולמי המייצר את עמודת היעד, בתוספת רשימת פעולות
בעלות שם: פונקציות SQL (sql_function), אופרטורים אריתמטיים/לוגיים (operator), פקודות
רשומות (command), הפניות עמודה חשופות (identity) וליטרלים (constant).
קשת מקריאת פקודה מוצגת כקו סגול מקווקו בממשק המשתמש.
גרף ברוחב הפדרציה¶
גרף הפדרציה ממזג את ה-lineage לכל משפט של כל MV רשום לגרף מקור אחד. הזהות של צומת היא relation.column, כך שעמודת פלט של view והפניית קלט של view אחר לאותה עמודה מתמזגות לצומת אחד. התוצאה היא DAG יחיד מעמודות מקורות הבסיס ועד כל מערך נתונים נגזר בפלטפורמה.
השתמשו ב-focus, direction ו-depth כדי לתחום את התצוגה בקנה מידה של פדרציה מבלי לחשב מחדש
את הגרף.
מעגלים¶
מנוע ה-lineage מזהה כל מעגל מכוון ומסווג אותו. מעגל מתואר, ולעולם לא נדחה.
| סיווג | צבע מסגרת | משמעות |
|---|---|---|
feedback |
צהוב | המעגל חוצה צומת ממוטריאל — לולאת משוב חוקית ומושהית בזמן. תצלום ה-MV הוא גבול הגרסה שהופך אותה למוגדרת היטב. |
error |
אדום | אין גבול מטריאליזציה על הלולאה — הגדרה מעגלית ללא סדר הערכה יציב. סביר להניח ששגיאת תכנון. |
מעגל feedback הוא דפוס תקין. MV העשרה שמחזיר עמודה נגזרת אל יחס המקור שלו עובד כל עוד צומת אחד בלולאה ממומש, כי ה-snapshot מפריד בין שני החצאים בזמן. מעגל error דורש שיקול דעת של מפעיל. בדרך כלל פירושו ששני views מפנים זה לזה בלי snapshot ביניהם.
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 |
הגרסה של הקלטים שלה: snapshot של Iceberg, סימן מים, אסימון רעננות או זמן הרענון |
trace_id |
ה-trace של הרענון ב-"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 השרת כותב את רשומות ה-trace שלו כקובצי Parquet בתיקיית הטלמטריה שלו (PROVISA_TELEMETRY_DIR, כברירת מחדל ~/.provisa/telemetry), קובץ אחד לכל אצוות ייצוא בתיקייה לכל יום. דחיסת הטלמטריה המתוזמנת (otel.compact_cron) ממזגת את הקבצים הקטנים של כל יום לקבצים של עד 64 MB. כל ארגון קורא רק את הרענונים שלו, והרשומות נשמרות ללא הגבלת זמן. spans נרשמים רק כל עוד ה-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 כשלא נקבע כינוי. תצוגות, Materialized Views, ביטויי מדדים, פרדיקטים של RLS, חוזי
DQ, גרעיני metric-view ומפתחות שורה של MV נכתבים כולם מול השם ההוא, ולכן שינוי שם של
כינוי שובר אותם בדיוק כפי שמחיקת העמודה עושה.
השם הפיזי הוא table_columns.column_name, הזהות ששורדת את ההחלפה הגורפת של עמודות
בעת upsert של הטבלה. קשרים, קישורי מילון, הקצאות תגיות, עמודת ה-watermark
וקבועות מראש של עמודות מאחסנים את זה, ולכן הם נשברים רק כשהעמודה מוסרת.
columnDependents מדווח על שניהם. תצוגות ו-MV במורד הזרם מגיעות מפריסת גרף הפדרציה
בשם החשוף של העמודה; הארטיפקטים שהגרף אינו מכסה מגיעים מסריקה ישירה של
הרישום.
query {
columnDependents(tableId: "42", renamed: ["order_total"], removed: ["legacy_code"]) {
columnName
dependents { kind name detail breaksOn }
}
}
breaksOn הוא rename עבור הפניה לשם חשוף ו-remove עבור הפניה לשם פיזי, כך
שקורא יכול לדעת לאיזה חצי של העריכה כל ארטיפקט מגיב.
שאלו זאת לפני השמירה. עמודה ששמה שונה מאותרת לפי השם החשוף שהיא עדיין נושאת ברישום; ברגע שהכינוי נחת, השם הישן נעלם והשאילתה אינה מוצאת דבר.
דף הטבלאות מריץ את השאילתה אוטומטית כשעריכה ממתינה משנה alias או מצמצמת את קבוצת העמודות, ומציג את מה שמצא. האזהרה מייעצת. היא מציינת את הרכיבים המושפעים, והמנהל מחליט. היא לעולם לא חוסמת את השמירה, כי אי אפשר להגיע לכל הצרכנים: לוח מחוונים חיצוני או יישום לקוח ששואלים את העמודה בשמה נמצאים מחוץ לידיעת הרישום. מאותה סיבה, סריקות של טקסט SQL חופשי מתאימות את העמודה כאסימון מזהה בלי לפתור תחום, ולכן הן עלולות לציין רכיב שמתברר שאינו משתמש בעמודה. באזהרה, דיווח יתר הוא הכיוון הבטוח.
שימוש ב-Data Lineage לממשל חוזי פקודות¶
משום שסגור הזיהום מחבר כל עמודת קלט מוצהרת לכל עמודת פלט מוצהרת, רוחבו של אותו סגור תלוי לחלוטין במה שאתם מצהירים.
חשבו על פקודה שמקבלת טבלת הזמנות מלאה (id, region, amount, customer_id, discount, notes, ...) ומחזירה embedding. אם חוזה הקלט מפרט את כל העמודות האלה, כל עמודה במורד הזרם שמשתמשת ב-embedding מציגה lineage מכולן. זה מדויק, אבל זה לא עוזר, כי אי אפשר לדעת מה באמת השפיע.
הצהירו רק על id ועל text (העמודות שמודל ה-embedding באמת קורא), וחרוט
ה-Data Lineage מתהדק לשתי עמודות המקור ההן. הגזירה תקינה ומדויקת כאחת.
ראו פקודות למכניקה של הצהרה על חוזה קלט צר.