Pular para o conteúdo

Adaptador SQL sobre Apache Ossie

O Saiku distribui um adaptador SQL Calcite que lê YAML Apache Ossie — o mesmo formato que o exportador Ossie produz a partir do seu schema Mondrian — e o expõe como uma superfície JDBC consultável. Aponte qualquer cliente SQL para ele e você pode dar SELECT nos seus datasets Ossie exatamente como se fossem tabelas de banco reais. Por baixo, o Calcite planeja a query e a empurra para seu warehouse real como SQL nativo.

O que você ganha

  • SQL padrão sobre seus datasets Ossie — qualquer coisa que o Calcite entenda, o que é um grande superset de ANSI SQL.
  • Pushdown para o warehouse. O Calcite lê a query, a planeja contra seu warehouse (Postgres, Snowflake, H2, o que for) e emite SQL nativo. Agregados rodam no warehouse, não na JVM.
  • JDBC-nativo. Funciona com qualquer cliente JDBC: dbt, DBeaver, Tableau, Power BI, psql, JetBrains DataGrip, código customizado.
  • Renomes Ossie sobrevivem. Se seu exportador emitiu nomes de dataset que não casam com as tabelas do warehouse subjacente (via o field source do Ossie), o adaptador mapeia de volta transparentemente — SELECT * FROM CUSTOMERS vira SELECT * FROM public.dim_customer_v2 no warehouse.

Início rápido

1. Produza o YAML Ossie

Terminal window
saiku ossie-export --in saiku-home/data/Pharma.xml --out pharma.ossie.yaml

Veja Exportando para Apache Ossie para a tabela de mapeamento e exemplo trabalhado.

2. Escreva um connect model do Calcite

Conexões JDBC do Calcite recebem um connect model JSON que diz ao Calcite qual factory instanciar:

{
"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"
}
}]
}

Chaves de operand:

ChaveObrigatórioDescrição
ossieYamlsimPath absoluto para o arquivo YAML Ossie.
modelNamenãoNome da entrada semantic_model[] a expor quando o documento carrega várias. Default para a primeira.
jdbcUrlfortemente encorajadoURL JDBC do warehouse. Sem ela, tabelas registram mas queries retornam zero linhas.
jdbcUser, jdbcPasswordconforme necessárioCredenciais do warehouse.

3. Conecte

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 de qualquer cliente JDBC — URL de conexão jdbc:calcite:model=/path/to/model.json.

4. Ou rode como um endpoint de rede

Para clientes remotos, use o subcomando CLI saiku sql-serve. Ele expõe dois endpoints — um endpoint Apache Avatica para clientes Avatica-aware E um endpoint de wire Postgres nativo para psql / pgAdmin / Tableau / DBeaver / dbt-postgres. Habilite qualquer um ou ambos:

Terminal window
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

O servidor loga ambas as URLs no startup. Clientes então conectam via:

Avatica:

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

Wire Postgres (qualquer cliente PG nativo):

Terminal window
# 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

Queries parametrizadas via PreparedStatement.setString(1, ...) etc. funcionam out of the box.

O que funciona hoje

Recurso SQLStatusNotas
SELECT de um datasetEmpurrado para o warehouse.
WHERE em colunas do datasetEmpurrado.
GROUP BY + agregadosAgregados rodam no warehouse.
JOIN … ON explícito entre datasetsPredicado de join empurra para baixo.
ORDER BY, LIMITEmpurrado onde o dialeto do warehouse suporta.
information_schema / DatabaseMetaData.getTables()Datasets, metrics E views de join descobríveis por ferramentas BI.
Mapeamento de rename (nome de dataset Ossie → tabela do warehouse via source)Transparente.
Fallback case-insensitive (H2 UPPER vs Postgres lower)O adaptador tenta todos os casos.
SELECT de métrica escalarSELECT * FROM PHARMA.TOTAL_QUANTITYExpande o ANSI_SQL da métrica contra seu dataset de origem. Tipo de retorno derivado do tipo da coluna subjacente (sem palpites grosseiros de DOUBLE/BIGINT).
Views de join de relationshipSELECT ... FROM PHARMA.FACT_PHARMA_JOIN_PRESCRIBER ...Uma view por relationship Ossie, nomeada <from>_JOIN_<to>. O predicado JOIN vive no YAML, não na query. O Calcite empurra tudo para baixo como um único JOIN — sem overhead de runtime.
Joins auto-injetadosSELECT c.x, SUM(o.y) FROM ORDERS o, CUSTOMERS c GROUP BY c.x (sem cláusula JOIN)Regra de planner customizada do Calcite detecta o join cartesiano entre dois datasets Ossie e injeta o predicado ON a partir da relationship. Mesmo resultado de escrever o JOIN à mão. Veja “Joins auto-injetados” abaixo.
Métricas só-MDX (calculated members)✅ (invisível)Corretamente não expostas na superfície SQL. Elas vivem no Mondrian.

Ainda não suportado

Rastreado no épico Ossie/SQL:

  • SSL/TLS + auth em ambos os endpoints. Atualmente anônimo + plaintext. Requisito básico antes de qualquer deploy de prod.
  • Parâmetros/resultados em formato binário no endpoint PG-wire — o Bind agora decodifica INT2/INT4/INT8 big-endian corretamente (a maioria das chamadas setInt/setLong de ferramentas BI) mas a decodificação binária completa dirigida por tipo (BOOL/DATE/NUMERIC/TIMESTAMP contra os OIDs de parâmetro declarados do statement) é um follow-up.
  • Suspensão de portal — Execute sempre retorna todas as linhas independentemente do pedido maxRows do cliente. Bom para queries BI interativas; importa para paginação de cursor grande.
  • Auto-joins de três viasFROM A, B, C onde A↔B e B↔C ambos existem. Reescritas aninhadas deveriam cascatear mas isso ainda não foi verificado em testes.
  • Joins cross-schema — unir um dataset Ossie com uma tabela não-Ossie (de um sub-schema Calcite diferente). A regra de auto-join desiste quando os dois lados pertencem a schemas diferentes.
  • Esquema de connect jdbc:saiku://. Hoje usuários passam por jdbc:calcite: + um arquivo model.json. Um driver JDBC de primeira parte vem com o trabalho de wire Postgres.
  • Protocolo de wire Postgres. Hoje a superfície é só-JDBC. jdbc:saiku: no wire (para que clientes psql / libpq conectem nativamente) é #1386.

Exemplo trabalhado — cubo Pharma

Dado o YAML Ossie do Pharma que o exportador produz:

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]

Você pode consultá-lo assim:

-- 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;

Todas as cinco rodam inteiramente no Postgres via o pushdown JDBC do Calcite — a JVM nunca vê linhas individuais para os agregados.

Joins auto-injetados

Usuários não precisam lembrar quais colunas ligam ORDERS a CUSTOMERS. Apenas liste ambos os datasets em FROM e o Calcite injetará o predicado ON para você:

-- 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;

A reescrita acontece durante a fase de otimização do Calcite — tudo ainda empurra para baixo ao warehouse como uma única query JOIN.

Quando a regra dispara

  • O join deve ser cartesiano. Cláusulas JOIN … ON … explícitas nunca são sobrescritas — o usuário pediu um predicado específico e nós o respeitamos.
  • Todas as tabelas devem ser datasets Ossie-owned no mesmo OssieSchema. Tabelas cross-schema e não-Ossie deixam a query inalterada.
  • Exatamente uma relationship Ossie deve ligar cada par sendo auto-unido. Múltiplos candidatos levantam AmbiguousJoinException com a lista de nomes candidatos — o usuário deve adicionar um ON explícito para escolher um. Resultados-silenciosamente-errados é o modo de falha contra o qual estamos nos protegendo.

Suporte a N vias

FROM A, B, C, … (três ou mais tabelas em um único cartesiano) também auto-une. A regra caminha para baixo através de Joins aninhados para alcançar cada TableScan cru, depois constrói uma cadeia de join left-deep fresca usando as relationships que ligam cada nova tabela ao conjunto já-unido. Queries em escala Pharma contra fact + múltiplas dims funcionam sem cláusulas JOIN.

Quando não dispara

  • Sem relationship entre dois datasets que precisariam ser ligados → o cartesiano permanece (provavelmente não o que o usuário quer, mas honesto).
  • Self-joins (FROM A a, A b) → deixados como cartesiano.
  • Joins cross-schema misturando tabelas Ossie com não-Ossie.

Views de join

Para cada relationship Ossie, o adaptador registra uma view pré-materializada nomeada <from>_JOIN_<to>. O SQL da view é o JOIN dos dois datasets nas from_columns / to_columns da relationship, e seu row type é a concatenação das colunas de ambos os datasets (com sufixos numéricos em colisões).

Dado este fragmento Ossie:

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

O adaptador expõe PHARMA.fact_pharma_JOIN_Prescriber com a união das colunas de ambas as tabelas. Usuários escrevem:

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

e o Calcite empurra para baixo SELECT * FROM fact_pharma JOIN dim_prescriber ON fact_pharma.prescriberkey = dim_prescriber.prescriberkey WHERE prescribername LIKE 'Dr%' — sem overhead de runtime sobre escrever o JOIN à mão.

Relationships multi-coluna são suportadas: o predicado ON faz AND de cada par from_columns[i] = to_columns[i].

Relacionado