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.
'''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
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.
- 1Run the pipeline twice and confirm identical
fact_rowsboth times. ThenSELECT * FROM pipeline_runs— your run log already tells a story. - 2Query the mart: net item revenue by category (
SUM(qty * unit_price)joined todim_product,status = 'completed'). Save this query — Module 5 will replace it with a governed metric. - 3Break it on purpose: comment out the
DELETEstep, run twice, and watchfact_rowsdouble. ('Comment out' means put a#at the start of each line so Python ignores it — here, the three lines of thestep('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. - 4Stretch: make the pipeline incremental — accept a
--since 2026-05-15argument 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).
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.