Skip to content

Implement EET oracle - #1349

Merged
mrigger merged 14 commits into
mainfrom
feature/eet-oracle
Jul 30, 2026
Merged

Implement EET oracle#1349
mrigger merged 14 commits into
mainfrom
feature/eet-oracle

Conversation

@tlmorgan24

Copy link
Copy Markdown
Collaborator

This PR implements the EET test oracle, based on:

Zu-Ming Jiang and Zhendong Su. Detecting Logic Bugs in Database Engines via Equivalent Expression Transformation.
18th USENIX Symposium on Operating Systems Design and Implementation (OSDI'24).

Transformation rules

The seven EET transformation rules are defined in the EETTransformer class (sqlancer/common/oracle/EETTransformer.java). The class' transformNode method may or may not (as per random choice) apply a transformation rule to an expression, unless the forceApply input of transformNode is set to true (in which case an applicable rule is definitely applied).

Which rules are applicable is determined by applyRandomRule's booleanContext input (which if true, enables rules 1-2) and CaseWhenApplicable(expr) method (which, if returning false, forces rule 7). If transformNode decides to apply a rule, then all applicable rules have an equal probability of being chosen. The exception is Rule 7, which is avoided if possible. I.e., rule 7 is only used if booleanContext is false and isCaseWhenApplicable(expr) is false. This is because Rule 7 is simple a "do nothing" rule, and the decision of whether to "do nothing" is already handled when deciding whether to apply a rule at all.

  • boolean context = true in the following contexts: the WHERE predicate, the operands of AND/OR/XOR, NOT, and switch-less CASE WHEN conditions.
  • boolean context = false in the following contexts: operands of comparisons, arithmetic, function arguments, CAST, BETWEEN, IN, and THEN/ELSE branches.

Architecture

The implementation follows the "common core + per-DBMS subclasses" pattern. As much as possible is DBMS-independent, while parts that must construct or walk a specific AST are DBMS-specific.

Common

File Role
common/oracle/EETOracle.java Orchestration + result comparison (paper §4 / Figure 4).
common/oracle/EETTransformer.java The 7 transformation rules of Table 2, expressed generically. Declares abstract factory methods (and, or (renamed to orExpr due to "short method name" checkstyle violation), not, isNull, isNotNull, caseWhen, …) that subclasses implement to construct DBMS-specific nodes, plus the type hooks inferType / generateExpressionOfType behind rand_expr(type(expr)) (see "type safety" below).
common/gen/EETGenerator.java Generator interface an EET-capable DBMS must implement.

EETOracle.check():

  1. Generates a random query (SELECT <fetch cols> FROM <tables> [joins] WHERE <predicate>).
  2. Runs it to get result set of original query.
  3. Transforms the fetch-column expressions and the WHERE predicate via EETTransformer.transform(...).
  4. Runs the transformed query to get result set of trasnformed query.
  5. Compares the two result sets (first column, as a multiset with a cardinality check, via ComparatorHelper); a mismatch throws an AssertionError reported by SQLancer as a bug.

It also installs a Reproducer (EETReproducer) that re-runs both the original and the transformed query against the current (reduced) database and re-checks the mismatch, which SQLancer uses to reduce the finding, similarly to other oracles (e.g. TLP WHERE).

DBMS-specific

MySQL is used as an example.

File Role
mysql/oracle/MySQLEETTransformer.java Recursive AST traversal (paper §4, "Expression Transformation"): rebuilds each node from transformed children and applies rules. Also implements the type hooks: coarse type inference over the MySQL AST and CAST-based typed generation.
mysql/ast/MySQLCastOperation.java CastType gained additional members, used only by EET's type-pinning casts (excluded from getRandom() to prevent affecting existing code that uses it).
mysql/gen/MySQLExpressionGenerator.java Now also implements EETGenerator; createTransformer() returns a MySQLEETTransformer.
mysql/ast/MySQLUnaryPrefixOperation.java Added a getOp() accessor so the transformer can rebuild the node (and pick the right child context for NOT).
mysql/MySQLOracleFactory.java Registers the EET oracle.

Expression transformation

The paper transforms expressions by traversing the query AST. SQLancer's AST has no generic child-rewrite facility, so MySQLEETTransformer walks the MySQL AST explicitly. For each node it:

  1. descends — transforms the pre-existing children and rebuilds the node (descend() / descendCase()), then
  2. wraps — with some probability applies a random rule to the rebuilt node (transformNode()).

This is a single bottom-up pass over the original AST. Auxiliary sub-expressions introduced by a rule (the random p in true_expr/false_expr, the random CASE predicate) are generated fresh with the query generator and are not themselves passed back through the transformer — only pre-existing nodes are visited. At the root a rule is always applied (forceApply), guaranteeing the transformed query actually differs from the original (unless only rule 7 is applicable). The generator reuses the columns available at that point in the query.

copy_expr / rand_expr and type safety

The paper notes (§3.2.2, footnote to Table 2) that the type of the expression placed in the dead branch of rules No. 3–4 must equal type(expr): although the dead branch is never evaluated, its static type participates in the DBMS's CASE result-type resolution, so a mismatch can alter the live branch's rendering (e.g. CASE WHEN FALSE THEN 1.5 ELSE 1 END might print 1.0 instead of 1).

EETTransformer therefore realises rand_expr(type(expr)) through two DBMS-specific methods:

  • inferType(expr) — returns the static type of expr in a DBMS-specific, possibly coarse type domain T, or
    null if it cannot be determined. The domain only needs to be precise enough that two expressions of the same T
    behave identically under CASE result-type resolution.
  • generateExpressionOfType(type) — generates a random expression of that static type.

When inferType returns null, the dead branch falls back to expr itself, which trivially has the correct type (degenerating rules No. 3–4 to 5–6). This fallback makes inference incrementally extensible: every classified node adds dead-branch diversity, while unclassified nodes do not prevent soundness.

For MySQL, whose expression generator is untyped, the type domain is MySQL's CAST target types (MySQLCastOperation.CastType: SIGNED, UNSIGNED, CHAR, FLOAT, DOUBLE, DECIMAL), and generateExpressionOfType pins the type of an arbitrary random expression by wrapping it in a CAST. DBMSs with typed expression generators (e.g. PostgreSQL) can implement inferType from their existing type tracking and generateExpressionOfType by delegating to generateExpression(type) directly, without the CAST trick.

Because the AST nodes are immutable and only rendered to SQL text, sharing the reference is safe and copy_expr (as used in the EET paper) is not required.

Running the oracle

Taking MySQL as an example:

mvn package -DskipTests
java -jar target/sqlancer-*.jar mysql --oracle EET

Scope and possible extensions

  • The common core (EETOracle, EETTransformer, EETGenerator) is DBMS-independent; new DBMSs can be added by providing their own EETTransformer subclass (implementing descend, the abstract factory methods, and inferType / generateExpressionOfType) and making their own expression generator implement EETGenerator.
  • Transformation currently targets SELECT queries (fetch columns + WHERE predicate). The paper also transforms expressions in other clauses and in DML (UPDATE/DELETE); these will be followed up in future PRs.
  • Reduction uses SQLancer's generic Reproducer, which reduces the database generation but leaves the transformed query unchanged. As the transformed query may become very complex with this oracle, automatic reduction of it may be a future PR.
  • FLOAT/DOUBLE/DECIMAL columns are created without an (M, D) precision/scale specifier while EET is active (see MySQLTableGenerator.optionallyAddPrecisionAndScale). This is because MySQLEETTransformer.inferColumnType maps these columns to the plain CastType.FLOAT/DOUBLE/DECIMAL targets, which only match the column's type when no (M, D) is present. This exclusion is scoped to EET only, preserving the (M, D) search space for the other oracles. A follow-up will be to track (M, D) through the schema and emit the exact precision/scale in the type-pinning CAST, at which point the restriction can be removed.

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
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<MySQLExpression>`.
…ate from DBMS-specific expression generators
@tlmorgan24
tlmorgan24 requested a review from mrigger July 28, 2026 04:24
@mrigger

mrigger commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Great, will review soon!

@JZuming, thanks to @tlmorgan24, we'll have EET support in SQLancer soon! If you have time and would like to, feel free to add any reviewing comments you might have as well.

@JZuming

JZuming commented Jul 28, 2026

Copy link
Copy Markdown

@tlmorgan24 @mrigger

Thanks for your effort and for letting me know. Excited to see that SQLancer will have EET support!

@mrigger mrigger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, very clean code! As a minor nitpick, could you have a look at the enum comment in the EETTransformer class?

// against a cached result set, which would be stale once statements have been removed.
originalResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors,
globalState);
if (transformedQueryString == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, but I'm also wondering whether the error-handling functionality is something that we could factor out and reuse among all the test oracles?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be good. I plan to implement testing of DML statements for EET first (initially without test case reduction). After that, I think the different requirements will be clearer for how best to factor out and reuse the reproducer/reducer logic across the whole of EET and the other oracles.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, having thought about it, I will do a PR for this refactor now, before any PR for implementing additional DML support

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel the logic here would be slightly cleaner if we changed the logic from integers to enums. We could have one enum item per rule, implement method applyRule and isApplicable in each enum item, and then apply a random rule by filtering the applicable rules and choosing a random one?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, should be cleaner now after the latest commit

}
if (Randomly.getBoolean() && !globalState.getDbmsSpecificOptions().getTestOracleFactory().stream()
.anyMatch(o -> o == MySQLOracleFactory.TLP_WHERE || o == MySQLOracleFactory.PQS
|| o == MySQLOracleFactory.DQP)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is some legacy logic, but if you want, you could try to improve it in a follow-up PR: checking for individual test oracles is not good OOP style. It would be better to have some method implemented in MySQLOracleFactory with each enum instance returning the appropriate value.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I will take note of this for a follow-up PR

@mrigger

mrigger commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Come to think of it, we should also update the README.md and ensure that the tests are run in the CI. We can also do that as a follow-up PR.

@tlmorgan24

Copy link
Copy Markdown
Collaborator Author

I have just pushed a commit for the EETTransformer rules refactor, and have added the other suggestions to my plans for upcoming PRs. Let me know if there is anything else!

@mrigger mrigger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@mrigger
mrigger merged commit 789c77b into main Jul 30, 2026
24 of 25 checks passed
@mrigger
mrigger deleted the feature/eet-oracle branch July 30, 2026 06:50
protected E applyRandomRule(E expr, boolean booleanContext) {
boolean caseWhenApplicable = isCaseWhenApplicable(expr);
List<Rule> applicableRules = new ArrayList<>();
for (Rule rule : Rule.values()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor suggestion for the future: using the Java Streams API, this could be written more concisely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants