Migrar do Looker (LookML)
O importador LookML carrega modelos LookML do Looker para o Saiku como schemas Mondrian-4 para que você possa rodar OLAP/MDX sobre eles. É um acelerador de migração com um gate de segurança rígido, não um conversor sem perdas — cada construct é classificado em um de três veredictos:
- CLEAN — portado para um elemento Mondrian.
- DEGRADE — portado, mas uma capacidade foi perdida (e nomeada precisamente).
- REFUSE — não portado, com um diagnóstico exato do porquê.
Como funciona
O importador é um pipeline de quatro etapas:
- Parse — lê os arquivos
.lkmlem um modelo (um parser LookML vendored e endurecido). - Classify — um gate de segurança estático marca cada explore e field CLEAN / DEGRADE / REFUSE, sem acesso ao data warehouse.
- Transpile — emite um schema Mondrian-4 para o subset CLEAN/DEGRADE (como YAML, carregável diretamente pelo Saiku), mais um mapa de proveniência de qual field LookML produziu qual elemento de schema.
- Report — um relatório de cobertura (Markdown + JSON) bucketing cada construct, com razões de resumo.
Apenas constructs que o gate aceita chegam ao transpiler, então uma medida recusada e silenciosamente errada nunca pode vazar para o cubo emitido.
Executar o importador
O importador é exposto como a ferramenta mondrian.lookml.report.LookmlReportCli (do mesmo jeito que a CLI de Schema envolve SchemaCli). Aponte-o para um único arquivo .lkml ou um diretório de projeto inteiro — diretórios são scaneados recursivamente para arquivos .lkml.
lookml-report report <path> [-o report.md] [--json report.json] [--fail-on-refuse]| Flag | Efeito |
|---|---|
| (nenhuma) | Imprime o relatório Markdown no stdout. |
-o <file> | Escreve o relatório Markdown em um arquivo. |
--json <file> | Também escreve o relatório JSON machine-readable. |
--fail-on-refuse | Ainda emite o relatório, depois sai com código não-zero se alguma coisa foi recusada — um gate de CI para “bloquear a migração até a lista de recusas estar vazia.” |
Códigos de saída: 0 sucesso · 1 argumentos ruins · 2 o caminho está faltando/ilegível, nada foi parseable, ou --fail-on-refuse viu uma recusa.
O que o relatório contém
- Métricas de resumo — contagens e percentuais CLEAN / DEGRADE / REFUSE em ambos a granularidade de explore e field. Essa razão é o número de prontidão de migração principal.
- Buckets por construct — cada explore e field listado sob Clean / Degrade / Refuse com uma razão precisa, o elemento M4 produzido (para clean/degrade) ou a capacidade perdida, e um link para o recurso do Saiku que removeria uma recusa.
- Arquivos unparseable / pulados — transparência total sobre qualquer coisa não ingerida.
Projetos multi-arquivo
Aponte o importador para um diretório de projeto e ele resolve o projeto inteiro, não apenas um arquivo:
- Cada
.lkmlé descoberto recursivamente e parseado independentemente (arquivos unparseable e*.dashboard.lkmlsão listados, nunca fatais). - Os objetos parseable de nível superior são mesclados, depois uma etapa de flatten resolve referências cross-file em um modelo antes da classificação:
include:— satisfeito pela mesclagem;extends:— a base é copiada, depois as próprias propriedades do objeto estendendo sobrescrevem;- refinamentos (
+view/+explore/+model) — colocados em camadas sobre a base (escalares sobrescrevem;dimension/measure/joinmesclam por nome); @{constant}— substituído de blocosconstant:.
Isso conserta más classificações onde uma medida ou field só se torna aditiva, Liquid ou row-secured após um refinamento — em um projeto real, isso moveu centenas de fields de um palpite de texto literal para o veredicto correto.
Importar de uma instância Looker ao vivo (Explore JSON)
Com uma instância Looker credenciada, você pode pular o parsing bruto de .lkml e importar os metadados já resolvidos de um explore:
lookml-report report --explore-json explore.jsonExporte o JSON lookml_model_explore do explore pela API do Looker. Como o Looker já aplicou todos os extends/refinamentos/constants/Liquid, esta entrada pula a etapa de flatten e alimenta direto no mesmo pipeline classifier → transpiler → report, produzindo o mesmo relatório de cobertura que o .lkml equivalente. Trade-off: requer acesso à API e é por explore, então complementa — em vez de substituir — o caminho offline de apontar para um repo git.
O que porta
A maior parte de um modelo LookML estrela/snowflake limpo converte diretamente. O importador mapeia:
| LookML | Mondrian-4 | Veredicto |
|---|---|---|
explore (estrela/snowflake de base única) | <Cube> com um <MeasureGroup> e dimensões conformantes | CLEAN |
explore juntando múltiplas bases de fato (conformantes) | um <Cube> com um <MeasureGroup> por base de fato sobre dimensões conformantes compartilhadas | CLEAN |
join: { relationship: many_to_one | one_to_one, type: left_outer } | ForeignKeyLink (dims degeneradas → FactLink) | CLEAN |
bridge two-hop: fato one_to_many (ou many_to_many) → view bridge → view dimensão many_to_one | <BridgeLink> (deduplicação full-count) + a dimensão como dimensão conformante | CLEAN |
measure: { type: sum | count | min | max | average | count_distinct } | <Measure> com o agregador correspondente | CLEAN |
measure: { type: median | percentile } | aggregator="median" / aggregator="percentile" | DEGRADE — precisa de backend capaz de PERCENTILE_CONT |
medida filtrada (filters: igualdade, sem Liquid) | calculated member | CLEAN |
dimension (+ value_format / value_format_name, label, description) | atributo / level (+ formatString — formatos nomeados do Looker como usd, percent_2, decimal_0 são traduzidos para máscaras Mondrian; um formato nomeado desconhecido é mantido literalmente com uma nota DEGRADE — mais caption, description) | CLEAN |
dimension: { type: tier } / dimension_group: { type: duration } | nativo <Tier> / <Duration> | CLEAN |
parameter (limitado: tipado, allowed_values) | <QueryParameter> | CLEAN |
measure: { type: sum_distinct | average_distinct }, sql_distinct_key resolve para uma coluna na própria view da medida (incl. uma não-primary-key) | <Measure> com um grão distinto no nível da medida (distinctKeyColumn) — deduplicado nessa chave antes de agregar; colapsa para um sum/avg plano quando a chave é a primary key da view | CLEAN |
Liquid limitado: {% parameter %}, {% condition %}, {{ _user_attributes['x'] }} | <QueryParameter> / binding de predicate-grant | DEGRADE |
access_filter em uma chave de dimensão modelada | Member grant de Role | CLEAN |
access_filter em uma coluna de fato arbitrária | Role de predicate-grant + parâmetro vinculado | DEGRADE |
derived_table | uma tabela física sustentada por SQL (<Query>) | DEGRADE — política de persistência descartada |
drill_fields | set RETURN de drillthrough, carregado como annotation de cubo (M4 não tem elemento <DrillThrough> de schema — é um statement runtime DRILLTHROUGH … RETURN) | CLEAN |
aggregate_table | (não convertido — o Saiku regenera agregados) | DEGRADE |
Uma medida sum/average que se abre em leque por um join one_to_many é portada (CLEAN) quando a view base declara uma primary key, porque a agregação simétrica fan-out-safe do Saiku deduplica nesse grão. Sem uma chave declarada, é recusada em vez de arriscar double-counting.
Joins many-to-many (dimensões bridge). O importador reconhece o bridge two-hop canônico do LookML — um fato unido one_to_many (ou many_to_many) a uma view bridge, que por sua vez se une many_to_one a uma view de dimensão — e o mapeia para um <BridgeLink> do Mondrian em vez de recusar o explore como não-estrela. A dimensão alcançada pelo bridge se torna uma dimensão conformante normal, e medidas no fato retornam o total deduplicado (full-count), não o fanned-out. Um bridge é emitido apenas quando cada hop reduz a uma chave de coluna única e a view de fato declara um grão primary_key: yes; uma chave de join composta/ambígua, ou um fato sem primary key, é deixada recusada em vez de produzir um cubo silenciosamente errado. Como o LookML não carrega peso de alocação, bridges default para deduplicação full-count.
Joins com alias from:. No LookML, um field unido é sempre referenciado pelo nome do join, enquanto from: (ou view_name:) só troca a view física subjacente. O importador casa o sql_on ${name.column} de cada join contra o nome do join — resolvendo as colunas e tabela da dimensão a partir da view subjacente — então um join como join: current_subscription_state { from: logical_subscriptions; sql_on: ${fact.fk} = ${current_subscription_state.id} } porta CLEAN como uma dimensão conformante nomeada pelo join. Dois joins que from: a mesma view base se tornam duas dimensões conformantes distintas (ex.: account_csm e account_owner sobre uma tabela account). O que ainda degrada (DEGRADE_JOIN_SQL_ON_UNPARSEABLE): um sql_on que não é uma igualdade de coluna única em cada lado — joins de constante/metadado (${meta.col} = 'literal') e joins compostos ou de expressão (chaves multi-coluna em cadeia AND, coalesce(...), casts ::date) são mantidos degradados em vez de reduzidos a uma única chave silenciosamente errada.
O que é recusado (e por quê)
| Recusado | Por quê | Caminho à frente |
|---|---|---|
Liquid computado ({% if %} / loops / assign / {{ }} computado em SQL) | SQL gerado em runtime é um buraco de correção/segurança que não importaremos | {% parameter %} / {% condition %} / {{ _user_attributes['x'] }} limitados agora portam (DEGRADE) |
Fan-out sum/average sem grão declarável | Contaria duplo silenciosamente | Adicione uma dimensão primary_key: yes na view base |
Topologias não-estrela: joins full_outer / cross, ou um many_to_many cujo bridge two-hop não pode ser recuperado (chave de join composta/ambígua, ou a view de fato não tem primary_key) | Quebram estruturalmente / abrem em leque sem controle, ou não há grão de coluna única para deduplicar | Para um many-to-many: dê à view de fato um primary_key: yes e chaves de join de coluna única para que o importador possa emitir um bridge (o two-hop recuperável agora porta automaticamente) |
type: sum_distinct / average_distinct cujo sql_distinct_key é uma chave cross-view (${other_view.field}), foreign-key ou de expressão | Um grão distinto no nível da medida só pode deduplicar em uma coluna na própria view de fato da medida; uma chave cross-view precisa do bridge two-hop | Modele o join com uma dimensão bridge (many-to-many) |
type: list | Sem equivalente multidimensional | — |
Extensões Mondrian++ ampliam cobertura
As extensões semânticas do Saiku existem em parte para estreitar a lista de recusas — cada uma transforma uma recusa anterior em uma porta limpa:
- Agregação simétrica (fan-out-safe), dimensões bridge (many-to-many) e grão distinto no nível da medida — veja Avançado.
- Agregadores median / percentile, tipos de dimensão nativos tier / duration — veja Dimensões e Cubos e medidas.
- Parâmetros limitados de contexto de consulta e row security baseada em predicado — veja Controle de acesso.
Conforme mais extensões aterrissam, o mesmo modelo LookML classifica mais limpo — rerode o relatório para ver a razão melhorar.
Cobertura no mundo real
Validado contra um corpus de projetos LookML públicos (blocks oficiais do Looker mais modelos de produção da comunidade, ~900 arquivos .lkml): ~99.8% dos arquivos in-scope parseiam, cada projeto produz um relatório de projeto inteiro, e em ~20.000 fields em modelos reais a cobertura é ~97.6% CLEAN, ~1.2% DEGRADE, ~1.2% REFUSE. As recusas residuais são dominadas por Liquid genuinamente computado (inerentemente dinâmico — um “não vai portar por design” em vez de um bug).
Validar equivalência numérica
O relatório de cobertura diz o que porta; ele não diz se o cubo convertido retorna os mesmos números que o Looker. Um port CLEAN que soma errado é o pior resultado, então um harness de equivalência separado checa o cubo convertido contra uma instância Looker ao vivo — o análogo de migração ao guard de paridade Calcite do lado do engine.
Dada uma especificação de consulta (um explore mais os fields de dimensão e medida), o harness roda a consulta de ambas as formas e compara os resultados:
- Lado Looker —
POST /api/4.0/logindepois/api/4.0/queries/run/jsonretorna as linhas como o oráculo. Aponte-o para sua instância com três configurações (propriedades de sistema ou variáveis de ambiente — nunca as commite):LOOKER_BASE_URL,LOOKER_CLIENT_ID,LOOKER_CLIENT_SECRET(uma chave API3 do Looker). Sem todas as três, o cliente está inerte e o harness permanece totalmente offline. - Lado Saiku — a mesma especificação é reescrita para MDX sobre o cubo convertido usando o mapa de proveniência do transpiler (medidas em colunas, levels de dimensão em linhas). Fields que o importador não converteu CLEAN são listados como pulados, nunca silenciosamente comparados.
- Comparação — linhas são alinhadas por sua tupla de chave de dimensão e medidas comparadas dentro de uma tolerância relativa (default
1e-6). Divergências são categorizadasROW_COUNT,DIMENSION_SETouMEASURE_VALUEe nomeiam apenas o field e categoria — nunca os valores subjacentes (sem dados em logs). Um run limpo reporta um match com zero divergências.
O harness também valida row security: um resultado restrito por access_filter do Looker é comparado contra o cubo convertido consultado sob o grant de role correspondente, confirmando que os números restritos casam.
Limitações (v1)
- Resolução cross-file de
include:,extends:/refinamentos e constants@{}é tratada por uma etapa de flatten (veja Projetos multi-arquivo); uma referência cuja base ou constant está fora do conjunto de arquivos descoberto é reportada como um diagnóstico e deixada como parseada em vez de resolvida. Para um modelo totalmente pré-resolvido, use o front-end Explore-JSON. - Explores conformantes multi-base portam para um cubo com um
<MeasureGroup>por base de fato; v1 liga cada measure group secundário de fato apenas às dimensões conformantes que suas próprias chavessql_on— dimensões degeneradas de view base e wiringcopy/no_linkcross-fact ainda não são sintetizadas. *.dashboard.lkml(dashboards Looker estruturados em YAML) são pulados — não fazem parte do modelo de cubo.
Veja Schemas YAML para o formato que o importador emite, e a CLI de Schema para converter ou fazer lint do resultado antes de implantá-lo no Saiku.