Skip to main content
Private Preview·Early access by invitation.Request access →
Kirimana.
Docs · Contracts

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 declare kind: dimension.
  • gold-missing-fact-kind — likewise a fact must declare a fact kind.
  • fact-references-non-dim — every dimension_ref must 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 declared conformed and match bit-for-bit.
  • role-playing-unregistered — a role-playing alias (a Date dimension playing order_date and ship_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

Updated 5 July 2026 · v1.0.0-beta.1