Appearance
The semantic model
A semantic model is the governed translation between physical tables (ord_hdr, cust_mst) and business meaning ("Net Revenue", "Customer", "Region"). In Keel it is one document — entities, joins and metrics — stored as immutable, content-addressed revisions (ADR-0008). The same document drives the workspace canvas, the YAML code view, the query compiler, lineage, exports and the MCP catalog.
Building blocks
| Object | What it is | Example |
|---|---|---|
| Entity | A business thing backed by one table (or a SQL query) with a primary key and a kind: Fact, Dimension, Bridge, Snapshot, Slowly changing dimension, Aggregate, Event, Other. | customer on dw.dim_customer |
| Dimension | An attribute to group or filter by: Categorical, Time (with grains Day…Year), Boolean, Geo or Numeric; may carry a sensitivity. | customer.email (Restricted) |
| Measure | An aggregation over a column or expression (Sum, Avg, Count, CountDistinct, Min, Max) with its additivity (Additive, SemiAdditive, NonAdditive). | credit_limit — semi-additive |
| Hierarchy | Ordered dimension levels for drill-down. | country → state → city |
| Segment | A named filter on an entity. | high_income |
| Join | A link between two entities: key columns, cardinality (ManyToOne, OneToOne, ManyToMany), join type, and a role for role-playing joins. | internet_sales → date as order_date, due_date, ship_date |
| Metric | What consumers ask for: Simple (one measure), Ratio, Derived (an expression over metrics), Cumulative or Conversion; with a certification level. | gross_margin = gross_profit / revenue |
Each object has a snake_case name, an optional business name and description, tags, an owner, and provenance: origin (Discovered, Inferred, LlmSuggested, UserDefined, Imported), confidence and rationale.
In YAML
The workspace's code view and the open-YAML export show the same document:
yaml
entities:
- name: customer
businessName: Customer
kind: Dimension
source: { dataSourceId: 0b5e…0001, schema: dw, table: dim_customer }
primaryKey: [customer_key]
dimensions:
- { name: email, expression: email_address, type: Categorical, sensitivity: Restricted }
- { name: full_name, expression: "CONCAT(first_name, ' ', last_name)", type: Categorical }
measures:
- { name: credit_limit, expression: credit_limit, aggregation: Sum, additivity: SemiAdditive }
segments:
- { name: high_income, filter: "yearly_income >= 100000" }
joins:
- name: internet_sales_customer
left: internet_sales
right: customer
keys: [{ leftColumn: customer_key, rightColumn: customer_key }]
cardinality: ManyToOne
joinType: Left
metrics:
- { name: revenue, type: Simple, measure: internet_sales.sales_amount }
- { name: gross_margin, type: Ratio, numerator: gross_profit, denominator: revenue, businessName: Gross Margin % }The canonical form sorts every named collection, omits empty values and uses string enums; a revision's content address is the SHA-256 of that canonical JSON, so identical documents always hash the same.
Validation
ModelValidator runs on every commit, autosave and change request. Errors (block a merge): invalid or duplicate names, unknown entities or columns (checked against the latest scan), missing join keys, ambiguous join paths, summing a non-additive measure, unresolved or cyclic metric references, time metrics without a time dimension. Warnings: fan traps and chasm traps — the compiler handles both safely, the warning just makes them visible.
How queries use it
The query compiler binds a request's metric and dimension names against a release of the model, plans the join paths (the unique shortest path over many-to-one and one-to-one joins, honouring roles), aggregates each fact at its own grain to avoid fan-out, applies access policies, and generates parameterized SQL for the source's dialect.