feat(sql): Advanced SQL — LATERAL, GROUP BY/HAVING, FILTER, aggregates, PIVOT, SQL/PGQ
- LATERAL JOIN: correlated subquery strategy (scan + merge + filter/sort/limit) - GROUP BY: SUM/AVG/MIN/MAX evaluation inside groups, HAVING filter - FILTER (WHERE ...): conditional aggregates for COUNT/SUM/AVG - ARRAY_AGG / STRING_AGG: multi-argument aggregate functions - GROUPING SETS / ROLLUP / CUBE: powerset generation for multi-level aggregation - PIVOT / UNPIVOT: row-to-column and column-to-row transformation - SQL/PGQ Property Graph: GRAPH_TABLE MATCH parser + executor skeleton - 330 tests passing, all 4 modalities (SQL/JSON/Vector/Graph) integrated
This commit is contained in:
@@ -969,8 +969,40 @@ suite "BaraQL Parser — Extended":
|
||||
|
||||
test "Parse GROUP BY with HAVING":
|
||||
let ast = parse("SELECT dept, count(*) FROM employees GROUP BY dept HAVING count(*) > 5")
|
||||
check ast.stmts[0].selGroupBy.len == 1
|
||||
check ast.stmts[0].selHaving != nil
|
||||
|
||||
test "Parse FILTER clause":
|
||||
let ast = parse("SELECT COUNT(*) FILTER (WHERE active = true) FROM users")
|
||||
check ast.stmts[0].selResult.len == 1
|
||||
check ast.stmts[0].selResult[0].funcFilter != nil
|
||||
|
||||
test "Parse ROLLUP":
|
||||
let ast = parse("SELECT dept, SUM(amount) FROM sales GROUP BY ROLLUP (dept)")
|
||||
check ast.stmts[0].selGroupingSetsKind == gskRollup
|
||||
check ast.stmts[0].selGroupBy.len == 1
|
||||
|
||||
test "Parse CUBE":
|
||||
let ast = parse("SELECT dept, job, SUM(amount) FROM sales GROUP BY CUBE (dept, job)")
|
||||
check ast.stmts[0].selGroupingSetsKind == gskCube
|
||||
check ast.stmts[0].selGroupBy.len == 2
|
||||
|
||||
test "Parse GROUPING SETS":
|
||||
let ast = parse("SELECT dept, job, SUM(amount) FROM sales GROUP BY GROUPING SETS ((dept), (job), ())")
|
||||
check ast.stmts[0].selGroupingSetsKind == gskGroupingSets
|
||||
check ast.stmts[0].selGroupingSets.len == 3
|
||||
|
||||
test "Parse PIVOT":
|
||||
let ast = parse("SELECT * FROM (SELECT dept, salary FROM emp) PIVOT (SUM(salary) FOR dept IN ('Eng', 'Sales'))")
|
||||
check ast.stmts[0].selFrom.kind == nkPivot
|
||||
check ast.stmts[0].selFrom.pivotForCol == "dept"
|
||||
check ast.stmts[0].selFrom.pivotInValues.len == 2
|
||||
|
||||
test "Parse GRAPH_TABLE":
|
||||
let ast = parse("SELECT * FROM GRAPH_TABLE(org_chart MATCH (e)-[r]->(d) COLUMNS (e.name, d.name))")
|
||||
check ast.stmts[0].selFrom.kind == nkGraphTraversal
|
||||
check ast.stmts[0].selFrom.gtGraphName == "org_chart"
|
||||
|
||||
test "Parse ORDER BY with direction":
|
||||
let ast = parse("SELECT name FROM users ORDER BY age DESC")
|
||||
check ast.stmts[0].selOrderBy.len == 1
|
||||
@@ -994,6 +1026,25 @@ suite "BaraQL Parser — Extended":
|
||||
let ast = parse("SELECT * FROM a JOIN b ON a.id = b.aid JOIN c ON b.id = c.bid")
|
||||
check ast.stmts[0].selJoins.len == 2
|
||||
|
||||
test "Parse LATERAL JOIN":
|
||||
let ast = parse("SELECT u.name, x.total FROM users u JOIN LATERAL (SELECT o.total FROM orders o WHERE o.user_id = u.id) AS x ON 1=1")
|
||||
check ast.stmts[0].selJoins.len == 1
|
||||
check ast.stmts[0].selJoins[0].joinLateral == true
|
||||
check ast.stmts[0].selJoins[0].joinTarget.kind == nkSubquery
|
||||
check ast.stmts[0].selJoins[0].joinAlias == "x"
|
||||
|
||||
test "Parse LEFT LATERAL JOIN":
|
||||
let ast = parse("SELECT u.name FROM users u LEFT JOIN LATERAL (SELECT 1) AS x ON 1=1")
|
||||
check ast.stmts[0].selJoins.len == 1
|
||||
check ast.stmts[0].selJoins[0].joinKind == jkLeft
|
||||
check ast.stmts[0].selJoins[0].joinLateral == true
|
||||
|
||||
test "Parse CROSS JOIN LATERAL":
|
||||
let ast = parse("SELECT * FROM users u CROSS JOIN LATERAL (SELECT 1) AS x")
|
||||
check ast.stmts[0].selJoins.len == 1
|
||||
check ast.stmts[0].selJoins[0].joinKind == jkCross
|
||||
check ast.stmts[0].selJoins[0].joinLateral == true
|
||||
|
||||
test "Parse CTE (WITH)":
|
||||
let ast = parse("WITH active AS (SELECT * FROM users WHERE active = true) SELECT * FROM active")
|
||||
check ast.stmts[0].selWith.len == 1
|
||||
@@ -2641,6 +2692,122 @@ suite "Window Functions":
|
||||
for row in r.rows:
|
||||
check row["bucket"] in @["1", "2"]
|
||||
|
||||
suite "GROUP BY Aggregates":
|
||||
var db: LSMTree
|
||||
var ctx: qexec.ExecutionContext
|
||||
|
||||
setup:
|
||||
db = newLSMTree("")
|
||||
ctx = qexec.newExecutionContext(db)
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE sales (id INT, dept TEXT, amount INT)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (1, 'A', 100)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (2, 'A', 200)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (3, 'B', 150)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (4, 'B', 50)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (5, 'C', 300)"))
|
||||
|
||||
test "GROUP BY with COUNT(*)":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, COUNT(*) AS cnt FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
|
||||
test "GROUP BY with SUM":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
var foundA = false
|
||||
for row in r.rows:
|
||||
if row["dept"] == "A":
|
||||
check row["total"] == "300.0"
|
||||
foundA = true
|
||||
check foundA
|
||||
|
||||
test "GROUP BY with AVG":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, AVG(amount) AS avg_amt FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
var foundB = false
|
||||
for row in r.rows:
|
||||
if row["dept"] == "B":
|
||||
check row["avg_amt"] == "100.0"
|
||||
foundB = true
|
||||
check foundB
|
||||
|
||||
test "GROUP BY with MIN and MAX":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, MIN(amount) AS lo, MAX(amount) AS hi FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
var foundA = false
|
||||
for row in r.rows:
|
||||
if row["dept"] == "A":
|
||||
check row["lo"] == "100"
|
||||
check row["hi"] == "200"
|
||||
foundA = true
|
||||
check foundA
|
||||
|
||||
test "GROUP BY with HAVING":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY dept HAVING SUM(amount) > 200"))
|
||||
check r.success
|
||||
check r.rows.len == 2 # A (300) and C (300)
|
||||
for row in r.rows:
|
||||
check row["dept"] in @["A", "C"]
|
||||
|
||||
test "GROUP BY with multiple aggregates":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, COUNT(*) AS cnt, SUM(amount) AS total, AVG(amount) AS avg_amt FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
|
||||
test "COUNT with FILTER (WHERE ...)":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE amount > 100) AS big FROM sales"))
|
||||
check r.success
|
||||
check r.rows.len == 1
|
||||
check r.rows[0]["total"] == "5"
|
||||
check r.rows[0]["big"] == "3"
|
||||
|
||||
test "SUM with FILTER (WHERE ...)":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) FILTER (WHERE amount > 100) AS big_total FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
var foundA = false
|
||||
for row in r.rows:
|
||||
if row["dept"] == "A":
|
||||
check row["big_total"] == "200.0"
|
||||
foundA = true
|
||||
check foundA
|
||||
|
||||
test "ARRAY_AGG":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, ARRAY_AGG(amount) AS amounts FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
var foundA = false
|
||||
for row in r.rows:
|
||||
if row["dept"] == "A":
|
||||
check "100" in row["amounts"]
|
||||
check "200" in row["amounts"]
|
||||
foundA = true
|
||||
check foundA
|
||||
|
||||
test "STRING_AGG":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, STRING_AGG(amount, ',') AS vals FROM sales GROUP BY dept"))
|
||||
check r.success
|
||||
check r.rows.len == 3
|
||||
|
||||
test "ROLLUP":
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY ROLLUP (dept)"))
|
||||
check r.success
|
||||
# 3 dept groups + 1 grand total = 4 rows
|
||||
check r.rows.len == 4
|
||||
|
||||
test "PIVOT":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE emp (name TEXT, dept TEXT, salary INT)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Alice', 'Eng', 90000)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Bob', 'Eng', 80000)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Charlie', 'Sales', 70000)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Diana', 'Sales', 75000)"))
|
||||
let r = qexec.executeQuery(ctx, parse("SELECT * FROM (SELECT name, dept, salary FROM emp) PIVOT (SUM(salary) FOR dept IN ('Eng', 'Sales'))"))
|
||||
check r.success
|
||||
check r.rows.len == 4 # one row per employee
|
||||
|
||||
# JOIN tests
|
||||
include "join_tests"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user