Back to course overview
Module 2Pipelines & ingestion 22 min

Lab: Build the Harbor Lane pipeline

Write the ELT job: staging views over the raw exports, dims by full refresh, the fact by partition overwrite — then prove idempotency by running it twice.

This lab produces pipeline.py — the job that turns data/raw/ into harbor.duckdb. It's about 60 lines, and it embodies everything from this module: layers, full refresh for dims, partition overwrite for the fact, and a run log. You will run it twice on purpose and watch nothing break. That un-event is the deliverable.

pipeline.pypython
'''Harbor Lane ELT: data/raw/*.csv -> staging views -> star schema.
Idempotent by construction: run it as many times as you like.'''
import time
import duckdb

con = duckdb.connect('harbor.duckdb')
t0 = time.time()

def step(label, sql):
    con.execute(sql)
    print(f'  ok  {label}')

# ---- staging: mechanical cleanup only, one view per source ----------------
step('stg_products', '''
  CREATE OR REPLACE VIEW stg_products AS
  SELECT sku, product_name, category, unit_cost::DECIMAL(8,2) AS unit_cost
  FROM read_csv_auto('data/raw/products.csv', header=true)
''')

step('stg_web_customers', '''
  CREATE OR REPLACE VIEW stg_web_customers AS
  SELECT web_id, lower(trim(email)) AS email, trim(first_name) AS first_name,
         trim(last_name) AS last_name, phone, zip, created_at::DATE AS created_at
  FROM read_csv_auto('data/raw/web_customers.csv', header=true)
''')

step('stg_pos_customers', '''
  CREATE OR REPLACE VIEW stg_pos_customers AS
  SELECT pos_id, trim(full_name) AS full_name,
         nullif(lower(trim(email)), '') AS email,
         phone, store_zip, signup_date::DATE AS signup_date
  FROM read_csv_auto('data/raw/pos_customers.csv', header=true)
''')

step('stg_orders', '''
  CREATE OR REPLACE VIEW stg_orders AS
  SELECT order_id, lower(channel) AS channel, customer_ref, sku,
         qty::INTEGER AS qty, unit_price::DECIMAL(8,2) AS unit_price,
         ordered_at::TIMESTAMP AS ordered_at,
         ordered_at::DATE AS order_date, lower(status) AS status
  FROM read_csv_auto('data/raw/orders_*.csv', header=true, union_by_name=true)
''')

# ---- dims: small, so full refresh - trivially idempotent ------------------
step('dim_product (full refresh)', '''
  CREATE OR REPLACE TABLE dim_product AS
  SELECT sku, product_name, category, unit_cost FROM stg_products
''')

# ---- fact: partition overwrite on order_date -------------------------------
step('fct_order_lines (create if needed)', '''
  CREATE TABLE IF NOT EXISTS fct_order_lines (
    order_id TEXT, channel TEXT, customer_ref TEXT, sku TEXT,
    qty INTEGER, unit_price DECIMAL(8,2), ordered_at TIMESTAMP,
    order_date DATE, status TEXT
  )
''')
step('fct_order_lines (delete batch dates)', '''
  DELETE FROM fct_order_lines
  WHERE order_date IN (SELECT DISTINCT order_date FROM stg_orders)
''')
step('fct_order_lines (insert batch)', '''
  INSERT INTO fct_order_lines
  SELECT order_id, channel, customer_ref, sku, qty, unit_price,
         ordered_at, order_date, status
  FROM stg_orders
''')

# ---- run log: every run leaves evidence ------------------------------------
con.execute('''
  CREATE TABLE IF NOT EXISTS pipeline_runs (
    ran_at TIMESTAMP DEFAULT now(), fact_rows BIGINT, max_order_date DATE,
    seconds DOUBLE
  )
''')
rows, hi = con.execute(
    'SELECT count(*), max(order_date) FROM fct_order_lines').fetchone()
con.execute('INSERT INTO pipeline_runs (fact_rows, max_order_date, seconds) VALUES (?, ?, ?)',
            [rows, hi, round(time.time() - t0, 3)])
print(f'\nfact rows: {rows}   watermark: {hi}   ({round(time.time() - t0, 2)}s)')

The idempotency proof

terminalbash
python3 pipeline.py     # first run:  fact rows: 3052, watermark: 2026-05-30
python3 pipeline.py     # second run: SAME fact rows, SAME watermark
python3 pipeline.py     # third run:  still the same. This is the whole point.

The DELETE before INSERT makes re-runs rewrite the same partitions instead of stacking duplicates. Verify against Module 1's inspection count: the fact-row total must equal the raw order count exactly. A pipeline that loses or invents even one row has a bug you want to find now, not in the capstone.

  1. 1Run the pipeline twice and confirm identical fact_rows both times. Then SELECT * FROM pipeline_runs — your run log already tells a story.
  2. 2Query the mart: net item revenue by category (SUM(qty * unit_price) joined to dim_product, status = 'completed'). Save this query — Module 5 will replace it with a governed metric.
  3. 3Break it on purpose: comment out the DELETE step, run twice, and watch fact_rows double. ('Comment out' means put a # at the start of each line so Python ignores it — here, the three lines of the step('fct_order_lines (delete batch dates)', ...) call.) Un-comment (delete those #s), run once, and confirm it self-heals (the overwrite rewrites clean partitions). Understanding why it self-heals is the module.
  4. 4Stretch: make the pipeline incremental — accept a --since 2026-05-15 argument and glob only matching files into a temp staging view. Confirm re-runs are still idempotent (they must be: partition overwrite doesn't care how the batch was selected).
Problem set 2

The problem set hands you three broken pipeline designs (a bare-INSERT loader, a MERGE with the wrong key, a watermark that assumes exactly-once) and asks you to predict each failure mode before fixing it. Reading failure out of a design — before it runs — is the skill interviews probe.