Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .hyf/grader_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
# Source this at the top of test.sh:
# source "$(dirname "$0")/grader_lib.sh"
#
# Provides: pass(), fail(), warn(), print_results(), write_score(),
# Provides: pass(), fail(), warn(), blocker(), print_results(), write_score(),
# and a set of common static-analysis checks derived from recurring
# PR review patterns across cohort c55.
#
# blocker(): use for leaked-secret findings (a committed profiles.yml/.env,
# a hardcoded password/connection string). It behaves like fail() for the
# printed report, but also flips a flag that forces write_score() to report
# pass=false regardless of the earned point total -- a leaked secret must
# be fixed before the PR can pass, it cannot be "pointed around."

_grader_details=()
_grader_blocker=false

pass() { _grader_details+=("✓ PASS $1"); }
fail() { _grader_details+=("✗ FAIL $1"); }
warn() { _grader_details+=("⚠ WARN $1"); }
blocker() { _grader_details+=("🚫 BLOCKER $1"); _grader_blocker=true; }

print_results() {
local header="${1:-Autograder Results}"
Expand All @@ -28,6 +36,10 @@ write_score() {
local outfile="${3:-$(dirname "${BASH_SOURCE[0]}")/score.json}"
local pass_flag="false"
[[ "$score" -ge "$passing" ]] && pass_flag="true"
if [[ "$_grader_blocker" == true ]]; then
pass_flag="false"
echo "🚫 A blocker was found (leaked secret) -- forcing pass=false regardless of score." >&2
fi
cat > "$outfile" << JSON
{
"score": $score,
Expand Down
76 changes: 38 additions & 38 deletions .hyf/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,25 +63,25 @@ for f in "${required_files[@]}"; do
pass "found $f"
else
fail "missing required file: $f"
((missing += 1))
missing=$((missing + 1))
fi
done
if ls "$REPO_ROOT/tests/"*.sql &>/dev/null 2>&1; then
pass "found singular test in tests/"
else
fail "no .sql file found in tests/ -- add at least one singular test"
((missing += 1))
missing=$((missing + 1))
fi
if [[ -s "$REPO_ROOT/docs/lineage.png" ]]; then
pass "found docs/lineage.png"
else
fail "docs/lineage.png missing -- run dbt docs generate + serve and screenshot the lineage graph"
((missing += 1))
missing=$((missing + 1))
fi
if [[ "$missing" -eq 0 ]]; then
l1=10
fi
((score += l1))
score=$((score + l1))
pass "Level 1: required files ($l1/10 pts)"

# ── Level 2 (15 pts): secrets hygiene ───────────────────────────────────────
Expand All @@ -90,23 +90,23 @@ gi="$REPO_ROOT/.gitignore"
ex="$REPO_ROOT/profiles.yml.example"

if [[ -f "$gi" ]] && grep -qE "^profiles\.yml$" "$gi"; then
((l2 += 5)); pass ".gitignore: profiles.yml excluded"
l2=$((l2 + 5)); pass ".gitignore: profiles.yml excluded"
else
fail ".gitignore must contain 'profiles.yml' on its own line"
fi

if [[ -f "$ex" ]] && grep -qE "env_var\(" "$ex"; then
((l2 += 5)); pass "profiles.yml.example: uses env_var() -- no hardcoded password"
l2=$((l2 + 5)); pass "profiles.yml.example: uses env_var() -- no hardcoded password"
else
fail "profiles.yml.example must use env_var('PG_PASSWORD') instead of a hardcoded password"
fi

if [[ -f "$REPO_ROOT/profiles.yml" ]]; then
fail "profiles.yml is committed -- BLOCKER: run: git rm --cached profiles.yml"
blocker "profiles.yml is committed -- run: git rm --cached profiles.yml, then rotate the Postgres password since it was pushed"
else
((l2 += 5)); pass "profiles.yml not committed (correctly git-ignored)"
l2=$((l2 + 5)); pass "profiles.yml not committed (correctly git-ignored)"
fi
((score += l2))
score=$((score + l2))
pass "Level 2: secrets hygiene ($l2/15 pts)"

# ── Level 3 (20 pts): staging models ────────────────────────────────────────
Expand All @@ -115,79 +115,79 @@ st="$REPO_ROOT/models/staging/stg_trips.sql"
sz="$REPO_ROOT/models/staging/stg_zones.sql"

if file_is_filled "$st" && sqlgrep "\{\{[[:space:]]*source\(" "$st"; then
((l3 += 4)); pass "stg_trips.sql: uses {{ source() }} reference"
l3=$((l3 + 4)); pass "stg_trips.sql: uses {{ source() }} reference"
else
fail "stg_trips.sql: must use {{ source('nyc_taxi', 'raw_trips') }}"
fi

if file_is_filled "$st" && sqlgrep "pickup_location_id" "$st" && sqlgrep "IS NOT NULL|NOT NULL" "$st"; then
((l3 += 4)); pass "stg_trips.sql: filters NULL pickup_location_id"
l3=$((l3 + 4)); pass "stg_trips.sql: filters NULL pickup_location_id"
else
fail "stg_trips.sql: must filter WHERE pickup_location_id IS NOT NULL"
fi

if file_is_filled "$st" && sqlgrep "fare_amount" "$st" && sqlgrep ">=.*0|> -1" "$st"; then
((l3 += 4)); pass "stg_trips.sql: filters negative fares (fare_amount >= 0)"
l3=$((l3 + 4)); pass "stg_trips.sql: filters negative fares (fare_amount >= 0)"
else
fail "stg_trips.sql: must filter WHERE fare_amount >= 0"
fi

if file_is_filled "$st" && sqlgrep "tip_pct|safe_divide" "$st"; then
((l3 += 4)); pass "stg_trips.sql: tip_pct column present"
l3=$((l3 + 4)); pass "stg_trips.sql: tip_pct column present"
else
fail "stg_trips.sql: tip_pct column missing -- add {{ safe_divide('tip_amount', 'fare_amount') }} AS tip_pct"
fi

if file_is_filled "$sz" && sqlgrep "\{\{[[:space:]]*source\(" "$sz"; then
((l3 += 4)); pass "stg_zones.sql: uses {{ source() }} and is filled"
l3=$((l3 + 4)); pass "stg_zones.sql: uses {{ source() }} and is filled"
else
fail "stg_zones.sql: must use {{ source('nyc_taxi', 'raw_zones') }}"
fi
((score += l3))
score=$((score + l3))
pass "Level 3: staging models ($l3/20 pts)"

# ── Level 4 (20 pts): mart model ─────────────────────────────────────────────
l4=0
mart="$REPO_ROOT/models/marts/fct_daily_borough_stats.sql"

if file_is_filled "$mart"; then
((l4 += 2)); pass "fct_daily_borough_stats.sql: file filled"
l4=$((l4 + 2)); pass "fct_daily_borough_stats.sql: file filled"

if sqlgrep "\{\{[[:space:]]*ref\('stg_trips'\)" "$mart" && sqlgrep "\{\{[[:space:]]*ref\('stg_zones'\)" "$mart"; then
((l4 += 4)); pass "mart: ref() to both stg_trips and stg_zones"
l4=$((l4 + 4)); pass "mart: ref() to both stg_trips and stg_zones"
else
fail "mart: must reference both {{ ref('stg_trips') }} and {{ ref('stg_zones') }}"
fi

if sqlgrep "(INNER|LEFT)?[[:space:]]*JOIN" "$mart"; then
((l4 += 4)); pass "mart: JOIN to zones present"
l4=$((l4 + 4)); pass "mart: JOIN to zones present"
else
fail "mart: no JOIN -- must join stg_trips to stg_zones on pickup_location_id = location_id"
fi

if sqlgrep "GROUP[[:space:]]+BY" "$mart"; then
((l4 += 4)); pass "mart: GROUP BY present"
l4=$((l4 + 4)); pass "mart: GROUP BY present"
else
fail "mart: no GROUP BY -- must aggregate to (pickup_borough, pickup_date) grain"
fi

cols_ok=0
for col in pickup_borough pickup_date trip_count total_fare avg_tip_pct avg_trip_distance; do
if sqlgrep "$col" "$mart"; then
((cols_ok += 1))
cols_ok=$((cols_ok + 1))
else
fail "mart: required output column '$col' not found"
fi
done
if [[ "$cols_ok" -eq 6 ]]; then
((l4 += 6)); pass "mart: all 6 required output columns present"
l4=$((l4 + 6)); pass "mart: all 6 required output columns present"
elif [[ "$cols_ok" -ge 4 ]]; then
((l4 += 3)); warn "mart: $cols_ok/6 required columns present"
l4=$((l4 + 3)); warn "mart: $cols_ok/6 required columns present"
fi
else
fail "fct_daily_borough_stats.sql: still contains TODO stubs"
fi
((score += l4))
score=$((score + l4))
pass "Level 4: mart model ($l4/20 pts)"

# ── Level 5 (15 pts): tests ─────────────────────────────────────────────────
Expand All @@ -196,28 +196,28 @@ mart_yml="$REPO_ROOT/models/marts/_fct_daily_borough_stats.yml"
singular_test=$(ls "$REPO_ROOT/tests/"*.sql 2>/dev/null | head -1 || true)

if [[ -f "$mart_yml" ]] && grep -qiE "unique_combination_of_columns" "$mart_yml"; then
((l5 += 5)); pass "mart YAML: dbt_utils.unique_combination_of_columns test present"
l5=$((l5 + 5)); pass "mart YAML: dbt_utils.unique_combination_of_columns test present"
else
fail "mart YAML: missing dbt_utils.unique_combination_of_columns on (pickup_borough, pickup_date)"
fi

stg_yml="$REPO_ROOT/models/staging/_stg_trips.yml"
if [[ -f "$stg_yml" ]] && grep -qiE "not_null" "$stg_yml"; then
((l5 += 5)); pass "staging YAML: not_null tests present"
l5=$((l5 + 5)); pass "staging YAML: not_null tests present"
else
fail "staging YAML (_stg_trips.yml): not_null tests missing on key columns"
fi

if [[ -n "$singular_test" ]] && file_is_filled "$singular_test"; then
if sqlgrep "\{\{[[:space:]]*ref\('fct_daily_borough_stats'\)" "$singular_test"; then
((l5 += 5)); pass "singular test: filled and references fct_daily_borough_stats"
l5=$((l5 + 5)); pass "singular test: filled and references fct_daily_borough_stats"
else
((l5 += 3)); pass "singular test: filled (does not reference fct_daily_borough_stats -- check)"
l5=$((l5 + 3)); pass "singular test: filled (does not reference fct_daily_borough_stats -- check)"
fi
else
fail "singular test: empty or still a TODO stub"
fi
((score += l5))
score=$((score + l5))
pass "Level 5: tests ($l5/15 pts)"

# ── Level 6 (10 pts): documentation ─────────────────────────────────────────
Expand All @@ -226,7 +226,7 @@ mart_yml="$REPO_ROOT/models/marts/_fct_daily_borough_stats.yml"

if [[ -f "$mart_yml" ]] && grep -qiE "grain|one row per" "$mart_yml"; then
if ! grep -B5 -A5 "grain\|one row per" "$mart_yml" | grep -qiE "TODO"; then
((l6 += 5)); pass "mart YAML: grain stated in model description"
l6=$((l6 + 5)); pass "mart YAML: grain stated in model description"
else
fail "mart YAML: grain mentioned but description still has TODO -- fill it in"
fi
Expand All @@ -237,14 +237,14 @@ fi
if [[ -f "$mart_yml" ]]; then
filled_descs=$(grep "description:" "$mart_yml" | grep -v "TODO" | grep -cvE '^\s*description:\s*("")?\s*$' || true)
if [[ "$filled_descs" -ge 4 ]]; then
((l6 += 5)); pass "mart YAML: $filled_descs column descriptions filled"
l6=$((l6 + 5)); pass "mart YAML: $filled_descs column descriptions filled"
elif [[ "$filled_descs" -ge 2 ]]; then
((l6 += 3)); warn "mart YAML: only $filled_descs column descriptions filled (target: all 6)"
l6=$((l6 + 3)); warn "mart YAML: only $filled_descs column descriptions filled (target: all 6)"
else
fail "mart YAML: column descriptions are empty -- explain meaning and units for each column"
fi
fi
((score += l6))
score=$((score + l6))
pass "Level 6: documentation ($l6/10 pts)"

# ── Level 7 (10 pts): business answers + AI log ─────────────────────────────
Expand All @@ -255,11 +255,11 @@ ai="$REPO_ROOT/AI_ASSIST.md"
if file_is_filled "$ans"; then
sql_blocks=$(grep -c "^\`\`\`sql" "$ans" 2>/dev/null || echo 0)
if [[ "$sql_blocks" -ge 4 ]]; then
((l7 += 5)); pass "reports/answers.md: 4 SQL blocks present and filled"
l7=$((l7 + 5)); pass "reports/answers.md: 4 SQL blocks present and filled"
elif [[ "$sql_blocks" -ge 2 ]]; then
((l7 += 3)); warn "reports/answers.md: $sql_blocks SQL block(s) present (need 4)"
l7=$((l7 + 3)); warn "reports/answers.md: $sql_blocks SQL block(s) present (need 4)"
else
((l7 += 2)); warn "reports/answers.md: filled but SQL blocks missing"
l7=$((l7 + 2)); warn "reports/answers.md: filled but SQL blocks missing"
fi
else
fail "reports/answers.md: empty or still contains TODO stubs"
Expand All @@ -268,14 +268,14 @@ fi
if file_is_filled "$ai"; then
chars=$(wc -c < "$ai" | tr -d ' ')
if [[ "$chars" -ge 800 ]]; then
((l7 += 5)); pass "AI_ASSIST.md: filled (${chars} chars)"
l7=$((l7 + 5)); pass "AI_ASSIST.md: filled (${chars} chars)"
else
((l7 += 2)); warn "AI_ASSIST.md: present but brief (${chars} chars -- target 800+)"
l7=$((l7 + 2)); warn "AI_ASSIST.md: present but brief (${chars} chars -- target 800+)"
fi
else
fail "AI_ASSIST.md: empty or still contains TODO stubs"
fi
((score += l7))
score=$((score + l7))
pass "Level 7: business answers + AI log ($l7/10 pts)"

# ── Final ─────────────────────────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions .user.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
id: 65fed7dc-cfb1-40ec-af82-fb56180cafce
29 changes: 17 additions & 12 deletions AI_ASSIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,29 @@ Document one place you used an LLM during this assignment.

## The problem

<!-- TODO: describe the specific problem you asked an LLM about.
Example: "My safe_divide macro compiled but returned NULL for all rows even
when tip_amount and fare_amount were both non-zero." -->

TODO

While building the stg_trips staging model, i had an SQL syntax issue because i placed the "WHERE" clause before the "FROM" clause.
## The prompt

<!-- TODO: paste the exact prompt you sent to the LLM. -->

TODO

Im building a dbt staging model and my SQL query is failing. what is the problem here?
my query was :
SELECT
pickup_datetime,
pickup_location_id,
fare_amount,
tip_amount,
trip_distance,

{{ safe_divide('tip_amount', 'fare_amount') }} as tip_pct

where pickup_location_id is not null
FROM {{ source('nyc_taxi', 'raw_trips') }}
and fare_amount >= 0
## The response

<!-- TODO: summarise or paste what the LLM returned. -->

TODO
the llm explained that SQL clauses follow a fixed order. The FROM clause must come before the WHERE clause because SQL needs to know the source table before applying filters.

## Reflection

Expand All @@ -29,8 +35,7 @@ TODO
'ratio' to 'tip_pct' to match the assignment schema." -->

TODO

---
i changed the query order so that "FROM" comes immediately after the SELECT columns, followed by the WHERE filters.

> Remember: never paste real connection strings, passwords, or PII into an LLM.
> The NYC TLC dataset is public so sample rows are safe here, but practise the habit.
Binary file added docs/Lineage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions macros/safe_divide.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
-- Use for tip_pct = tip_amount / fare_amount and similar ratio columns.

{% macro safe_divide(numerator, denominator) %}
-- TODO: implement the macro body.
-- Use NULLIF(denominator, 0) to avoid division-by-zero errors.
-- Return only the SQL expression (no SELECT, no semicolon).
NULL

{{ numerator }} / nullif({{ denominator }}, 0)

{% endmacro %}


32 changes: 23 additions & 9 deletions models/marts/_fct_daily_borough_stats.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,36 @@ version: 2
models:
- name: fct_daily_borough_stats
description: >
TODO: state the grain (one row per ___), the source lineage
(built from ___ and ___), and at least one known caveat
(e.g. rows dropped in staging, any WARN-severity tests).
Daily borough-level taxi statistics with a grain of one row per
(pickup_borough, pickup_date).
Built from stg_trips and stg_zones.
Trips with missing or invalid pickup locations are removed during staging
or excluded by the INNER JOIN.
The avg_tip_pct warning test may return some rows where
tips are higher than the fare amount.

tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- pickup_borough
- pickup_date
# TODO: Task 5 -- add the compound uniqueness test on the mart's primary
# key (pickup_borough, pickup_date). You need the dbt_utils package for
# this: declare it in packages.yml and run `dbt deps` first.
columns:
- name: pickup_borough
description: "TODO: explain what this column contains and where it comes from"
description: "The TLC borough where trips were picked up, sourced from stg_zones.borough"
tests:
- not_null
- name: pickup_date
description: "TODO: explain units and how it is derived"
description: "The calendar date of the trip pickup, derived from pickup_datetime."
tests:
- not_null
- name: trip_count
description: "TODO: explain what is counted (unit: number of trips)"
description: "The number of taxi trips picked up in the borough on that dat."
- name: total_fare
description: "TODO: explain units (USD) and what fare_amount represents"
description: "The total fare amount collected in US dollars, calculated as the sum of fare_amount"
- name: avg_tip_pct
description: "TODO: explain the ratio (tip_amount / fare_amount)"
description: "The average tipping ratio calculated as tip_amount divided by fare_amount."
- name: avg_trip_distance
description: "TODO: explain units (miles, from TLC source data)"
description: "The average trip distance in miles based on the TLC trip_distance field"
Loading
Loading