diff --git a/app/visualizer/layout.tsx b/app/visualizer/layout.tsx new file mode 100644 index 0000000..b382b78 --- /dev/null +++ b/app/visualizer/layout.tsx @@ -0,0 +1,17 @@ +import { Metadata } from "next"; +import { defaultMetadata } from "../sitemapmetadata"; + +export const metadata: Metadata = { + ...defaultMetadata, + title: "SQL Query Visualizer - See Your Queries in Action", + description: + "Visualize how SQL queries interact with databases through interactive diagrams and visual explanations", +}; + +export default function VisualizerLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/visualizer/page.tsx b/app/visualizer/page.tsx new file mode 100644 index 0000000..45fe377 --- /dev/null +++ b/app/visualizer/page.tsx @@ -0,0 +1,218 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { initializeDatabase } from "@/lib/lessons"; +import { useSqlJs } from "@/hooks/useSqlJs"; +import Loading from "@/components/ui/loading"; +import { SqlResult } from "@/types/database"; + +// SQL operation visualizations +import SelectOperation from "@/components/visualizer/SelectOperation"; +import WhereOperation from "@/components/visualizer/WhereOperation"; +import JoinOperation from "@/components/visualizer/JoinOperation"; +import GroupByOperation from "@/components/visualizer/GroupByOperation"; +import Image from "next/image"; + +export default function VisualizerPage() { + const [dbInitialized, setDbInitialized] = useState(false); + const [activeOperation, setActiveOperation] = useState("select"); + const [queryResults, setQueryResults] = useState(null); + + const { + isLoading, + error, + executeQuery, + initializeDatabase: initDb, + db, + } = useSqlJs(); + + // Initialize the database when component loads + useEffect(() => { + if (db && !dbInitialized) { + const sql = initializeDatabase(); + const success = initDb(sql); + if (success) { + setDbInitialized(true); + + // Run the initial SELECT query to show data on load + handleOperationChange("select"); + } + } + }, [db, dbInitialized, initDb]); + + // Function to run a preset query for the selected operation + const handleOperationChange = (operation: string) => { + setActiveOperation(operation); + + let query = ""; + + // Define preset queries for each operation + switch (operation) { + case "select": + query = + "SELECT customer_id, first_name, last_name, email FROM Customers LIMIT 5;"; + break; + case "where": + query = + "SELECT name, price, category FROM Products WHERE price > 70 ORDER BY price DESC;"; + break; + case "join": + query = + "SELECT c.first_name, c.last_name, o.order_date, o.total_amount FROM Customers c JOIN Orders o ON c.customer_id = o.customer_id LIMIT 6;"; + break; + case "groupby": + query = + "SELECT category, COUNT(*) as product_count, AVG(price) as avg_price FROM Products GROUP BY category;"; + break; + default: + query = "SELECT * FROM Customers LIMIT 5;"; + } + + // Execute the query + const result = executeQuery(query); + + if (result.results && result.results.length > 0) { + setQueryResults(result.results[0]); + } else { + setQueryResults(null); + } + }; + + if (isLoading) { + return ( + + ); + } + + if (error) { + return ( +
+
+

+ Error Loading SQL Engine +

+

{error}

+

+ Try refreshing the page or check your console for more details. +

+
+
+ ); + } + + if (!dbInitialized) { + return ; + } + + return ( +
+
+
+

+ SQL Query Visualizer +

+

+ Explore how different SQL operations transform data through visual + explanations +

+
+ + {/* Operation Selector */} +
+

+ Choose an SQL Operation +

+
+ handleOperationChange("select")} + icon="/images/select-icon.svg" + color="bg-blue-100 hover:bg-blue-200 text-blue-800" + /> + handleOperationChange("where")} + icon="/images/where-icon.svg" + color="bg-yellow-100 hover:bg-yellow-200 text-yellow-800" + /> + handleOperationChange("join")} + icon="/images/join-icon.svg" + color="bg-purple-100 hover:bg-purple-200 text-purple-800" + /> + handleOperationChange("groupby")} + icon="/images/groupby-icon.svg" + color="bg-green-100 hover:bg-green-200 text-green-800" + /> +
+
+ + {/* Operation Visualization */} +
+ {activeOperation === "select" && ( + + )} + {activeOperation === "where" && ( + + )} + {activeOperation === "join" && ( + + )} + {activeOperation === "groupby" && ( + + )} +
+
+
+ ); +} + +// Helper component for operation selection buttons +interface OperationButtonProps { + name: string; + description: string; + isActive: boolean; + onClick: () => void; + icon: string; + color: string; +} + +function OperationButton({ + name, + description, + isActive, + onClick, + icon, + color, +}: OperationButtonProps) { + return ( + + ); +} diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 673588f..600e178 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -47,6 +47,16 @@ export default function Header() { > Sandbox + + Visualizer +
diff --git a/components/visualizer/GroupByOperation.tsx b/components/visualizer/GroupByOperation.tsx new file mode 100644 index 0000000..85fc642 --- /dev/null +++ b/components/visualizer/GroupByOperation.tsx @@ -0,0 +1,360 @@ +import React from "react"; +import { SqlResult } from "@/types/database"; +import { ResultTable } from "@/components/lessons/ResultTable"; + +interface GroupByOperationProps { + results: SqlResult | null; +} + +const GroupByOperation: React.FC = ({ results }) => { + if (!results) { + return
No results available
; + } + + // Example query that's being visualized + const exampleQuery = + "SELECT category, COUNT(*) as product_count, AVG(price) as avg_price FROM Products GROUP BY category;"; + + return ( +
+
+

+ GROUP BY Operation +

+
+ {exampleQuery} +
+
+ +
+ {/* Visual explanation */} +
+
+

+ How GROUP BY Works +

+ +
+ {/* Initial data */} +
+
+ Original Product Data +
+
+
+
+ Name +
+
+ Price +
+
+ Category +
+
+
+
+
Laptop Pro
+
$1299.99
+
Electronics
+
+
+
Smartphone X
+
$799.99
+
Electronics
+
+
+
Headphones
+
$159.99
+
Electronics
+
+
+
Coffee Maker
+
$89.99
+
+ Home Appliances +
+
+
+
Blender
+
$69.99
+
+ Home Appliances +
+
+
+
Running Shoes
+
$79.99
+
+ Sportswear +
+
+
+
+
+ + {/* Grouping process arrows */} +
+
+
+ + + +
+
+ GROUP BY category +
+
+ + + +
+
+
+ + {/* Grouped data */} +
+
+ Grouped Results with Aggregation +
+
+
+
+ Category +
+
+ COUNT(*) +
+
+ AVG(price) +
+
+
+
+
Electronics
+
3 products
+
$753.32
+
+
+
Home Appliances
+
2 products
+
$79.99
+
+
+
Sportswear
+
1 product
+
$79.99
+
+
+
+
+
+ +
+

+ GROUP BY acts like a sorting hat for your data +

+

+ The GROUP BY clause organizes rows into groups based on matching + values in specified columns. Think of it as sorting items into + different buckets, then calculating summaries for each bucket. +

+

In our example:

+
    +
  • + + GROUP BY category + {" "} + - Organizes products into categories +
  • +
  • + + COUNT(*) as product_count + {" "} + - Counts the number of products in each category +
  • +
  • + + AVG(price) as avg_price + {" "} + - Calculates the average price for each category +
  • +
+

+ Without GROUP BY, aggregate functions like COUNT() would reduce + all rows to a single summary value. GROUP BY lets you create + summaries for specific segments of your data. +

+
+
+ +
+

+ Common Aggregate Functions +

+
+
+
COUNT()
+

+ Counts the number of rows or non-NULL values +

+
+ COUNT(*) - Counts all rows +
+ COUNT(column) - Counts non-NULL values +
+
+
+
SUM()
+

+ Calculates the total of values in a column +

+
+ SUM(price) - Total price of all items +
+
+
+
AVG()
+

+ Calculates the average of values +

+
+ AVG(rating) - Average rating +
+
+
+
MIN() / MAX()
+

+ Finds the smallest/largest values +

+
+ MIN(price) - Cheapest product +
+ MAX(price) - Most expensive product +
+
+
+
+
+ + {/* Query Results */} +
+
+
+

Query Results

+
+
+ +
+
+ +
+

+ SQL Query Execution Order +

+
+
+ 1 +
+
FROM Products
+
+
+
+ 2 +
+
(WHERE clause if present)
+
+
+
+ 3 +
+
+ GROUP BY category +
+
+
+
+ 4 +
+
(HAVING clause if present)
+
+
+
+ 5 +
+
+ SELECT category, COUNT(*), AVG(price) +
+
+

+ GROUP BY happens before SELECT is applied but after filtering with + WHERE! +

+
+ +
+

+ Important GROUP BY Rules +

+
    +
  • + + + + Every column in your SELECT list must either be in the GROUP BY + clause or inside an aggregate function +
  • +
  • + + + + Use HAVING (not WHERE) to filter groups based on aggregate + values +
  • +
+
+
+
+
+ ); +}; + +export default GroupByOperation; diff --git a/components/visualizer/JoinOperation.tsx b/components/visualizer/JoinOperation.tsx new file mode 100644 index 0000000..375ee4f --- /dev/null +++ b/components/visualizer/JoinOperation.tsx @@ -0,0 +1,387 @@ +import React from "react"; +import { SqlResult } from "@/types/database"; +import { ResultTable } from "@/components/lessons/ResultTable"; + +interface JoinOperationProps { + results: SqlResult | null; +} + +const JoinOperation: React.FC = ({ results }) => { + if (!results) { + return
No results available
; + } + + // Example query that's being visualized + const exampleQuery = + "SELECT c.first_name, c.last_name, o.order_date, o.total_amount FROM Customers c JOIN Orders o ON c.customer_id = o.customer_id LIMIT 6;"; + + return ( +
+
+

+ JOIN Operation +

+
+ {exampleQuery} +
+
+ +
+ {/* Visual explanation */} +
+
+

+ How JOIN Works +

+ +
+ {/* Customers table representation */} +
+
+ Customers +
+
+
+ customer_id +
+
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+
+
+
+ name +
+
+
+ John +
+
+ Jane +
+
+ Mike +
+
+
+
+ + {/* Join lines */} + + + + + + + JOIN ON + + + customer_id = customer_id + + + + {/* Orders table representation */} +
+
+ Orders +
+
+
+ customer_id +
+
+
+ 1 +
+
+ 1 +
+
+ 2 +
+
+
+
+
+ amount +
+
+
+ $50 +
+
+ $25 +
+
+ $75 +
+
+
+
+ + {/* Down arrows */} +
+ + + +
+
+ + + +
+ + {/* Result set */} +
+
+ Combined Result +
+
+
+
+ customer name +
+
+
+ John +
+
+ John +
+
+ Jane +
+
+
+
+
+ order amount +
+
+
+ $50 +
+
+ $25 +
+
+ $75 +
+
+
+
+
+
+ +
+

+ JOIN acts like a matchmaker for your tables +

+

+ The JOIN operation combines rows from two or more tables based + on a related column. Think of it as a marriage ceremony where + rows are paired based on matching values in the specified + columns. +

+

In our example:

+
    +
  • + + Customers c JOIN Orders o + {" "} + - Combines these two tables +
  • +
  • + + ON c.customer_id = o.customer_id + {" "} + - The matching condition +
  • +
  • + Each customer might have multiple orders, so one customer can + appear multiple times in the results +
  • +
  • + If a customer has no orders (or vice versa with RIGHT JOIN), + they won't appear in an INNER JOIN result +
  • +
+
+
+ +
+

Types of JOINs

+
+
+
+ INNER JOIN +
+
+
+
+
+
+
+
+

+ Returns rows when there is a match in both tables +

+
+
+
+ LEFT JOIN +
+
+
+
+
+
+
+
+

+ Returns all rows from the left table, plus matched rows from + the right table +

+
+
+
+ RIGHT JOIN +
+
+
+
+
+
+
+
+

+ Returns all rows from the right table, plus matched rows from + the left table +

+
+
+
+ FULL JOIN +
+
+
+
+
+
+
+
+

+ Returns rows when there is a match in either left or right + table +

+
+
+
+
+ + {/* Query Results */} +
+
+
+

Query Results

+
+
+ +
+
+ +
+

+ SQL Query Execution Order +

+
+
+ 1 +
+
FROM Customers
+
+
+
+ 2 +
+
+ JOIN Orders ON customer_id = customer_id +
+
+
+
+ 3 +
+
+ SELECT first_name, last_name, order_date, total_amount +
+
+
+
+ 4 +
+
LIMIT 6
+
+

+ JOINs are processed early in query execution, effectively creating + a temporary combined table before other operations! +

+
+
+
+
+ ); +}; + +export default JoinOperation; diff --git a/components/visualizer/SelectOperation.tsx b/components/visualizer/SelectOperation.tsx new file mode 100644 index 0000000..038bc49 --- /dev/null +++ b/components/visualizer/SelectOperation.tsx @@ -0,0 +1,236 @@ +import React from "react"; +import { SqlResult } from "@/types/database"; +import { ResultTable } from "@/components/lessons/ResultTable"; + +interface SelectOperationProps { + results: SqlResult | null; +} + +const SelectOperation: React.FC = ({ results }) => { + if (!results) { + return
No results available
; + } + + // Example query that's being visualized + const exampleQuery = + "SELECT customer_id, first_name, last_name, email FROM Customers LIMIT 5;"; + + return ( +
+
+

+ SELECT Operation +

+
+ {exampleQuery} +
+
+ +
+ {/* Visual explanation */} +
+
+

+ How SELECT Works +

+
+
+ {/* Visual database table representation */} +
+
+
+
+
+ Customers Table +
+
+
+ customer_id +
+
+ first_name +
+
+ last_name +
+
+ email +
+
+ join_date +
+
+ phone +
+
+
+ Selects specific columns... +
+
+
+ + {/* Arrow pointing down */} +
+ + + +
+ + {/* Result set */} +
+
+ Result Set +
+
+
+ customer_id +
+
+ first_name +
+
+ last_name +
+
+ email +
+
+
+
+
+ +
+

+ SELECT acts like a projector of data +

+

+ The SELECT statement determines which{" "} + columns will appear in + your query results. Think of it as a spotlight that illuminates + only the specific data attributes you want to see. +

+

In our example:

+
    +
  • + + customer_id, first_name, last_name, email + {" "} + - These are the columns we're selecting +
  • +
  • + + FROM Customers + {" "} + - The table we're selecting from +
  • +
  • + LIMIT 5 - + Restricts the output to only 5 rows +
  • +
+
+
+ +
+

Key Concepts

+
+
+
+ 1 +
+
+

Column Selection

+

+ Choose specific columns instead of using{" "} + SELECT * to improve query performance and + readability +

+
+
+
+
+ 2 +
+
+

Column Order

+

+ Columns appear in results in the same order specified in the + SELECT statement +

+
+
+
+
+ 3 +
+
+

Column Aliases

+

+ You can rename columns using AS for more + meaningful output (e.g.,{" "} + first_name AS "First Name") +

+
+
+
+
+
+ + {/* Query Results */} +
+
+
+

Query Results

+
+
+ +
+
+ +
+

+ SQL Query Execution Order +

+
+
+ 1 +
+
FROM Customers
+
+
+
+ 2 +
+
+ SELECT customer_id, first_name, last_name, email +
+
+
+
+ 3 +
+
LIMIT 5
+
+

+ Note: This shows conceptual execution order, not the order in + which SQL is written! +

+
+
+
+
+ ); +}; + +export default SelectOperation; diff --git a/components/visualizer/WhereOperation.tsx b/components/visualizer/WhereOperation.tsx new file mode 100644 index 0000000..9bcdc92 --- /dev/null +++ b/components/visualizer/WhereOperation.tsx @@ -0,0 +1,361 @@ +import React from "react"; +import { SqlResult } from "@/types/database"; +import { ResultTable } from "@/components/lessons/ResultTable"; + +interface WhereOperationProps { + results: SqlResult | null; +} + +const WhereOperation: React.FC = ({ results }) => { + if (!results) { + return
No results available
; + } + + // Example query that's being visualized + const exampleQuery = + "SELECT name, price, category FROM Products WHERE price > 70 ORDER BY price DESC;"; + + return ( +
+
+

+ WHERE Operation +

+
+ {exampleQuery} +
+
+ +
+ {/* Visual explanation */} +
+
+

+ How WHERE Works +

+
+
+ {/* Full dataset representation */} +
+
+
+ All Products +
+
+
+
Smartphone X
+
+ $799.99 +
+
+ Electronics +
+
+
+
Laptop Pro
+
+ $1299.99 +
+
+ Electronics +
+
+
+
Wireless Headphones
+
+ $159.99 +
+
+ Electronics +
+
+
+
Coffee Maker
+
+ $89.99 +
+
+ Home Appliances +
+
+
+
Running Shoes
+
+ $79.99 +
+
+ Sportswear +
+
+
+
Blender
+
+ $69.99 +
+
+ Home Appliances +
+
+
+
+
+ + {/* Filter representation */} +
+
+
+ price > 70 +
+
+
+ + + +
+
+ + + +
+
+ + {/* Filtered result set */} +
+
+ Filtered Products +
+
+
+
Smartphone X
+
$799.99
+
+ Electronics +
+
+
+
Laptop Pro
+
+ $1299.99 +
+
+ Electronics +
+
+
+
Wireless Headphones
+
$159.99
+
+ Electronics +
+
+
+
Coffee Maker
+
$89.99
+
+ Home Appliances +
+
+
+
Running Shoes
+
$79.99
+
+ Sportswear +
+
+ {/* This one is filtered out */} +
+
Blender
+
$69.99
+
+ Home Appliances +
+
+
+
+
+
+ +
+

+ WHERE acts like a sieve for your data +

+

+ The WHERE clause filters rows, allowing only those that match + specific conditions to pass through to your results. Think of it + as a bouncer that checks each row's credentials before + allowing it into your result set. +

+

In our example:

+
    +
  • + + price > 70 + {" "} + - Only includes products that cost more than $70 +
  • +
  • + The Blender at $69.99 is excluded because it doesn't meet + the condition +
  • +
  • + All other products pass through the filter because their + prices exceed $70 +
  • +
+
+
+ +
+

+ Common WHERE Operators +

+
+
+
Comparison
+
    +
  • + = Equal to +
  • +
  • + > Greater than +
  • +
  • + < Less than +
  • +
  • + >= Greater than or equal +
  • +
  • + <= Less than or equal +
  • +
  • + <> or != Not equal +
  • +
+
+
+
Logical
+
    +
  • + AND Both conditions must be true +
  • +
  • + OR Either condition can be true +
  • +
  • + NOT Negates a condition +
  • +
+
+
+
+ Pattern Matching +
+
    +
  • + LIKE Pattern matching with wildcards +
  • +
  • + % Any sequence of characters +
  • +
  • + _ Any single character +
  • +
+
+
+
Other
+
    +
  • + IN Matches any in a list +
  • +
  • + BETWEEN Within a range +
  • +
  • + IS NULL Is a null value +
  • +
  • + IS NOT NULL Is not a null value +
  • +
+
+
+
+
+ + {/* Query Results */} +
+
+
+

Query Results

+
+
+ +
+
+ +
+

+ SQL Query Execution Order +

+
+
+ 1 +
+
FROM Products
+
+
+
+ 2 +
+
+ WHERE price > 70 +
+
+
+
+ 3 +
+
SELECT name, price, category
+
+
+
+ 4 +
+
ORDER BY price DESC
+
+

+ The WHERE clause is applied early in query execution, before + selecting columns! +

+
+
+
+
+ ); +}; + +export default WhereOperation; diff --git a/lib/queryParser.ts b/lib/queryParser.ts new file mode 100644 index 0000000..acd83b1 --- /dev/null +++ b/lib/queryParser.ts @@ -0,0 +1,245 @@ +/** + * A simple SQL query parser to extract key components for visualization + * + * Note: This is a simplified parser and doesn't handle all SQL syntax variations + * It's meant for educational purposes to identify basic query structure for visualization + */ + +export interface ParsedQuery { + type: string; + select: { + columns: string[]; + allColumns: boolean; + }; + from: { + tables: string[]; + alias?: Record; + }; + where?: { + conditions: string[]; + }; + join?: { + type: string; + table: string; + on: string; + }[]; + groupBy?: { + columns: string[]; + }; + orderBy?: { + columns: string[]; + direction: string; + }; + limit?: number; + offset?: number; +} + +export function parseQuery(query: string): ParsedQuery { + // Normalize the query for easier parsing + const normalizedQuery = query.replace(/\s+/g, " ").trim(); + + // Basic structure + const result: ParsedQuery = { + type: "UNKNOWN", + select: { + columns: [], + allColumns: false, + }, + from: { + tables: [], + alias: {}, + }, + }; + + // Determine query type + if (normalizedQuery.toUpperCase().startsWith("SELECT")) { + result.type = "SELECT"; + } else if (normalizedQuery.toUpperCase().startsWith("INSERT")) { + result.type = "INSERT"; + } else if (normalizedQuery.toUpperCase().startsWith("UPDATE")) { + result.type = "UPDATE"; + } else if (normalizedQuery.toUpperCase().startsWith("DELETE")) { + result.type = "DELETE"; + } + + // Only process SELECT queries for now + if (result.type === "SELECT") { + // Extract SELECT columns + const selectMatch = normalizedQuery.match(/SELECT\s+(.*?)\s+FROM/i); + if (selectMatch && selectMatch[1]) { + const columnsStr = selectMatch[1]; + if (columnsStr.trim() === "*") { + result.select.allColumns = true; + } else { + // Split columns handling potential functions like COUNT(*) + const columns = splitWithAwareness(columnsStr); + result.select.columns = columns.map((col) => col.trim()); + } + } + + // Extract FROM tables + const fromMatch = normalizedQuery.match( + /FROM\s+(.*?)(?:\s+WHERE|\s+GROUP BY|\s+ORDER BY|\s+LIMIT|\s*;|\s*$)/i + ); + if (fromMatch && fromMatch[1]) { + const fromClause = fromMatch[1].trim(); + + // Check for JOINs + if (fromClause.toUpperCase().includes("JOIN")) { + result.join = []; + + // Split out the first table + const firstTableMatch = fromClause.match( + /(.*?)(?:\s+(?:LEFT|RIGHT|INNER|OUTER|CROSS|FULL)?\s*JOIN)/i + ); + if (firstTableMatch && firstTableMatch[1]) { + const mainTableParts = firstTableMatch[1].trim().split(/\s+/); + result.from.tables.push(mainTableParts[0]); + + // Check for alias + if (mainTableParts.length > 1) { + result.from.alias = result.from.alias || {}; + result.from.alias[mainTableParts[0]] = + mainTableParts[mainTableParts.length - 1]; + } + } + + // Extract JOIN clauses + const joinPattern = + /(?:(LEFT|RIGHT|INNER|OUTER|CROSS|FULL)?\s*JOIN)\s+(.*?)\s+ON\s+(.*?)(?:\s+(?:LEFT|RIGHT|INNER|OUTER|CROSS|FULL)?\s*JOIN|\s+WHERE|\s+GROUP BY|\s+ORDER BY|\s+LIMIT|\s*;|\s*$)/gi; + let joinMatch; + + while ((joinMatch = joinPattern.exec(fromClause)) !== null) { + const joinType = joinMatch[1] ? joinMatch[1].toUpperCase() : "INNER"; + const joinTable = joinMatch[2].trim(); + const joinCondition = joinMatch[3].trim(); + + // Extract alias if any + const tableParts = joinTable.split(/\s+/); + const actualTable = tableParts[0]; + const tableAlias = + tableParts.length > 1 + ? tableParts[tableParts.length - 1] + : actualTable; + + result.from.tables.push(actualTable); + result.from.alias = result.from.alias || {}; + result.from.alias[actualTable] = tableAlias; + + result.join.push({ + type: joinType, + table: actualTable, + on: joinCondition, + }); + } + } else { + // Simple FROM clause + const tables = fromClause.split(","); + tables.forEach((table) => { + const tableParts = table.trim().split(/\s+/); + const actualTable = tableParts[0]; + result.from.tables.push(actualTable); + + // Check for alias + if (tableParts.length > 1) { + result.from.alias = result.from.alias || {}; + result.from.alias[actualTable] = tableParts[tableParts.length - 1]; + } + }); + } + } + + // Extract WHERE conditions + const whereMatch = normalizedQuery.match( + /WHERE\s+(.*?)(?:\s+GROUP BY|\s+ORDER BY|\s+LIMIT|\s*;|\s*$)/i + ); + if (whereMatch && whereMatch[1]) { + result.where = { + conditions: [whereMatch[1].trim()], + }; + } + + // Extract GROUP BY + const groupByMatch = normalizedQuery.match( + /GROUP BY\s+(.*?)(?:\s+HAVING|\s+ORDER BY|\s+LIMIT|\s*;|\s*$)/i + ); + if (groupByMatch && groupByMatch[1]) { + result.groupBy = { + columns: groupByMatch[1].split(",").map((col) => col.trim()), + }; + } + + // Extract ORDER BY + const orderByMatch = normalizedQuery.match( + /ORDER BY\s+(.*?)(?:\s+LIMIT|\s*;|\s*$)/i + ); + if (orderByMatch && orderByMatch[1]) { + const orderByClause = orderByMatch[1].trim(); + const orderDirectionMatch = orderByClause.match(/(.*?)\s+(ASC|DESC)$/i); + + if (orderDirectionMatch) { + result.orderBy = { + columns: orderDirectionMatch[1].split(",").map((col) => col.trim()), + direction: orderDirectionMatch[2].toUpperCase(), + }; + } else { + result.orderBy = { + columns: orderByClause.split(",").map((col) => col.trim()), + direction: "ASC", // Default + }; + } + } + + // Extract LIMIT + const limitMatch = normalizedQuery.match( + /LIMIT\s+(\d+)(?:\s+OFFSET\s+(\d+))?/i + ); + if (limitMatch) { + result.limit = parseInt(limitMatch[1], 10); + + if (limitMatch[2]) { + result.offset = parseInt(limitMatch[2], 10); + } + } + + // Check for OFFSET without LIMIT + if (!result.offset) { + const offsetMatch = normalizedQuery.match(/OFFSET\s+(\d+)/i); + if (offsetMatch) { + result.offset = parseInt(offsetMatch[1], 10); + } + } + } + + return result; +} + +// Helper function to split by commas but be aware of function calls like COUNT(*) +function splitWithAwareness(str: string): string[] { + const result: string[] = []; + let current = ""; + let depth = 0; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + if (char === "(") { + depth++; + current += char; + } else if (char === ")") { + depth--; + current += char; + } else if (char === "," && depth === 0) { + result.push(current.trim()); + current = ""; + } else { + current += char; + } + } + + if (current.trim()) { + result.push(current.trim()); + } + + return result; +} diff --git a/public/images/groupby-icon.svg b/public/images/groupby-icon.svg new file mode 100644 index 0000000..92c5faf --- /dev/null +++ b/public/images/groupby-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/public/images/join-icon.svg b/public/images/join-icon.svg new file mode 100644 index 0000000..13101af --- /dev/null +++ b/public/images/join-icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/images/select-icon.svg b/public/images/select-icon.svg new file mode 100644 index 0000000..868c482 --- /dev/null +++ b/public/images/select-icon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/images/where-icon.svg b/public/images/where-icon.svg new file mode 100644 index 0000000..d3128c3 --- /dev/null +++ b/public/images/where-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file