From 2d8fff6383b16147228a0278d5b7e3f8707a35eb Mon Sep 17 00:00:00 2001 From: wkaiz Date: Mon, 8 Sep 2025 02:13:59 -0700 Subject: [PATCH 1/2] Adding MySQL Code Generator, Test Cases, and Sample Outputs --- .../org/qed/Generated/MySQL/FilterMerge1.sql | 5 + .../org/qed/Generated/MySQL/FilterMerge2.sql | 5 + .../org/qed/Generated/MySQL/ProjectMerge1.sql | 5 + .../org/qed/Generated/MySQL/ProjectMerge2.sql | 5 + .../org/qed/Generated/MySQLGenerator.java | 120 ++++++++++++++++++ .../java/org/qed/Generated/MySQLTester.java | 40 ++++++ .../org/qed/Generated/Tests-MySQL/script.py | 66 ++++++++++ 7 files changed, 246 insertions(+) create mode 100644 src/main/java/org/qed/Generated/MySQL/FilterMerge1.sql create mode 100644 src/main/java/org/qed/Generated/MySQL/FilterMerge2.sql create mode 100644 src/main/java/org/qed/Generated/MySQL/ProjectMerge1.sql create mode 100644 src/main/java/org/qed/Generated/MySQL/ProjectMerge2.sql create mode 100644 src/main/java/org/qed/Generated/MySQLGenerator.java create mode 100644 src/main/java/org/qed/Generated/MySQLTester.java create mode 100644 src/main/java/org/qed/Generated/Tests-MySQL/script.py diff --git a/src/main/java/org/qed/Generated/MySQL/FilterMerge1.sql b/src/main/java/org/qed/Generated/MySQL/FilterMerge1.sql new file mode 100644 index 0000000..3579e90 --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQL/FilterMerge1.sql @@ -0,0 +1,5 @@ +INSERT INTO query_rewrite.rewrite_rules + (pattern, replacement) VALUES( + 'SELECT * FROM (SELECT * FROM testdb.users WHERE id = ?) AS t0 WHERE status = ?', + 'SELECT * FROM testdb.users WHERE id = ? AND status = ?' +); \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/MySQL/FilterMerge2.sql b/src/main/java/org/qed/Generated/MySQL/FilterMerge2.sql new file mode 100644 index 0000000..a48e50c --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQL/FilterMerge2.sql @@ -0,0 +1,5 @@ +INSERT INTO query_rewrite.rewrite_rules + (pattern, replacement) VALUES( + 'SELECT * FROM (SELECT * FROM testdb.users WHERE status = ?) AS t0 WHERE id = ?', + 'SELECT * FROM testdb.users WHERE status = ? AND id = ?' +); \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/MySQL/ProjectMerge1.sql b/src/main/java/org/qed/Generated/MySQL/ProjectMerge1.sql new file mode 100644 index 0000000..4544506 --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQL/ProjectMerge1.sql @@ -0,0 +1,5 @@ +INSERT INTO query_rewrite.rewrite_rules + (pattern, replacement) VALUES( + 'SELECT id, status FROM (SELECT id, status FROM testdb.users) AS t0', + 'SELECT id, status FROM testdb.users' +); \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/MySQL/ProjectMerge2.sql b/src/main/java/org/qed/Generated/MySQL/ProjectMerge2.sql new file mode 100644 index 0000000..df80c53 --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQL/ProjectMerge2.sql @@ -0,0 +1,5 @@ +INSERT INTO query_rewrite.rewrite_rules + (pattern, replacement) VALUES( + 'SELECT status, id FROM (SELECT status, id FROM testdb.users) AS t0', + 'SELECT status, id FROM testdb.users' +); \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/MySQLGenerator.java b/src/main/java/org/qed/Generated/MySQLGenerator.java new file mode 100644 index 0000000..1a876aa --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQLGenerator.java @@ -0,0 +1,120 @@ +package org.qed.Generated; + +import org.qed.RelRN; +import org.qed.RexRN; + +import java.util.ArrayList; +import java.util.List; + +public class MySQLGenerator { + + private int subqueryCounter = 0; + private final String tableName; + private final List columnNames; + + public MySQLGenerator(String tableName, List columnNames) { + this.tableName = tableName; + this.columnNames = columnNames; + } + + private static class FlattenedSQLParts { + String fromClause = ""; + List projections = new ArrayList<>(); + List conditions = new ArrayList<>(); + } + + public String translate(String name, RelRN before, RelRN after) { + subqueryCounter = 0; + + String beforeSQL = transformNested(before, true); + String afterSQL = transformFlatten(after); + + return "INSERT INTO query_rewrite.rewrite_rules\n" + + " (pattern, replacement) VALUES(\n" + + " '" + beforeSQL + "',\n" + + " '" + afterSQL + "'\n" + + ");"; + } + + public String transformNested(RelRN node, boolean isRoot) { + return switch (node) { + case RelRN.Scan scan -> "SELECT * FROM " + tableName; + case RelRN.Project project -> { + String alias = "t" + (subqueryCounter++); + String cols = String.join(", ", columnNames); + if (project.source() instanceof RelRN.Scan) { + yield "SELECT " + cols + " FROM (SELECT " + cols + " FROM " + tableName + ") AS " + alias; + } else if (project.source() instanceof RelRN.Project) { + yield "SELECT " + cols + " FROM (SELECT " + cols + " FROM " + tableName + ") AS " + alias; + } else { + String innerSQL = transformNested(project.source(), false); + yield "SELECT " + cols + " FROM (" + innerSQL + ") AS " + alias; + } + } + case RelRN.Filter filter -> { + String innerSQL = transformNested(filter.source(), false); + String condSQL = columnNames.get(0) + " = ?"; + if (isRoot) { + yield innerSQL + " WHERE " + columnNames.get(1) + " = ?"; + } else { + String alias = "t" + (subqueryCounter++); + yield "SELECT * FROM (" + innerSQL + " WHERE " + condSQL + ") AS " + alias; + } + } + default -> throw new UnsupportedOperationException("Unsupported RelRN: " + node); + }; + } + + public String transformFlatten(RelRN node) { + FlattenedSQLParts parts = new FlattenedSQLParts(); + collectFlattenedParts(node, parts); + String selectClause = parts.projections.isEmpty() + ? "SELECT *" + : "SELECT " + String.join(", ", parts.projections); + + String whereClause = parts.conditions.isEmpty() + ? "" + : " WHERE " + String.join(" AND ", parts.conditions); + return selectClause + " FROM " + parts.fromClause + whereClause; + } + + private void collectFlattenedParts(RelRN node, FlattenedSQLParts parts) { + switch (node) { + case RelRN.Scan scan -> parts.fromClause = tableName; + case RelRN.Project project -> { + collectFlattenedParts(project.source(), parts); + addColumnNames(parts.projections); + } + case RelRN.Filter filter -> { + collectFlattenedParts(filter.source(), parts); + collectPredConditions(filter.cond(), parts.conditions); + } + default -> throw new UnsupportedOperationException("Unsupported RelRN: " + node); + } + } + + private void addColumnNames(List projections) { + projections.addAll(columnNames); + } + + private void collectPredConditions(RexRN pred, List conditions) { + switch (pred) { + case RexRN.Pred p -> { + if (conditions.isEmpty()) { + conditions.add(columnNames.get(0) + " = ?"); + } else if (conditions.size() == 1 && columnNames.size() > 1) { + conditions.add(columnNames.get(1) + " = ?"); + } else { + conditions.add(columnNames.get(0) + " = ?"); + } + } + case RexRN.And and -> { + for (RexRN child : and.sources()) { + collectPredConditions(child, conditions); + } + } + default -> throw new UnsupportedOperationException("Unsupported RexRN: " + pred); + } + } + +} diff --git a/src/main/java/org/qed/Generated/MySQLTester.java b/src/main/java/org/qed/Generated/MySQLTester.java new file mode 100644 index 0000000..3311d15 --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQLTester.java @@ -0,0 +1,40 @@ +package org.qed.Generated; + +import org.qed.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class MySQLTester { + + public static String genPath = "src/main/java/org/qed/Generated/MySQL"; + + public static String tableName = "testdb.users"; + public static List columnNames = List.of("id", "status"); + + public static void main(String[] args) { + var filterRule = new org.qed.Generated.RRuleInstances.FilterMerge(); + new MySQLTester().serializeWithNumericSuffix(filterRule, genPath); + + var projectRule = new org.qed.Generated.RRuleInstances.ProjectMerge(); + new MySQLTester().serializeWithNumericSuffix(projectRule, genPath); + } + + public void serializeWithNumericSuffix(RRule rule, String path) { + serialize(rule, path, tableName, columnNames, 1); + serialize(rule, path, tableName, List.of(columnNames.get(1), columnNames.get(0)), 2); + } + + private void serialize(RRule rule, String path, String tableName, List colNames, int fileIndex) { + var generator = new MySQLGenerator(tableName, colNames); + var codeGen = generator.translate(rule.name(), rule.before(), rule.after()); + try { + Files.createDirectories(Path.of(path)); + String fileName = rule.name() + fileIndex + ".sql"; + Files.write(Path.of(path, fileName), codeGen.getBytes()); + } catch (IOException ioe) { + System.err.println(ioe.getMessage()); + } + } +} diff --git a/src/main/java/org/qed/Generated/Tests-MySQL/script.py b/src/main/java/org/qed/Generated/Tests-MySQL/script.py new file mode 100644 index 0000000..0da0cac --- /dev/null +++ b/src/main/java/org/qed/Generated/Tests-MySQL/script.py @@ -0,0 +1,66 @@ +import mysql.connector +from pathlib import Path + +MYSQL_USER = "root" +MYSQL_PASSWORD = "wkaiz" +MYSQL_DATABASE = "query_rewrite" +SQL_FILE = Path("C:\\Users\\wesle\\OneDrive\\Desktop\\parser\\src\\main\\java\\org\\qed\\Generated\\MySQL\\FilterMerge2.sql") # path to your SQL file + +TEST_QUERY = """ +SELECT * FROM (SELECT * FROM testdb.users WHERE status = 'active') AS t0 +WHERE id = 1; +""" + +conn = mysql.connector.connect( + host="localhost", + user=MYSQL_USER, + password=MYSQL_PASSWORD, + database=MYSQL_DATABASE +) +cursor = conn.cursor() + +with SQL_FILE.open("r", encoding="utf-8") as f: + sql_commands = f.read() + +for cmd in sql_commands.split(";"): + cmd = cmd.strip() + if cmd: + cursor.execute(cmd + ";") + +conn.commit() +print(f"{SQL_FILE} executed successfully.") + +cursor.execute(""" +DELETE FROM rewrite_rules +WHERE id < ( + SELECT max_id FROM (SELECT MAX(id) AS max_id FROM rewrite_rules) AS t +); + +""") +conn.commit() +print("Deleted all rules except the last one.") + +cursor.execute("CALL flush_rewrite_rules();") +conn.commit() +print("Flushed rewrite rules.") + +cursor.execute("SELECT * FROM rewrite_rules;") +print("Current rules in table:") +for row in cursor.fetchall(): + print(row) + +cursor.execute(TEST_QUERY) +results = cursor.fetchall() + +print("\nTest query results:") +for row in results: + print(row) + +cursor.execute("SHOW WARNINGS;") +warnings = cursor.fetchall() +print("\nWarnings (should indicate rewrite):") +for w in warnings: + print(w) + +cursor.close() +conn.close() From 417f61d1d360e318cef24d70522c4ba5b84326bb Mon Sep 17 00:00:00 2001 From: wkaiz Date: Mon, 8 Sep 2025 03:04:56 -0700 Subject: [PATCH 2/2] Adding Support for JoinCommute --- .../org/qed/Generated/MySQL/JoinCommute1.sql | 5 + .../org/qed/Generated/MySQL/JoinCommute2.sql | 5 + .../org/qed/Generated/MySQLGenerator.java | 141 +++++++++++------- .../java/org/qed/Generated/MySQLTester.java | 3 + 4 files changed, 97 insertions(+), 57 deletions(-) create mode 100644 src/main/java/org/qed/Generated/MySQL/JoinCommute1.sql create mode 100644 src/main/java/org/qed/Generated/MySQL/JoinCommute2.sql diff --git a/src/main/java/org/qed/Generated/MySQL/JoinCommute1.sql b/src/main/java/org/qed/Generated/MySQL/JoinCommute1.sql new file mode 100644 index 0000000..4b6fffb --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQL/JoinCommute1.sql @@ -0,0 +1,5 @@ +INSERT INTO query_rewrite.rewrite_rules + (pattern, replacement) VALUES( + '(SELECT * FROM testdb.users) AS t0 INNER JOIN (SELECT * FROM testdb.users) AS t1 ON t0.id = t1.id', + '(SELECT * FROM testdb.users) AS t1 INNER JOIN (SELECT * FROM testdb.users) AS t0 ON t1.id = t0.id' +); \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/MySQL/JoinCommute2.sql b/src/main/java/org/qed/Generated/MySQL/JoinCommute2.sql new file mode 100644 index 0000000..a54e10f --- /dev/null +++ b/src/main/java/org/qed/Generated/MySQL/JoinCommute2.sql @@ -0,0 +1,5 @@ +INSERT INTO query_rewrite.rewrite_rules + (pattern, replacement) VALUES( + '(SELECT * FROM testdb.users) AS t0 INNER JOIN (SELECT * FROM testdb.users) AS t1 ON t0.status = t1.status', + '(SELECT * FROM testdb.users) AS t1 INNER JOIN (SELECT * FROM testdb.users) AS t0 ON t1.status = t0.status' +); \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/MySQLGenerator.java b/src/main/java/org/qed/Generated/MySQLGenerator.java index 1a876aa..4f7d49a 100644 --- a/src/main/java/org/qed/Generated/MySQLGenerator.java +++ b/src/main/java/org/qed/Generated/MySQLGenerator.java @@ -1,10 +1,12 @@ package org.qed.Generated; +import org.qed.Generated.RRuleInstances.JoinCommute; import org.qed.RelRN; import org.qed.RexRN; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; public class MySQLGenerator { @@ -24,10 +26,19 @@ private static class FlattenedSQLParts { } public String translate(String name, RelRN before, RelRN after) { - subqueryCounter = 0; - - String beforeSQL = transformNested(before, true); - String afterSQL = transformFlatten(after); + String beforeSQL; + String afterSQL; + + if (name.equals("JoinCommute")) { + subqueryCounter = 0; + beforeSQL = transformNested(before, true, false, new AtomicInteger(0)); + subqueryCounter = 0; + afterSQL = transformNested(before, true, true, new AtomicInteger(0)); + } else { + subqueryCounter = 0; + beforeSQL = transformNested(before, true, false, new AtomicInteger(0)); + afterSQL = transformFlatten(after); + } return "INSERT INTO query_rewrite.rewrite_rules\n" + " (pattern, replacement) VALUES(\n" + @@ -36,45 +47,73 @@ public String translate(String name, RelRN before, RelRN after) { ");"; } - public String transformNested(RelRN node, boolean isRoot) { - return switch (node) { - case RelRN.Scan scan -> "SELECT * FROM " + tableName; - case RelRN.Project project -> { + private String transformNested(RelRN node, boolean isRoot, boolean swapJoinSides, AtomicInteger filterIndex) { + if (node instanceof RelRN.Scan) { + return "SELECT * FROM " + tableName; + } else if (node instanceof RelRN.Project project) { + String cols = String.join(", ", columnNames); + if (project.source() instanceof RelRN.Scan) { + return "SELECT " + cols + " FROM " + tableName; + } + String innerSQL = transformNested(project.source(), false, swapJoinSides, filterIndex); + String alias = "t" + (subqueryCounter++); + return "SELECT " + cols + " FROM (" + innerSQL + ") AS " + alias; + } else if (node instanceof RelRN.Filter filter) { + String innerSQL = transformNested(filter.source(), false, swapJoinSides, filterIndex); + int currentIndex = filterIndex.getAndIncrement(); + String condition = (currentIndex < columnNames.size()) + ? columnNames.get(currentIndex) + " = ?" + : columnNames.get(0) + " = ?"; + + if (isRoot) { + return innerSQL + " WHERE " + condition; + } else { String alias = "t" + (subqueryCounter++); - String cols = String.join(", ", columnNames); - if (project.source() instanceof RelRN.Scan) { - yield "SELECT " + cols + " FROM (SELECT " + cols + " FROM " + tableName + ") AS " + alias; - } else if (project.source() instanceof RelRN.Project) { - yield "SELECT " + cols + " FROM (SELECT " + cols + " FROM " + tableName + ") AS " + alias; - } else { - String innerSQL = transformNested(project.source(), false); - yield "SELECT " + cols + " FROM (" + innerSQL + ") AS " + alias; - } + return "SELECT * FROM (" + innerSQL + " WHERE " + condition + ") AS " + alias; } - case RelRN.Filter filter -> { - String innerSQL = transformNested(filter.source(), false); - String condSQL = columnNames.get(0) + " = ?"; - if (isRoot) { - yield innerSQL + " WHERE " + columnNames.get(1) + " = ?"; - } else { - String alias = "t" + (subqueryCounter++); - yield "SELECT * FROM (" + innerSQL + " WHERE " + condSQL + ") AS " + alias; - } + } else if (node instanceof RelRN.Join join) { + String leftAlias = "t0"; + String rightAlias = "t1"; + + RelRN firstNode = swapJoinSides ? join.right() : join.left(); + String firstAlias = swapJoinSides ? rightAlias : leftAlias; + RelRN secondNode = swapJoinSides ? join.left() : join.right(); + String secondAlias = swapJoinSides ? leftAlias : rightAlias; + + String firstSQL = "(" + transformNested(firstNode, false, swapJoinSides, filterIndex) + ")"; + String secondSQL = "(" + transformNested(secondNode, false, swapJoinSides, filterIndex) + ")"; + + String joinCond = renderJoinCondition(join.cond(), leftAlias, rightAlias, swapJoinSides); + + return firstSQL + " AS " + firstAlias + + " " + join.ty().semantics().name() + " JOIN " + + secondSQL + " AS " + secondAlias + + " ON " + joinCond; + + } else if (node instanceof JoinCommute.ProjectionRelRN projRN) { + return transformNested(projRN.source(), isRoot, swapJoinSides, filterIndex); + } else { + throw new UnsupportedOperationException("Unsupported RelRN: " + node); + } + } + + private String renderJoinCondition(RexRN cond, String leftAlias, String rightAlias, boolean swap) { + if (cond instanceof RexRN.Pred p) { + if (p.sources().get(0) instanceof RexRN.JoinField jf) { + String colName = columnNames.get(jf.ordinal()); + String first = swap ? rightAlias : leftAlias; + String second = swap ? leftAlias : rightAlias; + return first + "." + colName + " = " + second + "." + colName; } - default -> throw new UnsupportedOperationException("Unsupported RelRN: " + node); - }; + } + throw new UnsupportedOperationException("Unsupported join condition: " + cond); } public String transformFlatten(RelRN node) { FlattenedSQLParts parts = new FlattenedSQLParts(); collectFlattenedParts(node, parts); - String selectClause = parts.projections.isEmpty() - ? "SELECT *" - : "SELECT " + String.join(", ", parts.projections); - - String whereClause = parts.conditions.isEmpty() - ? "" - : " WHERE " + String.join(" AND ", parts.conditions); + String selectClause = parts.projections.isEmpty() ? "SELECT *" : "SELECT " + String.join(", ", parts.projections); + String whereClause = parts.conditions.isEmpty() ? "" : " WHERE " + String.join(" AND ", parts.conditions); return selectClause + " FROM " + parts.fromClause + whereClause; } @@ -83,38 +122,26 @@ private void collectFlattenedParts(RelRN node, FlattenedSQLParts parts) { case RelRN.Scan scan -> parts.fromClause = tableName; case RelRN.Project project -> { collectFlattenedParts(project.source(), parts); - addColumnNames(parts.projections); + parts.projections.addAll(columnNames); } case RelRN.Filter filter -> { collectFlattenedParts(filter.source(), parts); collectPredConditions(filter.cond(), parts.conditions); } - default -> throw new UnsupportedOperationException("Unsupported RelRN: " + node); + default -> throw new UnsupportedOperationException("Unsupported RelRN for flatten: " + node); } } - private void addColumnNames(List projections) { - projections.addAll(columnNames); - } - private void collectPredConditions(RexRN pred, List conditions) { - switch (pred) { - case RexRN.Pred p -> { - if (conditions.isEmpty()) { - conditions.add(columnNames.get(0) + " = ?"); - } else if (conditions.size() == 1 && columnNames.size() > 1) { - conditions.add(columnNames.get(1) + " = ?"); - } else { - conditions.add(columnNames.get(0) + " = ?"); - } + if (pred instanceof RexRN.Pred) { + int currentConditions = conditions.size(); + if (currentConditions < columnNames.size()) { + conditions.add(columnNames.get(currentConditions) + " = ?"); } - case RexRN.And and -> { - for (RexRN child : and.sources()) { - collectPredConditions(child, conditions); - } + } else if (pred instanceof RexRN.And and) { + for (RexRN child : and.sources()) { + collectPredConditions(child, conditions); } - default -> throw new UnsupportedOperationException("Unsupported RexRN: " + pred); } } - -} +} \ No newline at end of file diff --git a/src/main/java/org/qed/Generated/MySQLTester.java b/src/main/java/org/qed/Generated/MySQLTester.java index 3311d15..9c02780 100644 --- a/src/main/java/org/qed/Generated/MySQLTester.java +++ b/src/main/java/org/qed/Generated/MySQLTester.java @@ -19,6 +19,9 @@ public static void main(String[] args) { var projectRule = new org.qed.Generated.RRuleInstances.ProjectMerge(); new MySQLTester().serializeWithNumericSuffix(projectRule, genPath); + + var joinCommute = new org.qed.Generated.RRuleInstances.JoinCommute(); + new MySQLTester().serializeWithNumericSuffix(joinCommute, genPath); } public void serializeWithNumericSuffix(RRule rule, String path) {