Skip to content

Update from MySQL 8.4 to MySQL 9.7#1344

Open
tlmorgan24 wants to merge 16 commits into
mainfrom
update-mysql-9.7
Open

Update from MySQL 8.4 to MySQL 9.7#1344
tlmorgan24 wants to merge 16 commits into
mainfrom
update-mysql-9.7

Conversation

@tlmorgan24

@tlmorgan24 tlmorgan24 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR updates the supported MySQL version from 8.4 to 9.7.0, and the associated JDBC driver from mysql-connector-java 8.0.30 to mysql-connector-j 9.7.0.

  • Existing bugs in MySQLBugs have been reviewed and removed if they have been fixed since being originally reported.
  • Testing has been carried out using every oracle supported for MySQL.
  • Identified bugs have been simplified and flagged to the developers. The bug reports are linked to in MySQLBugs.java.

Below is an overview of the newly encountered errors and their fixes.

Test case 1

Reduced input:

CREATE TABLE t0(c0 FLOAT ZEROFILL , c1 FLOAT ZEROFILL  UNIQUE KEY);
REPLACE INTO t0(c0, c1) VALUES(454327608, ""), ("0b#vNkoT", -64193336), ("0.0", -1067204564);

Error:

Incorrect FLOAT value: '' for column 'c1' at row 1

Conclusion:

This is not a DBMS bug; rather, it is due to invalid insertion by SQLancer. A solution would be to modify the SQLancer generation phase to avoid placing strings in FLOAT fields. However (in keeping with SQLancer’s approach for similar cases), the implemented solution is to add the error as an ExpectedError to MySQLErrors.

Likewise for Incorrect DOUBLE value.

Test case 2

Reduced input:

CREATE TABLE t0(c0 DOUBLE ZEROFILL);
INSERT INTO t0(c0) VALUES(12345678);
-- Query 1, returns 0000000000000012345678:
SELECT t0.c0 AS ref0 FROM t0;
-- Query 2, returns 12345678:
SELECT t0.c0 AS ref0 FROM t0 UNION ALL SELECT t0.c0 AS ref0 FROM t0 WHERE (!t0.c0) UNION ALL SELECT t0.c0 AS ref0 FROM t0 WHERE (t0.c0 IS UNKNOWN);

Error:

TLPWhereOracle: The content of the result sets mismatch!

Conclusion:

It appears that the ZEROFILL property is lost when doing a union of multiple tables, causing the first query to be zero-filled and not the second.

However, this is expected behaviour as per MySQL documentation, which states, “The ZEROFILL attribute is ignored for columns involved in expressions or UNION queries.” (https://dev.mysql.com/doc/refman/9.7/en/numeric-type-attributes.html). Therefore, this is not a DBMS bug.

The implemented solution is to prevent use of the ZEROFILL attribute when testing with oracles for which inconsistent ZEROFILL may lead to flagging of errors (PQS, TLP, and DQP).

Note, the ZEROFILL attribute in MySQL is deprecated for numeric data types, so may be removed in a future version (https://dev.mysql.com/doc/refman/9.7/en/numeric-type-syntax.html). However, while it remains available, it will continue to be implemented in SQLancer.

Test case 3 (bug120710)

Reduced input (exhibit A):

CREATE TABLE t0(c0 DECIMAL UNIQUE);
INSERT INTO t0 VALUES(0);
INSERT INTO t0 VALUES(NULL);
-- Query 1, returns 2 rows (NULL and 0)
SELECT t0.c0 AS ref0 FROM t0;
-- Query 2, returns 3 rows (0, NULL and 0)
SELECT ALL t0.c0 AS ref0 FROM t0 WHERE (t0.c0) || (IFNULL(NULL, t0.c0)) UNION ALL SELECT ALL t0.c0 AS ref0 FROM t0 WHERE (! ((t0.c0) || (IFNULL(NULL, t0.c0)))) UNION ALL SELECT ALL t0.c0 AS ref0 FROM t0 WHERE ((t0.c0) || (IFNULL(NULL, t0.c0))) IS UNKNOWN;

Error:

TLPWhereOracle: The size of the result sets mismatch!

The bug appears when inserting a number that rounds to 0 followed by a NULL value (or the other way round) into a DECIMAL-type column. The identified bug-inducing test cases all involve the IFNULL function.

This bug is not present in MySQL 8.4.

Conclusion:

This is a DBMS bug. It has been simplified and reported to the developers (https://bugs.mysql.com/bug.php?id=120710).

Until the bug is fixed, the workaround implemented in SQLancer is to avoid the insertion of numbers that round to 0 into DECIMAL-type columns in MySQL.

Test case 4 (bug120711)

Reduced input:

CREATE TABLE t0(c0 INT);
CREATE INDEX b ON t0(c0);
INSERT INTO t0 VALUES(1);
-- Query 1, returns 1 rows (1)
SELECT ALL t0.c0 AS ref0 FROM t0;
-- Query 2, returns 2 rows (1 and 1)
SELECT ALL t0.c0 AS ref0 FROM t0 WHERE (IFNULL(NULL, 0.9)) IN (t0.c0) UNION ALL SELECT ALL t0.c0 AS ref0 FROM t0 WHERE (NOT ((IFNULL(NULL, 0.9)) IN (t0.c0))) UNION ALL SELECT t0.c0 AS ref0 FROM t0 WHERE ((IFNULL(NULL, 0.9)) IN (t0.c0)) IS NULL;

Error:

TLPWhereOracle: The size of the result sets mismatch!

The bug appears when creating an index on a column, then inserting a value in the range $[0.5,1.5)$ (i.e., which rounds to 1). The bug also appears the other way round (i.e., when first inserting and then creating an index). The identified bug-inducing test cases all involve the IFNULL function.

The bug is present for any of the integer data types (INTEGER, INT, SMALLINT, TINYINT, MEDIUMINT, BIGINT).

The bug is also present in MySQL 8.4.

Conclusion:

This is a DBMS bug. It has been simplified and reported to the developers (https://bugs.mysql.com/bug.php?id=120711).

Until the bug is fixed, the workaround implemented in SQLancer is to avoid the insertion of values which round to 1 into integer-type columns. This approach restricts the search space less than preventing index creation.

Test case 5

Error:

All runs using CERTOracle throw SQLException: Column Index out of range, 10 > 1.

Conclusion:

This is not a DBMS bug; rather, it is due to an assumption of SQLancer no longer being valid. As of MySQL 9.5.0, the default explain format changed from TRADITIONAL to TREE (https://dev.mysql.com/doc/relnotes/mysql/9.7/en/news-9-5-0.html). This breaks the parser of CERTOracle’s EXPLAIN queries.

The implemented solution is to modify MySQLExpressionGenerator's implementation of the generateExplainQuery method to specify FORMAT=TRADITIONAL in the generated statement.

Test case 6 (bug120712)

Input:

CREATE TABLE t0(c0 INT);
INSERT INTO t0 VALUES(NULL);
CREATE INDEX a ON t0(c0);
INSERT INTO t0 VALUES(NULL);
ANALYZE TABLE t0 UPDATE HISTOGRAM ON c0;
-- Query 1, estimates 1 row
EXPLAIN FORMAT=TRADITIONAL SELECT ALL t0.c0 AS ref0 FROM t0;
-- Query 2, estimates 2 rows
EXPLAIN FORMAT=TRADITIONAL SELECT ALL t0.c0 AS ref0 FROM t0 WHERE (t0.c0) IS UNKNOWN;

Error:

CERTOracle: Inconsistent result for query. The more restrictive query (query 2) is estimated to have a higher row count than the more relaxed query (query 1).

The bug occurs when an index is created between two NULL inserts. The issue is not specific to INT columns (it is still present when the column is changed to FLOAT).

Conclusion:

This is a DBMS bug. It has been simplified and reported to the developers (https://bugs.mysql.com/bug.php?id=120712).

This must be worked around in SQLancer until the bug is fixed. We would ideally either prevent creating indices on columns which already have a NULL value, or prevent inserting a NULL value into columns which already have an index.

However, both of these options would involve a large refactor. Implementing the former would require tracking whether each column has had a NULL inserted or not; the latter would require tracking whether each column is currently an index or not. Alternatively, one could query for nulls each time a CREATE INDEX statement may be generated, or read schema information each time a NULL may be generated (respectively). However, this would be too computationally expensive.

The simpler options are to either prevent index creation entirely or prevent NULL inserts entirely. Unfortunately, either of these would restrict a lot of the search space (as many bugs are discovered through inserting NULLs or creating indices). As this bug is related specifically to the CERT oracle and the EXPLAIN statement, index creation is particularly relevant, as it is more closely tied to the optimiser’s row estimation logic. Therefore, it was chosen to prevent NULL inserts. The workaround is only implemented when the CERT oracle is used (therefore, the search space remains relaxed for other oracles).

However, preventing NULL inserts poses difficulties when using C-Reduce to reduce other bug-inducing CERT test cases. C-Reduce may, for example, reduce INSERT INTO t0 VALUES(123); to INSERT INTO t0 VALUES();, which is effectively inserting a NULL value. Therefore, though the bug is avoided by SQLancer, it may be encountered during C-Reduce’s reduction of a different bug, causing the process to become side-tracked from the actual bug of interest. Those using C-Reduce should bear this in mind when attempting to reduce MySQL bugs flagged by the CERT oracle.

@tlmorgan24 tlmorgan24 requested a review from mrigger June 1, 2026 18:56
@tlmorgan24 tlmorgan24 self-assigned this Jun 1, 2026
@mrigger

mrigger commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Great start! For test case 2, do you think we could still report it and then disable its generation until the bug is resolved, despite being deprecated? It is a bug after all. If you prefer not to, that's also fine.

For test case 3, I guess you could go ahead and report it?

For test case 4, disabling indices altogether would significantly restrict the search space as you pointed out. Perhaps, could we instead insert only integer values or so for now?

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 guess this was accidentally checked in?

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, I can remove the edits to this file

@tlmorgan24

tlmorgan24 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

Hi Manuel, I have pushed updates to this branch and updated the PR description. This PR is now ready for full review and merging once any remaining issues have been addressed. I have organised it as multiple commits which can be squashed as desired.

Regarding your comments:

  • For test case 2, I did further reading and found that the behaviour is expected (see update to the PR description), so I will not be reporting this as a bug. I have made adjustments so that the oracles that were flagging the behaviour as an error are no longer affected.
  • For test case 4, in the end I have achieved an even tighter workaround than the one you suggested, by specifically avoiding the insertion of numbers in the range [0.5,1.5) into INT columns.
  • I have also achieved a tighter workaround for test case 3 (avoid insertion of numbers that round to 0 into DECIMAL columns).

sb.append(MySQLVisitor.asString(gen.generateConstant()));
MySQLExpression constExpr;
// loop to regenerate until expression is valid (for bug workarounds)
while (true) {

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.

as a kind of sanity check, could we add a loop counter and add a named constant as a threshold? like this, in case we change the code later on and, due to a bug, we have an infinite loop, we can detect that case by throwing an assertion error if the threshold is exceeded.

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 implemented this

sb.append(" UNSIGNED");
}
if (!globalState.usesPQS() && Randomly.getBoolean()) {
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.

I guess here, the bug ID is missing?

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.

This is in response to test case 2, which is expected MySQL behaviour. The listed oracles would flag that expected behaviour as a bug, but it is not actually a bug. So, I have provided this workaround without assigning any bug ID to it. Does that sound suitable?

@mrigger

mrigger commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

It seems that the general tests are failing due to a formatting error. You can run mvn -B verify -DskipTests=true locally to check and then fix the formatting using mvn formatter:format.

@tlmorgan24

Copy link
Copy Markdown
Collaborator Author

I rebased to remove the initial edits to .settings/org.eclipse.jdt.core.prefs, did a new commit to add the loop counter, and reformatted to pass the general tests

@tlmorgan24

Copy link
Copy Markdown
Collaborator Author

I will investigate why the MySQL CI is failing

…MIT and known SELECT/DML error discrepancies

Appends rowId as an ORDER BY tiebreaker to eliminate non-determinism when user columns contain duplicate values (e.g. NULLs) under LIMIT. Introduces isKnownSelectDMLDiscrepancy to suppress false positives from error codes that MySQL legitimately raises in UPDATE/DELETE but not SELECT (or vice versa) due to differing execution paths: WHERE-clause type coercion (1292, 1366), functional index maintenance (1030, 3751), and range optimizer memory limits (3170).
@tlmorgan24 tlmorgan24 force-pushed the update-mysql-9.7 branch 2 times, most recently from 903adcc to f952b25 Compare June 18, 2026 07:08
@tlmorgan24

tlmorgan24 commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

After my latest commit, the MySQL CI is now passing. Summary of the failing test cases identified and fixed:

Test case 7

Reduced input (indicative):

CREATE TABLE t0(c0 DECIMAL ZEROFILL UNIQUE);
INSERT INTO t0(c0) VALUES(NULL), (NULL), (1284856216), ...;
SELECT rowId FROM t0 WHERE <cond> ORDER BY t0.c0 LIMIT 7;
UPDATE t0 SET updated=1 WHERE <cond> ORDER BY t0.c0 LIMIT 7;
DELETE FROM t0 WHERE <cond> ORDER BY t0.c0 LIMIT 7;

Error:

SELECT accessed different rows from UPDATE. UPDATE accessed different rows from DELETE.

Conclusion:

This is not a DBMS bug. When ORDER BY contains columns with duplicate values (here, many NULLs in c0) and LIMIT is applied, the tie-breaking between equally-sorted rows is non-deterministic in MySQL. The SELECT and UPDATE may traverse rows in a different physical order, selecting a different subset of NULLs. The fix is to append the rowId auxiliary column (unique per row) to the end of every generated ORDER BY clause, making the sort fully deterministic across all three statements.


Test case 8

Reduced input (indicative):

CREATE TABLE t0(c0 DECIMAL);
INSERT INTO t0(c0) VALUES(...);
SELECT rowId FROM t0 WHERE (t0.c0) NOT IN ("yS", NULL, ...);
UPDATE t0 SET updated=1 WHERE (t0.c0) NOT IN ("yS", NULL, ...);
DELETE FROM t0 WHERE (t0.c0) NOT IN ("yS", NULL, ...);

Related errors:

  • SELECT has different errors from UPDATE — UPDATE/DELETE raise 1292 (ERROR) + 1366 (WARNING) from type coercion, SELECT does not.
  • SELECT has errors, but UPDATE does not — the mirror case: SELECT raises 1292 (WARNING) from the same coercion, UPDATE/DELETE do not.
  • UPDATE accessed non-empty rows when specific errors happen — a false positive introduced by an initial overly-broad fix that added 1292/1366 to getUpdateSpecificErrorCodes().

Conclusion:

This is not a DBMS bug. MySQL evaluates type-incompatible literals in WHERE clauses (e.g. string "yS" against a DECIMAL column) differently depending on statement type: SELECT may short-circuit on a NULL in the IN list and not coerce the literal at all, while UPDATE/DELETE evaluate it against each row raising 1292 and 1366. The reverse can also occur. In all cases the accessed rows are identical across the three statements.

The initial fix of adding 1292 and 1366 to getUpdateSpecificErrorCodes() was wrong: those codes can appear in all three statements consistently (with rows accessed), causing a false positive. The correct fix introduces a helper isKnownSelectDMLDiscrepancy that skips these codes only in the "DML errors must appear in SELECT errors" check, leaving the row comparison intact. The same helper is also applied in the updateResult.hasEmptyErrors() / deleteResult.hasEmptyErrors() branches to handle the SELECT-raises-but-DML-doesn't direction.


Test case 9

Reduced input (indicative):

CREATE TABLE t0(c0 INT, c1 DECIMAL UNSIGNED ZEROFILL);
CREATE INDEX i2 ON t0(LEAST(t0.c1, ...), (... XOR ...));
INSERT INTO t0 VALUES(...);
SELECT rowId FROM t0 WHERE <cond>;
UPDATE t0 SET updated=1 WHERE <cond>;
DELETE FROM t0 WHERE <cond>;

Error:

SELECT has different errors from UPDATE. SELECT has different errors from DELETE.

Conclusion:

This is not a DBMS bug. The table has a functional index whose expression uses && (deprecated AND syntax). UPDATE and DELETE must maintain this index during writes, which triggers error 1030 (Got error 1 - 'Operation not permitted' from storage engine) and error 3751 (Data truncated for functional index). SELECT is read-only and never writes to the index, so it cannot produce these errors. Both 1030 and 3751 are added to isKnownSelectDMLDiscrepancy.


Test case 10

Reduced input (indicative):

CREATE TABLE t0(c0 MEDIUMTEXT);
CREATE INDEX i72 ON t0(c0(4));
INSERT INTO t0(c0) VALUES(...);
SELECT rowId FROM t0 WHERE t0.c0;
UPDATE t0 SET updated=1 WHERE t0.c0;
DELETE FROM t0 WHERE t0.c0;

Error:

SELECT has different errors from UPDATE. SELECT has different errors from DELETE.

Conclusion:

This is not a DBMS bug. MySQL's range optimiser has a configurable memory budget (range_optimizer_max_mem_size). When the budget is exceeded during DML, MySQL raises warning 3170 (Memory capacity exceeded. Range optimization was not done) and falls back to a full table scan. The same budget may not be exceeded during SELECT (which has a lighter memory footprint). The fallback plan still evaluates the WHERE predicate correctly, so all three statements access the same rows. Error 3170 is added to isKnownSelectDMLDiscrepancy.

*/
private static boolean isKnownSelectDMLDiscrepancy(SQLQueryError error) {
switch (error.getCode()) {
case 1030:

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.

As a nitpick, I think this would be more readable and easier to maintain by making these values part of an enum, so you can attach the individual comments above to each enum value.

@mrigger

mrigger commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Thanks a lot! Could you please take a look at the small nitpick? After that, we can merge the PR.

…nerator

MySQL rejects CREATE INDEX with a prefix key part (e.g. c0(3)) on a
column that participates in PARTITION BY KEY(). MySQLIndexGenerator
generated these without checking partition membership and did not
include this error in ExpectedErrors, causing checkException to
escalate it to a fatal AssertionError. Adds the error substring to
the expected errors list.
@tlmorgan24

tlmorgan24 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

I have made the improvement in MySQLDQEOracle by refactoring into an enum. Also discovered an unrelated issue with MySQLIndexGenerator which was rare enough to have not surfaced until now, and fixed that (detailed below). CI is passing again (apart from the tests for some other DBMSs, which were already failing before this PR).

Test case 11

Reduced input (indicative):

CREATE TABLE t0(c0 VARCHAR(500)) ENGINE = InnoDB PARTITION BY KEY (c0);
CREATE INDEX i1 USING BTREE ON t0(c0(3)) VISIBLE;

Error:

java.sql.SQLException: Column 'pqsdb0.t0.c0' having prefix key part 'c0(3)' in the PARTITION BY KEY() clause is not supported.

Conclusion:

This is not a DBMS bug. MySQL does not permit prefix key parts (e.g. c0(3)) on columns that participate in a PARTITION BY KEY() clause. MySQLIndexGenerator generates prefix lengths for VARCHAR columns without checking whether the target table is partitioned by key on that column, and this error was not included in ExpectedErrors. The fix is to add "in the PARTITION BY KEY() clause is not supported" to the expected errors list in MySQLIndexGenerator.create().

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants