Chapter 10 · Transaction, Periodic Snapshot, and Accumulating Snapshot Fact Tables

Model an Order Fulfillment Process Using Transaction + Periodic + Accumulating Facts and Compare Questions Supported

Run an executable AtlasMart acceptance suite that reconciles transaction sales, daily inventory, and fulfillment lifecycle state while mapping each business question to the correct fact shape.

Intermediate → Advanced120–140 minutesThree-fact acceptance suitePython stdlib sqlite3 · local/syntheticLast reviewed: September 2026

Learning outcomes

The final Chapter 10 lab treats fact-table choice as an executable contract. AtlasMart must be able to prove that sales events still reconcile to 625, each daily inventory snapshot is complete and time-aware, and fulfillment lifecycle rows update exactly once per accepted event while preserving milestone evidence. Then each representative business question is mapped to the fact shape whose grain actually supports it.

01

Run one deterministic acceptance suite across all three fact types.

02

Prove transaction controls, periodic snapshot completeness, and accumulating-row replay idempotency.

03

Map business questions to fact shapes before writing BI queries.

04

Inject missing-snapshot and time-summing failures and detect them programmatically.

05

Define observability, lineage, correction, migration, and cleanup expectations for production.

Chapter 10 continuity contract

Chapter 10 starts from the accepted Chapter 09 state. Canonical paid sales remain seven order-line facts, four paid orders, nine units, 625 paid GMV, 380 cost-at-sale, and 245 gross profit. Customer SCD history keeps Chapter 07's half-open business-effective intervals; Chapter 08's special-dimension patterns and Chapter 09's bridge/allocation contracts remain valid and are not rewritten here. The September 20 inventory control remains 137 units. Chapter 10 adds a second explicit inventory snapshot for September 19 (146 units) and an order-level fulfillment lifecycle fixture so learners can compare transaction, periodic snapshot, and accumulating snapshot grains without forcing them into one table.

Execution and interpretation note

The mandatory lab is synthetic, local, and disposable. It uses Python's standard-library sqlite3 module, so learners should record python --version and sqlite3.sqlite_version when executing. The fixture proves grain, aggregation, idempotency, and lifecycle-update semantics in one local process; it does not claim warehouse-scale performance, distributed exactly-once delivery, cloud concurrency behavior, or a universal SQL MERGE contract.

Important boundary

Passing all control totals is necessary but not sufficient. A report can still choose the wrong fact shape or time semantics. Acceptance therefore tests row meaning, replay behavior, completeness, and question fit as well as totals.

1. Decision matrix: ask what the row must mean

Business question Correct fact Why
Which products generated paid GMV on September 20? transaction sales atomic order-line event carries product and revenue at event time
How many units were on hand at end of September 20? periodic inventory snapshot balance is measured at the daily snapshot grain
How many orders have shipped but not delivered? accumulating fulfillment one lifecycle row carries current milestone state
What was the revenue-to-inventory relationship on September 20? aggregate sales and inventory separately, then align by date/product different grains must be reduced before comparison
What exact sequence of every fulfillment transition occurred? event log/transaction-style process events, not accumulating row alone accumulating summary can lose repeated transition detail

2. Complete deterministic setup

chapter10_fixture.sql
PRAGMA foreign_keys = ON;DROP TABLE IF EXISTS fulfillment_event_log;DROP TABLE IF EXISTS fact_fulfillment_accum;DROP TABLE IF EXISTS fact_inventory_daily;DROP TABLE IF EXISTS fact_sales_transaction;CREATE TABLE fact_sales_transaction (  order_id TEXT NOT NULL,  line_no INTEGER NOT NULL,  event_ts TEXT NOT NULL,  durable_customer_id TEXT NOT NULL,  product_id TEXT NOT NULL,  quantity INTEGER NOT NULL CHECK(quantity > 0),  extended_amount NUMERIC NOT NULL CHECK(extended_amount >= 0),  extended_cost NUMERIC NOT NULL CHECK(extended_cost >= 0),  source_event_id TEXT NOT NULL UNIQUE,  PRIMARY KEY(order_id,line_no));CREATE TABLE fact_inventory_daily (  snapshot_date TEXT NOT NULL,  product_id TEXT NOT NULL,  on_hand_units INTEGER NOT NULL CHECK(on_hand_units >= 0),  snapshot_batch_id TEXT NOT NULL,  PRIMARY KEY(snapshot_date,product_id));CREATE TABLE fact_fulfillment_accum (  order_id TEXT PRIMARY KEY,  order_ts TEXT,  paid_ts TEXT,  picked_ts TEXT,  shipped_ts TEXT,  delivered_ts TEXT,  reopened_ts TEXT,  current_status TEXT NOT NULL,  reopen_count INTEGER NOT NULL DEFAULT 0,  last_event_seq INTEGER NOT NULL DEFAULT 0,  updated_at TEXT NOT NULL);CREATE TABLE fulfillment_event_log (  event_id TEXT PRIMARY KEY,  order_id TEXT NOT NULL,  event_seq INTEGER NOT NULL,  event_type TEXT NOT NULL,  event_ts TEXT NOT NULL,  ingested_at TEXT NOT NULL,  UNIQUE(order_id,event_seq));

The Python fixture inserts the seven accepted sales rows, eight daily inventory rows, and the ordered fulfillment events. No cloud account, external database, credentials, or proprietary dataset is required.

3. Executable acceptance suite

verify_ch10.py
import sqlite3, math# This script mirrors the deterministic fixture shown throughout Chapter 10.# Run with the same schema/data definitions, then assert the contracts below.conn = build_conn(load_events=True)assert sales_controls(conn) == (7, 4, 9, 625, 380, 245)assert inventory_controls(conn) == [    ('2026-09-19', 4, 146),    ('2026-09-20', 4, 137),]rows = accum_rows(conn)assert len(rows) == 4state = {r[0]: r for r in rows}assert state['O1001'][1] == 'reopened' and state['O1001'][8] == 1assert state['O1002'][1] == 'shipped' and state['O1002'][6] is Noneassert state['O1003'][1] == 'delivered'assert state['O1005'][1] == 'picked' and state['O1005'][5] is None# Replay the same events: event count, lifecycle states, and reopen_count are unchanged.before_events = conn.execute('SELECT COUNT(*) FROM fulfillment_event_log').fetchone()[0]before_rows = accum_rows(conn)for e in EVENTS:    assert apply_event(conn, e) is Falseassert conn.execute('SELECT COUNT(*) FROM fulfillment_event_log').fetchone()[0] == before_eventsassert accum_rows(conn) == before_rows# Negative periodic-snapshot test: a missing product row must be visible.bad = build_conn(load_events=True)bad.execute("DELETE FROM fact_inventory_daily WHERE snapshot_date='2026-09-20' AND product_id='P400'")assert bad.execute("SELECT COUNT(*),SUM(on_hand_units) FROM fact_inventory_daily WHERE snapshot_date='2026-09-20'").fetchone() == (3,112)# Negative mixed-grain arithmetic: summing inventory through time is 283, not a current balance.assert conn.execute('SELECT SUM(on_hand_units) FROM fact_inventory_daily').fetchone()[0] == 283# Every fulfillment event sequence is unique per order and current summary has one row per order.assert conn.execute('SELECT COUNT(*) FROM fulfillment_event_log').fetchone()[0] == len(EVENTS)assert conn.execute('SELECT COUNT(*),COUNT(DISTINCT order_id) FROM fact_fulfillment_accum').fetchone() == (4,4)print('python=', __import__('sys').version.split()[0])print('sqlite=', sqlite3.sqlite_version)print('sales=', sales_controls(conn))print('inventory=', inventory_controls(conn))print('fulfillment=', [(r[0],r[1],r[8]) for r in rows])print('Chapter 10 acceptance: PASS')

Expected final line: Chapter 10 acceptance: PASS. The script also prints the actual Python and SQLite versions so the local execution baseline is not hidden.

4. What the negative tests prove

The deleted P400 snapshot row drops September 20 coverage from four products to three and the total from 137 to 112; the suite catches the missing member rather than accepting a plausible-looking total. The raw sum of both snapshot days is explicitly asserted as 283 so learners can see the wrong arithmetic, while the narrative states why that value is not a current balance. Replaying all fulfillment events must leave event count, lifecycle rows, current states, and reopen count unchanged.

5. Event-to-snapshot reconciliation

Not every snapshot can be reconstructed from the sales transaction fact alone because inventory also changes through receipts, transfers, adjustments, returns, reservations, and damage. Therefore Chapter 10 does not fabricate a formula claiming sales events explain the drop from 146 to 137. A production reconciliation would start from prior balance, add every governed inventory movement class, and prove the resulting closing balance to the published snapshot. Missing movement sources are a source-readiness gap, not a reason to invent numbers.

6. Observability and lineage

Operational monitoring should separate the three surfaces: transaction duplicate/reconciliation failures; periodic snapshot freshness/completeness/restatement status; and accumulating lifecycle event lag, out-of-order events, open-process age, replay duplicates, and milestone anomalies. Lineage should connect source event or state extract → ingestion/batch/event ID → fact row → semantic metric → consuming report. A task that “ran successfully” is not evidence that these fact contracts are correct.

7. Production performance and cost

This in-memory SQLite fixture is a correctness harness, not a warehouse benchmark. Transaction scans, snapshot density, and accumulating updates have different storage and execution profiles. Before a production physical-design decision, disclose engine/runtime, row counts, update frequency, partitioning/clustering/indexes, compression, cache/warm state, concurrency, query corpus, and measured latency/bytes worked. Do not infer a universal winner from this tiny lab.

8. Change, correction, and rollback contract

Corrections affect each fact differently. A transaction correction may require restatement or adjustment; a periodic snapshot correction republishes a period/member balance; an accumulating snapshot correction may revise one milestone or replay accepted events from raw evidence. Version the correction policy, preserve source evidence, reconcile before/after outputs, identify downstream impact, and retain a rollback path. Rebuilding all history is not automatically safe merely because the lab can reset instantly.

9. Cleanup/reset

All tables live in an in-memory SQLite connection. Closing the process discards them. Re-running build_conn() recreates the accepted fixture, including the event log and lifecycle summary, from deterministic constants. The mandatory lab has no production blast radius.

Knowledge check

Check your understanding

  1. Which fact answers “inventory on hand at end of September 20”?
  2. Why does the suite replay all fulfillment events?
  3. Why does Chapter 10 refuse to derive the 146→137 inventory change from sales alone?
  4. Can an accumulating snapshot replace the event log?
  5. What must be disclosed before comparing physical performance of the three fact shapes?
Review the answers

1. The periodic inventory snapshot filtered to September 20.

2. To prove duplicate delivery does not change accepted event count or lifecycle state, including reopen_count.

3. The model lacks all inventory movement classes needed for a valid balance equation; inventing them would fabricate evidence.

4. Not when exact repeated transition history matters; the snapshot is a lifecycle summary.

5. Engine/runtime, data size, update frequency, layout/indexing, cache state, concurrency, representative query corpus, and measured work/latency.

Summary and next step

Chapter 10 now proves three distinct fact contracts: events stay atomic, balances are measured by period with semi-additive time rules, and predictable workflows accumulate milestones through controlled updates. Chapter 11 next addresses surrogate-key assignment, late-arriving facts/dimensions, deletes, restatements, and history corrections.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.