Docs / Build Workflow

Soporte de Malloy

La versión de Malloy que corre Looky

Looky shippea una versión específica y pineada de Malloy — actualmente @malloydata/malloy 0.0.346, con los adapters de BigQuery, Postgres y MySQL en la misma versión. Cualquier sintaxis de Malloy más allá de lo que esa versión soporta no se entiende; cualquier feature agregado en releases posteriores de Malloy no está disponible hasta que Looky actualice.

No hay extensiones específicas de Looky a Malloy. El dialecto que escribes es lo que Malloy mismo documenta — ni más, ni menos.

Adapters soportados

  • BigQuery — conecta con un JSON key de service account.
  • Postgres — conecta con un connection string libpq (host / port / database, más flags TLS o de pool) y un secret de user/password.
  • MySQL — conecta con un connection string mysql://host:port/database y un secret de user/password. Dos detalles: las conexiones no van encriptadas (todavía no hay opción TLS), y MySQL no tiene un tipo boolean real, así que las columnas boolean vuelven como números — castea explícitamente cuando necesites un filtro true/false.

No hay otros adapters bundleados. Mira Sources para el schema YAML del source por adapter.

Cómo se cargan los models

El content de cada workspace está rooteado en workspaces/<billing>/<slug>/content/. Looky resuelve los archivos .malloy relativos a ese root.

Las declaraciones import "..." dentro de un model se resuelven contra archivos .malloy hermanos en el mismo directorio. Usa imports para compartir una declaración de source base entre múltiples models de dominio — declara el source y sus joins una vez, extendelo por tópico.

# models/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)
}

# models/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
  }
}

Cada model necesita el pragma ##! experimental.parameters arriba y paréntesis en cada declaración de source (y en cada referencia a un source) — incluso cuando el source no toma parámetros. Los paréntesis vacíos () declaran la lista de parámetros.

Aliases de source dentro de los models

Un model 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ático.
  • Si el model usa varios aliases (directo o vía 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.

Parámetros con nombre

Los valores de filtro y cross-filter llegan a una query a través de parámetros con nombre declarados en la signature del source, entre los paréntesis. Cada parámetro lleva un nombre, un tipo y un default — p_country::string is "all".

Hay una regla de naming: los parámetros cuyo nombre Malloy empieza con p_ tienen el prefix strippeado para el nombre externo. Un parámetro del model declarado como p_start_date se setea mandando start_date desde el filtro del dashboard. (El engine también acepta el nombre interno completo, pero la forma sin prefix es la convención que sigue cada ejemplo acá.)

##! experimental.parameters

source: ec_orders(
  p_country::string  is "all",
  p_start_date::date is @2024-01-01
) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  join_one: users is ecommerce.table('bigquery-public-data.thelook_ecommerce.users')
    on user_id = users.id
  dimension: country is users.country

  view: revenue is {
    where:
      (p_country = "all" or country = p_country)
      and created_at::date >= p_start_date
    aggregate:
      revenue is sum(sale_price)
  }
}

Un filtro select del dashboard con param: country entonces bindea un valor elegido como "MX" a p_country; un click de cross-filter en una barra de country hace exactamente lo mismo sin ninguna declaración. Usa el mismo nombre de parámetro para la misma dimension en cada model del dashboard — eso es lo que hace que un filtro o un click los estreche a todos juntos.

Los parámetros pueden referenciarse de dos maneras dentro del cuerpo del source:

  • Expresiones Malloy — escribe p_country, p_start_date directo en where:, aggregate:, etc. (el ejemplo de arriba).
  • Placeholders @param en SQL crudo — cuando el cuerpo del source se construye desde SQL crudo (connection.sql("""…""")), usa placeholders @param dentro del bloque SQL. Cada @param tiene que tener una declaración matcheante en la signature del source; si no, validate falla con un error claro.

Parámetros de date y timestamp en Postgres y MySQL

Postgres y MySQL comparten una limitación conocida cuando Malloy bindea un parámetro de date o timestamp nativo en algunos shapes de query. El camino confiable en ambos es el patrón @param en SQL crudo: arma el source desde connection.sql("""…"""), referencia placeholders @param dentro del SQL, y declara cada placeholder en la signature del source.

##! experimental.parameters

source: orders(
  p_date_from::date is null,
  p_date_to::date   is null
) is warehouse.sql("""
  select *
  from orders
  where (@date_from::date is null or created_at::date >= @date_from::date)
    and (@date_to::date   is null or created_at::date <= @date_to::date)
""") extend {
  view: revenue is {
    aggregate: revenue is sum(sale_price)
  }
}

Looky sustituye los placeholders @date_from / @date_to con valores literales (o NULL tipado cuando el dashboard no proveyó un valor). La sustitución funciona idéntica en los tres adapters — pero el cuerpo SQL corre en el dialecto propio del source, y los casts ::date de este ejemplo son sintaxis Postgres. En MySQL escribe CAST(@date_from AS DATE) en su lugar; en BigQuery COALESCE(CAST(@date_from AS DATE), …). Mira la comparación de adapters de source para la lista completa de divergencias.

(warehouse acá es el alias de conexión propio del workspace desde runtime/sources.runtime.yml — los nombres de alias son tuyos, no los nombres de los adapters.)

Cache y parámetros

Cada entry cacheada está scoped a una query en un model con una combinación de parámetro específica. Un usuario filtrando por "MX" obtiene un resultado cacheado para esa combinación específica; un usuario filtrando por "AR" dispara una entry de cache separada la primera vez, después hace hit al cache en cargas posteriores.

Una entry cacheada vive hasta que su TTL expira o el archivo de model cambia. Editar el archivo .malloy invalida cada entry cacheada para queries adentro en el próximo request. Mira Cache para el shape del cache sidecar.

Patrones trabajados

Parámetro string con un sentinel "all"

##! experimental.parameters

source: ec_orders(
  p_status::string is "all"
) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  view: detail is {
    where: p_status = "all" or status = p_status
    select: *
  }
}

Parámetro opcional con default null y guard coalesce

El otro idiom que funciona para "sin valor significa sin filtro" — usado por todos los workspaces de producción:

##! experimental.parameters

source: ec_orders(
  p_date_from::date is null,
  p_date_to::date   is null
) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  dimension: order_date is created_at::date
  view: revenue is {
    where:
      order_date >= coalesce(p_date_from, order_date),
      order_date <= coalesce(p_date_to, order_date)
    aggregate: revenue is sum(sale_price)
  }
}

Par de parámetros de date range (estilo @param en SQL crudo, dialecto MySQL)

##! experimental.parameters

source: orders(
  p_date_from::date is null,
  p_date_to::date   is null
) is shop.sql("""
  SELECT o.sale_price, o.created_at
  FROM orders o
  WHERE (CAST(@date_from AS DATE) IS NULL OR DATE(o.created_at) >= CAST(@date_from AS DATE))
    AND (CAST(@date_to   AS DATE) IS NULL OR DATE(o.created_at) <= CAST(@date_to   AS DATE))
""") extend {
  view: revenue is {
    aggregate: revenue is sum(sale_price)
  }
}

Los placeholders @date_from / @date_to se sustituyen en tiempo de run con los valores del filtro (o con NULL tipado cuando el dashboard no proveyó un valor). Nota que el dialecto MySQL usa CAST(… AS DATE) — nunca el ::date de Postgres.

Parámetro string de mes

##! experimental.parameters

source: ec_orders(
  p_month::string is "2024-12"
) is ecommerce.table('bigquery-public-data.thelook_ecommerce.order_items') extend {
  view: monthly is {
    where: format_datetime('%Y-%m', created_at) = p_month
    aggregate: revenue is sum(sale_price)
  }
}