diff --git a/evolution_log.md b/evolution_log.md index f342625..07f53a8 100644 --- a/evolution_log.md +++ b/evolution_log.md @@ -4,6 +4,26 @@ Each entry records one autonomous improvement iteration. --- +## Iteration 12 — 2026-06-29 + +### [UI/UX Improvements] + +- **Collapsible schema panel** (`GameProvider.tsx`): The right-hand Schema/Results column can now be collapsed to a narrow 40px rail on desktop (≥ lg breakpoint) by clicking the `›` chevron button appended to the right side of the tab bar. When collapsed, the rail shows a `‹` expand button and a vertical "SCHEMA" label so the affordance remains clear. The SQL editor column changes from a fixed `460px` to `lg:flex-1 min-w-0`, so it grows to fill the freed horizontal space — giving power users more room to compose long CTEs and window-function queries without layout thrash. A `transition-all duration-200 ease-in-out` CSS transition smooths the width change. The collapse state is session-local (`useState`), not persisted, so the panel always reopens on reload. Keyboard-accessible: both toggle buttons have `aria-label` and `title` attributes. Mobile layouts (< lg) are unaffected — the panels always stack vertically at full width. Directly implements the "collapsible schema panel" item from the UI/UX rotation. + +### [Game Design Tweaks] + +- **Level 72** *(Expert, difficulty 4)* — "Loan Product Acquisition Funnel": Teaches the canonical SQL product-analytics funnel pattern: a `UNION ALL` of four scalar aggregate queries (one per stage) tagged with an `ord` column, combined with a `CROSS JOIN` scalar CTE for the denominator. The four stages — All Customers (20), Loan Applicants (15), Active Loan Holders (12), Active Home Loan Holders (5) — yield conversion rates of 100 %, 75 %, 60 %, and 25 %. This exact UNION ALL + CROSS JOIN pattern is used in BigQuery, Redshift, Snowflake, and Spark SQL product-analytics pipelines at FAANG, digital banks, and MAS-regulated fintechs. Reinforces the CROSS JOIN scalar CTE idiom from Level 70 in a new product-analytics context. No new data or tables required. + +- **Level 73** *(Expert, difficulty 4)* — "First Salary Credit Per Account (ROW_NUMBER Dedup)": Teaches the universal SQL deduplication and latest/earliest-record-per-entity pattern: `ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY transaction_date ASC)` in a CTE, then `WHERE rn = 1` in the outer query. The compliance framing (MAS Notice 632 income-verification) makes the business case for dedup concrete. Returns 4 rows — accounts 5, 15, 25, 35 (customers Aisha Binte Yusof, Chen Mei Ling, Nur Hidayah, Fiona Tan) with their first Salary Credit amounts ($3,750 – $8,250) and dates in Jan–Feb 2024. The ROW_NUMBER PARTITION BY pattern is the universal dedup technique across every SQL dialect and is a staple of ETL pipelines, SCD snapshots, and data-quality remediations at FAANG, hedge funds, and GIC. No new data or tables required. + +### [Database & Code Optimizations] + +- **`LevelUpModal` hint map** extended with entries for levels 72 (funnel UNION ALL + CROSS JOIN) and 73 (ROW_NUMBER dedup). `nextHintKey` sentinel array extended from `[…, 71]` to `[…, 71, 72, 73]`; fallback updated `?? 71` → `?? 73`. `MAX_LEVEL` in `progression.ts` auto-derives from the last level in the array (73) — no other touch points needed. +- **Left panel flex layout**: changed `lg:w-[460px] flex-shrink-0` to `lg:flex-1 min-w-0` so the SQL editor column grows naturally when the right panel is collapsed, filling available horizontal space without overflow. +- **No new DB tables, seed rows, or indexes** — both new levels run cleanly against the existing `loans`, `transactions`, `accounts`, and `customers` tables. + +--- + ## Iteration 11 — 2026-06-22 ### [UI/UX Improvements] diff --git a/src/components/GameProvider.tsx b/src/components/GameProvider.tsx index f2eaaf2..c68eb5e 100644 --- a/src/components/GameProvider.tsx +++ b/src/components/GameProvider.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useRef, useCallback, Fragment } from 'react'; import dynamic from 'next/dynamic'; import Link from 'next/link'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; import SQLPanel from './SQLPanel'; import DataPreview from './DataPreview'; import HarbourStatus from './FlockStatus'; @@ -26,6 +27,7 @@ export default function GameProvider() { const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const [activeRightTab, setActiveRightTab] = useState<'schema' | 'data'>('schema'); const [showLevelNavigator, setShowLevelNavigator] = useState(false); + const [isPanelCollapsed, setIsPanelCollapsed] = useState(false); const rightPanelRef = useRef(null); const { currentLevel, totalXp, currentStreak, rehydrateFleet } = useGameStore(); @@ -269,43 +271,94 @@ export default function GameProvider() { {/* UI overlay */}
- {/* Left: SQL Editor */} -
+ {/* Left: SQL Editor — grows to fill when right panel is collapsed */} +
- {/* Right: Schema / Results + status */} -
- {/* Tab bar */} -
- {(['schema', 'data'] as const).map((tab) => ( + {/* Right: Schema / Results + status — collapsible on desktop */} +
+ {isPanelCollapsed ? ( + /* ── Collapsed rail (desktop only) ──────────────────────── */ +
- ))} -
+ + Schema + +
+ ) : ( + <> + {/* Tab bar */} +
+ {(['schema', 'data'] as const).map((tab) => ( + + ))} + {/* Desktop collapse toggle */} + +
- {/* Tab content */} -
- {activeRightTab === 'schema' ? : } -
+ {/* Tab content */} +
+ {activeRightTab === 'schema' ? : } +
- {/* Harbour status bar */} - setShowLevelNavigator(true)} /> + {/* Harbour status bar */} + setShowLevelNavigator(true)} /> + + )}
diff --git a/src/components/LevelUpModal.tsx b/src/components/LevelUpModal.tsx index 7215691..a4ee0e1 100644 --- a/src/components/LevelUpModal.tsx +++ b/src/components/LevelUpModal.tsx @@ -27,6 +27,8 @@ const EPOCH_NEXT_HINT: Record = { 69: 'Sessionization / gaps & islands: LAG → flag → SUM() OVER → aggregate — the universal session-detection pattern', 70: 'Risk scorecard: weighted-average rate via SUM(x*w)/SUM(w), CROSS JOIN scalar CTE for denominators, running exposure window function', 71: 'Year-over-Year growth: STRFTIME → GROUP BY year → LAG(total) OVER → CASE WHEN NULL — the canonical period-over-period growth pattern', + 72: 'Funnel analysis: UNION ALL multi-stage aggregation + CROSS JOIN scalar CTE for conversion rates — the standard product-analytics funnel pattern', + 73: 'Deduplication: ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) + WHERE rn = 1 — pick exactly one row per group, the universal dedup / latest-record-per-entity pattern', }; export default function LevelUpModal() { @@ -63,7 +65,7 @@ export default function LevelUpModal() { return () => window.removeEventListener('keydown', onKey); }, [showLevelUp, handleClose]); - const nextHintKey = [10, 20, 30, 40, 44, 54, 57, 59, 61, 63, 64, 65, 66, 67, 68, 69, 70, 71].find((k) => currentLevel < k) ?? 71; + const nextHintKey = [10, 20, 30, 40, 44, 54, 57, 59, 61, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73].find((k) => currentLevel < k) ?? 73; const xpEarned = xpFor(currentLevel); return ( diff --git a/src/data/levels/expert.ts b/src/data/levels/expert.ts index a8b5ae9..0e0245c 100644 --- a/src/data/levels/expert.ts +++ b/src/data/levels/expert.ts @@ -1337,4 +1337,175 @@ SELECT loan_year, epoch: 'Expert', difficulty: 4, }, + + // ============================================================ + // FUNNEL ANALYSIS & DEDUPLICATION (Levels 72–73) + // ============================================================ + + { + id: 72, + title: 'Loan Product Acquisition Funnel', + description: `Product Strategy needs a multi-stage acquisition funnel showing how many LCB customers progress through each stage of the loan product journey. Build a **vertical funnel** with four stages and a conversion percentage: + +| stage | customer_count | conversion_pct | +|---|---|---| +| All Customers | 20 | 100.0 | +| Loan Applicants | 15 | 75.0 | +| Active Loan Holders | 12 | 60.0 | +| Active Home Loan Holders | 5 | 25.0 | + +Use **two CTEs**: +1. **\`funnel\`**: a \`UNION ALL\` of four scalar aggregate queries, each tagged with an \`ord\` (1–4) and a \`stage\` label — one row per funnel stage. The four stages: + - \`ord=1\`: \`COUNT(*) FROM customers\` + - \`ord=2\`: \`COUNT(DISTINCT customer_id) FROM loans\` + - \`ord=3\`: \`COUNT(DISTINCT customer_id) FROM loans WHERE status = 'Active'\` + - \`ord=4\`: \`COUNT(DISTINCT customer_id) FROM loans WHERE status = 'Active' AND product_id = 7\` *(product 7 = HDB Home Loan)* +2. **\`total\`**: \`SELECT customer_count AS total_customers FROM funnel WHERE ord = 1\` — a scalar CTE for the denominator. + +Final SELECT: \`CROSS JOIN funnel f\` with \`total t\`, compute \`ROUND(100.0 * f.customer_count / t.total_customers, 1) AS conversion_pct\`. \`ORDER BY f.customer_count DESC\` — the four counts are distinct (20 → 15 → 12 → 5), so descending count order matches funnel stage order. + +This UNION ALL + CROSS JOIN scalar CTE pattern is the standard SQL funnel in BigQuery, Redshift, and Snowflake product-analytics pipelines at FAANG, digital banks, and MAS-regulated fintechs.`, + hint: 'CTE funnel: SELECT 1 AS ord, \'All Customers\' AS stage, COUNT(*) AS customer_count FROM customers UNION ALL SELECT 2, \'Loan Applicants\', COUNT(DISTINCT customer_id) FROM loans UNION ALL ... (stages 3 and 4 add WHERE status=\'Active\' and AND product_id=7). CTE total: SELECT customer_count AS total_customers FROM funnel WHERE ord=1. Final: f.stage, f.customer_count, ROUND(100.0 * f.customer_count / t.total_customers, 1) AS conversion_pct FROM funnel f CROSS JOIN total t ORDER BY f.customer_count DESC.', + seedQuery: `WITH funnel AS ( + SELECT 1 AS ord, 'All Customers' AS stage, + COUNT(*) AS customer_count + FROM customers + UNION ALL + SELECT 2, 'Loan Applicants', + COUNT(DISTINCT ) + FROM loans + UNION ALL + SELECT 3, 'Active Loan Holders', + COUNT(DISTINCT customer_id) + FROM loans + WHERE status = + UNION ALL + SELECT 4, 'Active Home Loan Holders', + COUNT(DISTINCT customer_id) + FROM loans + WHERE status = 'Active' AND product_id = +), +total AS ( + SELECT customer_count AS total_customers + FROM funnel + WHERE ord = 1 +) +SELECT f.stage, + f.customer_count, + ROUND(100.0 * f.customer_count / , 1) AS conversion_pct + FROM funnel f + CROSS JOIN total t + ORDER BY f.customer_count DESC`, + solutionQuery: `WITH funnel AS ( + SELECT 1 AS ord, 'All Customers' AS stage, + COUNT(*) AS customer_count + FROM customers + UNION ALL + SELECT 2, 'Loan Applicants', + COUNT(DISTINCT customer_id) + FROM loans + UNION ALL + SELECT 3, 'Active Loan Holders', + COUNT(DISTINCT customer_id) + FROM loans + WHERE status = 'Active' + UNION ALL + SELECT 4, 'Active Home Loan Holders', + COUNT(DISTINCT customer_id) + FROM loans + WHERE status = 'Active' AND product_id = 7 +), +total AS ( + SELECT customer_count AS total_customers + FROM funnel + WHERE ord = 1 +) +SELECT f.stage, + f.customer_count, + ROUND(100.0 * f.customer_count / t.total_customers, 1) AS conversion_pct + FROM funnel f + CROSS JOIN total t + ORDER BY f.customer_count DESC`, + epoch: 'Expert', + difficulty: 4, + }, + + { + id: 73, + title: 'First Salary Credit Per Account (ROW_NUMBER Dedup)', + description: `The Compliance team needs to verify when each LCB account first received a salary credit — a mandatory check for income-verification under MAS Notice 632. Some accounts have received multiple salary credits over the months; you must return **exactly one row per account**: the earliest one. + +Return: +- \`account_id\` +- \`customer_name\` +- \`amount\` — salary of the first credit +- \`first_salary_date\` — \`transaction_date\` of the earliest salary credit + +**Pattern: ROW_NUMBER deduplication** + +Use a single CTE \`ranked\`: +\`\`\`sql +WITH ranked AS ( + SELECT t.account_id, c.customer_name, t.amount, + t.transaction_date AS first_salary_date, + ROW_NUMBER() OVER ( + PARTITION BY t.account_id + ORDER BY t.transaction_date ASC + ) AS rn + FROM transactions t + JOIN accounts a ON t.account_id = a.account_id + JOIN customers c ON a.customer_id = c.customer_id + WHERE t.merchant_category = 'Salary Credit' +) +SELECT account_id, customer_name, amount, first_salary_date + FROM ranked + WHERE rn = 1 + ORDER BY first_salary_date +\`\`\` + +\`PARTITION BY account_id ORDER BY transaction_date ASC\` assigns \`rn = 1\` to the earliest salary transaction per account. \`WHERE rn = 1\` in the outer query selects exactly one row per account — the dedup step. + +This ROW_NUMBER PARTITION BY pattern is the universal deduplication and **latest/earliest record per group** technique used across every SQL dialect (BigQuery, Redshift, Snowflake, Spark SQL, PostgreSQL) in ETL pipelines, slowly-changing dimension snapshots, and data-quality remediations at FAANG, hedge funds, and GIC.`, + hint: 'CTE ranked: SELECT t.account_id, c.customer_name, t.amount, t.transaction_date AS first_salary_date, ROW_NUMBER() OVER (PARTITION BY t.account_id ORDER BY t.transaction_date ASC) AS rn FROM transactions t JOIN accounts a ON t.account_id=a.account_id JOIN customers c ON a.customer_id=c.customer_id WHERE t.merchant_category=\'Salary Credit\'. Outer: SELECT account_id, customer_name, amount, first_salary_date FROM ranked WHERE rn=1 ORDER BY first_salary_date.', + seedQuery: `WITH ranked AS ( + SELECT + t.account_id, + c.customer_name, + t.amount, + t.transaction_date AS first_salary_date, + ROW_NUMBER() OVER ( + PARTITION BY + ORDER BY t.transaction_date + ) AS rn + FROM transactions t + JOIN accounts a ON t.account_id = a.account_id + JOIN customers c ON a.customer_id = c.customer_id + WHERE t.merchant_category = +) +SELECT account_id, customer_name, amount, first_salary_date + FROM ranked + WHERE rn = + ORDER BY first_salary_date`, + solutionQuery: `WITH ranked AS ( + SELECT + t.account_id, + c.customer_name, + t.amount, + t.transaction_date AS first_salary_date, + ROW_NUMBER() OVER ( + PARTITION BY t.account_id + ORDER BY t.transaction_date ASC + ) AS rn + FROM transactions t + JOIN accounts a ON t.account_id = a.account_id + JOIN customers c ON a.customer_id = c.customer_id + WHERE t.merchant_category = 'Salary Credit' +) +SELECT account_id, customer_name, amount, first_salary_date + FROM ranked + WHERE rn = 1 + ORDER BY first_salary_date`, + epoch: 'Expert', + difficulty: 4, + }, ];