Aller au contenu

Adaptateur SQL au-dessus d'Apache Ossie

Saiku fournit un adaptateur SQL Calcite qui lit du YAML Apache Ossie — le même format que l’exporteur Ossie produit à partir de votre schéma Mondrian — et l’expose comme une surface JDBC interrogeable. Pointez n’importe quel client SQL dessus et vous pouvez faire des SELECT sur vos datasets Ossie exactement comme s’ils étaient de vraies tables de base de données. Sous le capot, Calcite planifie la requête et la pousse jusqu’à votre véritable entrepôt sous forme de SQL natif.

Ce que vous obtenez

  • Du SQL standard sur vos datasets Ossie — tout ce que Calcite comprend, ce qui est un large surensemble d’ANSI SQL.
  • Pushdown vers l’entrepôt. Calcite lit la requête, la planifie contre votre entrepôt (Postgres, Snowflake, H2, peu importe) et émet du SQL natif. Les agrégats tournent dans l’entrepôt, pas dans la JVM.
  • Natif JDBC. Fonctionne avec n’importe quel client JDBC : dbt, DBeaver, Tableau, Power BI, psql, JetBrains DataGrip, code personnalisé.
  • Les renommages Ossie survivent. Si votre exporteur a émis des noms de datasets qui ne correspondent pas aux tables sous-jacentes de l’entrepôt (via le champ source d’Ossie), l’adaptateur remappe de manière transparente — SELECT * FROM CUSTOMERS devient SELECT * FROM public.dim_customer_v2 au niveau de l’entrepôt.

Démarrage rapide

1. Produire le YAML Ossie

Fenêtre de terminal
saiku ossie-export --in saiku-home/data/Pharma.xml --out pharma.ossie.yaml

Voir Export vers Apache Ossie pour la table de correspondance et l’exemple détaillé.

2. Écrire un modèle de connexion Calcite

Les connexions JDBC Calcite prennent un modèle de connexion JSON qui indique à Calcite quelle fabrique instancier :

{
"version": "1.0",
"defaultSchema": "PHARMA",
"schemas": [{
"name": "PHARMA",
"type": "custom",
"factory": "org.saiku.sql.adapter.OssieSchemaFactory",
"operand": {
"ossieYaml": "/absolute/path/to/pharma.ossie.yaml",
"jdbcUrl": "jdbc:postgresql://localhost:5432/warehouse",
"jdbcUser": "app",
"jdbcPassword": "changeme"
}
}]
}

Clés d’operand :

CléRequisDescription
ossieYamlouiChemin absolu vers le fichier YAML Ossie.
modelNamenonNom de l’entrée semantic_model[] à exposer quand le document en porte plusieurs. Par défaut la première.
jdbcUrlfortement encouragéURL JDBC de l’entrepôt. Sans elle, les tables s’enregistrent mais les requêtes renvoient zéro ligne.
jdbcUser, jdbcPasswordselon besoinIdentifiants de l’entrepôt.

3. Se connecter

Properties p = new Properties();
p.put("model", "/path/to/model.json");
p.put("caseSensitive", "false");
try (Connection c = DriverManager.getConnection("jdbc:calcite:", p)) {
var rs = c.createStatement().executeQuery(
"SELECT REGION, COUNT(*) FROM PHARMA.PRESCRIBER GROUP BY REGION");
while (rs.next()) System.out.println(rs.getString(1) + "" + rs.getInt(2));
}

Ou depuis n’importe quel client JDBC — URL de connexion jdbc:calcite:model=/path/to/model.json.

4. Ou l’exécuter comme un endpoint réseau

Pour les clients distants, utilisez la sous-commande CLI saiku sql-serve. Elle expose deux endpoints — un endpoint Apache Avatica pour les clients compatibles Avatica ET un endpoint natif au protocole wire Postgres pour psql / pgAdmin / Tableau / DBeaver / dbt-postgres. Activez l’un ou les deux :

Fenêtre de terminal
saiku sql-serve \
--ossie pharma.ossie.yaml \
--schema PHARMA \
--jdbc-url jdbc:postgresql://warehouse:5432/prod \
--jdbc-user app --jdbc-password changeme \
--port 8765 \
--pg-port 5432

Le serveur logge les deux URL au démarrage. Les clients se connectent ensuite via :

Avatica :

jdbc:avatica:remote:url=http://localhost:8765
# with serialization=protobuf

Protocole wire Postgres (n’importe quel client PG natif) :

Fenêtre de terminal
# psql
PGSSLMODE=disable psql -h localhost -p 5432 saiku
# JDBC — pgjdbc defaults (extended query mode) work; simple mode is also fine
jdbc:postgresql://localhost:5432/saiku?sslmode=disable

Les requêtes paramétrées via PreparedStatement.setString(1, ...) etc. fonctionnent d’emblée.

Ce qui fonctionne aujourd’hui

Fonctionnalité SQLStatutNotes
SELECT depuis un datasetPoussé jusqu’à l’entrepôt.
WHERE sur les colonnes du datasetPoussé.
GROUP BY + agrégatsLes agrégats tournent dans l’entrepôt.
JOIN … ON explicite entre datasetsLe prédicat de join est poussé.
ORDER BY, LIMITPoussés là où le dialecte de l’entrepôt les prend en charge.
information_schema / DatabaseMetaData.getTables()Datasets, métriques ET vues de join découvrables par les outils de BI.
Mapping de renommage (nom de dataset Ossie → table d’entrepôt via source)Transparent.
Repli insensible à la casse (H2 UPPER vs Postgres lower)L’adaptateur essaie toutes les casses.
SELECT de métrique scalaireSELECT * FROM PHARMA.TOTAL_QUANTITYDéveloppe l’ANSI_SQL de la métrique contre son dataset d’origine. Type de retour dérivé du type de colonne sous-jacent (pas de suppositions grossières DOUBLE/BIGINT).
Vues de join de relationSELECT ... FROM PHARMA.FACT_PHARMA_JOIN_PRESCRIBER ...Une vue par relationship Ossie, nommée <from>_JOIN_<to>. Le prédicat JOIN vit dans le YAML, pas dans la requête. Calcite pousse le tout comme un seul JOIN — aucun surcoût d’exécution.
Joins auto-injectésSELECT c.x, SUM(o.y) FROM ORDERS o, CUSTOMERS c GROUP BY c.x (sans clause JOIN)Une règle de planificateur Calcite personnalisée détecte le join cartésien entre deux datasets Ossie et injecte le prédicat ON depuis la relation. Même résultat qu’écrire le JOIN à la main. Voir « Joins auto-injectés » ci-dessous.
Métriques MDX uniquement (membres calculés)✅ (invisible)Correctement non exposées sur la surface SQL. Elles vivent dans Mondrian.

Pas encore pris en charge

Suivi sur l’épopée Ossie/SQL :

  • SSL/TLS + auth sur les deux endpoints. Actuellement anonyme + en clair. Indispensable avant tout déploiement en prod.
  • Paramètres/résultats au format binaire sur l’endpoint PG-wire — Bind décode maintenant correctement les INT2/INT4/INT8 big-endian (la plupart des appels setInt/setLong des outils de BI) mais le décodage binaire complet dirigé par le type (BOOL/DATE/NUMERIC/TIMESTAMP contre les OID de paramètres déclarés de l’instruction) est un suivi.
  • Suspension de portail — Execute renvoie toujours toutes les lignes quelle que soit la demande maxRows du client. Correct pour les requêtes BI interactives ; compte pour la pagination à gros curseur.
  • Auto-joins à trois voiesFROM A, B, C où A↔B et B↔C existent tous les deux. Les réécritures imbriquées devraient cascader mais cela n’a pas encore été vérifié dans les tests.
  • Joins cross-schema — joiner un dataset Ossie avec une table non-Ossie (d’un sous-schéma Calcite différent). La règle d’auto-join abandonne quand les deux côtés appartiennent à des schémas différents.
  • Schéma de connexion jdbc:saiku://. Aujourd’hui les utilisateurs passent par jdbc:calcite: + un fichier model.json. Un pilote JDBC natif arrive avec le travail sur le protocole wire Postgres.
  • Protocole wire Postgres. Aujourd’hui la surface est JDBC uniquement. jdbc:saiku: sur le wire (pour que les clients psql / libpq se connectent nativement) est #1386.

Exemple détaillé — cube Pharma

Étant donné le YAML Ossie Pharma que l’exporteur produit :

version: 0.2.0.dev0
semantic_model:
- name: Pharma Rx
datasets:
- name: fact_pharma
source: public.fact_pharma
- name: Prescriber
source: public.dim_prescriber
primary_key: [prescriberkey]
relationships:
- name: fact_pharma_to_Prescriber
from: fact_pharma
to: Prescriber
from_columns: [prescriberkey]
to_columns: [prescriberkey]

Vous pouvez l’interroger ainsi :

-- Simple dataset scan.
SELECT * FROM "Pharma Rx".fact_pharma LIMIT 10;
-- Aggregate — pushed down as SUM() to Postgres.
SELECT SUM(quantity_units) AS total_units FROM "Pharma Rx".fact_pharma;
-- Scalar metric SELECT — same result as above, but the aggregate
-- expression lives in the Ossie YAML instead of the query.
SELECT * FROM "Pharma Rx"."Quantity";
-- Join via the relationship's foreign key.
SELECT
p.prescribername,
SUM(f.quantity_units) AS units,
COUNT(*) AS rx_count
FROM "Pharma Rx".fact_pharma f
JOIN "Pharma Rx"."Prescriber" p ON f.prescriberkey = p.prescriberkey
GROUP BY p.prescribername
ORDER BY units DESC
LIMIT 20;
-- Same query using the pre-materialised join view — the ON predicate
-- lives in the Ossie YAML. Users don't need to remember which columns
-- link fact_pharma to Prescriber.
SELECT
prescribername,
SUM(quantity_units) AS units,
COUNT(*) AS rx_count
FROM "Pharma Rx".fact_pharma_JOIN_Prescriber
GROUP BY prescribername
ORDER BY units DESC
LIMIT 20;

Les cinq tournent entièrement dans Postgres via le pushdown JDBC de Calcite — la JVM ne voit jamais les lignes individuelles pour les agrégats.

Joins auto-injectés

Les utilisateurs n’ont pas besoin de se souvenir quelles colonnes lient ORDERS à CUSTOMERS. Listez simplement les deux datasets dans FROM et Calcite injectera le prédicat ON pour vous :

-- User writes:
SELECT c.REGION, SUM(o.AMOUNT) AS TOTAL
FROM "Pharma Rx".fact_pharma o, "Pharma Rx"."Prescriber" c
GROUP BY c.REGION;
-- The adapter's OssieAutoJoinRule detects the Cartesian join between two datasets
-- in the same Ossie schema, looks up the relationship, and rewrites to:
SELECT c.REGION, SUM(o.AMOUNT) AS TOTAL
FROM "Pharma Rx".fact_pharma o JOIN "Pharma Rx"."Prescriber" c ON o.prescriberkey = c.prescriberkey
GROUP BY c.REGION;

La réécriture se produit pendant la phase d’optimisation de Calcite — le tout est quand même poussé jusqu’à l’entrepôt comme une seule requête JOIN.

Quand la règle se déclenche

  • Le join doit être cartésien. Les clauses JOIN … ON … explicites ne sont jamais écrasées — l’utilisateur a demandé un prédicat spécifique et nous le respectons.
  • Toutes les tables doivent être des datasets détenus par Ossie dans le même OssieSchema. Les tables cross-schema et non-Ossie laissent la requête inchangée.
  • Exactement une relationship Ossie doit lier chaque paire auto-jointe. Plusieurs candidats lèvent AmbiguousJoinException avec la liste des noms candidats — l’utilisateur doit ajouter un ON explicite pour en choisir un. Les résultats silencieusement faux sont le mode de défaillance contre lequel nous nous prémunissons.

Prise en charge N-way

FROM A, B, C, … (trois tables ou plus dans un seul cartésien) s’auto-joint aussi. La règle descend à travers les Joins imbriqués pour atteindre chaque TableScan brut, puis construit une nouvelle chaîne de join left-deep en utilisant les relations qui lient chaque nouvelle table à l’ensemble déjà joint. Les requêtes à l’échelle de Pharma contre fact + plusieurs dims fonctionnent sans aucune clause JOIN.

Quand elle ne se déclenche pas

  • Aucune relation entre deux datasets qui devraient être liés → le cartésien demeure (probablement pas ce que l’utilisateur veut, mais honnête).
  • Auto-joins (FROM A a, A b) → laissés en cartésien.
  • Joins cross-schema mélangeant des tables Ossie avec des tables non-Ossie.

Vues de join

Pour chaque relationship Ossie, l’adaptateur enregistre une vue pré-matérialisée nommée <from>_JOIN_<to>. Le SQL de la vue est le JOIN des deux datasets sur les from_columns / to_columns de la relation, et son type de ligne est la concaténation des colonnes des deux datasets (avec des suffixes numériques en cas de collision).

Étant donné ce fragment Ossie :

relationships:
- name: fact_pharma_to_Prescriber
from: fact_pharma
to: Prescriber
from_columns: [prescriberkey]
to_columns: [prescriberkey]

L’adaptateur expose PHARMA.fact_pharma_JOIN_Prescriber avec l’union des colonnes des deux tables. Les utilisateurs écrivent :

SELECT * FROM "Pharma Rx".fact_pharma_JOIN_Prescriber WHERE prescribername LIKE 'Dr%';

et Calcite pousse SELECT * FROM fact_pharma JOIN dim_prescriber ON fact_pharma.prescriberkey = dim_prescriber.prescriberkey WHERE prescribername LIKE 'Dr%' — aucun surcoût d’exécution par rapport à écrire le JOIN à la main.

Les relations multi-colonnes sont prises en charge : le prédicat ON fait un AND de chaque paire from_columns[i] = to_columns[i].

Voir aussi