Implement EET oracle - #1349
Conversation
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
…arlier made to TLP WHERE
|
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. |
|
Thanks for your effort and for letting me know. Excited to see that SQLancer will have EET support! |
mrigger
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Thanks, I will take note of this for a follow-up PR
|
Come to think of it, we should also update the |
|
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! |
| protected E applyRandomRule(E expr, boolean booleanContext) { | ||
| boolean caseWhenApplicable = isCaseWhenApplicable(expr); | ||
| List<Rule> applicableRules = new ArrayList<>(); | ||
| for (Rule rule : Rule.values()) { |
There was a problem hiding this comment.
One minor suggestion for the future: using the Java Streams API, this could be written more concisely.
This PR implements the EET test oracle, based on:
Transformation rules
The seven EET transformation rules are defined in the
EETTransformerclass (sqlancer/common/oracle/EETTransformer.java). The class'transformNodemethod may or may not (as per random choice) apply a transformation rule to an expression, unless theforceApplyinput oftransformNodeis set totrue(in which case an applicable rule is definitely applied).Which rules are applicable is determined by
applyRandomRule'sbooleanContextinput (which iftrue, enables rules 1-2) andCaseWhenApplicable(expr)method (which, if returningfalse, forces rule 7). IftransformNodedecides 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 ifbooleanContextisfalseandisCaseWhenApplicable(expr)isfalse. 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.truein the following contexts: the WHERE predicate, the operands ofAND/OR/XOR,NOT, and switch-lessCASE WHENconditions.falsein the following contexts: operands of comparisons, arithmetic, function arguments,CAST,BETWEEN,IN, andTHEN/ELSEbranches.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
common/oracle/EETOracle.javacommon/oracle/EETTransformer.javaand,or(renamed toorExprdue to "short method name" checkstyle violation),not,isNull,isNotNull,caseWhen, …) that subclasses implement to construct DBMS-specific nodes, plus the type hooksinferType/generateExpressionOfTypebehindrand_expr(type(expr))(see "type safety" below).common/gen/EETGenerator.javaEETOracle.check():SELECT <fetch cols> FROM <tables> [joins] WHERE <predicate>).EETTransformer.transform(...).ComparatorHelper); a mismatch throws anAssertionErrorreported 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.
mysql/oracle/MySQLEETTransformer.javaCAST-based typed generation.mysql/ast/MySQLCastOperation.javaCastTypegained additional members, used only by EET's type-pinning casts (excluded fromgetRandom()to prevent affecting existing code that uses it).mysql/gen/MySQLExpressionGenerator.javaEETGenerator;createTransformer()returns aMySQLEETTransformer.mysql/ast/MySQLUnaryPrefixOperation.javagetOp()accessor so the transformer can rebuild the node (and pick the right child context forNOT).mysql/MySQLOracleFactory.javaEEToracle.Expression transformation
The paper transforms expressions by traversing the query AST. SQLancer's AST has no generic child-rewrite facility, so
MySQLEETTransformerwalks the MySQL AST explicitly. For each node it:descend()/descendCase()), thentransformNode()).This is a single bottom-up pass over the original AST. Auxiliary sub-expressions introduced by a rule (the random
pintrue_expr/false_expr, the randomCASEpredicate) 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_exprand type safetyThe 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'sCASEresult-type resolution, so a mismatch can alter the live branch's rendering (e.g.CASE WHEN FALSE THEN 1.5 ELSE 1 ENDmight print1.0instead of1).EETTransformertherefore realisesrand_expr(type(expr))through two DBMS-specific methods:inferType(expr)— returns the static type ofexprin a DBMS-specific, possibly coarse type domainT, ornullif it cannot be determined. The domain only needs to be precise enough that two expressions of the sameTbehave identically under
CASEresult-type resolution.generateExpressionOfType(type)— generates a random expression of that static type.When
inferTypereturnsnull, the dead branch falls back toexpritself, 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
CASTtarget types (MySQLCastOperation.CastType:SIGNED,UNSIGNED,CHAR,FLOAT,DOUBLE,DECIMAL), andgenerateExpressionOfTypepins the type of an arbitrary random expression by wrapping it in aCAST. DBMSs with typed expression generators (e.g. PostgreSQL) can implementinferTypefrom their existing type tracking andgenerateExpressionOfTypeby delegating togenerateExpression(type)directly, without theCASTtrick.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:
Scope and possible extensions
EETOracle,EETTransformer,EETGenerator) is DBMS-independent; new DBMSs can be added by providing their ownEETTransformersubclass (implementingdescend, the abstract factory methods, andinferType/generateExpressionOfType) and making their own expression generator implementEETGenerator.SELECTqueries (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.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/DECIMALcolumns are created without an(M, D)precision/scale specifier while EET is active (seeMySQLTableGenerator.optionallyAddPrecisionAndScale). This is becauseMySQLEETTransformer.inferColumnTypemaps these columns to the plainCastType.FLOAT/DOUBLE/DECIMALtargets, 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-pinningCAST, at which point the restriction can be removed.