Comandi¶
Un comando è una funzione registrata e governata che porta il calcolo esterno sotto la governance, l'audit e la derivazione di Provisa. Il motore di federazione gestisce SQL in modo nativo. Un comando copre il calcolo che SQL non può esprimere, come un microservizio di arricchimento, un modello Python, uno script shell o una stored procedure nativa del database. Lo registrate una volta, e ogni superficie client (GraphQL, SQL via pgwire, REST, Arrow Flight, gRPC, Bolt/Cypher) può invocarlo con una governance identica.
Un comando è un RPC governato. I suoi input e output sono dichiarati, tipizzati, validati, tracciati e collegati alla derivazione. Un passo ETL improvvisato, una chiamata curl non governata o un sottoprocesso non hanno nulla di tutto ciò.
Tipi di implementazione¶
Sono supportati sei valori impl_kind:
impl_kind |
Trasporto |
|---|---|
source_procedure |
Stored procedure nativa su un'origine registrata |
source_operation |
Un'operazione di scrittura di un'origine OpenAPI, GraphQL remota o gRPC, passata così com'è (vedi Operazione di scrittura di un'origine remota) |
script |
Subprocess locale alimentato con JSON su stdin, legge JSON da stdout |
http |
Endpoint HTTP/S; corpo richiesta JSON, risposta JSON |
grpc |
gRPC unary; bridge JSON senza proto |
python |
Callable Python in-process (module:attr) |
L'indirizzamento (il name del catalogo e function_name) è disaccoppiato dal binding (trasporto
e posizione). Sostituendo il binding, la governance, la derivazione e i contratti verso il chiamante
del comando rimangono invariati.
Tipi di argomento¶
Ogni argomento dichiara un arg_kind:
arg_kind |
Comportamento |
|---|---|
column_value |
Scalare; passato direttamente nel payload della richiesta |
table_ref |
Lazy; Provisa passa il riferimento di relazione così com'è; il servizio recupera i dati |
result_set |
Eager; Provisa materializza la relazione referenziata e invia le sue righe |
I comandi http e grpc devono dichiarare almeno un argomento table_ref o result_set.
Un comando esterno che riceve solo argomenti scalari verrebbe invocato una volta per riga, il che
vanifica il batching. Il dispatcher rifiuta questa configurazione al momento della chiamata (422).
Un comando che restituisce un set (dichiarato tramite output_columns e return_schema) è una
funzione table-valued. Si usa in una clausola FROM o in un JOIN.
Il contratto del dataset¶
Ogni argomento table_ref o result_set può dichiarare un contratto di colonne di input: un
elenco ordinato, tipizzato IR, di colonne in FunctionArgument.columns. Il comando stesso dichiara
un contratto di colonne di output in Function.output_columns.
Entrambi i contratti vengono validati fail-loud a ogni invocazione:
-
Input (solo result_set): dopo la materializzazione, Provisa valida le righe contro le colonne dichiarate. Campi extra, campi mancanti e tipi errati sollevano tutti HTTP 422.
-
Output: le righe restituite dal comando vengono validate contro
output_columnsprima di raggiungere il chiamante. - Proiezione ristretta: quando è dichiarato un contratto di input, la query di
materializzazione proietta solo quelle colonne (
SELECT "id", "region" FROM ...) anzichéSELECT *.
Il vocabolario di tipi IR¶
I tipi di colonna dei contratti usano i nomi di tipo IR canonici:
smallint integer bigint text boolean float double numeric
date timestamp time uuid bytea json
Gli alias più comuni si risolvono automaticamente (varchar → text, int4 → integer, jsonb →
json, ecc.).
return_schema è la proiezione GraphQL di output_columns. La fonte di verità è output_columns, quindi dichiaratelo per la validazione e la derivazione, e aggiungete return_schema per generare i tipi GraphQL.
Creare un comando¶
File di config¶
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}
La variante gRPC (enrich_grpc_set) segue lo stesso schema ma indica impl_kind: grpc e un
binding con le chiavi target e method anziché 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 Admin¶
Il modulo del comando in Impostazioni → Comandi include un editor delle colonne di input per dataset (una riga per colonna dichiarata, con un selettore di tipo IR) e un editor delle colonne di output. Salvando il modulo si registra o si aggiorna il comando senza ricaricare la configurazione.
Composizione inline¶
I comandi possono comparire dentro un'istruzione SQL più ampia, dove li unite in join, li usate in sottoquery o li proiettate; SELECT * FROM fn(args) è solo la forma più semplice. L'eccezione è l'operazione di scrittura di una sorgente remota, che si chiama da sola (vedi Perché non può essere composta).
-- 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;
Prima che siano eseguite la governance, la validazione o l'instradamento, la pipeline rileva le chiamate ai comandi registrati, esegue ciascuna attraverso l'esecutore governato condiviso (così il contratto di I/O e il modello di identità si applicano esattamente come per una chiamata diretta) e riscrive il punto di chiamata come una relazione locale tipizzata.
La sostituzione si adatta alla dimensione: fino a 1.000 righe il risultato viene inserito inline
come elenco VALUES tipizzato; oltre quella soglia viene registrato come relazione locale nominata
nel motore.
Un'istruzione localizzata viene instradata normalmente. Le query su una sola origine restano sull'origine; solo le query realmente cross-origine passano al motore di federazione.
Operazione di scrittura di un'origine remota¶
Un'origine OpenAPI, GraphQL remota o gRPC offre operazioni di scrittura. Registrarne una come comando la rende richiamabile, governata e sottoposta ad audit da ogni superficie. Aggiungere l'origine non ne registra nessuna; si registrano quelle desiderate, una alla volta, come si registrano le tabelle. La registrazione è la curation.
Cosa offre un'origine:
| Tipo di origine | Operazioni offerte | Nome dell'operazione |
|---|---|---|
openapi |
Ogni operazione non GET della specifica | L'operationId |
graphql_remote |
Ogni campo del tipo Mutation remoto |
Il nome del campo |
grpc_remote |
Ogni metodo classificato come mutazione | Service.Method |
Registrarne una¶
- Aprire Modello → Comandi e aggiungere un comando.
- Scegliere l'origine remota. Il modulo passa a un selettore di operazioni che elenca ciò che l'origine offre.
- Scegliere l'operazione, un dominio e i ruoli che possono chiamarla.
- Facoltativamente attivare Richiede approvazione e compilare il campo Scrive nella tabella.
Tramite l'API GraphQL di amministrazione, availableFunctions(sourceId, schemaName) elenca le operazioni. Il nome dello schema è openapi, graphql o grpc_remote, in base al tipo di origine.
Tutto il resto deriva dall'operazione, qualunque cosa invii il modulo. Provisa sovrascrive questi campi:
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]
Ogni argomento è tipizzato json. Gli argomenti dell'operazione sono i suoi parametri di percorso OpenAPI (più body quando l'operazione accetta un corpo della richiesta), i suoi argomenti di mutazione GraphQL o i suoi campi di richiesta gRPC. Un'operazione che l'origine non offre viene rifiutata con 422, functions.operation_not_offered.
Chiamarla¶
Provisa non dà forma, non tipizza e non controlla l'input. Ogni argomento va al servizio remoto invariato, con la credenziale dell'origine, e la risposta del servizio remoto torna invariata. Provisa governa chi può chiamare, in quale dominio, se serve l'approvazione, e registra la chiamata.
Come il servizio remoto riceve gli argomenti:
- OpenAPI. I parametri di percorso riempiono il modello del percorso.
bodyè il corpo JSON della richiesta. Ogni altro argomento va nella query string. - GraphQL. Gli argomenti vengono inviati come variabili tipizzate, ciascuna dichiarata con il tipo che le assegna lo schema remoto. La mutazione richiede indietro i campi scalari e di enumerazione della risposta, e quelli degli oggetti al suo interno fino a due livelli.
- gRPC. Gli argomenti diventano il messaggio di richiesta di
Service.Method.
La risposta sono le righe del comando: un oggetto è una riga, una lista di oggetti sono le sue righe, qualsiasi altra cosa è una riga {"result": ...}.
In GraphQL il comando è un campo di mutazione e la sua risposta è lo scalare JSON.
Sulle superfici SQL (pgwire e le altre che passano SQL) scrivere ogni argomento come letterale JSON in una stringa. '{"title": "x"}' è un oggetto, '"text"' una stringa, '3' un numero. Un letterale che non è JSON valido fallisce con 422, functions.json_argument_invalid.
In REST, inviare con POST un oggetto JSON di argomenti a /data/rest/{domain}/commands/{command}. Un argomento json è documentato nella specifica generata come un valore qualsiasi.
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"}}'
Rifiuti¶
Il rifiuto del servizio remoto torna come l'ha formulato il servizio.
| Servizio remoto | Provisa risponde |
|---|---|
Rifiuta la chiamata (HTTP 4xx, errors di GraphQL, una chiamata gRPC rifiutata) |
422, functions.remote_refused, con remote_status e answer |
| Fallisce (HTTP 5xx) | 502, functions.remote_refused |
Se la credenziale dell'origine possa eseguire l'operazione lo decide il servizio remoto, quando l'operazione viene chiamata. Provisa non può provare una scrittura alla registrazione senza eseguirla.
Approvazione¶
Attivando Richiede approvazione, ogni chiamata viene sottoposta all'hook di approvazione del deployment prima di essere eseguita. Viene eseguita solo se l'hook approva.
- Nessun hook configurato: 403,
functions.approval_unavailable. - L'hook nega: 403,
functions.approval_denied, con il motivo dell'hook.
L'hook riceve il chiamante, il ruolo, il nome del comando e i suoi argomenti. Vedi Hook di approvazione ABAC. Il flag è memorizzato come Function.requires_approval, e il controllo si applica a qualsiasi comando che lo imposti.
Scrive nella tabella¶
Indicare nel campo Scrive nella tabella la tabella in cui scrive l'operazione, come schema.table. Deve essere una tabella registrata dell'origine propria del comando, altrimenti il salvataggio viene rifiutato con 422, actions.written_table_not_registered. L'impostazione è facoltativa.
Dopo ogni chiamata che il servizio remoto accetta, Provisa la tratta come una scrittura su quella tabella. Scarta le risposte in cache della tabella, segna come obsolete le viste materializzate costruite su di essa, emette l'evento di modifica, esegue i comandi che vengono eseguiti quando cambia e ricarica la tabella quando è tenuta hot.
Quando la tabella viene letta dalla sua replica completa (le impostazioni dell'operatore la collocano lì, o il motore non può leggere la sua sorgente sul posto), dopo la chiamata viene richiesta una costruzione della replica, con il motivo write. I lettori mantengono la vecchia replica finché la nuova non la sostituisce. Una tabella replicata riga per riga, o con una colonna parametro, non ha una replica completa da ricostruire.
Perché non può essere composta¶
Un'operazione di scrittura è un'azione. Non trasforma dati, e una vista o vista materializzata che ne contenesse una eseguirebbe la scrittura a ogni lettura o aggiornamento. Per questo la chiamata sta da sola. SELECT * FROM create_issue(...) da sola la esegue, e la stessa chiamata dentro un'istruzione più ampia viene rifiutata ovunque si trovi, in un join, in una sottoquery o in una proiezione.
Una vista o vista materializzata la cui definizione ne chiama una viene rifiutata al salvataggio. Anche una definizione che non si riesce a interpretare viene rifiutata finché è registrata una qualsiasi operazione di scrittura, perché non si può dimostrare che non ne chiami nessuna.
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
Neanche un'operazione di sorgente è un nodo di derivazione. La derivazione si legge dall'SQL di viste e query, dove un comando compare come nodo, e nessuna definizione salvata può chiamare un'operazione di sorgente.
Comandi e derivazione¶
Poiché ogni comando dichiara le proprie colonne di input e di output, la derivazione a livello di colonna si chiude attraverso il confine opaco del comando. Il motore di derivazione applica una chiusura per contaminazione: ogni colonna di output dichiarata deriva da ogni colonna di input dichiarata.
La conseguenza pratica: l'ampiezza del vostro contratto di input fissa la precisione di quella chiusura. Un input stretto, con solo le colonne che servono al comando, produce un cono di derivazione compatto e leggibile. Dichiarare ogni colonna della relazione sorgente allarga il ventaglio su ogni output. Resta corretto, perché nessuna derivazione va persa, ma offusca la tracciabilità.
Regola pratica: passate la proiezione minima che serve al comando e restituite solo colonne derivate. Gli input restituiti invariati allargano il cono di contaminazione, quindi lasciateli fuori.
Vedere Derivazione per come i nodi comando appaiono nel DAG e come leggerli.
Allowlist di egress¶
I comandi http e grpc chiamano endpoint esterni. Ogni host di destinazione deve comparire nella udf_egress_allowlist del deployment. Il loopback (localhost, 127.0.0.1, ::1) è sempre consentito. Senza allowlist, ogni uscita esterna viene negata con HTTP 403, e non c'è alcun default silenzioso.
Tracciamento delle invocazioni¶
Ogni invocazione emette una traccia, qualunque sia l'esito. La traccia contiene il nome del comando, il tipo di trasporto, il modello di identità (DEFINER o INVOKER), i riferimenti alle relazioni di input, l'id del ruolo e la cardinalità dell'output. La emette il dispatcher, quindi nessun impl_kind può saltarla.
CLI: provisa metadata export¶
provisa metadata export è un job del livello shell e gira fuori dal percorso RPC governato. Avvia la pubblicazione su richiesta dei metadati del server in esecuzione inviando a /admin/metadata-export/publish, l'endpoint che chiama il pulsante Pubblica ora della scheda Admin.
Si usa per pilotare esportazioni programmate da cron o CI quando la pianificazione
reconcile_cron configurata non è abbastanza granulare:
Uscita 0 = pubblicazione completa. Uscita 1 = pubblicazione parziale o errore di connessione.
Per il riferimento completo dei flag, le opzioni di autenticazione, la denominazione degli host in multitenancy e un esempio di cron, vedere Esportazione dei metadati — Dalla riga di comando.
I comandi compaiono nella proiezione git di ogni ambiente. Vedere Ambienti per come un comando e le sue assegnazioni di tag sopravvivono a un merge e a un pull.