Skip to content

Implement EET DML oracle (initially for DELETE statements only) - #1356

Merged
mrigger merged 4 commits into
mainfrom
feature/eet-delete
Aug 8, 2026
Merged

Implement EET DML oracle (initially for DELETE statements only)#1356
mrigger merged 4 commits into
mainfrom
feature/eet-delete

Conversation

@tlmorgan24

Copy link
Copy Markdown
Collaborator

Summary

The EET oracle previously only transformed SELECT queries and compared the two result sets. This PR adds a DML variant, EETDMLOracle, that transforms a DML statement's expressions and compares the two database states the original and transformed statements leave behind.

To observe the database state, a similar approach to the DQE oracle is used: add an auxiliary column that uniquely identifies each row, and run each statement inside a transaction that is rolled back. This way, the original and transformed statements can each be measured against the same starting state without permanently mutating the database.

Only DELETE is implemented in this PR, as it is the simplest (its only transformable site is the WHERE predicate, and row identity alone is a sufficient comparison surface). The generic infrastructure is designed so UPDATE and INSERT can be added later without rework. MySQL is included as a specific DBMS implementation.

The expression transformer (EETTransformer / MySQLEETTransformer) is statement-agnostic and remains unchanged.

Changes

Dedicated new oracle (common/oracle/EETDMLOracle.java)

check():

  1. Pick a single target table from the schema and confine the generator's column pool to it.
  2. Generate the WHERE predicate p; transform it in a boolean context (transformer.transform(p, true)).
  3. Assemble the original and transformed DELETE strings via the generator.
  4. ADD COLUMN rowid; then, inside a try whose finally drops the column, stamp every row once with a unique id.
  5. For each DELETE (original, then transformed): BEGIN; run it; snapshot the surviving rowids; ROLLBACK (rollback in a finally).
  6. Compare the two surviving-rowid sets; on mismatch throw an AssertionError whose message embeds both statement strings.

Error handling mirrors the SELECT oracle: an expected DBMS error (on the ExpectedErrors allow-list) aborts the iteration via IgnoreMeException, while an unexpected error surfaces as a reported bug.

Test case reduction will be implemented at a later stage.

New generator interface (common/gen/EETDMLGenerator.java)

A sibling of EETGenerator, supplying the DBMS-specific pieces the DML oracle needs.

  • Abstract methods: generateBooleanExpression() (the predicate), createTransformer(), setTablesAndColumns(...), asString(E) (render an expression), and stampRowIdsStatement(T) (assign every existing row a unique id).
  • Default methods: addRowIdColumnStatement / dropRowIdColumnStatement / selectRowIdsStatement / deleteStatement / beginTransactionStatement / rollbackTransactionStatement.

MySQL generator (mysql/gen/MySQLExpressionGenerator.java)

Now also implements EETDMLGenerator<MySQLExpression, MySQLTable, MySQLColumn>. It reuses the existing generateBooleanExpression / createTransformer / setTablesAndColumns and adds only asString and stampRowIdsStatement.

Oracle registration (mysql/MySQLOracleFactory.java)

New EET_DML enum entry that constructs an EETDMLOracle with existing expected errors plus a couple of extra ones encountered.

Table generation (mysql/gen/MySQLTableGenerator.java)

The ENGINE table option is forced to InnoDB if EET DML is being used (checked with usesEETDML() method). This is because it is the only transactional engine, and the test procedure relies on transactions.

Follow-ups (to be integrated into same oracle)

  • UPDATE support
  • INSERT support
  • Test case reduction, modelled on EETReproducer used for the SELECT oracle.

@tlmorgan24
tlmorgan24 requested a review from mrigger August 7, 2026 05:55
* @return the SQL statement
*/
default String addRowIdColumnStatement(T table) {
return "ALTER TABLE " + table.getName() + " ADD COLUMN " + ROW_ID_COLUMN + " VARCHAR(36)";

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.

Hmm, that part seems MySQL-specific. Can we make this code more general?

@tlmorgan24 tlmorgan24 Aug 8, 2026

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.

I have now addressed this in the latest commit pushed, let me know if it matches what you had in mind

*
* @return the SQL statement
*/
default String beginTransactionStatement() {

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.

Just to check, are there any database systems that SQLancer supports that support transactions, but not BEGIN and ROLLBACK? If not, we can keep it simple and omit these two methods.

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.

Most do support those statements, so in most cases they will just use the default method, but there are one or two exceptions. E.g. HSQLDB and H2 use START TRANSACTION instead of BEGIN.

@Override
public void check() throws SQLException {
List<T> tables = state.getSchema().getDatabaseTables();
if (tables.isEmpty()) {

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.

Are there possible situations where tables is empty?

@tlmorgan24 tlmorgan24 Aug 8, 2026

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.

Whether tables is empty depends on DBMS-specific code:

  • The generateDatabase method is DBMS-specific, and from what I've seen, the DBMSs do ensure at least one table is created.
  • If QPG is enabled, its mutateTables method may cause an attempted drop of the last remaining table in the schema. The drop method itself is DBMS-specific, and from what I've seen, the DBMS which support QPG do ensure they do not drop the last remaining table.

So, it seems like tables will never be empty at the moment, but that is only a consequence of DBMS-specific code. The non-emptiness of tables is not guaranteed by any shared/generic code, so if somebody adds extra DBMS support, they would risk breaking that guarantee unless they are careful. So, this emptiness check is guarding against that eventuality.

@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 88d717e into main Aug 8, 2026
24 of 25 checks passed
@mrigger
mrigger deleted the feature/eet-delete branch August 8, 2026 06:14
@mrigger

mrigger commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

By the way, as a minor suggestion, it would be fine to only summarize the high-level changes and rationale within a short paragraph or so. If the change summary is too detailed, it's easier to just read the code changes.

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.

2 participants