Los models son tu API semántica
Poné la lógica de negocio en los models de Malloy, no en el YAML de visualization. Si una métrica sirve en más de un chart, definila una vez en la capa de model y reusala en todos lados. Cambiar la definición de "revenue" debería ser editar una línea en un archivo, no rastrear configs de visualization.
Escribí Malloy idiomático — no solo Malloy válido
Looky requiere un shape de archivo estricto (pragma arriba, sources parametrizados, parámetros en la signature — cubierto en Model mínimo abajo). Más allá del compliance, los workspaces mantenibles usan Malloy para lo que está diseñado: una capa semántica clara sobre tus datos.
Preferí la capa semántica antes que defaultear a SQL crudo
Declará dimensions, measures y joins sobre alias.table(...) o sobre una base importada con extend. Poné la analítica chart-ready en named views o queries top-level. Recurrí a bloques grandes de alias.sql("""…""") cuando el SQL legacy es la única forma práctica del dato, o cuando necesitás sustitución @param en SQL crudo (frecuente en Postgres o MySQL — mirá Diferencias entre adapters de source). Cuando todo vive dentro de strings opacos de SQL, perdés reusabilidad entre views y hacés los reviews más difíciles.
Modularizá: scope de archivos por dominio
Mantené analítica no relacionada en archivos .malloy separados. Compartí una base estable vía import y extendé una vez por tópico — mirá el patrón Reusing a base source abajo. Evitá un solo archivo que crezca sin límite mezclando varios dominios; es difícil reusar, auditar o partir después.
Los nombres estables son el contrato — para teammates y tooling
Las visualizations se atan a path/model.malloy::query_or_view_name; tratá esos nombres como APIs. Preferí labels de campo output que reflejen significado de negocio. Alineá los nombres de parámetros p_* entre models que estén en el mismo dashboard donde representan la misma dimension, así filtros y cross-filtros se comportan predecibles. La semántica estructurada y con nombre es más fácil para gente revisando diffs y para assistants o automatización localizando la definición autoritativa.
Model mínimo que podés shippear hoy
##! experimental.parameters
source: sales() is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
dimension: product is product_name
dimension: order_date is created_at::date
measure: sales_amount is sum(sale_price)
}
query: total_sales is sales() -> {
aggregate: sales_amount
}
query: sales_by_product is sales() -> {
group_by: product
aggregate: sales_amount
order_by: sales_amount desc
limit: 10
}
Dos cosas que cada model necesita:
##! experimental.parametersal tope del archivo. El engine de Looky compila cada model bajo ese flag.- Paréntesis vacíos
()después del nombre del source (source: sales()) y después de cada referencia (sales() -> {...}). Incluso cuando el source no toma parámetros, los paréntesis son requeridos — declaran la lista de parámetros.
El alias del source (ecommerce) tiene que matchear un alias definido en runtime/sources.runtime.yml. Los nombres de query (total_sales, sales_by_product) se vuelven los handles de referencia que se usan en el YAML de visualization.
Views: definir named views dentro del source
Para models más grandes, definí named views dentro del source usando view: en vez de declaraciones query: top-level. Las views viven dentro del source y pueden referenciar sus dimensions y measures directo.
##! experimental.parameters
source: ec_revenue() is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
dimension: category is products.category
dimension: order_month is created_at::month
measure: revenue is sum(sale_price)
measure: order_count is count(order_id)
view: over_time is {
group_by: order_month
aggregate: revenue, order_count
order_by: order_month asc
}
view: by_category is {
group_by: category
aggregate: revenue, order_count
order_by: revenue desc
limit: 12
}
}
Una visualization referencia una view exactamente igual que a una query top-level:
query: "models/ec_revenue.malloy::over_time"
Usá views cuando todas las queries pertenecen al mismo source semántico. Usá queries top-level cuando necesitás referenciar múltiples sources o correr joins cross-source — y acordate de invocar al source con () en la forma top-level (query: x is ec_revenue() -> {...}).
Parámetros: cablear filtros de dashboard a queries de model
Los filtros de dashboard controlan queries vía parámetros. Declará el parámetro en la signature del source (entre los paréntesis) y el filtro del dashboard pasa su valor en tiempo de render.
##! experimental.parameters
source: ec_orders(
p_cutoff_date::date is @2024-01-01
) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
measure: revenue is sum(sale_price) ? created_at <= p_cutoff_date
view: kpi is {
aggregate: revenue
}
}
Cada parámetro lleva un nombre, un tipo y un default — p_cutoff_date::date is @2024-01-01. Tipos comunes: date, string, number, boolean. El default es lo que el engine usa cuando el dashboard no provee un valor (y lo que looky validate usa para el dry-run del model).
El filtro del dashboard se ata al parámetro por nombre:
# en el YAML del dashboard
filters:
- id: global_period
type: cutoff_date
granularity: year
param: p_cutoff_date # tiene que matchear el nombre del parámetro en el model
default: ""
Cuando un usuario cambia el filtro, el valor del parámetro se pasa a cada query del dashboard que lo referencia.
Contrato estricto de @param para sources de SQL crudo
Si tu source se construye desde SQL crudo (connection.sql("""…""")) y el SQL usa placeholders @param, cada @param tiene que tener una declaración matcheante en la signature del source. No hay fallback implícito — los placeholders no declarados hacen fallar el validate con un error claro apuntando a la declaración faltante.
##! experimental.parameters
source: nv_asesor_kpi_sql(
p_date_from::date is null,
p_date_to::date is null
) is natural_vitality_ventas.sql("""
WITH date_range AS (
SELECT
COALESCE(CAST(@date_from AS DATE), DATE '2026-02-01') AS sd,
COALESCE(CAST(@date_to AS DATE), DATE '2026-02-28') AS ed
)
-- ...
""") extend {
view: kpi_asesor_resumen is { select: * }
}
Los placeholders @date_from y @date_to en el SQL matchean las declaraciones p_date_from y p_date_to en la signature del source (el prefix p_ es convencional). El filtro del dashboard pasa el valor en tiempo de render; el validate usa el default declarado.
Reusar un source base entre models
El patrón es: definir un source base paramétrico una vez y extenderlo en cada model de dominio. Esto evita duplicar definiciones de join, declaraciones de dimension y declaraciones de parámetro en cada archivo.
# ec_orders_base.malloy — base compartida
##! experimental.parameters
source: ec_orders_base() is ecommerce.table('...order_items') extend {
join_one: products is ecommerce.table('...products') on product_id = products.id
measure: revenue is sum(sale_price)
measure: order_count is count(order_id)
}
# ec_revenue.malloy — model de dominio extiende la base
##! experimental.parameters
import "ec_orders_base.malloy"
source: ec_revenue() is ec_orders_base() extend {
view: by_category is {
group_by: products.category
aggregate: revenue
limit: 12
}
}
Mantené el model base estable. Agregá views nuevas en archivos por dominio, no en la base. Tanto el source base como el que extiende necesitan sus propios () — y el source que extiende tiene que invocar a la base con () también (is ec_orders_base() extend {...}).
Cómo se resuelve el alias del source de un model
Un model de Malloy usa un alias de conexión cuando llama a alias.table(...) o alias.sql(...). Looky matchea cada alias contra las declaraciones de runtime/sources.runtime.yml del workspace.
- Si el model usa un alias, Looky lo elige automáticamente.
- Si el model usa varios aliases (directo o a través de imports), el run tiene que decir cuál usar; si no, se rechaza.
Mirá Sources para la sintaxis de declaración de alias por adapter.
Notas de adapter para autores de model
Looky bundlea una versión específica y pineada de Malloy con los adapters de BigQuery, Postgres y MySQL. No hay extensiones específicas de Looky a Malloy más allá de lo que esa versión de Malloy documenta para esos adapters.
La diferencia más común entre adapters en models es alrededor de parámetros de date y timestamp. En Postgres y MySQL, preferí el patrón de placeholder @param en SQL crudo (apareado con una declaración matcheante en la signature del source, como se muestra arriba) así Looky sustituye el valor dentro del string de SQL en vez de bindearlo nativo. Mirá Diferencias entre adapters de source para el patrón completo.
Checklist de calidad de model
##! experimental.parametersarriba de cada archivo.malloy.- Cada declaración
source:usa paréntesis, incluso cuando están vacíos (source: foo() is …). - Cada placeholder
@paramen SQL crudo tiene una declaración matcheante en la signature del source. - El alias del source existe en
runtime/sources.runtime.yml(mirá Sources). - Los nombres de view y query son estables — renombrarlos rompe las referencias de visualization.
- Los nombres de campo reflejan significado de negocio, no necesidades de formato de chart.
- Los parámetros se declaran con defaults razonables así las queries funcionan sin un filtro.
- Cada view o query puede ser reusada por múltiples visualizations.
- Preferí
table+extend+ views declarativo antes de apoyarte en sourcessql(...)muy grandes, salvo donde shape legacy o patrones@paramde Postgres lo requieran. - Scope de archivos por un dominio o una base compartida deliberada — evitá meter queries no relacionadas en un único archivo monolítico
.malloy.
looky validate
looky diff
No empieces trabajo de visualization hasta que la validación de models esté limpia.