跳轉至

命令

指令是一個已登錄、受治理的函數,它把外部運算納入 Provisa 的治理、稽核與血緣之中。聯邦引擎原生處理 SQL。指令涵蓋 SQL 無法表達的運算,例如資料增強微服務、Python 模型、shell 指令碼或資料庫原生預存程序。你只需登錄一次,每個用戶端介面(GraphQL、pgwire SQL、REST、Arrow Flight、gRPC、Bolt/Cypher)都能在完全相同的治理下呼叫它。

指令是一次受治理的 RPC。它的輸入及輸出都經過宣告、定型、驗證、追蹤,並接入血緣。臨時拼湊的 ETL 步驟、不受治理的 curl 呼叫或子程序,這些都不具備。

實作種類

支援六個 impl_kind 值:

impl_kind 傳輸方式
source_procedure 已註冊數據來源上的原生預存程序
source_operation OpenAPI、遠端 GraphQL 或 gRPC 來源的寫入操作,原樣傳遞(見 遠端來源的寫入操作)
script 本機子行程,由 stdin 餵入 JSON,自 stdout 讀取 JSON
http HTTP/S 端點;JSON 請求主體,JSON 回應
grpc gRPC 一元呼叫;免 proto 的 JSON 橋接
python 行程內的 Python 可呼叫物件(module:attr)

定址(目錄中的 name 與 function_name)與 binding(傳輸方式與位置)是解耦的。換掉 binding,命令的治理、血緣與呼叫方合約都維持不變。

引數種類

每個引數都要宣告一個 arg_kind:

arg_kind 行為
column_value 純量;直接放進請求負載傳遞
table_ref 延遲式;Provisa 原樣傳遞關聯引用,由服務自行取數
result_set 積極式;Provisa 具體化被引用的關聯並送出其資料行

http 與 grpc 命令必須至少宣告一個 table_ref 或 result_set 引數。只收到純量引數的外部命令會被逐行呼叫一次,那就破壞了批次處理。派送器會在呼叫時拒絕這種設定(422)。

會傳回集合的命令(經 output_columns 與 return_schema 宣告)是一個表值函式。可用於 FROM 子句或 JOIN。

數據集合約

每個 table_ref 或 result_set 引數都可宣告一份輸入欄位合約:FunctionArgument.columns 中一份有序、以 IR 定型的欄位清單。命令本身則在 Function.output_columns 中宣告一份輸出欄位合約。

兩份合約在每次呼叫時都以失敗即報錯的方式驗證:

  • 輸入(僅限 result_set): 具體化之後,Provisa 會依所宣告的欄位驗證資料行。多出的欄位、缺少的欄位與類型不符,一律引發 HTTP 422。

  • 輸出: 命令傳回的資料行在抵達呼叫方之前,會先依 output_columns 驗證。

  • 窄投影: 宣告了輸入合約之後,具體化查詢只會投影那些欄位(SELECT "id", "region" FROM ...),而不是 SELECT *。

IR 類型詞彙

合約中的欄類型使用規範的 IR 類型名稱:

smallint integer bigint text boolean float double numeric date timestamp time uuid bytea json

常見別名會自動解析(varchar → text、int4 → integer、jsonb → json 等)。

return_schema 是 output_columns 的 GraphQL 投影。真正的依據是 output_columns,所以請為驗證及血緣宣告它,再為產生 GraphQL 類型加入 return_schema。

撰寫一個命令

設定檔

functions:
  - name: enrich_orders
    description: Enrich orders inline — deterministic score + region label
    domain_id: sales-analytics
    kind: query
    impl_kind: python
    source_id: ""
    function_name: enrich_orders
    returns: ""
    binding:
      callable: demo.py_functions:enrich_orders
    arguments:
      - name: input
        type: String
        arg_kind: result_set
        columns:
          - {name: id, type: integer}   # narrow input contract
          - {name: region, type: text}
    visible_to: [admin]
    output_columns:
      - {name: id, type: integer}
      - {name: score, type: double}
      - {name: region_label, type: text}
    return_schema:
      type: array
      items:
        type: object
        properties:
          id: {type: integer}
          score: {type: number}
          region_label: {type: string}

gRPC 變體(enrich_grpc_set)沿用同一套寫法,只是指定 impl_kind: grpc,並在 binding 中以 target 與 method 索引鍵取代 callable:

  - name: enrich_grpc_set
    impl_kind: grpc
    binding:
      target: ${env:DEMO_GRPC_TARGET:-localhost:50071}
      method: /provisa.demo.Enrich/EnrichRows
    arguments:
      - name: input
        type: String
        arg_kind: result_set
        columns:
          - {name: id, type: integer}
          - {name: region, type: text}
    output_columns:
      - {name: id, type: integer}
      - {name: embedding, type: text}
      - {name: geo, type: text}

管理 UI

設定 → 命令 中的命令表單包含一個逐數據集的輸入欄位編輯器(每個已宣告欄位一行,附 IR 類型選擇器)與一個輸出欄位編輯器。儲存表單即可註冊或更新命令,毋須重新載入設定。

內嵌組合

指令可以出現在更大的 SQL 陳述式內部,你可以對它做連接、子查詢或投影;SELECT * FROM fn(args) 只是最簡單的形式。例外是遠端資料來源的寫入操作,因為它只能單獨呼叫(見為何不能被組合)。

-- Enrich the orders relation and join the result back inline.
SELECT o.id, o.amount, e.score, e.region_label
FROM   orders o
JOIN   enrich_orders('main.public.orders') e ON o.id = e.id
WHERE  e.score > 0.8;

在治理、驗證或路由執行之前,管線會偵測出已註冊的命令呼叫,讓每一個都經由共用的受治理執行器執行(因此 I/O 合約與身分模型的套用方式,與直接呼叫時完全一致),再把呼叫點改寫為一個已定型的本機關聯。

替換方式會依大小自適應:一千行以內,結果會以已定型的 VALUES 清單內嵌;超過該門檻,則在引擎中註冊為一個具名的本機關聯。

在地化之後的陳述式照常路由。單一數據來源的查詢留在該數據來源上;只有真正跨數據來源的查詢才進聯邦引擎。

遠端來源的寫入操作

OpenAPI、遠端 GraphQL 或 gRPC 來源會提供寫入操作。將其中一個註冊為命令後,便可從所有介面呼叫,並受治理、被稽核。新增來源不會註冊其中任何一個;你要逐一註冊所需的操作,方式與註冊資料表相同。註冊即為策展。

來源提供的內容:

來源類型 提供的操作 操作名稱
openapi 規格中的每個非 GET 操作 operationId
graphql_remote 遠端 Mutation 類型的每個欄位 欄位名稱
grpc_remote 每個被歸類為 mutation 的方法 Service.Method

註冊一個

  1. 開啟 模型 → 命令 並新增一個命令。
  2. 選擇遠端來源。表單會切換為操作選擇器,列出該來源所提供的操作。
  3. 選擇操作、一個網域,以及可以呼叫它的角色。
  4. 可選:開啟 需要審批,並填寫 寫入表 欄位。

透過管理 GraphQL API,availableFunctions(sourceId, schemaName) 會列出這些操作。結構描述名稱依來源類型而定,為 openapi、graphql 或 grpc_remote。

其餘一切都由該操作決定,與表單傳送的內容無關。Provisa 會覆寫以下欄位:

body.implKind = "source_operation"
body.kind = "mutation"
body.schemaName = OPERATION_SCHEMA[source_type]
body.returns = ""
body.binding = {}
body.materialize = False
body.arguments = [{"name": a, "type": "json"} for a in operation.arguments]

每個引數的類型均為 json。操作的引數即其 OpenAPI 路徑參數(操作帶請求本文時另加 body)、其 GraphQL mutation 引數,或其 gRPC 請求欄位。 來源未提供的操作會被拒絕,傳回 422,functions.operation_not_offered。

呼叫

Provisa 不會對輸入進行整形、類型化或檢查。每個引數都原樣連同來源的憑證送往遠端服務,遠端服務的回應也原樣傳回。Provisa 負責治理誰可以呼叫、在哪個網域中呼叫、是否需要批准,並記錄該呼叫。

遠端服務如何接收引數:

  • OpenAPI. 路徑參數填入路徑範本。body 是 JSON 請求本文。其餘每個引數都放在查詢字串中。
  • GraphQL. 引數以類型化變數傳送,每個變數均按遠端結構描述給定的類型宣告。mutation 會回取回應中的純量欄位和列舉欄位,以及其內部物件的這類欄位,最多深入兩層。
  • gRPC. 這些引數成為 Service.Method 的請求訊息。

回應即命令的列:一個物件是一列,物件清單即其各列,其他任何內容都是一列 {"result": ...}。

在 GraphQL 中,命令是一個 mutation 欄位,其回應是 JSON 純量。

mutation {
  createIssue(input: {repositoryId: "R_kgDO...", title: "Crash on save"})
}

在 SQL 介面(pgwire 及其他傳遞 SQL 的介面)上,將每個引數寫成字串中的 JSON 字面值。'{"title": "x"}' 是物件,'"text"' 是字串,'3' 是數字。不是有效 JSON 的字面值會失敗,傳回 422,functions.json_argument_invalid。

SELECT * FROM create_issue('{"repositoryId": "R_kgDO...", "title": "Crash on save"}');

在 REST 上,向 /data/rest/{domain}/commands/{command} 以 POST 傳送由引數組成的 JSON 物件。json 引數在產生的規格中被記載為任意值。

curl -X POST https://acme.provisa.org/data/rest/engineering/commands/create_issue \
  -H "Content-Type: application/json" \
  -d '{"input": {"repositoryId": "R_kgDO...", "title": "Crash on save"}}'

拒絕

遠端服務的拒絕按其原樣傳回。

遠端服務 Provisa 回應
拒絕該呼叫(HTTP 4xx、GraphQL errors、被拒絕的 gRPC 呼叫) 422,functions.remote_refused,附帶 remote_status 和 answer
失敗(HTTP 5xx) 502, functions.remote_refused

來源的憑證能否執行該操作,由遠端服務在呼叫該操作時決定。Provisa 無法在註冊時試寫而不真正執行它。

批准

開啟 需要審批 後,每次呼叫在執行前都會提交給部署的批准掛鉤。僅當掛鉤批准時才會執行。

  • 未設定掛鉤:403,functions.approval_unavailable。
  • 掛鉤拒絕:403,functions.approval_denied,附帶掛鉤給出的原因。

掛鉤會收到呼叫者、角色、命令名稱及其引數。參見 ABAC 批准掛鉤。該旗標儲存為 Function.requires_approval,該檢查適用於任何設定了它的命令。

寫入表

在 寫入表 欄位中填寫該操作所寫入的資料表,格式為 schema.table。它必須是該命令自身來源下的已註冊資料表,否則儲存會被拒絕,傳回 422,actions.written_table_not_registered。該設定為選用。

遠端服務每接受一次呼叫,Provisa 就將其視為對該資料表的一次寫入。它會捨棄該表的快取回應,將基於它的具體化檢視標記為過期,發出變更事件,執行該表變更時所執行的命令,並在該表被保持為熱狀態時重新載入它。

當該表從其整表複本讀取時(營運方的設定將其置於複本上,或引擎無法就地讀取其資料來源),呼叫之後會請求建置該複本,原因為 write。讀取方在新複本取代之前繼續使用舊複本。逐行複製的表或帶有參數欄的表沒有可重建的整表複本。

為何不能被組合

寫入操作是一個動作,它不轉換資料;包含寫入操作的視圖或具體化視圖會在每次讀取或重新整理時執行一次寫入。因此這個呼叫只能單獨出現。單獨的 SELECT * FROM create_issue(...) 會執行它,而同一呼叫放在更大的陳述式中,無論位於連接、子查詢還是投影中,都會被拒絕。

若檢視或具體化檢視的定義呼叫了它,儲存時會被拒絕。只要註冊了任何寫入操作,無法剖析的定義也會被拒絕,因為無法證明它沒有呼叫任何一個。

command 'create_issue' writes to its source and is called on its own: it cannot be composed in a query, a view or a materialized view

來源操作亦不是血緣節點。血緣是從視圖及查詢的 SQL 中讀取的,指令在其中作為節點出現,而任何已儲存的定義都不能呼叫來源操作。

命令與血緣

由於每個命令都宣告了自己的輸入與輸出欄位,欄位層級血緣得以跨越不透明的命令邊界而閉合。血緣引擎會套用一次污染閉合:每個已宣告的輸出欄位,都推導自每個已宣告的輸入欄位。

由此而來的實際後果:輸入合約的寬度決定了這一閉包的精度。窄的輸入只包含指令真正需要的欄,會得到緊湊、易讀的血緣錐。宣告來源關係中的每一欄,則會在每個輸出上大面積扇入。這樣依然正確,因為不會遺失血緣,但會模糊可追溯性。

經驗法則:只傳入指令所需的最小投影,只傳回衍生欄。原樣回傳的輸入會擴大污染錐,所以不要傳回它們。

命令節點在 DAG 中如何呈現、又該怎麼閱讀,見 血緣。

外連允許清單

http 及 grpc 指令會呼叫外部端點。每個目標主機都必須出現在部署的 udf_egress_allowlist 中。回送位址(localhost、127.0.0.1、::1)一律允許。沒有允許清單時,所有外部輸出都會以 HTTP 403 拒絕,不存在靜默的預設值。

呼叫追蹤

每次呼叫都會產生一條追蹤,無論結果如何。追蹤包含指令名稱、傳輸類型、身分模型(DEFINER 或 INVOKER)、輸入關係參照、角色 id 及輸出基數。它由分派器產生,所以任何 impl_kind 都無法繞過。

CLI:provisa metadata export

provisa metadata export 是 shell 層的作業,執行於受治理的 RPC 路徑之外。它向 /admin/metadata-export/publish 傳送請求,觸發正在執行的伺服器按需發佈中繼資料,也就是 Admin 分頁中 立即發佈 按鈕呼叫的端點。

當設定的 reconcile_cron 排程粒度不夠細時,可用它從 cron 或 CI 驅動定時匯出:

provisa metadata export --api https://acme.provisa.org --token "$PROVISA_API_TOKEN"

結束碼 0 = 完整發佈。結束碼 1 = 部分發佈或連線失敗。

完整的旗標參考、驗證選項、多租用戶主機命名以及 cron 範例,見 中繼資料匯出——從命令列。

命令會出現在每個環境的 git 投影中。命令及其標籤指派如何在合併與拉取中存續,見 環境。