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/databasey 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_datedirecto enwhere:,aggregate:, etc. (el ejemplo de arriba). - Placeholders
@paramen SQL crudo — cuando el cuerpo del source se construye desde SQL crudo (connection.sql("""…""")), usa placeholders@paramdentro del bloque SQL. Cada@paramtiene 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)
}
}