From eb5e013a227eae7834c5b1f3865c451e57bb3f51 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Thu, 9 Jul 2026 13:21:12 +0800 Subject: [PATCH 01/14] Add EET (Equivalent Expression Transformation) oracle for MySQL This commit implements EET from Jiang & Su, "Detecting Logic Bugs in Database Engines via Equivalent Expression Transformation" (OSDI'24). Common, DBMS-independent core: - EETTransformation: the 7 transformation rules of the paper's Table 2. - EETNodeFactory: primitive node constructors the rules are built from. - EETGenerator: generator interface an EET-capable DBMS implements. - EETOracle: orchestration and result comparison (paper Figure 4), with a Reproducer for confirmation/reduction. MySQL-specific implementation: - MySQLEETTransformer recursively rewrites the MySQL AST, rebuilding each node from its transformed children and threading a boolean/scalar context flag so the determined-boolean rules are only applied where sound. - MySQLEETNodeFactory builds the MySQL nodes; MySQLExpressionGenerator implements EETGenerator; MySQLUnaryPrefixOperation exposes getOp(); MySQLOracleFactory registers the EET oracle. - MySQLTableGenerator skips ZEROFILL for EET (as for TLP_WHERE/PQS/DQP) to avoid display-metadata-only false positives while preserving search space. Runs via: java -jar target/sqlancer-*.jar mysql --oracle EET --- src/sqlancer/common/gen/EETGenerator.java | 44 +++++++ .../common/oracle/EETNodeFactory.java | 39 ++++++ src/sqlancer/common/oracle/EETOracle.java | 121 +++++++++++++++++ .../common/oracle/EETTransformation.java | 83 ++++++++++++ src/sqlancer/mysql/MySQLOracleFactory.java | 10 ++ .../mysql/ast/MySQLUnaryPrefixOperation.java | 4 + .../mysql/gen/MySQLEETNodeFactory.java | 70 ++++++++++ .../mysql/gen/MySQLEETTransformer.java | 124 ++++++++++++++++++ .../mysql/gen/MySQLExpressionGenerator.java | 13 +- .../mysql/gen/MySQLTableGenerator.java | 2 +- 10 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 src/sqlancer/common/gen/EETGenerator.java create mode 100644 src/sqlancer/common/oracle/EETNodeFactory.java create mode 100644 src/sqlancer/common/oracle/EETOracle.java create mode 100644 src/sqlancer/common/oracle/EETTransformation.java create mode 100644 src/sqlancer/mysql/gen/MySQLEETNodeFactory.java create mode 100644 src/sqlancer/mysql/gen/MySQLEETTransformer.java diff --git a/src/sqlancer/common/gen/EETGenerator.java b/src/sqlancer/common/gen/EETGenerator.java new file mode 100644 index 000000000..f468ff6d2 --- /dev/null +++ b/src/sqlancer/common/gen/EETGenerator.java @@ -0,0 +1,44 @@ +package sqlancer.common.gen; + +import java.util.List; + +import sqlancer.common.ast.newast.Expression; +import sqlancer.common.ast.newast.Join; +import sqlancer.common.ast.newast.Select; +import sqlancer.common.schema.AbstractTable; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.common.schema.AbstractTables; + +/** + * Generator interface used by {@link sqlancer.common.oracle.EETOracle}. In addition to generating a random query (like + * the other oracle generators), an EET generator can transform an expression into a semantically equivalent one + * according to the EET transformation rules. + */ +public interface EETGenerator, J extends Join, E extends Expression, T extends AbstractTable, C extends AbstractTableColumn> { + + EETGenerator setTablesAndColumns(AbstractTables tables); + + S generateSelect(); + + List getRandomJoinClauses(); + + List getTableRefs(); + + List generateFetchColumns(boolean shouldCreateDummy); + + E generateBooleanExpression(); + + /** + * Transforms an expression into a semantically equivalent one (the core of EET). Typically this recursively + * traverses the expression's AST and replaces sub-expressions with equivalent ones. + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value (e.g. a WHERE predicate); this controls + * which transformation rules are applicable + * + * @return a semantically equivalent expression + */ + E transformExpression(E expr, boolean booleanContext); +} diff --git a/src/sqlancer/common/oracle/EETNodeFactory.java b/src/sqlancer/common/oracle/EETNodeFactory.java new file mode 100644 index 000000000..2cc933ced --- /dev/null +++ b/src/sqlancer/common/oracle/EETNodeFactory.java @@ -0,0 +1,39 @@ +package sqlancer.common.oracle; + +/** + * Factory for constructing the AST nodes needed by the {@link EETTransformation equivalent expression transformation} + * rules. Because every DBMS has its own expression AST, the actual node construction is DBMS-specific; this interface + * lets the (DBMS-independent) transformation rules be expressed once in terms of a small set of primitive operations. + * + * @param + * the DBMS-specific expression type + */ +public interface EETNodeFactory { + + /** Builds {@code left AND right}. */ + E and(E left, E right); + + /** Builds {@code left OR right}. */ + E or(E left, E right); + + /** Builds {@code NOT expr}. */ + E not(E expr); + + /** Builds {@code expr IS NULL}. */ + E isNull(E expr); + + /** Builds {@code expr IS NOT NULL}. */ + E isNotNull(E expr); + + /** Builds {@code CASE WHEN condition THEN thenExpr ELSE elseExpr END}. */ + E caseWhen(E condition, E thenExpr, E elseExpr); + + /** Generates a fresh random boolean expression, reusing the variables available to the query generator. */ + E generateBooleanExpression(); + + /** + * Whether {@code expr} may be wrapped in a CASE WHEN expression. Some expressions (e.g. table references) are not + * CASE-WHEN applicable and must be transformed to themselves (rule No. 7 of the EET paper). + */ + boolean isCaseWhenApplicable(E expr); +} diff --git a/src/sqlancer/common/oracle/EETOracle.java b/src/sqlancer/common/oracle/EETOracle.java new file mode 100644 index 000000000..0f192f9ab --- /dev/null +++ b/src/sqlancer/common/oracle/EETOracle.java @@ -0,0 +1,121 @@ +package sqlancer.common.oracle; + +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +import sqlancer.ComparatorHelper; +import sqlancer.Reproducer; +import sqlancer.SQLGlobalState; +import sqlancer.common.ast.newast.Expression; +import sqlancer.common.ast.newast.Join; +import sqlancer.common.ast.newast.Select; +import sqlancer.common.gen.EETGenerator; +import sqlancer.common.query.ExpectedErrors; +import sqlancer.common.schema.AbstractSchema; +import sqlancer.common.schema.AbstractTable; +import sqlancer.common.schema.AbstractTableColumn; +import sqlancer.common.schema.AbstractTables; + +/** + * EET (Equivalent Expression Transformation) oracle, based on "Detecting Logic Bugs in Database Engines via Equivalent + * Expression Transformation" (Jiang & Su, OSDI'24). + * + *

+ * The oracle generates a random query and then transforms its expressions (the WHERE predicate and the fetch columns) + * into semantically equivalent ones using {@link EETGenerator#transformExpression}. Because the transformation preserves + * semantics, the original and the transformed query must return the same result set; any discrepancy indicates a logic + * bug in the DBMS. + */ +public class EETOracle, J extends Join, E extends Expression, S extends AbstractSchema, T extends AbstractTable, C extends AbstractTableColumn, G extends SQLGlobalState> + implements TestOracle { + + private final G state; + private EETGenerator gen; + private final ExpectedErrors errors; + + private Reproducer reproducer; + private String generatedQueryString; + + private final class EETReproducer implements Reproducer { + private final String originalQueryString; + private final String transformedQueryString; + private final List resultSet; + + EETReproducer(String originalQueryString, String transformedQueryString, List resultSet) { + this.originalQueryString = originalQueryString; + this.transformedQueryString = transformedQueryString; + this.resultSet = resultSet; + } + + @Override + public boolean bugStillTriggers(G globalState) { + try { + List transformedResultSet = ComparatorHelper + .getResultSetFirstColumnAsString(transformedQueryString, errors, globalState); + ComparatorHelper.assumeResultSetsAreEqual(resultSet, transformedResultSet, originalQueryString, + List.of(transformedQueryString), globalState); + } catch (AssertionError triggeredError) { + return true; + } catch (SQLException ignored) { + } + return false; + } + } + + public EETOracle(G state, EETGenerator gen, ExpectedErrors expectedErrors) { + if (state == null || gen == null || expectedErrors == null) { + throw new IllegalArgumentException("Null variables used to initialize test oracle."); + } + this.state = state; + this.gen = gen; + this.errors = expectedErrors; + } + + @Override + public void check() throws SQLException { + reproducer = null; + S schema = state.getSchema(); + AbstractTables targetTables = TestOracleUtils.getRandomTableNonEmptyTables(schema); + gen = gen.setTablesAndColumns(targetTables); + + Z select = gen.generateSelect(); + select.setJoinClauses(gen.getRandomJoinClauses()); + select.setFromList(gen.getTableRefs()); + List fetchColumns = gen.generateFetchColumns(true); + select.setFetchColumns(fetchColumns); + E whereClause = gen.generateBooleanExpression(); + select.setWhereClause(whereClause); + + String originalQueryString = select.asString(); + generatedQueryString = originalQueryString; + List originalResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, + state); + + // Transform the query's expressions into semantically equivalent ones. Fetch columns are scalar expressions, + // while the WHERE clause is evaluated in a boolean context. + List transformedFetchColumns = fetchColumns.stream().map(c -> gen.transformExpression(c, false)) + .collect(Collectors.toList()); + select.setFetchColumns(transformedFetchColumns); + select.setWhereClause(gen.transformExpression(whereClause, true)); + + String transformedQueryString = select.asString(); + List transformedResultSet = ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, + errors, state); + + ComparatorHelper.assumeResultSetsAreEqual(originalResultSet, transformedResultSet, originalQueryString, + List.of(transformedQueryString), state); + + reproducer = new EETReproducer(originalQueryString, transformedQueryString, originalResultSet); + } + + @Override + public Reproducer getLastReproducer() { + return reproducer; + } + + @Override + public String getLastQueryString() { + return generatedQueryString; + } +} diff --git a/src/sqlancer/common/oracle/EETTransformation.java b/src/sqlancer/common/oracle/EETTransformation.java new file mode 100644 index 000000000..ab9b7fefe --- /dev/null +++ b/src/sqlancer/common/oracle/EETTransformation.java @@ -0,0 +1,83 @@ +package sqlancer.common.oracle; + +import sqlancer.Randomly; + +/** + * Implements the semantic-preserving expression transformation rules of EET (Equivalent Expression Transformation, Jiang + * & Su, OSDI'24), Table 2. Given an expression, {@link #applyRandomRule} returns a semantically equivalent + * expression built from the primitives provided by an {@link EETNodeFactory}. The rules are DBMS-independent; only the + * node construction (via the factory) is DBMS-specific. + * + *

+ * The rules rely on two always-determined boolean expressions built from an arbitrary boolean {@code p}: + *

    + *
  • {@code true_expr(p) = p OR (NOT p) OR (p IS NULL)}, which always evaluates to TRUE, and
  • + *
  • {@code false_expr(p) = p AND (NOT p) AND (p IS NOT NULL)}, which always evaluates to FALSE.
  • + *
+ * + * @param + * the DBMS-specific expression type + */ +public class EETTransformation { + + private final EETNodeFactory factory; + + public EETTransformation(EETNodeFactory factory) { + this.factory = factory; + } + + // true_expr(p) = p OR (NOT p) OR (p IS NULL) -> always TRUE + private E trueExpr() { + E p = factory.generateBooleanExpression(); + return factory.or(factory.or(p, factory.not(p)), factory.isNull(p)); + } + + // false_expr(p) = p AND (NOT p) AND (p IS NOT NULL) -> always FALSE + private E falseExpr() { + E p = factory.generateBooleanExpression(); + return factory.and(factory.and(p, factory.not(p)), factory.isNotNull(p)); + } + + /** + * Transforms {@code expr} into a semantically equivalent expression by applying a randomly chosen, applicable + * transformation rule. + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value (e.g. a WHERE predicate or an operand of a + * logical operator). Only in a boolean context may the determined-boolean rules (No. 1 and 2), which + * reduce the expression to a boolean value, be applied; in a scalar context they would change the + * expression's value/type and are therefore excluded. + * + * @return a semantically equivalent expression + */ + public E applyRandomRule(E expr, boolean booleanContext) { + int rule; + if (booleanContext) { + // Rules No. 1-6 are all value-preserving in a boolean context. + rule = Randomly.fromOptions(1, 2, 3, 4, 5, 6); + } else { + if (!factory.isCaseWhenApplicable(expr)) { + return expr; // rule No. 7: transform the expression to itself + } + // In a scalar context only the CASE WHEN rules preserve the exact value and type. + rule = Randomly.fromOptions(3, 4, 5, 6); + } + switch (rule) { + case 1: // bool_expr => false_expr OR bool_expr + return factory.or(falseExpr(), expr); + case 2: // bool_expr => true_expr AND bool_expr + return factory.and(trueExpr(), expr); + case 3: // expr => CASE WHEN false_expr THEN copy(expr) ELSE expr END + return factory.caseWhen(falseExpr(), expr, expr); + case 4: // expr => CASE WHEN true_expr THEN expr ELSE copy(expr) END + return factory.caseWhen(trueExpr(), expr, expr); + case 5: // expr => CASE WHEN rand_bool THEN copy(expr) ELSE expr END + case 6: // expr => CASE WHEN rand_bool THEN expr ELSE copy(expr) END + return factory.caseWhen(factory.generateBooleanExpression(), expr, expr); + default: + throw new AssertionError(rule); + } + } +} diff --git a/src/sqlancer/mysql/MySQLOracleFactory.java b/src/sqlancer/mysql/MySQLOracleFactory.java index 83e08677a..ed5ddc489 100644 --- a/src/sqlancer/mysql/MySQLOracleFactory.java +++ b/src/sqlancer/mysql/MySQLOracleFactory.java @@ -5,6 +5,7 @@ import sqlancer.OracleFactory; import sqlancer.common.oracle.CERTOracle; +import sqlancer.common.oracle.EETOracle; import sqlancer.common.oracle.TLPWhereOracle; import sqlancer.common.oracle.TestOracle; import sqlancer.common.query.ExpectedErrors; @@ -82,5 +83,14 @@ public TestOracle create(MySQLGlobalState globalState) throws public TestOracle create(MySQLGlobalState globalState) throws SQLException { return new MySQLDQEOracle(globalState); } + }, + EET { + @Override + public TestOracle create(MySQLGlobalState globalState) throws SQLException { + MySQLExpressionGenerator gen = new MySQLExpressionGenerator(globalState); + ExpectedErrors expectedErrors = ExpectedErrors.newErrors().with(MySQLErrors.getExpressionErrors()) + .withRegex(MySQLErrors.getExpressionRegexErrors()).build(); + return new EETOracle<>(globalState, gen, expectedErrors); + } }; } diff --git a/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java b/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java index c87048a4b..45a1fe888 100644 --- a/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java +++ b/src/sqlancer/mysql/ast/MySQLUnaryPrefixOperation.java @@ -62,6 +62,10 @@ public MySQLUnaryPrefixOperation(MySQLExpression expr, MySQLUnaryPrefixOperator super(expr, op); } + public MySQLUnaryPrefixOperator getOp() { + return op; + } + @Override public MySQLConstant getExpectedValue() { MySQLConstant subExprVal = expr.getExpectedValue(); diff --git a/src/sqlancer/mysql/gen/MySQLEETNodeFactory.java b/src/sqlancer/mysql/gen/MySQLEETNodeFactory.java new file mode 100644 index 000000000..33f3ab587 --- /dev/null +++ b/src/sqlancer/mysql/gen/MySQLEETNodeFactory.java @@ -0,0 +1,70 @@ +package sqlancer.mysql.gen; + +import java.util.List; + +import sqlancer.common.oracle.EETNodeFactory; +import sqlancer.mysql.ast.MySQLAggregate; +import sqlancer.mysql.ast.MySQLBinaryLogicalOperation; +import sqlancer.mysql.ast.MySQLBinaryLogicalOperation.MySQLBinaryLogicalOperator; +import sqlancer.mysql.ast.MySQLCaseOperator; +import sqlancer.mysql.ast.MySQLExpression; +import sqlancer.mysql.ast.MySQLTableReference; +import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; +import sqlancer.mysql.ast.MySQLUnaryPostfixOperation.UnaryPostfixOperator; +import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; +import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; + +/** + * Constructs the MySQL AST nodes needed by the {@link sqlancer.common.oracle.EETTransformation EET transformation} + * rules. + */ +public class MySQLEETNodeFactory implements EETNodeFactory { + + private final MySQLExpressionGenerator gen; + + public MySQLEETNodeFactory(MySQLExpressionGenerator gen) { + this.gen = gen; + } + + @Override + public MySQLExpression and(MySQLExpression left, MySQLExpression right) { + return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.AND); + } + + @Override + public MySQLExpression or(MySQLExpression left, MySQLExpression right) { + return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.OR); + } + + @Override + public MySQLExpression not(MySQLExpression expr) { + return new MySQLUnaryPrefixOperation(expr, MySQLUnaryPrefixOperator.NOT); + } + + @Override + public MySQLExpression isNull(MySQLExpression expr) { + return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, false); + } + + @Override + public MySQLExpression isNotNull(MySQLExpression expr) { + return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, true); + } + + @Override + public MySQLExpression caseWhen(MySQLExpression condition, MySQLExpression thenExpr, MySQLExpression elseExpr) { + return new MySQLCaseOperator(null, List.of(condition), List.of(thenExpr), elseExpr); + } + + @Override + public MySQLExpression generateBooleanExpression() { + return gen.generateBooleanExpression(); + } + + @Override + public boolean isCaseWhenApplicable(MySQLExpression expr) { + // Table references cannot be wrapped in CASE WHEN (they would cause syntax errors, see rule No. 7 of the EET + // paper); aggregates are excluded to avoid placing them in invalid contexts. + return !(expr instanceof MySQLTableReference) && !(expr instanceof MySQLAggregate); + } +} diff --git a/src/sqlancer/mysql/gen/MySQLEETTransformer.java b/src/sqlancer/mysql/gen/MySQLEETTransformer.java new file mode 100644 index 000000000..000e29bb6 --- /dev/null +++ b/src/sqlancer/mysql/gen/MySQLEETTransformer.java @@ -0,0 +1,124 @@ +package sqlancer.mysql.gen; + +import java.util.List; +import java.util.stream.Collectors; + +import sqlancer.Randomly; +import sqlancer.common.oracle.EETTransformation; +import sqlancer.mysql.ast.MySQLBetweenOperation; +import sqlancer.mysql.ast.MySQLBinaryComparisonOperation; +import sqlancer.mysql.ast.MySQLBinaryLogicalOperation; +import sqlancer.mysql.ast.MySQLBinaryOperation; +import sqlancer.mysql.ast.MySQLCaseOperator; +import sqlancer.mysql.ast.MySQLCastOperation; +import sqlancer.mysql.ast.MySQLComputableFunction; +import sqlancer.mysql.ast.MySQLExpression; +import sqlancer.mysql.ast.MySQLInOperation; +import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; +import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; +import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; + +/** + * Recursively applies the {@link EETTransformation EET} transformation rules throughout a MySQL expression's AST. At + * each node the transformer first recurses into (and rebuilds the node from) its transformed children, then, with some + * probability, wraps the resulting sub-expression with a randomly chosen transformation rule. + * + *

+ * A boolean/scalar context flag is threaded through the recursion so that the determined-boolean rules (which reduce an + * expression to a boolean value) are only ever applied where the expression is used purely for its truth value. + */ +public class MySQLEETTransformer { + + private static final boolean BOOLEAN = true; + private static final boolean SCALAR = false; + + private final EETTransformation transformation; + + public MySQLEETTransformer(MySQLExpressionGenerator gen) { + this.transformation = new EETTransformation<>(new MySQLEETNodeFactory(gen)); + } + + /** + * Transforms {@code expr} into a semantically equivalent expression. A transformation rule is always applied at the + * root, guaranteeing that the returned expression differs from the input. + */ + public MySQLExpression transform(MySQLExpression expr, boolean booleanContext) { + return transformNode(expr, booleanContext, true); + } + + private MySQLExpression transformNode(MySQLExpression expr, boolean booleanContext, boolean forceApply) { + MySQLExpression descended = descend(expr, booleanContext); + if (forceApply || Randomly.getBoolean()) { + return transformation.applyRandomRule(descended, booleanContext); + } + return descended; + } + + /** + * Rebuilds {@code expr} with its children transformed. Leaf nodes (columns, constants, ...) and node types that are + * not rebuilt here are returned unchanged; any applicable transformation is still applied to them by the calling + * {@link #transformNode}. + */ + private MySQLExpression descend(MySQLExpression expr, boolean booleanContext) { + if (expr instanceof MySQLBinaryLogicalOperation) { + // AND/OR/XOR: both operands are evaluated in a boolean context. + MySQLBinaryLogicalOperation op = (MySQLBinaryLogicalOperation) expr; + return new MySQLBinaryLogicalOperation(transformNode(op.getLeft(), BOOLEAN, false), + transformNode(op.getRight(), BOOLEAN, false), op.getOp()); + } else if (expr instanceof MySQLBinaryComparisonOperation) { + MySQLBinaryComparisonOperation op = (MySQLBinaryComparisonOperation) expr; + return new MySQLBinaryComparisonOperation(transformNode(op.getLeft(), SCALAR, false), + transformNode(op.getRight(), SCALAR, false), op.getOp()); + } else if (expr instanceof MySQLBinaryOperation) { + MySQLBinaryOperation op = (MySQLBinaryOperation) expr; + return new MySQLBinaryOperation(transformNode(op.getLeft(), SCALAR, false), + transformNode(op.getRight(), SCALAR, false), op.getOp()); + } else if (expr instanceof MySQLUnaryPrefixOperation) { + MySQLUnaryPrefixOperation op = (MySQLUnaryPrefixOperation) expr; + boolean childContext = op.getOp() == MySQLUnaryPrefixOperator.NOT ? BOOLEAN : SCALAR; + return new MySQLUnaryPrefixOperation(transformNode(op.getExpression(), childContext, false), op.getOp()); + } else if (expr instanceof MySQLUnaryPostfixOperation) { + // The operand is transformed value-preservingly (scalar), which is safe for IS NULL/IS TRUE/IS FALSE. + MySQLUnaryPostfixOperation op = (MySQLUnaryPostfixOperation) expr; + return new MySQLUnaryPostfixOperation(transformNode(op.getExpression(), SCALAR, false), op.getOperator(), + op.isNegated()); + } else if (expr instanceof MySQLCastOperation) { + MySQLCastOperation op = (MySQLCastOperation) expr; + return new MySQLCastOperation(transformNode(op.getExpr(), SCALAR, false), op.getType()); + } else if (expr instanceof MySQLBetweenOperation) { + MySQLBetweenOperation op = (MySQLBetweenOperation) expr; + return new MySQLBetweenOperation(transformNode(op.getExpr(), SCALAR, false), + transformNode(op.getLeft(), SCALAR, false), transformNode(op.getRight(), SCALAR, false)); + } else if (expr instanceof MySQLInOperation) { + MySQLInOperation op = (MySQLInOperation) expr; + List listElements = op.getListElements().stream() + .map(e -> transformNode(e, SCALAR, false)).collect(Collectors.toList()); + return new MySQLInOperation(transformNode(op.getExpr(), SCALAR, false), listElements, op.isTrue()); + } else if (expr instanceof MySQLComputableFunction) { + MySQLComputableFunction op = (MySQLComputableFunction) expr; + MySQLExpression[] args = op.getArguments(); + MySQLExpression[] newArgs = new MySQLExpression[args.length]; + for (int i = 0; i < args.length; i++) { + newArgs[i] = transformNode(args[i], SCALAR, false); + } + return new MySQLComputableFunction(op.getFunction(), newArgs); + } else if (expr instanceof MySQLCaseOperator) { + return descendCase((MySQLCaseOperator) expr); + } + return expr; + } + + private MySQLExpression descendCase(MySQLCaseOperator caseOp) { + MySQLExpression switchCondition = caseOp.getSwitchCondition(); + // Without a switch operand the WHEN conditions are boolean; with one they are compared against the operand. + boolean conditionContext = switchCondition == null ? BOOLEAN : SCALAR; + MySQLExpression newSwitch = switchCondition == null ? null : transformNode(switchCondition, SCALAR, false); + List conditions = caseOp.getConditions().stream() + .map(e -> transformNode(e, conditionContext, false)).collect(Collectors.toList()); + List expressions = caseOp.getExpressions().stream().map(e -> transformNode(e, SCALAR, false)) + .collect(Collectors.toList()); + MySQLExpression elseExpr = caseOp.getElseExpr() == null ? null + : transformNode(caseOp.getElseExpr(), SCALAR, false); + return new MySQLCaseOperator(newSwitch, conditions, expressions, elseExpr); + } +} diff --git a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java index d8ce5dd37..29b8d5ac5 100644 --- a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java @@ -9,6 +9,7 @@ import sqlancer.IgnoreMeException; import sqlancer.Randomly; import sqlancer.common.gen.CERTGenerator; +import sqlancer.common.gen.EETGenerator; import sqlancer.common.gen.TLPWhereGenerator; import sqlancer.common.gen.UntypedExpressionGenerator; import sqlancer.common.schema.AbstractTables; @@ -48,11 +49,13 @@ public class MySQLExpressionGenerator extends UntypedExpressionGenerator implements TLPWhereGenerator, - CERTGenerator { + CERTGenerator, + EETGenerator { private final MySQLGlobalState state; private MySQLRowValue rowVal; private List tables; + private MySQLEETTransformer eetTransformer; public MySQLExpressionGenerator(MySQLGlobalState state) { this.state = state; @@ -236,6 +239,14 @@ public MySQLSelect generateSelect() { return new MySQLSelect(); } + @Override + public MySQLExpression transformExpression(MySQLExpression expr, boolean booleanContext) { + if (eetTransformer == null) { + eetTransformer = new MySQLEETTransformer(this); + } + return eetTransformer.transform(expr, booleanContext); + } + @Override public List getRandomJoinClauses() { return List.of(); diff --git a/src/sqlancer/mysql/gen/MySQLTableGenerator.java b/src/sqlancer/mysql/gen/MySQLTableGenerator.java index 054a66cb6..c17ccf0d5 100644 --- a/src/sqlancer/mysql/gen/MySQLTableGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLTableGenerator.java @@ -362,7 +362,7 @@ private void appendType(MySQLDataType randomType) { } if (Randomly.getBoolean() && !globalState.getDbmsSpecificOptions().getTestOracleFactory().stream() .anyMatch(o -> o == MySQLOracleFactory.TLP_WHERE || o == MySQLOracleFactory.PQS - || o == MySQLOracleFactory.DQP)) { + || o == MySQLOracleFactory.DQP || o == MySQLOracleFactory.EET)) { sb.append(" ZEROFILL"); } } From 7c9f80f81440213b7a00997e8c0605533fc06b90 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Fri, 10 Jul 2026 15:31:17 +0800 Subject: [PATCH 02/14] Clarify that the E in the EET generics must extend the Expression class --- src/sqlancer/common/oracle/EETNodeFactory.java | 4 +++- src/sqlancer/common/oracle/EETTransformation.java | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/sqlancer/common/oracle/EETNodeFactory.java b/src/sqlancer/common/oracle/EETNodeFactory.java index 2cc933ced..c446a6054 100644 --- a/src/sqlancer/common/oracle/EETNodeFactory.java +++ b/src/sqlancer/common/oracle/EETNodeFactory.java @@ -1,5 +1,7 @@ package sqlancer.common.oracle; +import sqlancer.common.ast.newast.Expression; + /** * Factory for constructing the AST nodes needed by the {@link EETTransformation equivalent expression transformation} * rules. Because every DBMS has its own expression AST, the actual node construction is DBMS-specific; this interface @@ -8,7 +10,7 @@ * @param * the DBMS-specific expression type */ -public interface EETNodeFactory { +public interface EETNodeFactory> { /** Builds {@code left AND right}. */ E and(E left, E right); diff --git a/src/sqlancer/common/oracle/EETTransformation.java b/src/sqlancer/common/oracle/EETTransformation.java index ab9b7fefe..ecf2eea74 100644 --- a/src/sqlancer/common/oracle/EETTransformation.java +++ b/src/sqlancer/common/oracle/EETTransformation.java @@ -1,5 +1,6 @@ package sqlancer.common.oracle; +import sqlancer.common.ast.newast.Expression; import sqlancer.Randomly; /** @@ -18,7 +19,7 @@ * @param * the DBMS-specific expression type */ -public class EETTransformation { +public class EETTransformation> { private final EETNodeFactory factory; @@ -39,7 +40,7 @@ private E falseExpr() { } /** - * Transforms {@code expr} into a semantically equivalent expression by applying a randomly chosen, applicable + * Transforms {@code expr} into a semantically equivalent expression by applying a randomly chosen applicable * transformation rule. * * @param expr From 95dcc8ef94c5793595ad1a2e5481fbd7411835c2 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Sat, 11 Jul 2026 20:56:54 +0800 Subject: [PATCH 03/14] Refactor EET oracle structure for consistency and extensibility Merge `EETTransformation` into a new abstract base class `EETTransformer` (template method pattern: rules + tree-walking orchestration in the base, `descend` abstract for DBMS-specific AST reconstruction). Move `MySQLEETNodeFactory` and `MySQLEETTransformer` from `mysql/gen` to `mysql/oracle` to mirror the placement of their common counterparts. `MySQLEETTransformer` now extends `EETTransformer`. --- .../common/oracle/EETNodeFactory.java | 6 +- .../common/oracle/EETTransformation.java | 84 ------------- .../common/oracle/EETTransformer.java | 112 ++++++++++++++++++ .../mysql/gen/MySQLExpressionGenerator.java | 1 + .../{gen => oracle}/MySQLEETNodeFactory.java | 6 +- .../{gen => oracle}/MySQLEETTransformer.java | 45 ++----- 6 files changed, 128 insertions(+), 126 deletions(-) delete mode 100644 src/sqlancer/common/oracle/EETTransformation.java create mode 100644 src/sqlancer/common/oracle/EETTransformer.java rename src/sqlancer/mysql/{gen => oracle}/MySQLEETNodeFactory.java (95%) rename src/sqlancer/mysql/{gen => oracle}/MySQLEETTransformer.java (72%) diff --git a/src/sqlancer/common/oracle/EETNodeFactory.java b/src/sqlancer/common/oracle/EETNodeFactory.java index c446a6054..9f97ea937 100644 --- a/src/sqlancer/common/oracle/EETNodeFactory.java +++ b/src/sqlancer/common/oracle/EETNodeFactory.java @@ -3,9 +3,9 @@ import sqlancer.common.ast.newast.Expression; /** - * Factory for constructing the AST nodes needed by the {@link EETTransformation equivalent expression transformation} - * rules. Because every DBMS has its own expression AST, the actual node construction is DBMS-specific; this interface - * lets the (DBMS-independent) transformation rules be expressed once in terms of a small set of primitive operations. + * Factory for constructing the AST nodes needed by the {@link EETTransformer EET transformer's} transformation rules. + * Because every DBMS has its own expression AST, the actual node construction is DBMS-specific; this interface lets the + * (DBMS-independent) transformation rules be expressed once in terms of a small set of primitive operations. * * @param * the DBMS-specific expression type diff --git a/src/sqlancer/common/oracle/EETTransformation.java b/src/sqlancer/common/oracle/EETTransformation.java deleted file mode 100644 index ecf2eea74..000000000 --- a/src/sqlancer/common/oracle/EETTransformation.java +++ /dev/null @@ -1,84 +0,0 @@ -package sqlancer.common.oracle; - -import sqlancer.common.ast.newast.Expression; -import sqlancer.Randomly; - -/** - * Implements the semantic-preserving expression transformation rules of EET (Equivalent Expression Transformation, Jiang - * & Su, OSDI'24), Table 2. Given an expression, {@link #applyRandomRule} returns a semantically equivalent - * expression built from the primitives provided by an {@link EETNodeFactory}. The rules are DBMS-independent; only the - * node construction (via the factory) is DBMS-specific. - * - *

- * The rules rely on two always-determined boolean expressions built from an arbitrary boolean {@code p}: - *

    - *
  • {@code true_expr(p) = p OR (NOT p) OR (p IS NULL)}, which always evaluates to TRUE, and
  • - *
  • {@code false_expr(p) = p AND (NOT p) AND (p IS NOT NULL)}, which always evaluates to FALSE.
  • - *
- * - * @param - * the DBMS-specific expression type - */ -public class EETTransformation> { - - private final EETNodeFactory factory; - - public EETTransformation(EETNodeFactory factory) { - this.factory = factory; - } - - // true_expr(p) = p OR (NOT p) OR (p IS NULL) -> always TRUE - private E trueExpr() { - E p = factory.generateBooleanExpression(); - return factory.or(factory.or(p, factory.not(p)), factory.isNull(p)); - } - - // false_expr(p) = p AND (NOT p) AND (p IS NOT NULL) -> always FALSE - private E falseExpr() { - E p = factory.generateBooleanExpression(); - return factory.and(factory.and(p, factory.not(p)), factory.isNotNull(p)); - } - - /** - * Transforms {@code expr} into a semantically equivalent expression by applying a randomly chosen applicable - * transformation rule. - * - * @param expr - * the expression to transform - * @param booleanContext - * whether {@code expr} is evaluated purely for its truth value (e.g. a WHERE predicate or an operand of a - * logical operator). Only in a boolean context may the determined-boolean rules (No. 1 and 2), which - * reduce the expression to a boolean value, be applied; in a scalar context they would change the - * expression's value/type and are therefore excluded. - * - * @return a semantically equivalent expression - */ - public E applyRandomRule(E expr, boolean booleanContext) { - int rule; - if (booleanContext) { - // Rules No. 1-6 are all value-preserving in a boolean context. - rule = Randomly.fromOptions(1, 2, 3, 4, 5, 6); - } else { - if (!factory.isCaseWhenApplicable(expr)) { - return expr; // rule No. 7: transform the expression to itself - } - // In a scalar context only the CASE WHEN rules preserve the exact value and type. - rule = Randomly.fromOptions(3, 4, 5, 6); - } - switch (rule) { - case 1: // bool_expr => false_expr OR bool_expr - return factory.or(falseExpr(), expr); - case 2: // bool_expr => true_expr AND bool_expr - return factory.and(trueExpr(), expr); - case 3: // expr => CASE WHEN false_expr THEN copy(expr) ELSE expr END - return factory.caseWhen(falseExpr(), expr, expr); - case 4: // expr => CASE WHEN true_expr THEN expr ELSE copy(expr) END - return factory.caseWhen(trueExpr(), expr, expr); - case 5: // expr => CASE WHEN rand_bool THEN copy(expr) ELSE expr END - case 6: // expr => CASE WHEN rand_bool THEN expr ELSE copy(expr) END - return factory.caseWhen(factory.generateBooleanExpression(), expr, expr); - default: - throw new AssertionError(rule); - } - } -} diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java new file mode 100644 index 000000000..d66935e18 --- /dev/null +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -0,0 +1,112 @@ +package sqlancer.common.oracle; + +import sqlancer.Randomly; +import sqlancer.common.ast.newast.Expression; + +/** + * Abstract base class for EET (Equivalent Expression Transformation) tree-walkers, based on "Detecting Logic Bugs in + * Database Engines via Equivalent Expression Transformation" (Jiang & Su, OSDI'24). + * + *

+ * This class implements the seven transformation rules (Table 2 of the paper) and provides a template-method framework + * for applying them throughout an expression's AST. Subclasses implement {@link #descend} to rebuild DBMS-specific AST + * nodes from their transformed children; everything else (the rule logic, context threading, and tree-walking + * orchestration) is provided here. + * + * @param + * the DBMS-specific expression type + */ +public abstract class EETTransformer> { + + private final EETNodeFactory factory; + + protected EETTransformer(EETNodeFactory factory) { + this.factory = factory; + } + + // true_expr(p) = p OR (NOT p) OR (p IS NULL) -> always TRUE + private E trueExpr() { + E p = factory.generateBooleanExpression(); + return factory.or(factory.or(p, factory.not(p)), factory.isNull(p)); + } + + // false_expr(p) = p AND (NOT p) AND (p IS NOT NULL) -> always FALSE + private E falseExpr() { + E p = factory.generateBooleanExpression(); + return factory.and(factory.and(p, factory.not(p)), factory.isNotNull(p)); + } + + /** + * Applies a randomly chosen applicable transformation rule to {@code expr}, returning a semantically equivalent + * expression. + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value; only in a boolean context may the + * determined-boolean rules (No. 1 and 2), which reduce the expression to a boolean value, be applied + * + * @return a semantically equivalent expression + */ + protected E applyRandomRule(E expr, boolean booleanContext) { + int rule; + if (booleanContext) { + // Rules No. 1-6 are all value-preserving in a boolean context. + rule = Randomly.fromOptions(1, 2, 3, 4, 5, 6); + } else { + if (!factory.isCaseWhenApplicable(expr)) { + return expr; // rule No. 7: transform the expression to itself + } + // In a scalar context only the CASE WHEN rules preserve the exact value and type. + rule = Randomly.fromOptions(3, 4, 5, 6); + } + switch (rule) { + case 1: // bool_expr => false_expr OR bool_expr + return factory.or(falseExpr(), expr); + case 2: // bool_expr => true_expr AND bool_expr + return factory.and(trueExpr(), expr); + case 3: // expr => CASE WHEN false_expr THEN copy(expr) ELSE expr END + return factory.caseWhen(falseExpr(), expr, expr); + case 4: // expr => CASE WHEN true_expr THEN expr ELSE copy(expr) END + return factory.caseWhen(trueExpr(), expr, expr); + case 5: // expr => CASE WHEN rand_bool THEN copy(expr) ELSE expr END + case 6: // expr => CASE WHEN rand_bool THEN expr ELSE copy(expr) END + return factory.caseWhen(factory.generateBooleanExpression(), expr, expr); + default: + throw new AssertionError(rule); + } + } + + /** + * Transforms {@code expr} into a semantically equivalent expression. A transformation rule is always applied at the + * root, guaranteeing (unless only rule 7 is applicable) that the returned expression differs from the input. + */ + public E transform(E expr, boolean booleanContext) { + return transformNode(expr, booleanContext, true); + } + + /** + * Descends into {@code expr}, rebuilds it from transformed children, then optionally applies a rule at this node. + */ + protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { + E descended = descend(expr, booleanContext); + if (forceApply || Randomly.getBoolean()) { + return applyRandomRule(descended, booleanContext); + } + return descended; + } + + /** + * Rebuilds {@code expr} with its children transformed, threading the correct boolean/scalar context into each child. + * Leaf nodes (columns, constants, table references, ...) should be returned unchanged; any applicable transformation + * will still be applied to them by the calling {@link #transformNode}. + * + * @param expr + * the expression to descend into + * @param booleanContext + * the context in which {@code expr} itself is evaluated (used to determine child contexts) + * + * @return a rebuilt copy of {@code expr} with transformed children, or {@code expr} itself if it is a leaf + */ + protected abstract E descend(E expr, boolean booleanContext); +} diff --git a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java index 29b8d5ac5..904013a37 100644 --- a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java @@ -46,6 +46,7 @@ import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; +import sqlancer.mysql.oracle.MySQLEETTransformer; public class MySQLExpressionGenerator extends UntypedExpressionGenerator implements TLPWhereGenerator, diff --git a/src/sqlancer/mysql/gen/MySQLEETNodeFactory.java b/src/sqlancer/mysql/oracle/MySQLEETNodeFactory.java similarity index 95% rename from src/sqlancer/mysql/gen/MySQLEETNodeFactory.java rename to src/sqlancer/mysql/oracle/MySQLEETNodeFactory.java index 33f3ab587..c73b576c5 100644 --- a/src/sqlancer/mysql/gen/MySQLEETNodeFactory.java +++ b/src/sqlancer/mysql/oracle/MySQLEETNodeFactory.java @@ -1,4 +1,4 @@ -package sqlancer.mysql.gen; +package sqlancer.mysql.oracle; import java.util.List; @@ -13,10 +13,10 @@ import sqlancer.mysql.ast.MySQLUnaryPostfixOperation.UnaryPostfixOperator; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; +import sqlancer.mysql.gen.MySQLExpressionGenerator; /** - * Constructs the MySQL AST nodes needed by the {@link sqlancer.common.oracle.EETTransformation EET transformation} - * rules. + * Constructs the MySQL AST nodes needed by the {@link sqlancer.common.oracle.EETTransformer EET transformer}. */ public class MySQLEETNodeFactory implements EETNodeFactory { diff --git a/src/sqlancer/mysql/gen/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java similarity index 72% rename from src/sqlancer/mysql/gen/MySQLEETTransformer.java rename to src/sqlancer/mysql/oracle/MySQLEETTransformer.java index 000e29bb6..71af94ed8 100644 --- a/src/sqlancer/mysql/gen/MySQLEETTransformer.java +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -1,10 +1,9 @@ -package sqlancer.mysql.gen; +package sqlancer.mysql.oracle; import java.util.List; import java.util.stream.Collectors; -import sqlancer.Randomly; -import sqlancer.common.oracle.EETTransformation; +import sqlancer.common.oracle.EETTransformer; import sqlancer.mysql.ast.MySQLBetweenOperation; import sqlancer.mysql.ast.MySQLBinaryComparisonOperation; import sqlancer.mysql.ast.MySQLBinaryLogicalOperation; @@ -17,49 +16,23 @@ import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; +import sqlancer.mysql.gen.MySQLExpressionGenerator; /** - * Recursively applies the {@link EETTransformation EET} transformation rules throughout a MySQL expression's AST. At - * each node the transformer first recurses into (and rebuilds the node from) its transformed children, then, with some - * probability, wraps the resulting sub-expression with a randomly chosen transformation rule. - * - *

- * A boolean/scalar context flag is threaded through the recursion so that the determined-boolean rules (which reduce an - * expression to a boolean value) are only ever applied where the expression is used purely for its truth value. + * MySQL implementation of the {@link EETTransformer EET} tree-walker. Implements {@link #descend} to rebuild MySQL AST + * nodes from their transformed children, threading the correct boolean/scalar context into each child. */ -public class MySQLEETTransformer { +public class MySQLEETTransformer extends EETTransformer { private static final boolean BOOLEAN = true; private static final boolean SCALAR = false; - private final EETTransformation transformation; - public MySQLEETTransformer(MySQLExpressionGenerator gen) { - this.transformation = new EETTransformation<>(new MySQLEETNodeFactory(gen)); - } - - /** - * Transforms {@code expr} into a semantically equivalent expression. A transformation rule is always applied at the - * root, guaranteeing that the returned expression differs from the input. - */ - public MySQLExpression transform(MySQLExpression expr, boolean booleanContext) { - return transformNode(expr, booleanContext, true); - } - - private MySQLExpression transformNode(MySQLExpression expr, boolean booleanContext, boolean forceApply) { - MySQLExpression descended = descend(expr, booleanContext); - if (forceApply || Randomly.getBoolean()) { - return transformation.applyRandomRule(descended, booleanContext); - } - return descended; + super(new MySQLEETNodeFactory(gen)); } - /** - * Rebuilds {@code expr} with its children transformed. Leaf nodes (columns, constants, ...) and node types that are - * not rebuilt here are returned unchanged; any applicable transformation is still applied to them by the calling - * {@link #transformNode}. - */ - private MySQLExpression descend(MySQLExpression expr, boolean booleanContext) { + @Override + protected MySQLExpression descend(MySQLExpression expr, boolean booleanContext) { if (expr instanceof MySQLBinaryLogicalOperation) { // AND/OR/XOR: both operands are evaluated in a boolean context. MySQLBinaryLogicalOperation op = (MySQLBinaryLogicalOperation) expr; From 657ec00a07e21a523924b7433283f780bbb07aee Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Sun, 12 Jul 2026 09:49:12 +0800 Subject: [PATCH 04/14] Move EET transformer ownership to EETOracle, removing EET-specific state from DBMS-specific expression generators --- src/sqlancer/common/gen/EETGenerator.java | 20 +++++++------------ src/sqlancer/common/oracle/EETOracle.java | 6 ++++-- .../mysql/gen/MySQLExpressionGenerator.java | 9 +++------ 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/sqlancer/common/gen/EETGenerator.java b/src/sqlancer/common/gen/EETGenerator.java index f468ff6d2..60b91e78a 100644 --- a/src/sqlancer/common/gen/EETGenerator.java +++ b/src/sqlancer/common/gen/EETGenerator.java @@ -5,14 +5,15 @@ import sqlancer.common.ast.newast.Expression; import sqlancer.common.ast.newast.Join; import sqlancer.common.ast.newast.Select; +import sqlancer.common.oracle.EETTransformer; import sqlancer.common.schema.AbstractTable; import sqlancer.common.schema.AbstractTableColumn; import sqlancer.common.schema.AbstractTables; /** * Generator interface used by {@link sqlancer.common.oracle.EETOracle}. In addition to generating a random query (like - * the other oracle generators), an EET generator can transform an expression into a semantically equivalent one - * according to the EET transformation rules. + * the other oracle generators), an EET generator creates a DBMS-specific {@link EETTransformer} that the oracle uses to + * rewrite expressions into semantically equivalent ones. */ public interface EETGenerator, J extends Join, E extends Expression, T extends AbstractTable, C extends AbstractTableColumn> { @@ -29,16 +30,9 @@ public interface EETGenerator, J extends Join createTransformer(); } diff --git a/src/sqlancer/common/oracle/EETOracle.java b/src/sqlancer/common/oracle/EETOracle.java index 0f192f9ab..0ae229414 100644 --- a/src/sqlancer/common/oracle/EETOracle.java +++ b/src/sqlancer/common/oracle/EETOracle.java @@ -32,6 +32,7 @@ public class EETOracle, J extends Join, E private final G state; private EETGenerator gen; + private final EETTransformer transformer; private final ExpectedErrors errors; private Reproducer reproducer; @@ -69,6 +70,7 @@ public EETOracle(G state, EETGenerator gen, ExpectedErrors expect } this.state = state; this.gen = gen; + this.transformer = gen.createTransformer(); this.errors = expectedErrors; } @@ -94,10 +96,10 @@ public void check() throws SQLException { // Transform the query's expressions into semantically equivalent ones. Fetch columns are scalar expressions, // while the WHERE clause is evaluated in a boolean context. - List transformedFetchColumns = fetchColumns.stream().map(c -> gen.transformExpression(c, false)) + List transformedFetchColumns = fetchColumns.stream().map(c -> transformer.transform(c, false)) .collect(Collectors.toList()); select.setFetchColumns(transformedFetchColumns); - select.setWhereClause(gen.transformExpression(whereClause, true)); + select.setWhereClause(transformer.transform(whereClause, true)); String transformedQueryString = select.asString(); List transformedResultSet = ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, diff --git a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java index 904013a37..f2d577aee 100644 --- a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java @@ -46,6 +46,7 @@ import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; +import sqlancer.common.oracle.EETTransformer; import sqlancer.mysql.oracle.MySQLEETTransformer; public class MySQLExpressionGenerator extends UntypedExpressionGenerator @@ -56,7 +57,6 @@ public class MySQLExpressionGenerator extends UntypedExpressionGenerator tables; - private MySQLEETTransformer eetTransformer; public MySQLExpressionGenerator(MySQLGlobalState state) { this.state = state; @@ -241,11 +241,8 @@ public MySQLSelect generateSelect() { } @Override - public MySQLExpression transformExpression(MySQLExpression expr, boolean booleanContext) { - if (eetTransformer == null) { - eetTransformer = new MySQLEETTransformer(this); - } - return eetTransformer.transform(expr, booleanContext); + public EETTransformer createTransformer() { + return new MySQLEETTransformer(this); } @Override From 65957adf9b8fb4b2702c20475c9c5d2e5c972560 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Sun, 12 Jul 2026 10:03:19 +0800 Subject: [PATCH 05/14] Rearrange MySQLExpressionGenerator code to group oracle-specific sections together --- .../mysql/gen/MySQLExpressionGenerator.java | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java index f2d577aee..5d1ec06f4 100644 --- a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java @@ -222,6 +222,22 @@ public List generateOrderBys() { return newOrderBys; } + public MySQLAggregate generateAggregate() { + MySQLAggregateFunction func = Randomly.fromOptions(MySQLAggregateFunction.values()); + + if (func.isVariadic()) { + int nrExprs = Randomly.smallNumber() + 1; + List exprs = IntStream.range(0, nrExprs).mapToObj(index -> generateExpression()) + .collect(Collectors.toList()); + + return new MySQLAggregate(exprs, func); + } else { + return new MySQLAggregate(List.of(generateExpression()), func); + } + } + + // --- Shared oracle infrastructure (TLPWhere / CERT / EET) --- + @Override public MySQLExpressionGenerator setTablesAndColumns(AbstractTables tables) { this.columns = tables.getColumns(); @@ -240,11 +256,6 @@ public MySQLSelect generateSelect() { return new MySQLSelect(); } - @Override - public EETTransformer createTransformer() { - return new MySQLEETTransformer(this); - } - @Override public List getRandomJoinClauses() { return List.of(); @@ -260,6 +271,8 @@ public List generateFetchColumns(boolean shouldCreateDummy) { return columns.stream().map(c -> new MySQLColumnReference(c, null)).collect(Collectors.toList()); } + // --- CERT oracle --- + @Override public String generateExplainQuery(MySQLSelect select) { return "EXPLAIN FORMAT=TRADITIONAL " + select.asString(); // as of MySQL 9.5.0, default EXPLAIN format changed @@ -267,20 +280,6 @@ public String generateExplainQuery(MySQLSelect select) { // now be specified } - public MySQLAggregate generateAggregate() { - MySQLAggregateFunction func = Randomly.fromOptions(MySQLAggregateFunction.values()); - - if (func.isVariadic()) { - int nrExprs = Randomly.smallNumber() + 1; - List exprs = IntStream.range(0, nrExprs).mapToObj(index -> generateExpression()) - .collect(Collectors.toList()); - - return new MySQLAggregate(exprs, func); - } else { - return new MySQLAggregate(List.of(generateExpression()), func); - } - } - @Override public boolean mutate(MySQLSelect select) { List> mutators = new ArrayList<>(); @@ -364,4 +363,11 @@ boolean mutateOr(MySQLSelect select) { return true; } } + + // --- EET oracle --- + + @Override + public EETTransformer createTransformer() { + return new MySQLEETTransformer(this); + } } From 03c487476cae5d040de76abff895c1fe5c79ceae Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Sun, 12 Jul 2026 10:24:34 +0800 Subject: [PATCH 06/14] Remove need for EETNodeFactory by merging it into EETTransformer --- .../common/oracle/EETNodeFactory.java | 41 ----------- .../common/oracle/EETTransformer.java | 57 ++++++++++----- .../mysql/oracle/MySQLEETNodeFactory.java | 70 ------------------- .../mysql/oracle/MySQLEETTransformer.java | 50 ++++++++++++- 4 files changed, 88 insertions(+), 130 deletions(-) delete mode 100644 src/sqlancer/common/oracle/EETNodeFactory.java delete mode 100644 src/sqlancer/mysql/oracle/MySQLEETNodeFactory.java diff --git a/src/sqlancer/common/oracle/EETNodeFactory.java b/src/sqlancer/common/oracle/EETNodeFactory.java deleted file mode 100644 index 9f97ea937..000000000 --- a/src/sqlancer/common/oracle/EETNodeFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package sqlancer.common.oracle; - -import sqlancer.common.ast.newast.Expression; - -/** - * Factory for constructing the AST nodes needed by the {@link EETTransformer EET transformer's} transformation rules. - * Because every DBMS has its own expression AST, the actual node construction is DBMS-specific; this interface lets the - * (DBMS-independent) transformation rules be expressed once in terms of a small set of primitive operations. - * - * @param - * the DBMS-specific expression type - */ -public interface EETNodeFactory> { - - /** Builds {@code left AND right}. */ - E and(E left, E right); - - /** Builds {@code left OR right}. */ - E or(E left, E right); - - /** Builds {@code NOT expr}. */ - E not(E expr); - - /** Builds {@code expr IS NULL}. */ - E isNull(E expr); - - /** Builds {@code expr IS NOT NULL}. */ - E isNotNull(E expr); - - /** Builds {@code CASE WHEN condition THEN thenExpr ELSE elseExpr END}. */ - E caseWhen(E condition, E thenExpr, E elseExpr); - - /** Generates a fresh random boolean expression, reusing the variables available to the query generator. */ - E generateBooleanExpression(); - - /** - * Whether {@code expr} may be wrapped in a CASE WHEN expression. Some expressions (e.g. table references) are not - * CASE-WHEN applicable and must be transformed to themselves (rule No. 7 of the EET paper). - */ - boolean isCaseWhenApplicable(E expr); -} diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java index d66935e18..010cd0a56 100644 --- a/src/sqlancer/common/oracle/EETTransformer.java +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -10,30 +10,24 @@ *

* This class implements the seven transformation rules (Table 2 of the paper) and provides a template-method framework * for applying them throughout an expression's AST. Subclasses implement {@link #descend} to rebuild DBMS-specific AST - * nodes from their transformed children; everything else (the rule logic, context threading, and tree-walking - * orchestration) is provided here. + * nodes from their transformed children, and the abstract factory methods to construct new nodes; everything else (the + * rule logic, context threading, and tree-walking orchestration) is provided here. * * @param * the DBMS-specific expression type */ public abstract class EETTransformer> { - private final EETNodeFactory factory; - - protected EETTransformer(EETNodeFactory factory) { - this.factory = factory; - } - // true_expr(p) = p OR (NOT p) OR (p IS NULL) -> always TRUE private E trueExpr() { - E p = factory.generateBooleanExpression(); - return factory.or(factory.or(p, factory.not(p)), factory.isNull(p)); + E p = generateBooleanExpression(); + return or(or(p, not(p)), isNull(p)); } // false_expr(p) = p AND (NOT p) AND (p IS NOT NULL) -> always FALSE private E falseExpr() { - E p = factory.generateBooleanExpression(); - return factory.and(factory.and(p, factory.not(p)), factory.isNotNull(p)); + E p = generateBooleanExpression(); + return and(and(p, not(p)), isNotNull(p)); } /** @@ -54,7 +48,7 @@ protected E applyRandomRule(E expr, boolean booleanContext) { // Rules No. 1-6 are all value-preserving in a boolean context. rule = Randomly.fromOptions(1, 2, 3, 4, 5, 6); } else { - if (!factory.isCaseWhenApplicable(expr)) { + if (!isCaseWhenApplicable(expr)) { return expr; // rule No. 7: transform the expression to itself } // In a scalar context only the CASE WHEN rules preserve the exact value and type. @@ -62,16 +56,16 @@ protected E applyRandomRule(E expr, boolean booleanContext) { } switch (rule) { case 1: // bool_expr => false_expr OR bool_expr - return factory.or(falseExpr(), expr); + return or(falseExpr(), expr); case 2: // bool_expr => true_expr AND bool_expr - return factory.and(trueExpr(), expr); + return and(trueExpr(), expr); case 3: // expr => CASE WHEN false_expr THEN copy(expr) ELSE expr END - return factory.caseWhen(falseExpr(), expr, expr); + return caseWhen(falseExpr(), expr, expr); case 4: // expr => CASE WHEN true_expr THEN expr ELSE copy(expr) END - return factory.caseWhen(trueExpr(), expr, expr); + return caseWhen(trueExpr(), expr, expr); case 5: // expr => CASE WHEN rand_bool THEN copy(expr) ELSE expr END case 6: // expr => CASE WHEN rand_bool THEN expr ELSE copy(expr) END - return factory.caseWhen(factory.generateBooleanExpression(), expr, expr); + return caseWhen(generateBooleanExpression(), expr, expr); default: throw new AssertionError(rule); } @@ -109,4 +103,31 @@ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { * @return a rebuilt copy of {@code expr} with transformed children, or {@code expr} itself if it is a leaf */ protected abstract E descend(E expr, boolean booleanContext); + + /** Builds {@code left AND right}. */ + protected abstract E and(E left, E right); + + /** Builds {@code left OR right}. */ + protected abstract E or(E left, E right); + + /** Builds {@code NOT expr}. */ + protected abstract E not(E expr); + + /** Builds {@code expr IS NULL}. */ + protected abstract E isNull(E expr); + + /** Builds {@code expr IS NOT NULL}. */ + protected abstract E isNotNull(E expr); + + /** Builds {@code CASE WHEN condition THEN thenExpr ELSE elseExpr END}. */ + protected abstract E caseWhen(E condition, E thenExpr, E elseExpr); + + /** Generates a fresh random boolean expression, reusing the variables available to the query generator. */ + protected abstract E generateBooleanExpression(); + + /** + * Whether {@code expr} may be wrapped in a CASE WHEN expression. Some expressions (e.g. table references) are not + * CASE-WHEN applicable and must be transformed to themselves (rule No. 7 of the EET paper). + */ + protected abstract boolean isCaseWhenApplicable(E expr); } diff --git a/src/sqlancer/mysql/oracle/MySQLEETNodeFactory.java b/src/sqlancer/mysql/oracle/MySQLEETNodeFactory.java deleted file mode 100644 index c73b576c5..000000000 --- a/src/sqlancer/mysql/oracle/MySQLEETNodeFactory.java +++ /dev/null @@ -1,70 +0,0 @@ -package sqlancer.mysql.oracle; - -import java.util.List; - -import sqlancer.common.oracle.EETNodeFactory; -import sqlancer.mysql.ast.MySQLAggregate; -import sqlancer.mysql.ast.MySQLBinaryLogicalOperation; -import sqlancer.mysql.ast.MySQLBinaryLogicalOperation.MySQLBinaryLogicalOperator; -import sqlancer.mysql.ast.MySQLCaseOperator; -import sqlancer.mysql.ast.MySQLExpression; -import sqlancer.mysql.ast.MySQLTableReference; -import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; -import sqlancer.mysql.ast.MySQLUnaryPostfixOperation.UnaryPostfixOperator; -import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; -import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; -import sqlancer.mysql.gen.MySQLExpressionGenerator; - -/** - * Constructs the MySQL AST nodes needed by the {@link sqlancer.common.oracle.EETTransformer EET transformer}. - */ -public class MySQLEETNodeFactory implements EETNodeFactory { - - private final MySQLExpressionGenerator gen; - - public MySQLEETNodeFactory(MySQLExpressionGenerator gen) { - this.gen = gen; - } - - @Override - public MySQLExpression and(MySQLExpression left, MySQLExpression right) { - return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.AND); - } - - @Override - public MySQLExpression or(MySQLExpression left, MySQLExpression right) { - return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.OR); - } - - @Override - public MySQLExpression not(MySQLExpression expr) { - return new MySQLUnaryPrefixOperation(expr, MySQLUnaryPrefixOperator.NOT); - } - - @Override - public MySQLExpression isNull(MySQLExpression expr) { - return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, false); - } - - @Override - public MySQLExpression isNotNull(MySQLExpression expr) { - return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, true); - } - - @Override - public MySQLExpression caseWhen(MySQLExpression condition, MySQLExpression thenExpr, MySQLExpression elseExpr) { - return new MySQLCaseOperator(null, List.of(condition), List.of(thenExpr), elseExpr); - } - - @Override - public MySQLExpression generateBooleanExpression() { - return gen.generateBooleanExpression(); - } - - @Override - public boolean isCaseWhenApplicable(MySQLExpression expr) { - // Table references cannot be wrapped in CASE WHEN (they would cause syntax errors, see rule No. 7 of the EET - // paper); aggregates are excluded to avoid placing them in invalid contexts. - return !(expr instanceof MySQLTableReference) && !(expr instanceof MySQLAggregate); - } -} diff --git a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java index 71af94ed8..9b0cac226 100644 --- a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -4,16 +4,20 @@ import java.util.stream.Collectors; import sqlancer.common.oracle.EETTransformer; +import sqlancer.mysql.ast.MySQLAggregate; import sqlancer.mysql.ast.MySQLBetweenOperation; import sqlancer.mysql.ast.MySQLBinaryComparisonOperation; import sqlancer.mysql.ast.MySQLBinaryLogicalOperation; +import sqlancer.mysql.ast.MySQLBinaryLogicalOperation.MySQLBinaryLogicalOperator; import sqlancer.mysql.ast.MySQLBinaryOperation; import sqlancer.mysql.ast.MySQLCaseOperator; import sqlancer.mysql.ast.MySQLCastOperation; import sqlancer.mysql.ast.MySQLComputableFunction; import sqlancer.mysql.ast.MySQLExpression; import sqlancer.mysql.ast.MySQLInOperation; +import sqlancer.mysql.ast.MySQLTableReference; import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; +import sqlancer.mysql.ast.MySQLUnaryPostfixOperation.UnaryPostfixOperator; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; import sqlancer.mysql.gen.MySQLExpressionGenerator; @@ -27,8 +31,10 @@ public class MySQLEETTransformer extends EETTransformer { private static final boolean BOOLEAN = true; private static final boolean SCALAR = false; + private final MySQLExpressionGenerator gen; + public MySQLEETTransformer(MySQLExpressionGenerator gen) { - super(new MySQLEETNodeFactory(gen)); + this.gen = gen; } @Override @@ -94,4 +100,46 @@ private MySQLExpression descendCase(MySQLCaseOperator caseOp) { : transformNode(caseOp.getElseExpr(), SCALAR, false); return new MySQLCaseOperator(newSwitch, conditions, expressions, elseExpr); } + + @Override + protected MySQLExpression and(MySQLExpression left, MySQLExpression right) { + return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.AND); + } + + @Override + protected MySQLExpression or(MySQLExpression left, MySQLExpression right) { + return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.OR); + } + + @Override + protected MySQLExpression not(MySQLExpression expr) { + return new MySQLUnaryPrefixOperation(expr, MySQLUnaryPrefixOperator.NOT); + } + + @Override + protected MySQLExpression isNull(MySQLExpression expr) { + return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, false); + } + + @Override + protected MySQLExpression isNotNull(MySQLExpression expr) { + return new MySQLUnaryPostfixOperation(expr, UnaryPostfixOperator.IS_NULL, true); + } + + @Override + protected MySQLExpression caseWhen(MySQLExpression condition, MySQLExpression thenExpr, MySQLExpression elseExpr) { + return new MySQLCaseOperator(null, List.of(condition), List.of(thenExpr), elseExpr); + } + + @Override + protected MySQLExpression generateBooleanExpression() { + return gen.generateBooleanExpression(); + } + + @Override + protected boolean isCaseWhenApplicable(MySQLExpression expr) { + // Table references cannot be wrapped in CASE WHEN (they would cause syntax errors, see rule No. 7 of the EET + // paper); aggregates are excluded to avoid placing them in invalid contexts. + return !(expr instanceof MySQLTableReference) && !(expr instanceof MySQLAggregate); + } } From 9285e9168dd3a3d06cc080b6cc75d46aea216529 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Sun, 12 Jul 2026 18:17:44 +0800 Subject: [PATCH 07/14] Correct EET transformation rule comments --- src/sqlancer/common/oracle/EETTransformer.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java index 010cd0a56..12999402c 100644 --- a/src/sqlancer/common/oracle/EETTransformer.java +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -55,17 +55,18 @@ protected E applyRandomRule(E expr, boolean booleanContext) { rule = Randomly.fromOptions(3, 4, 5, 6); } switch (rule) { - case 1: // bool_expr => false_expr OR bool_expr + case 1: // expr => false_expr OR expr return or(falseExpr(), expr); - case 2: // bool_expr => true_expr AND bool_expr + case 2: // expr => true_expr AND expr return and(trueExpr(), expr); - case 3: // expr => CASE WHEN false_expr THEN copy(expr) ELSE expr END + case 3: // expr => CASE WHEN false_expr THEN rand_expr(type(expr)) ELSE expr END return caseWhen(falseExpr(), expr, expr); - case 4: // expr => CASE WHEN true_expr THEN expr ELSE copy(expr) END + case 4: // expr => CASE WHEN true_expr THEN expr ELSE rand_expr(type(expr)) END return caseWhen(trueExpr(), expr, expr); - case 5: // expr => CASE WHEN rand_bool THEN copy(expr) ELSE expr END - case 6: // expr => CASE WHEN rand_bool THEN expr ELSE copy(expr) END - return caseWhen(generateBooleanExpression(), expr, expr); + case 5: // expr => CASE WHEN rand_expr(boolean) THEN copy(expr) ELSE expr END + case 6: // expr => CASE WHEN rand_expr(boolean) THEN expr ELSE copy(expr) END + return caseWhen(generateBooleanExpression(), expr, expr); + // deep copy of expr is not needed, as the AST nodes are immutable anyway default: throw new AssertionError(rule); } From 52ce86661581681b1eb754ebcdc89103c833fa76 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Mon, 13 Jul 2026 12:40:14 +0800 Subject: [PATCH 08/14] Implement rules 3 and 4 for EET --- src/sqlancer/common/gen/EETGenerator.java | 2 +- src/sqlancer/common/oracle/EETOracle.java | 2 +- .../common/oracle/EETTransformer.java | 51 ++++++- .../mysql/ast/MySQLCastOperation.java | 7 +- .../mysql/gen/MySQLExpressionGenerator.java | 2 +- .../mysql/oracle/MySQLEETTransformer.java | 128 +++++++++++++++++- 6 files changed, 179 insertions(+), 13 deletions(-) diff --git a/src/sqlancer/common/gen/EETGenerator.java b/src/sqlancer/common/gen/EETGenerator.java index 60b91e78a..85aaa7462 100644 --- a/src/sqlancer/common/gen/EETGenerator.java +++ b/src/sqlancer/common/gen/EETGenerator.java @@ -34,5 +34,5 @@ public interface EETGenerator, J extends Join createTransformer(); + EETTransformer createTransformer(); } diff --git a/src/sqlancer/common/oracle/EETOracle.java b/src/sqlancer/common/oracle/EETOracle.java index 0ae229414..148532e9b 100644 --- a/src/sqlancer/common/oracle/EETOracle.java +++ b/src/sqlancer/common/oracle/EETOracle.java @@ -32,7 +32,7 @@ public class EETOracle, J extends Join, E private final G state; private EETGenerator gen; - private final EETTransformer transformer; + private final EETTransformer transformer; private final ExpectedErrors errors; private Reproducer reproducer; diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java index 12999402c..f27a917b9 100644 --- a/src/sqlancer/common/oracle/EETTransformer.java +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -10,13 +10,16 @@ *

* This class implements the seven transformation rules (Table 2 of the paper) and provides a template-method framework * for applying them throughout an expression's AST. Subclasses implement {@link #descend} to rebuild DBMS-specific AST - * nodes from their transformed children, and the abstract factory methods to construct new nodes; everything else (the - * rule logic, context threading, and tree-walking orchestration) is provided here. + * nodes from their transformed children, the abstract factory methods to construct new nodes, and the type hooks + * ({@link #inferType} and {@link #generateExpressionOfType}) that realize the paper's {@code rand_expr(type(expr))}; + * everything else (the rule logic, context threading, and tree-walking orchestration) is provided here. * * @param - * the DBMS-specific expression type + * the DBMS-specific expression class + * @param + * the DBMS-specific type domain used by {@link #inferType} and {@link #generateExpressionOfType} */ -public abstract class EETTransformer> { +public abstract class EETTransformer, T> { // true_expr(p) = p OR (NOT p) OR (p IS NULL) -> always TRUE private E trueExpr() { @@ -30,6 +33,22 @@ private E falseExpr() { return and(and(p, not(p)), isNotNull(p)); } + /** + * Implements the paper's {@code rand_expr(type(expr))}: a random expression whose static type matches that of + * {@code expr}. Although the generated expression is never evaluated (it occupies the dead branch of rules No. 3 + * and 4), its static type participates in the DBMS's CASE WHEN result-type resolution, so a type mismatch could + * alter the live branch's value or rendering. When the type of {@code expr} cannot be inferred, this falls back to + * {@code expr} itself, which trivially has the correct type (degenerating the rule to the {@code copy_expr} form + * of rules No. 5 and 6). + */ + private E randExprOfSameType(E expr) { + T type = inferType(expr); + if (type == null) { + return expr; + } + return generateExpressionOfType(type); + } + /** * Applies a randomly chosen applicable transformation rule to {@code expr}, returning a semantically equivalent * expression. @@ -60,12 +79,12 @@ protected E applyRandomRule(E expr, boolean booleanContext) { case 2: // expr => true_expr AND expr return and(trueExpr(), expr); case 3: // expr => CASE WHEN false_expr THEN rand_expr(type(expr)) ELSE expr END - return caseWhen(falseExpr(), expr, expr); + return caseWhen(falseExpr(), randExprOfSameType(expr), expr); case 4: // expr => CASE WHEN true_expr THEN expr ELSE rand_expr(type(expr)) END - return caseWhen(trueExpr(), expr, expr); + return caseWhen(trueExpr(), expr, randExprOfSameType(expr)); case 5: // expr => CASE WHEN rand_expr(boolean) THEN copy(expr) ELSE expr END case 6: // expr => CASE WHEN rand_expr(boolean) THEN expr ELSE copy(expr) END - return caseWhen(generateBooleanExpression(), expr, expr); + return caseWhen(generateBooleanExpression(), expr, expr); // deep copy of expr is not needed, as the AST nodes are immutable anyway default: throw new AssertionError(rule); @@ -126,6 +145,24 @@ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { /** Generates a fresh random boolean expression, reusing the variables available to the query generator. */ protected abstract E generateBooleanExpression(); + /** + * Infers the static type of {@code expr}, or returns {@code null} if it cannot be determined. The type domain + * {@code T} is DBMS-specific and may be coarse: it only needs to be precise enough that replacing an expression + * with another of the same {@code T} leaves the DBMS's CASE WHEN result-type resolution unaffected. Returning + * {@code null} is always safe — rules No. 3 and 4 then fall back to reusing {@code expr} itself as the dead + * branch. Inference should therefore be conservative: prefer {@code null} over a type whose CASE WHEN behaviour is + * uncertain. + */ + protected abstract T inferType(E expr); + + /** + * Generates a fresh random expression of static type {@code type}, reusing the variables available to the query + * generator. DBMSs with a typed expression generator can delegate to it directly; DBMSs with an untyped generator + * can instead wrap an arbitrary random expression in a CAST to {@code type} (which requires every value of + * {@code T} to be a valid CAST target). + */ + protected abstract E generateExpressionOfType(T type); + /** * Whether {@code expr} may be wrapped in a CASE WHEN expression. Some expressions (e.g. table references) are not * CASE-WHEN applicable and must be transformed to themselves (rule No. 7 of the EET paper). diff --git a/src/sqlancer/mysql/ast/MySQLCastOperation.java b/src/sqlancer/mysql/ast/MySQLCastOperation.java index 8ae783fa0..8a457d0f1 100644 --- a/src/sqlancer/mysql/ast/MySQLCastOperation.java +++ b/src/sqlancer/mysql/ast/MySQLCastOperation.java @@ -6,11 +6,14 @@ public class MySQLCastOperation implements MySQLExpression { private final CastType type; public enum CastType { - SIGNED, UNSIGNED; + SIGNED, UNSIGNED, + // CHAR and DOUBLE are used only by the EET oracle's type-pinning casts and are never evaluated, so + // MySQLConstant.castAs does not support them; they must not be returned by getRandom(). + CHAR, DOUBLE; public static CastType getRandom() { return SIGNED; - // return Randomly.fromOptions(CastType.values()); + // return Randomly.fromOptions(CastType.SIGNED, CastType.UNSIGNED); } } diff --git a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java index 5d1ec06f4..9b019a6d4 100644 --- a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java @@ -367,7 +367,7 @@ boolean mutateOr(MySQLSelect select) { // --- EET oracle --- @Override - public EETTransformer createTransformer() { + public EETTransformer createTransformer() { return new MySQLEETTransformer(this); } } diff --git a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java index 9b0cac226..d16a8e517 100644 --- a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -1,5 +1,6 @@ package sqlancer.mysql.oracle; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -12,7 +13,11 @@ import sqlancer.mysql.ast.MySQLBinaryOperation; import sqlancer.mysql.ast.MySQLCaseOperator; import sqlancer.mysql.ast.MySQLCastOperation; +import sqlancer.mysql.ast.MySQLCastOperation.CastType; +import sqlancer.mysql.ast.MySQLColumnReference; import sqlancer.mysql.ast.MySQLComputableFunction; +import sqlancer.mysql.ast.MySQLConstant; +import sqlancer.mysql.ast.MySQLExists; import sqlancer.mysql.ast.MySQLExpression; import sqlancer.mysql.ast.MySQLInOperation; import sqlancer.mysql.ast.MySQLTableReference; @@ -25,8 +30,13 @@ /** * MySQL implementation of the {@link EETTransformer EET} tree-walker. Implements {@link #descend} to rebuild MySQL AST * nodes from their transformed children, threading the correct boolean/scalar context into each child. + * + *

+ * MySQL's expression generator is untyped, so type inference/generation works with a subset of MySQL's CAST target types + * ({@link CastType}): {@link #inferType} conservatively classifies AST nodes into that domain (returning {@code null} + * when uncertain), and {@link #generateExpressionOfType} pins the type of a random expression by wrapping it in a CAST. */ -public class MySQLEETTransformer extends EETTransformer { +public class MySQLEETTransformer extends EETTransformer { private static final boolean BOOLEAN = true; private static final boolean SCALAR = false; @@ -136,6 +146,122 @@ protected MySQLExpression generateBooleanExpression() { return gen.generateBooleanExpression(); } + @Override + protected MySQLExpression generateExpressionOfType(CastType type) { + // The MySQL expression generator is untyped, so the type of an arbitrary random expression is pinned by + // wrapping it in a CAST to the requested type. + return new MySQLCastOperation(gen.generateExpression(), type); + } + + @Override + protected CastType inferType(MySQLExpression expr) { + if (expr instanceof MySQLBinaryLogicalOperation || expr instanceof MySQLBinaryComparisonOperation + || expr instanceof MySQLUnaryPostfixOperation || expr instanceof MySQLBetweenOperation + || expr instanceof MySQLInOperation || expr instanceof MySQLExists) { + // Predicates evaluate to the boolean values 0/1, which are signed BIGINT. + return CastType.SIGNED; + } else if (expr instanceof MySQLBinaryOperation) { + // The bit operators &, | and ^ return BIGINT UNSIGNED. + return CastType.UNSIGNED; + } else if (expr instanceof MySQLCastOperation) { + return ((MySQLCastOperation) expr).getType(); + } else if (expr instanceof MySQLUnaryPrefixOperation) { + return inferUnaryPrefixType((MySQLUnaryPrefixOperation) expr); + } else if (expr instanceof MySQLConstant) { + return inferConstantType((MySQLConstant) expr); + } else if (expr instanceof MySQLColumnReference) { + return inferColumnType((MySQLColumnReference) expr); + } else if (expr instanceof MySQLComputableFunction) { + return inferFunctionType((MySQLComputableFunction) expr); + } else if (expr instanceof MySQLCaseOperator) { + return inferCaseType((MySQLCaseOperator) expr); + } + return null; + } + + private CastType inferUnaryPrefixType(MySQLUnaryPrefixOperation op) { + if (op.getOp() == MySQLUnaryPrefixOperator.NOT) { + return CastType.SIGNED; + } + CastType operandType = inferType(op.getExpression()); + if (op.getOp() == MySQLUnaryPrefixOperator.PLUS) { + return operandType; // unary + is the identity + } + // Unary -: strings are coerced to DOUBLE; negating UNSIGNED changes the type (and usually errors). + if (operandType == CastType.CHAR || operandType == CastType.DOUBLE) { + return CastType.DOUBLE; + } + return operandType == CastType.SIGNED ? CastType.SIGNED : null; + } + + private CastType inferConstantType(MySQLConstant constant) { + if (constant instanceof MySQLConstant.MySQLIntConstant) { + return constant.isSigned() ? CastType.SIGNED : CastType.UNSIGNED; + } else if (constant instanceof MySQLConstant.MySQLTextConstant) { + return CastType.CHAR; + } else if (constant instanceof MySQLConstant.MySQLDoubleConstant) { + return CastType.DOUBLE; + } + return null; // the NULL constant has no type of its own + } + + private CastType inferColumnType(MySQLColumnReference ref) { + switch (ref.getColumn().getType()) { + case INT: + return CastType.SIGNED; // the table generator never creates UNSIGNED INT columns + case VARCHAR: + return CastType.CHAR; + case DOUBLE: + return CastType.DOUBLE; + case FLOAT: // FLOAT-to-DOUBLE widening in the CASE result changes the rendered value + case DECIMAL: // the CASE result would need the column's exact precision and scale + default: + return null; + } + } + + private CastType inferFunctionType(MySQLComputableFunction func) { + MySQLExpression[] args = func.getArguments(); + switch (func.getFunction()) { + case BIT_COUNT: + return CastType.SIGNED; + case IF: + // The result type aggregates the types of the two value arguments (the condition does not contribute). + return commonType(args[1], args[2]); + case COALESCE: + case IFNULL: + case LEAST: + case GREATEST: + return commonType(args); + default: + return null; + } + } + + private CastType inferCaseType(MySQLCaseOperator caseOp) { + List branches = new ArrayList<>(caseOp.getExpressions()); + if (caseOp.getElseExpr() != null) { + branches.add(caseOp.getElseExpr()); + } + return commonType(branches.toArray(new MySQLExpression[0])); + } + + /** + * The common type of several result-type-determining subexpressions, or {@code null} if they do not have the + * same inferrable type (a conservative under-approximation of MySQL's aggregation rules). + */ + private CastType commonType(MySQLExpression... exprs) { + CastType common = null; + for (MySQLExpression expr : exprs) { + CastType type = inferType(expr); + if (type == null || common != null && type != common) { + return null; + } + common = type; + } + return common; + } + @Override protected boolean isCaseWhenApplicable(MySQLExpression expr) { // Table references cannot be wrapped in CASE WHEN (they would cause syntax errors, see rule No. 7 of the EET From 86e5b719a6b7fffc548e37ad15a6dd54657797c4 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Wed, 15 Jul 2026 09:50:26 +0800 Subject: [PATCH 09/14] Fix EET reproducer/reduction to port the same fixes to it that were earlier made to TLP WHERE --- src/sqlancer/common/oracle/EETOracle.java | 92 +++++++++++++++++++---- 1 file changed, 78 insertions(+), 14 deletions(-) diff --git a/src/sqlancer/common/oracle/EETOracle.java b/src/sqlancer/common/oracle/EETOracle.java index 148532e9b..5af0cd1b3 100644 --- a/src/sqlancer/common/oracle/EETOracle.java +++ b/src/sqlancer/common/oracle/EETOracle.java @@ -40,28 +40,73 @@ public class EETOracle, J extends Join, E private final class EETReproducer implements Reproducer { private final String originalQueryString; + // null if the original bug was a DBMS error on the original query alone private final String transformedQueryString; - private final List resultSet; + // null if the original bug is a result set mismatch; otherwise, the message of the + // unexpected DBMS error that the original or transformed query triggered + private final String expectedErrorMessage; - EETReproducer(String originalQueryString, String transformedQueryString, List resultSet) { + EETReproducer(String originalQueryString, String transformedQueryString, String expectedErrorMessage) { this.originalQueryString = originalQueryString; this.transformedQueryString = transformedQueryString; - this.resultSet = resultSet; + this.expectedErrorMessage = expectedErrorMessage; } @Override public boolean bugStillTriggers(G globalState) { + List originalResultSet; + List transformedResultSet; try { - List transformedResultSet = ComparatorHelper - .getResultSetFirstColumnAsString(transformedQueryString, errors, globalState); - ComparatorHelper.assumeResultSetsAreEqual(resultSet, transformedResultSet, originalQueryString, + // Re-execute both queries against the current (reduced) database instead of comparing + // against a cached result set, which would be stale once statements have been removed. + originalResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, + globalState); + if (transformedQueryString == null) { + // the original bug was a DBMS error on the original query alone, which no + // longer occurs + return false; + } + transformedResultSet = ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, errors, + globalState); + } catch (AssertionError unexpectedError) { + // a DBMS error reproduces the bug only if the original failure was the same error; + // other errors are artifacts of the reduction (e.g., a removed CREATE TABLE) + return expectedErrorMessage != null + && expectedErrorMessage.equals(TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + } catch (SQLException | RuntimeException e) { + return false; + } + if (expectedErrorMessage != null) { + // the original bug was a DBMS error, which no longer occurs + return false; + } + try { + ComparatorHelper.assumeResultSetsAreEqual(originalResultSet, transformedResultSet, originalQueryString, List.of(transformedQueryString), globalState); - } catch (AssertionError triggeredError) { + } catch (AssertionError resultSetMismatch) { return true; - } catch (SQLException ignored) { } return false; } + + @Override + public String getBugInformation() { + StringBuilder sb = new StringBuilder(); + if (expectedErrorMessage == null) { + sb.append("-- On the database set up by the statements above, the result sets of the following" + + " queries mismatch:").append(System.lineSeparator()); + } else { + sb.append("-- On the database set up by the statements above, the following queries trigger an" + + " unexpected error with message: ").append(expectedErrorMessage) + .append(System.lineSeparator()); + } + sb.append("-- original: ").append(originalQueryString).append(';').append(System.lineSeparator()); + if (transformedQueryString != null) { + sb.append("-- transformed: ").append(transformedQueryString).append(';') + .append(System.lineSeparator()); + } + return sb.toString(); + } } public EETOracle(G state, EETGenerator gen, ExpectedErrors expectedErrors) { @@ -91,8 +136,16 @@ public void check() throws SQLException { String originalQueryString = select.asString(); generatedQueryString = originalQueryString; - List originalResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, - state); + List originalResultSet; + try { + originalResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, state); + } catch (AssertionError unexpectedError) { + // an unexpected DBMS error on the original query alone is itself a bug worth reducing; + // transformedQueryString is null because no transformed query is involved + reproducer = new EETReproducer(originalQueryString, null, + TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } // Transform the query's expressions into semantically equivalent ones. Fetch columns are scalar expressions, // while the WHERE clause is evaluated in a boolean context. @@ -102,13 +155,24 @@ public void check() throws SQLException { select.setWhereClause(transformer.transform(whereClause, true)); String transformedQueryString = select.asString(); - List transformedResultSet = ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, - errors, state); + List transformedResultSet; + try { + transformedResultSet = ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, errors, + state); + } catch (AssertionError unexpectedError) { + // the semantics-preserving transformation made the query trigger a DBMS error that the + // original did not, which is a bug worth reducing + reproducer = new EETReproducer(originalQueryString, transformedQueryString, + TestOracleUtils.getUnexpectedErrorMessage(unexpectedError)); + throw unexpectedError; + } + + // Set the reproducer before the assertion: assumeResultSetsAreEqual throws when the bug is + // detected, so creating the reproducer afterwards would leave it null and prevent any reduction. + reproducer = new EETReproducer(originalQueryString, transformedQueryString, null); ComparatorHelper.assumeResultSetsAreEqual(originalResultSet, transformedResultSet, originalQueryString, List.of(transformedQueryString), state); - - reproducer = new EETReproducer(originalQueryString, transformedQueryString, originalResultSet); } @Override From 912197dba19e2f6321d9881cd815172c779dec73 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Thu, 23 Jul 2026 15:48:24 +0800 Subject: [PATCH 10/14] Add FLOAT and DECIMAL to MySQL EET target cast types --- src/sqlancer/common/gen/EETGenerator.java | 6 +++--- src/sqlancer/common/oracle/EETOracle.java | 9 ++++----- .../common/oracle/EETTransformer.java | 16 +++++++-------- .../mysql/ast/MySQLCastOperation.java | 6 +++--- .../mysql/oracle/MySQLEETTransformer.java | 20 +++++++++++++------ 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/sqlancer/common/gen/EETGenerator.java b/src/sqlancer/common/gen/EETGenerator.java index 85aaa7462..ef436d975 100644 --- a/src/sqlancer/common/gen/EETGenerator.java +++ b/src/sqlancer/common/gen/EETGenerator.java @@ -30,9 +30,9 @@ public interface EETGenerator, J extends Join createTransformer(); } diff --git a/src/sqlancer/common/oracle/EETOracle.java b/src/sqlancer/common/oracle/EETOracle.java index 5af0cd1b3..1422d5b6b 100644 --- a/src/sqlancer/common/oracle/EETOracle.java +++ b/src/sqlancer/common/oracle/EETOracle.java @@ -23,9 +23,9 @@ * *

* The oracle generates a random query and then transforms its expressions (the WHERE predicate and the fetch columns) - * into semantically equivalent ones using {@link EETGenerator#transformExpression}. Because the transformation preserves - * semantics, the original and the transformed query must return the same result set; any discrepancy indicates a logic - * bug in the DBMS. + * into semantically equivalent ones using {@link EETGenerator#transformExpression}. Because the transformation + * preserves semantics, the original and the transformed query must return the same result set; any discrepancy + * indicates a logic bug in the DBMS. */ public class EETOracle, J extends Join, E extends Expression, S extends AbstractSchema, T extends AbstractTable, C extends AbstractTableColumn, G extends SQLGlobalState> implements TestOracle { @@ -102,8 +102,7 @@ public String getBugInformation() { } sb.append("-- original: ").append(originalQueryString).append(';').append(System.lineSeparator()); if (transformedQueryString != null) { - sb.append("-- transformed: ").append(transformedQueryString).append(';') - .append(System.lineSeparator()); + sb.append("-- transformed: ").append(transformedQueryString).append(';').append(System.lineSeparator()); } return sb.toString(); } diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java index f27a917b9..932a6fb8e 100644 --- a/src/sqlancer/common/oracle/EETTransformer.java +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -38,8 +38,8 @@ private E falseExpr() { * {@code expr}. Although the generated expression is never evaluated (it occupies the dead branch of rules No. 3 * and 4), its static type participates in the DBMS's CASE WHEN result-type resolution, so a type mismatch could * alter the live branch's value or rendering. When the type of {@code expr} cannot be inferred, this falls back to - * {@code expr} itself, which trivially has the correct type (degenerating the rule to the {@code copy_expr} form - * of rules No. 5 and 6). + * {@code expr} itself, which trivially has the correct type (degenerating the rule to the {@code copy_expr} form of + * rules No. 5 and 6). */ private E randExprOfSameType(E expr) { T type = inferType(expr); @@ -85,7 +85,7 @@ protected E applyRandomRule(E expr, boolean booleanContext) { case 5: // expr => CASE WHEN rand_expr(boolean) THEN copy(expr) ELSE expr END case 6: // expr => CASE WHEN rand_expr(boolean) THEN expr ELSE copy(expr) END return caseWhen(generateBooleanExpression(), expr, expr); - // deep copy of expr is not needed, as the AST nodes are immutable anyway + // deep copy of expr is not needed, as the AST nodes are immutable anyway default: throw new AssertionError(rule); } @@ -111,9 +111,9 @@ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { } /** - * Rebuilds {@code expr} with its children transformed, threading the correct boolean/scalar context into each child. - * Leaf nodes (columns, constants, table references, ...) should be returned unchanged; any applicable transformation - * will still be applied to them by the calling {@link #transformNode}. + * Rebuilds {@code expr} with its children transformed, threading the correct boolean/scalar context into each + * child. Leaf nodes (columns, constants, table references, ...) should be returned unchanged; any applicable + * transformation will still be applied to them by the calling {@link #transformNode}. * * @param expr * the expression to descend into @@ -149,8 +149,8 @@ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { * Infers the static type of {@code expr}, or returns {@code null} if it cannot be determined. The type domain * {@code T} is DBMS-specific and may be coarse: it only needs to be precise enough that replacing an expression * with another of the same {@code T} leaves the DBMS's CASE WHEN result-type resolution unaffected. Returning - * {@code null} is always safe — rules No. 3 and 4 then fall back to reusing {@code expr} itself as the dead - * branch. Inference should therefore be conservative: prefer {@code null} over a type whose CASE WHEN behaviour is + * {@code null} is always safe — rules No. 3 and 4 then fall back to reusing {@code expr} itself as the dead branch. + * Inference should therefore be conservative: prefer {@code null} over a type whose CASE WHEN behaviour is * uncertain. */ protected abstract T inferType(E expr); diff --git a/src/sqlancer/mysql/ast/MySQLCastOperation.java b/src/sqlancer/mysql/ast/MySQLCastOperation.java index 8a457d0f1..b71d0498c 100644 --- a/src/sqlancer/mysql/ast/MySQLCastOperation.java +++ b/src/sqlancer/mysql/ast/MySQLCastOperation.java @@ -7,9 +7,9 @@ public class MySQLCastOperation implements MySQLExpression { public enum CastType { SIGNED, UNSIGNED, - // CHAR and DOUBLE are used only by the EET oracle's type-pinning casts and are never evaluated, so - // MySQLConstant.castAs does not support them; they must not be returned by getRandom(). - CHAR, DOUBLE; + // CHAR, FLOAT, DOUBLE and DECIMAL are used only by the EET oracle's type-pinning casts and are never + // evaluated, so MySQLConstant.castAs does not support them; they must not be returned by getRandom(). + CHAR, FLOAT, DOUBLE, DECIMAL; public static CastType getRandom() { return SIGNED; diff --git a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java index d16a8e517..5972996b8 100644 --- a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -80,8 +80,8 @@ protected MySQLExpression descend(MySQLExpression expr, boolean booleanContext) transformNode(op.getLeft(), SCALAR, false), transformNode(op.getRight(), SCALAR, false)); } else if (expr instanceof MySQLInOperation) { MySQLInOperation op = (MySQLInOperation) expr; - List listElements = op.getListElements().stream() - .map(e -> transformNode(e, SCALAR, false)).collect(Collectors.toList()); + List listElements = op.getListElements().stream().map(e -> transformNode(e, SCALAR, false)) + .collect(Collectors.toList()); return new MySQLInOperation(transformNode(op.getExpr(), SCALAR, false), listElements, op.isTrue()); } else if (expr instanceof MySQLComputableFunction) { MySQLComputableFunction op = (MySQLComputableFunction) expr; @@ -211,10 +211,18 @@ private CastType inferColumnType(MySQLColumnReference ref) { return CastType.SIGNED; // the table generator never creates UNSIGNED INT columns case VARCHAR: return CastType.CHAR; + case FLOAT: + // Assumes FLOAT columns are never created with (M, D); otherwise the CAST would need the exact + // precision/scale. + return CastType.FLOAT; case DOUBLE: + // Assumes DOUBLE columns are never created with (M, D); otherwise the CAST would need the exact + // precision/scale. return CastType.DOUBLE; - case FLOAT: // FLOAT-to-DOUBLE widening in the CASE result changes the rendered value - case DECIMAL: // the CASE result would need the column's exact precision and scale + case DECIMAL: + // Assumes DECIMAL columns are never created with (M, D); otherwise the CAST would need the exact + // precision/scale. + return CastType.DECIMAL; default: return null; } @@ -247,8 +255,8 @@ private CastType inferCaseType(MySQLCaseOperator caseOp) { } /** - * The common type of several result-type-determining subexpressions, or {@code null} if they do not have the - * same inferrable type (a conservative under-approximation of MySQL's aggregation rules). + * The common type of several result-type-determining subexpressions, or {@code null} if they do not have the same + * inferrable type (a conservative under-approximation of MySQL's aggregation rules). */ private CastType commonType(MySQLExpression... exprs) { CastType common = null; From 4d6a651d5da612c26b4c180796b174c1fddc2dfc Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Thu, 23 Jul 2026 17:38:12 +0800 Subject: [PATCH 11/14] Fix treatment of unary minus in MySQL EET implementation --- .../mysql/oracle/MySQLEETTransformer.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java index 5972996b8..eb107c94b 100644 --- a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -185,13 +185,18 @@ private CastType inferUnaryPrefixType(MySQLUnaryPrefixOperation op) { } CastType operandType = inferType(op.getExpression()); if (op.getOp() == MySQLUnaryPrefixOperator.PLUS) { - return operandType; // unary + is the identity + return operandType; } - // Unary -: strings are coerced to DOUBLE; negating UNSIGNED changes the type (and usually errors). - if (operandType == CastType.CHAR || operandType == CastType.DOUBLE) { - return CastType.DOUBLE; + if (op.getOp() == MySQLUnaryPrefixOperator.MINUS) { + if (operandType == CastType.UNSIGNED) { + return CastType.SIGNED; + } else if (operandType == CastType.FLOAT) { + return CastType.DOUBLE; + } else if (operandType != CastType.CHAR) { + return operandType; + } } - return operandType == CastType.SIGNED ? CastType.SIGNED : null; + return null; } private CastType inferConstantType(MySQLConstant constant) { From e3d9f61ec8e09310aed7e8b686d777116c36776a Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Sun, 26 Jul 2026 15:02:27 +0800 Subject: [PATCH 12/14] Restrict creation of (M, D) columns to prevent false positives in EET --- src/sqlancer/mysql/gen/MySQLTableGenerator.java | 9 +++++++-- .../mysql/oracle/MySQLEETTransformer.java | 16 +++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/sqlancer/mysql/gen/MySQLTableGenerator.java b/src/sqlancer/mysql/gen/MySQLTableGenerator.java index c17ccf0d5..40e325041 100644 --- a/src/sqlancer/mysql/gen/MySQLTableGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLTableGenerator.java @@ -368,8 +368,13 @@ private void appendType(MySQLDataType randomType) { } } - public static void optionallyAddPrecisionAndScale(StringBuilder sb) { - if (Randomly.getBoolean() && !MySQLBugs.bug99183) { + private void optionallyAddPrecisionAndScale(StringBuilder sb) { + // The EET oracle's type inference assumes FLOAT/DOUBLE/DECIMAL columns are created without (M, D) (see + // MySQLEETTransformer#inferColumnType), so precision/scale is omitted while EET is active. This restriction can + // be lifted once (M, D) is tracked through the codebase and reflected in the CAST target types. + boolean eetActive = globalState.getDbmsSpecificOptions().getTestOracleFactory().stream() + .anyMatch(o -> o == MySQLOracleFactory.EET); + if (Randomly.getBoolean() && !MySQLBugs.bug99183 && !eetActive) { sb.append("("); // The maximum number of digits (M) for DECIMAL is 65 long m = Randomly.getNotCachedInteger(1, 65); diff --git a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java index eb107c94b..27a62869a 100644 --- a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -32,9 +32,10 @@ * nodes from their transformed children, threading the correct boolean/scalar context into each child. * *

- * MySQL's expression generator is untyped, so type inference/generation works with a subset of MySQL's CAST target types - * ({@link CastType}): {@link #inferType} conservatively classifies AST nodes into that domain (returning {@code null} - * when uncertain), and {@link #generateExpressionOfType} pins the type of a random expression by wrapping it in a CAST. + * MySQL's expression generator is untyped, so type inference/generation works with a subset of MySQL's CAST target + * types ({@link CastType}): {@link #inferType} conservatively classifies AST nodes into that domain (returning + * {@code null} when uncertain), and {@link #generateExpressionOfType} pins the type of a random expression by wrapping + * it in a CAST. */ public class MySQLEETTransformer extends EETTransformer { @@ -216,17 +217,14 @@ private CastType inferColumnType(MySQLColumnReference ref) { return CastType.SIGNED; // the table generator never creates UNSIGNED INT columns case VARCHAR: return CastType.CHAR; + // FLOAT/DOUBLE/DECIMAL columns are created without (M, D) while EET is active, so the plain + // CAST target below matches the column's type. Reintroducing (M, D) for better coverage would + // require tracking it here and emitting the exact precision/scale in the CAST. case FLOAT: - // Assumes FLOAT columns are never created with (M, D); otherwise the CAST would need the exact - // precision/scale. return CastType.FLOAT; case DOUBLE: - // Assumes DOUBLE columns are never created with (M, D); otherwise the CAST would need the exact - // precision/scale. return CastType.DOUBLE; case DECIMAL: - // Assumes DECIMAL columns are never created with (M, D); otherwise the CAST would need the exact - // precision/scale. return CastType.DECIMAL; default: return null; From 0a09fba83235c0b129e10a11538e3835ee1a7a1c Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Sun, 26 Jul 2026 15:15:33 +0800 Subject: [PATCH 13/14] Fix checkstyle violations --- src/sqlancer/common/gen/EETGenerator.java | 13 +++ src/sqlancer/common/oracle/EETOracle.java | 15 +++ .../common/oracle/EETTransformer.java | 110 ++++++++++++++++-- .../mysql/gen/MySQLExpressionGenerator.java | 2 +- .../mysql/oracle/MySQLEETTransformer.java | 7 +- 5 files changed, 135 insertions(+), 12 deletions(-) diff --git a/src/sqlancer/common/gen/EETGenerator.java b/src/sqlancer/common/gen/EETGenerator.java index ef436d975..a87a635ec 100644 --- a/src/sqlancer/common/gen/EETGenerator.java +++ b/src/sqlancer/common/gen/EETGenerator.java @@ -14,6 +14,17 @@ * Generator interface used by {@link sqlancer.common.oracle.EETOracle}. In addition to generating a random query (like * the other oracle generators), an EET generator creates a DBMS-specific {@link EETTransformer} that the oracle uses to * rewrite expressions into semantically equivalent ones. + * + * @param + * the DBMS-specific SELECT statement class + * @param + * the DBMS-specific JOIN clause class + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific table class + * @param + * the DBMS-specific column class */ public interface EETGenerator, J extends Join, E extends Expression, T extends AbstractTable, C extends AbstractTableColumn> { @@ -33,6 +44,8 @@ public interface EETGenerator, J extends Join createTransformer(); } diff --git a/src/sqlancer/common/oracle/EETOracle.java b/src/sqlancer/common/oracle/EETOracle.java index 1422d5b6b..b5aedf2cb 100644 --- a/src/sqlancer/common/oracle/EETOracle.java +++ b/src/sqlancer/common/oracle/EETOracle.java @@ -26,6 +26,21 @@ * into semantically equivalent ones using {@link EETGenerator#transformExpression}. Because the transformation * preserves semantics, the original and the transformed query must return the same result set; any discrepancy * indicates a logic bug in the DBMS. + * + * @param + * the DBMS-specific SELECT statement class + * @param + * the DBMS-specific JOIN clause class + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific schema class + * @param + * the DBMS-specific table class + * @param + * the DBMS-specific column class + * @param + * the DBMS-specific global state class */ public class EETOracle, J extends Join, E extends Expression, S extends AbstractSchema, T extends AbstractTable, C extends AbstractTableColumn, G extends SQLGlobalState> implements TestOracle { diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java index 932a6fb8e..c46e148e1 100644 --- a/src/sqlancer/common/oracle/EETTransformer.java +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -24,7 +24,7 @@ public abstract class EETTransformer, T> { // true_expr(p) = p OR (NOT p) OR (p IS NULL) -> always TRUE private E trueExpr() { E p = generateBooleanExpression(); - return or(or(p, not(p)), isNull(p)); + return orExpr(orExpr(p, not(p)), isNull(p)); } // false_expr(p) = p AND (NOT p) AND (p IS NOT NULL) -> always FALSE @@ -40,6 +40,11 @@ private E falseExpr() { * alter the live branch's value or rendering. When the type of {@code expr} cannot be inferred, this falls back to * {@code expr} itself, which trivially has the correct type (degenerating the rule to the {@code copy_expr} form of * rules No. 5 and 6). + * + * @param expr + * the expression whose static type the generated expression must match + * + * @return a random expression whose static type matches that of {@code expr} */ private E randExprOfSameType(E expr) { T type = inferType(expr); @@ -75,7 +80,7 @@ protected E applyRandomRule(E expr, boolean booleanContext) { } switch (rule) { case 1: // expr => false_expr OR expr - return or(falseExpr(), expr); + return orExpr(falseExpr(), expr); case 2: // expr => true_expr AND expr return and(trueExpr(), expr); case 3: // expr => CASE WHEN false_expr THEN rand_expr(type(expr)) ELSE expr END @@ -94,6 +99,13 @@ protected E applyRandomRule(E expr, boolean booleanContext) { /** * Transforms {@code expr} into a semantically equivalent expression. A transformation rule is always applied at the * root, guaranteeing (unless only rule 7 is applicable) that the returned expression differs from the input. + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value + * + * @return a semantically equivalent expression */ public E transform(E expr, boolean booleanContext) { return transformNode(expr, booleanContext, true); @@ -101,6 +113,15 @@ public E transform(E expr, boolean booleanContext) { /** * Descends into {@code expr}, rebuilds it from transformed children, then optionally applies a rule at this node. + * + * @param expr + * the expression to transform + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value + * @param forceApply + * whether a rule must be applied at this node rather than only with some probability + * + * @return the transformed expression */ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { E descended = descend(expr, booleanContext); @@ -124,25 +145,79 @@ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { */ protected abstract E descend(E expr, boolean booleanContext); - /** Builds {@code left AND right}. */ + /** + * Builds {@code left AND right}. + * + * @param left + * the left operand + * @param right + * the right operand + * + * @return the {@code left AND right} expression + */ protected abstract E and(E left, E right); - /** Builds {@code left OR right}. */ - protected abstract E or(E left, E right); + /** + * Builds {@code left OR right}. + * + * @param left + * the left operand + * @param right + * the right operand + * + * @return the {@code left OR right} expression + */ + protected abstract E orExpr(E left, E right); - /** Builds {@code NOT expr}. */ + /** + * Builds {@code NOT expr}. + * + * @param expr + * the operand + * + * @return the {@code NOT expr} expression + */ protected abstract E not(E expr); - /** Builds {@code expr IS NULL}. */ + /** + * Builds {@code expr IS NULL}. + * + * @param expr + * the operand + * + * @return the {@code expr IS NULL} expression + */ protected abstract E isNull(E expr); - /** Builds {@code expr IS NOT NULL}. */ + /** + * Builds {@code expr IS NOT NULL}. + * + * @param expr + * the operand + * + * @return the {@code expr IS NOT NULL} expression + */ protected abstract E isNotNull(E expr); - /** Builds {@code CASE WHEN condition THEN thenExpr ELSE elseExpr END}. */ + /** + * Builds {@code CASE WHEN condition THEN thenExpr ELSE elseExpr END}. + * + * @param condition + * the WHEN condition + * @param thenExpr + * the THEN branch + * @param elseExpr + * the ELSE branch + * + * @return the CASE WHEN expression + */ protected abstract E caseWhen(E condition, E thenExpr, E elseExpr); - /** Generates a fresh random boolean expression, reusing the variables available to the query generator. */ + /** + * Generates a fresh random boolean expression, reusing the variables available to the query generator. + * + * @return a fresh random boolean expression + */ protected abstract E generateBooleanExpression(); /** @@ -152,6 +227,11 @@ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { * {@code null} is always safe — rules No. 3 and 4 then fall back to reusing {@code expr} itself as the dead branch. * Inference should therefore be conservative: prefer {@code null} over a type whose CASE WHEN behaviour is * uncertain. + * + * @param expr + * the expression whose static type is to be inferred + * + * @return the inferred static type of {@code expr}, or {@code null} if it cannot be determined */ protected abstract T inferType(E expr); @@ -160,12 +240,22 @@ protected E transformNode(E expr, boolean booleanContext, boolean forceApply) { * generator. DBMSs with a typed expression generator can delegate to it directly; DBMSs with an untyped generator * can instead wrap an arbitrary random expression in a CAST to {@code type} (which requires every value of * {@code T} to be a valid CAST target). + * + * @param type + * the static type the generated expression must have + * + * @return a fresh random expression of static type {@code type} */ protected abstract E generateExpressionOfType(T type); /** * Whether {@code expr} may be wrapped in a CASE WHEN expression. Some expressions (e.g. table references) are not * CASE-WHEN applicable and must be transformed to themselves (rule No. 7 of the EET paper). + * + * @param expr + * the expression to test + * + * @return {@code true} if {@code expr} may be wrapped in a CASE WHEN expression */ protected abstract boolean isCaseWhenApplicable(E expr); } diff --git a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java index 9b019a6d4..da304ac67 100644 --- a/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java +++ b/src/sqlancer/mysql/gen/MySQLExpressionGenerator.java @@ -12,6 +12,7 @@ import sqlancer.common.gen.EETGenerator; import sqlancer.common.gen.TLPWhereGenerator; import sqlancer.common.gen.UntypedExpressionGenerator; +import sqlancer.common.oracle.EETTransformer; import sqlancer.common.schema.AbstractTables; import sqlancer.mysql.MySQLBugs; import sqlancer.mysql.MySQLGlobalState; @@ -46,7 +47,6 @@ import sqlancer.mysql.ast.MySQLUnaryPostfixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation; import sqlancer.mysql.ast.MySQLUnaryPrefixOperation.MySQLUnaryPrefixOperator; -import sqlancer.common.oracle.EETTransformer; import sqlancer.mysql.oracle.MySQLEETTransformer; public class MySQLExpressionGenerator extends UntypedExpressionGenerator diff --git a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java index 27a62869a..2e4394eec 100644 --- a/src/sqlancer/mysql/oracle/MySQLEETTransformer.java +++ b/src/sqlancer/mysql/oracle/MySQLEETTransformer.java @@ -118,7 +118,7 @@ protected MySQLExpression and(MySQLExpression left, MySQLExpression right) { } @Override - protected MySQLExpression or(MySQLExpression left, MySQLExpression right) { + protected MySQLExpression orExpr(MySQLExpression left, MySQLExpression right) { return new MySQLBinaryLogicalOperation(left, right, MySQLBinaryLogicalOperator.OR); } @@ -260,6 +260,11 @@ private CastType inferCaseType(MySQLCaseOperator caseOp) { /** * The common type of several result-type-determining subexpressions, or {@code null} if they do not have the same * inferrable type (a conservative under-approximation of MySQL's aggregation rules). + * + * @param exprs + * the result-type-determining subexpressions + * + * @return the common inferred type of {@code exprs}, or {@code null} if they do not share one */ private CastType commonType(MySQLExpression... exprs) { CastType common = null; From c71d198fff6cb271e1f946addbe5c43597580636 Mon Sep 17 00:00:00 2001 From: Thomas Morgan Date: Thu, 30 Jul 2026 13:09:02 +0800 Subject: [PATCH 14/14] Refactor EETTransformer to encode the transformation rules as an enum --- .../common/oracle/EETTransformer.java | 159 ++++++++++++++---- 1 file changed, 129 insertions(+), 30 deletions(-) diff --git a/src/sqlancer/common/oracle/EETTransformer.java b/src/sqlancer/common/oracle/EETTransformer.java index c46e148e1..b472b8aff 100644 --- a/src/sqlancer/common/oracle/EETTransformer.java +++ b/src/sqlancer/common/oracle/EETTransformer.java @@ -1,5 +1,8 @@ package sqlancer.common.oracle; +import java.util.ArrayList; +import java.util.List; + import sqlancer.Randomly; import sqlancer.common.ast.newast.Expression; @@ -35,11 +38,10 @@ private E falseExpr() { /** * Implements the paper's {@code rand_expr(type(expr))}: a random expression whose static type matches that of - * {@code expr}. Although the generated expression is never evaluated (it occupies the dead branch of rules No. 3 + * {@code expr}. Although the generated expression is never evaluated (it occupies the redundant branch of rules 3 * and 4), its static type participates in the DBMS's CASE WHEN result-type resolution, so a type mismatch could * alter the live branch's value or rendering. When the type of {@code expr} cannot be inferred, this falls back to - * {@code expr} itself, which trivially has the correct type (degenerating the rule to the {@code copy_expr} form of - * rules No. 5 and 6). + * {@code expr} itself, which trivially has the correct type (degenerating to rules 5 and 6). * * @param expr * the expression whose static type the generated expression must match @@ -54,46 +56,143 @@ private E randExprOfSameType(E expr) { return generateExpressionOfType(type); } + /** + * The first six transformation rules of the EET paper (Table 2). Each rule knows how to apply itself + * ({@link #apply}) and in which contexts it preserves the expression's value ({@link #isApplicable}). Rule No. 7 + * (transform the expression to itself) is not modelled here: it is the fallback applied by {@link #applyRandomRule} + * when no other rule is applicable. + */ + private enum Rule { + // expr => false_expr OR expr + RULE_1 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.orExpr(t.falseExpr(), expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + // Reduces the expression to a boolean value, so it is value-preserving only in a boolean context. + return booleanContext; + } + }, + // expr => true_expr AND expr + RULE_2 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.and(t.trueExpr(), expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + // Reduces the expression to a boolean value, so it is value-preserving only in a boolean context. + return booleanContext; + } + }, + // expr => CASE WHEN false_expr THEN rand_expr(type(expr)) ELSE expr END + RULE_3 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.caseWhen(t.falseExpr(), t.randExprOfSameType(expr), expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }, + // expr => CASE WHEN true_expr THEN expr ELSE rand_expr(type(expr)) END + RULE_4 { + @Override + , T> E apply(EETTransformer t, E expr) { + return t.caseWhen(t.trueExpr(), expr, t.randExprOfSameType(expr)); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }, + // expr => CASE WHEN rand_expr(boolean) THEN copy(expr) ELSE expr END + RULE_5 { + @Override + , T> E apply(EETTransformer t, E expr) { + // deep copy of expr is not needed, as the AST nodes are immutable anyway + return t.caseWhen(t.generateBooleanExpression(), expr, expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }, + // expr => CASE WHEN rand_expr(boolean) THEN expr ELSE copy(expr) END + RULE_6 { + @Override + , T> E apply(EETTransformer t, E expr) { + // deep copy of expr is not needed, as the AST nodes are immutable anyway + return t.caseWhen(t.generateBooleanExpression(), expr, expr); + } + + @Override + boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable) { + return caseWhenApplicable; + } + }; + + /** + * Applies this rule to {@code expr}, producing a semantically equivalent expression. + * + * @param + * the DBMS-specific expression class + * @param + * the DBMS-specific type domain + * @param t + * the transformer providing the DBMS-specific node factories + * @param expr + * the expression to transform + * + * @return a semantically equivalent expression + */ + abstract , T> E apply(EETTransformer t, E expr); + + /** + * Whether this rule preserves {@code expr}'s value in the given context. + * + * @param booleanContext + * whether {@code expr} is evaluated purely for its truth value (rules 1 and 2 are only applicable if + * this is the case) + * @param caseWhenApplicable + * whether {@code expr} may be wrapped in a CASE WHEN expression + * + * @return {@code true} if this rule preserves {@code expr}'s value in the given context + */ + abstract boolean isApplicable(boolean booleanContext, boolean caseWhenApplicable); + } + /** * Applies a randomly chosen applicable transformation rule to {@code expr}, returning a semantically equivalent - * expression. + * expression. When no rule is applicable, {@code expr} is returned unchanged (rule No. 7 of the EET paper). * * @param expr * the expression to transform * @param booleanContext - * whether {@code expr} is evaluated purely for its truth value; only in a boolean context may the - * determined-boolean rules (No. 1 and 2), which reduce the expression to a boolean value, be applied + * whether {@code expr} is evaluated purely for its truth value * * @return a semantically equivalent expression */ protected E applyRandomRule(E expr, boolean booleanContext) { - int rule; - if (booleanContext) { - // Rules No. 1-6 are all value-preserving in a boolean context. - rule = Randomly.fromOptions(1, 2, 3, 4, 5, 6); - } else { - if (!isCaseWhenApplicable(expr)) { - return expr; // rule No. 7: transform the expression to itself + boolean caseWhenApplicable = isCaseWhenApplicable(expr); + List applicableRules = new ArrayList<>(); + for (Rule rule : Rule.values()) { + if (rule.isApplicable(booleanContext, caseWhenApplicable)) { + applicableRules.add(rule); } - // In a scalar context only the CASE WHEN rules preserve the exact value and type. - rule = Randomly.fromOptions(3, 4, 5, 6); } - switch (rule) { - case 1: // expr => false_expr OR expr - return orExpr(falseExpr(), expr); - case 2: // expr => true_expr AND expr - return and(trueExpr(), expr); - case 3: // expr => CASE WHEN false_expr THEN rand_expr(type(expr)) ELSE expr END - return caseWhen(falseExpr(), randExprOfSameType(expr), expr); - case 4: // expr => CASE WHEN true_expr THEN expr ELSE rand_expr(type(expr)) END - return caseWhen(trueExpr(), expr, randExprOfSameType(expr)); - case 5: // expr => CASE WHEN rand_expr(boolean) THEN copy(expr) ELSE expr END - case 6: // expr => CASE WHEN rand_expr(boolean) THEN expr ELSE copy(expr) END - return caseWhen(generateBooleanExpression(), expr, expr); - // deep copy of expr is not needed, as the AST nodes are immutable anyway - default: - throw new AssertionError(rule); + if (applicableRules.isEmpty()) { + return expr; // rule 7 fallback: transform expression to itself } + return Randomly.fromList(applicableRules).apply(this, expr); } /**