diff --git a/databases/mariadb/matrix.gradle b/databases/mariadb/matrix.gradle index f66f5cacb300..b72fdee03569 100644 --- a/databases/mariadb/matrix.gradle +++ b/databases/mariadb/matrix.gradle @@ -4,4 +4,4 @@ * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or . */ -jdbcDependency 'org.mariadb.jdbc:mariadb-java-client:3.4.0' +jdbcDependency 'org.mariadb.jdbc:mariadb-java-client:3.5.1' diff --git a/docker_db.sh b/docker_db.sh index 4715eaa38b1d..877e5df7b7fe 100755 --- a/docker_db.sh +++ b/docker_db.sh @@ -92,7 +92,7 @@ mysql_8_2() { } mariadb() { - mariadb_11_4 + mariadb_11_7 } mariadb_wait_until_start() @@ -138,6 +138,12 @@ mariadb_11_4() { mariadb_wait_until_start } +mariadb_11_7() { + $CONTAINER_CLI rm -f mariadb || true + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_11_7:-docker.io/mariadb:11.7-rc} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + mariadb_wait_until_start +} + mariadb_verylatest() { $CONTAINER_CLI rm -f mariadb || true $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_VERYLATEST:-quay.io/mariadb-foundation/mariadb-devel:verylatest} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 @@ -992,6 +998,7 @@ if [ -z ${1} ]; then echo -e "\thana" echo -e "\tmariadb" echo -e "\tmariadb_verylatest" + echo -e "\tmariadb_11_7" echo -e "\tmariadb_11_4" echo -e "\tmariadb_11_1" echo -e "\tmariadb_10_11" diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 88f292f62db6..377d6f75cb62 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -604,10 +604,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio BasicTypeRegistry basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry(); - functionRegistry.noArgsBuilder( "localtime" ) - .setInvariantType(basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP )) - .setUseParenthesesWhenNoArgs( false ) - .register(); + // pi() produces a value with 7 digits unless we're explicit if ( getMySQLVersion().isSameOrAfter( 8 ) ) { functionRegistry.patternDescriptorBuilder( "pi", "cast(pi() as double)" ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index 75b9335dafbc..6dc4ce2bda91 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -9,6 +9,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; +import org.hibernate.PessimisticLockException; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.function.CommonFunctionFactory; @@ -21,6 +22,10 @@ import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.LockTimeoutException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -37,6 +42,7 @@ import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; +import static org.hibernate.internal.util.JdbcExceptionHelper.extractSqlState; import static org.hibernate.query.sqm.produce.function.FunctionParameterType.NUMERIC; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.OTHER; @@ -281,4 +287,44 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D public String getDual() { return "dual"; } + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + switch ( sqlException.getErrorCode() ) { + // If @@innodb_snapshot_isolation is set (default since 11.6.2), + // if an attempt to acquire a lock on a record that does not exist in the current read view is made, + // an error DB_RECORD_CHANGED will be raised. + case 1020: + return new LockAcquisitionException( message, sqlException, sql ); + case 1205: + case 3572: + return new PessimisticLockException( message, sqlException, sql ); + case 1207: + case 1206: + return new LockAcquisitionException( message, sqlException, sql ); + case 1062: + // Unique constraint violation + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); + } + + final String sqlState = extractSqlState( sqlException ); + if ( sqlState != null ) { + switch ( sqlState ) { + case "41000": + return new LockTimeoutException( message, sqlException, sql ); + case "40001": + return new LockAcquisitionException( message, sqlException, sql ); + } + } + + return null; + }; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 44102a28ad72..2473fc233045 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -654,11 +654,6 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry(); - functionRegistry.noArgsBuilder( "localtime" ) - .setInvariantType(basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP )) - .setUseParenthesesWhenNoArgs( false ) - .register(); - // pi() produces a value with 7 digits unless we're explicit functionRegistry.patternDescriptorBuilder( "pi", "cast(pi() as double)" ) .setInvariantType( basicTypeRegistry.resolve( StandardBasicTypes.DOUBLE ) ) diff --git a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java index 081e60f283b3..3ff9013eb131 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java @@ -678,8 +678,9 @@ public class SqlTypes { /** - * A type code representing an {@code embedding vector} type for databases like - * {@link org.hibernate.dialect.PostgreSQLDialect PostgreSQL} and {@link org.hibernate.dialect.OracleDialect Oracle 23ai}. + * A type code representing an {@code embedding vector} type for databases + * like {@link org.hibernate.dialect.PostgreSQLDialect PostgreSQL}, + * {@link org.hibernate.dialect.OracleDialect Oracle 23ai} and {@link org.hibernate.dialect.MariaDBDialect MariaDB}. * An embedding vector essentially is a {@code float[]} with a fixed size. * * @since 6.4 diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java index 17708e7471d0..caebfc592047 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java @@ -17,6 +17,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; import org.junit.Test; @@ -72,7 +73,7 @@ public void testBatchAndOptimisticLocking() { } ); try { - inTransaction( (session) -> { + inTransaction( session -> { List persons = session .createSelectionQuery( "select p from Person p", Person.class ) .getResultList(); @@ -109,10 +110,18 @@ public void testBatchAndOptimisticLocking() { ); } else { - assertEquals( - "Batch update returned unexpected row count from update [1]; actual row count: 0; expected: 1; statement executed: update Person set name=?,version=? where id=? and version=?", - expected.getMessage() - ); + if ( getDialect() instanceof MariaDBDialect && getDialect().getVersion().isAfter( 11, 6, 2 )) { + assertTrue( + expected.getMessage() + .contains( "Record has changed since last read in table 'Person'" ) + ); + } else { + assertTrue( + expected.getMessage() + .startsWith( + "Batch update returned unexpected row count from update [1]; actual row count: 0; expected: 1; statement executed: update Person set name=?,version=? where id=? and version=?" ) + ); + } } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java index bd3a24d38c76..d52704e299f0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java @@ -13,6 +13,7 @@ import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.exception.SQLGrammarException; @@ -106,6 +107,7 @@ public void testStaleVersionedInstanceFoundInQueryResult() { @Test @SkipForDialect(dialectClass = CockroachDialect.class, reason = "Cockroach uses SERIALIZABLE by default and fails to acquire a write lock after a TX in between committed changes to a row") + @SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public void testStaleVersionedInstanceFoundOnLock() { if ( !readCommittedIsolationMaintained( "repeatable read tests" ) ) { return; @@ -228,6 +230,7 @@ public void testStaleNonVersionedInstanceFoundInQueryResult() { @Test @SkipForDialect(dialectClass = CockroachDialect.class, reason = "Cockroach uses SERIALIZABLE by default and fails to acquire a write lock after a TX in between committed changes to a row") + @SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public void testStaleNonVersionedInstanceFoundOnLock() { if ( !readCommittedIsolationMaintained( "repeatable read tests" ) ) { return; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java index 62763467a42e..65c7c3a61036 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java @@ -6,7 +6,7 @@ import org.hibernate.LockMode; import org.hibernate.dialect.CockroachDialect; - +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; @@ -26,6 +26,7 @@ @SessionFactory @JiraKey("HHH-16461") @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB uses SERIALIZABLE isolation, and does not support this") +@SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public class OptimisticAndPessimisticLockTest { public Stream pessimisticLockModes() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java index 006865240519..d81da65aa971 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java @@ -12,8 +12,9 @@ import jakarta.persistence.Version; import org.hibernate.annotations.OptimisticLock; import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; -import org.hibernate.testing.SkipForDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; @@ -31,7 +32,8 @@ protected Class[] getAnnotatedClasses() { } @Test - @SkipForDialect(value = CockroachDialect.class, comment = "Fails at SERIALIZABLE isolation") + @SkipForDialect(dialectClass = CockroachDialect.class, reason = "Fails at SERIALIZABLE isolation") + @SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public void test() { doInJPA(this::entityManagerFactory, entityManager -> { Phone phone = new Phone(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java index 30096e56aaa6..6782f11ad60b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java @@ -15,6 +15,7 @@ import org.hibernate.StaleStateException; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.testing.orm.junit.DialectFeatureChecks; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import static org.hibernate.testing.orm.junit.DialectContext.getDialect; import static org.junit.jupiter.api.Assertions.fail; /** @@ -191,8 +193,9 @@ else if ( dialect instanceof CockroachDialect && ( (JDBCException) cause ).getSQ "40001" ) ) { // CockroachDB always runs in SERIALIZABLE isolation, and uses SQL state 40001 to indicate // serialization failure. - } - else { + } else if (dialect instanceof MariaDBDialect && getDialect().getVersion().isAfter( 11, 6, 2 )) { + // Mariadb snapshot_isolation throws error + } else { throw e; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java index 372d21dccceb..58681dfc1069 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.temporal; +import java.sql.Time; import java.sql.Timestamp; import org.hibernate.dialect.MySQLDialect; @@ -18,6 +19,7 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.descriptor.java.JdbcTimeJavaType; import org.junit.jupiter.api.Test; import static org.junit.Assert.assertEquals; @@ -48,11 +50,13 @@ public void testTimeStampFunctions(SessionFactoryScope scope) { ); Object[] oArray = (Object[]) q.uniqueResult(); for ( Object o : oArray ) { - ( (Timestamp) o ).setNanos( 0 ); + if ( o instanceof Timestamp) { + ( (Timestamp) o ).setNanos( 0 ); + } } final Timestamp now = (Timestamp) oArray[0]; assertEquals( now, oArray[1] ); - assertEquals( now, oArray[2] ); + assertTrue( JdbcTimeJavaType.INSTANCE.areEqual( new Time( now.getTime() ), (Time) oArray[2] ) ); assertEquals( now, oArray[3] ); assertTrue( now.compareTo( (Timestamp) oArray[4] ) <= 0 ); }