From fd8afe59df01820b1f8bacc1dd3acc2400532779 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Mon, 13 Apr 2026 23:18:38 -0600 Subject: [PATCH] fix: redesign SQL console UX, add data model context, fix lab bugs - Redesign SQL Console: always-visible terminal-style panel with Primary/Replica toggle buttons, mysql> prompt, clear button, green-on-black output theme - Add Data Model section to LAB explaining the university enrollment schema before Task 1 - Strip \G suffix from SQL queries (mysql CLI feature, not valid SQL) - Fix wrong table alias in LAB SQL example (c.code -> code) - Use approximate row count (~10,000) in LAB text --- 10-databases/visualizer/LAB-VISUALIZER.md | 20 +++- 10-databases/visualizer/app.js | 20 +++- 10-databases/visualizer/index.html | 26 ++--- 10-databases/visualizer/server.py | 7 +- 10-databases/visualizer/style.css | 128 ++++++++++++++++------ 5 files changed, 152 insertions(+), 49 deletions(-) diff --git a/10-databases/visualizer/LAB-VISUALIZER.md b/10-databases/visualizer/LAB-VISUALIZER.md index 95c1dad..48b3248 100644 --- a/10-databases/visualizer/LAB-VISUALIZER.md +++ b/10-databases/visualizer/LAB-VISUALIZER.md @@ -166,6 +166,22 @@ graph LR └── style.css # Dark theme styling ``` +## Data Model + +The lab uses a **university enrollment** scenario pre-loaded with +sample data: + +| Table | Rows | Description | +| --- | --- | --- | +| `students` | 10 | Name, email, major (e.g., Alice Johnson, Computer Science) | +| `courses` | 4 | CS101, CS201, MATH101, PHYS101 with capacity and enrolled count | +| `enrollments` | 10 | Links students to courses (foreign keys with unique constraint) | +| `access_log` | 10,000 | Simulated resource access records for indexing exercises | + +This is the same data model used across all Module 10 labs (MySQL, +MongoDB, Cassandra), so you can compare how each database handles the +same scenario differently. + --- ## Task 1: Explore the Environment @@ -342,7 +358,7 @@ remain unchanged. In the SQL Console (Primary), run: ```sql -SELECT c.code, c.enrolled FROM courses ORDER BY code; +SELECT code, enrolled FROM courses ORDER BY code; ``` Confirm the counts match what the sidebar shows. The failed @@ -376,7 +392,7 @@ accessing resource-10. 1. Set Student ID to `3`, Resource to `resource-10` 1. Click **Run EXPLAIN** -The result shows **Rows scanned: ~9,894** in red, with Key: **NONE +The result shows **Rows scanned: ~10,000** in red, with Key: **NONE (full scan)**. MySQL examined nearly all 10,000 rows to find a handful of matches. Check the sidebar: Indexes shows **None**: diff --git a/10-databases/visualizer/app.js b/10-databases/visualizer/app.js index 8c1f5fb..aacdca0 100644 --- a/10-databases/visualizer/app.js +++ b/10-databases/visualizer/app.js @@ -473,7 +473,7 @@ async function doSqlExec() { const query = input.value.trim(); if (!query) return; - const target = document.querySelector('input[name="sql-target"]:checked').value; + const target = document.querySelector('.target-btn.active')?.dataset.target || 'primary'; const output = $('#console-output'); // Add to history @@ -564,6 +564,24 @@ function initConsole() { consoleHistoryIndex >= 0 ? consoleHistory[consoleHistoryIndex] : ''; } }); + + // Target toggle buttons + $$('.target-btn').forEach(btn => { + btn.addEventListener('click', () => { + $$('.target-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const target = btn.dataset.target; + const prompt = $('.console-prompt'); + if (prompt) { + prompt.textContent = target === 'replica' ? 'replica>' : 'mysql>'; + } + }); + }); + + // Clear button + $('#console-clear').addEventListener('click', () => { + $('#console-output').innerHTML = ''; + }); } function initButtons() { diff --git a/10-databases/visualizer/index.html b/10-databases/visualizer/index.html index 1c75233..76718ad 100644 --- a/10-databases/visualizer/index.html +++ b/10-databases/visualizer/index.html @@ -260,20 +260,20 @@

Event Log

-
- SQL Console -
-
- - -
-
-
- - -
+
+ SQL Console +
+ +
-
+ +
+
+
+ mysql> + + +
diff --git a/10-databases/visualizer/server.py b/10-databases/visualizer/server.py index 1ba7138..28fd31e 100644 --- a/10-databases/visualizer/server.py +++ b/10-databases/visualizer/server.py @@ -368,8 +368,13 @@ def sql_exec(body): if not query: return {"error": "Empty query"} + # Strip mysql CLI formatting suffix (\G) -- not valid SQL + query = query.rstrip(";").rstrip() + if query.endswith("\\G"): + query = query[:-2].rstrip() + # Reject multi-statement queries - if ";" in query.rstrip(";"): + if ";" in query: return {"error": "Multi-statement queries are not allowed"} # Allowlist: only permit known safe SQL commands diff --git a/10-databases/visualizer/style.css b/10-databases/visualizer/style.css index f105e05..e47fb04 100644 --- a/10-databases/visualizer/style.css +++ b/10-databases/visualizer/style.css @@ -435,12 +435,12 @@ h3 { color: var(--color-text-muted); } -/* --- Bottom Bar (Event Log) --- */ +/* --- Bottom Bar --- */ .bottom-bar { background: var(--color-bg-surface); border-top: 1px solid var(--color-border); padding: 0.75rem 1rem; - max-height: var(--bottom-bar-height); + height: 260px; } .event-log { @@ -519,68 +519,121 @@ h3 { .sql-console { flex: 1; min-width: 0; + display: flex; + flex-direction: column; + background: #0a0e17; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + overflow: hidden; } -.sql-console details { - height: 100%; +.console-header { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--color-border); } -.sql-console summary { - cursor: pointer; - font-size: 0.9rem; +.console-title { + font-size: 0.8rem; font-weight: 600; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; - margin-bottom: 0.5rem; } -.console-body { +.console-target-toggle { display: flex; - flex-direction: column; - gap: 0.5rem; + background: var(--color-bg); + border-radius: 6px; + padding: 2px; + gap: 2px; } -.console-target { - display: flex; - gap: 1rem; - font-size: 0.8rem; +.target-btn { + padding: 3px 12px; + border: none; + border-radius: 4px; + background: transparent; color: var(--color-text-muted); + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; } -.console-target label { - display: flex; - align-items: center; - gap: 4px; +.target-btn.active { + background: var(--color-primary); + color: #fff; +} + +.target-btn:hover:not(.active) { + color: var(--color-text); +} + +.console-clear-btn { + margin-left: auto; + padding: 2px 8px; + border: 1px solid var(--color-border); + border-radius: 4px; + background: transparent; + color: var(--color-text-muted); + font-size: 0.7rem; cursor: pointer; + transition: all 0.15s ease; +} + +.console-clear-btn:hover { + color: var(--color-error); + border-color: var(--color-error); } .console-output { font-family: var(--font-mono); font-size: 0.75rem; - background: var(--color-bg); - border-radius: var(--radius); - padding: 0.5rem; - max-height: 100px; + background: #0a0e17; + padding: 8px 12px; + flex: 1; overflow-y: auto; white-space: pre-wrap; - color: var(--color-text); + color: #a3e635; } .console-input-row { display: flex; - gap: 0.5rem; + align-items: center; + gap: 0; + border-top: 1px solid var(--color-border); + background: rgba(255, 255, 255, 0.02); +} + +.console-prompt { + padding: 8px 4px 8px 12px; + color: #22d3ee; + font-family: var(--font-mono); + font-size: 0.8rem; + font-weight: 600; + white-space: nowrap; } .console-input-row input { flex: 1; - padding: 6px 10px; - border-radius: var(--radius); - border: 1px solid var(--color-border); - background: var(--color-bg); + padding: 8px 8px; + border: none; + background: transparent; color: var(--color-text); font-family: var(--font-mono); font-size: 0.8rem; + outline: none; +} + +.console-input-row .btn { + border-radius: 0; + padding: 8px 16px; + border: none; + border-left: 1px solid var(--color-border); } .console-result-table { @@ -602,14 +655,25 @@ h3 { } .console-query { - color: var(--color-primary); + color: #22d3ee; + margin-top: 6px; } .console-error { - color: var(--color-error); + color: #f87171; } .console-meta { - color: var(--color-text-muted); + color: #94a3b8; font-size: 0.7rem; + margin-bottom: 4px; +} + +.console-result-table th { + color: #a3e635; + background: rgba(163, 230, 53, 0.08); +} + +.console-result-table td { + color: #e2e8f0; }