Tutorial
Chapter 3 - Structuring Business Logic
Goal: Understand grouped attributes and atomic concepts, then practice the model evolution workflow by adding a new attribute.
Prerequisites: You must have completed Chapter 2: The Building Blocks.
The Concept: Atomic Questions
Not every attribute is independent. Some pieces of information are semantically inseparable and must always be stored and versioned together.
Consider a monetary value. The number 60.33 on its own is ambiguous. Is that USD, EUR, or JPY? The amount and the currency together answer the atomic question: "What is the value of this order?"
Daana supports this through grouped attributes, which bundle related sub-attributes into a single logical concept.
Deconstructing order_value
In Chapter 1, you queried order_value_amount and order_value_currency from view_order. These two columns are the result of a grouped attribute declaration in the model.
Open model.yaml and locate the ORDER entity:
- id: ORDER
name: ORDER
definition: Purchase order
description: Orders placed by customers
attributes:
- id: order_id
name: order_id
definition: Unique order identifier
type: STRING
- id: order_status
name: order_status
definition: Current order status
type: STRING
effective_timestamp: true
- id: order_purchase_ts
name: order_purchase_ts
definition: When order was placed
type: START_TIMESTAMP
# NEW: Grouped Attribute for Order Value
- id: order_value
name: order_value
definition: Total monetary value of the order
description: Amount and currency bundled as an atomic concept
effective_timestamp: true
group: # bundles related sub-attributes that always change together (e.g. amount + currency)
- id: order_value_amount
name: order_value_amount
definition: Monetary amount
type: NUMBER
- id: order_value_currency
name: order_value_currency
definition: Currency code (ISO 4217, e.g. USD, EUR)
type: UNIT
The group keyword declares that order_value_amount and order_value_currency belong to a single concept (order_value). Two consequences follow:
Joint versioning. Because
effective_timestamp: trueis set on the parent (order_value), both sub-attributes share a single timeline. If the amount changes, the currency is re-recorded alongside it. An amount value can never exist without its corresponding currency in the historical record.Physical co-location. In the underlying
order_desctable, both sub-attributes share the sameTYPE_KEYand are stored on the same row. The user-facingview_orderhides this and presents amount and currency as two ordinary columns, but the EAV layer reveals it. Each non-grouped attribute on an order gets its own row, whileorder_valuelives on a single row carrying both the amount (val_num) and the currency (uom):SELECT name, val_num, uom FROM daana_dw.v_order_desc WHERE order_key = '1' ORDER BY name;name | val_num | uom --------------------------+-------------+----- ORDER_order_id | | ORDER_order_purchase_ts | | ORDER_order_status | | ORDER_order_value | 60.33000000 | USDORDER_order_purchase_tsappears as its own row even though its value lives insta_tmstprather thanval_num; that column isNULLhere.ORDER_order_valueis a single row carrying bothval_num(amount) anduom(currency). A single row means a single insert and a singleeff_tmstp, so amount and currency cannot drift apart.
Now open mappings/order-mapping.yaml to see how the group is mapped:
attributes:
- id: order_value_amount
transformation_expression: total_amount
- id: order_value_currency
transformation_expression: currency
Each sub-attribute is mapped individually as a flat entry. The grouping relationship is defined in the model, not in the mapping.
See Group Attributes for the full specification.
Why Grouped Attributes Matter
- Semantic clarity.
order_valueis a complete concept, not two unrelated columns. - Data quality. The single-row layout reduces two consistency checks to one: a test like "every populated amount has a populated currency" becomes a single-row assertion against
v_order_desc(val_num IS NOT NULL AND uom IS NOT NULL) rather than a join between two attribute rows. - Standardization. The
UNITtype provides consistent handling of measurement units (currency, weight, distance) across the entire warehouse.
Exercise: Add customer_registration_date
You've now seen how existing configuration works. The next step is to modify the model yourself and observe the model evolution workflow: edit the model, merge the mapping, redeploy, and query the result.
The stage.customers table includes a registration_date column that records when each customer account was created. This data is not yet part of the model. You'll add it as a START_TIMESTAMP attribute.
Step 1: Add the attribute to the model
Open model.yaml and add a new attribute to the CUSTOMER entity, after the existing attributes:
- id: customer_registration_date
name: customer_registration_date
definition: Date when the customer account was created
type: START_TIMESTAMP
The START_TIMESTAMP type records when an entity's lifecycle begins. Unlike STRING or NUMBER attributes, it is stored in a dedicated timestamp column (STA_TMSTP in the physical _desc table) rather than a generic value column.
Note:
effective_timestampis not set here. A registration date does not change over time; it is a fixed point.
Step 2: Merge the mapping
The model now declares an attribute that has no corresponding mapping entry. Instead of editing the mapping file manually, use the merge command:
daana-cli merge mapping
This command scans the model, detects new attributes, and adds placeholder entries to the existing mapping files while preserving all current transformation expressions.
The merge step also normalises the file to Daana's canonical mapping format: it adds allow_multiple_identifiers: false on each mapping group when the field is missing, and appends two tracking fields at the bottom (template_model_source and template_generated_at) that record which model the mapping was last reconciled against. A timestamped .bak of the previous version lands next to the file.
Open mappings/customer-mapping.yaml and locate the new entry generated for customer_registration_date. It appears in the first source table (stage.customers) and looks like this:
- id: customer_registration_date
transformation_expression: customer_registration_date # TODO: New attribute - update transformation
The placeholder uses the attribute id as a stand-in expression and tags the line with # TODO: so a grep "TODO:" mappings/ from CI or a code review surfaces every mapping still awaiting work. Update the line to reference the correct source column and drop the comment:
- id: customer_registration_date
transformation_expression: registration_date
Step 3: Validate
daana-cli check workflow
Confirm that the workflow is valid before deploying.
Step 4: Deploy and execute
daana-cli deploy
daana-cli execute
Step 5: Query the result
Open a psql shell against the local container so you can run the upcoming SQL queries:
docker exec -it daana-customerdb psql -U dev -d customerdb
SELECT
customer_key,
customer_name,
customer_registration_date
FROM daana_dw.view_customer
ORDER BY customer_key::int;
Expected result:
customer_key | customer_name | customer_registration_date
--------------+-------------------+----------------------------
1 | John Smith | 2023-01-10 00:00:00
2 | Emily Johnson | 2023-02-14 00:00:00
3 | Michael Brown | 2023-03-05 00:00:00
4 | Sarah Davis | 2023-04-20 00:00:00
5 | David Wilson | 2023-05-15 00:00:00
6 | Jennifer Martinez | 2023-06-10 00:00:00
7 | Robert Anderson | 2023-07-01 00:00:00
8 | Lisa Taylor | 2023-08-05 00:00:00
9 | James Thomas | 2023-09-12 00:00:00
10 | Maria Garcia | 2023-10-18 00:00:00
(10 rows)
Note:
customer_registration_datecarries a time component (00:00:00) becauseSTART_TIMESTAMPattributes are stored as full timestamps even when the source value is just a date.
The new column is available immediately. No migration scripts, no ALTER TABLE statements.
What You Practiced
This exercise demonstrated the standard model evolution workflow:
| Step | Command | Purpose |
|---|---|---|
| 1 | Edit model.yaml | Declare the new attribute |
| 2 | daana-cli merge mapping | Generate mapping placeholders for new attributes |
| 3 | Edit mapping file | Set the correct transformation expression |
| 4 | daana-cli check workflow | Validate before deployment |
| 5 | daana-cli deploy | Update physical tables and views |
| 6 | daana-cli execute | Load data for the new attribute |
This workflow is non-destructive. Adding a new attribute does not alter existing tables or invalidate existing data. Daana adds new rows to the _desc table with a new TYPE_KEY and updates the view definition to include the new column.
Summary
- Grouped attributes bundle semantically inseparable properties (amount + currency) into a single concept with joint versioning.
- START_TIMESTAMP records when an entity's lifecycle begins, stored in a dedicated timestamp column.
- Model evolution follows a predictable workflow: edit model, merge mapping, validate, deploy, execute.
- The
merge mappingcommand preserves existing transformation expressions while adding placeholders for new attributes.