A tiny semantic layer for SQL, written in Go. Describe your tables, the columns you expose, and how tables join — all in YAML. Then ask for the columns you want, and cubeql compiles a query that joins in only the tables it needs.
The interesting part is join resolution. A wide model might have dozens of related tables, but any single query touches a handful. cubeql represents joins as a directed graph and runs a breadth-first shortest-path search from the base table to each referenced table, so the generated SQL never drags in joins a query doesn't use — and multi-hop relationships (orders → customers → regions) are stitched together automatically.
Inspired by metric/semantic layers like Cube.js and LookML, distilled to the one idea worth showing: graph-driven join planning. ~700 lines, no heavy dependencies (just a YAML parser).
YAML model ──▶ Schema ──▶ Join graph ──▶ BFS shortest path ──▶ SQL + params
per view per query
- Model (
model/) loads cubes and views from YAML and validates them. - Graph (
graph/) turns a view's joins into a directed graph and resolves the minimal edge set to reach a set of target cubes. - Compiler (
compiler/) parses a query, asks the graph which joins are needed, and rendersSELECT / FROM / LEFT JOIN / WHEREwith bound?parameters (no string interpolation of user values).
go run ./cmd/cubeql compile -schema ./examples/schema -query ./examples/query.jsonGiven this query against the example e-commerce model:
{
"cube": "order_analytics",
"select": ["orders.id", "orders.amount", "regions.name", "products.category"],
"filters": [
{ "column": "orders.status", "op": "in", "values": ["paid", "shipped"] },
{ "column": "regions.name", "op": "eq", "values": ["EMEA"] }
]
}cubeql emits:
SELECT orders.id AS orders_id,
orders.amount_cents / 100.0 AS orders_amount,
regions.name AS regions_name,
products.category AS products_category
FROM fact_orders AS orders
LEFT JOIN dim_customers AS customers ON orders.customer_id = customers.id
LEFT JOIN dim_regions AS regions ON customers.region_id = regions.id
LEFT JOIN dim_products AS products ON orders.product_id = products.id
WHERE orders.status IN (?, ?)
AND regions.name = ?;
-- params: ["paid","shipped","EMEA"]Note that regions was requested but isn't directly joinable to orders — the
compiler found the path orders → customers → regions and inserted the
intermediate customers join on its own. Ask for only orders.id and you get
zero joins.
A cube maps to a physical table and exposes typed columns. A column's expr
is SQL evaluated in the cube's context; {token} references a sibling column and
defaults to the column name.
cubes:
- name: orders
table: fact_orders
columns:
- { name: id, type: int }
- { name: customer_id, type: int }
- { name: amount, type: float, expr: "{amount_cents} / 100.0" }A view composes cubes into a join graph rooted at base. Each join is a
directed edge; from defaults to the base, so edges chain into multiple hops.
The on condition references columns as {cube.column}.
cubes:
- name: order_analytics
base: orders
joins:
- { to: customers, on: "{orders.customer_id} = {customers.id}" }
- { to: products, on: "{orders.product_id} = {products.id}" }
- { from: customers, to: regions, on: "{customers.region_id} = {regions.id}" }go run ./cmd/cubeql serve -schema ./examples/schema -addr :8080
curl -s localhost:8080/compile -d '{
"cube": "order_analytics",
"select": ["orders.amount", "regions.name"]
}'Returns { "sql": "...", "params": [...] }.
eq ne gt gte lt lte like in between null notnull.
Values are always passed as bound parameters.
go test ./...The graph tests cover shortest-path selection, de-duplication of shared join prefixes, and unreachable targets; the compiler tests cover plain cubes, views, two-hop resolution, filter-driven joins, and parameter ordering.
model/ YAML schema: cubes, columns, joins, views, validation
graph/ directed join graph + BFS shortest-path resolution
compiler/ query -> SQL + bound parameters
cmd/cubeql/ CLI (compile) and HTTP server (serve)
examples/ sample e-commerce model and query
MIT — see LICENSE.