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 2c5db6f4334a..40a51496d6f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -35,6 +35,7 @@ import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.query.sqm.CastType; import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstTranslator; @@ -42,6 +43,8 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorMariaDBDatabaseImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.SqlTypes; @@ -421,4 +424,10 @@ public boolean supportsWithClauseInSubquery() { return false; } + @Override + public MutationOperation createOptionalTableUpdateOperation(EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) { + final MariaDBSqlAstTranslator translator = new MariaDBSqlAstTranslator<>( factory, optionalTableUpdate, MariaDBDialect.this ); + return translator.createMergeOperation( optionalTableUpdate ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDeleteOrUpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDeleteOrUpsertOperation.java new file mode 100644 index 000000000000..0e7e6860857c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDeleteOrUpsertOperation.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + + +import org.hibernate.StaleStateException; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.jdbc.Expectation; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.sql.model.ValuesAnalysis; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.DeleteOrUpsertOperation; +import org.hibernate.sql.model.jdbc.UpsertOperation; + +import java.sql.PreparedStatement; + + +/** + * @author Jan Schatteman + */ +public class MySQLDeleteOrUpsertOperation extends DeleteOrUpsertOperation { + + private Expectation customExpectation; + + public MySQLDeleteOrUpsertOperation(EntityMutationTarget mutationTarget, EntityTableMapping tableMapping, UpsertOperation upsertOperation, OptionalTableUpdate optionalTableUpdate) { + super( mutationTarget, tableMapping, upsertOperation, optionalTableUpdate ); + } + + @Override + public void performMutation(JdbcValueBindings jdbcValueBindings, ValuesAnalysis valuesAnalysis, SharedSessionContractImplementor session) { + customExpectation = new MySQLRowCountExpectation(); + super.performMutation( jdbcValueBindings, valuesAnalysis, session ); + } + + @Override + protected Expectation getExpectation() { + return customExpectation; + } + + private class MySQLRowCountExpectation implements Expectation { + @Override + public final void verifyOutcome(int rowCount, PreparedStatement statement, int batchPosition, String sql) { + if ( getOptionalTableUpdate().getNumberOfOptimisticLockBindings() > 0 && rowCount == 0) { + throw new StaleStateException( + "Unexpected row count" + + " (for a versioned entity the expected row count should != 0)" + + " [" + sql + "]" + ); + } + if ( rowCount > 2 ) { + throw new StaleStateException( + "Unexpected row count" + + " (the expected row count for an ON DUPLICATE KEY UPDATE statement should be either 0, 1 or 2 )" + + " [" + sql + "]" + ); + } + } + } + +} 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 7e5c119076f6..5f15a8ef7941 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -45,6 +45,7 @@ import org.hibernate.mapping.CheckConstraint; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.query.common.TemporalUnit; import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; @@ -63,6 +64,8 @@ import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.type.BasicTypeRegistry; import org.hibernate.type.NullType; import org.hibernate.type.SqlTypes; @@ -1668,4 +1671,10 @@ public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } + @Override + public MutationOperation createOptionalTableUpdateOperation(EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) { + final MySQLSqlAstTranslator translator = new MySQLSqlAstTranslator<>( factory, optionalTableUpdate, MySQLDialect.this ); + return translator.createMergeOperation( optionalTableUpdate ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java index 481c9445ea81..9316b822312b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java @@ -14,7 +14,6 @@ import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; -import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; @@ -44,7 +43,7 @@ * * @author Christian Beikov */ -public class MariaDBSqlAstTranslator extends AbstractSqlAstTranslator { +public class MariaDBSqlAstTranslator extends SqlAstTranslatorWithOnDuplicateKeyUpdate { private final MariaDBDialect dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java index f4d714987de6..c53283ca195d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java @@ -12,7 +12,6 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; -import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; @@ -46,7 +45,7 @@ * * @author Christian Beikov */ -public class MySQLSqlAstTranslator extends AbstractSqlAstTranslator { +public class MySQLSqlAstTranslator extends SqlAstTranslatorWithOnDuplicateKeyUpdate { /** * On MySQL, 1GB or {@code 2^30 - 1} is the maximum size that a char value can be casted. diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SqlAstTranslatorWithOnDuplicateKeyUpdate.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SqlAstTranslatorWithOnDuplicateKeyUpdate.java new file mode 100644 index 000000000000..911589529a82 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SqlAstTranslatorWithOnDuplicateKeyUpdate.java @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.sql.ast; + + +import org.hibernate.dialect.MySQLDeleteOrUpsertOperation; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.sql.ast.spi.SqlAstTranslatorWithUpsert; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.UpsertOperation; + +import java.util.List; + +/** + * @author Jan Schatteman + */ +public class SqlAstTranslatorWithOnDuplicateKeyUpdate extends SqlAstTranslatorWithUpsert { + + public SqlAstTranslatorWithOnDuplicateKeyUpdate(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + } + + @Override + public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableUpdate) { + renderUpsertStatement( optionalTableUpdate ); + + final UpsertOperation upsertOperation = new UpsertOperation( + optionalTableUpdate.getMutatingTable().getTableMapping(), + optionalTableUpdate.getMutationTarget(), + getSql(), + getParameterBinders() + ); + + return new MySQLDeleteOrUpsertOperation( + optionalTableUpdate.getMutationTarget(), + (EntityTableMapping) optionalTableUpdate.getMutatingTable().getTableMapping(), + upsertOperation, + optionalTableUpdate + ); + } + + @Override + protected void renderUpsertStatement(OptionalTableUpdate optionalTableUpdate) { +/* + Template: (for an entity with @Version, and without using values() - but this might require changes in parameter binding) + INSERT INTO employees (id, name, salary, version) + VALUES (?, ?, ?, ?) AS t + ON DUPLICATE KEY UPDATE + name = IF(employees.version=?,t.name,employees.name), + salary = IF(employees.version=?,t.salary,employees.salary), + version = IF(employees.version=?,t.version,employees.version), + + So, initially we'll have: + INSERT INTO employees (id, name, salary, version) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + name = IF(version=@oldversion:=?,VALUES(name), employees.name), + salary = IF(version=@oldversion?,VALUES(salary),employees.salary), + version = IF(version=@oldversion?,VALUES(version),employees.version), +*/ + renderInsertInto( optionalTableUpdate ); + appendSql( " " ); + renderOnDuplicateKeyUpdate( optionalTableUpdate ); + } + + protected void renderInsertInto(OptionalTableUpdate optionalTableUpdate) { + appendSql( "insert into " ); + appendSql( optionalTableUpdate.getMutatingTable().getTableName() ); + appendSql( " (" ); + + final List keyBindings = optionalTableUpdate.getKeyBindings(); + for ( ColumnValueBinding keyBinding : keyBindings ) { + appendSql( keyBinding.getColumnReference().getColumnExpression() ); + appendSql( ',' ); + } + + optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + if ( columnPosition != optionalTableUpdate.getValueBindings().size() - 1 ) { + appendSql( ',' ); + } + } ); + + appendSql( ") values (" ); + + for ( ColumnValueBinding keyBinding : keyBindings ) { + keyBinding.getValueExpression().accept( this ); + appendSql( ',' ); + } + + optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { + if ( columnPosition > 0 ) { + appendSql( ',' ); + } + columnValueBinding.getValueExpression().accept( this ); + } ); + appendSql( ")" ); + } + + protected void renderOnDuplicateKeyUpdate(OptionalTableUpdate optionalTableUpdate) { + appendSql( "on duplicate key update " ); + optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> { + final String columnName = columnValueBinding.getColumnReference().getColumnExpression(); + if ( columnPosition > 0 ) { + appendSql( ',' ); + } + appendSql( columnName ); + append( " = " ); + + if ( optionalTableUpdate.getNumberOfOptimisticLockBindings() > 0 ) { + renderVersionedUpdate( optionalTableUpdate, columnPosition, columnValueBinding ); + } + else { + renderNonVersionedUpdate( columnValueBinding ); + } + } ); + } + + private void renderVersionedUpdate(OptionalTableUpdate optionalTableUpdate, Integer columnPosition, ColumnValueBinding columnValueBinding) { + final String tableName = optionalTableUpdate.getMutatingTable().getTableName(); + appendSql( "if(" ); + renderVersionRestriction( tableName, optionalTableUpdate.getOptimisticLockBindings(), columnPosition ); + appendSql( "," ); + appendSql( "values(" ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + appendSql( ")" ); + appendSql( "," ); + columnValueBinding.getColumnReference().appendColumnForWrite( this, tableName ); + appendSql( ")" ); + } + + private void renderNonVersionedUpdate(ColumnValueBinding columnValueBinding) { + appendSql( "values(" ); + appendSql( columnValueBinding.getColumnReference().getColumnExpression() ); + appendSql( ")" ); + } + + private void renderVersionRestriction(String tableName, List optimisticLockBindings, int index) { + final String operator = index == 0 ? ":=" : ""; + final String versionVariable = "@oldversion" + operator; + for (int i = 0; i < optimisticLockBindings.size(); i++) { +// if ( i>0 ) { +// appendSql(" and "); +// } + final ColumnValueBinding binding = optimisticLockBindings.get( i ); + binding.getColumnReference().appendColumnForWrite( this, tableName ); + appendSql( "=" ); + appendSql( versionVariable ); +// if ( i == 0 ) { + if ( index == 0) { + binding.getValueExpression().accept( this ); + } +// } + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java index c3fea08c8745..a17a7d8a561c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java @@ -41,7 +41,7 @@ public class DeleteOrUpsertOperation implements SelfExecutingUpdateOperation { private final OptionalTableUpdate optionalTableUpdate; - private final Expectation expectation = new Expectation.RowCount(); + private final Expectation expectation = getExpectation(); public DeleteOrUpsertOperation( EntityMutationTarget mutationTarget, @@ -127,7 +127,7 @@ private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionCon .executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() ); MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() ); try { - expectation.verifyOutcome( rowCount, upsertDeleteStatement, -1, statementDetails.getSqlString() ); + getExpectation().verifyOutcome( rowCount, upsertDeleteStatement, -1, statementDetails.getSqlString() ); } catch (SQLException e) { throw jdbcServices.getSqlExceptionHelper().convert( @@ -203,7 +203,7 @@ private void performUpsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon .executeUpdate( updateStatement, statementDetails.getSqlString() ); MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() ); try { - expectation.verifyOutcome( rowCount, updateStatement, -1, statementDetails.getSqlString() ); + getExpectation().verifyOutcome( rowCount, updateStatement, -1, statementDetails.getSqlString() ); } catch (SQLException e) { throw jdbcServices.getSqlExceptionHelper().convert( @@ -231,4 +231,8 @@ public UpsertOperation getUpsertOperation() { public OptionalTableUpdate getOptionalTableUpdate() { return optionalTableUpdate; } + + protected Expectation getExpectation() { + return new Expectation.RowCount(); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertTest.java index b86e880e70b7..a5970987731d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertTest.java @@ -6,14 +6,20 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialects; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -@SessionFactory +@SessionFactory(useCollectingStatementInspector = true) @DomainModel(annotatedClasses = UpsertTest.Record.class) public class UpsertTest { @Test void test(SessionFactoryScope scope) { @@ -25,24 +31,82 @@ public class UpsertTest { assertEquals("hello earth", s.get( Record.class,123L).message); assertEquals("hello mars", s.get( Record.class,456L).message); }); - scope.inStatelessTransaction(s-> { - s.upsert(new Record(123L,"goodbye earth")); - }); + scope.inStatelessTransaction(s-> s.upsert(new Record(123L,"goodbye earth")) ); scope.inStatelessTransaction(s-> { assertEquals("goodbye earth", s.get( Record.class,123L).message); assertEquals("hello mars", s.get( Record.class,456L).message); }); } + + @RequiresDialects( + value = { + @RequiresDialect( MySQLDialect.class ), + @RequiresDialect( MariaDBDialect.class ) + } + ) + @Test void testMySQL(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inStatelessTransaction(s-> { + s.upsert(new Record(123L,"hello earth")); + s.upsert(new Record(456L,"hello mars")); + }); + // Verify that only a single query is executed for each upsert, in contrast to the former update+insert + statementInspector.assertExecutedCount( 2 ); + + scope.inStatelessTransaction(s-> { + assertEquals("hello earth",s.get(Record.class,123L).message); + assertEquals("hello mars",s.get(Record.class,456L).message); + }); + statementInspector.clear(); + + scope.inStatelessTransaction(s-> s.upsert(new Record(123L,"goodbye earth")) ); + statementInspector.assertExecutedCount( 1 ); + + scope.inStatelessTransaction(s-> { + assertEquals("goodbye earth",s.get(Record.class,123L).message); + assertEquals("hello mars",s.get(Record.class,456L).message); + }); + } + + @RequiresDialects( + value = { + @RequiresDialect( MySQLDialect.class ), + @RequiresDialect( MariaDBDialect.class ) + } + ) + @Test void testMySQLRowCounts(SessionFactoryScope scope) { + // insert => rowcount 1 + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L,"hello earth", 321))) ); + + // Partial update => rowcount 2 + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L,"goodbye earth"))) ); + + // Nothing updated => rowcount 1 (?) + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L,"goodbye earth"))) ); + + // all null => delete + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L,null, null))) ); + } + @Entity static class Record { @Id Long id; String message; + Integer someInt; Record(Long id, String message) { this.id = id; this.message = message; } + Record(Long id, String message, Integer someInt) { + this.id = id; + this.message = message; + this.someInt = someInt; + } + Record() { } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java index 0c2b71a64a60..2ffef57d27c7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java @@ -8,11 +8,18 @@ import jakarta.persistence.Id; import jakarta.persistence.Version; import org.hibernate.StaleStateException; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialects; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.orm.junit.SkipForDialectGroup; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -46,6 +53,13 @@ public class UpsertVersionedTest { }); } + @SkipForDialectGroup( + value = { + @SkipForDialect( dialectClass = MySQLDialect.class, + reason = "These dialects return a rowcount of 1 in a stale upsert, so no StaleStateException is thrown is such cases"), + @SkipForDialect( dialectClass = MariaDBDialect.class ) + } + ) @Test void testStaleUpsert(SessionFactoryScope scope) { scope.getSessionFactory().getSchemaManager().truncate(); scope.inStatelessTransaction( s -> { @@ -68,6 +82,26 @@ public class UpsertVersionedTest { } ); } + @RequiresDialects( + value = { + @RequiresDialect( MySQLDialect.class ), + @RequiresDialect( MariaDBDialect.class ) + } + ) + @Test void testMySQLRowCounts(SessionFactoryScope scope) { + // insert => rowcount 1 + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L, null, "hello earth"))) ); + + // Partial update => rowcount 2 + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L,0L,"goodbye earth"))) ); + + // Only version updated rowcount 2 + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L, 1L, "goodbye earth"))) ); + + // all null => partial update, version reset to 0 + scope.inStatelessTransaction(s-> assertDoesNotThrow(() -> s.upsert(new Record(123L,null, null))) ); + } + @Entity(name = "Record") static class Record { @Id Long id;