Architecture de Provisa¶
Vue d'ensemble¶
Provisa est une plateforme de virtualisation de données pilotée par configuration, conçue spécifiquement pour alimenter une couche sémantique, des petites équipes aux grandes entreprises. Elle fournit une API unifiée sur des sources de données hétérogènes avec gouvernance, sécurité et optimisation des performances. Les clients interrogent via SQL, GraphQL ou Cypher ; les trois sont des interfaces de premier ordre avec exactement la même gouvernance appliquée. (REQ-002, REQ-038)
La distinction de la couche sémantique est importante. Pour ajouter du contenu à la couche sémantique, il faut créer de nouvelles sources de données ou des agrégats au sein de la couche de virtualisation de données. Cela crée une séparation nette : aucun ajout à la sémantique ne peut être effectué en dehors de la plateforme, ce qui permet une véritable gouvernance des données. (REQ-136) L'application se fait au niveau du compilateur : le catalogue de relations approuvé est la source de vérité, quel que soit le langage de requête utilisé. (REQ-002)
Provisa est conçu pour être hautement performant pour les besoins opérationnels et hautement évolutif pour les besoins analytiques d'entreprise. Une seule plateforme répond aux deux besoins sans sacrifier la vitesse ni l'évolutivité.
Config YAML → PG Metadata → Federation Catalogs
↓
Federation engine metadata → Schema Generator → SDL / SQL catalog / Cypher labels / gRPC proto (per role)
↓
Query → Parser → SQL Compiler → Transpiler
↓
Router (Smart Dispatch)
/ | \
Federation Direct PG Direct MySQL/etc.
\ | /
Executor Pool
↓
┌───── Inline ─────┐ ┌──── Redirect ────┐
│ JSON (HTTP) │ │ CTAS → S3 │
│ Arrow (Flight) │ │ (Parquet, ORC) │
│ Protobuf (gRPC) │ │ Provisa → S3 │
└─────────────────-┘ │ (JSON, CSV, …) │
└─────────────────-┘
Interfaces de requête¶
Chaque interface est un transport distinct. Les quatre appliquent le même pipeline de sécurité (RLS, masquage, échantillonnage, vérifications de rôle). (REQ-002, REQ-038) Les clients ne communiquent jamais directement avec le moteur de fédération. (REQ-266) Le « langage de requête » (SQL / GraphQL / Cypher) est orthogonal au transport — plusieurs langages peuvent arriver via le même transport.
| Port | Transport | Accepted query languages | Use case |
|---|---|---|---|
| 8001 | HTTP | GraphQL, SQL, Cypher | Web clients, BI tools, curl, REST consumers |
| 8815 | Arrow Flight (gRPC) | SQL (via Arrow Flight SQL) | Data tools (Pandas, DuckDB, Spark, ADBC) |
| 50051 | Protobuf gRPC | Per-role generated proto RPCs | Service-to-service with typed contracts |
| configurable¹ | PostgreSQL wire protocol (pgwire) | SQL | psql, DBeaver, SQLAlchemy, any PG-compatible client |
¹ Définir PROVISA_PGWIRE_PORT (p. ex. 5433). Désactivé si non défini ou égal à 0.
HTTP (port 8001)¶
Plusieurs endpoints sous le même port, distingués par chemin :
| Path | Language | Notes |
|---|---|---|
POST /data/graphql |
GraphQL | Reads and mutations; APQ hash accepted via extensions.persistedQuery |
POST /data/sql |
SQL | Read-only; no capability gate — governed by object visibility + RLS + masking (REQ-001, REQ-267) |
POST /data/query |
Cypher | Read-only; standard role |
GET /data/nl |
Natural language | Translates to SQL/GraphQL/Cypher based on source type |
GET /data/subscribe/{table} |
GraphQL | SSE subscription stream |
GET /neo4j/... |
Cypher (Neo4j compat) | Neo4j HTTP API compatibility shim |
POST /admin/graphql |
GraphQL | Admin API (superuser/admin role required) |
Tous les chemins renvoient du JSON par défaut. Accept: text/csv, application/vnd.apache.parquet, application/vnd.apache.arrow.stream et application/octet-stream (binaire brut) sont pris en charge via la négociation de contenu. Les résultats dépassant le seuil de taille configuré sont automatiquement redirigés vers une URL S3 signée. (REQ-029, REQ-137)
Arrow Flight (port 8815)¶
Transport columnaire natif Arrow sur gRPC. (REQ-045, REQ-143) Les clients envoient un ticket JSON :
et reçoivent des RecordBatches Arrow diffusés de manière différée. Lorsque le proxy Zaychik Flight SQL est disponible, les données circulent sous forme d'un flux de lots d'enregistrements Arrow de bout en bout : (REQ-144)
Le résultat complet n'est jamais matérialisé dans la mémoire de Provisa — les lots sont transmis au fur et à mesure de leur arrivée. (REQ-145) Cela fait d'Arrow Flight une voie sans limite, adaptée à des résultats arbitrairement volumineux.
Protobuf gRPC (port 50051)¶
.proto généré automatiquement à partir du schéma de données, généré par rôle. (REQ-525) Requêtes en flux (un message par ligne), mutations unaires. Réflexion du serveur activée. (REQ-526) Le rôle est transmis via la clé de métadonnées x-provisa-role.
Protocole filaire PostgreSQL / pgwire (port configurable)¶
Implémente le protocole filaire frontend/backend de PostgreSQL à l'aide de la bibliothèque buenavista. (REQ-527) Tout client compatible PostgreSQL — psql, DBeaver, SQLAlchemy avec psycopg2, JDBC — peut se connecter sans modification. Accepte uniquement SQL. Le pipeline de gouvernance complet (RLS, masquage, autorisations de domaine) s'applique de manière identique aux connexions pgwire. (REQ-266, REQ-002) Activé en définissant PROVISA_PGWIRE_PORT sur un port non nul.
Pipeline de requêtes¶
Trois langages de requête sont acceptés. Tous convergent vers la gouvernance après leurs étapes respectives d'analyse/compilation. (REQ-262, REQ-263) Seul GraphQL prend en charge les écritures. (REQ-037) Il n'existe aucune porte de capacité sur la requête elle-même — toute identité authentifiée peut interroger dans n'importe quel langage, et les données sont gouvernées uniquement par la visibilité des objets, le RLS et le masquage. (REQ-001)
| Interface | Reads | Writes | Query gate |
|---|---|---|---|
GraphQL (/data/graphql) |
Yes | Yes (mutations) | None — data-layer governance only |
SQL (/data/sql) |
Yes | No | None — data-layer governance only (REQ-267) |
Cypher (/data/query) |
Yes | No | None — data-layer governance only |
flowchart TD
A[GraphQL Request] --> B[Auth / Role Resolution]
A2[SQL Request] --> B
A3[Cypher Request] --> B
B --> E[APQ Hash Check]
E --> F[Parse & Validate]
F --> G[Extract Directives / Hints]
G --> H{Cache Hit?}
H -- yes --> R
H -- no --> I{Input Type}
I -- GraphQL --> I1[Compile → Semantic SQL]
I -- SQL --> I2[Parse & Validate SQL\nApply Namespace / Source Binding]
I -- Cypher --> I3[Translate Cypher → SQL\nResolve Node / Rel Mappings]
I1 --> J[Governance: RLS + Masking + Visibility + Sampling]
I2 --> J
I3 --> J
J --> K[MV Rewrite]
K --> L{Route}
L -- Direct --> M[Transpile → Source Dialect\nExecute via Driver]
L -- Federation --> N[Transpile → Federation SQL\nInject Session Hints\nExecute via Federation Engine / Flight]
L -- Materialize --> O[Fetch from REST / GraphQL / gRPC\nMaterialize → S3 Parquet\nPost-filter via Federation Engine]
L -- Mutation --> P[RLS Injection\nTranspile → Source Dialect\nExecute via Driver\nInvalidate Cache + MV\nEmit Change Event]
M --> Q{Redirect?}
N --> Q
O --> Q
Q -- yes --> S[Upload to S3\nReturn Signed URL]
Q -- no --> R[Serialize: JSON / CSV / Parquet / Arrow]
R --> T[Store in Cache]
T --> U[Return to Client]
P --> U
Décisions de routage :
| Route | When |
|---|---|
| Cache | Result cache hit — evaluated first, serves the stored result with no execution (REQ-865) |
| Cheap-count | count(*)-shaped query over an unmaterialized source that exposes an exact native count — routed to the native count call instead of materializing to count (REQ-875) |
| Direct | Single source + has native driver + has federation connector |
| Federation | Multi-source federation, or source has connector but no driver |
| Materialize | Source has no federation connector — fetch and cache to S3/PG first |
| Mutation | GraphQL mutation — always direct, never federated |
Le routage consomme la sortie de l'étape d'optimisation postérieure à la gouvernance, jamais le SQL gouverné antérieur à l'optimisation. La gouvernance peut AJOUTER des sources (prédicats de sous-requête RLS) ; l'étape d'optimisation peut les SUPPRIMER (intégration VALUES-CTE des tables actives, réécritures de cache API, élagage des branches d'union). Une requête fédérée qui se réduit à une seule source active après intégration est donc réacheminée en direct. (REQ-863)
Requêtes à racines multiples¶
Les requêtes GraphQL comportant plusieurs champs racines (p. ex. { orders { id } customers { name } }) sont compilées en requêtes SQL distinctes et exécutées indépendamment. (REQ-534) Les requêtes SQL et Cypher sont par définition à racine unique. Les résultats sont fusionnés en une seule réponse :
- Les champs sous le seuil de redirection sont renvoyés en ligne dans
data - Les champs dépassant le seuil sont redirigés, avec des entrées par champ dans
redirects - Les formats binaires (Parquet, Arrow) ne sont pris en charge que pour les requêtes à racine unique
Voies d'exécution de la fédération¶
| Path | Transport | Via | When used |
|---|---|---|---|
| REST | federation engine client (HTTP :8080) | Direct query | Default, always available |
| Flight SQL | adbc-driver-flightsql (gRPC :8480) |
Zaychik proxy → JDBC | When Zaychik is running |
| CTAS | federation engine client (HTTP :8080) | Direct write, Iceberg to S3 | Parquet/ORC redirect |
Proxy Zaychik Arrow Flight SQL¶
Le moteur de fédération ne prend pas en charge nativement le protocole Arrow Flight SQL. Zaychik est un proxy Java qui implémente l'interface gRPC Arrow Flight SQL, traduit les requêtes en requêtes JDBC et renvoie les résultats sous forme de lots d'enregistrements Arrow. (REQ-144)
ADBC client → gRPC :8480 → Zaychik → JDBC :8080 → Federation Engine → results → Arrow batches → client
Le serveur Flight de Provisa (port 8815) se connecte à Zaychik en tant que client ADBC, permettant une diffusion Arrow de bout en bout sans matérialiser les résultats. (REQ-145)
Catalogue de résultats Iceberg¶
La redirection CTAS utilise un connecteur Iceberg (catalogue results) adossé à un catalogue JDBC sur l'instance PostgreSQL existante. (REQ-169) Iceberg écrit les fichiers Parquet/ORC directement sur MinIO/S3 via le système de fichiers natif S3 (fs.native-s3.enabled=true).
Moteurs de fédération¶
Provisa sélectionne un moteur de fédération au démarrage via la variable d'environnement PROVISA_ENGINE, la configuration persistée de l'UI d'administration, ou la valeur par défaut. Lorsque rien n'est défini, DuckDB est le choix par défaut — entièrement in-process, sans service externe (REQ-989). Voir Configuration pour le détail de la sélection.
Chaque moteur est une instance de FederationEngine définie dans provisa/federation/engine.py. L'instance possède une collection de connecteurs qui détermine quels types de source le moteur peut lire en direct (ATTACH), par opposition à ceux qui doivent d'abord atterrir dans le magasin de matérialisation du moteur. [tool-verified: engine.py _ENGINE_BUILDERS, ENGINE_REGISTRY]
Classes de driver (REQ-840) [tool-verified: engine.py DriverClass]¶
| Class | Meaning | Examples |
|---|---|---|
BROAD |
Reaches many external source types via native connectors | Trino |
PARTIAL |
Reaches a subset (relational, files, cloud object/lake) plus lands everything else | DuckDB, PostgreSQL, ClickHouse, Databricks, Snowflake, BigQuery, Fabric, Synapse |
SELF_ONLY |
Reaches only its own store; every other source lands in | SQLAlchemy |
Moteurs disponibles [tool-verified: engine.py _ENGINE_BUILDERS]¶
| Engine key | Dialect | MPP | External-link mechanism | Auth |
|---|---|---|---|---|
trino / trino-byo |
Trino SQL | Yes | Trino catalogs (broad connector set) | JDBC credentials |
pg |
PostgreSQL | No | FDW / pg_duckdb | PostgreSQL credentials |
duckdb |
DuckDB | No | Extension-native ATTACH | None (in-process) |
clickhouse / clickhouse-server |
ClickHouse | Yes (shards) | S3 / IcebergS3 / DeltaLake table engines (REQ-986) | ClickHouse credentials |
snowflake |
Snowflake | Yes | External stage + external table (REQ-988) | PROVISA_ENGINE_URL |
databricks |
Databricks SQL | Yes | Unity Catalog external tables via REST (REQ-987) | Bearer token (http_path in federation_hints) |
bigquery |
BigQuery | Yes (Dremel) | BigQuery external / BigLake tables | GOOGLE_APPLICATION_CREDENTIALS service-account key |
fabric |
T-SQL | Yes | OneLake shortcuts → OPENROWSET | Azure AD (az login / managed identity) |
synapse |
T-SQL | Yes | ADLS OPENROWSET / external tables | Azure AD |
sqlalchemy |
Any SQLAlchemy dialect | No | None (land-only) | Per-dialect credentials |
Valeur par défaut sans configuration : DuckDB (REQ-989) [tool-verified: engine.py build_duckdb_engine, _embedded_duckdb_materialize_default]¶
Lorsque PROVISA_ENGINE n'est pas défini, Provisa utilise le moteur DuckDB entièrement embarqué in-process. Le magasin de matérialisation de DuckDB est un fichier DuckDB embarqué situé à $PROVISA_DATA_DIR/materialize.duckdb (par défaut ~/.provisa/materialize.duckdb). Aucune base de données ni service externe n'est requis.
Comme DuckDB impose un seul rédacteur (writer) par fichier, store_connection.py écrit dans le magasin embarqué via la propre connexion du moteur — jamais via une seconde connexion indépendante. C'est le seul cas où le moteur et le magasin de matérialisation partagent un descripteur de fichier par conception. [tool-verified: store_connection.py module docstring]
Transport de lecture natif Arrow (REQ-986, REQ-987, REQ-988) [tool-verified: engine.py build_*_engine capabilities=]¶
ClickHouse, DuckDB, Snowflake, Databricks, BigQuery, Fabric et Synapse annoncent tous EngineCapability.ARROW et EngineCapability.ARROW_STREAM. Les requêtes exécutées sur ces moteurs renvoient directement des RecordBatches Arrow — le chemin de sérialisation ligne par ligne est entièrement contourné. Le serveur Flight diffuse ces lots aux clients sans matérialiser le résultat complet dans la mémoire de processus de Provisa. Pour Trino, la diffusion Arrow repose sur le proxy Zaychik ; pour les moteurs d'entrepôt de données, l'API native Arrow propre à chaque moteur (Cloud Fetch pour Databricks, Storage Read API pour BigQuery, fetch_arrow_table pour DuckDB et Snowflake) alimente le flux Flight.
Liens de données externes (ATTACH) [tool-verified: engine.py _warehouse_connectors]¶
Chaque moteur d'entrepôt de données peut scanner des données d'objets/lacs cloud sur place, sans en atterrir de copie. Les fichiers Parquet, CSV, Iceberg et Delta Lake sur S3, GCS ou OneLake s'attachent directement au moteur comme s'il s'agissait de tables natives. La stratégie — ATTACH (scan sur place) ou LAND (copie dans le magasin) — est déterminée par le Mechanism déclaré du connecteur ; aucune ramification spécifique au moteur n'existe dans le planificateur. Un connecteur Mechanism.ATTACH_R déclenche un scan sans copie ; un connecteur Mechanism.DIRECT ou l'absence de connecteur déclenche un atterrissage. [tool-verified: connector_base.py Mechanism, engine.py _warehouse_connectors]
L'attachement provisionne automatiquement tous les prérequis au moment de l'attachement :
| Engine | Object/lake formats | Mechanism | Auto-provisioning [tool-verified] |
|---|---|---|---|
| Databricks | parquet, csv, iceberg, delta_lake | UC external table (ATTACH_R) |
REST installs Unity Catalog storage credential + external location, then CREATE TABLE … USING <format> LOCATION … — live-verified over Cloudflare R2 |
| BigQuery | parquet, csv, json, iceberg, delta_lake | BigQuery external / BigLake table (ATTACH_R) |
CREATE OR REPLACE EXTERNAL TABLE … OPTIONS(format=…, uris=[…]) — live-verified |
| ClickHouse | csv, parquet, iceberg, delta_lake | S3 / IcebergS3 / DeltaLake table engine (ATTACH_R) |
Validation probe executed at attach time — live-verified over Cloudflare R2 |
| Fabric | parquet, csv, iceberg, delta_lake | OneLake shortcut → OPENROWSET (ATTACH_R) |
REST creates an AmazonS3Compatible connection + lakehouse + shortcut; returns the OneLake BULK path — live-verified reading R2 through Fabric |
| Snowflake | parquet, csv, json, iceberg, delta_lake | External stage + external table (ATTACH_R) |
CREATE STAGE … URL=… CREDENTIALS=…, then CREATE OR REPLACE EXTERNAL TABLE … LOCATION=@stage FILE_FORMAT=(TYPE=…) — implemented; not live-tested (no account available) |
Les identifiants pour le stockage cloud circulent dans le federation_hints de la source (voir Sources). Tout type de source ne pouvant pas faire d'ATTACH atterrit d'abord dans le magasin de matérialisation du moteur.
Écritures de matérialisation columnaire (REQ-990) [tool-verified: core/database.py:436, store_connection.py:99]¶
Connection.bulk_copy dans provisa/core/database.py choisit la voie d'ingestion en masse la plus rapide selon le dialecte du magasin : COPY binaire (copy_records_to_table d'asyncpg) pour les magasins PostgreSQL, et une unique instruction préparée executemany pour tous les autres magasins relationnels. Le magasin embarqué DuckDB atterrit via land_duckdb_native dans store_connection.py — un seul appel executemany pour l'ensemble du lot, jamais une boucle ligne par ligne.
Redirection des résultats volumineux¶
Les résultats dépassant un seuil de lignes sont redirigés vers un stockage compatible S3 (MinIO) au lieu d'être renvoyés en ligne. (REQ-029)
Modes de redirection¶
| Mode | How it works | Data touches Provisa? |
|---|---|---|
| CTAS (Parquet, ORC) | Federation engine writes directly to S3 via CREATE TABLE AS SELECT |
No |
| Provisa upload (JSON, NDJSON, CSV, Arrow IPC) | Provisa serializes and uploads via boto3 | Yes |
Pour les formats natifs CTAS, Provisa ne manipule jamais les données — le moteur de fédération écrit les fichiers directement sur MinIO/S3. (REQ-138) C'est la voie privilégiée pour les exportations analytiques volumineuses.
En-têtes de redirection¶
| Header | Effect |
|---|---|
X-Provisa-Redirect-Format: <mime> |
Redirect in this format (implies force unless threshold set) |
X-Provisa-Redirect-Threshold: N |
Only redirect if result exceeds N rows |
X-Provisa-Redirect: true |
Force redirect using default format |
Ces en-têtes mettent en œuvre la redirection contrôlée par le client. (REQ-137)
Réponse :
{
"data": {"orders": null},
"redirect": {
"redirect_url": "https://minio:9000/provisa-results/results/abc.parquet?...",
"row_count": 50000,
"expires_in": 3600,
"content_type": "application/vnd.apache.parquet"
}
}
Configuration du serveur¶
| Env var | Default | Purpose |
|---|---|---|
PROVISA_REDIRECT_ENABLED |
false |
Enable server-side threshold redirect |
PROVISA_REDIRECT_THRESHOLD |
1000 |
Default row count threshold |
PROVISA_REDIRECT_FORMAT |
parquet |
Default redirect format |
PROVISA_REDIRECT_BUCKET |
provisa-results |
S3 bucket name |
PROVISA_REDIRECT_ENDPOINT |
S3-compatible endpoint URL | |
PROVISA_REDIRECT_TTL |
3600 |
Presigned URL TTL (seconds) |
Arbre de décision de routage¶
Multi-source query? → Federation engine
NoSQL source (MongoDB, Cassandra)? → Federation engine
Uses path columns on non-PG source? → Federation engine
Single RDBMS with driver? → Direct (sub-100ms target)
Single RDBMS without driver? → Federation engine
Steward hint "federated"? → Federation engine (override)
Steward hint "direct"? → Direct (if possible)
Redirect to Parquet/ORC? → Federation engine (CTAS, regardless of source count)
(REQ-027, REQ-028, REQ-030, REQ-279)
Optimisation des requêtes de fédération¶
Provisa amorce automatiquement l'optimiseur à base de coûts du moteur de fédération afin que les plans de requête inter-sources reposent sur la distribution réelle des données, et non sur des valeurs par défaut codées en dur.
Statistiques automatiques (ANALYZE)¶
Lors de l'enregistrement d'une source, Provisa exécute ANALYZE catalog.schema.table pour chaque table publiée. (REQ-275) Cela recueille :
- Le nombre de lignes
- Par colonne : la fraction de valeurs nulles, le nombre de valeurs distinctes, le min/max, les histogrammes (selon le connecteur)
L'optimiseur utilise ces données pour estimer la sélectivité des requêtes filtrées. Sans statistiques, il se rabat sur des valeurs par défaut fixes (p. ex. 10 % de sélectivité pour les prédicats d'égalité), qui produisent de mauvais plans de jointure sur des données asymétriques ou à forte cardinalité. Avec des statistiques, les estimations sont suffisamment précises pour prendre les bonnes décisions entre jointure par diffusion (broadcast) et jointure partitionnée pour la plupart des charges de travail.
Couverture : la prise en charge des statistiques varie selon le connecteur. PostgreSQL, MySQL, Hive, Iceberg et Delta Lake prennent entièrement en charge ANALYZE. Les connecteurs MongoDB et Cassandra offrent une prise en charge partielle ou nulle. Provisa absorbe silencieusement les échecs d'ANALYZE — l'enregistrement n'est jamais bloqué. (REQ-275)
Limites de sélectivité : les statistiques fournissent des estimations par colonne. Pour les prédicats corrélés (WHERE region = 'US' AND city = 'Seattle'), l'optimiseur suppose l'indépendance des colonnes, ce qui peut sous-estimer le nombre de lignes. Il s'agit d'une limitation connue des statistiques au niveau des colonnes dans tous les optimiseurs à base de coûts.
Sources API : les tables api_cache_{table_name} dans PostgreSQL sont analysées automatiquement après chaque cycle de rafraîchissement du cache, de sorte que l'optimiseur dispose d'estimations de lignes à jour lors de la jointure de sources adossées à des API avec des sources relationnelles. (REQ-280)
Administration : rafraîchir les statistiques¶
Relancez la collecte de statistiques à la demande via l'API d'administration : (REQ-276)
mutation {
refreshSourceStatistics(sourceId: "sales-pg") {
tablesAnalyzed
failures { table message }
}
}
Utile lorsqu'une source a reçu de nouvelles données significatives depuis son enregistrement.
Vues matérialisées¶
Les vues matérialisées (MV) optimisent de manière transparente les requêtes coûteuses en précalculant et en mettant en cache les résultats.
Les relations comme indices pour les MV¶
Une déclaration de relation n'est pas seulement un artefact de gouvernance — c'est aussi la description structurelle d'une forme de jointure. Cette forme est exactement ce dont l'optimiseur de MV a besoin : deux tables, deux colonnes, un type de jointure. Cela signifie qu'une relation peut directement piloter la matérialisation.
Pour les relations inter-sources, cela se produit automatiquement au démarrage : chaque relation inter-sources approuvée génère une MV JoinPattern (auto-mv-<rel_id>). (REQ-158) Aucune configuration de MV distincte n'est requise. Lorsque le compilateur détecte cette jointure dans une requête, le réécrivain substitue le résultat prématérialisé de manière transparente.
Pour les relations de même source, les stewards peuvent opter explicitement via materialize: true. Les JOIN de même source sont déjà rapides via l'exécution directe, la matérialisation n'est donc intéressante que pour les chemins de jointure très sollicités. (REQ-159)
Conséquence pratique : les stewards qui approuvent une relation décident implicitement si la jointure est une bonne candidate à la matérialisation. L'acte de gouvernance et l'indice d'optimisation sont une seule et même déclaration.
Modes¶
| Mode | Config | Behavior |
|---|---|---|
| Join-pattern | join_pattern in MV config |
Rewrites matching JOINs to read from MV table |
| Custom SQL | sql in MV config |
Arbitrary SELECT, optionally exposed in SDL |
| Auto-materialized relationship | cross-source relationship (automatic) | Auto-generates a join-pattern MV; no config required |
| Steward-materialized relationship | materialize: true on same-source relationship |
Explicit opt-in for hot same-source join paths |
Matérialisation automatique¶
Les JOIN inter-sources sont les requêtes les plus coûteuses (toujours fédérées). Les relations inter-sources génèrent automatiquement des définitions de MV au démarrage : (REQ-158)
relationships:
- id: orders-to-reviews
source_table_id: orders # sales-pg
target_table_id: product_reviews # reviews-mongo
source_column: product_id
target_column: product_id
cardinality: one-to-many
materialize: true # auto-create MV
refresh_interval: 600 # refresh every 10 minutes
Seules les relations inter-sources génèrent des MV (les JOIN de même source sont déjà rapides via l'exécution directe). (REQ-159) La MV démarre en statut STALE et est rafraîchie par la boucle de rafraîchissement en arrière-plan avant d'être utilisée par l'optimiseur de requêtes. (REQ-160)
Cycle de vie du rafraîchissement¶
STALE → (refresh loop picks up) → REFRESHING → FRESH
↑ |
└──── mutation hits source table ────────────────┘
La boucle de rafraîchissement s'exécute toutes les 30 secondes, vérifie get_due_for_refresh(), et exécute CREATE TABLE AS SELECT (première exécution) ou DELETE + INSERT (exécutions suivantes) sur la table cible de la MV via le moteur de fédération. (REQ-160, REQ-234)
Plan des modules¶
| Module | Purpose |
|---|---|
api/ |
FastAPI app, routers, middleware, lifespan management |
api/flight/ |
Arrow Flight server (gRPC, port 8815) |
api/admin/ |
Strawberry GraphQL admin API — config, discovery, views |
api/rest/ |
Auto-generated REST endpoints from registered tables |
api/jsonapi/ |
Auto-generated JSON:API endpoints with pagination and error handling |
api/data/subscribe.py |
SSE subscriptions — LISTEN/NOTIFY, polling, Debezium CDC |
compiler/ |
GraphQL/SQL parsers, semantic SQL generator, RLS, masking, sampling, two-stage governance (stage2.py) |
cypher/ |
Cypher → SQL translator, parser, label map (REQ-351), write translator for Cypher mutations |
pgwire/ |
PostgreSQL wire-protocol server; catalog.py intercepts pg_catalog/information_schema for per-role object visibility (REQ-527, REQ-883, REQ-891) |
vector/ |
Vector search — model registry, embedding providers (openai/ollama/huggingface), cosine_similarity() translation, pgvector fallback cache, declarative embedding generation (REQ-419–431) |
compiler/federation.py |
Apollo Federation v2 subgraph support |
transpiler/ |
Dialect transpilation, routing logic |
executor/ |
Federated/direct execution, serialization, output formats |
executor/drivers/ |
Direct source drivers (PostgreSQL, MySQL, DuckDB, Snowflake, Databricks, ClickHouse, …) |
executor/trino_flight.py |
ADBC Flight SQL client for the federation engine |
executor/ctas_write.py |
CTAS-based redirect (federation engine writes to S3) |
executor/redirect.py |
S3 redirect logic, Provisa-side upload |
federation/engine.py |
FederationEngine, DriverClass, _ENGINE_BUILDERS, ENGINE_REGISTRY, build_engine |
federation/connector.py |
Connector abstractions — Trino, ClickHouse; Mechanism, WarehouseNativeConnector |
federation/connector_duckdb.py |
DuckDB and PostgreSQL FDW connector definitions |
federation/snowflake_connectors.py |
Snowflake external stage + external table ATTACH connectors (REQ-988) |
federation/databricks_connectors.py |
Databricks UC external table ATTACH connectors (REQ-987) |
federation/bigquery_connectors.py |
BigQuery external / BigLake ATTACH connectors |
federation/databricks_uc.py |
Unity Catalog credential + external location auto-provisioning |
federation/databricks_backend.py |
Databricks SQL warehouse execution backend |
federation/snowflake_backend.py |
Snowflake execution backend |
federation/bigquery_backend.py |
BigQuery execution backend (Storage Read API Arrow transport) |
federation/mssql_warehouse_backend.py |
Fabric Warehouse + Synapse execution backends (T-SQL over ODBC) |
federation/mssql_warehouse_connectors.py |
OPENROWSET ATTACH connectors for Fabric / Synapse |
federation/fabric_shortcuts.py |
OneLake shortcut auto-provisioning (connection → lakehouse → shortcut) |
federation/clickhouse_backend.py |
ClickHouse execution backend |
federation/duckdb_backend.py |
DuckDB in-process execution backend |
federation/pg_backend.py |
PostgreSQL execution backend |
federation/store_connection.py |
DuckDB-native materialization store write face (REQ-989, REQ-990) |
registry/ |
Persisted query registry, governance |
security/ |
Visibility, rights, column masking |
cache/ |
Redis-backed query result caching (hot tier) |
mv/ |
Materialized view registry, refresh, SQL rewriter |
events/ |
Dataset change events and trigger dispatch |
webhooks/ |
Outbound webhook execution for mutations and events |
scheduler/ |
APScheduler-based background job management — cron and interval triggers that fire webhooks, mutations, or Kafka sink publishes |
apq/ |
Apollo APQ wire protocol — Redis-backed query hash cache; separate from result caching |
compiler/cursor.py |
Relay-style cursor pagination — first/after/last/before arguments and pageInfo generation on all list queries |
compiler/aggregate_gen.py |
Auto-generated {table}_aggregate query types with count, sum, avg, min, max sub-fields and filtered nodes access |
compiler/enum_detect.py |
Enum type auto-detection — PostgreSQL native enum types (pg_enum) exposed as GraphQL enum types rather than string scalars |
compiler/hints.py |
Federation performance hints — query-level routing directives embedded as SQL comments (/* @provisa route=federated */) that override automatic routing |
compiler/mutation_gen.py |
Mutation compiler; column presets — server-side static or session-variable values applied on insert/update, not exposed in the mutation input type |
auth/approval_hook.py |
ABAC approval hook — pluggable external authorization called before query execution; webhook, gRPC, and unix_socket transports; per-table/source/global scope; configurable fallback policy |
subscriptions/ |
SSE subscription state and delivery |
discovery/ |
LLM relationship discovery (Claude API) |
grpc/ |
Proto generation, gRPC server, reflection |
api_source/ |
REST/GraphQL/gRPC API sources with PG cache |
kafka/ |
Kafka topic sources, sink, Schema Registry |
auth/ |
Pluggable auth providers, middleware, role mapping |
core/ |
Config, models, DB, repositories, secrets; role model supports parent_role_id and flatten_roles() for recursive role inheritance |
hasura_v2/ |
Hasura v2 metadata → Provisa config converter |
ddn/ |
Hasura DDN supergraph → Provisa config converter |
mongodb/ |
MongoDB source connector |
elasticsearch/ |
Elasticsearch source connector |
cassandra/ |
Cassandra source connector |
prometheus/ |
Prometheus metrics source connector |
source_adapters/ |
Generic adapter layer for source connections |
API d'administration¶
L'API GraphQL d'administration Strawberry est montée sur /admin/graphql (port HTTP 8001). Elle est distincte de l'endpoint GraphQL de données et requiert le rôle superutilisateur ou administrateur.
| Capability | Description |
|---|---|
| Config download/upload | Export or replace the full Provisa YAML config |
| Relationship editor | Create, update, delete relationship definitions |
| AI FK discovery | Trigger Claude-powered FK candidate analysis |
| Schema introspection | Browse published tables, columns, and roles |
| View management | Register and manage materialized view definitions |
(REQ-164, REQ-165, REQ-166, REQ-167)
Endpoints REST et JSON:API générés automatiquement¶
Les tables enregistrées sont exposées comme endpoints REST et JSON:API en plus de l'interface GraphQL. (REQ-256, REQ-257)
| Interface | Mount path | Spec |
|---|---|---|
| REST | /rest/<table-id> |
Simple GET/POST with query parameters |
| JSON:API | /jsonapi/<table-id> |
jsonapi.org compliant — pagination, relationships, error objects |
Ces endpoints appliquent le même pipeline de sécurité (RLS, masquage, vérifications de rôle) que l'endpoint GraphQL. (REQ-002, REQ-038)
Abonnements¶
Les abonnements SSE sont servis sur GET /data/subscribe/{table}. Trois modes de livraison : (REQ-258)
| Mode | Mechanism | When used |
|---|---|---|
| LISTEN/NOTIFY | PostgreSQL LISTEN on a channel |
PG sources with mutation activity |
| Polling | Re-execute query on interval | Non-PG sources, or when CDC unavailable |
| Debezium CDC | Kafka topic from Debezium connector | High-frequency change streams |
Le client reçoit text/event-stream avec un événement JSON par ligne modifiée ou par différence.
Système d'événements et de webhooks¶
Les mutations de base de données (INSERT/UPDATE/DELETE) peuvent déclencher des événements sortants via les modules events/ et webhooks/. (REQ-172, REQ-173, REQ-220)
Mutation executed → EventDispatcher → match event trigger rules
↓
WebhookExecutor → HTTP POST to configured URL
Les déclencheurs d'événements sont définis dans la configuration et associés par table, type d'opération et filtre de ligne optionnel. Les charges utiles de webhook incluent le type d'opération, la ligne modifiée et le contexte de rôle.
Services en arrière-plan¶
Quatre boucles en arrière-plan démarrent pendant le cycle de vie de l'application (api/app.py) :
| Service | Interval | Purpose |
|---|---|---|
| MV refresh loop | 30 s | Polls get_due_for_refresh(), executes CTAS or DELETE+INSERT on stale MVs |
| Warm table manager | Configurable | Promotes frequently-queried tables to Iceberg local SSD cache |
| Hot table loader | Configurable | Loads small reference tables into in-memory cache for sub-millisecond access |
| API source poller | Per-source interval | Re-fetches and re-caches remote REST/GraphQL/gRPC sources |
(REQ-160, REQ-238, REQ-239, REQ-236)
Niveaux de mise en cache des tables actives/tièdes¶
| Tier | Storage | Promotion criteria | Access latency |
|---|---|---|---|
| Hot | In-process memory | Row count < threshold, or is a relationship target | <1 ms |
| Warm | Iceberg on local SSD | Query frequency threshold exceeded | ~5–20 ms |
| Cold | Remote source | Default | 50–500 ms |
(REQ-230, REQ-236, REQ-238, REQ-241)
Importation de métadonnées (Hasura v2 / DDN)¶
Les déploiements Hasura existants peuvent être convertis en configuration Provisa sans réécriture manuelle. (REQ-182, REQ-183)
| Module | Input | Output |
|---|---|---|
hasura_v2/ |
Hasura v2 metadata.yaml |
Provisa config.yaml |
ddn/ |
Hasura DDN supergraph JSON | Provisa config.yaml |
Les deux convertisseurs mappent les tables suivies, les relations, les autorisations et les schémas distants. Le résultat est une configuration Provisa complète, prête pour le déploiement. (REQ-182, REQ-183)
Apollo Federation¶
compiler/federation.py expose Provisa comme un sous-graphe Apollo Federation v2. (REQ-259) Le SDL du sous-graphe est généré automatiquement à partir du schéma publié, avec des directives @key sur les colonnes de clé primaire et des annotations @external/@provides sur les relations inter-sous-graphes. Provisa répond aux requêtes _entities et _service requises par la passerelle de fédération. (REQ-259)
Pagination par curseur¶
Toutes les requêtes de liste prennent en charge la pagination par curseur de style Relay via compiler/cursor.py. (REQ-218) Les clients passent des arguments first/after (vers l'avant) ou last/before (vers l'arrière). Le compilateur encode la position de la ligne sous forme de curseur opaque en base64 et injecte les clauses WHERE/LIMIT appropriées. Chaque requête de liste renvoie un objet pageInfo :
| Field | Type | Description |
|---|---|---|
hasNextPage |
Boolean | True if more results exist after this page |
hasPreviousPage |
Boolean | True if results exist before this page |
startCursor |
String | Cursor of the first node in this page |
endCursor |
String | Cursor of the last node in this page |
Requêtes d'agrégation¶
Chaque table enregistrée obtient un champ racine {table}_aggregate généré automatiquement (compiler/aggregate_gen.py). (REQ-196) Le type d'agrégation expose count, sum, avg, min, max par colonne numérique, ainsi que nodes pour un accès filtré aux lignes avec sélection complète des champs (même RLS/masquage que la requête de base). (REQ-196, REQ-198) Les requêtes d'agrégation sont éligibles au routage vers une MV d'agrégation — voir mv/aggregate_catalog.py. (REQ-198)
Requêtes persistées automatiques (APQ)¶
apq/cache.py implémente le protocole filaire APQ d'Apollo. (REQ-288) Lorsqu'un client envoie uniquement un hash de requête (extensions.persistedQuery), Provisa le recherche dans Redis. (REQ-289) En cas d'échec, il renvoie une erreur PersistedQueryNotFound ; le client réessaie avec le corps complet de la requête, que Provisa stocke. (REQ-288) Ceci est indépendant de la mise en cache des résultats (cache/).
Rôles hérités¶
Les rôles dans core/models.py peuvent référencer un parent_role_id. (REQ-215) flatten_roles() résout de manière récursive la chaîne d'héritage et fusionne les clauses WHERE du RLS (avec ET logique), la visibilité des colonnes (union, la plus restrictive l'emporte) et les politiques de masquage (l'enfant remplace le parent par colonne). Cela évite de dupliquer des ensembles d'autorisations entre rôles similaires (p. ex. analyst héritant de reader). (REQ-215)
Hook d'approbation ABAC¶
auth/approval_hook.py est un hook d'autorisation enfichable invoqué avant l'exécution de la requête, après le RLS et le masquage. (REQ-203) Il s'intègre à des moteurs de politiques externes (OPA, services ABAC personnalisés).
| Setting | Description |
|---|---|
| Transport | webhook (HTTP POST), grpc, or unix_socket |
| Scope | Per-table, per-source, or global |
| Fallback policy | allow or deny when the hook endpoint is unreachable |
Détection automatique des types énumérés¶
compiler/enum_detect.py introspecte les types énumérés natifs de PostgreSQL (pg_enum) au moment de la génération du schéma. (REQ-221) Les colonnes utilisant un type énuméré défini par l'utilisateur dans PostgreSQL sont promues en types énumérés GraphQL — leurs valeurs deviennent des membres d'énumération plutôt que des scalaires de type chaîne.
Déclencheurs planifiés¶
scheduler/jobs.py utilise APScheduler pour exécuter des tâches en arrière-plan définies comme des déclencheurs cron ou à intervalle. (REQ-216) Chaque tâche peut envoyer un POST vers une URL de webhook, exécuter une mutation contre l'endpoint de données, ou publier des résultats de requête dans un topic Kafka. Les déclencheurs sont configurés via l'API d'administration (mutations scheduledTrigger) ou la clé scheduled_triggers dans la configuration YAML. (REQ-216)
Indices de performance de la fédération¶
compiler/hints.py analyse les indices de steward intégrés dans les requêtes sous forme de commentaires, selon la syntaxe de commentaire de Provisa. (REQ-279) Le format de l'indice varie selon le langage de requête :
| Hint | Effect |
|---|---|
route=federated |
Force federation through the federation engine, bypassing direct-driver routing |
route=direct |
Force direct-driver execution |
Préréglages de colonnes dans les mutations¶
compiler/mutation_gen.py prend en charge des préréglages par colonne côté serveur, appliqués sur INSERT ou UPDATE. (REQ-214) Les préréglages ne sont pas inclus dans le type d'entrée de mutation GraphQL généré — ils sont injectés par le compilateur de manière transparente. Types de préréglage : static (valeur littérale) ou session (valeur provenant de la session/de l'en-tête de la requête, p. ex. x-hasura-user-id). (REQ-214)
Explorateur de schéma GraphQL Voyager¶
L'UI d'administration (provisa-ui/src/pages/SchemaExplorer.tsx) intègre GraphQL Voyager comme outil interactif de visualisation de schéma. (REQ-248) Il affiche le schéma limité par rôle sous forme de diagramme entité-relation navigable — les tables comme nœuds, les relations comme arêtes. Le schéma affiché est toujours filtré selon le rôle actuellement sélectionné.
Ordre d'application de la sécurité¶
Il n'existe aucune porte de capacité sur la requête — la gouvernance s'exprime entièrement via des contrôles de la couche de données. (REQ-001) Une requête SQL brute rejette (HTTP 403) toute table hors du périmètre d'objets du rôle avant même que la gouvernance ne s'exécute. (REQ-267)
- Visibilité des objets : le schéma par rôle masque les tables/colonnes non autorisées ; les tables hors périmètre en SQL brut sont rejetées (REQ-039, REQ-267)
- Application des relations : les traversées doivent exister dans le catalogue de relations approuvé, sauf si le rôle possède
ignore_relationships(REQ-001) - RLS : injection de clause WHERE par table et par rôle (REQ-040, REQ-041, REQ-263)
- Masquage des colonnes : transformation de données par colonne et par rôle (REQ-263)
- Plafond de lignes (LIMIT) : plafond du nombre de lignes pour les rôles sans
full_results; l'échantillonnage statistique aléatoire est une fonctionnalité de requête utilisateur distincte (REQ-263, REQ-478)
Les quatre interfaces de requête (HTTP, Flight, gRPC, pgwire) appliquent le même pipeline de gouvernance de l'étape 2 ; aucune voie client ne peut le contourner sans contourner le serveur. (REQ-002, REQ-038, REQ-266)
Limites d'évolutivité¶
Provisa est une couche de compilation et de routage légère — elle n'ajoute qu'une latence à un seul chiffre en millisecondes à la requête. Cependant, les voies où Provisa sérialise les données de résultat sont limitées par la mémoire du processus. Deux voies sont véritablement sans limite :
| Path | Memory bound? | Suitable for |
|---|---|---|
| JSON inline (HTTP) | Yes | Small-medium results |
| Arrow Flight streaming (gRPC :8815) | No | Unbounded — streaming via Zaychik or warehouse Arrow API |
| Protobuf gRPC inline (:50051) | Yes | Medium results, service-to-service |
| Redirect: Provisa upload (JSON, CSV, NDJSON, Arrow IPC) | Yes | Medium results, file download |
| Redirect: CTAS (Parquet, ORC) | No | Unbounded — federation engine writes to S3 |
Sondage de seuil¶
Pour la redirection basée sur un seuil, Provisa injecte LIMIT threshold + 1 dans la requête comme sonde. (REQ-140) Si le résultat comporte moins de lignes, il est renvoyé en ligne (résultat complet, sans travail gaspillé). Si le résultat atteint la limite, la sonde est écartée et la requête complète est réexécutée via CTAS ou téléversement Provisa. Cela évite SELECT COUNT(*) (que certaines sources n'optimisent pas) et fonctionne sur toute source.
Pour les charges de travail analytiques volumineuses, utilisez l'une des options suivantes :
- Arrow Flight (port 8815) pour la diffusion vers des outils de données — les lots transitent par Provisa sans être matérialisés (REQ-145)
- Redirection Parquet/ORC pour les exports basés sur des fichiers — le moteur de fédération écrit directement sur S3, Provisa renvoie une URL présignée (REQ-138, REQ-044)
Infrastructure¶
| Service | Image | Port | Purpose |
|---|---|---|---|
| Provisa API | (host process) | 8001 | HTTP/REST endpoint |
| Provisa Flight | (host process) | 8815 | Arrow Flight gRPC server |
| Provisa gRPC | (host process) | 50051 | Protobuf gRPC server |
| Federation Engine | trinodb/trino (default) or external warehouse |
8080 / varies | Query federation engine — Trino for the embedded stack; Snowflake/Databricks/BigQuery/Fabric/Synapse/DuckDB for warehouse targets |
| Zaychik | provisa-zaychik (built from source) |
8480 | Arrow Flight SQL proxy for Trino; not required for warehouse engines |
| PostgreSQL | postgres:16 |
5432 | Config metadata + Iceberg catalog |
| MongoDB | mongo:7 |
27017 | Demo NoSQL data source |
| MinIO | minio/minio |
9000/9001 | S3-compatible object storage |
| Redis | redis:7-alpine |
6379 | Query result cache |
| PgBouncer | edoburu/pgbouncer |
6432 | Connection pooling for PG |
| Kafka | confluentinc/cp-kafka:7.6.0 |
9092 | Streaming data sources |
| Schema Registry | confluentinc/cp-schema-registry:7.6.0 |
8081 | Avro/Protobuf schema management |