Pular para o conteúdo

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:

  1. Parse — lê os arquivos .lkml em um modelo (um parser LookML vendored e endurecido).
  2. Classify — um gate de segurança estático marca cada explore e field CLEAN / DEGRADE / REFUSE, sem acesso ao data warehouse.
  3. 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.
  4. 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.

Terminal window
lookml-report report <path> [-o report.md] [--json report.json] [--fail-on-refuse]
FlagEfeito
(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-refuseAinda 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:

  1. Cada .lkml é descoberto recursivamente e parseado independentemente (arquivos unparseable e *.dashboard.lkml são listados, nunca fatais).
  2. 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 / join mesclam por nome);
    • @{constant} — substituído de blocos constant:.

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:

Terminal window
lookml-report report --explore-json explore.json

Exporte 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:

LookMLMondrian-4Veredicto
explore (estrela/snowflake de base única)<Cube> com um <MeasureGroup> e dimensões conformantesCLEAN
explore juntando múltiplas bases de fato (conformantes)um <Cube> com um <MeasureGroup> por base de fato sobre dimensões conformantes compartilhadasCLEAN
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 conformanteCLEAN
measure: { type: sum | count | min | max | average | count_distinct }<Measure> com o agregador correspondenteCLEAN
measure: { type: median | percentile }aggregator="median" / aggregator="percentile"DEGRADE — precisa de backend capaz de PERCENTILE_CONT
medida filtrada (filters: igualdade, sem Liquid)calculated memberCLEAN
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 viewCLEAN
Liquid limitado: {% parameter %}, {% condition %}, {{ _user_attributes['x'] }}<QueryParameter> / binding de predicate-grantDEGRADE
access_filter em uma chave de dimensão modeladaMember grant de RoleCLEAN
access_filter em uma coluna de fato arbitráriaRole de predicate-grant + parâmetro vinculadoDEGRADE
derived_tableuma tabela física sustentada por SQL (<Query>)DEGRADE — política de persistência descartada
drill_fieldsset 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ê)

RecusadoPor 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ávelContaria duplo silenciosamenteAdicione 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 deduplicarPara 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ãoUm 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-hopModele o join com uma dimensão bridge (many-to-many)
type: listSem 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 LookerPOST /api/4.0/login depois /api/4.0/queries/run/json retorna 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 categorizadas ROW_COUNT, DIMENSION_SET ou MEASURE_VALUE e 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 chaves sql_on — dimensões degeneradas de view base e wiring copy/no_link cross-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.