Phase 4 (auth + payments) is live in production and independently audited
against its original GitHub acceptance criteria (docs/querytuner-phase4-audit.md)
— the one live gap the audit found (#53, unenforced Pro-tier LLM routing)
was fixed the same day. Also includes the first batch of Phase 5 quick
wins, the EXPLAIN plan parser chain, Quiz Mode, batch workload analysis,
the report management dashboard's completion, and three bugs found
live-testing the deployed site.
Added
- Client-side query sanitizer (commit 042a23f):
- Table and column names replaced with dummy values (
table_a,col_b) in browser
before any data sent to server. Real schema names never reach QueryTuner's backend
or Supabase. - Substitution map stored in React
useStateonly — never localStorage, never
server, gone on page refresh. - "Restore original names" button on DDL output applies reverse substitution —
produces copy-pasteable DDL with real table names. - Same map applied to EXPLAIN plan if provided, for consistency.
- 15 tests via vitest.
- Table and column names replaced with dummy values (
- 4 new heuristic rules (total now 16):
not_in_nullable(high, deterministic) — NOT IN with nullable subquery returns zero rowscase_in_predicate(high, deterministic) — CASE in WHERE prevents index useor_expansion(medium, estimated) — OR on different columns may force full scancte_multiple_references(medium, estimated) — CTE referenced 2+ times may re-execute
- Three-tier evidence labelling system: deterministic / schema-verified / needs-runtime-evidence
- Rollback DDL per dialect on all index recommendations (5 dialect-correct statements)
- Privacy warning on share button and report page
- AI Insights section on shareable report page
- Report page brought to full parity with main app: evidence badges, DDL blocks, rollback
toggles, Query Diagnosis section, AI Insights findKey()resilient JSON parser for AI output — handles key name variations across LLM
providers- OpenAI (recommended) label added to AI provider dropdown (UI label change only — provider
selection remains user's choice) - Phase 4: Authentication + Stripe payments
- Query history for Pro users —
GET /history, gated server-side on Pro status (#54,db2f1306) - Shareable report link expiration (90-day default) + owner-initiated early revoke via
DELETE /report/{id}(#116,860950e3) - Composite index recommendations now order columns by standard convention (equality →
JOIN → range → sort) instead of raw extraction order (#117,7121cc12) - Write/storage cost estimate alongside every index recommendation's existing read-side
benefit estimate (#118,4e747b95) - EXPLAIN plan parser chain — full acceptance-criteria coverage: remaining Postgres node
types (Merge Join, Hash Aggregate, Group Aggregate) + EXPLAIN ANALYZE actual-time/rows
parsing, MySQL plain tabular EXPLAIN parsing + explicitkey=NULLflagging +Using filesort/Using temporarydetection, andfull_scan_risk/order_by_no_limit/
function_in_wherecross-referencing against the parsed plan (#61, #62, #63,428facb5) - Quiz Mode — before revealing the analysis, up to 2–3 interview-style multiple-choice
questions generated from the query's own findings (confidence-gated: only
evidence_level !== "needs-runtime-evidence"suggestions are used as answer keys),
with a "Skip quiz" path and full reveal underneath either way. Free-tier feature,
not Pro-gated. (PR #153,247c7d58) - Batch workload analysis —
POST /analyze/batch(Pro-gated) accepts a pasted export
from PostgreSQLpg_stat_statements, MySQLperformance_schema, or SQL Server
Query Store, ranks top-N queries by production cost, runs each through the existing
index recommender, and reconciles overlapping/redundant index suggestions across
queries into one deduplicated set rather than N independent recommendation lists
(#115, #120, PR #154,90b7fd0b) - User report dashboard (History page) completed — sanitized/unsanitized indicator
and a "Show sanitized only" filter (newanalyses.was_sanitizedcolumn, migration
009, self-reported by the client's own sanitizer state), a delete button wired to
the existingDELETE /report/{id}(#116), and a copy-link button per row (#124,
PR #159,aedf65fe)
Fixed
schema_verifiedterminology replaces "confirmed" — more accurate, does not imply planner
will use the index- CSS wildcard reset specificity conflict on report page — was zeroing Tailwind utility
classes, fixed with:where()wrapper - Markdown fence regex anchoring in AI JSON parser — trailing LLM prose after closing fence
was causing raw JSON display - QueryInput help text updated to schema-verified
- MySQL backtick-quoted identifier columns (
`status`etc.) were silently dropped
from WHERE/comparison-operator extraction —IndexRecommender's column-extraction
regexes only matched double-quoted or bare identifiers. This affected all
single-query MySQL analysis, not just the new batch mode that surfaced it. (PR #154,
90b7fd0b) - History page items linked via a plain
<a href>, causing a full page reload on every
click despite the app usingreact-router-domthroughout — switched to<Link>for
client-side transitions (PR #155,221a23b9) - Shared report page (
/report/:id) had no way back into the app — both nav links
pointed to the external marketing site, a leftover from before the History feature
existed and started linking into this page. Signed-in users now see a "← Back to
history" link in the nav (PR #155,221a23b9) - OpenAI (GPT-4o-mini) Pro-tier gating was not enforced server-side — a signed-in
free-tier user could select "OpenAI" in the AI-provider dropdown and the backend
would honor it, running real GPT-4o-mini calls against Pro's cost budget with
nothing checkingis_proanywhere in the call path.POST /analyzenow returns a
structured403 pro_requiredfor this case; the dropdown also disables the option
client-side for non-Pro users. Found during a full Phase 4 audit, fixed same-day
(#53, PR #156,ecd66de7) - Quiz Mode's AI Insights panel leaked the quiz answer before reveal — the
panel's render condition had no reference to the quiz's reveal state at all, so it
rendered unconditionally whenever AI insights was on, showing the recommended DDL
right under an unanswered question. Now gated behind the same quiz-then-reveal
conditionOptimizationSuggestionsalready uses. Found live-testing the deployed
site (PR #157,7746df08) - AI provider dropdown could get stuck showing a disabled "OpenAI (Pro only)" option
as selected with no way back (a stale selection from before the#53Pro-gate, or
after Pro status lapsed), and confirmed Pro users weren't defaulted to OpenAI
either despite it being the recommended, already-paid-for option. Now resets a
stale/disallowed selection automatically and auto-selects OpenAI once for a fresh
Pro session, without fighting a later manual switch back (PR #157,7746df08) - CORS
allow_originswas"*"— a known dev-mode leftover flagged ahead of launch
readiness. Replaced with a real allowlist (settings.frontend_url+
localhost:3000for local dev), reusing the existingFRONTEND_URLsetting
already used elsewhere — no new env var (PR #158,deacb6ca)
Tests
- 316 backend tests passing (1 intentional xfail — LATERAL join correlated-column
detection), up from 102 at v0.2.0 - 59 frontend tests passing (vitest) — all added this cycle; v0.2.0 had none