From 5e32d7df0fb35d40d5128c70aee36879e8454316 Mon Sep 17 00:00:00 2001 From: Berk Durmus Date: Mon, 26 Jan 2026 00:09:34 +0300 Subject: [PATCH] feat(sql-minifier): add sql minifier --- components/seo/SQLMinifierSEO.tsx | 14 +- components/utils/sql-summary.utils.ts | 353 ++++++++++++++++++++++++++ pages/utilities/sql-minifier.tsx | 230 ++++++++++++++++- 3 files changed, 590 insertions(+), 7 deletions(-) create mode 100644 components/utils/sql-summary.utils.ts diff --git a/components/seo/SQLMinifierSEO.tsx b/components/seo/SQLMinifierSEO.tsx index 038da78..c7f0e84 100644 --- a/components/seo/SQLMinifierSEO.tsx +++ b/components/seo/SQLMinifierSEO.tsx @@ -6,9 +6,11 @@ export default function SQLMinifierSEO() {

Transform your SQL queries with our free online SQL minifier tool. Instantly remove comments, unnecessary whitespace, and line breaks to - create compact, optimized SQL code. Perfect for reducing query size by - up to 50% while maintaining full functionality. Works with MySQL, - PostgreSQL, SQL Server, Oracle, and SQLite. + create compact, optimized SQL code. Generate AI-assisted summaries and + risk flags to quickly understand what a query does before you ship it. + Perfect for reducing query size by up to 50% while maintaining full + functionality. Works with MySQL, PostgreSQL, SQL Server, Oracle, and + SQLite.

@@ -40,6 +42,12 @@ export default function SQLMinifierSEO() { comments (-- and /* */), extra spaces, tabs, and line breaks while preserving the exact query logic and results. +
  • + Can I get a plain-English summary of my SQL? +
    + Yes. Add your API key and generate a concise human summary plus + common risk flags like missing WHERE clauses or expensive joins. +
  • How much can SQL minification improve performance?
    diff --git a/components/utils/sql-summary.utils.ts b/components/utils/sql-summary.utils.ts new file mode 100644 index 0000000..8767a8f --- /dev/null +++ b/components/utils/sql-summary.utils.ts @@ -0,0 +1,353 @@ +export type SqlSummaryProvider = "openai" | "anthropic"; + +export type SqlRiskSeverity = "low" | "medium" | "high"; + +export interface SqlRiskFlag { + id: string; + title: string; + description: string; + severity: SqlRiskSeverity; +} + +export interface SqlSummaryResult { + summary: string; + cautions: string[]; +} + +const OPENAI_ENDPOINT = "https://api.openai.com/v1/chat/completions"; +const ANTHROPIC_ENDPOINT = "https://api.anthropic.com/v1/messages"; + +const SUMMARY_SYSTEM_PROMPT = + "You are a senior database engineer. Return JSON only with keys: summary (string, <= 2 sentences) and cautions (array of short strings). No markdown, no code fences."; + +const SUMMARY_USER_PROMPT = (sql: string) => + `Analyze this SQL and respond with JSON only:\n${sql}`; + +const ensureString = (value: unknown): string => + typeof value === "string" ? value : ""; + +const parseJsonFromText = (text: string): SqlSummaryResult => { + const trimmed = text.trim(); + if (!trimmed) { + return { summary: "", cautions: [] }; + } + + const firstBrace = trimmed.indexOf("{"); + const lastBrace = trimmed.lastIndexOf("}"); + if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { + const slice = trimmed.slice(firstBrace, lastBrace + 1); + try { + const parsed = JSON.parse(slice); + return { + summary: ensureString(parsed.summary), + cautions: Array.isArray(parsed.cautions) + ? parsed.cautions.map(ensureString).filter(Boolean) + : [], + }; + } catch (error) { + // fall through to plain text fallback + } + } + + return { summary: trimmed, cautions: [] }; +}; + +export async function summarizeSqlWithLLM( + sql: string, + provider: SqlSummaryProvider, + apiKey: string +): Promise { + if (!apiKey) { + throw new Error("Missing API key"); + } + + if (!sql.trim()) { + return { summary: "", cautions: [] }; + } + + if (provider === "openai") { + const response = await fetch(OPENAI_ENDPOINT, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "gpt-4o-mini", + temperature: 0.2, + messages: [ + { role: "system", content: SUMMARY_SYSTEM_PROMPT }, + { role: "user", content: SUMMARY_USER_PROMPT(sql) }, + ], + }), + }); + + if (!response.ok) { + throw new Error("OpenAI request failed"); + } + + const data = await response.json(); + const content = ensureString(data?.choices?.[0]?.message?.content); + return parseJsonFromText(content); + } + + const response = await fetch(ANTHROPIC_ENDPOINT, { + method: "POST", + headers: { + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "claude-3-haiku-20240307", + temperature: 0.2, + max_tokens: 400, + system: SUMMARY_SYSTEM_PROMPT, + messages: [{ role: "user", content: SUMMARY_USER_PROMPT(sql) }], + }), + }); + + if (!response.ok) { + throw new Error("Anthropic request failed"); + } + + const data = await response.json(); + const content = ensureString(data?.content?.[0]?.text); + return parseJsonFromText(content); +} + +const stripCommentsPreserveStrings = (sql: string): string => { + let result = ""; + let i = 0; + let inSingle = false; + let inDouble = false; + + while (i < sql.length) { + const char = sql[i]; + const next = i + 1 < sql.length ? sql[i + 1] : ""; + + if (inSingle) { + result += char; + if (char === "'" && next === "'") { + result += next; + i += 2; + continue; + } + if (char === "'") { + inSingle = false; + } + i++; + continue; + } + + if (inDouble) { + result += char; + if (char === '"' && next === '"') { + result += next; + i += 2; + continue; + } + if (char === '"') { + inDouble = false; + } + i++; + continue; + } + + if (char === "'" && !inDouble) { + inSingle = true; + result += char; + i++; + continue; + } + + if (char === '"' && !inSingle) { + inDouble = true; + result += char; + i++; + continue; + } + + if (char === "/" && next === "*") { + i += 2; + while (i < sql.length - 1) { + if (sql[i] === "*" && sql[i + 1] === "/") { + i += 2; + break; + } + i++; + } + continue; + } + + if (char === "-" && next === "-") { + i += 2; + while (i < sql.length && sql[i] !== "\n" && sql[i] !== "\r") { + i++; + } + continue; + } + + result += char; + i++; + } + + return result; +}; + +const splitStatements = (sql: string): string[] => { + const statements: string[] = []; + let buffer = ""; + let inSingle = false; + let inDouble = false; + + for (let i = 0; i < sql.length; i++) { + const char = sql[i]; + const next = i + 1 < sql.length ? sql[i + 1] : ""; + + if (inSingle) { + buffer += char; + if (char === "'" && next === "'") { + buffer += next; + i++; + continue; + } + if (char === "'") inSingle = false; + continue; + } + + if (inDouble) { + buffer += char; + if (char === '"' && next === '"') { + buffer += next; + i++; + continue; + } + if (char === '"') inDouble = false; + continue; + } + + if (char === "'") { + inSingle = true; + buffer += char; + continue; + } + + if (char === '"') { + inDouble = true; + buffer += char; + continue; + } + + if (char === ";") { + if (buffer.trim()) statements.push(buffer.trim()); + buffer = ""; + continue; + } + + buffer += char; + } + + if (buffer.trim()) statements.push(buffer.trim()); + return statements; +}; + +const normalizeForAnalysis = (sql: string): string => + stripCommentsPreserveStrings(sql) + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + +const extractClause = (statement: string, clause: string): string => { + const regex = new RegExp( + `${clause}\\s+(.+?)(where|group by|order by|limit|having|$)`, + "i" + ); + const match = statement.match(regex); + return match ? match[1].trim() : ""; +}; + +export function deriveSqlRisks(sql: string): SqlRiskFlag[] { + const risks: SqlRiskFlag[] = []; + const withoutComments = stripCommentsPreserveStrings(sql); + const statements = splitStatements(withoutComments); + + statements.forEach((statement, index) => { + const normalized = normalizeForAnalysis(statement); + const statementLabel = statements.length > 1 ? `Statement ${index + 1}` : ""; + + const addRisk = (risk: Omit) => { + risks.push({ + id: `${risk.title}-${index}`, + ...risk, + }); + }; + + if (/^\s*update\b/.test(normalized) && !/\bwhere\b/.test(normalized)) { + addRisk({ + title: "UPDATE without WHERE", + description: `${statementLabel} updates all rows without a filter.`, + severity: "high", + }); + } + + if (/^\s*delete\b/.test(normalized) && !/\bwhere\b/.test(normalized)) { + addRisk({ + title: "DELETE without WHERE", + description: `${statementLabel} deletes all rows without a filter.`, + severity: "high", + }); + } + + const fromClause = extractClause(statement, "from"); + const hasCommaJoin = /,/.test(fromClause); + const hasJoinKeyword = /\bjoin\b/i.test(statement); + const hasWhere = /\bwhere\b/i.test(statement); + + if (hasCommaJoin && !hasJoinKeyword && !hasWhere) { + addRisk({ + title: "Possible Cartesian join", + description: `${statementLabel} uses multiple tables without join conditions.`, + severity: "high", + }); + } + + if ( + /\bselect\s+\*/i.test(statement) && + (hasCommaJoin || hasJoinKeyword) + ) { + addRisk({ + title: "SELECT * on multi-table query", + description: `${statementLabel} selects all columns across multiple tables.`, + severity: "medium", + }); + } + + if (/like\s+['"]%.*%['"]/i.test(statement)) { + addRisk({ + title: "Leading wildcard LIKE", + description: `${statementLabel} uses LIKE with leading wildcard, which can be slow.`, + severity: "medium", + }); + } + + const whereClause = extractClause(statement, "where"); + if (whereClause && /\sor\s/i.test(whereClause) && !/[()]/.test(whereClause)) { + addRisk({ + title: "OR chain without grouping", + description: `${statementLabel} has OR conditions without parentheses.`, + severity: "low", + }); + } + + if (/order\s+by\s+(rand|random)\s*\(\s*\)/i.test(statement)) { + addRisk({ + title: "ORDER BY RAND()", + description: `${statementLabel} uses random ordering, which can be expensive.`, + severity: "medium", + }); + } + }); + + return risks; +} diff --git a/pages/utilities/sql-minifier.tsx b/pages/utilities/sql-minifier.tsx index 5009e39..8c47d9a 100644 --- a/pages/utilities/sql-minifier.tsx +++ b/pages/utilities/sql-minifier.tsx @@ -1,9 +1,10 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Textarea } from "@/components/ds/TextareaComponent"; import PageHeader from "@/components/PageHeader"; import { Card } from "@/components/ds/CardComponent"; import { Button } from "@/components/ds/ButtonComponent"; import { Label } from "@/components/ds/LabelComponent"; +import { Input } from "@/components/ds/InputComponent"; import Header from "@/components/Header"; import { useCopyToClipboard } from "@/components/hooks/useCopyToClipboard"; import { CMDK } from "@/components/CMDK"; @@ -14,6 +15,12 @@ import { minifySQL, validateSQLInput, } from "@/components/utils/sql-minifier.utils"; +import { + deriveSqlRisks, + SqlRiskFlag, + SqlSummaryProvider, + summarizeSqlWithLLM, +} from "@/components/utils/sql-summary.utils"; import GitHubContribution from "@/components/GitHubContribution"; export default function SQLMinifier() { @@ -21,6 +28,47 @@ export default function SQLMinifier() { const [output, setOutput] = useState(""); const [error, setError] = useState(""); const { buttonText, handleCopy } = useCopyToClipboard(); + const [provider, setProvider] = useState("openai"); + const [openAiKey, setOpenAiKey] = useState(""); + const [anthropicKey, setAnthropicKey] = useState(""); + const [analysisSummary, setAnalysisSummary] = useState(""); + const [analysisCautions, setAnalysisCautions] = useState([]); + const [riskFlags, setRiskFlags] = useState([]); + const [analysisError, setAnalysisError] = useState(""); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [hasAnalyzed, setHasAnalyzed] = useState(false); + + useEffect(() => { + if (typeof window === "undefined") return; + const storedProvider = window.localStorage.getItem("sql-summary-provider"); + const storedOpenAiKey = window.localStorage.getItem("sql-summary-openai-key"); + const storedAnthropicKey = window.localStorage.getItem( + "sql-summary-anthropic-key" + ); + + if (storedProvider === "openai" || storedProvider === "anthropic") { + setProvider(storedProvider); + } + if (storedOpenAiKey) setOpenAiKey(storedOpenAiKey); + if (storedAnthropicKey) setAnthropicKey(storedAnthropicKey); + }, []); + + useEffect(() => { + if (typeof window === "undefined") return; + window.localStorage.setItem("sql-summary-provider", provider); + }, [provider]); + + useEffect(() => { + if (typeof window === "undefined") return; + window.localStorage.setItem("sql-summary-openai-key", openAiKey); + }, [openAiKey]); + + useEffect(() => { + if (typeof window === "undefined") return; + window.localStorage.setItem("sql-summary-anthropic-key", anthropicKey); + }, [anthropicKey]); + + const activeApiKey = provider === "openai" ? openAiKey : anthropicKey; const handleChange = useCallback( (event: React.ChangeEvent) => { @@ -30,6 +78,11 @@ export default function SQLMinifier() { if (value.trim() === "") { setOutput(""); + setAnalysisSummary(""); + setAnalysisCautions([]); + setRiskFlags([]); + setAnalysisError(""); + setHasAnalyzed(false); return; } @@ -37,6 +90,11 @@ export default function SQLMinifier() { if (!validation.isValid) { setError(validation.error || "Invalid input"); setOutput(""); + setAnalysisSummary(""); + setAnalysisCautions([]); + setRiskFlags([]); + setAnalysisError(""); + setHasAnalyzed(false); return; } @@ -48,11 +106,44 @@ export default function SQLMinifier() { err instanceof Error ? err.message : "Failed to minify SQL"; setError(errorMessage); setOutput(""); + setAnalysisError(""); } }, [] ); + const handleAnalyze = useCallback(async () => { + if (!input.trim() || error || !activeApiKey) return; + setIsAnalyzing(true); + setAnalysisError(""); + setHasAnalyzed(true); + try { + const risks = deriveSqlRisks(input); + setRiskFlags(risks); + const summary = await summarizeSqlWithLLM(input, provider, activeApiKey); + setAnalysisSummary(summary.summary); + setAnalysisCautions(summary.cautions); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Failed to analyze SQL"; + setAnalysisError(errorMessage); + } finally { + setIsAnalyzing(false); + } + }, [activeApiKey, error, input, provider]); + + const summaryPlaceholder = isAnalyzing + ? "Analyzing SQL..." + : hasAnalyzed + ? "Summary unavailable." + : "Run analysis to get a human summary."; + + const riskPlaceholder = isAnalyzing + ? "Scanning for common SQL risks..." + : hasAnalyzed + ? "No obvious risks found." + : "Run analysis to see risk flags."; + return (
    -
    +
    -
    +
    -
    +