Model gold — facts and SCD Type 2 dimensions
Gold is your analytics surface, and in Kirimana it is always a Kimball
star schema — facts and dimensions — no matter which technique shapes
your silver layer. A flat, Kimball, or Data Vault silver all land in the
same gold shape. The star metadata lives under kiri.gold.*, meaningful
only when kiri.lifecycle.state = gold.
A dimension
A dimension names its business_keys, its SCD type, and how its
surrogate key is built. An SCD Type 2 dimension tracks history with
valid_from / valid_to / is_current so historical facts join to
the right version:
apiVersion: 3.0.0
kind: DataContract
id: io.acme.sales.gold.dim_customer
name: dim_customer
version: 0.1.0
status: draft
domain: sales
description: >
Conformed customer dimension. One row per customer per SCD2 epoch —
valid_from/valid_to/is_current so historical facts join the right
version. Built from sat_customer_details.
owner: data-platform@acme.example
schema:
- name: dim_customer
logicalType: table
properties:
- name: customer_key
logicalType: string
primaryKey: true
required: true
description: SHA-256 of business keys per surrogate_key_strategy=hash.
- name: customer_id
logicalType: string
required: true
- name: name
logicalType: string
- name: valid_from
logicalType: timestamp
required: true
- name: valid_to
logicalType: timestamp
- name: is_current
logicalType: boolean
required: true
customProperties:
- property: kiri.classification
value: internal
- property: kiri.lifecycle.state
value: gold
- property: kiri.gold.kind
value: dimension
- property: kiri.gold.scd_type
value: type_2
- property: kiri.gold.business_keys
value: [customer_id]
- property: kiri.gold.surrogate_key_strategy
value: hash
- property: kiri.gold.surrogate_key_column
value: customer_key
- property: kiri.gold.star_schema
value: sales_mart
kiri.gold.scd_type accepts type_1, type_2, type_3, or type_7
and is required when kind = dimension. The surrogate-key strategy is
hash, sequence, or natural — and its default follows your silver
technique (Data Vault → hash, Kimball → sequence, flat → hash), so you
usually don’t set it explicitly.
A fact
A fact names its grain, its measures (each with an aggregation and
declared additivity), and the dimensions it references by URN. It joins
to an SCD2 dimension on the surrogate key, so it always sees the
version that was current at the fact’s time:
customProperties:
- property: kiri.lifecycle.state
value: gold
- property: kiri.gold.kind
value: fact
- property: kiri.gold.grain
value: [customer_key, order_date]
- property: kiri.gold.measures
value:
- { name: order_count, column: order_count, aggregation: sum, additivity: additive }
- { name: revenue_cents, column: revenue_cents, aggregation: sum, additivity: additive }
- property: kiri.gold.dimension_refs
value:
- urn:kirimana:gold:dim_customer
- property: kiri.gold.star_schema
value: sales_mart
kiri.gold.kind for facts is one of fact, factless_fact,
snapshot_fact, or accumulating_snapshot. Fact columns that are
neither foreign keys to dimensions nor measures — an order number, a
tracking id — are declared as kiri.gold.degenerate_dimensions. Set
kind: flat to opt a gold table out of dimensional modelling
entirely; the star-schema lint rules then skip it.
Surrogate keys
The surrogate key is the join spine of the star. hash computes a
deterministic hash of the business keys (the default under Data Vault
and flat silver); sequence assigns a monotonic integer (the Kimball
default); natural uses the business key itself. kiri.gold.surrogate_key_column
names the column — a literal {name} in the template is replaced with
the contract name, defaulting to {name}_key.
Computed gold columns
Sometimes a gold column is not a straight rename of a silver column but
a value derived from one with a different type or semantics. Declare
that transformation on the property with kiri.gold.computed —
structured and safe-by-construction, never a raw SQL string. Three
kinds exist.
A conditional flag with case_when — branches evaluated in order,
first match wins, with a required else:
- name: ar_ja
logicalType: integer
customProperties:
- property: kiri.lineage.from_columns
value: [urn:kirimana:silver:sat_link_person_votering::rost]
- property: kiri.gold.computed
value:
kind: case_when
when:
- {equals: "Ja", then: 1}
- {equals: "Nej", then: 0}
- {in: ["Avstår", "Frånvarande"], then: null}
else: 0
A type cast with type_cast — a dialect-native CAST with
optional formatting and controlled error handling:
- property: kiri.gold.computed
value:
kind: type_cast
target: integer # matches the property's logicalType
format: "yyyyMMdd" # optional, for date/timestamp keys
on_error: null # null (default) | raise | default-value
And a string normalisation with string_normalise — an ordered
list of safe operations (lower, upper, trim, literal replace,
coalesce), no regex:
- property: kiri.gold.computed
value:
kind: string_normalise
ops:
- lower
- trim
- {replace: {from: " ", to: "_"}}
Literals in every kind are dialect-quoted by the generator, so SQL
injection is impossible by construction. Straight projection stays the
default — a column without kiri.gold.computed emits as a plain
silver.col AS gold_col.
Star-schema lint rules
Six rules gate a well-formed star (kiri contract lint):
gold-missing-dim-kind— a gold contract acting as a dimension must declarekind: dimension.gold-missing-fact-kind— likewise a fact must declare a fact kind.fact-references-non-dim— everydimension_refmust resolve to a real dimension contract.measure-missing-additivity— every measure must declare its additivity (additive,semi_additive,non_additive).conformed-dim-conflict— a dimension reused across goals must be declaredconformedand match bit-for-bit.role-playing-unregistered— a role-playing alias (a Date dimension playingorder_dateandship_date) must be registered.
A seventh, star-schema-conformity, ties it together: a dimension
referenced from facts in more than one star_schema must be
conformed, and a fact’s star_schema must agree with its
non-conformed dimensions. Legacy gold SQL with no star metadata falls
back to kind: flat with a deprecated-dimension-shorthand warning.
Localized gold view twins
Gold can be served in more than one language. A gold layer may declare locales, and the generator emits — for each participating gold table — one canonical view in the default-locale schema plus one thin twin view per additional locale in a sibling schema. Twins are views only; they never duplicate storage, and they are legal only at gold. Names and column aliases come from contract-authored localization metadata, so translations are deterministic, not synthesized at apply time.
Locale config lives layer-wide on the domain’s gold.yml:
customProperties:
- property: kiri.localization.default_locale
value: en
- property: kiri.localization.locales
value: [sv] # [] = off, today's default
- property: kiri.localization.schema_pattern
value: "{schema}_{locale}" # → sales_sv
- property: kiri.localization.on_missing
value: fallback # closed menu: fallback | fail
Localization is opt-in per table. A gold table participates — and
gets a twin — only if it declares a table-level
kiri.localization.names. A table without it is canonical-only: no
twin, and exempt from locale coverage even under on_missing: fail. So
a mixed estate — a few localized dimensions, many English-only — is
expressed directly, with no phantom twins for tables that were never
translated.
Where next
- Conformed dimensions — reuse a dimension across marts safely.
- Handle schema drift — keep the star in step with the live warehouse.