Introduction¶
This page is for a data platform engineer deciding whether bloomery fits their stack. It explains the problem the library exists to remove, the spec kinds it compiles, the invariants it holds itself to, and — just as important — what it deliberately does not do.
The problem¶
In a config-driven data platform, the worst failure mode is not a crash. It is a number that looks right and is wrong: an order-level shipping cost joined to line items and summed three times over, a net price subtracted from a gross cost, an average stored as a column and then re-averaged. These bugs pass syntax checks, pass type checks, survive code review, and surface months later as a finance discrepancy. The formula was right, the data was right, and the answer was 3× wrong.
The failure mode gets sharper when spec changes arrive from many hands — humans and LLM-assisted proposal systems — across many tenants. A typo'd key that is silently ignored, a join that silently fans out, a recipe that is silently re-chosen: each is a plausible number with no audit trail.
Bloomery's answer is a compiler that refuses. Every spec is validated against a domain model that carries units, tax bases, grains, and additivity; anything that would produce a plausible-but-wrong number is a compile error with a source path, not a warning. The system may not know an answer, but it may not return a wrong one without warning.
The refusals/ example is six of those specs, each one a project that a
hand-written model would run without complaint, printed next to the message bloomery
gives instead — worth ten minutes if this section is the reason you are here.
What bloomery is¶
A pure function library. Specs go in as data structures; artifacts and plans come out as
data structures. Compilation produces SQLMesh, dbt, and Cube artifacts plus a MetricFlow
semantic manifest; the planner turns a MetricRequest into SQL over a wide mart at
request time, using an embedded, render-only MetricFlow behind a stable bloomery-owned
contract; plan() diffs two spec versions and classifies every change. Nothing is
executed, read, or written by the library itself.
The spec kinds¶
Six document kinds describe a deployment end to end. Each is strict YAML — unknown keys are rejected loudly — and each self-identifies by its version key, so bloomery never has to guess what it is reading.
The four below the Catalog are the tenant's project; the Catalog sits outside it,
loaded separately and shared across tenants. The sixth kind, StepSet
(steps_version), is optional: it wires platform-owned code into the pipeline where a
declaration cannot reach — see Steps.
Catalog¶
Domain knowledge, one per vertical, written by the platform operator — never by tenants. It holds canonical fields with units and tax bases, derivation recipes ordered by reliability, canonical relationships, and metric templates: the domain graph, independent of any tenant's data.
canonical_fields:
unit_price:
entity: order_item
type: decimal(12,4)
unit: currency
tax_basis: net
recipes:
- {id: direct, requires: [unit_price]}
- {id: from_total, requires: [line_total, quantity], expr: "line_total / quantity"}
EntityModel¶
What one tenant's data means: entities with grains, keys, and typed fields. A field with
a canonical: link is the tenant's realization of a Catalog field; a field without one
is tenant-native — legal, but it participates in no catalog-derived metric.
entities:
order_item:
grain: one row per line on an order
key: [order_id, line_no]
fields:
unit_price: {type: decimal(12,4), canonical: unit_price}
quantity: {type: int, canonical: quantity}
Mapping¶
How one source becomes one entity: field extractions, whitelisted transform chains, and — crucially — the recorded recipe choice for each derived field. Resolution happened upstream; the compiler validates the recorded decision and never re-makes it.
source: shopify__order_lines
target: order_item
fields:
unit_price:
recipe: from_total
from: {line_total: "$.total", quantity: "$.qty"}
order_date:
from: "$.created_at"
transform: [{parse_ts: "ISO8601"}, {to_utc: "Europe/Paris"}]
MetricSet¶
The tenant's measures, each carrying grain and an additivity class — additive,
semi_additive, ratio, non_additive, or distinct_count — which decides how the
metric may ever be aggregated or stored.
metrics:
gross_revenue:
requires: [unit_price, quantity]
grain: order_item
additivity: additive
agg: sum
expr: "unit_price * quantity"
Marts¶
The gold layer: one wide, pre-joined mart per (grain × subject area), with dimensions flattened in at build time so the common query path has no joins at all.
marts:
orders:
grain: order
base: order
flatten:
- {via: order_of_customer, prefix: customer_}
- {date: order_date, role: ordered}
measures: [order_count, shipping_cost]
The hard invariants¶
These are the reason bloomery exists as a separate package; violating any of them makes it untestable.
- No I/O. No filesystem, network, database, clock, environment, or randomness. Loaders take strings, not paths.
- Deterministic.
compile(x) == compile(x)byte-for-byte, across processes, machines, andPYTHONHASHSEEDvalues — enforced by test, not by intent. - Tenant-agnostic. The compiler does not know what a tenant is; tenant scoping is ordinary spec values supplied by the caller.
- No framework dependency. No orchestrator, no cloud SDK.
- Total errors. Every failure is a typed
BloomeryErrorwith a source path into the offending spec node — never a bareKeyError.
What bloomery is not¶
- It does not execute SQL. It emits text and plans; something else runs them.
- It does not read catalogs or profile data. Those are inputs, computed elsewhere.
- It does no orchestration — no scheduling, no backfill execution.
- It contains no LLM. Proposals may be produced upstream with LLM assistance, but they arrive as validated specs, and everything the library outputs — including human-readable query explanations — is generated deterministically.
If that boundary fits your platform, continue to Installation and the Quickstart, run one of the examples, or read Specs and the catalog for the full domain model.