Conversão de moeda (FX declarativo via <CurrencyConversion>)
O Mondrian-4 suporta um elemento de schema <CurrencyConversion> que produz uma medida convertida como SUM(base_measure × exchange_rate), escolhendo automaticamente a taxa correta na data da própria linha de fato via uma tabela de taxas com intervalos de validade não sobrepostos. Você declara o que você quer; o engine compila o band join e a agregação para você.
Por que conversão de moeda declarativa?
Sem <CurrencyConversion>, converter uma medida para uma moeda diferente exige um calculated member manual que precisa reimplementar a busca por data efetiva sempre que a estrutura da tabela de taxas muda:
<CalculatedMember name="Revenue (USD)" dimension="Measures"> <Formula> Sum( Existing [Calendar].[Month].Members, [Measures].[Revenue] * LookupCube("[fx_rate]", ...) ) </Formula></CalculatedMember>Com <CurrencyConversion> a mesma métrica é:
<CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/>currency_conversions:- name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"O loader valida que a medida base nomeada e todas as colunas referenciadas existem, gera o band join no momento da carga e dispara um erro claro em vez de produzir um resultado silenciosamente errado.
Posição no schema
Elementos <CurrencyConversion> são envoltos em um bloco <CurrencyConversions> dentro de um <MeasureGroup>, como irmão de <Measures> e <DimensionLinks>:
<Cube name="Monthly Revenue">
<MeasureGroups> <MeasureGroup name="Revenue" table="mm_monthly">
<Measures> <Measure name="Revenue" column="revenue" aggregator="sum"/> </Measures>
<DimensionLinks> <ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/> </DimensionLinks>
<CurrencyConversions> <CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/> </CurrencyConversions>
</MeasureGroup> </MeasureGroups>
</Cube>measure_groups:- name: "Revenue" table: "mm_monthly" measures: - name: "Revenue" column: "revenue" aggregator: "sum" dimension_links: - type: "foreign_key" dimension: "Calendar" foreign_key_column: "month_key" currency_conversions: - name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Referência de atributos
| Atributo | Chave XML | Chave YAML | Obrigatório | Descrição |
|---|---|---|---|---|
name | name | name | sim | O nome da medida convertida gerada. Aparece em [Measures] como qualquer outra medida. |
measure | measure | measure | sim | O nome de um <Measure> existente no mesmo measure group (o valor base a multiplicar). |
rateTable | rateTable | rate_table | sim | Nome da tabela física que segura as taxas de câmbio. Deve existir no schema físico. |
rateColumn | rateColumn | rate_column | sim | Coluna em rateTable que segura a taxa numérica de câmbio. |
rateType | rateType | rate_type | sim | Valor literal usado para filtrar a tabela de taxas a uma fonte (ex.: "ECB", "BLOOMBERG"). |
rateTypeColumn | rateTypeColumn | rate_type_column | sim | Coluna em rateTable que segura o discriminador de tipo de taxa. |
factCurrencyColumn | factCurrencyColumn | fact_currency_column | sim | Coluna na tabela de fato que identifica a moeda de origem de cada linha. |
rateCurrencyColumn | rateCurrencyColumn | rate_currency_column | sim | Coluna em rateTable que identifica a moeda da qual a taxa converte. |
factDateColumn | factDateColumn | fact_date_column | sim | Coluna na tabela de fato usada para determinar qual intervalo de taxa se aplica. |
rateValidFromColumn | rateValidFromColumn | rate_valid_from_column | sim | Coluna em rateTable para o início do intervalo (inclusivo). |
rateValidToColumn | rateValidToColumn | rate_valid_to_column | sim | Coluna em rateTable para o fim do intervalo (exclusivo). |
formatString | formatString | format_string | não | Format string MDX para a medida convertida, ex.: "#,##0.00" ou "$#,##0". |
Contrato de dados do intervalo por data efetiva
A tabela de taxas segura uma linha por combinação (currency, rate_type) por período de validade. O engine executa um band join que seleciona exatamente a taxa cujo intervalo contém a data da linha de fato:
fact.factDateColumn >= rate.rateValidFromColumnAND fact.factDateColumn < rate.rateValidToColumnAND fact.factCurrencyColumn = rate.rateCurrencyColumnAND rate.rateTypeColumn = '<rateType literal>'O join é inerentemente 1-para-1 — uma linha de fato corresponde no máximo a uma linha de taxa — desde que os intervalos da tabela de taxas sejam não sobrepostos. Isso significa que a agregação da medida convertida é simplesmente SUM(fact.measure × rate.rate), e qualquer medida não convertida consultada ao lado dela fica completamente intacta.
Estrutura recomendada da tabela de taxas
CREATE TABLE fx_rate ( currency_id VARCHAR(3) NOT NULL, -- e.g. 'EUR' rate_type VARCHAR(16) NOT NULL, -- e.g. 'ECB' rate DECIMAL(18,6) NOT NULL, valid_from DATE NOT NULL, -- inclusive valid_to DATE NOT NULL, -- exclusive PRIMARY KEY (currency_id, rate_type, valid_from));Use valid_to = '9999-12-31' (ou o próximo valid_from do próximo período) como sentinela para o intervalo aberto atual para que o predicado < valid_to sempre funcione limpo.
Requisito de backend e validação fail-closed
A validação é fail-closed: a carga do schema é abortada com uma mensagem de erro clara para qualquer das seguintes condições.
| Condição | Erro |
|---|---|
measure não nomeia uma medida existente no measure group | CurrencyConversion "X": measure "Y" not found in measure group |
rateTable não existe no schema físico | CurrencyConversion "X": rateTable "Y" not found in physical schema |
| Qualquer das colunas nomeadas não existe na tabela referenciada | CurrencyConversion "X": column "Y" not found in table "Z" |
| Backend Calcite não está ativo | CurrencyConversion "X": requires Calcite backend |
Não há resultado silenciosamente errado — cada configuração ruim é pega antes da primeira consulta rodar.
Exemplo trabalhado: receita mensal do demo Bank
O demo Bank traz um cubo Monthly Revenue cujas linhas de fato mm_monthly são denominadas em EUR. Uma tabela fx_rate segura duas taxas ECB anuais:
| currency_id | rate_type | rate | valid_from | valid_to |
|---|---|---|---|---|
| EUR | ECB | 1.10 | 2024-01-01 | 2025-01-01 |
| EUR | ECB | 1.20 | 2025-01-01 | 2026-01-01 |
Os dados brutos de fato:
| Year | Revenue (EUR) |
|---|---|
| 2024 | 600 |
| 2025 | 750 |
| Total | 1350 |
Declaração no schema
<MeasureGroup name="Revenue" table="mm_monthly">
<Measures> <Measure name="Revenue" column="revenue" aggregator="sum"/> </Measures>
<DimensionLinks> <ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/> </DimensionLinks>
<CurrencyConversions> <CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/> </CurrencyConversions>
</MeasureGroup>- name: "Revenue" table: "mm_monthly" measures: - name: "Revenue" column: "revenue" aggregator: "sum" dimension_links: - type: "foreign_key" dimension: "Calendar" foreign_key_column: "month_key" currency_conversions: - name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Resultados de ouro
| Linha | Revenue (EUR) | Revenue (USD) | Cálculo |
|---|---|---|---|
| 2024 | 600 | 660 | 600 × 1.10 (taxa ECB 2024) |
| 2025 | 750 | 900 | 750 × 1.20 (taxa ECB 2025) |
| Grand total | 1350 | 1560 | 660 + 900 |
As linhas de 2024 e 2025 usam taxas diferentes porque o band join escolhe a taxa cujo intervalo [valid_from, valid_to) contém o month_key de cada linha de fato. O Revenue (EUR) base não é afetado e permanece 1350.
Consulta MDX de exemplo
SELECT { [Measures].[Revenue], [Measures].[Revenue (USD)] } ON COLUMNS, [Calendar].[Year].Members ON ROWSFROM [Monthly Revenue]Resultado:
| Year | Revenue (EUR) | Revenue (USD) |
|---|---|---|
| 2024 | 600 | 660.00 |
| 2025 | 750 | 900.00 |
| All Time | 1,350 | 1,560.00 |
Relação com <CalculatedMembers>
<CurrencyConversion> compila para uma agregação SQL nativa (SUM(fact.revenue × rate.rate)) em vez de um <CalculatedMember> MDX. Isso significa:
- A conversão é avaliada no banco, não no engine MDX — é tão rápida quanto qualquer medida agregada ordinária.
- A medida convertida aparece em enumerações de membros XMLA ao lado de medidas regulares.
- Pode ser referenciada por fórmulas de
<CalculatedMember>(ex.: para computar uma margem na moeda alvo).
Se você precisa de uma fórmula que <CurrencyConversion> não pode expressar — por exemplo, converter apenas uma fatia de uma medida ou misturar taxas de múltiplas fontes — use um <CalculatedMember> plano junto com uma busca scripted separada. As duas abordagens podem coexistir no mesmo cubo.
Veja Avançado — Calculated members para a referência completa de <CalculatedMember>.