לדלג לתוכן

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 המיוצר על ידי פונקציית ה-SQL UPPER.

גבול הפקודה אינו אטום. משום ש-enrich_grpc_set מצהיר על עמודות הקלט שלו (id, region) ועל עמודות הפלט שלו (id, embedding, geo), מנוע ה-Data Lineage משחיל את סגור הזיהום ברציפות מהעמודות המוצהרות של יחס המקור אל כל פלט.

סוגי צמתים ורמזים חזותיים

סוג צומת צבע משמעות
source ירוק עמודה בטבלת בסיס
derived כחול מיוצרת על ידי ביטוי SQL (פונקציה, אופרטור, CTE)
command סגול עמודת פלט מפקודה רשומה
native כתום השדה הגולמי שמקור קורא, מתחת לעמודה של טבלה רשומה

טבעות נוספות על צומת:

  • טבעת כתומה — עמודת פלט סופית של המשפט.
  • מסגרת כפולה — היחס של העמודה הוא Materialized View ‏(תצלום MV/CTAS).
  • טבעת אדומה — חבר במעגל המסווג כשגיאה.
  • טבעת צהובה — חבר במעגל המסווג כלולאת משוב.

טרנספורמציות בעלות שם על קשתות

כל קשת נושאת את ביטוי ה-SQL הגולמי המייצר את עמודת היעד, בתוספת רשימת פעולות בעלות שם: פונקציות SQL ‏(sql_function), אופרטורים אריתמטיים/לוגיים (operator), פקודות רשומות (command), הפניות עמודה חשופות (identity) וליטרלים (constant).

קשת מקריאת פקודה מוצגת כקו סגול מקווקו בממשק המשתמש.

מתחת לטבלה הרשומה

משתמש עם הרשאת רישום מקורות רואה צעד נוסף מתחת לכל עמודת מקור: השדה כפי שהמקור קורא אותו, בתיבה עבור המקום שממנו הוא נקרא. התיבה נושאת את הלוגו ואת תווית הסוג של המקור, כפי שעמוד Sources מציג אותם, ואת הכתובת לפי סוג המקור:

מקור כתובת
OpenAPI הפעולה (GET /v2/customers), הפרמטרים הקבועים שלה ומיקום השורות
Remote GraphQL שדה השורש, הארגומנטים הנדרשים שלו ומיקום השורות
MongoDB, Cosmos DB מסד הנתונים והאוסף או ה-container
Elasticsearch, Qdrant האינדקס או האוסף
CSV, Parquet, קבצים, לוגים הנתיב, ותבנית הקבצים של טבלה שקוראת קבצים רבים
Kafka ה-topic
מקור קוד הפונקציה
כל מקור אחר הסכימה והטבלה המקוריות

השדה הגולמי הוא נתיב השדה של העמודה כשיש לה כזה, ואחרת השם שלה עצמה. הקשת ממנו לעמודה הרשומה מציינת את שינוי השם: path, ‏alias או naming (מוסכמת השמות). התיבה של הטבלה הרשומה אומרת איך היא מוגשת: virtual, נקראת בכל הצהרה, או replica, עם ה-TTL שלה ומתי נטענה לאחרונה. אישורים, הפניות לסודות, וכן המשתמש, הסיסמה וה-query של כתובת URL, אינם מוצגים לעולם. משתמש בלי הרשאת רישום מקורות רואה את השושלת מהטבלה הרשומה ומעלה.

קישורים

כל רכיב נפתח במקום שבו הוא מוגדר, כשהרשאות המשתמש פותחות את העמוד הזה. לחיצה על שדה פותחת את הטבלה, התצוגה או המקור שלו; לחיצה כפולה על תיבה פותחת את הרלציה שלה; לחיצה על הקשת של command פותחת את ה-command. לפונקציית SQL מובנית אין עמוד ואין קישור.

גרף ברוחב הפדרציה

גרף הפדרציה ממזג את ה-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"]
}

עבור קורא עם הרשאת רישום מקורות, שני ה-endpoints מוסיפים את הצעד המקורי: צמתים מהסוג native, ואובייקט natives שמפתחותיו הם הרלציה של כל תיבה מקורית (native:<domain>.<table>):

"natives": {
  "native:sales.customers": {
    "relation": "sales.customers",
    "source": "crm",
    "type": "openapi",
    "hints": {"brand": "stripe"},
    "address": [
      {"what": "operation", "value": "GET /v2/customers"},
      {"what": "rows", "value": "data.items"}
    ],
    "served": {"mode": "virtual"}
  }
}

ה-served של רפליקה הוא {"mode": "replica", "cache_ttl": 3600, "last_load": "...", "next_load": "..."}. עבור כל קורא אחר natives ריק.

מחזירה 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).

תצוגה כפי שהייתה, וכפי שנרשמה

קריאה פשוטה מחזירה את המצב הנוכחי. שני ארגומנטים של התצוגה, שנכתבים במקום שבו ההצהרה נוקבת בה, קוראים את עברה:

  • as_of קורא את התצוגה בזמן מסוים, או על פני טווח: "reproduce"."view_exposure"(as_of => '2026-09-30T12:00:00').
  • 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 מתהדק לשתי עמודות המקור ההן. הגזירה תקינה ומדויקת כאחת.

ראו פקודות למכניקה של הצהרה על חוזה קלט צר.