+ 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.
+
+
+
+
+
Benefits of SQL Minification
+
+
+ Improved Query Performance:
+ Minified SQL queries parse up to 20% faster, reducing database load
+ and improving application response times, especially for complex
+ queries with multiple joins.
+
+
+ Reduced Network Bandwidth:
+ Compress SQL queries by 30-50%, saving bandwidth costs and speeding
+ up data transfer between applications and databases, crucial for
+ cloud-based systems.
+
+
+
+
+
+
SQL Minifier FAQs
+
+
+ What does SQL minification do exactly?
+
+ SQL minification removes all unnecessary characters including
+ comments (-- and /* */), extra spaces, tabs, and line breaks while
+ preserving the exact query logic and results.
+
+
+ How much can SQL minification improve performance?
+
+ Performance gains vary: 5-20% faster parsing for complex queries,
+ 30-50% bandwidth reduction, and noticeable improvements in
+ high-traffic applications processing thousands of queries.
+
+
+ Which SQL databases are supported?
+
+ Our minifier supports all ANSI SQL standards and major databases
+ including MySQL, PostgreSQL, Microsoft SQL Server, Oracle, SQLite,
+ MariaDB, and Amazon Redshift.
+
+
+
+
+ );
+}
diff --git a/components/utils/sql-minifier.utils.test.ts b/components/utils/sql-minifier.utils.test.ts
new file mode 100644
index 0000000..a0458d8
--- /dev/null
+++ b/components/utils/sql-minifier.utils.test.ts
@@ -0,0 +1,567 @@
+import { minifySQL, validateSQLInput } from "./sql-minifier.utils";
+
+const COMPLEX_SQL_QUERY = `
+WITH RECURSIVE employee_hierarchy AS (
+ -- Anchor member: top-level employees
+ SELECT
+ e.employee_id,
+ e.first_name || ' ' || e.last_name AS full_name, -- Concatenate names
+ e.manager_id,
+ e.department_id,
+ 0 AS level,
+ CAST(e.employee_id AS VARCHAR(1000)) AS path
+ FROM
+ employees e
+ WHERE
+ e.manager_id IS NULL -- Top level employees have no manager
+
+ UNION ALL
+
+ -- Recursive member: employees with managers
+ SELECT
+ e.employee_id,
+ e.first_name || ' ' || e.last_name,
+ e.manager_id,
+ e.department_id,
+ eh.level + 1,
+ eh.path || ' -> ' || CAST(e.employee_id AS VARCHAR(1000))
+ FROM
+ employees e
+ INNER JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
+ WHERE
+ eh.level < 10 -- Prevent infinite recursion
+),
+
+-- CTE for department statistics
+department_stats AS (
+ SELECT
+ d.department_id,
+ d.department_name,
+ COUNT(DISTINCT e.employee_id) AS employee_count,
+ AVG(e.salary) AS avg_salary,
+ MIN(e.hire_date) AS earliest_hire,
+ MAX(e.hire_date) AS latest_hire,
+ /* Calculate salary ranges */
+ PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY e.salary) AS salary_q1,
+ PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY e.salary) AS salary_median,
+ PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY e.salary) AS salary_q3
+ FROM
+ departments d
+ LEFT JOIN employees e ON d.department_id = e.department_id
+ WHERE
+ e.is_active = TRUE -- Only active employees
+ AND e.hire_date >= '2020-01-01'::DATE
+ GROUP BY
+ d.department_id,
+ d.department_name
+),
+
+-- CTE for project allocations
+project_allocations AS (
+ SELECT
+ p.project_id,
+ p.project_name,
+ pa.employee_id,
+ pa.allocation_percentage,
+ pa.start_date,
+ pa.end_date,
+ -- Complex date calculations
+ CASE
+ WHEN pa.end_date IS NULL THEN CURRENT_DATE - pa.start_date
+ ELSE pa.end_date - pa.start_date
+ END AS days_on_project,
+ -- Nested CASE for status
+ CASE
+ WHEN pa.end_date IS NULL THEN
+ CASE
+ WHEN p.status = 'ACTIVE' THEN 'ONGOING'
+ WHEN p.status = 'PAUSED' THEN 'PAUSED'
+ ELSE 'UNKNOWN'
+ END
+ WHEN pa.end_date < CURRENT_DATE THEN 'COMPLETED'
+ ELSE 'SCHEDULED'
+ END AS allocation_status
+ FROM
+ projects p
+ INNER JOIN project_assignments pa ON p.project_id = pa.project_id
+ WHERE
+ p.budget > 100000 -- High-value projects only
+ AND (
+ pa.end_date IS NULL
+ OR pa.end_date >= DATEADD(MONTH, -6, CURRENT_DATE)
+ )
+)
+
+-- Main query combining all CTEs
+SELECT
+ eh.employee_id,
+ eh.full_name,
+ eh.level AS hierarchy_level,
+ eh.path AS reporting_path,
+ ds.department_name,
+ ds.employee_count AS dept_employee_count,
+ ROUND(ds.avg_salary, 2) AS dept_avg_salary,
+
+ -- Subquery for individual employee salary comparison
+ (
+ SELECT
+ ROUND(
+ (e.salary - ds.avg_salary) / ds.avg_salary * 100,
+ 2
+ )
+ FROM
+ employees e
+ WHERE
+ e.employee_id = eh.employee_id
+ ) AS salary_variance_percentage,
+
+ -- Correlated subquery for direct reports count
+ (
+ SELECT
+ COUNT(*)
+ FROM
+ employees sub_e
+ WHERE
+ sub_e.manager_id = eh.employee_id
+ AND sub_e.is_active = TRUE
+ ) AS direct_reports_count,
+
+ -- JSON aggregation of projects
+ JSON_AGG(
+ JSON_BUILD_OBJECT(
+ 'project_id', pa.project_id,
+ 'project_name', pa.project_name,
+ 'allocation', pa.allocation_percentage,
+ 'days', pa.days_on_project,
+ 'status', pa.allocation_status
+ ) ORDER BY pa.allocation_percentage DESC
+ ) FILTER (WHERE pa.project_id IS NOT NULL) AS projects,
+
+ -- Window functions for ranking
+ RANK() OVER (
+ PARTITION BY eh.department_id
+ ORDER BY eh.level DESC, eh.employee_id
+ ) AS dept_seniority_rank,
+
+ DENSE_RANK() OVER (
+ ORDER BY ds.avg_salary DESC
+ ) AS dept_salary_rank,
+
+ -- Running totals
+ SUM(COALESCE(pa.allocation_percentage, 0)) OVER (
+ PARTITION BY eh.employee_id
+ ORDER BY pa.start_date
+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+ ) AS cumulative_allocation
+
+FROM
+ employee_hierarchy eh
+ INNER JOIN department_stats ds ON eh.department_id = ds.department_id
+ LEFT JOIN project_allocations pa ON eh.employee_id = pa.employee_id
+
+ -- Additional joins with complex conditions
+ LEFT JOIN (
+ SELECT
+ employee_id,
+ STRING_AGG(skill_name, ', ' ORDER BY proficiency_level DESC) AS skills
+ FROM
+ employee_skills es
+ INNER JOIN skills s ON es.skill_id = s.skill_id
+ WHERE
+ proficiency_level >= 3
+ GROUP BY
+ employee_id
+ ) AS emp_skills ON eh.employee_id = emp_skills.employee_id
+
+WHERE
+ -- Complex WHERE conditions
+ eh.level <= 5
+ AND ds.employee_count > 10
+ AND (
+ ds.avg_salary BETWEEN 50000 AND 200000
+ OR eh.employee_id IN (
+ SELECT
+ employee_id
+ FROM
+ employee_awards
+ WHERE
+ award_date >= '2023-01-01'
+ )
+ )
+ AND NOT EXISTS (
+ SELECT
+ 1
+ FROM
+ employee_violations ev
+ WHERE
+ ev.employee_id = eh.employee_id
+ AND ev.severity = 'HIGH'
+ )
+
+GROUP BY
+ eh.employee_id,
+ eh.full_name,
+ eh.level,
+ eh.path,
+ eh.department_id,
+ ds.department_name,
+ ds.employee_count,
+ ds.avg_salary,
+ pa.allocation_percentage,
+ pa.start_date
+
+HAVING
+ COUNT(DISTINCT pa.project_id) <= 5 -- Not overallocated
+ OR SUM(pa.allocation_percentage) <= 120
+
+ORDER BY
+ eh.level ASC,
+ ds.avg_salary DESC,
+ eh.full_name ASC
+
+LIMIT 100 OFFSET 0;`;
+
+const MINIFIED_COMPLEX_SQL_QUERY = `WITH RECURSIVE employee_hierarchy AS ( SELECT e.employee_id, e.first_name || ' ' || e.last_name AS full_name, e.manager_id, e.department_id, 0 AS level, CAST(e.employee_id AS VARCHAR(1000)) AS path FROM employees e WHERE e.manager_id IS NULL UNION ALL SELECT e.employee_id, e.first_name || ' ' || e.last_name, e.manager_id, e.department_id, eh.level + 1, eh.path || ' -> ' || CAST(e.employee_id AS VARCHAR(1000)) FROM employees e INNER JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id WHERE eh.level < 10 ), department_stats AS ( SELECT d.department_id, d.department_name, COUNT(DISTINCT e.employee_id) AS employee_count, AVG(e.salary) AS avg_salary, MIN(e.hire_date) AS earliest_hire, MAX(e.hire_date) AS latest_hire, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY e.salary) AS salary_q1, PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY e.salary) AS salary_median, PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY e.salary) AS salary_q3 FROM departments d LEFT JOIN employees e ON d.department_id = e.department_id WHERE e.is_active = TRUE AND e.hire_date >= '2020-01-01'::DATE GROUP BY d.department_id, d.department_name ), project_allocations AS ( SELECT p.project_id, p.project_name, pa.employee_id, pa.allocation_percentage, pa.start_date, pa.end_date, CASE WHEN pa.end_date IS NULL THEN CURRENT_DATE - pa.start_date ELSE pa.end_date - pa.start_date END AS days_on_project, CASE WHEN pa.end_date IS NULL THEN CASE WHEN p.status = 'ACTIVE' THEN 'ONGOING' WHEN p.status = 'PAUSED' THEN 'PAUSED' ELSE 'UNKNOWN' END WHEN pa.end_date < CURRENT_DATE THEN 'COMPLETED' ELSE 'SCHEDULED' END AS allocation_status FROM projects p INNER JOIN project_assignments pa ON p.project_id = pa.project_id WHERE p.budget > 100000 AND ( pa.end_date IS NULL OR pa.end_date >= DATEADD(MONTH, -6, CURRENT_DATE) ) ) SELECT eh.employee_id, eh.full_name, eh.level AS hierarchy_level, eh.path AS reporting_path, ds.department_name, ds.employee_count AS dept_employee_count, ROUND(ds.avg_salary, 2) AS dept_avg_salary, ( SELECT ROUND( (e.salary - ds.avg_salary) / ds.avg_salary * 100, 2 ) FROM employees e WHERE e.employee_id = eh.employee_id ) AS salary_variance_percentage, ( SELECT COUNT(*) FROM employees sub_e WHERE sub_e.manager_id = eh.employee_id AND sub_e.is_active = TRUE ) AS direct_reports_count, JSON_AGG( JSON_BUILD_OBJECT( 'project_id', pa.project_id, 'project_name', pa.project_name, 'allocation', pa.allocation_percentage, 'days', pa.days_on_project, 'status', pa.allocation_status ) ORDER BY pa.allocation_percentage DESC ) FILTER (WHERE pa.project_id IS NOT NULL) AS projects, RANK() OVER ( PARTITION BY eh.department_id ORDER BY eh.level DESC, eh.employee_id ) AS dept_seniority_rank, DENSE_RANK() OVER ( ORDER BY ds.avg_salary DESC ) AS dept_salary_rank, SUM(COALESCE(pa.allocation_percentage, 0)) OVER ( PARTITION BY eh.employee_id ORDER BY pa.start_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_allocation FROM employee_hierarchy eh INNER JOIN department_stats ds ON eh.department_id = ds.department_id LEFT JOIN project_allocations pa ON eh.employee_id = pa.employee_id LEFT JOIN ( SELECT employee_id, STRING_AGG(skill_name, ', ' ORDER BY proficiency_level DESC) AS skills FROM employee_skills es INNER JOIN skills s ON es.skill_id = s.skill_id WHERE proficiency_level >= 3 GROUP BY employee_id ) AS emp_skills ON eh.employee_id = emp_skills.employee_id WHERE eh.level <= 5 AND ds.employee_count > 10 AND ( ds.avg_salary BETWEEN 50000 AND 200000 OR eh.employee_id IN ( SELECT employee_id FROM employee_awards WHERE award_date >= '2023-01-01' ) ) AND NOT EXISTS ( SELECT 1 FROM employee_violations ev WHERE ev.employee_id = eh.employee_id AND ev.severity = 'HIGH' ) GROUP BY eh.employee_id, eh.full_name, eh.level, eh.path, eh.department_id, ds.department_name, ds.employee_count, ds.avg_salary, pa.allocation_percentage, pa.start_date HAVING COUNT(DISTINCT pa.project_id) <= 5 OR SUM(pa.allocation_percentage) <= 120 ORDER BY eh.level ASC, ds.avg_salary DESC, eh.full_name ASC LIMIT 100 OFFSET 0;`;
+
+const COMPLEX_SQL_QUERY_2 = `
+-- ====================================================================
+-- SQL Minifier Torture Test
+-- Purpose: To test the robustness of a SQL minifier with edge cases.
+-- Features: Nested CTEs, Window Functions, Lateral Joins, JSONB,
+-- XML, Arrays, Regex, Grouping Sets, and tricky comments.
+-- ====================================================================
+
+WITH
+ -- CTE 1: User behavior analysis with window functions
+ user_sessions AS (
+ SELECT
+ user_id,
+ session_id,
+ created_at,
+ -- Calculate time difference between consecutive sessions for a user
+ EXTRACT(EPOCH FROM (created_at - LAG(created_at, 1, created_at) OVER (PARTITION BY user_id ORDER BY created_at))) AS time_since_last_session,
+ -- Rank sessions from newest to oldest
+ ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as session_rank
+ FROM sessions
+ WHERE is_active = 'true' -- boolean as string
+ ),
+
+ -- CTE 2: Product inventory with complex CASE logic
+ product_inventory AS (
+ SELECT
+ p.product_id,
+ p.product_name,
+ p.category_id,
+ -- Multi-level nested CASE statement
+ CASE
+ WHEN w.region = 'EU' THEN
+ CASE
+ WHEN i.quantity < 10 THEN 'VERY LOW'
+ WHEN i.quantity BETWEEN 10 AND 50 THEN 'LOW'
+ ELSE 'STABLE'
+ END
+ WHEN w.region IN ('NA', 'APAC') THEN
+ CASE
+ WHEN i.quantity = 0 THEN 'OUT OF STOCK'
+ ELSE 'IN STOCK'
+ END
+ ELSE 'UNSPECIFIED' /* Default catch-all */
+ END AS inventory_status,
+ i.quantity
+ FROM products p
+ JOIN inventory i ON p.product_id = i.product_id
+ JOIN warehouses w ON i.warehouse_id = w.id
+ ),
+
+ -- CTE 3: Financial calculations with GROUPING SETS
+ financial_summary AS (
+ SELECT
+ c.category_id,
+ EXTRACT(YEAR FROM o.order_date) as order_year,
+ EXTRACT(QUARTER FROM o.order_date) as order_quarter,
+ -- Use GROUPING to identify aggregation level
+ GROUPING(c.category_id, EXTRACT(YEAR FROM o.order_date), EXTRACT(QUARTER FROM o.order_date)) as grouping_level,
+ SUM(oi.price * oi.quantity) as total_revenue,
+ COUNT(DISTINCT o.order_id) as total_orders
+ FROM orders o
+ JOIN order_items oi ON o.order_id = oi.order_id
+ JOIN products p ON oi.product_id = p.product_id
+ JOIN categories c ON p.category_id = c.id
+ GROUP BY
+ GROUPING SETS (
+ (c.category_id, EXTRACT(YEAR FROM o.order_date), EXTRACT(QUARTER FROM o.order_date)),
+ (c.category_id, EXTRACT(YEAR FROM o.order_date)),
+ (c.category_id),
+ () -- Grand total
+ )
+ )
+
+-- Main SELECT statement combining everything
+SELECT
+ u.id as user_id,
+ u.email,
+ us.time_since_last_session,
+ pi.product_name,
+ pi.inventory_status,
+
+ -- Correlated subquery to get the date of the first order for the user
+ (SELECT MIN(o.order_date) FROM orders o WHERE o.user_id = u.id) as first_order_date,
+
+ -- Build a JSONB object with user details, including a nested array
+ JSONB_BUILD_OBJECT(
+ 'user_id', u.id,
+ 'email_domain', SUBSTRING(u.email FROM '@(.*)$'),
+ 'tags', (SELECT ARRAY_AGG(t.tag_name) FROM user_tags ut JOIN tags t ON ut.tag_id = t.id WHERE ut.user_id = u.id),
+ 'metadata', u.metadata::jsonb -- Cast text to JSONB
+ ) AS user_profile,
+
+ -- Use a LATERAL join to get the top 2 products for each user
+ lat.top_products,
+
+ -- XML Generation just for fun
+ XMLELEMENT(NAME "UserOrder", XMLATTRIBUTES(u.id as "id"),
+ XMLFOREST(pi.product_name as "product", fs.total_revenue as "revenue")
+ ) AS order_xml
+
+FROM users u
+-- Use the first CTE
+JOIN user_sessions us ON u.id = us.user_id AND us.session_rank = 1 -- Only the latest session
+-- Use the second CTE
+CROSS JOIN product_inventory pi
+-- Use the third CTE
+LEFT JOIN financial_summary fs ON pi.category_id = fs.category_id
+-- Use a LATERAL join
+CROSS JOIN LATERAL (
+ SELECT JSONB_AGG(p.product_name ORDER BY oi.quantity DESC) as top_products
+ FROM orders o
+ JOIN order_items oi ON o.order_id = oi.order_id
+ JOIN products p ON oi.product_id = p.product_id
+ WHERE o.user_id = u.id
+ LIMIT 2
+) lat
+
+WHERE
+ -- A complex WHERE clause
+ u.created_at > '2022-01-01'::timestamp
+ AND u.email ~* '^[A-Za-z0-9._%+-]+@google.com$' -- Regex for a specific domain
+ AND pi.quantity > 0
+ AND EXISTS ( -- Check if user has made at least one order
+ SELECT 1 FROM orders o WHERE o.user_id = u.id
+ )
+
+INTERSECT -- Use a set operator
+
+SELECT * FROM (VALUES
+ (9999, 'test@google.com', 0, 'Test Product', 'STABLE', '2023-01-01'::date, '{}'::jsonb, '[]'::jsonb, ''::xml)
+) AS dummy_row(user_id, email, time_since_last_session, product_name, inventory_status, first_order_date, user_profile, top_products, order_xml)
+
+ORDER BY
+ u.email,
+ pi.product_name DESC
+
+LIMIT 50 OFFSET 10; -- Add final limit and offset
+`;
+
+const MINIFIED_COMPLEX_SQL_QUERY_2 = `WITH user_sessions AS ( SELECT user_id, session_id, created_at, EXTRACT(EPOCH FROM (created_at - LAG(created_at, 1, created_at) OVER (PARTITION BY user_id ORDER BY created_at))) AS time_since_last_session, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as session_rank FROM sessions WHERE is_active = 'true' ), product_inventory AS ( SELECT p.product_id, p.product_name, p.category_id, CASE WHEN w.region = 'EU' THEN CASE WHEN i.quantity < 10 THEN 'VERY LOW' WHEN i.quantity BETWEEN 10 AND 50 THEN 'LOW' ELSE 'STABLE' END WHEN w.region IN ('NA', 'APAC') THEN CASE WHEN i.quantity = 0 THEN 'OUT OF STOCK' ELSE 'IN STOCK' END ELSE 'UNSPECIFIED' END AS inventory_status, i.quantity FROM products p JOIN inventory i ON p.product_id = i.product_id JOIN warehouses w ON i.warehouse_id = w.id ), financial_summary AS ( SELECT c.category_id, EXTRACT(YEAR FROM o.order_date) as order_year, EXTRACT(QUARTER FROM o.order_date) as order_quarter, GROUPING(c.category_id, EXTRACT(YEAR FROM o.order_date), EXTRACT(QUARTER FROM o.order_date)) as grouping_level, SUM(oi.price * oi.quantity) as total_revenue, COUNT(DISTINCT o.order_id) as total_orders FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id JOIN categories c ON p.category_id = c.id GROUP BY GROUPING SETS ( (c.category_id, EXTRACT(YEAR FROM o.order_date), EXTRACT(QUARTER FROM o.order_date)), (c.category_id, EXTRACT(YEAR FROM o.order_date)), (c.category_id), () ) ) SELECT u.id as user_id, u.email, us.time_since_last_session, pi.product_name, pi.inventory_status, (SELECT MIN(o.order_date) FROM orders o WHERE o.user_id = u.id) as first_order_date, JSONB_BUILD_OBJECT( 'user_id', u.id, 'email_domain', SUBSTRING(u.email FROM '@(.*)$'), 'tags', (SELECT ARRAY_AGG(t.tag_name) FROM user_tags ut JOIN tags t ON ut.tag_id = t.id WHERE ut.user_id = u.id), 'metadata', u.metadata::jsonb ) AS user_profile, lat.top_products, XMLELEMENT(NAME "UserOrder", XMLATTRIBUTES(u.id as "id"), XMLFOREST(pi.product_name as "product", fs.total_revenue as "revenue") ) AS order_xml FROM users u JOIN user_sessions us ON u.id = us.user_id AND us.session_rank = 1 CROSS JOIN product_inventory pi LEFT JOIN financial_summary fs ON pi.category_id = fs.category_id CROSS JOIN LATERAL ( SELECT JSONB_AGG(p.product_name ORDER BY oi.quantity DESC) as top_products FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.user_id = u.id LIMIT 2 ) lat WHERE u.created_at > '2022-01-01'::timestamp AND u.email ~* '^[A-Za-z0-9._%+-]+@google.com$' AND pi.quantity > 0 AND EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id ) INTERSECT SELECT * FROM (VALUES (9999, 'test@google.com', 0, 'Test Product', 'STABLE', '2023-01-01'::date, '{}'::jsonb, '[]'::jsonb, ''::xml) ) AS dummy_row(user_id, email, time_since_last_session, product_name, inventory_status, first_order_date, user_profile, top_products, order_xml) ORDER BY u.email, pi.product_name DESC LIMIT 50 OFFSET 10;`;
+
+describe("sql-minifier.utils", () => {
+ describe("minifySQL", () => {
+ test("should minify complex SQL query", () => {
+ const input = COMPLEX_SQL_QUERY;
+ const expected = MINIFIED_COMPLEX_SQL_QUERY;
+ expect(minifySQL(input)).toBe(expected);
+ });
+
+ test("should minify complex SQL query 2", () => {
+ const input = COMPLEX_SQL_QUERY_2;
+ const expected = MINIFIED_COMPLEX_SQL_QUERY_2;
+ expect(minifySQL(input)).toBe(expected);
+ });
+
+ 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();
+ });
+ });
+});
diff --git a/components/utils/sql-minifier.utils.ts b/components/utils/sql-minifier.utils.ts
new file mode 100644
index 0000000..b37d31d
--- /dev/null
+++ b/components/utils/sql-minifier.utils.ts
@@ -0,0 +1,210 @@
+/**
+ * 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 };
+}
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..5009e39
--- /dev/null
+++ b/pages/utilities/sql-minifier.tsx
@@ -0,0 +1,109 @@
+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";
+import GitHubContribution from "@/components/GitHubContribution";
+
+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 (
+
+
+
+
+
+
+
+
+
+
+
+