Skip to content

YAML schemas

XML schemas are precise but they are a lot of angle brackets. YAML schemas give you the same model in a format that is easier to read at a glance, easier to review in a pull request, and easy to annotate with # comments. The Saiku engine treats them identically — the converter builds the same typed object graph either way.

Why YAML?

  • Readable. Fewer characters for the same information — a typical dimension that takes 60 XML lines fits in 20 YAML lines.
  • Diffable. Structural changes (adding a level, renaming a measure) produce clean, readable diffs instead of attribute-soup diffs.
  • Commentable. You can annotate any section with # comments; XML comments are legal but rarely survive round-trips through graphical tools.
  • Round-trippable. XML → YAML → XML produces byte-equivalent query results. You can convert your existing schemas at any time with the CLI.

Top-level structure

A complete M4 YAML schema uses these top-level keys (only schema is required):

schema: # required — schema header
name: "FoodMart"
metamodel_version: "4.0"
annotations: # optional — schema-level metadata
caption.de_DE: "Verkaufen"
physical_schema: # optional — tables, calculated columns, links
tables: [...]
links: [...]
shared_dimensions: # optional — named dimensions reused across cubes
Store: { ... }
Time: { ... }
cubes: # optional — one entry per cube
Sales: { ... }
roles: # optional — access-control roles
- name: "California manager"
schema_grant: { ... }

schema (header)

Maps to <Schema name="..." metamodelVersion="...">.

KeyRequiredNotes
nameyesSchema display name
metamodel_versionnoTypically "4.0"

Short form (name only):

schema: FoodMart

Long form:

schema:
name: "FoodMart"
metamodel_version: "4.0"

annotations

A flat map of name: text pairs. Supported at schema, cube, dimension, hierarchy, level, attribute, measure, calculated member, and role level. Mondrian uses dot-qualified names as a convention for locale-specific metadata:

annotations:
caption.de_DE: "Verkaufen"
caption.fr_FR: "Ventes"
description.fr_FR: "Cube des ventes"

physical_schema

Declares the physical tables and the foreign-key relationships between them.

tables

A list of table definitions. Each entry maps to a <Table> element.

KeyRequiredNotes
nameyesTable name in the database
aliasnoAlternative name used elsewhere in the schema (e.g. for self-joins)
schemanoDatabase schema qualifier (e.g. dbo)
key_columnnoSingle-column primary key shorthand
keynoMulti-column primary key — list of column name strings
calculated_columnsnoDerived columns defined as SQL expressions (see below)

key_column and key are mutually exclusive. Use key_column for a single column; use key for composite keys. Fact tables typically have no key declared.

physical_schema:
tables:
- name: "customer"
key:
- "customer_id"
- name: "product"
key_column: "product_id"
- name: "salary"
alias: "salary2" # second alias for a self-join
- name: "sales_fact_1997" # fact table — no key declared

calculated_columns

Virtual columns computed from SQL expressions. Maps to <ColumnDefs><CalculatedColumnDef>.

KeyRequiredNotes
nameyesColumn name used elsewhere in the schema
typenoMondrian type: String, Numeric, Integer
expressionyesMap of SQL dialect name → SQL body

Inline column references inside SQL bodies use {col:column_name} or {col:table.column_name} tokens, which the converter parses back to <Column> elements:

- name: "customer"
key:
- "customer_id"
calculated_columns:
- name: "full_name"
type: "String"
expression:
oracle: "{col:fname} || ' ' || {col:lname}"
mysql: "CONCAT({col:fname}, ' ', {col:lname})"
mssql: "{col:fname} + ' ' + {col:lname}"
generic: "{col:fullname}"

Supported dialect keys include generic, mysql, oracle, postgres, mssql, access, derby, db2, luciddb.

Foreign-key relationships between tables. Each entry maps to a <Link source="..." target="..."> element.

KeyRequiredNotes
sourceyesChild (many-side) table
targetyesParent (one-side) table
foreign_key_columnno*Single FK column name (shorthand)
foreign_keyno*List of FK column names (compound FK)

*One of foreign_key_column or foreign_key is required.

links:
- source: "product_class"
target: "product"
foreign_key:
- "product_class_id"
- source: "store"
target: "employee"
foreign_key:
- "store_id"

shared_dimensions

A map of dimension_name: dimension_body. The map key becomes the name attribute on the resulting <Dimension> element. Shared dimensions live outside any cube and can be referenced by multiple cubes.

KeyRequiredNotes
tablenoDefault table for attributes in this dimension
keynoName of the key attribute (must match an attribute name)
typeno"TIME" for time dimensions; omit for standard
attributesnoList of attribute definitions
hierarchiesnoList of hierarchy definitions
annotationsnoMap of annotation name → text

attributes

KeyRequiredNotes
nameyesAttribute display name
tablenoOverride the dimension’s default table
key_columnno*Single-column key
keyno*Multi-column key — list of "table.column" or "column" strings
name_columnnoColumn used for the member display name
name_columnsnoMulti-column name — list of column strings
order_by_columnnoColumn used for member ordering
caption_columnnoColumn used for the member caption
level_typenoTime grain: TimeYears, TimeQuarters, TimeMonths, TimeWeeks, TimeDays
datatypenoBoolean, Numeric, Integer, String (default String is omitted)
has_hierarchynofalse suppresses the auto-generated single-attribute hierarchy (default true; omit when true)
hierarchy_all_member_namenoAll-member label for the auto-generated hierarchy
hierarchy_all_member_captionnoAll-member caption for the auto-generated hierarchy
hierarchy_default_membernoDefault member MDX unique name for the auto-generated hierarchy
hierarchy_has_allnofalse suppresses the All level in the auto-generated hierarchy
propertiesnoList of sibling attribute names that are properties of this attribute
annotationsnoMap of annotation name → text

*key_column and key are mutually exclusive. For cross-table references inside key or name_columns, qualify with table.column:

attributes:
- name: "Brand Name"
table: "product"
key:
- "product_class.product_family" # qualified cross-table ref
- "product_class.product_department"
- "brand_name" # unqualified — belongs to default table
name_column: "brand_name"
has_hierarchy: false

hierarchies

KeyRequiredNotes
nameyesHierarchy name
all_member_namenoLabel for the All member
default_membernoMDX unique name of the default member
has_allnofalse suppresses the All level
levelsyesOrdered list of levels
annotationsnoMap of annotation name → text

Each entry in levels is either a bare string (when the level name equals the attribute name) or a map {name, attribute} (when they differ):

hierarchies:
- name: "Stores"
all_member_name: "All Stores"
levels:
- "Store Country" # bare string
- "Store State"
- "Store City"
- "Store Name"
- name: "Education Level"
levels:
- name: "Education Level" # map form — level name differs from attribute
attribute: "Education"

Full example — the Store shared dimension from FoodMart:

shared_dimensions:
Store:
table: "store"
key: "Store Id"
attributes:
- name: "Store Country"
key_column: "store_country"
has_hierarchy: false
- name: "Store State"
key_column: "store_state"
has_hierarchy: false
- name: "Store City"
key:
- "store_state"
- "store_city"
name_column: "store_city"
has_hierarchy: false
- name: "Store Id"
key_column: "store_id"
has_hierarchy: false
- name: "Store Name"
key_column: "store_name"
has_hierarchy: false
properties:
- "Store Type"
- "Store Manager"
- name: "Store Type"
key_column: "store_type"
hierarchy_all_member_name: "All Store Types"
hierarchies:
- name: "Stores"
all_member_name: "All Stores"
levels:
- "Store Country"
- "Store State"
- "Store City"
- "Store Name"

cubes

A map of cube_name: cube_body. The map key becomes the cube’s name.

KeyRequiredNotes
default_measurenoName of the default measure
annotationsnoMap of annotation name → text
dimensionsnoList of dimension usages and local dimension definitions
measure_groupsnoList of measure group definitions
calculated_membersnoList of calculated member definitions
named_setsnoList of named set definitions

dimensions (cube-level)

Each entry is either a usage (a reference to a shared dimension) or a local definition (an inline dimension defined only for this cube).

Usage — a map with only source:

dimensions:
- source: "Store"
- source: "Time"
- source: "Product"

Local definition — a map with name plus the full dimension body (same keys as shared_dimensions):

dimensions:
- name: "Customer"
table: "customer"
key: "Name"
attributes:
- name: "Country"
key_column: "country"
has_hierarchy: false
- name: "Name"
key_column: "customer_id"
name_column: "full_name"
order_by_column: "full_name"
has_hierarchy: false
hierarchies:
- name: "Customers"
all_member_name: "All Customers"
levels:
- "Country"
- "State Province"
- "City"
- "Name"

measure_groups

KeyRequiredNotes
namenoMeasure group name; optional for the primary group
tableyesFact or aggregate table name
typeno"aggregate" for aggregate measure groups; omit for "fact"
approx_row_countnoHint for the approximate fact table row count (string, e.g. "86837")
ignore_unrelated_dimensionsnotrue treats unrelated dimensions as [All] rather than returning null
measuresnoList of measure or measure-ref definitions
dimension_linksnoList of links from this measure group to its dimensions

measures

Each entry is either a measure definition (has name) or a measure reference (has ref, used in aggregate measure groups).

Measure definition:

KeyRequiredNotes
nameyesMeasure display name
columnnoSource column
aggregatoryessum, count, distinct-count, min, max, avg
format_stringnoMDX format string (e.g. "#,###.00", "Standard", "Currency")
datatypenoNumeric, Integer, String
propertiesnoList of {name, value} or {name, expression} maps
annotationsnoMap of annotation name → text

Measure reference (aggregate measure groups only):

KeyRequiredNotes
refyesName of the referenced measure from the primary group
agg_columnnoColumn in the aggregate table holding the pre-aggregated value
measures:
- name: "Unit Sales"
column: "unit_sales"
aggregator: "sum"
format_string: "Standard"
- name: "Customer Count"
column: "customer_id"
aggregator: "distinct-count"
format_string: "#,###"
# Inside an aggregate measure group:
- ref: "Unit Sales"
agg_column: "unit_sales_sum"
- ref: "Fact Count"
agg_column: "fact_count"

Each link has a type field that determines the join kind.

foreign_key — the standard join from fact table to dimension via a FK column:

KeyRequiredNotes
typeyes"foreign_key"
dimensionyesDimension name
foreign_key_columnno*Single FK column name
foreign_keyno*List of FK column names (compound FK)
attributenoAttribute to join to when the FK does not point to the dimension key

copy — the aggregate table inherits dimension data from another measure group:

KeyRequiredNotes
typeyes"copy"
dimensionyesDimension name
column_refsnoList of {table, name, agg_column} maps

no_link — this measure group does not link to this dimension:

KeyRequiredNotes
typeyes"no_link"
dimensionyesDimension name

fact — the dimension’s data comes directly from the fact table:

KeyRequiredNotes
typeyes"fact"
dimensionyesDimension name

reference — the dimension is reached indirectly via another dimension’s attribute:

KeyRequiredNotes
typeyes"reference"
dimensionyesDimension name
via_dimensionnoIntermediate dimension name
via_attributenoAttribute on the intermediate dimension used to join

Full measure group example:

measure_groups:
- name: "Sales"
table: "sales_fact_1997"
measures:
- name: "Unit Sales"
column: "unit_sales"
aggregator: "sum"
format_string: "Standard"
- name: "Store Cost"
column: "store_cost"
aggregator: "sum"
format_string: "#,###.00"
- name: "Customer Count"
column: "customer_id"
aggregator: "distinct-count"
format_string: "#,###"
dimension_links:
- type: "foreign_key"
dimension: "Store"
foreign_key_column: "store_id"
- type: "foreign_key"
dimension: "Time"
foreign_key_column: "time_id"
- type: "foreign_key"
dimension: "Product"
foreign_key_column: "product_id"
- table: "agg_c_special_sales_fact_1997"
type: "aggregate"
measures:
- ref: "Fact Count"
agg_column: "fact_count"
- ref: "Unit Sales"
agg_column: "unit_sales_sum"
dimension_links:
- type: "foreign_key"
dimension: "Store"
foreign_key_column: "store_id"
- type: "copy"
dimension: "Time"

calculated_members

KeyRequiredNotes
nameyesMember name
dimensionnoTarget dimension (e.g. "Measures")
hierarchynoTarget hierarchy MDX unique name
parentnoParent member MDX unique name
formulanoMDX formula
format_stringnoMDX format string
captionnoDisplay caption shown in client tools
descriptionnoHuman-readable description
visiblenofalse hides the member from client tools (default true; omit when true)
cell_formatternoCustom cell formatter — {class_name} or {script: {language, body}}
propertiesnoList of {name, value} or {name, expression} maps
annotationsnoMap of annotation name → text
calculated_members:
- name: "Profit"
dimension: "Measures"
formula: "[Measures].[Store Sales] - [Measures].[Store Cost]"
properties:
- name: "FORMAT_STRING"
value: "$#,##0.00"
- name: "Profit Growth"
dimension: "Measures"
formula: >-
([Measures].[Profit] - [Measures].[Profit last Period])
/ [Measures].[Profit last Period]
properties:
- name: "FORMAT_STRING"
value: "0.0%"

named_sets

named_sets:
- name: "Top Sellers"
formula: "TopCount([Warehouse].[Warehouse Name].MEMBERS, 5, [Measures].[Warehouse Sales])"

roles

A list of role definitions.

KeyRequiredNotes
nameyesRole name
class_namenoJava class implementing custom role logic
schema_grantnoTop-level grant

schema_grant has access ("all", "none", "all_dimensions", "custom") and an optional cubes list. Each cube grant can contain dimensions and hierarchies lists with their own grants. Hierarchy grants support top_level, bottom_level, rollup_policy ("full", "partial", "hidden"), and a members list.

roles:
- name: "California manager"
schema_grant:
access: "none"
cubes:
- cube: "Sales"
access: "all"
dimensions:
- dimension: "Gender"
access: "none"
hierarchies:
- hierarchy: "[Store].[Stores]"
access: "custom"
top_level: "[Store].[Stores].[Store Country]"
members:
- member: "[Store].[Stores].[USA].[CA]"
access: "all"
- member: "[Store].[Stores].[USA].[CA].[Los Angeles]"
access: "none"

XML vs YAML side-by-side

Here is the same Time dimension in both formats:

shared_dimensions:
Time:
table: "time_by_day"
key: "Time Id"
type: "TIME"
attributes:
- name: "Year"
key_column: "the_year"
level_type: "TimeYears"
has_hierarchy: false
- name: "Quarter"
key:
- "the_year"
- "quarter"
name_column: "quarter"
level_type: "TimeQuarters"
has_hierarchy: false
- name: "Month"
key:
- "the_year"
- "month_of_year"
name_column: "the_month"
level_type: "TimeMonths"
has_hierarchy: false
- name: "Time Id"
key_column: "time_id"
has_hierarchy: false
hierarchies:
- name: "Time"
has_all: false
levels:
- "Year"
- "Quarter"
- "Month"

Column reference encoding

Two encoding conventions appear throughout the YAML format:

table.column qualified references — inside key and name_columns lists, a column belonging to a non-default table is written as "table_name.column_name". The converter splits on the first . to recover table and column:

key:
- "product_class.product_family"
- "product_class.product_department"
- "brand_name" # unqualified — belongs to the default table

{col:...} tokens — inside SQL expression bodies for calculated columns, inline column references use {col:column_name} or {col:table.column_name}. The converter parses these back to <Column> elements:

expression:
mysql: "CONCAT({col:fname}, ' ', {col:lname})"
generic: "{col:fullname}"

Known limitations

  1. Identifiers containing a literal dot — the table.column encoding splits on the first . character. Table or column names that themselves contain a . (e.g. quoted identifiers) will not round-trip correctly. The same applies to {col:table.column} tokens.

  2. Whitespace in calculated-column SQL — the YAML serializer may introduce extra leading whitespace on SQL bodies when a file has been machine-generated and re-read. SQL with significant leading whitespace may acquire extra indentation on a second round-trip.

  3. $ref requires a file URL$ref include resolution only works when the schema is loaded via a Catalog=file:///... connect-string property. Schemas loaded via CatalogContent have no base directory and $ref resolution is skipped.


  • CLI — convert between XML and YAML and lint schemas from the command line.
  • Schema designer — generate an XML schema from a natural-language description.
  • Schemas — manage and edit saved schemas in the Saiku dashboard.