diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index c40a5cdad6d7..210838957b0c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -51,6 +51,7 @@ import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.config.ConfigurationHelper; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.query.SemanticException; import org.hibernate.query.common.TemporalUnit; import org.hibernate.query.sqm.IntervalType; @@ -62,6 +63,9 @@ 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.sql.model.jdbc.OptionalTableUpdateWithUpsertOperation; import org.hibernate.tool.schema.extract.internal.InformationExtractorPostgreSQLImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.tool.schema.extract.spi.ExtractionContext; @@ -676,6 +680,14 @@ protected SqlAstTranslator buildTranslator( }; } + @Override + public MutationOperation createOptionalTableUpdateOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + return new OptionalTableUpdateWithUpsertOperation( mutationTarget, optionalTableUpdate, factory ); + } + @Override public NationalizationSupport getNationalizationSupport() { // TEXT / STRING inherently support nationalized data diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 6621a58cd495..8051844e842e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -85,6 +85,7 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.OptionalTableUpdateWithUpsertOperation; import org.hibernate.tool.schema.extract.internal.InformationExtractorPostgreSQLImpl; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.tool.schema.extract.spi.ExtractionContext; @@ -1587,7 +1588,7 @@ public MutationOperation createOptionalTableUpdateOperation( .createMergeOperation( optionalTableUpdate ); } else { - return super.createOptionalTableUpdateOperation( mutationTarget, optionalTableUpdate, factory ); + return new OptionalTableUpdateWithUpsertOperation( mutationTarget, optionalTableUpdate, factory ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java index 85bdacbc40ab..43bb87f4e509 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/CockroachSqlAstTranslator.java @@ -27,6 +27,8 @@ import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.model.internal.OptionalTableInsert; +import org.hibernate.sql.model.internal.TableInsertStandard; /** * A SQL AST translator for Cockroach. @@ -47,6 +49,39 @@ public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeti super.visitBinaryArithmeticExpression(arithmeticExpression); } + @Override + public void visitStandardTableInsert(TableInsertStandard tableInsert) { + getCurrentClauseStack().push( Clause.INSERT ); + try { + renderInsertInto( tableInsert ); + if ( tableInsert instanceof OptionalTableInsert optionalTableInsert ) { + appendSql( " on conflict " ); + final String constraintName = optionalTableInsert.getConstraintName(); + if ( constraintName != null ) { + appendSql( " on constraint " ); + appendSql( constraintName ); + } + else { + char separator = '('; + for ( String constraintColumnName : optionalTableInsert.getConstraintColumnNames() ) { + appendSql( separator ); + appendSql( constraintColumnName ); + separator = ','; + } + appendSql( ')' ); + } + appendSql( " do nothing" ); + } + + if ( tableInsert.getNumberOfReturningColumns() > 0 ) { + visitReturningColumns( tableInsert::getReturningColumns ); + } + } + finally { + getCurrentClauseStack().pop(); + } + } + @Override protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { visitInsertStatement( sqlAst ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java index da605ae88d61..f251e7aaded6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/PostgreSQLSqlAstTranslator.java @@ -32,6 +32,7 @@ import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; +import org.hibernate.sql.model.internal.OptionalTableInsert; import org.hibernate.sql.model.internal.TableInsertStandard; import org.hibernate.type.SqlTypes; @@ -59,6 +60,39 @@ protected String getArrayContainsFunction() { return super.getArrayContainsFunction(); } + @Override + public void visitStandardTableInsert(TableInsertStandard tableInsert) { + getCurrentClauseStack().push( Clause.INSERT ); + try { + renderInsertInto( tableInsert ); + if ( tableInsert instanceof OptionalTableInsert optionalTableInsert ) { + appendSql( " on conflict " ); + final String constraintName = optionalTableInsert.getConstraintName(); + if ( constraintName != null ) { + appendSql( " on constraint " ); + appendSql( constraintName ); + } + else { + char separator = '('; + for ( String constraintColumnName : optionalTableInsert.getConstraintColumnNames() ) { + appendSql( separator ); + appendSql( constraintColumnName ); + separator = ','; + } + appendSql( ')' ); + } + appendSql( " do nothing" ); + } + + if ( tableInsert.getNumberOfReturningColumns() > 0 ) { + visitReturningColumns( tableInsert::getReturningColumns ); + } + } + finally { + getCurrentClauseStack().pop(); + } + } + @Override protected void renderInsertIntoNoColumns(TableInsertStandard tableInsert) { renderIntoIntoAndTable( tableInsert ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 2fe0d6e35eef..1e89cb8c2072 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -188,6 +188,7 @@ import org.hibernate.sql.model.ast.ColumnWriteFragment; import org.hibernate.sql.model.ast.RestrictedTableMutation; import org.hibernate.sql.model.ast.TableMutation; +import org.hibernate.sql.model.internal.OptionalTableInsert; import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.sql.model.internal.TableDeleteCustomSql; import org.hibernate.sql.model.internal.TableDeleteStandard; @@ -8508,6 +8509,9 @@ private T translateTableMutation(TableMutation mutation) { @Override public void visitStandardTableInsert(TableInsertStandard tableInsert) { + if ( tableInsert instanceof OptionalTableInsert ) { + throw new IllegalQueryOperationException( "Optional table insert is not supported" ); + } getCurrentClauseStack().push( Clause.INSERT ); try { renderInsertInto( tableInsert ); @@ -8521,7 +8525,7 @@ public void visitStandardTableInsert(TableInsertStandard tableInsert) { } } - private void renderInsertInto(TableInsertStandard tableInsert) { + protected void renderInsertInto(TableInsertStandard tableInsert) { applySqlComment( tableInsert.getMutationComment() ); if ( tableInsert.getNumberOfValueBindings() == 0 ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableInsert.java b/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableInsert.java new file mode 100644 index 000000000000..99d1915ea41f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableInsert.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.model.internal; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.model.MutationTarget; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.model.ast.ColumnValueParameter; +import org.hibernate.sql.model.ast.MutatingTableReference; + +import java.util.List; + +public class OptionalTableInsert extends TableInsertStandard { + + private final @Nullable String constraintName; + private final List constraintColumnNames; + + public OptionalTableInsert( + MutatingTableReference mutatingTable, + MutationTarget mutationTarget, + List valueBindings, + List returningColumns, + List parameters, + @Nullable String constraintName, + List constraintColumnNames) { + super( mutatingTable, mutationTarget, valueBindings, returningColumns, parameters ); + this.constraintName = constraintName; + this.constraintColumnNames = constraintColumnNames; + } + + public @Nullable String getConstraintName() { + return constraintName; + } + + public List getConstraintColumnNames() { + return constraintColumnNames; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java index 7978e09a8080..01614f4f1ca8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java @@ -105,6 +105,22 @@ public TableMapping getTableDetails() { return tableMapping; } + public List getValueBindings() { + return valueBindings; + } + + public List getKeyBindings() { + return keyBindings; + } + + public List getOptimisticLockBindings() { + return optimisticLockBindings; + } + + public List getParameters() { + return parameters; + } + @Override public JdbcValueDescriptor findValueDescriptor(String columnName, ParameterUsage usage) { for ( int i = 0; i < jdbcValueDescriptors.size(); i++ ) { @@ -375,7 +391,7 @@ protected JdbcMutationOperation createJdbcUpdate(SharedSessionContractImplemento } private void performInsert(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) { - final JdbcInsertMutation jdbcInsert = createJdbcInsert( session ); + final JdbcMutationOperation jdbcInsert = createJdbcOptionalInsert( session ); final JdbcServices jdbcServices = session.getJdbcServices(); final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); final PreparedStatement insertStatement = createStatementDetails( jdbcInsert, jdbcCoordinator ); @@ -413,6 +429,13 @@ private void performInsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon } } + /* + * Used by Hibernate Reactive + */ + protected JdbcMutationOperation createJdbcOptionalInsert(SharedSessionContractImplementor session) { + return createJdbcInsert( session ); + } + /* * Used by Hibernate Reactive */ diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateWithUpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateWithUpsertOperation.java new file mode 100644 index 000000000000..af5c148f4191 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateWithUpsertOperation.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.model.jdbc; + +import org.hibernate.engine.jdbc.mutation.internal.MutationQueryOptions; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.sql.model.ast.MutatingTableReference; +import org.hibernate.sql.model.ast.TableMutation; +import org.hibernate.sql.model.internal.OptionalTableInsert; +import org.hibernate.sql.model.internal.OptionalTableUpdate; + +import java.util.Arrays; +import java.util.Collections; + +/** + * Uses {@link org.hibernate.sql.model.internal.OptionalTableInsert} for the insert operation, + * to avoid primary key constraint violations when inserting only primary key columns. + */ +public class OptionalTableUpdateWithUpsertOperation extends OptionalTableUpdateOperation { + + public OptionalTableUpdateWithUpsertOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate upsert, + @SuppressWarnings("unused") SessionFactoryImplementor factory) { + super( mutationTarget, upsert, factory ); + } + + @Override + protected JdbcMutationOperation createJdbcOptionalInsert(SharedSessionContractImplementor session) { + if ( getTableDetails().getInsertDetails() != null + && getTableDetails().getInsertDetails().getCustomSql() != null + || !getValueBindings().isEmpty() ) { + return super.createJdbcOptionalInsert( session ); + } + else { + // Ignore a primary key violation on insert when inserting just the primary key columns + final TableMutation tableInsert = new OptionalTableInsert( + new MutatingTableReference( getTableDetails() ), + getMutationTarget(), + CollectionHelper.combine( getValueBindings(), getKeyBindings() ), + Collections.emptyList(), + getParameters(), + null, + Arrays.asList( ((EntityPersister) getMutationTarget()).getIdentifierColumnNames() ) + ); + + final SessionFactoryImplementor factory = session.getSessionFactory(); + return factory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory() + .buildModelMutationTranslator( tableInsert, factory ) + .translate( null, MutationQueryOptions.INSTANCE ); + } + } + + @Override + public String toString() { + return "OptionalTableUpdateWithUpsertOperation(" + getTableDetails() + ")"; + } +}