From 9f1aa43ec8b8972d8145a0658c9e6cbf8c55df29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:32:40 +0000 Subject: [PATCH 1/2] Initial plan From 174fdd2bdc0ead56a3ddf8d22faa0c9763f92021 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:50:14 +0000 Subject: [PATCH 2/2] Implement SQL Minifier with comprehensive tests and fixes for all issues Co-authored-by: peckz <18050177+peckz@users.noreply.github.com> --- components/seo/SQLMinifierSEO.tsx | 73 +++++++ components/utils/sql-minifier.utils.test.ts | 183 +++++++++++++++++ components/utils/sql-minifier.utils.ts | 205 ++++++++++++++++++++ components/utils/tools-list.ts | 6 + pages/utilities/sql-minifier.tsx | 103 ++++++++++ 5 files changed, 570 insertions(+) create mode 100644 components/seo/SQLMinifierSEO.tsx create mode 100644 components/utils/sql-minifier.utils.test.ts create mode 100644 components/utils/sql-minifier.utils.ts create mode 100644 pages/utilities/sql-minifier.tsx diff --git a/components/seo/SQLMinifierSEO.tsx b/components/seo/SQLMinifierSEO.tsx new file mode 100644 index 0000000..73059a7 --- /dev/null +++ b/components/seo/SQLMinifierSEO.tsx @@ -0,0 +1,73 @@ +export default function SQLMinifierSEO() { + return ( +
+
+

+ You can use this SQL Minifier to clean and optimize your SQL queries + by removing comments and extra spaces. Just paste your SQL and get a + minified result instantly. Made with 💜 by the developers building + Jam. +

+
+ +
+

How to Use the SQL Minifier

+

+ Whether you're working with large queries or optimizing database + performance, our SQL Minifier helps streamline your SQL code in + seconds—no signup required. +

+

+ The tool ensures your SQL remains functional while improving + readability and execution speed. +

+
+ +
+

Benefits of Minifying SQL

+

+ Minified SQL reduces unnecessary whitespace and removes comments, + making queries more efficient and easier to process. +

+ +
+ +
+

FAQs

+ +
+
+ ); +} \ No newline at end of file diff --git a/components/utils/sql-minifier.utils.test.ts b/components/utils/sql-minifier.utils.test.ts new file mode 100644 index 0000000..2337c74 --- /dev/null +++ b/components/utils/sql-minifier.utils.test.ts @@ -0,0 +1,183 @@ +import { minifySQL, validateSQLInput } from './sql-minifier.utils'; + +describe('sql-minifier.utils', () => { + describe('minifySQL', () => { + test('should handle basic SQL without comments', () => { + const input = 'SELECT * FROM users WHERE id = 1'; + const expected = 'SELECT * FROM users WHERE id = 1'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should remove single-line comments', () => { + const input = `SELECT * FROM users -- this is a comment +WHERE id = 1`; + const expected = 'SELECT * FROM users WHERE id = 1'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should remove multi-line comments', () => { + const input = `SELECT * /* this is a +multi-line comment */ FROM users WHERE id = 1`; + const expected = 'SELECT * FROM users WHERE id = 1'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should preserve strings with spaces', () => { + const input = `SELECT 'hello world' FROM users WHERE name = 'John Doe'`; + const expected = `SELECT 'hello world' FROM users WHERE name = 'John Doe'`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should not remove double dashes inside strings', () => { + const input = `SELECT 'this -- is not a comment' FROM users`; + const expected = `SELECT 'this -- is not a comment' FROM users`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle single quotes inside double quotes', () => { + const input = `SELECT "It's a test" FROM users`; + const expected = `SELECT "It's a test" FROM users`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle double quotes inside single quotes', () => { + const input = `SELECT 'He said "hello"' FROM users`; + const expected = `SELECT 'He said "hello"' FROM users`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle escaped quotes in strings', () => { + const input = `SELECT 'It''s a test' FROM users WHERE name = "John ""Big"" Doe"`; + const expected = `SELECT 'It''s a test' FROM users WHERE name = "John ""Big"" Doe"`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle complex query with mixed content', () => { + const input = ` + SELECT + u.name, -- user name + u.email, + p.title /* post title */ + FROM users u + JOIN posts p ON u.id = p.user_id + WHERE u.name = 'John -- not a comment' + AND p.created_at > '2023-01-01' + /* AND p.status = 'published' -- this is commented out */ + `; + const expected = `SELECT u.name, u.email, p.title FROM users u JOIN posts p ON u.id = p.user_id WHERE u.name = 'John -- not a comment' AND p.created_at > '2023-01-01'`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle comments at end of line correctly', () => { + const input = `SELECT * FROM users WHERE id = 1 -- comment`; + const expected = 'SELECT * FROM users WHERE id = 1'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle comments in middle of line', () => { + const input = `SELECT * /* comment */ FROM users`; + const expected = 'SELECT * FROM users'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle multiple consecutive comments', () => { + const input = `SELECT * -- comment1 +-- comment2 +FROM users /* comment3 */ /* comment4 */`; + const expected = 'SELECT * FROM users'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should preserve necessary whitespace around operators', () => { + const input = `SELECT * FROM users WHERE id=1 AND name<>'test'`; + const expected = `SELECT * FROM users WHERE id=1 AND name<>'test'`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle empty input', () => { + expect(minifySQL('')).toBe(''); + expect(minifySQL(' ')).toBe(''); + }); + + test('should handle input with only comments', () => { + const input = `-- just a comment +/* another comment */`; + expect(minifySQL(input)).toBe(''); + }); + + test('should throw error for non-string input', () => { + expect(() => minifySQL(null as unknown as string)).toThrow('Input must be a string'); + expect(() => minifySQL(undefined as unknown as string)).toThrow('Input must be a string'); + expect(() => minifySQL(123 as unknown as string)).toThrow('Input must be a string'); + }); + + test('should handle nested comment-like patterns in strings', () => { + const input = `SELECT 'Price: $/* not a comment */' FROM products`; + const expected = `SELECT 'Price: $/* not a comment */' FROM products`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle SQL with line breaks and tabs', () => { + const input = `SELECT\t*\nFROM\tusers\n\tWHERE\tid = 1`; + const expected = 'SELECT * FROM users WHERE id = 1'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle malformed comments gracefully', () => { + // Unclosed multi-line comment should be treated as comment to end of string + const input = `SELECT * FROM users /* unclosed comment`; + const expected = 'SELECT * FROM users'; + expect(minifySQL(input)).toBe(expected); + }); + + test('should preserve strings with newlines', () => { + const input = `SELECT 'line1\nline2' FROM users`; + const expected = `SELECT 'line1\nline2' FROM users`; + expect(minifySQL(input)).toBe(expected); + }); + + test('should handle multiple single-line comments on same line', () => { + const input = `SELECT * FROM users -- comment1 -- comment2`; + const expected = 'SELECT * FROM users'; + expect(minifySQL(input)).toBe(expected); + }); + }); + + describe('validateSQLInput', () => { + test('should validate correct string input', () => { + const result = validateSQLInput('SELECT * FROM users'); + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + test('should reject non-string input', () => { + const result = validateSQLInput(123 as unknown as string); + expect(result.isValid).toBe(false); + expect(result.error).toBe('Input must be a string'); + }); + + test('should reject empty input', () => { + const result = validateSQLInput(''); + expect(result.isValid).toBe(false); + expect(result.error).toBe('Input cannot be empty'); + }); + + test('should reject whitespace-only input', () => { + const result = validateSQLInput(' '); + expect(result.isValid).toBe(false); + expect(result.error).toBe('Input cannot be empty'); + }); + + test('should validate input with comments', () => { + const result = validateSQLInput('SELECT * FROM users -- comment'); + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + test('should validate input with strings containing special characters', () => { + const result = validateSQLInput(`SELECT 'test -- not comment' FROM users`); + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + }); +}); \ No newline at end of file diff --git a/components/utils/sql-minifier.utils.ts b/components/utils/sql-minifier.utils.ts new file mode 100644 index 0000000..3ab1d19 --- /dev/null +++ b/components/utils/sql-minifier.utils.ts @@ -0,0 +1,205 @@ +/** + * SQL Minifier utility that safely removes comments and unnecessary whitespace + * while preserving string literals and essential SQL syntax + */ + +/** + * Minifies SQL by removing comments and unnecessary whitespace while preserving string literals + */ +export function minifySQL(sql: string): string { + if (typeof sql !== 'string') { + throw new Error('Input must be a string'); + } + + if (sql.trim() === '') { + return ''; + } + + try { + let result = ''; + let i = 0; + + while (i < sql.length) { + const char = sql[i]; + + // Handle single-quoted strings + if (char === "'") { + let stringContent = "'"; + i++; // skip opening quote + + while (i < sql.length) { + stringContent += sql[i]; + if (sql[i] === "'") { + // Check if it's escaped (doubled quote) + if (i + 1 < sql.length && sql[i + 1] === "'") { + i++; // skip first quote + stringContent += sql[i]; // add second quote + } else { + // End of string + break; + } + } + i++; + } + + result += stringContent; + i++; + continue; + } + + // Handle double-quoted strings + if (char === '"') { + let stringContent = '"'; + i++; // skip opening quote + + while (i < sql.length) { + stringContent += sql[i]; + if (sql[i] === '"') { + // Check if it's escaped (doubled quote) + if (i + 1 < sql.length && sql[i + 1] === '"') { + i++; // skip first quote + stringContent += sql[i]; // add second quote + } else { + // End of string + break; + } + } + i++; + } + + result += stringContent; + i++; + continue; + } + + // Handle multi-line comments /* ... */ + if (char === '/' && i + 1 < sql.length && sql[i + 1] === '*') { + i += 2; // skip /* + + // Find closing */ or end of string + let found = false; + while (i < sql.length - 1) { + if (sql[i] === '*' && sql[i + 1] === '/') { + i += 2; // skip */ + found = true; + break; + } + i++; + } + + // If we didn't find closing */, we consumed everything to the end + if (!found) { + i = sql.length; + } + + // Add space if we removed a comment between words + if (result.length > 0 && /\w/.test(result.slice(-1))) { + // Look ahead to see if next non-whitespace character is a word character + let j = i; + while (j < sql.length && /\s/.test(sql[j])) { + j++; + } + if (j < sql.length && /\w/.test(sql[j])) { + result += ' '; + } + } + continue; + } + + // Handle single-line comments -- + if (char === '-' && i + 1 < sql.length && sql[i + 1] === '-') { + // Find end of line or end of string + while (i < sql.length && sql[i] !== '\n' && sql[i] !== '\r') { + i++; + } + + // Add space if we removed a comment between words and there's more content + if (result.length > 0 && /\w/.test(result.slice(-1))) { + // Look ahead to see if there's more content after the newline + let j = i; + while (j < sql.length && /[\r\n\s]/.test(sql[j])) { + j++; + } + if (j < sql.length && /\w/.test(sql[j])) { + result += ' '; + } + } + continue; + } + + // Handle regular characters and whitespace + if (/\s/.test(char)) { + // Replace multiple whitespace characters with single space + // but only if we don't already have a space at the end + if (result.length > 0 && !result.endsWith(' ')) { + result += ' '; + } + + // Skip additional whitespace + while (i + 1 < sql.length && /\s/.test(sql[i + 1])) { + i++; + } + } else { + // Regular character + result += char; + } + + i++; + } + + // Final cleanup + return result.trim(); + } catch (error) { + throw new Error(`Failed to minify SQL: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} + +/** + * Validates that the input is a valid string for SQL minification + */ +export function validateSQLInput(input: string): { isValid: boolean; error?: string } { + if (typeof input !== 'string') { + return { isValid: false, error: 'Input must be a string' }; + } + + if (input.trim() === '') { + return { isValid: false, error: 'Input cannot be empty' }; + } + + // Basic validation - check for unmatched quotes + let singleQuoteCount = 0; + let doubleQuoteCount = 0; + let i = 0; + + while (i < input.length) { + if (input[i] === "'") { + if (i + 1 < input.length && input[i + 1] === "'") { + // Skip escaped quote + i += 2; + } else { + singleQuoteCount++; + i++; + } + } else if (input[i] === '"') { + if (i + 1 < input.length && input[i + 1] === '"') { + // Skip escaped quote + i += 2; + } else { + doubleQuoteCount++; + i++; + } + } else { + i++; + } + } + + if (singleQuoteCount % 2 !== 0) { + return { isValid: false, error: 'Unmatched single quote' }; + } + + if (doubleQuoteCount % 2 !== 0) { + return { isValid: false, error: 'Unmatched double quote' }; + } + + return { isValid: true }; +} \ No newline at end of file diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index ac99700..ef41923 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -143,4 +143,10 @@ export const tools = [ "Convert images to WebP format with batch processing and quality control. Reduce file sizes while maintaining image quality.", link: "/utilities/webp-converter", }, + { + title: "SQL Minifier", + description: + "Minify SQL by removing comments, extra spaces, and formatting for cleaner, optimized queries.", + link: "/utilities/sql-minifier", + }, ]; diff --git a/pages/utilities/sql-minifier.tsx b/pages/utilities/sql-minifier.tsx new file mode 100644 index 0000000..c9cfa13 --- /dev/null +++ b/pages/utilities/sql-minifier.tsx @@ -0,0 +1,103 @@ +import { useCallback, 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 Header from "@/components/Header"; +import { useCopyToClipboard } from "@/components/hooks/useCopyToClipboard"; +import { CMDK } from "@/components/CMDK"; +import CallToActionGrid from "../../components/CallToActionGrid"; +import Meta from "@/components/Meta"; +import SQLMinifierSEO from "@/components/seo/SQLMinifierSEO"; +import { minifySQL, validateSQLInput } from "@/components/utils/sql-minifier.utils"; + +export default function SQLMinifier() { + const [input, setInput] = useState(""); + const [output, setOutput] = useState(""); + const [error, setError] = useState(""); + const { buttonText, handleCopy } = useCopyToClipboard(); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const { value } = event.currentTarget; + setInput(value); + setError(""); + + if (value.trim() === "") { + setOutput(""); + return; + } + + const validation = validateSQLInput(value); + if (!validation.isValid) { + setError(validation.error || "Invalid input"); + setOutput(""); + return; + } + + try { + const minified = minifySQL(value); + setOutput(minified); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to minify SQL"; + setError(errorMessage); + setOutput(""); + } + }, + [] + ); + + return ( +
+ +
+ + +
+ +
+ +
+ +
+ +