Docs / Build Workflow

Models

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.

Escribe 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.

Prefiere la capa semántica antes que defaultear a SQL crudo

Declara 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. Recurre a bloques grandes de alias.sql("""…""") cuando el SQL legacy es la única forma práctica del dato, o cuando necesitas sustitución @param en SQL crudo (frecuente en Postgres o MySQL — mira Diferencias entre adapters de source). Cuando todo vive dentro de strings opacos de SQL, pierdes reusabilidad entre views y haces los reviews más difíciles.

Modulariza: scope de archivos por dominio

Mantén analítica no relacionada en archivos .malloy separados. Comparte una base estable vía import y extiende una vez por tópico — mira el patrón Reusar un source base abajo. Evita 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; trata esos nombres como APIs. Prefiere labels de campo output que reflejen significado de negocio. Alinea 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 puedes 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.parameters al 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, define 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"

Usa views cuando todas las queries pertenecen al mismo source semántico. Usa queries top-level cuando necesitas referenciar múltiples sources o correr joins cross-source — y recuerda 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. Declara 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).

Para parámetros opcionales, declara el default como null y protege cada uso con coalesce, así un valor ausente colapsa la condición en un no-op. Este es el idiom que funciona en workspaces de producción:

source: ec_revenue(
  p_date_from::date  is null,
  p_date_to::date    is null,
  p_category::string is null
) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  join_one: products is ecommerce.table('bigquery-public-data.thelook_ecommerce.products')
    on product_id = products.id
  dimension: order_date  is created_at::date
  dimension: order_month is order_date.month
  dimension: category    is products.category
  measure: revenue is sum(sale_price)

  view: over_time is {
    where:
      order_date >= coalesce(p_date_from, order_date),
      order_date <= coalesce(p_date_to, order_date),
      category   = coalesce(p_category, category)
    group_by: order_month
    aggregate: revenue
    order_by: order_month asc
  }
}

El filtro del dashboard se ata al parámetro por nombre. La convención es referenciar el nombre externo del parámetro — el nombre declarado sin el prefix p_ (p_cutoff_datecutoff_date). El engine también acepta el nombre interno completo, pero elige la forma sin prefix y mantente consistente:

# in the dashboard YAML
filters:
  - id: global_period
    type: cutoff_date
    granularity: year
    param: cutoff_date    # binds to p_cutoff_date on the source signature
    default: "{{today}}"

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: ec_daily_revenue(
  p_date_from::date is null,
  p_date_to::date   is null
) is ecommerce.sql("""
  SELECT
    DATE(oi.created_at) AS order_date,
    SUM(oi.sale_price)  AS revenue
  FROM `bigquery-public-data.thelook_ecommerce.order_items` oi
  WHERE DATE(oi.created_at) >= COALESCE(CAST(@date_from AS DATE), DATE(oi.created_at))
    AND DATE(oi.created_at) <= COALESCE(CAST(@date_to AS DATE), DATE(oi.created_at))
  GROUP BY 1
  ORDER BY 1
""") extend {
  view: daily 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. El guard COALESCE(CAST(@x AS DATE), fallback) es el idiom que funciona para parámetros de fecha opcionales: cuando el filtro no manda nada, la condición colapsa en un no-op en vez de comparar contra NULL.

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 — shared foundation
##! experimental.parameters

source: ec_orders_base() is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  join_one: products is ecommerce.table('bigquery-public-data.thelook_ecommerce.products')
    on product_id = products.id
  measure: revenue is sum(sale_price)
  measure: order_count is count(order_id)
}

# ec_revenue.malloy — domain model extends the 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
  }
}

Mantén el model base estable. Agrega 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 {...}).

Cuando la base declara parámetros, el source que extiende los re-declara en su propia signature y reenvía cada uno por nombre en la invocación de la base:

source: ec_revenue(
  p_date_from::date is null,
  p_date_to::date   is null
) is ec_orders_base(
  p_date_from is p_date_from,
  p_date_to   is p_date_to
) extend {
  # views here see the parameters through the base's guards
}

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.

Mira 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, prefiere 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. Mira Diferencias entre adapters de source para el patrón completo.

Checklist de calidad de model

  • ##! experimental.parameters arriba de cada archivo .malloy.
  • Cada declaración source: usa paréntesis, incluso cuando están vacíos (source: foo() is …).
  • Cada placeholder @param en SQL crudo tiene una declaración matcheante en la signature del source.
  • El alias del source existe en runtime/sources.runtime.yml (mira 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.
  • Prefiere table + extend + views declarativo antes de apoyarte en sources sql(...) muy grandes, salvo donde shape legacy o patrones @param de Postgres lo requieran.
  • Scope de archivos por un dominio o una base compartida deliberada — evita 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.