diff --git a/databases/hana/resources/hibernate.properties b/databases/hana/resources/hibernate.properties index cfaf8c610e..87ff2fc50b 100644 --- a/databases/hana/resources/hibernate.properties +++ b/databases/hana/resources/hibernate.properties @@ -5,7 +5,7 @@ # See the lgpl.txt file in the root directory or . # -hibernate.dialect org.hibernate.dialect.HANAColumnStoreDialect +hibernate.dialect org.hibernate.dialect.HANADialect hibernate.connection.driver_class com.sap.db.jdbc.Driver hibernate.connection.url jdbc:sap://localhost:39015/ hibernate.connection.username HIBERNATE_TEST diff --git a/etc/hibernate.properties.template b/etc/hibernate.properties.template index d7f7c79a2c..a6b8d493eb 100644 --- a/etc/hibernate.properties.template +++ b/etc/hibernate.properties.template @@ -161,7 +161,7 @@ hibernate.connection.url @DB_URL@ ## HANA -#hibernate.dialect org.hibernate.dialect.HANAColumnStoreDialect +#hibernate.dialect org.hibernate.dialect.HANADialect #hibernate.connection.driver_class com.sap.db.jdbc.Driver #hibernate.connection.url jdbc:sap://localhost:30015 #hibernate.connection.username HIBERNATE_TEST diff --git a/gradle/databases.gradle b/gradle/databases.gradle index 7bba849dcd..8054134659 100644 --- a/gradle/databases.gradle +++ b/gradle/databases.gradle @@ -254,7 +254,7 @@ ext { 'connection.init_sql' : '' ], hana_cloud : [ - 'db.dialect' : 'org.hibernate.dialect.HANAColumnStoreDialect', + 'db.dialect' : 'org.hibernate.dialect.HANADialect', 'jdbc.driver': 'com.sap.db.jdbc.Driver', 'jdbc.user' : 'HIBERNATE_TEST', 'jdbc.pass' : 'H1bernate_test', @@ -263,7 +263,7 @@ ext { 'connection.init_sql' : '' ], hana_ci : [ - 'db.dialect' : 'org.hibernate.dialect.HANAColumnStoreDialect', + 'db.dialect' : 'org.hibernate.dialect.HANADialect', 'jdbc.driver': 'com.sap.db.jdbc.Driver', 'jdbc.user' : 'SYSTEM', 'jdbc.pass' : 'H1bernate_test', diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java index 82c1b8e42c..509f38941d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java @@ -146,13 +146,14 @@ public class AltibaseSqlAstTranslator extends AbstractS emulateQueryPartTableReferenceColumnAliasing( tableReference ); } - protected String getFromDual() { - return " from dual"; + @Override + protected String getDual() { + return "dual"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java index e409562d87..e187406b3b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java @@ -82,14 +82,14 @@ public class CUBRIDSqlAstTranslator extends AbstractSql } @Override - protected String getFromDual() { + protected String getDual() { //TODO: is this really needed? //TODO: would "from table({0})" be better? - return " from db_root"; + return "db_root"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 80c8d2da51..0e91fb68eb 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -570,6 +570,10 @@ public class CockroachLegacyDialect extends Dialect { public boolean supportsRecursiveCTE() { return getVersion().isSameOrAfter( 20, 1 ); } + @Override + public boolean supportsConflictClauseForInsertCTE() { + return true; + } @Override public String getNoColumnsInsertString() { @@ -1165,4 +1169,14 @@ public class CockroachLegacyDialect extends Dialect { // RuntimeModelCreationContext runtimeModelCreationContext) { // return new CteInsertStrategy( rootEntityDescriptor, runtimeModelCreationContext ); // } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java index 0aa4c60a6e..658b7f14ba 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacySqlAstTranslator.java @@ -7,20 +7,27 @@ package org.hibernate.community.dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; +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.cte.CteMaterialization; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; /** * A SQL AST translator for Cockroach. @@ -33,6 +40,56 @@ public class CockroachLegacySqlAstTranslator extends Ab super( sessionFactory, statement ); } + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { + final String identificationVariable = tableReference.getIdentificationVariable(); + if ( identificationVariable != null ) { + final Clause currentClause = getClauseStack().getCurrent(); + if ( currentClause == Clause.INSERT ) { + // PostgreSQL requires the "as" keyword for inserts + appendSql( " as " ); + } + else { + append( WHITESPACE ); + } + append( tableReference.getIdentificationVariable() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + final Statement currentStatement = getStatementStack().getCurrent(); + if ( !( currentStatement instanceof UpdateStatement ) + || !hasNonTrivialFromClause( ( (UpdateStatement) currentStatement ).getFromClause() ) ) { + // For UPDATE statements we render a full FROM clause and a join condition to match target table rows, + // but for that to work, we have to omit the alias for the target table reference here + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + renderFromClauseJoiningDmlTargetReference( statement ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitStandardConflictClause( conflictClause ); + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index e3d6a35360..746a565f32 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -24,6 +24,7 @@ import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DB2StructJdbcType; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.aggregate.AggregateSupport; import org.hibernate.dialect.aggregate.DB2AggregateSupport; @@ -49,8 +50,11 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; 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.LockTimeoutException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.mapping.Column; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -94,6 +98,7 @@ import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.TemporalType; +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.BLOB; import static org.hibernate.type.SqlTypes.BOOLEAN; @@ -889,6 +894,20 @@ public class DB2LegacyDialect extends Dialect { appender.appendSql( '\'' ); } + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return new TemplatedViolatedConstraintNameExtractor( + sqle -> { + switch ( JdbcExceptionHelper.extractErrorCode( sqle ) ) { + case -803: + return extractUsingTemplate( "SQLERRMC=1;", ",", sqle.getMessage() ); + default: + return null; + } + } + ); + } + @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { @@ -896,6 +915,14 @@ public class DB2LegacyDialect extends Dialect { switch ( errorCode ) { case -952: return new LockTimeoutException( message, sqlException, sql ); + case -803: + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); } return null; }; @@ -1079,4 +1106,14 @@ public class DB2LegacyDialect extends Dialect { public int rowIdSqlType() { return VARBINARY; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return getDB2Version().isSameOrAfter( 11 ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java index bcd50a0559..fdab41ce02 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java @@ -13,8 +13,10 @@ import org.hibernate.LockMode; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; @@ -28,10 +30,12 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.Predicate; @@ -344,12 +348,40 @@ public class DB2LegacySqlAstTranslator extends Abstract @Override protected void visitInsertStatementOnly(InsertSelectStatement statement) { final boolean closeWrapper = renderReturningClause( statement ); - super.visitInsertStatementOnly( statement ); + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } if ( closeWrapper ) { appendSql( ')' ); } } + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + renderFromClauseExcludingDmlTargetReference( statement ); + } + protected boolean renderReturningClause(MutationStatement statement) { final List returningColumns = statement.getReturningColumns(); final int size = returningColumns.size(); @@ -497,13 +529,13 @@ public class DB2LegacySqlAstTranslator extends Abstract } @Override - protected String getFromDual() { - return " from sysibm.dual"; + protected String getDual() { + return "sysibm.dual"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java index 1616eafae2..b42697d177 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java @@ -39,8 +39,11 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; 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.LockTimeoutException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; @@ -706,14 +709,42 @@ public class DerbyLegacyDialect extends Dialect { return false; } + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return new TemplatedViolatedConstraintNameExtractor( sqle -> { + final String sqlState = JdbcExceptionHelper.extractSqlState( sqle ); + if ( sqlState != null ) { + switch ( sqlState ) { + case "23505": + return TemplatedViolatedConstraintNameExtractor.extractUsingTemplate( + "'", "'", + sqle.getMessage() + ); + } + } + return null; + } ); + } + @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); // final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); + final String constraintName; if ( sqlState != null ) { switch ( sqlState ) { + case "23505": + // Unique constraint violation + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); case "40XL1": case "40XL2": return new LockTimeoutException( message, sqlException, sql ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java index deee312b08..191c8ef286 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java @@ -235,13 +235,13 @@ public class DerbyLegacySqlAstTranslator extends Abstra } @Override - protected String getFromDual() { - return " from (values 0) dual"; + protected String getDual() { + return "(values 0)"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual() + " dual"; } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java index 4eaa45abe6..2b15a734d1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java @@ -263,13 +263,13 @@ public class FirebirdSqlAstTranslator extends AbstractS } @Override - protected String getFromDual() { - return " from rdb$database"; + protected String getDual() { + return "rdb$database"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } private boolean supportsOffsetFetchClause() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 73bd5da49a..a1e2cf91c6 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -776,8 +776,19 @@ public class H2LegacyDialect extends Dialect { public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); + final String constraintName; switch (errorCode) { + case 23505: + // Unique constraint violation + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); case 40001: // DEADLOCK DETECTED return new LockAcquisitionException(message, sqlException, sql); @@ -786,7 +797,7 @@ public class H2LegacyDialect extends Dialect { return new PessimisticLockException(message, sqlException, sql); case 90006: // NULL not allowed for column [90006-145] - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); return new ConstraintViolationException(message, sqlException, sql, constraintName); case 57014: return new QueryTimeoutException( message, sqlException, sql ); @@ -940,4 +951,9 @@ public class H2LegacyDialect extends Dialect { public int rowIdSqlType() { return BIGINT; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java index 9e27640531..26ce7b2a7f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java @@ -12,6 +12,7 @@ import org.hibernate.LockMode; import org.hibernate.dialect.identity.H2IdentityColumnSupport; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; @@ -27,14 +28,18 @@ import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.internal.TableInsertStandard; @@ -79,6 +84,44 @@ public class H2LegacySqlAstTranslator extends AbstractS ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + if ( hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitUpdateStatementEmulateMerge( statement ); + } + else { + super.visitUpdateStatementOnly( statement ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected void visitReturningColumns(List returningColumns) { // do nothing - this is handled via `#visitReturningInsertStatement` @@ -289,8 +332,8 @@ public class H2LegacySqlAstTranslator extends AbstractS } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } private boolean supportsOffsetFetchClause() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index 7d196ca37f..3a59956a70 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -41,6 +41,8 @@ import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.event.spi.EventSource; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.CoreMessageLogger; @@ -497,6 +499,29 @@ public class HSQLLegacyDialect extends Dialect { return null; } ); + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); + final String constraintName; + + switch ( errorCode ) { + case -104: + // Unique constraint violation + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); + } + + return null; + }; + } + /** * HSQLDB 2.0 messages have changed * messages may be localized - therefore use the common, non-locale element " table: " @@ -856,4 +881,9 @@ public class HSQLLegacyDialect extends Dialect { public UniqueDelegate getUniqueDelegate() { return uniqueDelegate; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java index c92a29f91e..f2aa783be4 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java @@ -11,23 +11,32 @@ import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; @@ -42,6 +51,44 @@ public class HSQLLegacySqlAstTranslator extends Abstrac super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + if ( hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitUpdateStatementEmulateMerge( statement ); + } + else { + super.visitUpdateStatementOnly( statement ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -295,14 +342,9 @@ public class HSQLLegacySqlAstTranslator extends Abstrac return false; } - @Override - protected String getFromDual() { - return " from (values(0))"; - } - @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } private boolean supportsOffsetFetchClause() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java index 8c20677347..63c3d9c200 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java @@ -134,13 +134,13 @@ public class InformixSqlAstTranslator extends AbstractS } @Override - protected String getFromDual() { - return " from (select 0 from systables where tabid=1) dual"; + protected String getDual() { + return "(select 0 from systables where tabid=1)"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual() + " dual"; } private boolean supportsParameterOffsetFetchExpression() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java index 63b94af516..7a4af55cc9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java @@ -139,14 +139,14 @@ public class IngresSqlAstTranslator extends AbstractSql } @Override - protected String getFromDual() { - //this is only necessary if the query has a where clause - return " from (select 0) dual"; + protected String getDual() { + return "(select 0)"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + //this is only necessary if the query has a where clause + return " from " + getDual() + " dual"; } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java index cc4b556692..9e7d02ad80 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java @@ -6,22 +6,38 @@ */ package org.hibernate.community.dialect; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.MySQLSqlAstTranslator; import org.hibernate.engine.spi.SessionFactoryImplementor; +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.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; /** * A SQL AST translator for MariaDB. @@ -37,6 +53,135 @@ public class MariaDBLegacySqlAstTranslator extends Abst this.dialect = (MariaDBLegacyDialect)super.getDialect(); } + @Override + protected void visitInsertSource(InsertSelectStatement statement) { + if ( statement.getSourceSelectStatement() != null ) { + if ( statement.getConflictClause() != null ) { + final List targetColumnReferences = statement.getTargetColumns(); + final List columnNames = new ArrayList<>( targetColumnReferences.size() ); + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + columnNames.add( targetColumnReference.getColumnExpression() ); + } + appendSql( "select * from " ); + emulateQueryPartTableReferenceColumnAliasing( + new QueryPartTableReference( + new SelectStatement( statement.getSourceSelectStatement() ), + "excluded", + columnNames, + false, + getSessionFactory() + ) + ); + } + else { + statement.getSourceSelectStatement().accept( this ); + } + } + else { + visitValuesList( statement.getValuesList() ); + } + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + final Statement currentStatement; + if ( "excluded".equals( columnReference.getQualifier() ) + && ( currentStatement = getStatementStack().getCurrent() ) instanceof InsertSelectStatement + && ( (InsertSelectStatement) currentStatement ).getSourceSelectStatement() == null ) { + // Accessing the excluded row for an insert-values statement in the conflict clause requires the values qualifier + appendSql( "values(" ); + columnReference.appendReadExpression( this, null ); + append( ')' ); + } + else { + super.visitColumnReference( columnReference ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + super.renderUpdateClause( updateStatement ); + } + else { + appendSql( "update " ); + renderFromClauseSpaces( updateStatement.getFromClause() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitOnDuplicateKeyConflictClause( conflictClause ); + } + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + // Since MariaDB does not support aliasing the insert target table, + // we must detect column reference that are used in the conflict clause + // and use the table expression as qualifier instead + if ( getClauseStack().getCurrent() != Clause.SET + || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !getQueryPartStack().isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); + } + else { + return null; + } + } + @Override protected boolean supportsWithClause() { return dialect.getVersion().isSameOrAfter( 10, 2 ); @@ -222,13 +367,13 @@ public class MariaDBLegacySqlAstTranslator extends Abst } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isBefore( 10, 4 ) ? getFromDual() : ""; + return getDialect().getVersion().isBefore( 10, 4 ) ? ( " from " + getDual() ) : ""; } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java index a87ef8057f..0110983894 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java @@ -94,12 +94,12 @@ public class MaxDBSqlAstTranslator extends AbstractSqlA } @Override - public String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java index 652833630c..7510df6451 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java @@ -82,13 +82,8 @@ public class MimerSQLSqlAstTranslator extends AbstractS return false; } - @Override - protected String getFromDual() { - return " from (values(0))"; - } - @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } } 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 0d80444119..8d391fa066 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 @@ -1395,4 +1395,14 @@ public class MySQLLegacyDialect extends Dialect { return "set foreign_key_checks = 1"; } + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java index 59ed599c9b..d503f797d2 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java @@ -6,24 +6,40 @@ */ package org.hibernate.community.dialect; +import java.util.ArrayList; +import java.util.List; + import org.hibernate.dialect.DialectDelegateWrapper; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.MySQLSqlAstTranslator; import org.hibernate.engine.spi.SessionFactoryImplementor; +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.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; /** * A SQL AST translator for MySQL. @@ -36,6 +52,146 @@ public class MySQLLegacySqlAstTranslator extends Abstra super( sessionFactory, statement ); } + @Override + protected void visitInsertSource(InsertSelectStatement statement) { + if ( statement.getSourceSelectStatement() != null ) { + if ( statement.getConflictClause() != null ) { + final List targetColumnReferences = statement.getTargetColumns(); + final List columnNames = new ArrayList<>( targetColumnReferences.size() ); + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + columnNames.add( targetColumnReference.getColumnExpression() ); + } + appendSql( "select * from " ); + emulateQueryPartTableReferenceColumnAliasing( + new QueryPartTableReference( + new SelectStatement( statement.getSourceSelectStatement() ), + "excluded", + columnNames, + false, + getSessionFactory() + ) + ); + } + else { + statement.getSourceSelectStatement().accept( this ); + } + } + else { + visitValuesList( statement.getValuesList() ); + if ( statement.getConflictClause() != null && getDialect().getMySQLVersion().isSameOrAfter( 8, 0, 19 ) ) { + appendSql( " as excluded" ); + char separator = '('; + for ( ColumnReference targetColumn : statement.getTargetColumns() ) { + appendSql( separator ); + appendSql( targetColumn.getColumnExpression() ); + separator = ','; + } + appendSql( ')' ); + } + } + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + final Statement currentStatement; + if ( getDialect().getMySQLVersion().isBefore( 8, 0, 19 ) + && "excluded".equals( columnReference.getQualifier() ) + && ( currentStatement = getStatementStack().getCurrent() ) instanceof InsertSelectStatement + && ( (InsertSelectStatement) currentStatement ).getSourceSelectStatement() == null ) { + // Accessing the excluded row for an insert-values statement in the conflict clause requires the values qualifier + appendSql( "values(" ); + columnReference.appendReadExpression( this, null ); + append( ')' ); + } + else { + super.visitColumnReference( columnReference ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + super.renderUpdateClause( updateStatement ); + } + else { + appendSql( "update " ); + renderFromClauseSpaces( updateStatement.getFromClause() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitOnDuplicateKeyConflictClause( conflictClause ); + } + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + // Since MySQL does not support aliasing the insert target table, + // we must detect column reference that are used in the conflict clause + // and use the table expression as qualifier instead + if ( getClauseStack().getCurrent() != Clause.SET + || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !getQueryPartStack().isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); + } + else { + return null; + } + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -234,13 +390,13 @@ public class MySQLLegacySqlAstTranslator extends Abstra } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isSameOrAfter( 8 ) ? "" : getFromDual(); + return getDialect().getVersion().isSameOrAfter( 8 ) ? "" : ( " from " + getDual() ); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 4e1553fd1d..98c5251549 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -1008,6 +1008,7 @@ public class OracleLegacyDialect extends Dialect { @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { + final String constraintName; // interpreting Oracle exceptions is much much more precise based on their specific vendor codes. switch ( JdbcExceptionHelper.extractErrorCode( sqlException ) ) { @@ -1040,9 +1041,19 @@ public class OracleLegacyDialect extends Dialect { // data integrity violation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + case 1: + // ORA-00001: unique constraint violated + constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); case 1407: // ORA-01407: cannot update column to NULL - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); return new ConstraintViolationException( message, sqlException, sql, constraintName ); default: @@ -1520,4 +1531,9 @@ public class OracleLegacyDialect extends Dialect { public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java index efbd182b17..9a1f66faeb 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java @@ -37,10 +37,12 @@ import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.FromClause; import org.hibernate.sql.ast.tree.from.FunctionTableReference; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.UnionTableGroup; import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; @@ -51,6 +53,7 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.type.SqlTypes; @@ -67,6 +70,54 @@ public class OracleLegacySqlAstTranslator extends Abstr super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + if ( hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitUpdateStatementEmulateInlineView( statement ); + } + else { + renderUpdateClause( statement ); + renderSetClause( statement.getAssignments() ); + visitWhereClause( statement.getRestriction() ); + visitReturningColumns( statement.getReturningColumns() ); + } + } + + @Override + protected void renderMergeUpdateClause(List assignments, Predicate wherePredicate) { + appendSql( " then update" ); + renderSetClause( assignments ); + visitWhereClause( wherePredicate ); + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean needsRecursiveKeywordInWithClause() { return false; @@ -227,7 +278,14 @@ public class OracleLegacySqlAstTranslator extends Abstr @Override protected void visitValuesList(List valuesList) { - visitValuesListEmulateSelectUnion( valuesList ); + if ( valuesList.size() < 2 ) { + visitValuesListStandard( valuesList ); + } + else { + // Oracle doesn't support a multi-values insert + // So we render a select union emulation instead + visitValuesListEmulateSelectUnion( valuesList ); + } } @Override @@ -617,13 +675,13 @@ public class OracleLegacySqlAstTranslator extends Abstr } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } private boolean supportsOffsetFetchClause() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index d36df7fafa..35f0077c30 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -732,6 +732,11 @@ public class PostgreSQLLegacyDialect extends Dialect { return getVersion().isSameOrAfter( 9, 1 ); } + @Override + public boolean supportsConflictClauseForInsertCTE() { + return getVersion().isSameOrAfter( 9, 5 ); + } + @Override public SequenceSupport getSequenceSupport() { return getVersion().isBefore( 8, 2 ) @@ -1467,4 +1472,14 @@ public class PostgreSQLLegacyDialect extends Dialect { // The maximum scale for `interval second` is 6 unfortunately return 6; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java index 079a208092..cd6291a228 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java @@ -10,6 +10,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; +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.cte.CteMaterialization; @@ -18,13 +19,20 @@ import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +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.TableInsertStandard; import org.hibernate.type.SqlTypes; @@ -45,6 +53,56 @@ public class PostgreSQLLegacySqlAstTranslator extends A appendSql( "default values" ); } + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { + final String identificationVariable = tableReference.getIdentificationVariable(); + if ( identificationVariable != null ) { + final Clause currentClause = getClauseStack().getCurrent(); + if ( currentClause == Clause.INSERT ) { + // PostgreSQL requires the "as" keyword for inserts + appendSql( " as " ); + } + else { + append( WHITESPACE ); + } + append( tableReference.getIdentificationVariable() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + final Statement currentStatement = getStatementStack().getCurrent(); + if ( !( currentStatement instanceof UpdateStatement ) + || !hasNonTrivialFromClause( ( (UpdateStatement) currentStatement ).getFromClause() ) ) { + // For UPDATE statements we render a full FROM clause and a join condition to match target table rows, + // but for that to work, we have to omit the alias for the target table reference here + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + renderFromClauseJoiningDmlTargetReference( statement ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitStandardConflictClause( conflictClause ); + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -218,9 +276,7 @@ public class PostgreSQLLegacySqlAstTranslator extends A appendSql( "()" ); } else { - appendSql( "(select 1" ); - appendSql( getFromDualForSelectOnly() ); - appendSql( ')' ); + appendSql( "(select 1)" ); } } else if ( expression instanceof Summarization ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java index c755f71542..9a1f97109d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java @@ -126,12 +126,12 @@ public class RDMSOS2200SqlAstTranslator extends Abstrac } @Override - protected String getFromDual() { - return " from rdms.rdms_dummy where key_col=1"; + protected String getDual() { + return "rdms.rdms_dummy"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual() + " where key_col=1"; } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index 642b262770..53b67b94a3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -16,6 +16,7 @@ import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.dialect.AbstractTransactSQLDialect; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.Replacer; import org.hibernate.dialect.TimeZoneSupport; import org.hibernate.dialect.function.CommonFunctionFactory; @@ -41,8 +42,11 @@ import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.LockTimeoutException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.mapping.Column; import org.hibernate.query.sqm.CastType; @@ -85,6 +89,7 @@ import java.util.TimeZone; import jakarta.persistence.TemporalType; +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; import static org.hibernate.query.sqm.TemporalUnit.NANOSECOND; import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; import static org.hibernate.type.SqlTypes.*; @@ -723,6 +728,20 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 11 ); } + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return new TemplatedViolatedConstraintNameExtractor( + sqle -> { + switch ( JdbcExceptionHelper.extractErrorCode( sqle ) ) { + case 2627: + case 2601: + return extractUsingTemplate( "'", "'", sqle.getMessage() ); + default: + return null; + } + } + ); + } @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { @@ -740,6 +759,13 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { case 1222: return new LockTimeoutException( message, sqlException, sql ); case 2627: + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); case 2601: return new ConstraintViolationException( message, diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java index fe2d2d5cd5..b28aac7f55 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java @@ -12,15 +12,20 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; @@ -30,12 +35,15 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.type.SqlTypes; @@ -54,6 +62,84 @@ public class SQLServerLegacySqlAstTranslator extends Ab super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + appendSql( ';' ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + appendSql( "update" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + renderTableReferenceIdentificationVariable( updateStatement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean needsRecursiveKeywordInWithClause() { return false; @@ -129,14 +215,7 @@ public class SQLServerLegacySqlAstTranslator extends Ab appendSql( " )" ); registerAffectedTable( tableReference ); - final Clause currentClause = getClauseStack().getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - final String identificationVariable = tableReference.getIdentificationVariable(); - if ( identificationVariable != null ) { - appendSql( ' ' ); - appendSql( identificationVariable ); - } - } + renderTableReferenceIdentificationVariable( tableReference ); } else { super.renderNamedTableReference( tableReference, lockMode ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java index e219369c68..d3e8c40ed0 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java @@ -628,27 +628,17 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { final int errorCode = JdbcExceptionHelper.extractErrorCode( sqle ); if ( sqlState != null ) { switch ( sqlState ) { - // UNIQUE VIOLATION case "S1000": - if ( 2601 == errorCode ) { - return extractUsingTemplate( "with unique index '", "'", sqle.getMessage() ); - } - break; case "23000": - if ( 546 == errorCode ) { - // Foreign key violation - return extractUsingTemplate( "constraint name = '", "'", sqle.getMessage() ); + switch ( errorCode ) { + case 2601: + // UNIQUE VIOLATION + return extractUsingTemplate( "with unique index '", "'", sqle.getMessage() ); + case 546: + // Foreign key violation + return extractUsingTemplate( "constraint name = '", "'", sqle.getMessage() ); } break; -// // FOREIGN KEY VIOLATION -// case 23503: -// return extractUsingTemplate( "violates foreign key constraint \"","\"", sqle.getMessage() ); -// // NOT NULL VIOLATION -// case 23502: -// return extractUsingTemplate( "null value in column \"","\" violates not-null constraint", sqle.getMessage() ); -// // TODO: RESTRICT VIOLATION -// case 23001: -// return null; } } return null; @@ -659,7 +649,6 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { if ( getVersion().isBefore( 15, 7 ) ) { return null; } - return (sqlException, message, sql) -> { final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); @@ -669,30 +658,44 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { case "JZ006": return new LockTimeoutException( message, sqlException, sql ); case "S1000": + case "23000": switch ( errorCode ) { case 515: // Attempt to insert NULL value into column; column does not allow nulls. + return new ConstraintViolationException( + message, + sqlException, + sql, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); + case 546: + // Foreign key violation + return new ConstraintViolationException( + message, + sqlException, + sql, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); case 2601: // Unique constraint violation - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( - sqlException ); - return new ConstraintViolationException( message, sqlException, sql, constraintName ); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); } break; case "ZZZZZ": if ( 515 == errorCode ) { // Attempt to insert NULL value into column; column does not allow nulls. - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( - sqlException ); - return new ConstraintViolationException( message, sqlException, sql, constraintName ); - } - break; - case "23000": - if ( 546 == errorCode ) { - // Foreign key violation - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( - sqlException ); - return new ConstraintViolationException( message, sqlException, sql, constraintName ); + return new ConstraintViolationException( + message, + sqlException, + sql, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); } break; } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java index 104b275c86..8a7f78af0d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java @@ -11,14 +11,19 @@ import java.util.function.Consumer; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; @@ -33,6 +38,9 @@ import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.Predicate; @@ -40,6 +48,7 @@ import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; /** @@ -55,6 +64,77 @@ public class SybaseASELegacySqlAstTranslator extends Ab super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + appendSql( "update " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + renderDmlTargetTableExpression( updateStatement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean supportsWithClause() { return false; @@ -130,14 +210,7 @@ public class SybaseASELegacySqlAstTranslator extends Ab appendSql( " )" ); registerAffectedTable( tableReference ); - final Clause currentClause = getClauseStack().getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - final String identificationVariable = tableReference.getIdentificationVariable(); - if ( identificationVariable != null ) { - appendSql( ' ' ); - appendSql( identificationVariable ); - } - } + renderTableReferenceIdentificationVariable( tableReference ); } else { super.renderNamedTableReference( tableReference, lockMode ); @@ -252,6 +325,14 @@ public class SybaseASELegacySqlAstTranslator extends Ab visitValuesListEmulateSelectUnion( valuesList ); } + @Override + public void visitValuesTableReference(ValuesTableReference tableReference) { + append( '(' ); + visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); + append( ')' ); + renderDerivedTableReference( tableReference ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { assertRowsOnlyFetchClauseType( queryPart ); @@ -386,63 +467,32 @@ public class SybaseASELegacySqlAstTranslator extends Ab } @Override - public void visitColumnReference(ColumnReference columnReference) { - final String dmlTargetTableAlias = getDmlTargetTableAlias(); - if ( dmlTargetTableAlias != null && dmlTargetTableAlias.equals( columnReference.getQualifier() ) ) { - // Sybase needs a table name prefix - // but not if this is a restricted union table reference subquery - final QuerySpec currentQuerySpec = (QuerySpec) getQueryPartStack().getCurrent(); - final List roots; - if ( currentQuerySpec != null && !currentQuerySpec.isRoot() - && (roots = currentQuerySpec.getFromClause().getRoots()).size() == 1 - && roots.get( 0 ).getPrimaryTableReference() instanceof UnionTableReference ) { - columnReference.appendReadExpression( this ); - } - // for now, use the unqualified form - else if ( columnReference.isColumnExpressionFormula() ) { - // For formulas, we have to replace the qualifier as the alias was already rendered into the formula - // This is fine for now as this is only temporary anyway until we render aliases for table references - appendSql( - columnReference.getColumnExpression() - .replaceAll( "(\\b)(" + dmlTargetTableAlias + "\\.)(\\b)", "$1$3" ) - ); - } - else { - columnReference.appendReadExpression( - this, - getCurrentDmlStatement().getTargetTable().getTableExpression() - ); - } + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + if ( qualifierSupport == DmlTargetColumnQualifierSupport.TABLE_ALIAS + || ( currentDmlStatement = getCurrentDmlStatement() ) == null + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Sybase needs a table name prefix + // but not if this is a restricted union table reference subquery + final QuerySpec currentQuerySpec = (QuerySpec) getQueryPartStack().getCurrent(); + final List roots; + if ( currentQuerySpec != null && !currentQuerySpec.isRoot() + && (roots = currentQuerySpec.getFromClause().getRoots()).size() == 1 + && roots.get( 0 ).getPrimaryTableReference() instanceof UnionTableReference ) { + return columnReference.getQualifier(); + } + else if ( columnReference.isColumnExpressionFormula() ) { + // For formulas, we have to replace the qualifier as the alias was already rendered into the formula + // This is fine for now as this is only temporary anyway until we render aliases for table references + return null; } else { - columnReference.appendReadExpression( this ); - } - } - - @Override - public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) { - final String dmlTargetTableAlias = getDmlTargetTableAlias(); - final ColumnReference columnReference = aggregateColumnWriteExpression.getColumnReference(); - if ( dmlTargetTableAlias != null && dmlTargetTableAlias.equals( columnReference.getQualifier() ) ) { - // Sybase needs a table name prefix - // but not if this is a restricted union table reference subquery - final QuerySpec currentQuerySpec = (QuerySpec) getQueryPartStack().getCurrent(); - final List roots; - if ( currentQuerySpec != null && !currentQuerySpec.isRoot() - && (roots = currentQuerySpec.getFromClause().getRoots()).size() == 1 - && roots.get( 0 ).getPrimaryTableReference() instanceof UnionTableReference ) { - aggregateColumnWriteExpression.appendWriteExpression( this, this ); - } - else { - aggregateColumnWriteExpression.appendWriteExpression( - this, - this, - getCurrentDmlStatement().getTargetTable().getTableExpression() - ); - } - } - else { - aggregateColumnWriteExpression.appendWriteExpression( this, this ); + return getCurrentDmlStatement().getTargetTable().getTableExpression(); } } @@ -472,8 +522,8 @@ public class SybaseASELegacySqlAstTranslator extends Ab } @Override - protected String getFromDual() { - return " from (select 1) dual(c1)"; + protected String getDual() { + return "(select 1 c1)"; } private boolean supportsTopClause() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java index 56de0b9622..9dfb1b221c 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java @@ -116,14 +116,7 @@ public class SybaseAnywhereSqlAstTranslator extends Abs appendSql( " )" ); registerAffectedTable( tableReference ); - final Clause currentClause = getClauseStack().getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - final String identificationVariable = tableReference.getIdentificationVariable(); - if ( identificationVariable != null ) { - appendSql( ' ' ); - appendSql( identificationVariable ); - } - } + renderTableReferenceIdentificationVariable( tableReference ); } else { super.renderNamedTableReference( tableReference, lockMode ); @@ -248,12 +241,12 @@ public class SybaseAnywhereSqlAstTranslator extends Abs } @Override - protected String getFromDual() { - return " from sys.dummy"; + protected String getDual() { + return "sys.dummy"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java index 2b35a5c10e..5e143c7540 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java @@ -18,6 +18,7 @@ import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.AbstractTransactSQLDialect; import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.NationalizationSupport; import org.hibernate.dialect.SybaseDriverKind; import org.hibernate.dialect.function.CommonFunctionFactory; @@ -473,4 +474,14 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect { // Only the jTDS driver supports named parameters properly return driverKind == SybaseDriverKind.JTDS && super.supportsNamedParameters( databaseMetaData ); } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java index faaec68593..d280f0f5c7 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java @@ -11,6 +11,8 @@ import java.util.function.Consumer; import org.hibernate.LockMode; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; @@ -18,6 +20,7 @@ import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; @@ -27,8 +30,13 @@ import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; /** @@ -44,6 +52,46 @@ public class SybaseLegacySqlAstTranslator extends Abstr super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + appendSql( ';' ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + visitFromClause( statement.getFromClause() ); + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + visitFromClause( statement.getFromClause() ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean supportsWithClause() { return false; @@ -119,14 +167,7 @@ public class SybaseLegacySqlAstTranslator extends Abstr appendSql( " )" ); registerAffectedTable( tableReference ); - final Clause currentClause = getClauseStack().getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - final String identificationVariable = tableReference.getIdentificationVariable(); - if ( identificationVariable != null ) { - appendSql( ' ' ); - appendSql( identificationVariable ); - } - } + renderTableReferenceIdentificationVariable( tableReference ); } else { super.renderNamedTableReference( tableReference, lockMode ); @@ -147,6 +188,19 @@ public class SybaseLegacySqlAstTranslator extends Abstr // Sybase does not support the FOR UPDATE clause } + @Override + protected void visitValuesList(List valuesList) { + visitValuesListEmulateSelectUnion( valuesList ); + } + + @Override + public void visitValuesTableReference(ValuesTableReference tableReference) { + append( '(' ); + visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); + append( ')' ); + renderDerivedTableReference( tableReference ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { assertRowsOnlyFetchClauseType( queryPart ); diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index e8f289c4c4..9fc550567c 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -158,6 +158,8 @@ BY : [bB] [yY]; CASE : [cC] [aA] [sS] [eE]; CAST : [cC] [aA] [sS] [tT]; COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE]; +CONFLICT : [cC] [oO] [nN] [fF] [lL] [iI] [cC] [tT]; +CONSTRAINT : [cC] [oO] [nN] [sS] [tT] [rR] [aA] [iI] [nN] [tT]; COUNT : [cC] [oO] [uU] [nN] [tT]; CROSS : [cC] [rR] [oO] [sS] [sS]; CUBE : [cC] [uU] [bB] [eE]; @@ -175,6 +177,7 @@ DELETE : [dD] [eE] [lL] [eE] [tT] [eE]; DEPTH : [dD] [eE] [pP] [tT] [hH]; DESC : [dD] [eE] [sS] [cC]; DISTINCT : [dD] [iI] [sS] [tT] [iI] [nN] [cC] [tT]; +DO : [dD] [oO]; ELEMENT : [eE] [lL] [eE] [mM] [eE] [nN] [tT]; ELEMENTS : [eE] [lL] [eE] [mM] [eE] [nN] [tT] [sS]; ELSE : [eE] [lL] [sS] [eE]; @@ -246,6 +249,7 @@ NEW : [nN] [eE] [wW]; NEXT : [nN] [eE] [xX] [tT]; NO : [nN] [oO]; NOT : [nN] [oO] [tT]; +NOTHING : [nN] [oO] [tT] [hH] [iI] [nN] [gG]; NULLS : [nN] [uU] [lL] [lL] [sS]; OBJECT : [oO] [bB] [jJ] [eE] [cC] [tT]; OF : [oO] [fF]; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 273f8cdb28..a956df54c5 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -83,7 +83,7 @@ assignment * An 'insert' statement */ insertStatement - : INSERT INTO? targetEntity targetFields (queryExpression | valuesList) + : INSERT INTO? targetEntity targetFields (queryExpression | valuesList) conflictClause? ; /** @@ -107,6 +107,18 @@ values : LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)* RIGHT_PAREN ; +/** + * a 'conflict' clause in an 'insert' statement + */ +conflictClause: ON CONFLICT conflictTarget? conflictAction; +conflictTarget + : ON CONSTRAINT identifier + | LEFT_PAREN simplePath (COMMA simplePath)* RIGHT_PAREN; +conflictAction + : DO NOTHING + | DO UPDATE setClause whereClause? + ; + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // QUERY SPEC - general structure of root sqm or sub sqm @@ -1600,6 +1612,8 @@ rollup | CASE | CAST | COLLATE + | CONFLICT + | CONSTRAINT | COUNT | CROSS | CUBE @@ -1617,6 +1631,7 @@ rollup | DEPTH | DESC | DISTINCT + | DO | ELEMENT | ELEMENTS | ELSE @@ -1690,6 +1705,7 @@ rollup | NEXT | NO | NOT + | NOTHING | NULLS | OBJECT | OF diff --git a/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/DefaultDialectSelector.java b/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/DefaultDialectSelector.java index 8fc30f0fc5..56f0ff3370 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/DefaultDialectSelector.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/DefaultDialectSelector.java @@ -21,6 +21,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HANACloudColumnStoreDialect; import org.hibernate.dialect.HANAColumnStoreDialect; +import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.HANARowStoreDialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.MariaDBDialect; @@ -70,6 +71,8 @@ public class DefaultDialectSelector implements DialectSelector { return findCommunityDialect( name ); case "H2": return H2Dialect.class; + case "HANA": + return HANADialect.class; case "HANACloudColumnStore": return HANACloudColumnStoreDialect.class; case "HANAColumnStore": diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java index 41041a248a..5ed23f434d 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/DialectSpecificSettings.java @@ -57,4 +57,12 @@ public interface DialectSpecificSettings { */ public static final String COCKROACH_VERSION_STRING = "hibernate.dialect.cockroach.version_string"; + /** + * Specifies the LOB prefetch size. LOBs larger than this value will be read into memory as the HANA JDBC driver closes + * the LOB when the result set is closed. + * + * @settingDefault {@code 1024} + */ + public static final String HANA_MAX_LOB_PREFETCH_SIZE = "hibernate.dialect.hana.max_lob_prefetch_size"; + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java index 4a40280a7b..7ff305cb68 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractHANADialect.java @@ -20,20 +20,20 @@ import java.nio.charset.StandardCharsets; import java.sql.Blob; import java.sql.CallableStatement; import java.sql.Clob; -import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; -import java.sql.Statement; import java.sql.Types; import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -54,6 +54,8 @@ import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.LimitOffsetLimitHandler; import org.hibernate.dialect.sequence.HANASequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.dialect.temptable.TemporaryTableKind; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.jdbc.BinaryStream; @@ -61,7 +63,6 @@ import org.hibernate.engine.jdbc.BlobImplementer; import org.hibernate.engine.jdbc.CharacterStream; import org.hibernate.engine.jdbc.ClobImplementer; import org.hibernate.engine.jdbc.NClobImplementer; -import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy; import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; @@ -73,15 +74,19 @@ import org.hibernate.exception.LockAcquisitionException; import org.hibernate.exception.LockTimeoutException; import org.hibernate.exception.SQLGrammarException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; -import org.hibernate.internal.CoreLogging; -import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.mapping.Table; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.procedure.internal.StandardCallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; +import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableInsertStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableMutationStrategy; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.query.sqm.produce.function.FunctionParameterType; import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; @@ -121,6 +126,7 @@ import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.TemporalType; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.BOOLEAN; import static org.hibernate.type.SqlTypes.CHAR; @@ -160,15 +166,15 @@ import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithM *

* Note: This dialect is configured to create foreign keys with {@code on update cascade}. * + * @deprecated Will be replaced with {@link HANADialect} in the future. * @author Andrew Clemons * @author Jonathan Bregler */ +@Deprecated(forRemoval = true) public abstract class AbstractHANADialect extends Dialect { - private static final CoreMessageLogger LOG = CoreLogging.messageLogger( AbstractHANADialect.class ); - // Set the LOB prefetch size. LOBs larger than this value will be read into memory as the HANA JDBC driver closes - // the LOB when the result set is closed. - private static final String MAX_LOB_PREFETCH_SIZE_PARAMETER_NAME = "hibernate.dialect.hana.max_lob_prefetch_size"; + // Use column or row tables by default + public static final String USE_DEFAULT_TABLE_TYPE_COLUMN = "hibernate.dialect.hana.use_default_table_type_column"; // Use TINYINT instead of the native BOOLEAN type private static final String USE_LEGACY_BOOLEAN_TYPE_PARAMETER_NAME = "hibernate.dialect.hana.use_legacy_boolean_type"; // Use unicode (NVARCHAR, NCLOB, etc.) instead of non-unicode (VARCHAR, CLOB) string types @@ -177,18 +183,15 @@ public abstract class AbstractHANADialect extends Dialect { // JDBC driver (https://service.sap.com/sap/support/notes/2590160) private static final String TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_PARAMETER_NAME = "hibernate.dialect.hana.treat_double_typed_fields_as_decimal"; - private static final int MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE = 1024; private static final Boolean USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE = Boolean.FALSE; private static final Boolean TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_DEFAULT_VALUE = Boolean.FALSE; - private HANANClobJdbcType nClobTypeDescriptor = new HANANClobJdbcType( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); - - private HANABlobType blobTypeDescriptor = new HANABlobType( MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); - - private HANAClobJdbcType clobTypeDescriptor; + private final int maxLobPrefetchSize; + private boolean defaultTableTypeColumn; private boolean useLegacyBooleanType = USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE; private boolean useUnicodeStringTypes; + private boolean treatDoubleTypedFieldsAsDecimal; /* * Tables named "TYPE" need to be quoted @@ -232,14 +235,56 @@ public abstract class AbstractHANADialect extends Dialect { } }; - public AbstractHANADialect(DatabaseVersion version) { - super( version ); + public AbstractHANADialect(DatabaseVersion version) { + this( new HANAServerConfiguration( version ), true ); + } + + public AbstractHANADialect(HANAServerConfiguration configuration, boolean defaultTableTypeColumn) { + super( configuration.getFullVersion() ); + this.defaultTableTypeColumn = defaultTableTypeColumn; + this.maxLobPrefetchSize = configuration.getMaxLobPrefetchSize(); this.useUnicodeStringTypes = useUnicodeStringTypesDefault(); - this.clobTypeDescriptor = new HANAClobJdbcType( - MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE, - useUnicodeStringTypesDefault() + } + + @Override + public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + // This is the best hook for consuming dialect configuration that we have for now, + // since this method is called very early in the bootstrap process + final ConfigurationService configurationService = serviceRegistry.getService( ConfigurationService.class ); + assert configurationService != null; + + this.defaultTableTypeColumn = configurationService.getSetting( + USE_DEFAULT_TABLE_TYPE_COLUMN, + StandardConverters.BOOLEAN, + this.defaultTableTypeColumn ); + if ( supportsAsciiStringTypes() ) { + this.useUnicodeStringTypes = configurationService.getSetting( + USE_UNICODE_STRING_TYPES_PARAMETER_NAME, + StandardConverters.BOOLEAN, + useUnicodeStringTypesDefault() + ); + } + this.useLegacyBooleanType = configurationService.getSetting( + USE_LEGACY_BOOLEAN_TYPE_PARAMETER_NAME, + StandardConverters.BOOLEAN, + USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE + ); + this.treatDoubleTypedFieldsAsDecimal = configurationService.getSetting( + TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_PARAMETER_NAME, + StandardConverters.BOOLEAN, + TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_DEFAULT_VALUE + ); + super.contribute( typeContributions, serviceRegistry ); + } + + protected boolean isDefaultTableTypeColumn() { + return defaultTableTypeColumn; + } + + protected boolean isCloud() { + return getVersion().isSameOrAfter( 4 ); } @Override @@ -280,20 +325,6 @@ public abstract class AbstractHANADialect extends Dialect { @Override protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { - final ConfigurationService configurationService = serviceRegistry.getService( ConfigurationService.class ); - if ( supportsAsciiStringTypes() ) { - this.useUnicodeStringTypes = configurationService.getSetting( - USE_UNICODE_STRING_TYPES_PARAMETER_NAME, - StandardConverters.BOOLEAN, - useUnicodeStringTypesDefault() - ); - } - this.useLegacyBooleanType = configurationService.getSetting( - USE_LEGACY_BOOLEAN_TYPE_PARAMETER_NAME, - StandardConverters.BOOLEAN, - USE_LEGACY_BOOLEAN_TYPE_DEFAULT_VALUE - ); - super.registerColumnTypes( typeContributions, serviceRegistry ); final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); @@ -320,24 +351,12 @@ public abstract class AbstractHANADialect extends Dialect { return false; } + /** + * @deprecated Use {@link HANAServerConfiguration#fromDialectResolutionInfo(DialectResolutionInfo)} instead + */ + @Deprecated(forRemoval = true) protected static DatabaseVersion createVersion(DialectResolutionInfo info) { - // Parse the version according to https://answers.sap.com/questions/9760991/hana-sps-version-check.html - final String versionString = info.getDatabaseVersion(); - int majorVersion = 1; - int minorVersion = 0; - int patchLevel = 0; - final String[] components = versionString.split( "\\." ); - if ( components.length >= 3 ) { - try { - majorVersion = Integer.parseInt( components[0] ); - minorVersion = Integer.parseInt( components[1] ); - patchLevel = Integer.parseInt( components[2] ); - } - catch (NumberFormatException ex) { - // Ignore - } - } - return DatabaseVersion.make( majorVersion, minorVersion, patchLevel ); + return HANAServerConfiguration.fromDialectResolutionInfo( info ).getFullVersion(); } @Override @@ -434,6 +453,22 @@ public abstract class AbstractHANADialect extends Dialect { functionContributions.getFunctionRegistry().register( "timestampadd", new IntegralTimestampaddFunction( this, typeConfiguration ) ); + + // full-text search functions + functionContributions.getFunctionRegistry().registerNamed( + "score", + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) + ); + functionContributions.getFunctionRegistry().registerNamed( "snippets" ); + functionContributions.getFunctionRegistry().registerNamed( "highlighted" ); + functionContributions.getFunctionRegistry().registerBinaryTernaryPattern( + "contains", + typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.BOOLEAN ), + "contains(?1,?2)", + "contains(?1,?2,?3)", + ANY, ANY, ANY, + typeConfiguration + ); } @Override @@ -526,7 +561,15 @@ public abstract class AbstractHANADialect extends Dialect { final String constraintName = getViolatedConstraintNameExtractor() .extractConstraintName( sqlException ); - return new ConstraintViolationException( message, sqlException, sql, constraintName ); + return new ConstraintViolationException( + message, + sqlException, + sql, + errorCode == 301 + ? ConstraintViolationException.ConstraintKind.UNIQUE + : ConstraintViolationException.ConstraintKind.OTHER, + constraintName + ); } return null; @@ -538,6 +581,11 @@ public abstract class AbstractHANADialect extends Dialect { return RowLockStrategy.COLUMN; } + @Override + public String getCreateTableString() { + return isDefaultTableTypeColumn() ? "create column table" : "create row table"; + } + @Override public String getAddColumnString() { return "add ("; @@ -621,6 +669,7 @@ public abstract class AbstractHANADialect extends Dialect { @Override protected void registerDefaultKeywords() { super.registerDefaultKeywords(); + // https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/28bcd6af3eb6437892719f7c27a8a285.html?locale=en-US registerKeyword( "all" ); registerKeyword( "alter" ); registerKeyword( "as" ); @@ -668,6 +717,7 @@ public abstract class AbstractHANADialect extends Dialect { registerKeyword( "into" ); registerKeyword( "is" ); registerKeyword( "join" ); + registerKeyword( "lateral" ); registerKeyword( "leading" ); registerKeyword( "left" ); registerKeyword( "limit" ); @@ -706,6 +756,29 @@ public abstract class AbstractHANADialect extends Dialect { registerKeyword( "where" ); registerKeyword( "while" ); registerKeyword( "with" ); + if ( isCloud() ) { + // https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/reserved-words + registerKeyword( "array" ); + registerKeyword( "at" ); + registerKeyword( "authorization" ); + registerKeyword( "between" ); + registerKeyword( "by" ); + registerKeyword( "collate" ); + registerKeyword( "empty" ); + registerKeyword( "filter" ); + registerKeyword( "grouping" ); + registerKeyword( "no" ); + registerKeyword( "not" ); + registerKeyword( "of" ); + registerKeyword( "over" ); + registerKeyword( "recursive" ); + registerKeyword( "row" ); + registerKeyword( "table" ); + registerKeyword( "to" ); + registerKeyword( "unnest" ); + registerKeyword( "window" ); + registerKeyword( "within" ); + } } @Override @@ -930,127 +1003,40 @@ public abstract class AbstractHANADialect extends Dialect { public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.contributeTypes( typeContributions, serviceRegistry ); - final ConnectionProvider connectionProvider = serviceRegistry.getService( ConnectionProvider.class ); - - int maxLobPrefetchSizeDefault = MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE; - if ( connectionProvider != null ) { - Connection conn = null; - try { - conn = connectionProvider.getConnection(); - try ( Statement statement = conn.createStatement() ) { - try ( ResultSet rs = statement.executeQuery( - "SELECT TOP 1 VALUE,MAP(LAYER_NAME,'DEFAULT',1,'SYSTEM',2,'DATABASE',3,4) AS LAYER FROM SYS.M_INIFILE_CONTENTS WHERE FILE_NAME='indexserver.ini' AND SECTION='session' AND KEY='max_lob_prefetch_size' ORDER BY LAYER DESC" ) ) { - // This only works if the current user has the privilege INIFILE ADMIN - if ( rs.next() ) { - maxLobPrefetchSizeDefault = rs.getInt( 1 ); - } - } - } - } - catch (Exception e) { - LOG.debug( - "An error occurred while trying to determine the value of the HANA parameter indexserver.ini / session / max_lob_prefetch_size. Using the default value " - + maxLobPrefetchSizeDefault, - e ); - } - finally { - if ( conn != null ) { - try { - connectionProvider.closeConnection( conn ); - } - catch (SQLException e) { - // ignore - } - } - } - } - - final ConfigurationService configurationService = serviceRegistry.getService( ConfigurationService.class ); - int maxLobPrefetchSize = configurationService.getSetting( - MAX_LOB_PREFETCH_SIZE_PARAMETER_NAME, - value -> Integer.valueOf( value.toString() ), - maxLobPrefetchSizeDefault - ); - - if ( this.nClobTypeDescriptor.getMaxLobPrefetchSize() != maxLobPrefetchSize ) { - this.nClobTypeDescriptor = new HANANClobJdbcType( maxLobPrefetchSize ); - } - - if ( this.blobTypeDescriptor.getMaxLobPrefetchSize() != maxLobPrefetchSize ) { - this.blobTypeDescriptor = new HANABlobType( maxLobPrefetchSize ); - } - - if ( supportsAsciiStringTypes() ) { - if ( this.clobTypeDescriptor.getMaxLobPrefetchSize() != maxLobPrefetchSize - || this.clobTypeDescriptor.isUseUnicodeStringTypes() != this.useUnicodeStringTypes ) { - this.clobTypeDescriptor = new HANAClobJdbcType( maxLobPrefetchSize, this.useUnicodeStringTypes ); - } - } - - boolean treatDoubleTypedFieldsAsDecimal = configurationService.getSetting(TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_PARAMETER_NAME, StandardConverters.BOOLEAN, - TREAT_DOUBLE_TYPED_FIELDS_AS_DECIMAL_DEFAULT_VALUE); - - final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() - .getJdbcTypeRegistry(); - if (treatDoubleTypedFieldsAsDecimal) { - typeContributions.getTypeConfiguration().getBasicTypeRegistry() + final TypeConfiguration typeConfiguration = typeContributions.getTypeConfiguration(); + final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); + if ( treatDoubleTypedFieldsAsDecimal ) { + typeConfiguration.getBasicTypeRegistry() .register( - new BasicTypeImpl<>( - DoubleJavaType.INSTANCE, - NumericJdbcType.INSTANCE - ), + new BasicTypeImpl<>( DoubleJavaType.INSTANCE, NumericJdbcType.INSTANCE ), Double.class.getName() ); - typeContributions.getTypeConfiguration().getJdbcToHibernateTypeContributionMap() - .computeIfAbsent( Types.FLOAT, code -> new HashSet<>() ) - .clear(); - typeContributions.getTypeConfiguration().getJdbcToHibernateTypeContributionMap() - .computeIfAbsent( Types.REAL, code -> new HashSet<>() ) - .clear(); - typeContributions.getTypeConfiguration().getJdbcToHibernateTypeContributionMap() - .computeIfAbsent( Types.DOUBLE, code -> new HashSet<>() ) - .clear(); - typeContributions.getTypeConfiguration().getJdbcToHibernateTypeContributionMap() - .get( Types.FLOAT ) - .add( StandardBasicTypes.BIG_DECIMAL.getName() ); - typeContributions.getTypeConfiguration().getJdbcToHibernateTypeContributionMap() - .get( Types.REAL ) - .add( StandardBasicTypes.BIG_DECIMAL.getName() ); - typeContributions.getTypeConfiguration().getJdbcToHibernateTypeContributionMap() - .get( Types.DOUBLE ) - .add( StandardBasicTypes.BIG_DECIMAL.getName() ); - jdbcTypeRegistry.addDescriptor( - Types.FLOAT, - NumericJdbcType.INSTANCE - ); - jdbcTypeRegistry.addDescriptor( - Types.REAL, - NumericJdbcType.INSTANCE - ); - jdbcTypeRegistry.addDescriptor( - Types.DOUBLE, - NumericJdbcType.INSTANCE - ); + final Map> jdbcToHibernateTypeContributionMap = typeConfiguration.getJdbcToHibernateTypeContributionMap(); + jdbcToHibernateTypeContributionMap.computeIfAbsent( Types.FLOAT, code -> new HashSet<>() ).clear(); + jdbcToHibernateTypeContributionMap.computeIfAbsent( Types.REAL, code -> new HashSet<>() ).clear(); + jdbcToHibernateTypeContributionMap.computeIfAbsent( Types.DOUBLE, code -> new HashSet<>() ).clear(); + jdbcToHibernateTypeContributionMap.get( Types.FLOAT ).add( StandardBasicTypes.BIG_DECIMAL.getName() ); + jdbcToHibernateTypeContributionMap.get( Types.REAL ).add( StandardBasicTypes.BIG_DECIMAL.getName() ); + jdbcToHibernateTypeContributionMap.get( Types.DOUBLE ).add( StandardBasicTypes.BIG_DECIMAL.getName() ); + jdbcTypeRegistry.addDescriptor( Types.FLOAT, NumericJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( Types.REAL, NumericJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( Types.DOUBLE, NumericJdbcType.INSTANCE ); } - jdbcTypeRegistry.addDescriptor( Types.CLOB, this.clobTypeDescriptor ); - jdbcTypeRegistry.addDescriptor( Types.NCLOB, this.nClobTypeDescriptor ); - jdbcTypeRegistry.addDescriptor( Types.BLOB, this.blobTypeDescriptor ); + jdbcTypeRegistry.addDescriptor( Types.CLOB, new HANAClobJdbcType( maxLobPrefetchSize, useUnicodeStringTypes ) ); + jdbcTypeRegistry.addDescriptor( Types.NCLOB, new HANANClobJdbcType( maxLobPrefetchSize ) ); + jdbcTypeRegistry.addDescriptor( Types.BLOB, new HANABlobType( maxLobPrefetchSize ) ); // tinyint is unsigned on HANA jdbcTypeRegistry.addDescriptor( Types.TINYINT, TinyIntAsSmallIntJdbcType.INSTANCE ); if ( isUseUnicodeStringTypes() ) { jdbcTypeRegistry.addDescriptor( Types.VARCHAR, NVarcharJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( Types.CHAR, NCharJdbcType.INSTANCE ); } - if (treatDoubleTypedFieldsAsDecimal) { + if ( treatDoubleTypedFieldsAsDecimal ) { jdbcTypeRegistry.addDescriptor( Types.DOUBLE, DecimalJdbcType.INSTANCE ); } } - public JdbcType getBlobTypeDescriptor() { - return this.blobTypeDescriptor; - } - @Override public void appendBooleanValueString(SqlAppender appender, boolean bool) { if ( this.useLegacyBooleanType ) { @@ -1266,12 +1252,16 @@ public abstract class AbstractHANADialect extends Dialect { } public boolean isUseUnicodeStringTypes() { - return this.useUnicodeStringTypes; + return this.useUnicodeStringTypes || isDefaultTableTypeColumn() && isCloud(); } - protected abstract boolean supportsAsciiStringTypes(); + protected boolean supportsAsciiStringTypes() { + return !isDefaultTableTypeColumn() || !isCloud(); + } - protected abstract Boolean useUnicodeStringTypesDefault(); + protected Boolean useUnicodeStringTypesDefault() { + return isDefaultTableTypeColumn() ? isCloud() : Boolean.FALSE; + } private static class CloseSuppressingReader extends FilterReader { @@ -1756,6 +1746,7 @@ public abstract class AbstractHANADialect extends Dialect { public static class HANABlobType implements JdbcType { private static final long serialVersionUID = 5874441715643764323L; + public static final JdbcType INSTANCE = new HANABlobType( HANAServerConfiguration.MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); final int maxLobPrefetchSize; @@ -1843,4 +1834,59 @@ public abstract class AbstractHANADialect extends Dialect { return this.maxLobPrefetchSize; } } + + @Override + public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( + EntityMappingType entityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + return new GlobalTemporaryTableMutationStrategy( + TemporaryTable.createIdTable( + entityDescriptor, + basename -> TemporaryTable.ID_TABLE_PREFIX + basename, + this, + runtimeModelCreationContext + ), + runtimeModelCreationContext.getSessionFactory() + ); + } + + @Override + public SqmMultiTableInsertStrategy getFallbackSqmInsertStrategy( + EntityMappingType entityDescriptor, + RuntimeModelCreationContext runtimeModelCreationContext) { + return new GlobalTemporaryTableInsertStrategy( + TemporaryTable.createEntityTable( + entityDescriptor, + name -> TemporaryTable.ENTITY_TABLE_PREFIX + name, + this, + runtimeModelCreationContext + ), + runtimeModelCreationContext.getSessionFactory() + ); + } + + @Override + public TemporaryTableKind getSupportedTemporaryTableKind() { + return TemporaryTableKind.GLOBAL; + } + + @Override + public String getTemporaryTableCreateOptions() { + return "on commit delete rows"; + } + + @Override + public String getTemporaryTableCreateCommand() { + return "create global temporary row table"; + } + + @Override + public String getTemporaryTableTruncateCommand() { + return "truncate table"; + } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } } 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 b3aaa646b2..19f95eb552 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -572,6 +572,11 @@ public class CockroachDialect extends Dialect { return true; } + @Override + public boolean supportsConflictClauseForInsertCTE() { + return true; + } + @Override public String getNoColumnsInsertString() { return "default values"; @@ -1160,4 +1165,14 @@ public class CockroachDialect extends Dialect { // RuntimeModelCreationContext runtimeModelCreationContext) { // return new CteInsertStrategy( rootEntityDescriptor, runtimeModelCreationContext ); // } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java index d90f325f1b..e5ca18d474 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachSqlAstTranslator.java @@ -7,20 +7,27 @@ package org.hibernate.dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; +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.cte.CteMaterialization; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; /** * A SQL AST translator for Cockroach. @@ -33,6 +40,56 @@ public class CockroachSqlAstTranslator extends Abstract super( sessionFactory, statement ); } + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { + final String identificationVariable = tableReference.getIdentificationVariable(); + if ( identificationVariable != null ) { + final Clause currentClause = getClauseStack().getCurrent(); + if ( currentClause == Clause.INSERT ) { + // PostgreSQL requires the "as" keyword for inserts + appendSql( " as " ); + } + else { + append( WHITESPACE ); + } + append( tableReference.getIdentificationVariable() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + final Statement currentStatement = getStatementStack().getCurrent(); + if ( !( currentStatement instanceof UpdateStatement ) + || !hasNonTrivialFromClause( ( (UpdateStatement) currentStatement ).getFromClause() ) ) { + // For UPDATE statements we render a full FROM clause and a join condition to match target table rows, + // but for that to work, we have to omit the alias for the target table reference here + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + renderFromClauseJoiningDmlTargetReference( statement ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitStandardConflictClause( conflictClause ); + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index 1abe8c44f7..ca531daa49 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -42,8 +42,11 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; 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.LockTimeoutException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.mapping.Column; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -87,6 +90,7 @@ import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.TemporalType; +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.BLOB; import static org.hibernate.type.SqlTypes.BOOLEAN; @@ -973,6 +977,20 @@ public class DB2Dialect extends Dialect { appender.appendSql( '\'' ); } + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return new TemplatedViolatedConstraintNameExtractor( + sqle -> { + switch ( JdbcExceptionHelper.extractErrorCode( sqle ) ) { + case -803: + return extractUsingTemplate( "SQLERRMC=1;", ",", sqle.getMessage() ); + default: + return null; + } + } + ); + } + @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { @@ -980,6 +998,14 @@ public class DB2Dialect extends Dialect { switch ( errorCode ) { case -952: return new LockTimeoutException( message, sqlException, sql ); + case -803: + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); } return null; }; @@ -1171,4 +1197,14 @@ public class DB2Dialect extends Dialect { public int rowIdSqlType() { return VARBINARY; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return getDB2Version().isSameOrAfter( 11 ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java index 30bb8236bf..c1f1d594ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java @@ -12,8 +12,10 @@ import java.util.function.Consumer; import org.hibernate.LockMode; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; @@ -27,10 +29,12 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.Predicate; @@ -347,12 +351,40 @@ public class DB2SqlAstTranslator extends AbstractSqlAst @Override protected void visitInsertStatementOnly(InsertSelectStatement statement) { final boolean closeWrapper = renderReturningClause( statement ); - super.visitInsertStatementOnly( statement ); + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } if ( closeWrapper ) { appendSql( ')' ); } } + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + renderFromClauseExcludingDmlTargetReference( statement ); + } + protected boolean renderReturningClause(MutationStatement statement) { final List returningColumns = statement.getReturningColumns(); if ( isEmpty( returningColumns ) ) { @@ -521,13 +553,13 @@ public class DB2SqlAstTranslator extends AbstractSqlAst } @Override - protected String getFromDual() { - return " from sysibm.dual"; + protected String getDual() { + return "sysibm.dual"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Database.java b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java index 0f0803553d..0e323ae564 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Database.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java @@ -135,7 +135,7 @@ public enum Database { HANA { @Override public Dialect createDialect(DialectResolutionInfo info) { - return new HANAColumnStoreDialect( info ); + return new HANADialect( info ); } @Override public boolean productNameMatches(String databaseName) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java index 339665ac2a..db1f634f58 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java @@ -35,8 +35,11 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; 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.LockTimeoutException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; @@ -690,14 +693,42 @@ public class DerbyDialect extends Dialect { return 512; } + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return new TemplatedViolatedConstraintNameExtractor( sqle -> { + final String sqlState = JdbcExceptionHelper.extractSqlState( sqle ); + if ( sqlState != null ) { + switch ( sqlState ) { + case "23505": + return TemplatedViolatedConstraintNameExtractor.extractUsingTemplate( + "'", "'", + sqle.getMessage() + ); + } + } + return null; + } ); + } + @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); // final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); + final String constraintName; if ( sqlState != null ) { switch ( sqlState ) { + case "23505": + // Unique constraint violation + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); case "40XL1": case "40XL2": return new LockTimeoutException( message, sqlException, sql ); @@ -1013,4 +1044,5 @@ public class DerbyDialect extends Dialect { public UniqueDelegate getUniqueDelegate() { return uniqueDelegate; } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java index be9f8577bb..e7db2e76a2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java @@ -233,13 +233,13 @@ public class DerbySqlAstTranslator extends AbstractSqlA } @Override - protected String getFromDual() { - return " from (values 0) dual"; + protected String getDual() { + return "(values 0)"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual() + " dual"; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index d37c797236..f0f943b25f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -4359,6 +4359,17 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun return false; } + /** + * Does this dialect support the {@code conflict} clause for insert statements + * that appear in a CTE? + * + * @return {@code true} if {@code conflict} clause is supported + * @since 6.5 + */ + public boolean supportsConflictClauseForInsertCTE() { + return false; + } + /** * Does this dialect support {@code values} lists of form * {@code VALUES (1), (2), (3)}? @@ -4380,6 +4391,16 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun return true; } + /** + * Does this dialect support the {@code from} clause for update statements? + * + * @return {@code true} if {@code from} clause is supported + * @since 6.5 + */ + public boolean supportsFromClauseInUpdate() { + return false; + } + /** * Does this dialect support {@code SKIP_LOCKED} timeout. * diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 992283b679..be6ae66a37 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -721,8 +721,19 @@ public class H2Dialect extends Dialect { public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); + final String constraintName; switch (errorCode) { + case 23505: + // Unique constraint violation + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); case 40001: // DEADLOCK DETECTED return new LockAcquisitionException(message, sqlException, sql); @@ -731,7 +742,7 @@ public class H2Dialect extends Dialect { return new PessimisticLockException(message, sqlException, sql); case 90006: // NULL not allowed for column [90006-145] - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); return new ConstraintViolationException(message, sqlException, sql, constraintName); case 57014: return new QueryTimeoutException( message, sqlException, sql ); @@ -935,4 +946,9 @@ public class H2Dialect extends Dialect { return "?" + position; } } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java index 258d5cab97..dc7d1e3dbb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java @@ -11,7 +11,9 @@ import java.util.List; import org.hibernate.LockMode; import org.hibernate.dialect.identity.H2IdentityColumnSupport; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; @@ -23,13 +25,17 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.internal.TableInsertStandard; @@ -80,6 +86,44 @@ public class H2SqlAstTranslator extends SqlAstTranslato ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + if ( hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitUpdateStatementEmulateMerge( statement ); + } + else { + super.visitUpdateStatementOnly( statement ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected void visitReturningColumns(List returningColumns) { // do nothing - this is handled via `#visitReturningInsertStatement` @@ -267,11 +311,10 @@ public class H2SqlAstTranslator extends SqlAstTranslato } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } - private boolean supportsOffsetFetchClause() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANACloudColumnStoreDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANACloudColumnStoreDialect.java index 6818096036..7170aa97b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANACloudColumnStoreDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANACloudColumnStoreDialect.java @@ -19,35 +19,14 @@ package org.hibernate.dialect; * * @author Jonathan Bregler * - * @deprecated use HANAColumnStoreDialect(400) + * @deprecated use {@link HANADialect} with {@code DatabaseVersion.make( 4 )} instead */ -@Deprecated +@Deprecated(forRemoval = true) public class HANACloudColumnStoreDialect extends HANAColumnStoreDialect { public HANACloudColumnStoreDialect() { // No idea how the versioning scheme is here, but since this is deprecated anyway, keep it as is - super( DatabaseVersion.make( 4 ) ); - } - - @Override - public boolean supportsLateral() { - // Couldn't find a reference since when this is supported - return true; - } - - @Override - protected boolean supportsAsciiStringTypes() { - return getVersion().isBefore( 4 ); - } - - @Override - protected Boolean useUnicodeStringTypesDefault() { - return getVersion().isSameOrAfter( 4 ); - } - - @Override - public boolean isUseUnicodeStringTypes() { - return getVersion().isSameOrAfter( 4 ) || super.isUseUnicodeStringTypes(); + super( new HANAServerConfiguration( DatabaseVersion.make( 4 ) ) ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java index 04cc380e72..763647cdc5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java @@ -34,11 +34,14 @@ import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY * * @author Andrew Clemons * @author Jonathan Bregler + * + * @deprecated use {@link HANADialect} instead */ +@Deprecated(forRemoval = true) public class HANAColumnStoreDialect extends AbstractHANADialect { public HANAColumnStoreDialect(DialectResolutionInfo info) { - this( AbstractHANADialect.createVersion( info ) ); + this( HANAServerConfiguration.fromDialectResolutionInfo( info ) ); registerKeywords( info ); } @@ -48,129 +51,10 @@ public class HANAColumnStoreDialect extends AbstractHANADialect { } public HANAColumnStoreDialect(DatabaseVersion version) { - super( version ); + this( new HANAServerConfiguration( version ) ); } - @Override - public boolean isUseUnicodeStringTypes() { - return getVersion().isSameOrAfter( 4 ) || super.isUseUnicodeStringTypes(); - } - - @Override - public int getMaxVarcharLength() { - return 5000; - } - - @Override - protected void registerDefaultKeywords() { - super.registerDefaultKeywords(); - registerKeyword( "array" ); - registerKeyword( "at" ); - registerKeyword( "authorization" ); - registerKeyword( "between" ); - registerKeyword( "by" ); - registerKeyword( "collate" ); - registerKeyword( "empty" ); - registerKeyword( "filter" ); - registerKeyword( "grouping" ); - registerKeyword( "no" ); - registerKeyword( "not" ); - registerKeyword( "of" ); - registerKeyword( "over" ); - registerKeyword( "recursive" ); - registerKeyword( "row" ); - registerKeyword( "table" ); - registerKeyword( "to" ); - registerKeyword( "window" ); - registerKeyword( "within" ); - } - - - @Override - public void initializeFunctionRegistry(FunctionContributions functionContributions) { - super.initializeFunctionRegistry(functionContributions); - final TypeConfiguration typeConfiguration = functionContributions.getTypeConfiguration(); - - // full-text search functions - functionContributions.getFunctionRegistry().registerNamed( - "score", - typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) - ); - functionContributions.getFunctionRegistry().registerNamed( "snippets" ); - functionContributions.getFunctionRegistry().registerNamed( "highlighted" ); - functionContributions.getFunctionRegistry().registerBinaryTernaryPattern( - "contains", - typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.BOOLEAN ), - "contains(?1,?2)", - "contains(?1,?2,?3)", - ANY, ANY, ANY, - typeConfiguration - ); - } - - @Override - public String getCreateTableString() { - return "create column table"; - } - - @Override - public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( - EntityMappingType entityDescriptor, - RuntimeModelCreationContext runtimeModelCreationContext) { - return new GlobalTemporaryTableMutationStrategy( - TemporaryTable.createIdTable( - entityDescriptor, - basename -> TemporaryTable.ID_TABLE_PREFIX + basename, - this, - runtimeModelCreationContext - ), - runtimeModelCreationContext.getSessionFactory() - ); - } - - @Override - public SqmMultiTableInsertStrategy getFallbackSqmInsertStrategy( - EntityMappingType entityDescriptor, - RuntimeModelCreationContext runtimeModelCreationContext) { - return new GlobalTemporaryTableInsertStrategy( - TemporaryTable.createEntityTable( - entityDescriptor, - name -> TemporaryTable.ENTITY_TABLE_PREFIX + name, - this, - runtimeModelCreationContext - ), - runtimeModelCreationContext.getSessionFactory() - ); - } - - @Override - public TemporaryTableKind getSupportedTemporaryTableKind() { - return TemporaryTableKind.GLOBAL; - } - - @Override - public String getTemporaryTableCreateOptions() { - return "on commit delete rows"; - } - - @Override - public String getTemporaryTableCreateCommand() { - // We use a row table for temporary tables here because HANA doesn't support UPDATE on temporary column tables - return "create global temporary row table"; - } - - @Override - public String getTemporaryTableTruncateCommand() { - return "truncate table"; - } - - @Override - protected boolean supportsAsciiStringTypes() { - return true; - } - - @Override - protected Boolean useUnicodeStringTypesDefault() { - return true; + public HANAColumnStoreDialect(HANAServerConfiguration configuration) { + super( configuration, true ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java new file mode 100644 index 0000000000..3352cbccc7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect; + +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; + +/** + * An SQL dialect for the SAP HANA Platform and Cloud. + *

+ * For more information on SAP HANA Cloud, refer to the + * SAP HANA Cloud SQL Reference Guide. + * For more information on SAP HANA Platform, refer to the + * SAP HANA Platform SQL Reference Guide. + *

+ * Column tables are created by this dialect by default when using the auto-ddl feature. + * + * @author Andrew Clemons + * @author Jonathan Bregler + */ +@SuppressWarnings("removal") +public class HANADialect extends AbstractHANADialect { + + private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 1, 0, 120 ); + + public HANADialect(DialectResolutionInfo info) { + this( HANAServerConfiguration.fromDialectResolutionInfo( info ), true ); + registerKeywords( info ); + } + + public HANADialect() { + // SAP HANA 1.0 SPS12 R0 is the default + this( MINIMUM_VERSION ); + } + + public HANADialect(DatabaseVersion version) { + this( new HANAServerConfiguration( version ), true ); + } + + public HANADialect(DatabaseVersion version, boolean defaultTableTypeColumn) { + this( new HANAServerConfiguration( version ), defaultTableTypeColumn ); + } + + public HANADialect(HANAServerConfiguration configuration, boolean defaultTableTypeColumn) { + super( configuration, defaultTableTypeColumn ); + } + + @Override + protected DatabaseVersion getMinimumSupportedVersion() { + return MINIMUM_VERSION; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANARowStoreDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANARowStoreDialect.java index 7b9e57ba69..2d0f771c3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANARowStoreDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANARowStoreDialect.java @@ -29,11 +29,14 @@ import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; * * @author Andrew Clemons * @author Jonathan Bregler + * + * @deprecated use {@link HANADialect} instead */ +@Deprecated(forRemoval = true) public class HANARowStoreDialect extends AbstractHANADialect { public HANARowStoreDialect(DialectResolutionInfo info) { - this( AbstractHANADialect.createVersion( info ) ); + this( HANAServerConfiguration.fromDialectResolutionInfo( info ) ); registerKeywords( info ); } @@ -43,71 +46,10 @@ public class HANARowStoreDialect extends AbstractHANADialect { } public HANARowStoreDialect(DatabaseVersion version) { - super( version ); + this( new HANAServerConfiguration( version ) ); } - @Override - public String getCreateTableString() { - return "create row table"; - } - - @Override - public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( - EntityMappingType entityDescriptor, - RuntimeModelCreationContext runtimeModelCreationContext) { - return new GlobalTemporaryTableMutationStrategy( - TemporaryTable.createIdTable( - entityDescriptor, - basename -> TemporaryTable.ID_TABLE_PREFIX + basename, - this, - runtimeModelCreationContext - ), - runtimeModelCreationContext.getSessionFactory() - ); - } - - @Override - public SqmMultiTableInsertStrategy getFallbackSqmInsertStrategy( - EntityMappingType entityDescriptor, - RuntimeModelCreationContext runtimeModelCreationContext) { - return new GlobalTemporaryTableInsertStrategy( - TemporaryTable.createEntityTable( - entityDescriptor, - name -> TemporaryTable.ENTITY_TABLE_PREFIX + name, - this, - runtimeModelCreationContext - ), - runtimeModelCreationContext.getSessionFactory() - ); - } - - @Override - public TemporaryTableKind getSupportedTemporaryTableKind() { - return TemporaryTableKind.GLOBAL; - } - - @Override - public String getTemporaryTableCreateOptions() { - return "on commit delete rows"; - } - - @Override - public String getTemporaryTableCreateCommand() { - return "create global temporary row table"; - } - - @Override - public String getTemporaryTableTruncateCommand() { - return "truncate table"; - } - - @Override - protected boolean supportsAsciiStringTypes() { - return true; - } - - @Override - protected Boolean useUnicodeStringTypesDefault() { - return Boolean.FALSE; + public HANARowStoreDialect(HANAServerConfiguration configuration) { + super( configuration, false ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java new file mode 100644 index 0000000000..4e7d36d776 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java @@ -0,0 +1,99 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.internal.util.config.ConfigurationHelper; + +import static org.hibernate.cfg.DialectSpecificSettings.HANA_MAX_LOB_PREFETCH_SIZE; + +/** + * Utility class that extract some initial configuration from the database for {@link HANADialect}. + */ +public class HANAServerConfiguration { + + private static final CoreMessageLogger LOG = CoreLogging.messageLogger( HANAServerConfiguration.class ); + public static final int MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE = 1024; + + private final DatabaseVersion fullVersion; + private final int maxLobPrefetchSize; + + public HANAServerConfiguration(DatabaseVersion fullVersion) { + this( fullVersion, MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE ); + } + + public HANAServerConfiguration(DatabaseVersion fullVersion, int maxLobPrefetchSize) { + this.fullVersion = fullVersion; + this.maxLobPrefetchSize = maxLobPrefetchSize; + } + + public DatabaseVersion getFullVersion() { + return fullVersion; + } + + public int getMaxLobPrefetchSize() { + return maxLobPrefetchSize; + } + + public static HANAServerConfiguration fromDialectResolutionInfo(DialectResolutionInfo info) { + Integer maxLobPrefetchSize = null; + final DatabaseMetaData databaseMetaData = info.getDatabaseMetadata(); + if ( databaseMetaData != null ) { + try (final Statement statement = databaseMetaData.getConnection().createStatement()) { + try ( ResultSet rs = statement.executeQuery( + "SELECT TOP 1 VALUE,MAP(LAYER_NAME,'DEFAULT',1,'SYSTEM',2,'DATABASE',3,4) AS LAYER FROM SYS.M_INIFILE_CONTENTS WHERE FILE_NAME='indexserver.ini' AND SECTION='session' AND KEY='max_lob_prefetch_size' ORDER BY LAYER DESC" ) ) { + // This only works if the current user has the privilege INIFILE ADMIN + if ( rs.next() ) { + maxLobPrefetchSize = rs.getInt( 1 ); + } + } + } + catch (SQLException e) { + // Ignore + LOG.debug( + "An error occurred while trying to determine the value of the HANA parameter indexserver.ini / session / max_lob_prefetch_size.", + e ); + } + } + // default to the dialect-specific configuration settings + if ( maxLobPrefetchSize == null ) { + maxLobPrefetchSize = ConfigurationHelper.getInt( + HANA_MAX_LOB_PREFETCH_SIZE, + info.getConfigurationValues(), + MAX_LOB_PREFETCH_SIZE_DEFAULT_VALUE + ); + } + return new HANAServerConfiguration( createVersion( info ), maxLobPrefetchSize ); + } + + private static DatabaseVersion createVersion(DialectResolutionInfo info) { + // Parse the version according to https://answers.sap.com/questions/9760991/hana-sps-version-check.html + final String versionString = info.getDatabaseVersion(); + int majorVersion = 1; + int minorVersion = 0; + int patchLevel = 0; + final String[] components = versionString.split( "\\." ); + if ( components.length >= 3 ) { + try { + majorVersion = Integer.parseInt( components[0] ); + minorVersion = Integer.parseInt( components[1] ); + patchLevel = Integer.parseInt( components[2] ); + } + catch (NumberFormatException ex) { + // Ignore + } + } + return DatabaseVersion.make( majorVersion, minorVersion, patchLevel ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java index 4a0cc3af5a..f543a4c228 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java @@ -8,24 +8,34 @@ package org.hibernate.dialect; import java.util.List; +import org.hibernate.LockMode; import org.hibernate.MappingException; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.FunctionTableReference; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.internal.TableInsertStandard; @@ -42,6 +52,83 @@ public class HANASqlAstTranslator extends AbstractSqlAs super( sessionFactory, statement ); } + @SuppressWarnings("removal") + private boolean isHanaCloud() { + return ( (AbstractHANADialect) getDialect() ).isCloud(); + } + + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + // HANA Cloud does not support the FROM clause in UPDATE statements + if ( isHanaCloud() && hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitUpdateStatementEmulateMerge( statement ); + } + else { + super.visitUpdateStatementOnly( statement ); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + // HANA Cloud does not support the FROM clause in UPDATE statements + if ( isHanaCloud() ) { + super.renderUpdateClause( updateStatement ); + } + else { + appendSql( "update" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + renderTableReferenceIdentificationVariable( updateStatement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + // HANA Cloud does not support the FROM clause in UPDATE statements + if ( !isHanaCloud() ) { + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + protected boolean shouldEmulateFetchClause(QueryPart queryPart) { // HANA only supports the LIMIT + OFFSET syntax but also window functions // Check if current query part is already row numbering to avoid infinite recursion @@ -141,13 +228,13 @@ public class HANASqlAstTranslator extends AbstractSqlAs } @Override - protected String getFromDual() { - return " from sys.dummy"; + protected String getDual() { + return "sys.dummy"; } @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } @Override @@ -165,4 +252,9 @@ public class HANASqlAstTranslator extends AbstractSqlAs protected void visitValuesList(List valuesList) { visitValuesListEmulateSelectUnion( valuesList ); } + + @Override + public void visitValuesTableReference(ValuesTableReference tableReference) { + emulateValuesTableReferenceColumnAliasing( tableReference ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index 94a5a84ad8..2357b9f9ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -27,6 +27,8 @@ import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; @@ -423,6 +425,29 @@ public class HSQLDialect extends Dialect { return null; } ); + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); + final String constraintName; + + switch ( errorCode ) { + case -104: + // Unique constraint violation + constraintName = getViolatedConstraintNameExtractor().extractConstraintName(sqlException); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); + } + + return null; + }; + } + @Override public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) { switch ( sqlType ) { @@ -683,4 +708,9 @@ public class HSQLDialect extends Dialect { public String quoteCollation(String collation) { return '\"' + collation + '\"'; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java index 96d36ae036..18a28a96e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java @@ -11,22 +11,30 @@ import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; @@ -41,6 +49,44 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + if ( hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitUpdateStatementEmulateMerge( statement ); + } + else { + super.visitUpdateStatementOnly( statement ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -283,14 +329,9 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs return false; } - @Override - protected String getFromDual() { - return " from (values(0))"; - } - @Override protected String getFromDualForSelectOnly() { - return getFromDual(); + return " from " + getDual(); } private boolean supportsOffsetFetchClause() { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java index ac611c6dba..123f446b01 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java @@ -6,21 +6,36 @@ */ package org.hibernate.dialect; +import java.util.ArrayList; +import java.util.List; + import org.hibernate.engine.spi.SessionFactoryImplementor; +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.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; /** * A SQL AST translator for MariaDB. @@ -36,6 +51,135 @@ public class MariaDBSqlAstTranslator extends AbstractSq this.dialect = (MariaDBDialect) DialectDelegateWrapper.extractRealDialect( super.getDialect() ); } + @Override + protected void visitInsertSource(InsertSelectStatement statement) { + if ( statement.getSourceSelectStatement() != null ) { + if ( statement.getConflictClause() != null ) { + final List targetColumnReferences = statement.getTargetColumns(); + final List columnNames = new ArrayList<>( targetColumnReferences.size() ); + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + columnNames.add( targetColumnReference.getColumnExpression() ); + } + appendSql( "select * from " ); + emulateQueryPartTableReferenceColumnAliasing( + new QueryPartTableReference( + new SelectStatement( statement.getSourceSelectStatement() ), + "excluded", + columnNames, + false, + getSessionFactory() + ) + ); + } + else { + statement.getSourceSelectStatement().accept( this ); + } + } + else { + visitValuesList( statement.getValuesList() ); + } + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + final Statement currentStatement; + if ( "excluded".equals( columnReference.getQualifier() ) + && ( currentStatement = getStatementStack().getCurrent() ) instanceof InsertSelectStatement + && ( (InsertSelectStatement) currentStatement ).getSourceSelectStatement() == null ) { + // Accessing the excluded row for an insert-values statement in the conflict clause requires the values qualifier + appendSql( "values(" ); + columnReference.appendReadExpression( this, null ); + append( ')' ); + } + else { + super.visitColumnReference( columnReference ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + super.renderUpdateClause( updateStatement ); + } + else { + appendSql( "update " ); + renderFromClauseSpaces( updateStatement.getFromClause() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitOnDuplicateKeyConflictClause( conflictClause ); + } + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + // Since MariaDB does not support aliasing the insert target table, + // we must detect column reference that are used in the conflict clause + // and use the table expression as qualifier instead + if ( getClauseStack().getCurrent() != Clause.SET + || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !getQueryPartStack().isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); + } + else { + return null; + } + } + @Override protected boolean supportsWithClauseInSubquery() { return false; @@ -216,8 +360,8 @@ public class MariaDBSqlAstTranslator extends AbstractSq } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override 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 2a065d37fa..c396df8e5c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -1513,4 +1513,14 @@ public class MySQLDialect extends Dialect { public String getEnableConstraintsStatement() { return "set foreign_key_checks = 1"; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java index 5c5059fcb8..f9ebad112d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java @@ -6,25 +6,39 @@ */ package org.hibernate.dialect; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.spi.SessionFactoryImplementor; +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.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; /** * A SQL AST translator for MySQL. @@ -87,6 +101,146 @@ public class MySQLSqlAstTranslator extends AbstractSqlA return sqlType; } + @Override + protected void visitInsertSource(InsertSelectStatement statement) { + if ( statement.getSourceSelectStatement() != null ) { + if ( statement.getConflictClause() != null ) { + final List targetColumnReferences = statement.getTargetColumns(); + final List columnNames = new ArrayList<>( targetColumnReferences.size() ); + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + columnNames.add( targetColumnReference.getColumnExpression() ); + } + appendSql( "select * from " ); + emulateQueryPartTableReferenceColumnAliasing( + new QueryPartTableReference( + new SelectStatement( statement.getSourceSelectStatement() ), + "excluded", + columnNames, + false, + getSessionFactory() + ) + ); + } + else { + statement.getSourceSelectStatement().accept( this ); + } + } + else { + visitValuesList( statement.getValuesList() ); + if ( statement.getConflictClause() != null && getDialect().getMySQLVersion().isSameOrAfter( 8, 0, 19 ) ) { + appendSql( " as excluded" ); + char separator = '('; + for ( ColumnReference targetColumn : statement.getTargetColumns() ) { + appendSql( separator ); + appendSql( targetColumn.getColumnExpression() ); + separator = ','; + } + appendSql( ')' ); + } + } + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + final Statement currentStatement; + if ( getDialect().getMySQLVersion().isBefore( 8, 0, 19 ) + && "excluded".equals( columnReference.getQualifier() ) + && ( currentStatement = getStatementStack().getCurrent() ) instanceof InsertSelectStatement + && ( (InsertSelectStatement) currentStatement ).getSourceSelectStatement() == null ) { + // Accessing the excluded row for an insert-values statement in the conflict clause requires the values qualifier + appendSql( "values(" ); + columnReference.appendReadExpression( this, null ); + append( ')' ); + } + else { + super.visitColumnReference( columnReference ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + super.renderUpdateClause( updateStatement ); + } + else { + appendSql( "update " ); + renderFromClauseSpaces( updateStatement.getFromClause() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitOnDuplicateKeyConflictClause( conflictClause ); + } + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + // Since MySQL does not support aliasing the insert target table, + // we must detect column reference that are used in the conflict clause + // and use the table expression as qualifier instead + if ( getClauseStack().getCurrent() != Clause.SET + || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !getQueryPartStack().isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); + } + else { + return null; + } + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -275,8 +429,8 @@ public class MySQLSqlAstTranslator extends AbstractSqlA } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index f88e29224c..9687eee598 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -1048,6 +1048,7 @@ public class OracleDialect extends Dialect { @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { + final String constraintName; // interpreting Oracle exceptions is much much more precise based on their specific vendor codes. switch ( JdbcExceptionHelper.extractErrorCode( sqlException ) ) { @@ -1080,9 +1081,19 @@ public class OracleDialect extends Dialect { // data integrity violation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + case 1: + // ORA-00001: unique constraint violated + constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); case 1407: // ORA-01407: cannot update column to NULL - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); return new ConstraintViolationException( message, sqlException, sql, constraintName ); default: @@ -1564,4 +1575,9 @@ public class OracleDialect extends Dialect { public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java index daddaacc43..e47f84bd49 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java @@ -33,25 +33,28 @@ import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.FromClause; import org.hibernate.sql.ast.tree.from.FunctionTableReference; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.UnionTableGroup; import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.ast.ColumnValueBinding; import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.type.SqlTypes; -import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; /** @@ -65,6 +68,54 @@ public class OracleSqlAstTranslator extends SqlAstTrans super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void visitUpdateStatementOnly(UpdateStatement statement) { + if ( hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitUpdateStatementEmulateInlineView( statement ); + } + else { + renderUpdateClause( statement ); + renderSetClause( statement.getAssignments() ); + visitWhereClause( statement.getRestriction() ); + visitReturningColumns( statement.getReturningColumns() ); + } + } + + @Override + protected void renderMergeUpdateClause(List assignments, Predicate wherePredicate) { + appendSql( " then update" ); + renderSetClause( assignments ); + visitWhereClause( wherePredicate ); + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean needsRecursiveKeywordInWithClause() { return false; @@ -171,7 +222,14 @@ public class OracleSqlAstTranslator extends SqlAstTrans @Override protected void visitValuesList(List valuesList) { - visitValuesListEmulateSelectUnion( valuesList ); + if ( valuesList.size() < 2 ) { + visitValuesListStandard( valuesList ); + } + else { + // Oracle doesn't support a multi-values insert + // So we render a select union emulation instead + visitValuesListEmulateSelectUnion( valuesList ); + } } @Override @@ -555,13 +613,13 @@ public class OracleSqlAstTranslator extends SqlAstTrans } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isSameOrAfter( 23 ) ? super.getFromDualForSelectOnly() : getFromDual(); + return getDialect().getVersion().isSameOrAfter( 23 ) ? "" : ( " from " + getDual() ); } private boolean supportsOffsetFetchClause() { 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 66f54e1fde..e2e79b7f34 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -787,6 +787,10 @@ public class PostgreSQLDialect extends Dialect { public boolean supportsNonQueryWithCTE() { return true; } + @Override + public boolean supportsConflictClauseForInsertCTE() { + return true; + } @Override public SequenceSupport getSequenceSupport() { @@ -1547,4 +1551,14 @@ public class PostgreSQLDialect extends Dialect { // The maximum scale for `interval second` is 6 unfortunately return 6; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java index cea73d022f..11c804945e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java @@ -10,6 +10,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.cte.CteStatement; @@ -17,6 +18,10 @@ import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; @@ -24,7 +29,10 @@ import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +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.TableInsertStandard; import org.hibernate.type.SqlTypes; @@ -58,6 +66,55 @@ public class PostgreSQLSqlAstTranslator extends SqlAstT appendSql( "default values" ); } + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { + final String identificationVariable = tableReference.getIdentificationVariable(); + if ( identificationVariable != null ) { + final Clause currentClause = getClauseStack().getCurrent(); + if ( currentClause == Clause.INSERT ) { + // PostgreSQL requires the "as" keyword for inserts + appendSql( " as " ); + } + else { + append( WHITESPACE ); + } + append( tableReference.getIdentificationVariable() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + final Statement currentStatement = getStatementStack().getCurrent(); + if ( !( currentStatement instanceof UpdateStatement ) + || !hasNonTrivialFromClause( ( (UpdateStatement) currentStatement ).getFromClause() ) ) { + // For UPDATE statements we render a full FROM clause and a join condition to match target table rows, + // but for that to work, we have to omit the alias for the target table reference here + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + renderFromClauseJoiningDmlTargetReference( statement ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitStandardConflictClause( conflictClause ); + } @Override protected void renderExpressionAsClauseItem(Expression expression) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 7f69f440ca..f4a37338d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -49,6 +49,8 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.LockTimeoutException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.mapping.Column; import org.hibernate.persister.entity.mutation.EntityMutationTarget; @@ -84,6 +86,7 @@ import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import jakarta.persistence.TemporalType; +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; import static org.hibernate.query.sqm.TemporalUnit.NANOSECOND; import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; import static org.hibernate.type.SqlTypes.BLOB; @@ -699,6 +702,21 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { return true; } + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return new TemplatedViolatedConstraintNameExtractor( + sqle -> { + switch ( JdbcExceptionHelper.extractErrorCode( sqle ) ) { + case 2627: + case 2601: + return extractUsingTemplate( "'", "'", sqle.getMessage() ); + default: + return null; + } + } + ); + } + @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { return (sqlException, message, sql) -> { @@ -712,6 +730,13 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { case 1222: return new LockTimeoutException( message, sqlException, sql ); case 2627: + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); case 2601: return new ConstraintViolationException( message, @@ -1106,4 +1131,14 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { final SQLServerSqlAstTranslator translator = new SQLServerSqlAstTranslator<>( factory, optionalTableUpdate ); return translator.createMergeOperation( optionalTableUpdate ); } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java index 9fae16a627..102e1d8982 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java @@ -11,14 +11,19 @@ import java.util.List; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; @@ -28,12 +33,15 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.type.SqlTypes; @@ -53,6 +61,85 @@ public class SQLServerSqlAstTranslator extends SqlAstTr super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + // A merge statement must end with a `;` on SQL Server + appendSql( ';' ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + appendSql( "update" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + renderTableReferenceIdentificationVariable( updateStatement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean needsRecursiveKeywordInWithClause() { return false; @@ -128,14 +215,7 @@ public class SQLServerSqlAstTranslator extends SqlAstTr appendSql( " )" ); registerAffectedTable( tableReference ); - final Clause currentClause = getClauseStack().getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - final String identificationVariable = tableReference.getIdentificationVariable(); - if ( identificationVariable != null ) { - appendSql( ' ' ); - appendSql( identificationVariable ); - } - } + renderTableReferenceIdentificationVariable( tableReference ); } else { super.renderNamedTableReference( tableReference, lockMode ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java index 404adaeacb..9853ac78ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -633,28 +633,17 @@ public class SybaseASEDialect extends SybaseDialect { final int errorCode = JdbcExceptionHelper.extractErrorCode( sqle ); if ( sqlState != null ) { switch ( sqlState ) { - // UNIQUE VIOLATION case "S1000": - if ( 2601 == errorCode ) { - return extractUsingTemplate( "with unique index '", "'", sqle.getMessage() ); - } - break; case "23000": - if ( 546 == errorCode ) { - // Foreign key violation - return extractUsingTemplate( "constraint name = '", "'", sqle.getMessage() ); + switch ( errorCode ) { + case 2601: + // UNIQUE VIOLATION + return extractUsingTemplate( "with unique index '", "'", sqle.getMessage() ); + case 546: + // Foreign key violation + return extractUsingTemplate( "constraint name = '", "'", sqle.getMessage() ); } break; - // // FOREIGN KEY VIOLATION - // case 23503: - // return extractUsingTemplate( "violates foreign key constraint \"","\"", sqle.getMessage() ); - // // NOT NULL VIOLATION - // case 23502: - // return extractUsingTemplate( "null value in column \"","\" violates not-null constraint", sqle.getMessage() ); - // // TODO: RESTRICT VIOLATION - // case 23001: - // return null; - // ALL OTHER } } return null; @@ -671,30 +660,44 @@ public class SybaseASEDialect extends SybaseDialect { case "JZ006": return new LockTimeoutException( message, sqlException, sql ); case "S1000": + case "23000": switch ( errorCode ) { case 515: // Attempt to insert NULL value into column; column does not allow nulls. + return new ConstraintViolationException( + message, + sqlException, + sql, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); + case 546: + // Foreign key violation + return new ConstraintViolationException( + message, + sqlException, + sql, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); case 2601: // Unique constraint violation - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( - sqlException ); - return new ConstraintViolationException( message, sqlException, sql, constraintName ); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); } break; case "ZZZZZ": if ( 515 == errorCode ) { // Attempt to insert NULL value into column; column does not allow nulls. - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( - sqlException ); - return new ConstraintViolationException( message, sqlException, sql, constraintName ); - } - break; - case "23000": - if ( 546 == errorCode ) { - // Foreign key violation - final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( - sqlException ); - return new ConstraintViolationException( message, sqlException, sql, constraintName ); + return new ConstraintViolationException( + message, + sqlException, + sql, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); } break; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java index 3ac818ebfb..820c930fe2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java @@ -12,13 +12,17 @@ import java.util.function.Consumer; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; @@ -33,6 +37,9 @@ import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.Predicate; @@ -40,6 +47,7 @@ import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; /** @@ -55,6 +63,77 @@ public class SybaseASESqlAstTranslator extends Abstract super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + appendSql( "update " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + renderDmlTargetTableExpression( updateStatement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean supportsWithClause() { return false; @@ -130,14 +209,7 @@ public class SybaseASESqlAstTranslator extends Abstract appendSql( " )" ); registerAffectedTable( tableReference ); - final Clause currentClause = getClauseStack().getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - final String identificationVariable = tableReference.getIdentificationVariable(); - if ( identificationVariable != null ) { - appendSql( ' ' ); - appendSql( identificationVariable ); - } - } + renderTableReferenceIdentificationVariable( tableReference ); } else { super.renderNamedTableReference( tableReference, lockMode ); @@ -250,6 +322,14 @@ public class SybaseASESqlAstTranslator extends Abstract visitValuesListEmulateSelectUnion( valuesList ); } + @Override + public void visitValuesTableReference(ValuesTableReference tableReference) { + append( '(' ); + visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); + append( ')' ); + renderDerivedTableReference( tableReference ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { assertRowsOnlyFetchClauseType( queryPart ); @@ -384,63 +464,32 @@ public class SybaseASESqlAstTranslator extends Abstract } @Override - public void visitColumnReference(ColumnReference columnReference) { - final String dmlTargetTableAlias = getDmlTargetTableAlias(); - if ( dmlTargetTableAlias != null && dmlTargetTableAlias.equals( columnReference.getQualifier() ) ) { - // Sybase needs a table name prefix - // but not if this is a restricted union table reference subquery - final QuerySpec currentQuerySpec = (QuerySpec) getQueryPartStack().getCurrent(); - final List roots; - if ( currentQuerySpec != null && !currentQuerySpec.isRoot() - && (roots = currentQuerySpec.getFromClause().getRoots()).size() == 1 - && roots.get( 0 ).getPrimaryTableReference() instanceof UnionTableReference ) { - columnReference.appendReadExpression( this ); - } - // for now, use the unqualified form - else if ( columnReference.isColumnExpressionFormula() ) { - // For formulas, we have to replace the qualifier as the alias was already rendered into the formula - // This is fine for now as this is only temporary anyway until we render aliases for table references - appendSql( - columnReference.getColumnExpression() - .replaceAll( "(\\b)(" + dmlTargetTableAlias + "\\.)(\\b)", "$1$3" ) - ); - } - else { - columnReference.appendReadExpression( - this, - getCurrentDmlStatement().getTargetTable().getTableExpression() - ); - } + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + if ( qualifierSupport == DmlTargetColumnQualifierSupport.TABLE_ALIAS + || ( currentDmlStatement = getCurrentDmlStatement() ) == null + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Sybase needs a table name prefix + // but not if this is a restricted union table reference subquery + final QuerySpec currentQuerySpec = (QuerySpec) getQueryPartStack().getCurrent(); + final List roots; + if ( currentQuerySpec != null && !currentQuerySpec.isRoot() + && (roots = currentQuerySpec.getFromClause().getRoots()).size() == 1 + && roots.get( 0 ).getPrimaryTableReference() instanceof UnionTableReference ) { + return columnReference.getQualifier(); + } + else if ( columnReference.isColumnExpressionFormula() ) { + // For formulas, we have to replace the qualifier as the alias was already rendered into the formula + // This is fine for now as this is only temporary anyway until we render aliases for table references + return null; } else { - columnReference.appendReadExpression( this ); - } - } - - @Override - public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) { - final String dmlTargetTableAlias = getDmlTargetTableAlias(); - final ColumnReference columnReference = aggregateColumnWriteExpression.getColumnReference(); - if ( dmlTargetTableAlias != null && dmlTargetTableAlias.equals( columnReference.getQualifier() ) ) { - // Sybase needs a table name prefix - // but not if this is a restricted union table reference subquery - final QuerySpec currentQuerySpec = (QuerySpec) getQueryPartStack().getCurrent(); - final List roots; - if ( currentQuerySpec != null && !currentQuerySpec.isRoot() - && (roots = currentQuerySpec.getFromClause().getRoots()).size() == 1 - && roots.get( 0 ).getPrimaryTableReference() instanceof UnionTableReference ) { - aggregateColumnWriteExpression.appendWriteExpression( this, this ); - } - else { - aggregateColumnWriteExpression.appendWriteExpression( - this, - this, - getCurrentDmlStatement().getTargetTable().getTableExpression() - ); - } - } - else { - aggregateColumnWriteExpression.appendWriteExpression( this, this ); + return getCurrentDmlStatement().getTargetTable().getTableExpression(); } } @@ -465,8 +514,8 @@ public class SybaseASESqlAstTranslator extends Abstract } @Override - protected String getFromDual() { - return " from (select 1) dual(c1)"; + protected String getDual() { + return "(select 1 c1)"; } private boolean supportsParameterOffsetFetchExpression() { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java index fbc236b311..a30ddc165f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java @@ -514,4 +514,14 @@ public class SybaseDialect extends AbstractTransactSQLDialect { ? AbstractTransactSQLIdentityColumnSupport.INSTANCE : SybaseJconnIdentityColumnSupport.INSTANCE; } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java index 1f749c1ea9..10399925c3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java @@ -11,12 +11,15 @@ import java.util.function.Consumer; import org.hibernate.LockMode; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; @@ -26,8 +29,13 @@ import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; /** @@ -43,6 +51,46 @@ public class SybaseSqlAstTranslator extends AbstractSql super( sessionFactory, statement ); } + @Override + protected void visitInsertStatementOnly(InsertSelectStatement statement) { + if ( statement.getConflictClause() == null || statement.getConflictClause().isDoNothing() ) { + // Render plain insert statement and possibly run into unique constraint violation + super.visitInsertStatementOnly( statement ); + } + else { + visitInsertStatementEmulateMerge( statement ); + appendSql( ';' ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + visitFromClause( statement.getFromClause() ); + } + + @Override + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + visitFromClause( statement.getFromClause() ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + if ( conflictClause.isDoUpdate() && conflictClause.getConstraintName() != null ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause with constraint name is not supported" ); + } + } + } + @Override protected boolean supportsWithClause() { return false; @@ -118,14 +166,7 @@ public class SybaseSqlAstTranslator extends AbstractSql appendSql( " )" ); registerAffectedTable( tableReference ); - final Clause currentClause = getClauseStack().getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - final String identificationVariable = tableReference.getIdentificationVariable(); - if ( identificationVariable != null ) { - appendSql( ' ' ); - appendSql( identificationVariable ); - } - } + renderTableReferenceIdentificationVariable( tableReference ); } else { super.renderNamedTableReference( tableReference, lockMode ); @@ -146,6 +187,19 @@ public class SybaseSqlAstTranslator extends AbstractSql // Sybase does not support the FOR UPDATE clause } + @Override + protected void visitValuesList(List valuesList) { + visitValuesListEmulateSelectUnion( valuesList ); + } + + @Override + public void visitValuesTableReference(ValuesTableReference tableReference) { + append( '(' ); + visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); + append( ')' ); + renderDerivedTableReference( tableReference ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { assertRowsOnlyFetchClauseType( queryPart ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java index 8bce1d70ad..41aa3dafd6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java @@ -6,23 +6,38 @@ */ package org.hibernate.dialect; +import java.util.ArrayList; +import java.util.List; + import org.hibernate.LockOptions; import org.hibernate.engine.spi.SessionFactoryImplementor; +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.MutationStatement; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; /** * A SQL AST translator for TiDB. @@ -39,6 +54,135 @@ public class TiDBSqlAstTranslator extends AbstractSqlAs this.dialect = (TiDBDialect) super.getDialect(); } + @Override + protected void visitInsertSource(InsertSelectStatement statement) { + if ( statement.getSourceSelectStatement() != null ) { + if ( statement.getConflictClause() != null ) { + final List targetColumnReferences = statement.getTargetColumns(); + final List columnNames = new ArrayList<>( targetColumnReferences.size() ); + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + columnNames.add( targetColumnReference.getColumnExpression() ); + } + appendSql( "select * from " ); + emulateQueryPartTableReferenceColumnAliasing( + new QueryPartTableReference( + new SelectStatement( statement.getSourceSelectStatement() ), + "excluded", + columnNames, + false, + getSessionFactory() + ) + ); + } + else { + statement.getSourceSelectStatement().accept( this ); + } + } + else { + visitValuesList( statement.getValuesList() ); + } + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + final Statement currentStatement; + if ( "excluded".equals( columnReference.getQualifier() ) + && ( currentStatement = getStatementStack().getCurrent() ) instanceof InsertSelectStatement + && ( (InsertSelectStatement) currentStatement ).getSourceSelectStatement() == null ) { + // Accessing the excluded row for an insert-values statement in the conflict clause requires the values qualifier + appendSql( "values(" ); + columnReference.appendReadExpression( this, null ); + append( ')' ); + } + else { + super.visitColumnReference( columnReference ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + super.renderUpdateClause( updateStatement ); + } + else { + appendSql( "update " ); + renderFromClauseSpaces( updateStatement.getFromClause() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { + visitInsertStatement( sqlAst ); + + return new JdbcOperationQueryInsertImpl( + getSql(), + getParameterBinders(), + getAffectedTableNames(), + null + ); + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitOnDuplicateKeyConflictClause( conflictClause ); + } + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + // Since TiDB does not support aliasing the insert target table, + // we must detect column reference that are used in the conflict clause + // and use the table expression as qualifier instead + if ( getClauseStack().getCurrent() != Clause.SET + || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !getQueryPartStack().isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); + } + else { + return null; + } + } + @Override protected void renderExpressionAsClauseItem(Expression expression) { expression.accept( this ); @@ -174,8 +318,8 @@ public class TiDBSqlAstTranslator extends AbstractSqlAs } @Override - protected String getFromDual() { - return " from dual"; + protected String getDual() { + return "dual"; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index 004786ebc1..0f23d253fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -53,6 +53,7 @@ import org.hibernate.procedure.ProcedureCall; import org.hibernate.query.MutationQuery; import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.criteria.JpaCriteriaInsertSelect; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.query.spi.QueryProducerImplementor; @@ -513,6 +514,12 @@ public class SessionDelegatorBaseImpl implements SessionImplementor { return delegate().createMutationQuery( insertSelect ); } + @Override + public MutationQuery createMutationQuery(@SuppressWarnings("rawtypes") JpaCriteriaInsert insertSelect) { + //noinspection resource + return delegate().createMutationQuery( insertSelect ); + } + @Override public QueryImplementor createQuery(CriteriaQuery criteriaQuery) { return queryDelegate().createQuery( criteriaQuery ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java index ab0245ccf5..c598d4e9b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionLazyDelegator.java @@ -40,6 +40,7 @@ import org.hibernate.query.NativeQuery; import org.hibernate.query.Query; import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.criteria.JpaCriteriaInsertSelect; import org.hibernate.stat.SessionStatistics; @@ -773,6 +774,11 @@ public class SessionLazyDelegator implements Session { return this.lazySession.get().createMutationQuery( insertSelect ); } + @Override + public MutationQuery createMutationQuery(@SuppressWarnings("rawtypes") JpaCriteriaInsert insertSelect) { + return this.lazySession.get().createMutationQuery( insertSelect ); + } + @Override public MutationQuery createNativeMutationQuery(String sqlString) { return this.lazySession.get().createNativeMutationQuery( sqlString ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index 61cafc24fa..a1b6452a36 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -34,6 +34,7 @@ import org.hibernate.procedure.ProcedureCall; import org.hibernate.query.MutationQuery; import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.criteria.JpaCriteriaInsertSelect; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.query.spi.QueryProducerImplementor; @@ -101,6 +102,12 @@ public class SharedSessionDelegatorBaseImpl implements SharedSessionContractImpl return delegate().createMutationQuery( insertSelect ); } + @Override + public MutationQuery createMutationQuery(@SuppressWarnings("rawtypes") JpaCriteriaInsert insertSelect) { + //noinspection resource + return delegate().createMutationQuery( insertSelect ); + } + @Override public QueryImplementor createQuery(CriteriaQuery criteriaQuery) { return queryDelegate().createQuery( criteriaQuery ); diff --git a/hibernate-core/src/main/java/org/hibernate/exception/ConstraintViolationException.java b/hibernate-core/src/main/java/org/hibernate/exception/ConstraintViolationException.java index 111c3bb1ca..45401c3b3a 100644 --- a/hibernate-core/src/main/java/org/hibernate/exception/ConstraintViolationException.java +++ b/hibernate-core/src/main/java/org/hibernate/exception/ConstraintViolationException.java @@ -19,15 +19,26 @@ import org.checkerframework.checker.nullness.qual.Nullable; */ public class ConstraintViolationException extends JDBCException { + private final ConstraintKind kind; private final @Nullable String constraintName; public ConstraintViolationException(String message, SQLException root, @Nullable String constraintName) { - super( message, root ); - this.constraintName = constraintName; + this( message, root, ConstraintKind.OTHER, constraintName ); } public ConstraintViolationException(String message, SQLException root, String sql, @Nullable String constraintName) { + this( message, root, sql, ConstraintKind.OTHER, constraintName ); + } + + public ConstraintViolationException(String message, SQLException root, ConstraintKind kind, @Nullable String constraintName) { + super( message, root ); + this.kind = kind; + this.constraintName = constraintName; + } + + public ConstraintViolationException(String message, SQLException root, String sql, ConstraintKind kind, @Nullable String constraintName) { super( message, root, sql ); + this.kind = kind; this.constraintName = constraintName; } @@ -39,4 +50,16 @@ public class ConstraintViolationException extends JDBCException { public @Nullable String getConstraintName() { return constraintName; } + + /** + * Returns the kind of constraint that was violated. + */ + public ConstraintKind getKind() { + return kind; + } + + public enum ConstraintKind { + UNIQUE, + OTHER + } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index c481be634d..0a794f9309 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -70,6 +70,7 @@ import org.hibernate.query.SelectionQuery; import org.hibernate.query.UnknownNamedQueryException; import org.hibernate.query.criteria.CriteriaDefinition; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.criteria.JpaCriteriaInsertSelect; import org.hibernate.query.hql.spi.SqmQueryImplementor; import org.hibernate.query.named.NamedResultSetMappingMemento; @@ -88,6 +89,7 @@ import org.hibernate.query.sqm.tree.SqmDmlStatement; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; +import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.select.SqmQueryGroup; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; @@ -1236,6 +1238,17 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont } } + @Override + public MutationQuery createMutationQuery(@SuppressWarnings("rawtypes") JpaCriteriaInsert insertSelect) { + checkOpen(); + try { + return createCriteriaQuery( (SqmInsertStatement) insertSelect, null ); + } + catch ( RuntimeException e ) { + throw getExceptionConverter().convert( e ); + } + } + @Override @SuppressWarnings("UnnecessaryLocalVariable") public ProcedureCall getNamedProcedureCall(String name) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/PrimaryKey.java b/hibernate-core/src/main/java/org/hibernate/mapping/PrimaryKey.java index 56df2a8c4d..2da567eaaa 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/PrimaryKey.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/PrimaryKey.java @@ -65,7 +65,11 @@ public class PrimaryKey extends Constraint { } public String sqlConstraintString(Dialect dialect) { - StringBuilder buf = new StringBuilder("primary key ("); + StringBuilder buf = new StringBuilder(); + if ( orderingUniqueKey != null && orderingUniqueKey.isNameExplicit() ) { + buf.append( "constraint " ).append( orderingUniqueKey.getName() ).append( ' ' ); + } + buf.append( "primary key (" ); boolean first = true; for ( Column column : getColumns() ) { if ( first ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java index e645f5fc0e..fdc20dcdb2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/QueryProducer.java @@ -6,6 +6,7 @@ */ package org.hibernate.query; +import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.criteria.JpaCriteriaInsertSelect; import jakarta.persistence.criteria.CriteriaDelete; @@ -323,6 +324,11 @@ public interface QueryProducer { */ MutationQuery createMutationQuery(@SuppressWarnings("rawtypes") JpaCriteriaInsertSelect insertSelect); + /** + * Create a {@link MutationQuery} from the given insert criteria tree + */ + MutationQuery createMutationQuery(@SuppressWarnings("rawtypes") JpaCriteriaInsert insertSelect); + /** * Create a {@link NativeQuery} instance for the given native SQL statement. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 21d2f4a04f..860a0afc9b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -123,6 +123,12 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { JpaCriteriaInsertSelect createCriteriaInsertSelect(Class targetEntity); + @Incubating + JpaValues values(Expression... expressions); + + @Incubating + JpaValues values(List> expressions); + /** * Transform the given HQL {@code select} query to an equivalent criteria query. * @@ -693,8 +699,6 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { JpaExpression value(T value); - > JpaExpression> values(C collection); - @Override > Expression> values(M map); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaConflictClause.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaConflictClause.java new file mode 100644 index 0000000000..5b1ce25d77 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaConflictClause.java @@ -0,0 +1,107 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.criteria; + +import java.util.List; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Path; +import jakarta.persistence.metamodel.SingularAttribute; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A conflict clause for insert statements. + * + * @since 6.5 + */ +@Incubating +public interface JpaConflictClause { + + /** + * The excluded row/object which was not inserted. + */ + JpaRoot getExcludedRoot(); + + /** + * The unique constraint name for which a constraint violation is allowed. + */ + @Nullable String getConstraintName(); + + /** + * Sets the unique constraint name for which a constraint violation is allowed. + * + * @throws IllegalStateException when constraint paths have already been defined + */ + JpaConflictClause conflictOnConstraint(@Nullable String constraintName); + + /** + * The paths which are part of a unique constraint, for which a constraint violation is allowed. + */ + List> getConstraintPaths(); + + /** + * Shorthand for calling {@link #conflictOnConstraintPaths(List)} with paths resolved for the given attributes + * against the insert target. + */ + JpaConflictClause conflictOnConstraintAttributes(String... attributes); + + /** + * Shorthand for calling {@link #conflictOnConstraintPaths(List)} with paths resolved for the given attributes + * against the insert target. + */ + JpaConflictClause conflictOnConstraintAttributes(SingularAttribute... attributes); + + /** + * See {@link #conflictOnConstraintPaths(List)}. + */ + JpaConflictClause conflictOnConstraintPaths(Path... paths); + + /** + * Sets the paths which are part of a unique constraint, for which a constraint violation is allowed. + * + * @throws IllegalStateException when a constraint name has already been defined + */ + JpaConflictClause conflictOnConstraintPaths(List> paths); + + /** + * The action to do when a conflict due to a unique constraint violation happens. + */ + @Nullable JpaConflictUpdateAction getConflictAction(); + + /** + * Sets the action to do on a conflict. Setting {@code null} means to do nothing. + * + * @see #createConflictUpdateAction() + */ + JpaConflictClause onConflictDo(@Nullable JpaConflictUpdateAction action); + + /** + * Shorthand version for calling {@link #onConflictDo(JpaConflictUpdateAction)} with {@link #createConflictUpdateAction()} + * as argument and returning the update action. + */ + default JpaConflictUpdateAction onConflictDoUpdate() { + final JpaConflictUpdateAction conflictUpdateAction = createConflictUpdateAction(); + onConflictDo( conflictUpdateAction ); + return conflictUpdateAction; + } + + /** + * Shorthand version for calling {@link #onConflictDo(JpaConflictUpdateAction)} with a {@code null} argument. + */ + default JpaConflictClause onConflictDoNothing() { + return onConflictDo( null ); + } + + /** + * Create a new conflict update action for this insert statement. + * + * @return a new conflict update action + * @see #onConflictDo(JpaConflictUpdateAction) + */ + JpaConflictUpdateAction createConflictUpdateAction(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaConflictUpdateAction.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaConflictUpdateAction.java new file mode 100644 index 0000000000..bf1307a4b0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaConflictUpdateAction.java @@ -0,0 +1,92 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.metamodel.SingularAttribute; + +/** + * The update action that should happen on a unique constraint violation for an insert statement. + * + * @since 6.5 + */ +@Incubating +public interface JpaConflictUpdateAction { + + /** + * Update the value of the specified attribute. + * @param attribute attribute to be updated + * @param value new value + * @return the modified update query + */ + JpaConflictUpdateAction set(SingularAttribute attribute, X value); + + /** + * Update the value of the specified attribute. + * @param attribute attribute to be updated + * @param value new value + * @return the modified update query + */ + JpaConflictUpdateAction set(SingularAttribute attribute, Expression value); + + /** + * Update the value of the specified attribute. + * @param attribute attribute to be updated + * @param value new value + * @return the modified update query + */ + JpaConflictUpdateAction set(Path attribute, X value); + + /** + * Update the value of the specified attribute. + * @param attribute attribute to be updated + * @param value new value + * @return the modified update query + */ + JpaConflictUpdateAction set(Path attribute, Expression value); + + /** + * Update the value of the specified attribute. + * @param attributeName name of the attribute to be updated + * @param value new value + * @return the modified update query + */ + JpaConflictUpdateAction set(String attributeName, Object value); + + /** + * Modify the update query to restrict the target of the update + * according to the specified boolean expression. + * Replaces the previously added restriction(s), if any. + * @param restriction a simple or compound boolean expression + * @return the modified update query + */ + JpaConflictUpdateAction where(Expression restriction); + + /** + * Modify the update query to restrict the target of the update + * according to the conjunction of the specified restriction + * predicates. + * Replaces the previously added restriction(s), if any. + * If no restrictions are specified, any previously added + * restrictions are simply removed. + * @param restrictions zero or more restriction predicates + * @return the modified update query + */ + JpaConflictUpdateAction where(Predicate... restrictions); + + /** + * Return the predicate that corresponds to the where clause + * restriction(s), or null if no restrictions have been + * specified. + * @return where clause predicate + */ + JpaPredicate getRestriction(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsert.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsert.java new file mode 100644 index 0000000000..ec4eb11226 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsert.java @@ -0,0 +1,62 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import java.util.List; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Path; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * The commonalities between insert-select and insert-values. + * + * @since 6.5 + */ +@Incubating +public interface JpaCriteriaInsert extends JpaManipulationCriteria { + + /** + * Returns the insertion target paths. + */ + List> getInsertionTargetPaths(); + + /** + * Sets the insertion target paths. + */ + JpaCriteriaInsert setInsertionTargetPaths(Path... insertionTargetPaths); + + /** + * Sets the insertion target paths. + */ + JpaCriteriaInsert setInsertionTargetPaths(List> insertionTargetPaths); + + /** + * Sets the conflict clause that defines what happens when an insert violates a unique constraint. + */ + JpaConflictClause onConflict(); + + /** + * Sets the conflict clause that defines what happens when an insert violates a unique constraint. + */ + JpaCriteriaInsert onConflict(@Nullable JpaConflictClause conflictClause); + + /** + * Returns the conflict clause that defines what happens when an insert violates a unique constraint, + * or {@code null} if there is none. + */ + @Nullable JpaConflictClause getConflictClause(); + + /** + * Create a new conflict clause for this insert statement. + * + * @return a new conflict clause + * @see JpaCriteriaInsert#onConflict(JpaConflictClause) + */ + JpaConflictClause createConflictClause(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertSelect.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertSelect.java index 344231b2ca..856fc98d9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertSelect.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertSelect.java @@ -8,6 +8,9 @@ package org.hibernate.query.criteria; import org.hibernate.Incubating; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaQuery; + /** * A representation of SqmInsertSelectStatement at the * {@link org.hibernate.query.criteria} level, even though JPA does @@ -30,5 +33,10 @@ import org.hibernate.Incubating; * @author Steve Ebersole */ @Incubating -public interface JpaCriteriaInsertSelect extends JpaManipulationCriteria { +public interface JpaCriteriaInsertSelect extends JpaCriteriaInsert { + + JpaCriteriaInsertSelect select(CriteriaQuery criteriaQuery); + + @Override + JpaCriteriaInsertSelect onConflict(JpaConflictClause conflictClause); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertValues.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertValues.java index 2a1ae18a95..6ff56f73a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertValues.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaInsertValues.java @@ -6,6 +6,8 @@ */ package org.hibernate.query.criteria; +import java.util.List; + import org.hibernate.Incubating; /** @@ -13,7 +15,7 @@ import org.hibernate.Incubating; * {@link org.hibernate.query.criteria} level, even though JPA does * not define support for insert-values criteria. * - * @see org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement + * @see org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement * * @apiNote Incubating mainly for 2 purposes:

    *
  • @@ -30,5 +32,13 @@ import org.hibernate.Incubating; * @author Gavin King */ @Incubating -public interface JpaCriteriaInsertValues extends JpaManipulationCriteria { +public interface JpaCriteriaInsertValues extends JpaCriteriaInsert { + + JpaCriteriaInsertValues values(JpaValues... values); + + JpaCriteriaInsertValues values(List values); + + @Override + JpaCriteriaInsertValues onConflict(JpaConflictClause conflictClause); + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaValues.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaValues.java new file mode 100644 index 0000000000..1787ee4ce6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaValues.java @@ -0,0 +1,25 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import java.util.List; + +import org.hibernate.Incubating; + +/** + * A tuple of values. + * + * @since 6.5 + */ +@Incubating +public interface JpaValues { + + /** + * Returns the expressions of this tuple. + */ + List> getExpressions(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index c7b34e4615..fc22ac6b9a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -53,6 +53,7 @@ import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.JpaSetJoin; import org.hibernate.query.criteria.JpaSimpleCase; import org.hibernate.query.criteria.JpaSubQuery; +import org.hibernate.query.criteria.JpaValues; import org.hibernate.query.criteria.JpaWindow; import org.hibernate.query.criteria.JpaWindowFrame; import org.hibernate.query.NullPrecedence; @@ -148,6 +149,18 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde return criteriaBuilder.createCriteriaInsertSelect( targetEntity ); } + @Override + @Incubating + public JpaValues values(Expression... expressions) { + return criteriaBuilder.values( expressions ); + } + + @Override + @Incubating + public JpaValues values(List> expressions) { + return criteriaBuilder.values( expressions ); + } + @Override public JpaCriteriaQuery unionAll(CriteriaQuery query1, CriteriaQuery... queries) { return criteriaBuilder.unionAll( query1, queries ); @@ -746,11 +759,6 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde return criteriaBuilder.value( value ); } - @Override - public > JpaExpression> values(C collection) { - return criteriaBuilder.values( collection ); - } - @Override public > Expression> values(M map) { return criteriaBuilder.values( map ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 5d7f628e4b..de0fe7ac08 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -24,9 +24,11 @@ import java.time.ZonedDateTime; import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -105,6 +107,7 @@ import org.hibernate.query.sqm.produce.function.FunctionArgumentException; import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; import org.hibernate.query.sqm.spi.ParameterDeclarationContext; import org.hibernate.query.sqm.spi.SqmCreationContext; +import org.hibernate.query.sqm.tree.AbstractSqmDmlStatement; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.SqmQuery; import org.hibernate.query.sqm.tree.SqmStatement; @@ -171,6 +174,8 @@ import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.insert.SqmConflictClause; +import org.hibernate.query.sqm.tree.insert.SqmConflictUpdateAction; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; @@ -207,7 +212,10 @@ import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.query.sqm.tree.select.SqmSortSpecification; import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import org.hibernate.query.sqm.tree.update.SqmAssignment; +import org.hibernate.query.sqm.tree.update.SqmSetClause; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.cte.CteSearchClauseKind; @@ -487,8 +495,6 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem processingStateStack.push( processingState ); try { - queryExpressionContext.accept( this ); - final SqmCreationProcessingState stateFieldsProcessingState = new SqmCreationProcessingStateImpl( insertStatement, this @@ -506,6 +512,9 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem processingStateStack.pop(); } + queryExpressionContext.accept( this ); + + insertStatement.onConflict( visitConflictClause( ctx.conflictClause() ) ); return insertStatement; } finally { @@ -526,21 +535,43 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem processingState.getPathRegistry().register( root ); try { - final HqlParser.ValuesListContext valuesListContext = ctx.valuesList(); - for ( int i = 1; i < valuesListContext.getChildCount(); i += 2 ) { - final ParseTree values = valuesListContext.getChild( i ); - final SqmValues sqmValues = new SqmValues(); - for ( int j = 1; j < values.getChildCount(); j += 2 ) { - sqmValues.getExpressions().add( (SqmExpression) values.getChild( j ).accept( this ) ); - } - insertStatement.getValuesList().add( sqmValues ); - } - for ( HqlParser.SimplePathContext stateFieldCtx : targetFieldsSpecContext.simplePath() ) { final SqmPath stateField = (SqmPath) visitSimplePath( stateFieldCtx ); insertStatement.addInsertTargetStateField( stateField ); } + final ArrayList valuesList = new ArrayList<>(); + final HqlParser.ValuesListContext valuesListContext = ctx.valuesList(); + for ( int i = 1; i < valuesListContext.getChildCount(); i += 2 ) { + final ParseTree values = valuesListContext.getChild( i ); + final ArrayList> valuesExpressions = new ArrayList<>(); + final Iterator> iterator = insertStatement.getInsertionTargetPaths().iterator(); + for ( int j = 1; j < values.getChildCount(); j += 2 ) { + final SqmPath targetPath = iterator.next(); + final Class targetPathJavaType = targetPath.getJavaType(); + final boolean isEnum = targetPathJavaType != null && targetPathJavaType.isEnum(); + final ParseTree valuesContext = values.getChild( j ); + final HqlParser.ExpressionContext expressionContext; + final Map, Enum> possibleEnumValues; + final SqmExpression value; + if ( isEnum && valuesContext.getChild( 0 ) instanceof HqlParser.ExpressionContext + && ( possibleEnumValues = getPossibleEnumValues( expressionContext = (HqlParser.ExpressionContext) valuesContext.getChild( 0 ) ) ) != null ) { + value = resolveEnumShorthandLiteral( + expressionContext, + possibleEnumValues, + targetPathJavaType + ); + } + else { + value = (SqmExpression) valuesContext.accept( this ); + } + valuesExpressions.add( value ); + } + valuesList.add( new SqmValues( valuesExpressions ) ); + } + + insertStatement.values( valuesList ); + insertStatement.onConflict( visitConflictClause( ctx.conflictClause() ) ); return insertStatement; } finally { @@ -549,6 +580,42 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem } } + @Override + public SqmConflictClause visitConflictClause(HqlParser.ConflictClauseContext ctx) { + if ( ctx == null ) { + return null; + } + final SqmCreationProcessingState processingState = processingStateStack.getCurrent(); + final SqmInsertStatement statement = (SqmInsertStatement) processingState.getProcessingQuery(); + final SqmConflictClause conflictClause = new SqmConflictClause<>( statement ); + final HqlParser.ConflictTargetContext conflictTargetContext = ctx.conflictTarget(); + if ( conflictTargetContext != null ) { + final HqlParser.IdentifierContext identifierCtx = conflictTargetContext.identifier(); + if ( identifierCtx != null ) { + conflictClause.conflictOnConstraint( visitIdentifier( identifierCtx ) ); + } + else { + final List> constraintAttributes = new ArrayList<>(); + for ( HqlParser.SimplePathContext pathContext : conflictTargetContext.simplePath() ) { + constraintAttributes.add( consumeDomainPath( pathContext ) ); + } + conflictClause.conflictOnConstraintPaths( constraintAttributes ); + } + } + final HqlParser.ConflictActionContext conflictActionContext = ctx.conflictAction(); + final HqlParser.SetClauseContext setClauseContext = conflictActionContext.setClause(); + if ( setClauseContext != null ) { + processingState.getPathRegistry().registerByAliasOnly( conflictClause.getExcludedRoot() ); + final SqmConflictUpdateAction updateAction = conflictClause.onConflictDoUpdate(); + for ( HqlParser.AssignmentContext assignmentContext : setClauseContext.assignment() ) { + updateAction.addAssignment( visitAssignment( assignmentContext ) ); + } + final SqmPredicate sqmPredicate = visitWhereClause( conflictActionContext.whereClause() ); + updateAction.where( sqmPredicate ); + } + return conflictClause; + } + @Override public SqmUpdateStatement visitUpdateStatement(HqlParser.UpdateStatementContext ctx) { final boolean versioned = ctx.VERSIONED() != null; @@ -577,27 +644,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem final HqlParser.SetClauseContext setClauseCtx = ctx.setClause(); for ( ParseTree subCtx : setClauseCtx.children ) { if ( subCtx instanceof HqlParser.AssignmentContext ) { - final HqlParser.AssignmentContext assignmentContext = (HqlParser.AssignmentContext) subCtx; - //noinspection unchecked - final SqmPath targetPath = (SqmPath) consumeDomainPath( assignmentContext.simplePath() ); - final Class targetPathJavaType = targetPath.getJavaType(); - final boolean isEnum = targetPathJavaType != null && targetPathJavaType.isEnum(); - final ParseTree rightSide = assignmentContext.getChild( 2 ); - final HqlParser.ExpressionContext expressionContext; - final Map, Enum> possibleEnumValues; - final SqmExpression value; - if ( isEnum && rightSide.getChild( 0 ) instanceof HqlParser.ExpressionContext - && ( possibleEnumValues = getPossibleEnumValues( expressionContext = (HqlParser.ExpressionContext) rightSide.getChild( 0 ) ) ) != null ) { - value = resolveEnumShorthandLiteral( - expressionContext, - possibleEnumValues, - targetPathJavaType - ); - } - else { - value = (SqmExpression) rightSide.accept( this ); - } - updateStatement.applyAssignment( targetPath, value ); + updateStatement.applyAssignment( visitAssignment( (HqlParser.AssignmentContext) subCtx ) ); } } @@ -613,6 +660,30 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem } } + @Override + public SqmAssignment visitAssignment(HqlParser.AssignmentContext ctx) { + //noinspection unchecked + final SqmPath targetPath = (SqmPath) consumeDomainPath( ctx.simplePath() ); + final Class targetPathJavaType = targetPath.getJavaType(); + final boolean isEnum = targetPathJavaType != null && targetPathJavaType.isEnum(); + final ParseTree rightSide = ctx.getChild( 2 ); + final HqlParser.ExpressionContext expressionContext; + final Map, Enum> possibleEnumValues; + final SqmExpression value; + if ( isEnum && rightSide.getChild( 0 ) instanceof HqlParser.ExpressionContext + && ( possibleEnumValues = getPossibleEnumValues( expressionContext = (HqlParser.ExpressionContext) rightSide.getChild( 0 ) ) ) != null ) { + value = resolveEnumShorthandLiteral( + expressionContext, + possibleEnumValues, + targetPathJavaType + ); + } + else { + value = (SqmExpression) rightSide.accept( this ); + } + return new SqmAssignment<>( targetPath, value ); + } + @Override public SqmDeleteStatement visitDeleteStatement(HqlParser.DeleteStatementContext ctx) { final HqlParser.TargetEntityContext dmlTargetContext = ctx.targetEntity(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java index 438f187a49..877140c07b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java @@ -71,26 +71,7 @@ public class SqmPathRegistryImpl implements SqmPathRegistry { if ( sqmPath instanceof SqmFrom ) { final SqmFrom sqmFrom = (SqmFrom) sqmPath; - final String alias = sqmPath.getExplicitAlias(); - if ( alias != null ) { - final String aliasToUse = jpaCompliance.isJpaQueryComplianceEnabled() - ? alias.toLowerCase( Locale.getDefault() ) - : alias; - - final SqmFrom previousFrom = sqmFromByAlias.put( aliasToUse, sqmFrom ); - - if ( previousFrom != null ) { - throw new AliasCollisionException( - String.format( - Locale.ENGLISH, - "Alias [%s] used for multiple from-clause elements : %s, %s", - alias, - previousFrom, - sqmPath - ) - ); - } - } + registerByAliasOnly( sqmFrom ); final SqmFrom previousFromByPath = sqmFromByPath.put( sqmPath.getNavigablePath(), sqmFrom ); @@ -124,6 +105,30 @@ public class SqmPathRegistryImpl implements SqmPathRegistry { } } + @Override + public void registerByAliasOnly(SqmFrom sqmFrom) { + final String alias = sqmFrom.getExplicitAlias(); + if ( alias != null ) { + final String aliasToUse = jpaCompliance.isJpaQueryComplianceEnabled() + ? alias.toLowerCase( Locale.getDefault() ) + : alias; + + final SqmFrom previousFrom = sqmFromByAlias.put( aliasToUse, sqmFrom ); + + if ( previousFrom != null ) { + throw new AliasCollisionException( + String.format( + Locale.ENGLISH, + "Alias [%s] used for multiple from-clause elements : %s, %s", + alias, + previousFrom, + sqmFrom + ) + ); + } + } + } + @Override public void replace(SqmEntityJoin sqmJoin, SqmRoot sqmRoot) { final String alias = sqmJoin.getExplicitAlias(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmPathRegistry.java b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmPathRegistry.java index b4d151c7d1..4b06756e8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmPathRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmPathRegistry.java @@ -36,6 +36,13 @@ public interface SqmPathRegistry { */ void register(SqmPath sqmPath); + /** + * Register an SqmFrom by alias only. + * Effectively, this makes the from node only resolvable via the alias, + * which means that the from node is ignored in {@link #findFromExposing(String)}. + */ + void registerByAliasOnly(SqmFrom sqmFrom); + /** * Used with {@linkplain JpaCompliance#isJpaQueryComplianceEnabled() JPA compliance} * to treat secondary query roots as cross-joins. Here we will replace the {@code sqmRoot} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index e47b1f1875..fa6dfd80af 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -32,6 +32,7 @@ import org.hibernate.query.criteria.JpaPredicate; import org.hibernate.query.criteria.JpaSearchedCase; import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.JpaSimpleCase; +import org.hibernate.query.criteria.JpaValues; import org.hibernate.query.criteria.JpaWindow; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; @@ -48,6 +49,7 @@ import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; +import org.hibernate.query.sqm.tree.insert.SqmValues; import org.hibernate.query.sqm.tree.predicate.SqmInPredicate; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; @@ -522,6 +524,12 @@ public interface NodeBuilder extends HibernateCriteriaBuilder { @Override SqmInsertSelectStatement createCriteriaInsertSelect(Class targetEntity); + @Override + SqmValues values(Expression... expressions); + + @Override + SqmValues values(List> expressions); + @Override SqmExpression abs(Expression x); @@ -775,9 +783,6 @@ public interface NodeBuilder extends HibernateCriteriaBuilder { @Override > SqmExpression> indexes(L list); - @Override - > SqmExpression> values(C collection); - @Override > Expression> values(M map); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java index 2ede622325..9dda6003de 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java @@ -87,6 +87,7 @@ import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.insert.SqmConflictClause; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; import org.hibernate.query.sqm.tree.insert.SqmValues; @@ -136,6 +137,8 @@ public interface SemanticQueryWalker { T visitInsertValuesStatement(SqmInsertValuesStatement statement); + T visitConflictClause(SqmConflictClause sqmConflictClause); + T visitDeleteStatement(SqmDeleteStatement statement); T visitSelectStatement(SqmSelectStatement statement); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index d1aacc3040..aa9076b041 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -827,7 +827,7 @@ public class QuerySqmImpl final NonSelectQueryPlan[] planParts = new NonSelectQueryPlan[valuesList.size()]; for ( int i = 0; i < valuesList.size(); i++ ) { final SqmInsertValuesStatement subInsert = insertValues.copyWithoutValues( SqmCopyContext.simpleContext() ); - subInsert.getValuesList().add( valuesList.get( i ) ); + subInsert.values( valuesList ); planParts[i] = new SimpleInsertQueryPlan( subInsert, domainParameterXref ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index f2f8658715..976713ceba 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -131,6 +131,7 @@ import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; +import org.hibernate.query.sqm.tree.insert.SqmValues; import org.hibernate.query.sqm.tree.predicate.SqmBetweenPredicate; import org.hibernate.query.sqm.tree.predicate.SqmBooleanExpressionPredicate; import org.hibernate.query.sqm.tree.predicate.SqmComparisonPredicate; @@ -353,6 +354,17 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, return new SqmInsertSelectStatement<>( targetEntity, this ); } + @Override + public SqmValues values(Expression... expressions) { + return values( Arrays.asList( expressions ) ); + } + + @Override + public SqmValues values(List> expressions) { + //noinspection unchecked + return new SqmValues( (List>) expressions ); + } + @Override public JpaCriteriaQuery union(boolean all, CriteriaQuery query1, CriteriaQuery... queries) { return setOperation( all ? SetOperator.UNION_ALL : SetOperator.UNION, query1, queries ); @@ -1904,14 +1916,9 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, // || is a literal enum mapped to a PostgreSQL named 'enum' type } - @Override - public > SqmExpression> values(C collection) { - throw new UnsupportedOperationException(); - } - @Override public > Expression> values(M map) { - throw new UnsupportedOperationException(); + return value( map.values() ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java index 37793607ef..0b456410d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java @@ -78,6 +78,8 @@ import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.insert.SqmConflictClause; +import org.hibernate.query.sqm.tree.insert.SqmConflictUpdateAction; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; import org.hibernate.query.sqm.tree.insert.SqmValues; @@ -330,6 +332,12 @@ public class SqmTreePrinter implements SemanticQueryWalker { "into", () -> statement.getInsertionTargetPaths().forEach( sqmPath -> sqmPath.accept( this ) ) ); + if ( statement.getConflictClause() != null ) { + processStanza( + "on conflict", + () -> statement.getConflictClause().accept( this ) + ); + } } ); } @@ -337,6 +345,29 @@ public class SqmTreePrinter implements SemanticQueryWalker { return null; } + @Override + public Object visitConflictClause(SqmConflictClause sqmConflictClause) { + if ( sqmConflictClause.getConstraintName() != null ) { + logWithIndentation( "[constraintName = %s]", sqmConflictClause.getConstraintName() ); + } + else { + processStanza( + "constraint attributes", + () -> sqmConflictClause.getConstraintPaths().forEach( sqmPath -> sqmPath.accept( this ) ) + ); + } + final SqmConflictUpdateAction updateAction = sqmConflictClause.getConflictAction(); + if ( updateAction == null ) { + logWithIndentation( "do nothing" ); + } + else { + logWithIndentation( "do update " ); + visitSetClause( updateAction.getSetClause() ); + visitWhereClause( updateAction.getWhereClause() ); + } + return null; + } + @Override public Object visitSelectStatement(SqmSelectStatement statement) { if ( DEBUG_ENABLED ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java index a2bc74ea3f..62adc2dbf2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java @@ -6,62 +6,37 @@ */ package org.hibernate.query.sqm.mutation.internal; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; - import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.internal.util.collections.Stack; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter; import org.hibernate.query.sqm.sql.internal.SqlAstProcessingStateImpl; import org.hibernate.query.sqm.tree.SqmStatement; -import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; -import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.predicate.SqmWhereClause; -import org.hibernate.query.sqm.tree.update.SqmSetClause; -import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.spi.SqlAstCreationContext; import org.hibernate.sql.ast.spi.SqlAstHelper; import org.hibernate.sql.ast.spi.SqlAstProcessingState; -import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.ast.tree.update.Assignable; -import org.hibernate.sql.ast.tree.update.Assignment; /** * Specialized BaseSqmToSqlAstConverter implementation used during conversion * of an SQM mutation query tree representing into the various SQL AST trees * needed to perform that operation. * - * @see #visitSetClause(SqmSetClause, Consumer, SqmParameterResolutionConsumer) - * @see #visitWhereClause(SqmWhereClause, Consumer, SqmParameterResolutionConsumer) - * * @author Steve Ebersole */ public class MultiTableSqmMutationConverter extends BaseSqmToSqlAstConverter { - public interface SqmParameterResolutionConsumer { - void accept(SqmParameter sqmParam, MappingModelExpressible mappingType, List jdbcParameters); - } private final EntityMappingType mutatingEntityDescriptor; private final TableGroup mutatingTableGroup; private Predicate discriminatorPredicate; - private SqmParameterResolutionConsumer parameterResolutionConsumer; - public MultiTableSqmMutationConverter( EntityMappingType mutatingEntityDescriptor, SqmStatement statement, @@ -119,6 +94,7 @@ public class MultiTableSqmMutationConverter extends BaseSqmToSqlAstConverter (predicate) -> { + assert this.discriminatorPredicate == null; this.discriminatorPredicate = predicate; }, this @@ -146,98 +122,9 @@ public class MultiTableSqmMutationConverter extends BaseSqmToSqlAstConverter assignmentConsumer, - SqmParameterResolutionConsumer parameterResolutionConsumer) { - this.parameterResolutionConsumer = parameterResolutionConsumer; - - final List assignments = super.visitSetClause( setClause ); - for ( Assignment assignment : assignments ) { - assignmentConsumer.accept( assignment ); - } - } - - public List visitSetClause(SqmSetClause setClause) { - throw new UnsupportedOperationException(); - } - - public Predicate visitWhereClause( - SqmWhereClause sqmWhereClause, - Consumer restrictionColumnReferenceConsumer, - SqmParameterResolutionConsumer parameterResolutionConsumer) { - this.parameterResolutionConsumer = parameterResolutionConsumer; - - if ( sqmWhereClause == null || sqmWhereClause.getPredicate() == null ) { - return discriminatorPredicate; - } - - final SqlAstProcessingState rootProcessingState = getCurrentProcessingState(); - final SqlAstProcessingStateImpl restrictionProcessingState = new SqlAstProcessingStateImpl( - rootProcessingState, - this, - getCurrentClauseStack()::getCurrent - ) { - @Override - public SqlExpressionResolver getSqlExpressionResolver() { - return this; - } - - @Override - public Expression resolveSqlExpression( - ColumnReferenceKey key, Function creator) { - final Expression expression = rootProcessingState.getSqlExpressionResolver().resolveSqlExpression( - key, - creator - ); - if ( expression instanceof ColumnReference ) { - restrictionColumnReferenceConsumer.accept( (ColumnReference) expression ); - } - return expression; - } - }; - - pushProcessingState( restrictionProcessingState, getFromClauseIndex() ); - try { - getCurrentClauseStack().push( Clause.WHERE ); - return SqlAstHelper.combinePredicates( - (Predicate) sqmWhereClause.getPredicate().accept( this ), - discriminatorPredicate - ); - } - finally { - getCurrentClauseStack().pop(); - popProcessingStateStack(); - this.parameterResolutionConsumer = null; - } - } - @Override public Predicate visitWhereClause(SqmWhereClause whereClause) { - return (Predicate) super.visitWhereClause( whereClause ); - } - - @Override - protected Expression consumeSqmParameter( - SqmParameter sqmParameter, - MappingModelExpressible valueMapping, - BiConsumer jdbcParameterConsumer) { - assert parameterResolutionConsumer != null; - - final Expression expression = super.consumeSqmParameter( sqmParameter, valueMapping, jdbcParameterConsumer ); - - final List> jdbcParameters = getJdbcParamsBySqmParam().get( sqmParameter ); - final MappingModelExpressible mappingType = getSqmParameterMappingModelExpressibleResolutions().get( sqmParameter ); - parameterResolutionConsumer.accept( - sqmParameter, - mappingType, - jdbcParameters.get( jdbcParameters.size() - 1 ) - ); - - return expression; + return SqlAstHelper.combinePredicates( super.visitWhereClause( whereClause ), discriminatorPredicate ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java index 816f4f5aba..235d9789ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/AbstractCteMutationHandler.java @@ -28,6 +28,7 @@ import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.mutation.internal.MatchingIdSelectionHelper; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.spi.AbstractMutationHandler; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmParameter; @@ -131,14 +132,7 @@ public abstract class AbstractCteMutationHandler extends AbstractMutationHandler parameterResolutions = new IdentityHashMap<>(); } - //noinspection rawtypes - final Map paramTypeResolutions = new LinkedHashMap<>(); - - final Predicate restriction = sqmConverter.visitWhereClause( - sqmMutationStatement.getWhereClause(), - columnReference -> {}, - (sqmParam, mappingType, jdbcParameters) -> paramTypeResolutions.put( sqmParam, mappingType ) - ); + final Predicate restriction = sqmConverter.visitWhereClause( sqmMutationStatement.getWhereClause() ); sqmConverter.pruneTableGroupJoins(); final CteStatement idSelectCte = new CteStatement( @@ -193,7 +187,12 @@ public abstract class AbstractCteMutationHandler extends AbstractMutationHandler SqmUtil.generateJdbcParamsXref( domainParameterXref, sqmConverter ), factory.getRuntimeMetamodels().getMappingMetamodel(), navigablePath -> sqmConverter.getMutatingTableGroup(), - paramTypeResolutions::get, + new SqmParameterMappingModelResolutionAccess() { + @Override @SuppressWarnings("unchecked") + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) sqmConverter.getSqmParameterMappingModelExpressibleResolutions().get( parameter ); + } + }, executionContext.getSession() ); final LockOptions lockOptions = executionContext.getQueryOptions().getLockOptions(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java index 33e03cec73..a9852e693a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java @@ -8,14 +8,15 @@ package org.hibernate.query.sqm.mutation.internal.cte; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.temptable.TemporaryTable; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; @@ -37,6 +38,7 @@ import org.hibernate.query.results.TableGroupImpl; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.SetOperator; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.query.sqm.internal.SqmUtil; @@ -46,9 +48,11 @@ import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter; import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation; +import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmStar; +import org.hibernate.query.sqm.tree.insert.SqmConflictClause; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; @@ -70,21 +74,32 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.Star; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.FromClause; import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableGroup; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; import org.hibernate.sql.ast.tree.from.UnionTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableGroup; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.ExistsPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.graph.DomainResult; @@ -92,6 +107,7 @@ import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.generator.Generator; +import org.hibernate.type.BasicType; import org.hibernate.type.spi.TypeConfiguration; /** @@ -191,12 +207,6 @@ public class CteInsertHandler implements InsertHandler { final int size = sqmStatement.getInsertionTargetPaths().size(); final List, Assignment>> targetPathColumns = new ArrayList<>( size ); final List targetPathCteColumns = new ArrayList<>( size ); - final NamedTableReference entityTableReference = new NamedTableReference( - cteTable.getTableExpression(), - TemporaryTable.DEFAULT_ALIAS, - true - ); - final InsertSelectStatement insertStatement = new InsertSelectStatement( entityTableReference ); final BaseSqmToSqlAstConverter.AdditionalInsertValues additionalInsertValues = sqmConverter.visitInsertionTargetPaths( (assignable, columnReferences) -> { @@ -211,7 +221,6 @@ public class CteInsertHandler implements InsertHandler { final int end = offset + pathInterpretation.getExpressionType().getJdbcTypeCount(); // Find a matching cte table column and set that at the current index final List columns = cteTable.getCteColumns().subList( offset, end ); - insertStatement.addTargetColumnReferences( columnReferences ); targetPathCteColumns.addAll( columns ); targetPathColumns.add( new AbstractMap.SimpleEntry<>( @@ -245,17 +254,6 @@ public class CteInsertHandler implements InsertHandler { if ( additionalInsertValues.applySelections( querySpec, sessionFactory ) ) { final CteColumn rowNumberColumn = cteTable.getCteColumns() .get( cteTable.getCteColumns().size() - 1 ); - final ColumnReference columnReference = new ColumnReference( - (String) null, - rowNumberColumn.getColumnExpression(), - false, - null, - rowNumberColumn.getJdbcMapping() - ); - insertStatement.getTargetColumns().set( - insertStatement.getTargetColumns().size() - 1, - columnReference - ); targetPathCteColumns.set( targetPathCteColumns.size() - 1, rowNumberColumn @@ -326,14 +324,6 @@ public class CteInsertHandler implements InsertHandler { // Add the row number to the assignments final CteColumn rowNumberColumn = cteTable.getCteColumns() .get( cteTable.getCteColumns().size() - 1 ); - final ColumnReference columnReference = new ColumnReference( - (String) null, - rowNumberColumn.getColumnExpression(), - false, - null, - rowNumberColumn.getJdbcMapping() - ); - insertStatement.getTargetColumns().add( columnReference ); targetPathCteColumns.add( rowNumberColumn ); } @@ -743,11 +733,13 @@ public class CteInsertHandler implements InsertHandler { throw new IllegalStateException( "There must be at least a single root table assignment" ); } + final ConflictClause conflictClause = sqmConverter.visitConflictClause( sqmStatement.getConflictClause() ); + final int tableSpan = persister.getTableSpan(); final String[] rootKeyColumns = persister.getKeyColumns( 0 ); final List keyCteColumns = queryCte.getCteTable().getCteColumns().subList( 0, rootKeyColumns.length ); - for ( int i = 0; i < tableSpan; i++ ) { - final String tableExpression = persister.getTableName( i ); + for ( int tableIndex = 0; tableIndex < tableSpan; tableIndex++ ) { + final String tableExpression = persister.getTableName( tableIndex ); final TableReference updatingTableReference = updatingTableGroup.getTableReference( updatingTableGroup.getNavigablePath(), tableExpression, @@ -758,7 +750,7 @@ public class CteInsertHandler implements InsertHandler { updatingTableReference, tableExpression ); - final String[] keyColumns = persister.getKeyColumns( i ); + final String[] keyColumns = persister.getKeyColumns( tableIndex ); final List returningColumnReferences = new ArrayList<>( keyColumns.length + ( assignmentList == null ? 0 : assignmentList.size() ) ); @@ -766,7 +758,7 @@ public class CteInsertHandler implements InsertHandler { final QuerySpec insertSelectSpec = new QuerySpec( true ); CteStatement finalCteStatement = null; final CteTable dmlResultCte; - if ( i == 0 && !assignsId && identifierGenerator.generatedOnExecution() ) { + if ( tableIndex == 0 && !assignsId && identifierGenerator.generatedOnExecution() ) { // Special handling for identity generation final String cteTableName = getCteTableName( tableExpression, "base_" ); if ( statement.getCteStatement( cteTableName ) != null ) { @@ -999,11 +991,57 @@ public class CteInsertHandler implements InsertHandler { } } dmlStatement.setSourceSelectStatement( insertSelectSpec ); - statement.addCteStatement( new CteStatement( dmlResultCte, dmlStatement ) ); + if ( conflictClause != null ) { + if ( conflictClause.isDoNothing() && conflictClause.getConstraintColumnNames().isEmpty() ) { + // Conflict clauses that use a constraint name and do nothing can just use the conflict clause as it is + handleConflictClause( dmlResultCte, dmlStatement, queryCte, tableIndex, conflictClause, statement ); + } + else { + final List compatibleAssignments = getCompatibleAssignments( dmlStatement, conflictClause ); + if ( isIdentifierConflictClause( sqmStatement ) ) { + // If the identifier is used in the SqmInsert, use the key columns of the respective table + handleConflictClause( + dmlResultCte, + dmlStatement, + queryCte, + tableIndex, + new ConflictClause( + conflictClause.getConstraintName(), + Arrays.asList( keyColumns ), + compatibleAssignments, + compatibleAssignments.isEmpty() ? null : conflictClause.getPredicate() + ), + statement + ); + } + else if ( targetColumnsContainAllConstraintColumns( dmlStatement, conflictClause ) ) { + // Also apply the conflict clause if the insert target columns contain the constraint columns + handleConflictClause( + dmlResultCte, + dmlStatement, + queryCte, + tableIndex, + new ConflictClause( + conflictClause.getConstraintName(), + conflictClause.getConstraintColumnNames(), + compatibleAssignments, + compatibleAssignments.isEmpty() ? null : conflictClause.getPredicate() + ), + statement + ); + } + else { + statement.addCteStatement( new CteStatement( dmlResultCte, dmlStatement ) ); + } + } + } + else { + statement.addCteStatement( new CteStatement( dmlResultCte, dmlStatement ) ); + } if ( finalCteStatement != null ) { statement.addCteStatement( finalCteStatement ); } - if ( i == 0 && !assignsId && identifierGenerator.generatedOnExecution() ) { + if ( tableIndex == 0 && !assignsId && identifierGenerator.generatedOnExecution() ) { // Special handling for identity generation statement.addCteStatement( queryCte ); } @@ -1011,6 +1049,319 @@ public class CteInsertHandler implements InsertHandler { return getCteTableName( rootTableName ); } + private void handleConflictClause( + CteTable dmlResultCte, + InsertSelectStatement insertStatement, + CteStatement queryCte, + int tableIndex, + ConflictClause conflictClause, + CteContainer statement) { + if ( sessionFactory.getJdbcServices().getDialect().supportsConflictClauseForInsertCTE() ) { + insertStatement.setConflictClause( conflictClause ); + statement.addCteStatement( new CteStatement( dmlResultCte, insertStatement ) ); + } + else { + // Build an exists subquery clause to only insert if no row with a matching constraint column value exists i.e. + // insert into target (c1, c2) + // select e.c1, e.c2 from HTE_target e + // where not exists (select 1 from target excluded where e.c1=excluded.c1 and e.c2=excluded.c2) + final BasicType booleanType = sessionFactory.getNodeBuilder().getBooleanType(); + final List constraintColumnNames = conflictClause.getConstraintColumnNames(); + final QuerySpec insertQuerySpec = (QuerySpec) insertStatement.getSourceSelectStatement(); + final QuerySpec subquery = new QuerySpec( false, 1 ); + // This is the table group we use in the subquery to check no row for the given constraint columns exists. + // We name it "excluded" because the predicates we build for this check are reused for the + // check in the update statement below. + // "excluded" is our well known name to refer to data that was not inserted + final TableGroup tableGroup = new StandardTableGroup( + false, + new NavigablePath( "excluded" ), + entityDescriptor, + null, + new NamedTableReference( + insertStatement.getTargetTable().getTableExpression(), + "excluded" + ), + null, + sessionFactory + ); + subquery.getSelectClause().addSqlSelection( + new SqlSelectionImpl( new QueryLiteral<>( 1, sessionFactory.getNodeBuilder().getIntegerType() ) ) + ); + subquery.getFromClause().addRoot( tableGroup ); + List columnsToMatch; + if ( constraintColumnNames.isEmpty() ) { + // Assume the primary key columns + final AbstractEntityPersister aep = (AbstractEntityPersister) entityDescriptor; + Predicate predicate = buildColumnMatchPredicate( + columnsToMatch = Arrays.asList( aep.getKeyColumns( tableIndex ) ), + insertStatement, + false, + true + ); + if ( predicate == null ) { + throw new IllegalArgumentException( "Couldn't infer conflict constraint columns" ); + } + subquery.applyPredicate( predicate ); + } + else { + columnsToMatch = constraintColumnNames; + subquery.applyPredicate( buildColumnMatchPredicate( constraintColumnNames, insertStatement, true, true ) ); + } + + insertQuerySpec.applyPredicate( new ExistsPredicate( subquery, true, booleanType ) ); + + // Emulate the conflict do update clause by creating a separate update CTEs + if ( conflictClause.isDoUpdate() ) { + final TableGroup temporaryTableGroup = insertQuerySpec.getFromClause().getRoots().get( 0 ); + final QuerySpec renamingSubquery = new QuerySpec( false, 1 ); + final List columnNames = buildCteRenaming( + renamingSubquery, + temporaryTableGroup, + queryCte + ); + renamingSubquery.getFromClause().addRoot( temporaryTableGroup ); + final QueryPartTableGroup excludedTableGroup = new QueryPartTableGroup( + new NavigablePath( "excluded" ), + null, + new SelectStatement( renamingSubquery ), + "excluded", + columnNames, + false, + false, + sessionFactory + ); + + final UpdateStatement updateStatement; + if ( sessionFactory.getJdbcServices().getDialect().supportsFromClauseInUpdate() ) { + final FromClause fromClause = new FromClause( 1 ); + final TableGroup updateTableGroup = new StandardTableGroup( + false, + new NavigablePath( "updated" ), + entityDescriptor, + null, + insertStatement.getTargetTable(), + null, + sessionFactory + ); + fromClause.addRoot( updateTableGroup ); + updateStatement = new UpdateStatement( + insertStatement.getTargetTable(), + fromClause, + conflictClause.getAssignments(), + conflictClause.getPredicate(), + insertStatement.getReturningColumns() + ); + updateTableGroup.addTableGroupJoin( + new TableGroupJoin( + excludedTableGroup.getNavigablePath(), + SqlAstJoinType.INNER, + excludedTableGroup, + buildColumnMatchPredicate( + columnsToMatch, + insertStatement, + true, + false + ) + ) + ); + } + else { + final List assignments = conflictClause.getAssignments(); + final List assignmentColumns = new ArrayList<>( assignments.size() ); + final QuerySpec updateSubquery = new QuerySpec( false, 1 ); + for ( Assignment assignment : assignments ) { + assignmentColumns.add( (ColumnReference) assignment.getAssignable() ); + updateSubquery.getSelectClause().addSqlSelection( + new SqlSelectionImpl( assignment.getAssignedValue() ) + ); + } + updateSubquery.getFromClause().addRoot( excludedTableGroup ); + updateSubquery.applyPredicate( buildColumnMatchPredicate( + columnsToMatch, + insertStatement, + true, + false + ) ); + final QuerySpec matchCteSubquery = new QuerySpec( false, 1 ); + matchCteSubquery.getSelectClause().addSqlSelection( + new SqlSelectionImpl( new QueryLiteral<>( + 1, + sessionFactory.getNodeBuilder().getIntegerType() + ) ) + ); + matchCteSubquery.getFromClause().addRoot( updateSubquery.getFromClause().getRoots().get( 0 ) ); + matchCteSubquery.applyPredicate( updateSubquery.getWhereClauseRestrictions() ); + updateStatement = new UpdateStatement( + insertStatement.getTargetTable(), + List.of( new Assignment( + new SqlTuple( assignmentColumns, null ), + new SelectStatement( updateSubquery ) + ) ), + Predicate.combinePredicates( + new ExistsPredicate( matchCteSubquery, false, booleanType ), + conflictClause.getPredicate() + ), + insertStatement.getReturningColumns() + ); + } + + final CteTable updateCte = dmlResultCte.withName( dmlResultCte.getTableExpression() + "_upd" ); + statement.addCteStatement( new CteStatement( updateCte, updateStatement ) ); + + final CteTable insertCte = dmlResultCte.withName( dmlResultCte.getTableExpression() + "_ins" ); + statement.addCteStatement( new CteStatement( insertCte, insertStatement ) ); + + // Union the update and inserted ids together to be able to determine the effective update count + final List queryParts = new ArrayList<>( 2 ); + final QuerySpec dmlCombinationQ1 = new QuerySpec( false, 1 ); + final QuerySpec dmlCombinationQ2 = new QuerySpec( false, 1 ); + dmlCombinationQ1.getSelectClause().addSqlSelection( new SqlSelectionImpl( new Star() ) ); + dmlCombinationQ2.getSelectClause().addSqlSelection( new SqlSelectionImpl( new Star() ) ); + dmlCombinationQ1.getFromClause().addRoot( new CteTableGroup( new NamedTableReference( updateCte.getTableExpression(), "t" ) ) ); + dmlCombinationQ2.getFromClause().addRoot( new CteTableGroup( new NamedTableReference( insertCte.getTableExpression(), "t" ) ) ); + queryParts.add( dmlCombinationQ1 ); + queryParts.add( dmlCombinationQ2 ); + final SelectStatement dmlCombinationStatement = new SelectStatement( new QueryGroup( true, SetOperator.UNION_ALL, queryParts ) ); + statement.addCteStatement( new CteStatement( dmlResultCte, dmlCombinationStatement ) ); + } + else { + statement.addCteStatement( new CteStatement( dmlResultCte, insertStatement ) ); + } + } + } + + private List buildCteRenaming( + QuerySpec renamingSubquery, + TableGroup temporaryTableGroup, + CteStatement queryCte) { + final List cteColumns = queryCte.getCteTable().getCteColumns(); + for ( CteColumn cteColumn : cteColumns ) { + renamingSubquery.getSelectClause().addSqlSelection( + new SqlSelectionImpl( + new ColumnReference( + temporaryTableGroup.getPrimaryTableReference(), + cteColumn.getColumnExpression(), + cteColumn.getJdbcMapping() + ) + ) + ); + } + final SelectStatement selectStatement = (SelectStatement) queryCte.getCteDefinition(); + final QuerySpec querySpec = (QuerySpec) selectStatement.getQueryPart(); + final DerivedTableReference tableReference = (DerivedTableReference) querySpec.getFromClause() + .getRoots() + .get( 0 ) + .getPrimaryTableReference(); + return tableReference.getColumnNames(); + } + + private Predicate buildColumnMatchPredicate( + List constraintColumnNames, + InsertSelectStatement dmlStatement, + boolean errorIfMissing, + boolean compareAgainstSelectItems) { + final BasicType booleanType = sessionFactory.getNodeBuilder().getBooleanType(); + final QuerySpec insertQuerySpec = (QuerySpec) dmlStatement.getSourceSelectStatement(); + Predicate predicate = null; + OUTER: for ( String constraintColumnName : constraintColumnNames ) { + final List targetColumns = dmlStatement.getTargetColumns(); + for ( int i = 0; i < targetColumns.size(); i++ ) { + final ColumnReference columnReference = targetColumns.get( i ); + if ( columnReference.getColumnExpression().equals( constraintColumnName ) ) { + if ( compareAgainstSelectItems ) { + predicate = Predicate.combinePredicates( + predicate, + new ComparisonPredicate( + new ColumnReference( + "excluded", + columnReference.getColumnExpression(), + false, + null, + columnReference.getJdbcMapping() + ), + ComparisonOperator.EQUAL, + insertQuerySpec.getSelectClause() + .getSqlSelections() + .get( i ) + .getExpression(), + booleanType + ) + ); + } + else { + predicate = Predicate.combinePredicates( + predicate, + new ComparisonPredicate( + columnReference, + ComparisonOperator.EQUAL, + new ColumnReference( + "excluded", + columnReference.getColumnExpression(), + false, + null, + columnReference.getJdbcMapping() + ), + booleanType + ) + ); + } + continue OUTER; + } + } + if ( errorIfMissing ) { + // Should never happen + final List targetColumnNames = targetColumns.stream() + .map( ColumnReference::getColumnExpression ) + .collect( Collectors.toList() ); + throw new IllegalArgumentException( "Couldn't find conflict constraint column [" + constraintColumnName + "] in insert target columns: " + targetColumnNames ); + } + return null; + } + return predicate; + } + + private List getCompatibleAssignments(InsertSelectStatement dmlStatement, ConflictClause conflictClause) { + if ( conflictClause.isDoNothing() ) { + return Collections.emptyList(); + } + List compatibleAssignments = null; + final List assignments = conflictClause.getAssignments(); + for ( Assignment assignment : assignments ) { + for ( ColumnReference targetColumn : dmlStatement.getTargetColumns() ) { + if ( targetColumn.equals( assignment.getAssignable() ) ) { + if ( compatibleAssignments == null ) { + compatibleAssignments = new ArrayList<>( assignments.size() ); + } + compatibleAssignments.add( assignment ); + break; + } + } + } + return compatibleAssignments == null ? Collections.emptyList() : compatibleAssignments; + } + + private boolean isIdentifierConflictClause(SqmInsertStatement sqmStatement) { + final SqmConflictClause conflictClause = sqmStatement.getConflictClause(); + assert conflictClause != null; + final List> constraintPaths = conflictClause.getConstraintPaths(); + return constraintPaths.size() == 1 + && constraintPaths.get( 0 ).getReferencedPathSource() == sqmStatement.getTarget().getModel().getIdentifierDescriptor(); + } + + private boolean targetColumnsContainAllConstraintColumns(InsertSelectStatement statement, ConflictClause conflictClause) { + OUTER: for ( String constraintColumnName : conflictClause.getConstraintColumnNames() ) { + for ( ColumnReference targetColumn : statement.getTargetColumns() ) { + if ( targetColumn.getColumnExpression().equals( constraintColumnName ) ) { + continue OUTER; + } + } + return false; + } + + return true; + } + protected NamedTableReference resolveUnionTableReference( TableReference tableReference, String tableExpression) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java index dbe4332987..1e4a3f5982 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java @@ -98,15 +98,10 @@ public class CteUpdateHandler extends AbstractCteMutationHandler implements Upda // visit the set-clause using our special converter, collecting // information about the assignments final SqmSetClause setClause = updateStatement.getSetClause(); - final List assignments = new ArrayList<>( setClause.getAssignments().size() ); - - sqmConverter.visitSetClause( - setClause, - assignments::add, - (sqmParam, mappingType, jdbcParameters) -> { - parameterResolutions.put( sqmParam, jdbcParameters ); - } - ); + final List assignments = sqmConverter.visitSetClause( setClause ); + for ( Map.Entry, List>> entry : sqmConverter.getJdbcParamsBySqmParam().entrySet() ) { + parameterResolutions.put( entry.getKey(), entry.getValue().get( entry.getValue().size() - 1 ) ); + } sqmConverter.addVersionedAssignment( assignments::add, updateStatement ); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InPredicateRestrictionProducer.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InPredicateRestrictionProducer.java index d316e8b59f..052a8a001c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InPredicateRestrictionProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InPredicateRestrictionProducer.java @@ -11,16 +11,15 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; -import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.InListPredicate; @@ -45,19 +44,42 @@ import org.hibernate.sql.exec.spi.ExecutionContext; * @author Steve Ebersole */ public class InPredicateRestrictionProducer implements MatchingIdRestrictionProducer { + + @Override + public List produceIdExpressionList(List idsAndFks, EntityMappingType entityDescriptor) { + final List inListExpressions = new ArrayList<>( idsAndFks.size() ); + final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); + if ( identifierMapping instanceof BasicValuedModelPart ) { + final BasicValuedModelPart basicValuedModelPart = (BasicValuedModelPart) identifierMapping; + for ( int i = 0; i < idsAndFks.size(); i++ ) { + inListExpressions.add( new QueryLiteral<>( idsAndFks.get( i ), basicValuedModelPart ) ); + } + } + else { + final int jdbcTypeCount = identifierMapping.getJdbcTypeCount(); + for ( int i = 0; i < idsAndFks.size(); i++ ) { + final Object[] id = (Object[]) idsAndFks.get( i ); + final List tupleElements = new ArrayList<>( jdbcTypeCount ); + inListExpressions.add( new SqlTuple( tupleElements, identifierMapping ) ); + identifierMapping.forEachJdbcType( (index, jdbcMapping) -> { + tupleElements.add( new QueryLiteral<>( id[index], (BasicValuedMapping) jdbcMapping ) ); + } ); + } + } + return inListExpressions; + } + @Override public InListPredicate produceRestriction( - List matchingIdValues, + List matchingIdValueExpressions, EntityMappingType entityDescriptor, int valueIndex, ModelPart valueModelPart, TableReference mutatingTableReference, Supplier> columnsToMatchVisitationSupplier, ExecutionContext executionContext) { - assert matchingIdValues != null; - assert ! matchingIdValues.isEmpty(); - - final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory(); + assert matchingIdValueExpressions != null; + assert ! matchingIdValueExpressions.isEmpty(); final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); final int idColumnCount = identifierMapping.getJdbcTypeCount(); @@ -76,45 +98,27 @@ public class InPredicateRestrictionProducer implements MatchingIdRestrictionProd null, basicIdMapping.getJdbcMapping() ); - predicate = new InListPredicate( inFixture ); - - matchingIdValues.forEach( - matchingId -> predicate.addExpression( new JdbcLiteral<>( matchingId, basicIdMapping.getJdbcMapping() ) ) - ); + predicate = new InListPredicate( inFixture, matchingIdValueExpressions ); } else { final List columnReferences = new ArrayList<>( idColumnCount ); - final List jdbcMappings = new ArrayList<>( idColumnCount ); - identifierMapping.forEachSelectable( - (columnIndex, selection) -> { - columnReferences.add( - new ColumnReference( - mutatingTableReference, - selection - ) - ); - jdbcMappings.add( selection.getJdbcMapping() ); - } - ); + final SelectableConsumer selectableConsumer = (columnIndex, selection) -> { + columnReferences.add( + new ColumnReference( + mutatingTableReference, + selection + ) + ); + }; + if ( columnsToMatchVisitationSupplier == null ) { + identifierMapping.forEachSelectable( selectableConsumer ); + } + else { + columnsToMatchVisitationSupplier.get().accept( selectableConsumer ); + } final Expression inFixture = new SqlTuple( columnReferences, identifierMapping ); - predicate = new InListPredicate( inFixture ); - - matchingIdValues.forEach( - matchingId -> { - assert matchingId instanceof Object[]; - final Object[] matchingIdParts = (Object[]) matchingId; - - final List> tupleParts = new ArrayList<>( idColumnCount ); - for ( int p = 0; p < matchingIdParts.length; p++ ) { - tupleParts.add( - new JdbcLiteral<>( matchingIdParts[p],jdbcMappings.get( p ) ) - ); - } - - predicate.addExpression( new SqlTuple( tupleParts, identifierMapping ) ); - } - ); + predicate = new InListPredicate( inFixture, matchingIdValueExpressions ); } return predicate; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java index 8621c79e60..47139ba176 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java @@ -28,6 +28,7 @@ import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.update.Assignment; @@ -93,6 +94,7 @@ public class InlineDeleteHandler implements DeleteHandler { final String mutatingEntityName = sqmDeleteStatement.getTarget().getModel().getHibernateEntityName(); final EntityMappingType entityDescriptor = factory.getRuntimeMetamodels().getEntityMappingType( mutatingEntityName ); + final List inListExpressions = matchingIdsPredicateProducer.produceIdExpressionList( idsAndFks, entityDescriptor ); final JdbcParameterBindings jdbcParameterBindings = new JdbcParameterBindingsImpl( domainParameterXref.getQueryParameterCount() ); // delete from the tables @@ -125,7 +127,7 @@ public class InlineDeleteHandler implements DeleteHandler { pluralAttribute.getSeparateCollectionTable(), entityDescriptor, () -> fkTargetPart::forEachSelectable, - idsAndFks, + inListExpressions, valueIndex, fkTargetPart, jdbcParameterBindings, @@ -139,7 +141,7 @@ public class InlineDeleteHandler implements DeleteHandler { if ( softDeleteMapping != null ) { performSoftDelete( entityDescriptor, - idsAndFks, + inListExpressions, jdbcParameterBindings, executionContext ); @@ -150,7 +152,7 @@ public class InlineDeleteHandler implements DeleteHandler { tableExpression, entityDescriptor, tableKeyColumnsVisitationSupplier, - idsAndFks, + inListExpressions, 0, null, jdbcParameterBindings, @@ -167,7 +169,7 @@ public class InlineDeleteHandler implements DeleteHandler { */ private void performSoftDelete( EntityMappingType entityDescriptor, - List idsAndFks, + List idExpressions, JdbcParameterBindings jdbcParameterBindings, DomainQueryExecutionContext executionContext) { final TableDetails softDeleteTable = entityDescriptor.getSoftDeleteTableDetails(); @@ -182,7 +184,7 @@ public class InlineDeleteHandler implements DeleteHandler { final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ); final Predicate matchingIdsPredicate = matchingIdsPredicateProducer.produceRestriction( - idsAndFks, + idExpressions, entityDescriptor, 0, entityDescriptor.getIdentifierMapping(), @@ -224,7 +226,7 @@ public class InlineDeleteHandler implements DeleteHandler { String targetTableExpression, EntityMappingType entityDescriptor, Supplier> tableKeyColumnsVisitationSupplier, - List ids, + List idExpressions, int valueIndex, ModelPart valueModelPart, JdbcParameterBindings jdbcParameterBindings, @@ -237,7 +239,7 @@ public class InlineDeleteHandler implements DeleteHandler { final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ); final Predicate matchingIdsPredicate = matchingIdsPredicateProducer.produceRestriction( - ids, + idExpressions, entityDescriptor, valueIndex, valueModelPart, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java index ebd23b88d3..364edf4254 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java @@ -10,29 +10,21 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; -import org.hibernate.boot.model.internal.SoftDeleteHelper; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; -import org.hibernate.metamodel.mapping.BasicValuedMapping; -import org.hibernate.metamodel.mapping.BasicValuedModelPart; -import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.SelectableConsumer; -import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.Joinable; import org.hibernate.query.SemanticException; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.ComparisonOperator; @@ -40,22 +32,17 @@ import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.mutation.internal.MatchingIdSelectionHelper; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; -import org.hibernate.query.sqm.mutation.internal.TableKeyExpressionCollector; import org.hibernate.query.sqm.mutation.internal.UpdateHandler; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.sql.SqmTranslation; import org.hibernate.query.sqm.tree.expression.SqmParameter; -import org.hibernate.query.sqm.tree.predicate.SqmWhereClause; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; -import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.SqlAliasBaseImpl; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; -import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -70,13 +57,11 @@ import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; import org.hibernate.sql.ast.tree.predicate.InListPredicate; import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.ast.tree.predicate.PredicateCollector; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.update.Assignment; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcMutationExecutor; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.internal.SqlSelectionImpl; @@ -88,12 +73,7 @@ public class InlineUpdateHandler implements UpdateHandler { private final SqmUpdateStatement sqmUpdate; private final DomainParameterXref domainParameterXref; private final MatchingIdRestrictionProducer matchingIdsPredicateProducer; - - private final DomainQueryExecutionContext executionContext; - private final SessionFactoryImplementor sessionFactory; - private final SqlAstTranslatorFactory sqlAstTranslatorFactory; - private final JdbcMutationExecutor jdbcMutationExecutor; public InlineUpdateHandler( MatchingIdRestrictionProducer matchingIdsPredicateProducer, @@ -103,12 +83,7 @@ public class InlineUpdateHandler implements UpdateHandler { this.matchingIdsPredicateProducer = matchingIdsPredicateProducer; this.domainParameterXref = domainParameterXref; this.sqmUpdate = sqmUpdate; - - this.executionContext = context; - - this.sessionFactory = executionContext.getSession().getFactory(); - this.sqlAstTranslatorFactory = sessionFactory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory(); - this.jdbcMutationExecutor = sessionFactory.getJdbcServices().getJdbcMutationExecutor(); + this.sessionFactory = context.getSession().getFactory(); } @Override @@ -128,125 +103,21 @@ public class InlineUpdateHandler implements UpdateHandler { final String mutatingEntityName = sqmUpdate.getTarget().getModel().getHibernateEntityName(); final EntityPersister entityDescriptor = domainModel.getEntityDescriptor( mutatingEntityName ); + final List inListExpressions = matchingIdsPredicateProducer.produceIdExpressionList( ids, entityDescriptor ); - final String rootEntityName = entityDescriptor.getEntityPersister().getRootEntityName(); - final EntityPersister rootEntityDescriptor = domainModel.getEntityDescriptor( rootEntityName ); - - final String hierarchyRootTableName = ( (Joinable) rootEntityDescriptor ).getTableName(); - - final List inListExpressions = new ArrayList<>( ids.size() ); - final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); - if ( identifierMapping instanceof BasicValuedModelPart ) { - final BasicValuedModelPart basicValuedModelPart = (BasicValuedModelPart) identifierMapping; - for ( int i = 0; i < ids.size(); i++ ) { - inListExpressions.add( new QueryLiteral<>( ids.get( i ), basicValuedModelPart ) ); - } - } - else { - final int jdbcTypeCount = identifierMapping.getJdbcTypeCount(); - for ( int i = 0; i < ids.size(); i++ ) { - final Object[] id = (Object[]) ids.get( i ); - final List tupleElements = new ArrayList<>( jdbcTypeCount ); - inListExpressions.add( new SqlTuple( tupleElements, identifierMapping ) ); - identifierMapping.forEachJdbcType( (index, jdbcMapping) -> { - tupleElements.add( new QueryLiteral<>( id[index], (BasicValuedMapping) jdbcMapping ) ); - } ); - } - } - - final MultiTableSqmMutationConverter converterDelegate = new MultiTableSqmMutationConverter( - entityDescriptor, - sqmUpdate, - sqmUpdate.getTarget(), - domainParameterXref, - executionContext.getQueryOptions(), - executionContext.getSession().getLoadQueryInfluencers(), - executionContext.getQueryParameterBindings(), - sessionFactory - ); - - final TableGroup updatingTableGroup = converterDelegate.getMutatingTableGroup(); - - final NamedTableReference hierarchyRootTableReference = (NamedTableReference) updatingTableGroup.resolveTableReference( - updatingTableGroup.getNavigablePath(), - hierarchyRootTableName - ); - assert hierarchyRootTableReference != null; - - final Map, List>> parameterResolutions; - if ( domainParameterXref.getSqmParameterCount() == 0 ) { - parameterResolutions = Collections.emptyMap(); - } - else { - parameterResolutions = new IdentityHashMap<>(); - } - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // visit the set-clause using our special converter, collecting - // information about the assignments - - final List assignments = new ArrayList<>(); - final Map, MappingModelExpressible> paramTypeResolutions = new LinkedHashMap<>(); - - converterDelegate.visitSetClause( - sqmUpdate.getSetClause(), - assignments::add, - (sqmParameter, mappingType, jdbcParameters) -> { - parameterResolutions.computeIfAbsent( - sqmParameter, - k -> new ArrayList<>( 1 ) - ).add( jdbcParameters ); - paramTypeResolutions.put( sqmParameter, mappingType ); - } - ); - converterDelegate.addVersionedAssignment( assignments::add, sqmUpdate ); - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // visit the where-clause using our special converter, collecting information - // about the restrictions - - final Predicate providedPredicate; - final SqmWhereClause whereClause = sqmUpdate.getWhereClause(); - if ( whereClause == null || whereClause.getPredicate() == null ) { - final SoftDeleteMapping softDeleteMapping = entityDescriptor.getSoftDeleteMapping(); - if ( softDeleteMapping != null ) { - providedPredicate = SoftDeleteHelper.createNonSoftDeletedRestriction( - hierarchyRootTableReference, - softDeleteMapping - ); - } - else { - providedPredicate = null; - } - } - else { - providedPredicate = converterDelegate.visitWhereClause( - whereClause, - columnReference -> {}, - (sqmParameter, mappingType, jdbcParameters) -> { - parameterResolutions.computeIfAbsent( - sqmParameter, - k -> new ArrayList<>( 1 ) - ).add( jdbcParameters ); - paramTypeResolutions.put( sqmParameter, mappingType ); - } - - ); - assert providedPredicate != null; - } - - final PredicateCollector predicateCollector = new PredicateCollector( providedPredicate ); - - entityDescriptor.applyBaseRestrictions( - predicateCollector::applyPredicate, - updatingTableGroup, - true, - executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), - null, - converterDelegate - ); - - converterDelegate.pruneTableGroupJoins(); + //noinspection unchecked + final SqmTranslation translation = (SqmTranslation) sessionFactory.getQueryEngine() + .getSqmTranslatorFactory() + .createMutationTranslator( + sqmUpdate, + executionContext.getQueryOptions(), + domainParameterXref, + executionContext.getQueryParameterBindings(), + executionContext.getSession().getLoadQueryInfluencers(), + sessionFactory + ) + .translate(); + final TableGroup updatingTableGroup = translation.getSqlAst().getFromClause().getRoots().get( 0 ); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // cross-reference the TableReference by alias. The TableGroup already @@ -263,7 +134,7 @@ public class InlineUpdateHandler implements UpdateHandler { domainParameterXref, SqmUtil.generateJdbcParamsXref( domainParameterXref, - () -> parameterResolutions + translation::getJdbcParamsBySqmParam ), sessionFactory.getRuntimeMetamodels().getMappingMetamodel(), navigablePath -> updatingTableGroup, @@ -271,7 +142,7 @@ public class InlineUpdateHandler implements UpdateHandler { @Override @SuppressWarnings("unchecked") public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) paramTypeResolutions.get( parameter ); + return (MappingModelExpressible) translation.getSqmParameterMappingModelTypeResolutions().get( parameter ); } }, executionContext.getSession() @@ -281,6 +152,7 @@ public class InlineUpdateHandler implements UpdateHandler { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // segment the assignments by table-reference final Map> assignmentsByTable = new HashMap<>(); + final List assignments = translation.getSqlAst().getAssignments(); for ( int i = 0; i < assignments.size(); i++ ) { final Assignment assignment = assignments.get( i ); final List assignmentColumnRefs = assignment.getAssignable().getColumnReferences(); @@ -342,7 +214,7 @@ public class InlineUpdateHandler implements UpdateHandler { final TableReference updatingTableReference = updatingTableGroup.getTableReference( updatingTableGroup.getNavigablePath(), tableExpression, - true + false ); final List assignments = assignmentsByTable.get( updatingTableReference ); @@ -356,20 +228,16 @@ public class InlineUpdateHandler implements UpdateHandler { // create the in-subquery predicate to restrict the updates to just // matching ids - final TableKeyExpressionCollector keyColumnCollector = new TableKeyExpressionCollector( entityDescriptor ); - - tableKeyColumnVisitationSupplier.get().accept( - (columnIndex, selection) -> { - assert selection.getContainingTableExpression().equals( tableExpression ); - keyColumnCollector.apply( new ColumnReference( (String) null, selection ) ); - } - ); - - final Expression keyExpression = keyColumnCollector.buildKeyExpression(); - final InListPredicate idListPredicate = new InListPredicate( - keyExpression, - inListExpressions + final InListPredicate idListPredicate = (InListPredicate) matchingIdsPredicateProducer.produceRestriction( + inListExpressions, + entityDescriptor, + 0, + null, + updatingTableReference, + tableKeyColumnVisitationSupplier, + executionContext ); + final Expression keyExpression = idListPredicate.getTestExpression(); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/MatchingIdRestrictionProducer.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/MatchingIdRestrictionProducer.java index b44771501c..fb8c8b33c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/MatchingIdRestrictionProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/MatchingIdRestrictionProducer.java @@ -13,6 +13,7 @@ import java.util.function.Supplier; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.exec.spi.ExecutionContext; @@ -24,15 +25,20 @@ import org.hibernate.sql.exec.spi.ExecutionContext; * @author Steve Ebersole */ public interface MatchingIdRestrictionProducer { + /** + * Produces a list of expression for which a restriction can be produced per-table. + */ + List produceIdExpressionList(List idsAndFks, EntityMappingType entityDescriptor); + /** * Produce the restriction predicate * - * @param matchingIdValues The matching id values. + * @param idExpressions The matching id value expressions. * @param mutatingTableReference The TableReference for the table being mutated * @param columnsToMatchVisitationSupplier The columns against which to restrict the mutations */ Predicate produceRestriction( - List matchingIdValues, + List idExpressions, EntityMappingType entityDescriptor, int valueIndex, ModelPart valueModelPart, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/ColumnReferenceCheckingSqlAstWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/ColumnReferenceCheckingSqlAstWalker.java new file mode 100644 index 0000000000..37dd491b47 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/ColumnReferenceCheckingSqlAstWalker.java @@ -0,0 +1,57 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.mutation.internal.temptable; + +import org.hibernate.persister.internal.SqlFragmentPredicate; +import org.hibernate.sql.ast.spi.AbstractSqlAstWalker; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.predicate.FilterPredicate; +import org.hibernate.sql.ast.tree.select.SelectStatement; + +/** + * Visitor to determine if all visited column references use the same qualifier. + */ +public class ColumnReferenceCheckingSqlAstWalker extends AbstractSqlAstWalker { + + private final String identificationVariable; + private boolean allColumnReferencesFromIdentificationVariable = true; + + public ColumnReferenceCheckingSqlAstWalker(String identificationVariable) { + this.identificationVariable = identificationVariable; + } + + public boolean isAllColumnReferencesFromIdentificationVariable() { + return allColumnReferencesFromIdentificationVariable; + } + + @Override + public void visitSelectStatement(SelectStatement statement) { + // Ignore subquery + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + if ( allColumnReferencesFromIdentificationVariable && !identificationVariable.equals( columnReference.getQualifier() ) ) { + allColumnReferencesFromIdentificationVariable = false; + } + } + + @Override + public void visitFilterPredicate(FilterPredicate filterPredicate) { + allColumnReferencesFromIdentificationVariable = false; + } + + @Override + public void visitFilterFragmentPredicate(FilterPredicate.FilterFragmentPredicate fragmentPredicate) { + allColumnReferencesFromIdentificationVariable = false; + } + + @Override + public void visitSqlFragmentPredicate(SqlFragmentPredicate predicate) { + allColumnReferencesFromIdentificationVariable = false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java index b12ec0b3a5..38c513600d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java @@ -42,6 +42,7 @@ import org.hibernate.query.SortDirection; import org.hibernate.query.results.TableGroupImpl; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; @@ -50,10 +51,12 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; import org.hibernate.sql.ast.tree.predicate.Predicate; @@ -95,6 +98,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio private final DomainParameterXref domainParameterXref; private final TableGroup updatingTableGroup; private final InsertSelectStatement insertStatement; + private final ConflictClause conflictClause; private final EntityMappingType entityDescriptor; @@ -102,7 +106,6 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio private final JdbcParameter sessionUidParameter; private final Map> assignmentsByTable; - private final Map, MappingModelExpressible> paramTypeResolutions; private final SessionFactoryImplementor sessionFactory; public InsertExecutionDelegate( @@ -116,9 +119,8 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio Map tableReferenceByAlias, List assignments, InsertSelectStatement insertStatement, - Map, List>> parameterResolutions, + ConflictClause conflictClause, JdbcParameter sessionUidParameter, - Map, MappingModelExpressible> paramTypeResolutions, DomainQueryExecutionContext executionContext) { this.sqmInsert = sqmInsert; this.sqmConverter = sqmConverter; @@ -127,8 +129,8 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio this.sessionUidAccess = sessionUidAccess; this.domainParameterXref = domainParameterXref; this.updatingTableGroup = insertingTableGroup; + this.conflictClause = conflictClause; this.sessionUidParameter = sessionUidParameter; - this.paramTypeResolutions = paramTypeResolutions; this.insertStatement = insertStatement; this.sessionFactory = executionContext.getSession().getFactory(); @@ -145,14 +147,14 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio domainParameterXref, SqmUtil.generateJdbcParamsXref( domainParameterXref, - () -> parameterResolutions + sqmConverter::getJdbcParamsBySqmParam ), sessionFactory.getRuntimeMetamodels().getMappingMetamodel(), navigablePath -> insertingTableGroup, new SqmParameterMappingModelResolutionAccess() { @Override @SuppressWarnings("unchecked") public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) paramTypeResolutions.get(parameter); + return (MappingModelExpressible) sqmConverter.getSqmParameterMappingModelExpressibleResolutions().get( parameter ); } } , @@ -218,7 +220,12 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio if ( rows != 0 ) { final AbstractEntityPersister persister = (AbstractEntityPersister) entityDescriptor.getEntityPersister(); final int tableSpan = persister.getTableSpan(); - insertRootTable( persister.getTableName( 0 ), rows, persister.getKeyColumns( 0 ), executionContext ); + final int insertedRows = insertRootTable( + persister.getTableName( 0 ), + rows, + persister.getKeyColumns( 0 ), + executionContext + ); if ( persister.hasDuplicateTables() ) { final String[] insertedTables = new String[tableSpan]; @@ -251,6 +258,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio ); } } + return insertedRows; } return rows; @@ -294,7 +302,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio } } - private void insertRootTable( + private int insertRootTable( String tableExpression, int rows, String[] keyColumns, @@ -322,7 +330,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio final QuerySpec querySpec = new QuerySpec( true ); final NamedTableReference temporaryTableReference = new NamedTableReference( insertStatement.getTargetTable().getTableExpression(), - updatingTableReference.getIdentificationVariable() + "hte_tmp" ); final TableGroupImpl temporaryTableGroup = new TableGroupImpl( updatingTableGroup.getNavigablePath(), @@ -331,7 +339,18 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio entityDescriptor ); querySpec.getFromClause().addRoot( temporaryTableGroup ); + if ( insertStatement.getValuesList().size() == 1 ) { + // Potentially apply a limit 1 to allow the use of the conflict clause emulation + querySpec.setFetchClauseExpression( + new QueryLiteral<>( + 1, + executionContext.getSession().getFactory().getNodeBuilder() .getIntegerType() + ), + FetchClauseType.ROWS_ONLY + ); + } final InsertSelectStatement insertStatement = new InsertSelectStatement( dmlTableReference ); + insertStatement.setConflictClause( conflictClause ); insertStatement.setSourceSelectStatement( querySpec ); if ( assignments != null ) { for ( Assignment assignment : assignments ) { @@ -341,7 +360,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( new ColumnReference( - updatingTableReference.getIdentificationVariable(), + temporaryTableReference.getIdentificationVariable(), columnReference.getColumnExpression(), false, null, @@ -528,7 +547,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( new ColumnReference( - updatingTableReference.getIdentificationVariable(), + temporaryTableReference.getIdentificationVariable(), idColumnReference.getColumnExpression(), false, null, @@ -617,9 +636,11 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio executionContext ); } + + return entityTableToRootIdentity.size(); } else { - jdbcServices.getJdbcMutationExecutor().execute( + return jdbcServices.getJdbcMutationExecutor().execute( jdbcInsert, JdbcParameterBindings.NO_BINDINGS, sql -> session @@ -668,13 +689,14 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio final NamedTableReference dmlTargetTableReference = resolveUnionTableReference( updatingTableReference, tableExpression ); final QuerySpec querySpec = new QuerySpec( true ); + final NamedTableReference temporaryTableReference = new NamedTableReference( + insertStatement.getTargetTable().getTableExpression(), + "hte_tmp" + ); final TableGroupImpl temporaryTableGroup = new TableGroupImpl( updatingTableGroup.getNavigablePath(), null, - new NamedTableReference( - insertStatement.getTargetTable().getTableExpression(), - updatingTableReference.getIdentificationVariable() - ), + temporaryTableReference, entityDescriptor ); querySpec.getFromClause().addRoot( temporaryTableGroup ); @@ -687,7 +709,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( new ColumnReference( - updatingTableReference.getIdentificationVariable(), + temporaryTableReference.getIdentificationVariable(), columnReference.getColumnExpression(), false, null, @@ -730,7 +752,7 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( new ColumnReference( - updatingTableReference.getIdentificationVariable(), + temporaryTableReference.getIdentificationVariable(), identifierMapping ) ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java index 1e2dfd1d16..2d2fd474f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java @@ -7,9 +7,6 @@ package org.hibernate.query.sqm.mutation.internal.temptable; import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -21,7 +18,6 @@ import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.internal.util.MutableBoolean; import org.hibernate.internal.util.MutableInteger; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; @@ -107,46 +103,16 @@ public class RestrictedDeleteExecutionDelegate extends AbstractDeleteExecutionDe ); assert hierarchyRootTableReference != null; - final Map, List>> parameterResolutions; - final Map, MappingModelExpressible> paramTypeResolutions; - - if ( getDomainParameterXref().getSqmParameterCount() == 0 ) { - parameterResolutions = Collections.emptyMap(); - paramTypeResolutions = Collections.emptyMap(); - } - else { - parameterResolutions = new IdentityHashMap<>(); - paramTypeResolutions = new LinkedHashMap<>(); - } - // Use the converter to interpret the where-clause. We do this for 2 reasons: // 1) the resolved Predicate is ultimately the base for applying restriction to the deletes // 2) we also inspect each ColumnReference that is part of the where-clause to see which // table it comes from. if all of the referenced columns (if any at all) are from the root table // we can perform all of the deletes without using an id-table - final MutableBoolean needsIdTableWrapper = new MutableBoolean( false ); - final Predicate specifiedRestriction = getConverter().visitWhereClause( - getSqmDelete().getWhereClause(), - columnReference -> { - if ( ! hierarchyRootTableReference.getIdentificationVariable().equals( columnReference.getQualifier() ) ) { - needsIdTableWrapper.setValue( true ); - } - }, - (sqmParameter, mappingType, jdbcParameters) -> { - parameterResolutions.computeIfAbsent( - sqmParameter, - k -> new ArrayList<>( 1 ) - ).add( jdbcParameters ); - paramTypeResolutions.put( sqmParameter, mappingType ); - } - ); + final Predicate specifiedRestriction = getConverter().visitWhereClause( getSqmDelete().getWhereClause() ); final PredicateCollector predicateCollector = new PredicateCollector( specifiedRestriction ); entityDescriptor.applyBaseRestrictions( - (filterPredicate) -> { - needsIdTableWrapper.setValue( true ); - predicateCollector.applyPredicate( filterPredicate ); - }, + predicateCollector, deletingTableGroup, true, executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), @@ -155,12 +121,18 @@ public class RestrictedDeleteExecutionDelegate extends AbstractDeleteExecutionDe ); getConverter().pruneTableGroupJoins(); + final ColumnReferenceCheckingSqlAstWalker walker = new ColumnReferenceCheckingSqlAstWalker( + hierarchyRootTableReference.getIdentificationVariable() + ); + if ( predicateCollector.getPredicate() != null ) { + predicateCollector.getPredicate().accept( walker ); + } // We need an id table if we want to delete from an intermediate table to avoid FK violations // The intermediate table has a FK to the root table, so we can't delete from the root table first // Deleting from the intermediate table first also isn't possible, // because that is the source for deletion in other tables, hence we need an id table - final boolean needsIdTable = needsIdTableWrapper.getValue() + final boolean needsIdTable = !walker.isAllColumnReferencesFromIdentificationVariable() || entityDescriptor != entityDescriptor.getRootEntityDescriptor(); final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ); @@ -169,8 +141,8 @@ public class RestrictedDeleteExecutionDelegate extends AbstractDeleteExecutionDe return executeWithIdTable( predicateCollector.getPredicate(), deletingTableGroup, - parameterResolutions, - paramTypeResolutions, + getConverter().getJdbcParamsBySqmParam(), + getConverter().getSqmParameterMappingModelExpressibleResolutions(), executionContextAdapter ); } @@ -178,8 +150,8 @@ public class RestrictedDeleteExecutionDelegate extends AbstractDeleteExecutionDe return executeWithoutIdTable( predicateCollector.getPredicate(), deletingTableGroup, - parameterResolutions, - paramTypeResolutions, + getConverter().getJdbcParamsBySqmParam(), + getConverter().getSqmParameterMappingModelExpressibleResolutions(), getConverter().getSqlExpressionResolver(), executionContextAdapter ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java index a533da1a01..4d8fa2d546 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java @@ -8,10 +8,7 @@ package org.hibernate.query.sqm.mutation.internal.temptable; import java.util.ArrayList; import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.function.Function; import org.hibernate.boot.model.internal.SoftDeleteHelper; @@ -20,7 +17,6 @@ import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.internal.util.MutableBoolean; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.MappingModelExpressible; @@ -42,7 +38,6 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.from.MutatingTableReferenceGroupWrapper; import org.hibernate.sql.ast.tree.from.NamedTableReference; @@ -55,7 +50,6 @@ import org.hibernate.sql.ast.tree.update.Assignment; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQueryUpdate; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.internal.SqlSelectionImpl; @@ -99,27 +93,13 @@ public class SoftDeleteExecutionDelegate extends AbstractDeleteExecutionDelegate .getEntityDescriptor( targetEntityName ); final EntityMappingType rootEntityDescriptor = targetEntityDescriptor.getRootEntityDescriptor(); - final boolean targetIsHierarchyRoot = rootEntityDescriptor == targetEntityDescriptor; // determine if we need to use a sub-query for matching ids - // 1. if the target is not the root we will // 2. if the supplied predicate (if any) refers to columns from a table // other than the identifier table we will - final MutableBoolean needsSubQueryRef = new MutableBoolean( !targetIsHierarchyRoot ); - final SqmJdbcExecutionContextAdapter executionContext = omittingLockingAndPaging( domainQueryExecutionContext ); - final Map, List>> parameterResolutions; - final Map, MappingModelExpressible> paramTypeResolutions; - if ( getDomainParameterXref().getSqmParameterCount() == 0 ) { - parameterResolutions = Collections.emptyMap(); - paramTypeResolutions = Collections.emptyMap(); - } - else { - parameterResolutions = new IdentityHashMap<>(); - paramTypeResolutions = new LinkedHashMap<>(); - } - final TableGroup deletingTableGroup = getConverter().getMutatingTableGroup(); final TableDetails softDeleteTable = rootEntityDescriptor.getSoftDeleteTableDetails(); final NamedTableReference rootTableReference = (NamedTableReference) deletingTableGroup.resolveTableReference( @@ -129,29 +109,11 @@ public class SoftDeleteExecutionDelegate extends AbstractDeleteExecutionDelegate assert rootTableReference != null; // NOTE : `converter.visitWhereClause` already applies the soft-delete restriction - final Predicate specifiedRestriction = getConverter().visitWhereClause( - getSqmDelete().getWhereClause(), - columnReference -> { - if ( !rootTableReference.getIdentificationVariable().equals( columnReference.getQualifier() ) ) { - // the predicate referred to a column from a table other than hierarchy identifier table - needsSubQueryRef.setValue( true ); - } - }, - (sqmParameter, mappingType, jdbcParameters) -> { - parameterResolutions.computeIfAbsent( - sqmParameter, - k -> new ArrayList<>( 1 ) - ).add( jdbcParameters ); - paramTypeResolutions.put( sqmParameter, mappingType ); - } - ); + final Predicate specifiedRestriction = getConverter().visitWhereClause( getSqmDelete().getWhereClause() ); final PredicateCollector predicateCollector = new PredicateCollector( specifiedRestriction ); targetEntityDescriptor.applyBaseRestrictions( - (filterPredicate) -> { - needsSubQueryRef.setValue( true ); - predicateCollector.applyPredicate( filterPredicate ); - }, + predicateCollector, deletingTableGroup, true, executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), @@ -160,26 +122,33 @@ public class SoftDeleteExecutionDelegate extends AbstractDeleteExecutionDelegate ); getConverter().pruneTableGroupJoins(); + final ColumnReferenceCheckingSqlAstWalker walker = new ColumnReferenceCheckingSqlAstWalker( + rootTableReference.getIdentificationVariable() + ); + if ( predicateCollector.getPredicate() != null ) { + predicateCollector.getPredicate().accept( walker ); + } final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( executionContext.getQueryParameterBindings(), getDomainParameterXref(), SqmUtil.generateJdbcParamsXref( getDomainParameterXref(), - () -> parameterResolutions + getConverter()::getJdbcParamsBySqmParam ), getSessionFactory().getRuntimeMetamodels().getMappingMetamodel(), navigablePath -> deletingTableGroup, new SqmParameterMappingModelResolutionAccess() { @Override @SuppressWarnings("unchecked") public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) paramTypeResolutions.get(parameter); + return (MappingModelExpressible) getConverter().getSqmParameterMappingModelExpressibleResolutions().get(parameter); } }, executionContext.getSession() ); - final boolean needsSubQuery = needsSubQueryRef.getValue(); + final boolean needsSubQuery = !walker.isAllColumnReferencesFromIdentificationVariable() + || targetEntityDescriptor != rootEntityDescriptor; if ( needsSubQuery ) { if ( getSessionFactory().getJdbcServices().getDialect().supportsSubqueryOnMutatingTable() ) { return performDeleteWithSubQuery( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java index 384ca3f74a..1be8d2c3d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java @@ -46,6 +46,7 @@ import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.select.QueryPart; @@ -290,6 +291,7 @@ public class TableBasedInsertHandler implements InsertHandler { } insertStatement.setValuesList( valuesList ); } + final ConflictClause conflictClause = converterDelegate.visitConflictClause( sqmInsertStatement.getConflictClause() ); converterDelegate.pruneTableGroupJoins(); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -313,9 +315,8 @@ public class TableBasedInsertHandler implements InsertHandler { tableReferenceByAlias, targetPathColumns, insertStatement, - converterDelegate.getJdbcParamsBySqmParam(), + conflictClause, sessionUidParameter, - converterDelegate.getSqmParameterMappingModelExpressibleResolutions(), executionContext ); } @@ -334,9 +335,8 @@ public class TableBasedInsertHandler implements InsertHandler { Map tableReferenceByAlias, List assignments, InsertSelectStatement insertStatement, - Map, List>> parameterResolutions, + ConflictClause conflictClause, JdbcParameter sessionUidParameter, - Map, MappingModelExpressible> paramTypeResolutions, DomainQueryExecutionContext executionContext) { return new InsertExecutionDelegate( sqmInsertStatement, @@ -349,9 +349,8 @@ public class TableBasedInsertHandler implements InsertHandler { tableReferenceByAlias, assignments, insertStatement, - parameterResolutions, + conflictClause, sessionUidParameter, - paramTypeResolutions, executionContext ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedUpdateHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedUpdateHandler.java index 965d4d0888..4e2543eca2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedUpdateHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedUpdateHandler.java @@ -131,60 +131,20 @@ public class TableBasedUpdateHandler ); assert hierarchyRootTableReference != null; - final Map, List>> parameterResolutions; - if ( domainParameterXref.getSqmParameterCount() == 0 ) { - parameterResolutions = Collections.emptyMap(); - } - else { - parameterResolutions = new IdentityHashMap<>(); - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // visit the set-clause using our special converter, collecting // information about the assignments - final List assignments = new ArrayList<>(); - final Map, MappingModelExpressible> paramTypeResolutions = new LinkedHashMap<>(); - - converterDelegate.visitSetClause( - getSqmDeleteOrUpdateStatement().getSetClause(), - assignments::add, - (sqmParameter, mappingType, jdbcParameters) -> { - parameterResolutions.computeIfAbsent( - sqmParameter, - k -> new ArrayList<>( 1 ) - ).add( jdbcParameters ); - paramTypeResolutions.put( sqmParameter, mappingType ); - } - ); + final List assignments = converterDelegate.visitSetClause( getSqmDeleteOrUpdateStatement().getSetClause() ); converterDelegate.addVersionedAssignment( assignments::add, getSqmDeleteOrUpdateStatement() ); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // visit the where-clause using our special converter, collecting information // about the restrictions - final Predicate providedPredicate; - final SqmWhereClause whereClause = getSqmUpdate().getWhereClause(); - if ( whereClause == null || whereClause.getPredicate() == null ) { - providedPredicate = null; - } - else { - providedPredicate = converterDelegate.visitWhereClause( - whereClause, - columnReference -> {}, - (sqmParameter, mappingType, jdbcParameters) -> { - parameterResolutions.computeIfAbsent( - sqmParameter, - k -> new ArrayList<>( 1 ) - ).add( jdbcParameters ); - paramTypeResolutions.put( sqmParameter, mappingType ); - } - - ); - assert providedPredicate != null; - } - - final PredicateCollector predicateCollector = new PredicateCollector( providedPredicate ); + final PredicateCollector predicateCollector = new PredicateCollector( + converterDelegate.visitWhereClause( getSqmUpdate().getWhereClause() ) + ); entityDescriptor.applyBaseRestrictions( predicateCollector::applyPredicate, @@ -217,8 +177,6 @@ public class TableBasedUpdateHandler tableReferenceByAlias, assignments, predicateCollector.getPredicate(), - parameterResolutions, - paramTypeResolutions, executionContext ); } @@ -233,8 +191,6 @@ public class TableBasedUpdateHandler Map tableReferenceByAlias, List assignments, Predicate suppliedPredicate, - Map, List>> parameterResolutions, - Map, MappingModelExpressible> paramTypeResolutions, DomainQueryExecutionContext executionContext) { return new UpdateExecutionDelegate( sqmConverter, @@ -246,8 +202,6 @@ public class TableBasedUpdateHandler tableReferenceByAlias, assignments, suppliedPredicate, - parameterResolutions, - paramTypeResolutions, executionContext ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java index cf9f51beb9..568a849953 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java @@ -92,8 +92,6 @@ public class UpdateExecutionDelegate implements TableBasedUpdateHandler.Executio Map tableReferenceByAlias, List assignments, Predicate suppliedPredicate, - Map, List>> parameterResolutions, - Map, MappingModelExpressible> paramTypeResolutions, DomainQueryExecutionContext executionContext) { this.sqmConverter = sqmConverter; this.idTable = idTable; @@ -130,14 +128,14 @@ public class UpdateExecutionDelegate implements TableBasedUpdateHandler.Executio domainParameterXref, SqmUtil.generateJdbcParamsXref( domainParameterXref, - () -> parameterResolutions + sqmConverter::getJdbcParamsBySqmParam ), sessionFactory.getRuntimeMetamodels().getMappingMetamodel(), navigablePath -> updatingTableGroup, new SqmParameterMappingModelResolutionAccess() { @Override @SuppressWarnings("unchecked") public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) paramTypeResolutions.get(parameter); + return (MappingModelExpressible) sqmConverter.getSqmParameterMappingModelExpressibleResolutions().get(parameter); } }, executionContext.getSession() diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java index ddd39bf7f3..8231bd09a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java @@ -80,6 +80,8 @@ import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.insert.SqmConflictClause; +import org.hibernate.query.sqm.tree.insert.SqmConflictUpdateAction; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; import org.hibernate.query.sqm.tree.insert.SqmValues; @@ -166,6 +168,10 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker conflictClause = statement.getConflictClause(); + if ( conflictClause != null ) { + visitConflictClause( conflictClause ); + } return statement; } @@ -179,9 +185,29 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker conflictClause = statement.getConflictClause(); + if ( conflictClause != null ) { + visitConflictClause( conflictClause ); + } return statement; } + @Override + public Object visitConflictClause(SqmConflictClause sqmConflictClause) { + final SqmConflictUpdateAction updateAction = sqmConflictClause.getConflictAction(); + for ( SqmPath stateField : sqmConflictClause.getConstraintPaths() ) { + stateField.accept( this ); + } + if ( updateAction != null ) { + visitSetClause( updateAction.getSetClause() ); + final SqmWhereClause whereClause = updateAction.getWhereClause(); + if ( whereClause != null ) { + visitWhereClause( whereClause ); + } + } + return sqmConflictClause; + } + @Override public Object visitDeleteStatement(SqmDeleteStatement statement) { visitCteContainer( statement ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 297cf2e124..619b1c3596 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -224,6 +224,8 @@ import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.insert.SqmConflictClause; +import org.hibernate.query.sqm.tree.insert.SqmConflictUpdateAction; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; @@ -270,6 +272,7 @@ import org.hibernate.sql.ast.SqlTreeCreationException; import org.hibernate.sql.ast.SqlTreeCreationLogger; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlAliasBase; +import org.hibernate.sql.ast.spi.SqlAliasBaseConstant; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; import org.hibernate.sql.ast.spi.SqlAliasBaseManager; import org.hibernate.sql.ast.spi.SqlAstCreationContext; @@ -332,6 +335,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.InsertStatement; import org.hibernate.sql.ast.tree.insert.Values; @@ -961,21 +965,11 @@ public abstract class BaseSqmToSqlAstConverter extends Base for ( SqmAssignment sqmAssignment : setClause.getAssignments() ) { final SqmPathInterpretation assignedPathInterpretation; try { - pushProcessingState( - new SqlAstProcessingStateImpl( - getCurrentProcessingState(), - this, - getCurrentClauseStack()::getCurrent - ), - getFromClauseIndex() - ); currentClauseStack.push( Clause.SET ); - assignedPathInterpretation = (SqmPathInterpretation) sqmAssignment.getTargetPath().accept( this ); } finally { currentClauseStack.pop(); - popProcessingStateStack(); } try { @@ -1039,6 +1033,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base addAssignment( assignments, aggregateColumnAssignmentHandler, columnReference, expressions.get( i ) ); } } + else if ( valueExpression instanceof EmbeddableValuedPathInterpretation ) { + final List expressions = ( (EmbeddableValuedPathInterpretation) valueExpression ).getSqlTuple().getExpressions(); + assert targetColumnReferences.size() == expressions.size(); + for ( int i = 0; i < targetColumnReferences.size(); i++ ) { + final ColumnReference columnReference = targetColumnReferences.get( i ); + addAssignment( assignments, aggregateColumnAssignmentHandler, columnReference, expressions.get( i ) ); + } + } else { for ( ColumnReference columnReference : targetColumnReferences ) { addAssignment( assignments, aggregateColumnAssignmentHandler, columnReference, valueExpression ); @@ -1227,6 +1229,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base } ); + insertStatement.setConflictClause( visitConflictClause( sqmStatement.getConflictClause() ) ); + this.currentSqmStatement = oldSqmStatement; this.cteContainer = oldCteContainer; @@ -1321,6 +1325,8 @@ public abstract class BaseSqmToSqlAstConverter extends Base insertStatement.getValuesList().add( values ); } + insertStatement.setConflictClause( visitConflictClause( sqmStatement.getConflictClause() ) ); + return insertStatement; } finally { @@ -1330,6 +1336,46 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } + @Override + public ConflictClause visitConflictClause(SqmConflictClause sqmConflictClause) { + if ( sqmConflictClause == null ) { + return null; + } + final List> constraintAttributes = sqmConflictClause.getConstraintPaths(); + final List constraintColumnNames = new ArrayList<>( constraintAttributes.size() ); + for ( SqmPath constraintAttribute : constraintAttributes ) { + final Assignable assignable = ( (Assignable) constraintAttribute.accept( this ) ); + for ( ColumnReference columnReference : assignable.getColumnReferences() ) { + constraintColumnNames.add( columnReference.getSelectableName() ); + } + } + final SqmConflictUpdateAction updateAction = sqmConflictClause.getConflictAction(); + final List assignments; + final Predicate predicate; + if ( updateAction == null ) { + assignments = Collections.emptyList(); + predicate = null; + } + else { + final SqmRoot excludedRoot = sqmConflictClause.getExcludedRoot(); + final EntityPersister entityDescriptor = resolveEntityPersister( excludedRoot.getModel() ); + final TableGroup tableGroup = entityDescriptor.createRootTableGroup( + true, + excludedRoot.getNavigablePath(), + excludedRoot.getExplicitAlias(), + new SqlAliasBaseConstant( "excluded" ), + () -> null, + this + ); + registerSqmFromTableGroup( excludedRoot, tableGroup ); + assignments = visitSetClause( updateAction.getSetClause() ); + final SqmWhereClause whereClause = updateAction.getWhereClause(); + predicate = whereClause == null ? null : visitWhereClause( whereClause.getPredicate() ); + } + + return new ConflictClause( sqmConflictClause.getConstraintName(), constraintColumnNames, assignments, predicate ); + } + public AdditionalInsertValues visitInsertionTargetPaths( BiConsumer> targetColumnReferenceConsumer, SqmInsertStatement sqmStatement, @@ -1579,12 +1625,13 @@ public abstract class BaseSqmToSqlAstConverter extends Base @Override public Values visitValues(SqmValues sqmValues) { - final Values values = new Values(); - for ( SqmExpression expression : sqmValues.getExpressions() ) { + final List> expressions = sqmValues.getExpressions(); + final ArrayList valuesExpressions = new ArrayList<>( expressions.size() ); + for ( SqmExpression expression : expressions ) { // todo: add WriteExpression handling - values.getExpressions().add( (Expression) expression.accept( this ) ); + valuesExpressions.add( (Expression) expression.accept( this ) ); } - return values; + return new Values( valuesExpressions ); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2473,6 +2520,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base return Collections.emptyList(); } + @Override + public Predicate visitWhereClause(SqmWhereClause whereClause) { + if ( whereClause == null ) { + return null; + } + return visitWhereClause( whereClause.getPredicate() ); + } + private Predicate visitWhereClause(SqmPredicate sqmPredicate) { if ( sqmPredicate == null ) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java index bbd93e034b..8cb08bcbb9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmDmlStatement.java @@ -16,6 +16,7 @@ import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmQuerySource; +import org.hibernate.query.sqm.tree.cte.SqmCteContainer; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; @@ -62,6 +63,14 @@ public abstract class AbstractSqmDmlStatement return cteStatements; } + protected void putAllCtes(SqmCteContainer cteContainer) { + for ( SqmCteStatement cteStatement : cteContainer.getCteStatements() ) { + if ( cteStatements.putIfAbsent( cteStatement.getName(), cteStatement ) != null ) { + throw new IllegalArgumentException( "A CTE with the label " + cteStatement.getCteTable().getCteName() + " already exists" ); + } + } + } + @Override public Collection> getCteStatements() { return cteStatements.values(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java index 77f83a2142..15af53e2b5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java @@ -7,12 +7,15 @@ package org.hibernate.query.sqm.tree.insert; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import org.hibernate.query.criteria.JpaConflictClause; +import org.hibernate.query.criteria.JpaCriteriaInsert; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.tree.AbstractSqmDmlStatement; @@ -22,6 +25,9 @@ import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; +import jakarta.persistence.criteria.Path; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Convenience base class for InsertSqmStatement implementations. * @@ -29,6 +35,7 @@ import org.hibernate.query.sqm.tree.from.SqmRoot; */ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatement implements SqmInsertStatement { private List> insertionTargetPaths; + private @Nullable SqmConflictClause conflictClause; protected AbstractSqmInsertStatement(SqmQuerySource querySource, NodeBuilder nodeBuilder) { super( querySource, nodeBuilder ); @@ -38,6 +45,7 @@ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatem super( targetRoot, querySource, nodeBuilder ); } + @Deprecated(forRemoval = true) protected AbstractSqmInsertStatement( NodeBuilder builder, SqmQuerySource querySource, @@ -45,8 +53,20 @@ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatem Map> cteStatements, SqmRoot target, List> insertionTargetPaths) { + this( builder, querySource, parameters, cteStatements, target, insertionTargetPaths, null ); + } + + protected AbstractSqmInsertStatement( + NodeBuilder builder, + SqmQuerySource querySource, + Set> parameters, + Map> cteStatements, + SqmRoot target, + List> insertionTargetPaths, + SqmConflictClause conflictClause) { super( builder, querySource, parameters, cteStatements, target ); this.insertionTargetPaths = insertionTargetPaths; + this.conflictClause = conflictClause; } protected List> copyInsertionTargetPaths(SqmCopyContext context) { @@ -69,8 +89,16 @@ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatem : Collections.unmodifiableList( insertionTargetPaths ); } - public void setInsertionTargetPaths(List> insertionTargetPaths) { - this.insertionTargetPaths = insertionTargetPaths; + @Override + public SqmInsertStatement setInsertionTargetPaths(Path... insertionTargetPaths) { + return setInsertionTargetPaths( Arrays.asList( insertionTargetPaths ) ); + } + + @Override + public SqmInsertStatement setInsertionTargetPaths(List> insertionTargetPaths) { + //noinspection unchecked + this.insertionTargetPaths = (List>) insertionTargetPaths; + return this; } public void addInsertTargetStateField(SqmPath stateField) { @@ -87,6 +115,27 @@ public abstract class AbstractSqmInsertStatement extends AbstractSqmDmlStatem } } + @Override + public SqmConflictClause createConflictClause() { + return new SqmConflictClause<>( this ); + } + + @Override + public @Nullable SqmConflictClause getConflictClause() { + return conflictClause; + } + + @Override + public JpaConflictClause onConflict() { + return this.conflictClause = createConflictClause(); + } + + @Override + public JpaCriteriaInsert onConflict(@Nullable JpaConflictClause conflictClause) { + this.conflictClause = (SqmConflictClause) conflictClause; + return this; + } + @Override public void appendHqlString(StringBuilder sb) { appendHqlCteString( sb ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java new file mode 100644 index 0000000000..b1fcc57ca1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java @@ -0,0 +1,216 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.insert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.hibernate.query.criteria.JpaConflictClause; +import org.hibernate.query.criteria.JpaConflictUpdateAction; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmVisitableNode; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.from.SqmRoot; + +import jakarta.persistence.criteria.Path; +import jakarta.persistence.metamodel.SingularAttribute; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * @since 6.5 + */ +public class SqmConflictClause implements SqmVisitableNode, JpaConflictClause { + + private final SqmInsertStatement insertStatement; + private final SqmRoot excludedRoot; + private @Nullable String constraintName; + private List> constraintPaths; + private @Nullable SqmConflictUpdateAction updateAction; + + public SqmConflictClause(SqmInsertStatement insertStatement) { + this.insertStatement = insertStatement; + this.excludedRoot = new SqmRoot<>( + insertStatement.getTarget().getManagedType(), + "excluded", + false, + insertStatement.nodeBuilder() + ); + } + + private SqmConflictClause( + SqmInsertStatement insertStatement, + SqmRoot excludedRoot, + @Nullable String constraintName, + List> constraintPaths, + @Nullable SqmConflictUpdateAction updateAction) { + this.insertStatement = insertStatement; + this.excludedRoot = excludedRoot; + this.constraintName = constraintName; + this.constraintPaths = Collections.unmodifiableList( constraintPaths ); + this.updateAction = updateAction; + } + + @Override + public SqmRoot getExcludedRoot() { + return excludedRoot; + } + + @Override + public @Nullable String getConstraintName() { + return constraintName; + } + + @Override + public SqmConflictClause conflictOnConstraint(@Nullable String constraintName) { + if ( !constraintPaths.isEmpty() ) { + throw new IllegalStateException( "Constraint paths were already set: " + constraintPaths ); + } + this.constraintName = constraintName; + return this; + } + + @Override + public JpaConflictClause conflictOnConstraintAttributes(String... attributes) { + final ArrayList> paths = new ArrayList<>( attributes.length ); + for ( String attribute : attributes ) { + paths.add( insertStatement.getTarget().get( attribute ) ); + } + return conflictOnConstraintPaths( paths ); + } + + @Override + public JpaConflictClause conflictOnConstraintAttributes(SingularAttribute... attributes) { + final ArrayList> paths = new ArrayList<>( attributes.length ); + for ( SingularAttribute attribute : attributes ) { + paths.add( insertStatement.getTarget().get( attribute ) ); + } + return conflictOnConstraintPaths( paths ); + } + + @Override + public SqmConflictClause conflictOnConstraintPaths(Path... paths) { + return conflictOnConstraintPaths( Arrays.asList( paths ) ); + } + + @Override + public SqmConflictClause conflictOnConstraintPaths(List> paths) { + if ( constraintName != null ) { + throw new IllegalStateException( "Constraint name was already set: " + constraintName ); + } + //noinspection unchecked + this.constraintPaths = (List>) Collections.unmodifiableList( paths ); + return this; + } + + @Override + public List> getConstraintPaths() { + return constraintPaths == null + ? Collections.emptyList() + : constraintPaths; + } + + @Override + public SqmConflictUpdateAction createConflictUpdateAction() { + return new SqmConflictUpdateAction<>( insertStatement ); + } + + @Override + public @Nullable SqmConflictUpdateAction getConflictAction() { + return updateAction; + } + + @Override + public JpaConflictClause onConflictDo(JpaConflictUpdateAction action) { + this.updateAction = (SqmConflictUpdateAction) action; + return this; + } + + @Override + public SqmConflictUpdateAction onConflictDoUpdate() { + final SqmConflictUpdateAction conflictUpdateAction = createConflictUpdateAction(); + onConflictDo( conflictUpdateAction ); + return conflictUpdateAction; + } + + @Override + public NodeBuilder nodeBuilder() { + return insertStatement.nodeBuilder(); + } + + @Override + public SqmConflictClause copy(SqmCopyContext context) { + final SqmConflictClause existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + return context.registerCopy( + this, + new SqmConflictClause<>( + insertStatement.copy( context ), + excludedRoot.copy( context ), + constraintName, + copyOf( constraintPaths, context ), + updateAction == null ? null : updateAction.copy( context ) + ) + ); + } + + private List> copyOf(List> constraintPaths, SqmCopyContext context) { + if ( constraintPaths.isEmpty() ) { + return constraintPaths; + } + final ArrayList> copies = new ArrayList<>( constraintPaths.size() ); + for ( SqmPath constraintPath : constraintPaths ) { + copies.add( constraintPath.copy( context ) ); + } + return copies; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitConflictClause( this ); + } + + public void appendHqlString(StringBuilder sb) { + sb.append( " on conflict" ); + if ( constraintName != null ) { + sb.append( " on constraint " ); + sb.append( constraintName ); + } + else if ( !constraintPaths.isEmpty() ) { + char separator = '('; + for ( SqmPath path : constraintPaths ) { + sb.append( separator ); + appendUnqualifiedPath( sb, path ); + separator = ','; + } + sb.append( ')' ); + } + if ( updateAction == null ) { + sb.append( " do nothing" ); + } + else { + updateAction.appendHqlString( sb ); + } + } + + private static void appendUnqualifiedPath(StringBuilder sb, SqmPath path) { + if ( path.getLhs() == null ) { + // Skip rendering the root + return; + } + appendUnqualifiedPath( sb, path.getLhs() ); + if ( path.getLhs().getLhs() != null ) { + sb.append( '.' ); + } + sb.append( path.getReferencedPathSource().getPathName() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictUpdateAction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictUpdateAction.java new file mode 100644 index 0000000000..4f713b12f6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictUpdateAction.java @@ -0,0 +1,170 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.insert; + +import org.hibernate.query.criteria.JpaConflictUpdateAction; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmNode; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.predicate.SqmWhereClause; +import org.hibernate.query.sqm.tree.update.SqmAssignment; +import org.hibernate.query.sqm.tree.update.SqmSetClause; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.metamodel.SingularAttribute; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static org.hibernate.query.sqm.internal.TypecheckUtil.assertAssignable; + +/** + * @since 6.5 + */ +public class SqmConflictUpdateAction implements SqmNode, JpaConflictUpdateAction { + + private final SqmInsertStatement insertStatement; + private final SqmSetClause setClause; + private @Nullable SqmWhereClause whereClause; + + public SqmConflictUpdateAction(SqmInsertStatement insertStatement) { + this.insertStatement = insertStatement; + this.setClause = new SqmSetClause(); + } + + private SqmConflictUpdateAction( + SqmInsertStatement insertStatement, + SqmSetClause setClause, + @Nullable SqmWhereClause whereClause) { + this.insertStatement = insertStatement; + this.setClause = setClause; + this.whereClause = whereClause; + } + + @Override + public SqmConflictUpdateAction set(SingularAttribute attribute, X value) { + applyAssignment( getTarget().get( attribute ), (SqmExpression) nodeBuilder().value( value ) ); + return this; + } + + @Override + public SqmConflictUpdateAction set(SingularAttribute attribute, Expression value) { + applyAssignment( getTarget().get( attribute ), (SqmExpression) value ); + return this; + } + + @Override + public SqmConflictUpdateAction set(Path attribute, X value) { + applyAssignment( (SqmPath) attribute, (SqmExpression) nodeBuilder().value( value ) ); + return this; + } + + @Override + public SqmConflictUpdateAction set(Path attribute, Expression value) { + applyAssignment( (SqmPath) attribute, (SqmExpression) value ); + return this; + } + + @Override + public SqmConflictUpdateAction set(String attributeName, Object value) { + final SqmPath sqmPath = getTarget().get(attributeName); + final SqmExpression expression; + if ( value instanceof SqmExpression ) { + expression = (SqmExpression) value; + } + else { + expression = (SqmExpression) nodeBuilder().value( value ); + } + assertAssignable( null, sqmPath, expression, nodeBuilder().getSessionFactory() ); + applyAssignment( sqmPath, expression ); + return this; + } + + public void addAssignment(SqmAssignment assignment) { + setClause.addAssignment( assignment ); + } + + private void applyAssignment(SqmPath targetPath, SqmExpression value) { + setClause.addAssignment( new SqmAssignment<>( targetPath, value ) ); + } + + @Override + public SqmConflictUpdateAction where(Expression restriction) { + initAndGetWhereClause().setPredicate( (SqmPredicate) restriction ); + return this; + } + + @Override + public SqmConflictUpdateAction where(Predicate... restrictions) { + final SqmWhereClause whereClause = initAndGetWhereClause(); + // Clear the current predicate if one is present + whereClause.setPredicate(null); + for ( Predicate restriction : restrictions ) { + whereClause.applyPredicate( (SqmPredicate) restriction ); + } + return this; + } + + @Override + public SqmPredicate getRestriction() { + return whereClause == null ? null : whereClause.getPredicate(); + } + + protected SqmWhereClause initAndGetWhereClause() { + if ( whereClause == null ) { + whereClause = new SqmWhereClause( nodeBuilder() ); + } + return whereClause; + } + + @Override + public NodeBuilder nodeBuilder() { + return insertStatement.nodeBuilder(); + } + + @Override + public SqmConflictUpdateAction copy(SqmCopyContext context) { + final SqmConflictUpdateAction existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + return context.registerCopy( + this, + new SqmConflictUpdateAction<>( + insertStatement.copy( context ), + setClause.copy( context ), + whereClause == null ? null : whereClause.copy( context ) + ) + ); + } + + public SqmSetClause getSetClause() { + return setClause; + } + + public SqmWhereClause getWhereClause() { + return whereClause; + } + + private SqmRoot getTarget() { + return insertStatement.getTarget(); + } + + public void appendHqlString(StringBuilder sb) { + sb.append( " do update" ); + setClause.appendHqlString( sb ); + + if ( whereClause != null && whereClause.getPredicate() != null ) { + sb.append( " where " ); + whereClause.getPredicate().appendHqlString( sb ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java index d9b10a24f8..8b431eab82 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Set; import org.hibernate.Incubating; +import org.hibernate.query.criteria.JpaConflictClause; import org.hibernate.query.criteria.JpaCriteriaInsertSelect; import org.hibernate.query.criteria.JpaPredicate; import org.hibernate.query.sqm.NodeBuilder; @@ -23,6 +24,11 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; + +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Path; /** * @author Steve Ebersole @@ -57,8 +63,9 @@ public class SqmInsertSelectStatement extends AbstractSqmInsertStatement i Map> cteStatements, SqmRoot target, List> insertionTargetPaths, + SqmConflictClause conflictClause, SqmQueryPart selectQueryPart) { - super( builder, querySource, parameters, cteStatements, target, insertionTargetPaths ); + super( builder, querySource, parameters, cteStatements, target, insertionTargetPaths, conflictClause ); this.selectQueryPart = selectQueryPart; } @@ -77,11 +84,20 @@ public class SqmInsertSelectStatement extends AbstractSqmInsertStatement i copyCteStatements( context ), getTarget().copy( context ), copyInsertionTargetPaths( context ), + getConflictClause() == null ? null : getConflictClause().copy( context ), selectQueryPart.copy( context ) ) ); } + @Override + public SqmInsertSelectStatement select(CriteriaQuery criteriaQuery) { + final SqmSelectStatement selectStatement = (SqmSelectStatement) criteriaQuery; + putAllCtes( selectStatement ); + setSelectQueryPart( selectStatement.getQueryPart() ); + return this; + } + public SqmQueryPart getSelectQueryPart() { return selectQueryPart; } @@ -101,10 +117,32 @@ public class SqmInsertSelectStatement extends AbstractSqmInsertStatement i return null; } + @Override + public SqmInsertSelectStatement setInsertionTargetPaths(Path... insertionTargetPaths) { + super.setInsertionTargetPaths( insertionTargetPaths ); + return this; + } + + @Override + public SqmInsertSelectStatement setInsertionTargetPaths(List> insertionTargetPaths) { + super.setInsertionTargetPaths( insertionTargetPaths ); + return this; + } + + @Override + public SqmInsertSelectStatement onConflict(JpaConflictClause conflictClause) { + super.onConflict( conflictClause ); + return this; + } + @Override public void appendHqlString(StringBuilder sb) { super.appendHqlString( sb ); sb.append( ' ' ); selectQueryPart.appendHqlString( sb ); + final SqmConflictClause conflictClause = getConflictClause(); + if ( conflictClause != null ) { + conflictClause.appendHqlString( sb ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertStatement.java index 2a5b8f3911..7aba91cb4b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertStatement.java @@ -9,16 +9,34 @@ package org.hibernate.query.sqm.tree.insert; import java.util.List; import java.util.function.Consumer; +import org.hibernate.query.criteria.JpaCriteriaInsert; +import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmDmlStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; +import jakarta.persistence.criteria.Path; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * The general contract for INSERT statements. At the moment only the INSERT-SELECT * forms is implemented/supported. * * @author Steve Ebersole */ -public interface SqmInsertStatement extends SqmDmlStatement { +public interface SqmInsertStatement extends SqmDmlStatement, JpaCriteriaInsert { + @Override List> getInsertionTargetPaths(); + + @Override + SqmInsertStatement setInsertionTargetPaths(Path... insertionTargetPaths); + + @Override + SqmInsertStatement setInsertionTargetPaths(List> insertionTargetPaths); + + @Override + SqmInsertStatement copy(SqmCopyContext context); + void visitInsertionTargetPaths(Consumer> consumer); + + @Nullable SqmConflictClause getConflictClause(); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java index a1142c85ee..fac062cbe1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java @@ -7,12 +7,16 @@ package org.hibernate.query.sqm.tree.insert; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import org.hibernate.query.criteria.JpaConflictClause; import org.hibernate.query.criteria.JpaCriteriaInsertValues; import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.criteria.JpaValues; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.SqmQuerySource; @@ -23,15 +27,17 @@ import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; +import jakarta.persistence.criteria.Path; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Gavin King */ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement implements JpaCriteriaInsertValues { - private final List valuesList; + private @Nullable List valuesList; public SqmInsertValuesStatement(SqmRoot targetRoot, NodeBuilder nodeBuilder) { super( targetRoot, SqmQuerySource.HQL, nodeBuilder ); - this.valuesList = new ArrayList<>(); } public SqmInsertValuesStatement(Class targetEntity, NodeBuilder nodeBuilder) { @@ -45,7 +51,6 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i SqmQuerySource.CRITERIA, nodeBuilder ); - this.valuesList = new ArrayList<>(); } private SqmInsertValuesStatement( @@ -55,8 +60,9 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i Map> cteStatements, SqmRoot target, List> insertionTargetPaths, + SqmConflictClause conflictClause, List valuesList) { - super( builder, querySource, parameters, cteStatements, target, insertionTargetPaths ); + super( builder, querySource, parameters, cteStatements, target, insertionTargetPaths, conflictClause ); this.valuesList = valuesList; } @@ -66,9 +72,15 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i if ( existing != null ) { return existing; } - final List valuesList = new ArrayList<>( this.valuesList.size() ); - for ( SqmValues sqmValues : this.valuesList ) { - valuesList.add( sqmValues.copy( context ) ); + final List valuesList; + if ( this.valuesList == null ) { + valuesList = null; + } + else { + valuesList = new ArrayList<>( this.valuesList.size() ); + for ( SqmValues sqmValues : this.valuesList ) { + valuesList.add( sqmValues.copy( context ) ); + } } return context.registerCopy( this, @@ -79,6 +91,7 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i copyCteStatements( context ), getTarget().copy( context ), copyInsertionTargetPaths( context ), + getConflictClause() == null ? null : getConflictClause().copy( context ), valuesList ) ); @@ -94,13 +107,16 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i copyCteStatements( context ), getTarget().copy( context ), copyInsertionTargetPaths( context ), - new ArrayList<>() + getConflictClause() == null ? null : getConflictClause().copy( context ), + null ) ); } public List getValuesList() { - return valuesList; + return valuesList == null + ? Collections.emptyList() + : Collections.unmodifiableList( valuesList ); } @Override @@ -113,8 +129,39 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i return null; } + @Override + public SqmInsertValuesStatement setInsertionTargetPaths(Path... insertionTargetPaths) { + super.setInsertionTargetPaths( insertionTargetPaths ); + return this; + } + + @Override + public SqmInsertValuesStatement setInsertionTargetPaths(List> insertionTargetPaths) { + super.setInsertionTargetPaths( insertionTargetPaths ); + return this; + } + + @Override + public SqmInsertValuesStatement values(JpaValues... values) { + return values( Arrays.asList( values ) ); + } + + @Override + public SqmInsertValuesStatement values(List values) { + //noinspection unchecked + this.valuesList = (List) values; + return this; + } + + @Override + public SqmInsertValuesStatement onConflict(JpaConflictClause conflictClause) { + super.onConflict( conflictClause ); + return this; + } + @Override public void appendHqlString(StringBuilder sb) { + assert valuesList != null; super.appendHqlString( sb ); sb.append( " values (" ); appendValues( valuesList.get( 0 ), sb ); @@ -123,6 +170,10 @@ public class SqmInsertValuesStatement extends AbstractSqmInsertStatement i appendValues( valuesList.get( i ), sb ); } sb.append( ')' ); + final SqmConflictClause conflictClause = getConflictClause(); + if ( conflictClause != null ) { + conflictClause.appendHqlString( sb ); + } } private static void appendValues(SqmValues sqmValues, StringBuilder sb) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmValues.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmValues.java index a6e637c87b..70365defae 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmValues.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmValues.java @@ -6,21 +6,23 @@ */ package org.hibernate.query.sqm.tree.insert; +import org.hibernate.query.criteria.JpaValues; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.expression.SqmExpression; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** * @author Gavin King */ -public class SqmValues implements Serializable { +public class SqmValues implements JpaValues, Serializable { private final List> expressions; - public SqmValues() { - this.expressions = new ArrayList<>(); + public SqmValues(List> expressions) { + this.expressions = expressions; } private SqmValues(SqmValues original, SqmCopyContext context) { @@ -34,7 +36,8 @@ public class SqmValues implements Serializable { return new SqmValues( this, context ); } + @Override public List> getExpressions() { - return expressions; + return Collections.unmodifiableList( expressions ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java index c9a3b94d27..c884824466 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java @@ -47,4 +47,19 @@ public class SqmSetClause { public void addAssignment(SqmPath targetPath, SqmExpression value) { addAssignment( new SqmAssignment<>( targetPath, value ) ); } + + public void appendHqlString(StringBuilder sb) { + sb.append( " set " ); + appendAssignment( assignments.get( 0 ), sb ); + for ( int i = 1; i < assignments.size(); i++ ) { + sb.append( ", " ); + appendAssignment( assignments.get( i ), sb ); + } + } + + private static void appendAssignment(SqmAssignment sqmAssignment, StringBuilder sb) { + sqmAssignment.getTargetPath().appendHqlString( sb ); + sb.append( " = " ); + sqmAssignment.getValue().appendHqlString( sb ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java index 6bbc43aa20..d592f1db76 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java @@ -6,7 +6,6 @@ */ package org.hibernate.query.sqm.tree.update; -import java.util.List; import java.util.Map; import java.util.Set; @@ -176,10 +175,14 @@ public class SqmUpdateStatement } public void applyAssignment(SqmPath targetPath, SqmExpression value) { + applyAssignment( new SqmAssignment<>( targetPath, value ) ); + } + + public void applyAssignment(SqmAssignment assignment) { if ( setClause == null ) { setClause = new SqmSetClause(); } - setClause.addAssignment( new SqmAssignment<>( targetPath, value ) ); + setClause.addAssignment( assignment ); } @Override @@ -191,20 +194,8 @@ public class SqmUpdateStatement } sb.append( getTarget().getEntityName() ); sb.append( ' ' ).append( getTarget().resolveAlias() ); - sb.append( " set " ); - final List> assignments = setClause.getAssignments(); - appendAssignment( assignments.get( 0 ), sb ); - for ( int i = 1; i < assignments.size(); i++ ) { - sb.append( ", " ); - appendAssignment( assignments.get( i ), sb ); - } + setClause.appendHqlString( sb ); super.appendHqlString( sb ); } - - private static void appendAssignment(SqmAssignment sqmAssignment, StringBuilder sb) { - sqmAssignment.getTargetPath().appendHqlString( sb ); - sb.append( " = " ); - sqmAssignment.getValue().appendHqlString( sb ); - } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java index 98fff078e0..7171a0e142 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/Clause.java @@ -46,6 +46,7 @@ public enum Clause { * delete */ DELETE, + MERGE, SELECT, FROM, @@ -62,6 +63,7 @@ public enum Clause { WITH, WITHIN_GROUP, PARTITION, + CONFLICT, CALL, /** 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 c723ea7f7f..6edf0e2e54 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 @@ -49,9 +49,11 @@ import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityAssociationMapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; @@ -83,6 +85,7 @@ import org.hibernate.query.sqm.sql.internal.EntityValuedPathInterpretation; import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation; import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation; import org.hibernate.query.sqm.tree.expression.Conversion; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; @@ -129,6 +132,7 @@ import org.hibernate.sql.ast.tree.expression.Over; import org.hibernate.sql.ast.tree.expression.Overflow; import org.hibernate.sql.ast.tree.expression.QueryLiteral; import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; @@ -145,7 +149,6 @@ import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableGroup; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.StandardTableGroup; -import org.hibernate.sql.ast.tree.from.StandardVirtualTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableGroupProducer; @@ -153,6 +156,7 @@ import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.TableReferenceJoin; import org.hibernate.sql.ast.tree.from.ValuesTableReference; import org.hibernate.sql.ast.tree.from.VirtualTableGroup; +import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.predicate.BetweenPredicate; @@ -488,23 +492,16 @@ public abstract class AbstractSqlAstTranslator implemen return affectedTableNames; } - protected String getDmlTargetTableAlias() { - final MutationStatement currentDmlStatement = getCurrentDmlStatement(); - return currentDmlStatement == null - ? null - : currentDmlStatement.getTargetTable().getIdentificationVariable(); - } - protected Statement getStatement() { return statementStack.getRoot(); } public MutationStatement getCurrentDmlStatement() { - return statementStack.findCurrentFirst( AbstractSqlAstTranslator::matchMutationStatementNoInsertSelect ); + return statementStack.findCurrentFirst( AbstractSqlAstTranslator::matchMutationStatement ); } - private static MutationStatement matchMutationStatementNoInsertSelect(Statement stmt) { - if ( stmt instanceof MutationStatement && !( stmt instanceof InsertSelectStatement ) ) { + private static MutationStatement matchMutationStatement(Statement stmt) { + if ( stmt instanceof MutationStatement ) { return (MutationStatement) stmt; } return null; @@ -744,6 +741,10 @@ public abstract class AbstractSqlAstTranslator implemen return clauseStack; } + protected Stack getStatementStack() { + return statementStack; + } + protected Stack getQueryPartStack() { return queryPartStack; } @@ -847,10 +848,25 @@ public abstract class AbstractSqlAstTranslator implemen protected JdbcOperationQueryInsert translateInsert(InsertSelectStatement sqlAst) { visitInsertStatement( sqlAst ); + final ConflictClause conflictClause = sqlAst.getConflictClause(); + final String uniqueConstraintNameThatMayFail; + if ( conflictClause == null || !conflictClause.getConstraintColumnNames().isEmpty() ) { + uniqueConstraintNameThatMayFail = null; + } + else { + if ( sqlAst.getSourceSelectStatement() != null && !isFetchFirstRowOnly( sqlAst.getSourceSelectStatement() ) + || sqlAst.getValuesList().size() > 1 ) { + throw new IllegalQueryOperationException( "Can't emulate conflict clause with constraint name for more than one row to insert" ); + } + uniqueConstraintNameThatMayFail = conflictClause.getConstraintName() == null + ? "" + : conflictClause.getConstraintName(); + } return new JdbcOperationQueryInsertImpl( getSql(), getParameterBinders(), - getAffectedTableNames() + getAffectedTableNames(), + uniqueConstraintNameThatMayFail ); } @@ -1048,98 +1064,109 @@ public abstract class AbstractSqlAstTranslator implemen } protected void visitDeleteStatementOnly(DeleteStatement statement) { - // todo (6.0) : to support joins we need dialect support - appendSql( "delete from " ); - final Stack clauseStack = getClauseStack(); - try { - clauseStack.push( Clause.DELETE ); - renderNamedTableReference( statement.getTargetTable(), LockMode.NONE ); - } - finally { - clauseStack.pop(); - } - - if ( statement.getFromClause().hasJoins() ) { - visitWhereClause( determineWhereClauseRestrictionWithJoinEmulation( statement ) ); + renderDeleteClause( statement ); + if ( supportsJoinsInDelete() || !hasNonTrivialFromClause( statement.getFromClause() ) ) { + visitWhereClause( statement.getRestriction() ); } else { - visitWhereClause( statement.getRestriction() ); + visitWhereClause( determineWhereClauseRestrictionWithJoinEmulation( statement ) ); } visitReturningColumns( statement.getReturningColumns() ); } - protected void visitUpdateStatementOnly(UpdateStatement statement) { - // todo (6.0) : to support joins we need dialect support - appendSql( "update " ); + protected boolean supportsJoinsInDelete() { + return false; + } + + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete from " ); final Stack clauseStack = getClauseStack(); try { - clauseStack.push( Clause.UPDATE ); - renderNamedTableReference( statement.getTargetTable(), LockMode.NONE ); + clauseStack.push( Clause.DELETE ); + renderDmlTargetTableExpression( statement.getTargetTable() ); } finally { clauseStack.pop(); } + } - renderSetClause( statement, clauseStack ); - if ( statement.getFromClause().hasJoins() ) { - visitWhereClause( determineWhereClauseRestrictionWithJoinEmulation( statement ) ); - } - else { + protected void visitUpdateStatementOnly(UpdateStatement statement) { + renderUpdateClause( statement ); + renderSetClause( statement.getAssignments() ); + renderFromClauseAfterUpdateSet( statement ); + if ( dialect.supportsFromClauseInUpdate() || !hasNonTrivialFromClause( statement.getFromClause() ) ) { visitWhereClause( statement.getRestriction() ); } + else { + visitWhereClause( determineWhereClauseRestrictionWithJoinEmulation( statement ) ); + } visitReturningColumns( statement.getReturningColumns() ); } + protected void renderUpdateClause(UpdateStatement updateStatement) { + appendSql( "update " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + renderDmlTargetTableExpression( updateStatement.getTargetTable() ); + } + finally { + clauseStack.pop(); + } + } + + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + appendSql( tableReference.getTableExpression() ); + registerAffectedTable( tableReference ); + } + + protected static boolean hasNonTrivialFromClause(FromClause fromClause) { + return fromClause != null && !fromClause.getRoots().isEmpty() + && ( fromClause.getRoots().size() > 1 || fromClause.getRoots().get( 0 ).hasRealJoins() ); + } + protected Predicate determineWhereClauseRestrictionWithJoinEmulation(AbstractUpdateOrDeleteStatement statement) { final QuerySpec querySpec = new QuerySpec( false ); querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( new QueryLiteral<>( 1, getIntegerType() ) ) ); - final List collectedNonInnerJoins; if ( supportsJoinInMutationStatementSubquery() ) { - collectedNonInnerJoins = new ArrayList<>(); - emulateWhereClauseRestrictionJoins( statement, querySpec, tableGroupJoin -> { - if ( tableGroupJoin.getJoinType() == SqlAstJoinType.INNER ) { - final TableGroup joinedGroup = tableGroupJoin.getJoinedGroup(); - final FromClause fromClause = querySpec.getFromClause(); - if ( fromClause.getRoots().isEmpty() ) { - final TableGroup copy = new StandardTableGroup( - joinedGroup.canUseInnerJoins(), - joinedGroup.getNavigablePath(), - (TableGroupProducer) joinedGroup.getModelPart(), - joinedGroup.getSourceAlias(), - joinedGroup.getPrimaryTableReference(), - null, - null - ); - fromClause.addRoot( copy ); + for ( TableGroup root : statement.getFromClause().getRoots() ) { + if ( root.getPrimaryTableReference() == statement.getTargetTable() ) { + final TableGroup dmlTargetTableGroup = new StandardTableGroup( + true, + new NavigablePath( "dual" ), + null, + null, + new NamedTableReference( getDual(), "d_" ), + null, + sessionFactory + ); + querySpec.getFromClause().addRoot( dmlTargetTableGroup ); + dmlTargetTableGroup.getTableReferenceJoins().addAll( root.getTableReferenceJoins() ); + for ( TableGroupJoin tableGroupJoin : root.getTableGroupJoins() ) { + dmlTargetTableGroup.addTableGroupJoin( tableGroupJoin ); } - else { - fromClause.addRoot( joinedGroup ); + for ( TableGroupJoin tableGroupJoin : root.getNestedTableGroupJoins() ) { + dmlTargetTableGroup.addNestedTableGroupJoin( tableGroupJoin ); } - querySpec.applyPredicate( tableGroupJoin.getPredicate() ); } else { - collectedNonInnerJoins.add( tableGroupJoin ); + querySpec.getFromClause().addRoot( root ); } - } ); + } } else { - collectedNonInnerJoins = null; emulateWhereClauseRestrictionJoins( statement, querySpec, tableGroupJoin -> { if ( tableGroupJoin.getJoinType() == SqlAstJoinType.INNER ) { querySpec.getFromClause().addRoot( tableGroupJoin.getJoinedGroup() ); querySpec.applyPredicate( tableGroupJoin.getPredicate() ); } } ); - } - - if ( querySpec.getFromClause().getRoots().isEmpty() ) { - return statement.getRestriction(); - } - else if ( collectedNonInnerJoins != null ) { - collectedNonInnerJoins.forEach( querySpec.getFromClause().getRoots().get( 0 )::addTableGroupJoin ); + if ( querySpec.getFromClause().getRoots().isEmpty() ) { + return statement.getRestriction(); + } } querySpec.applyPredicate( statement.getRestriction() ); @@ -1173,12 +1200,12 @@ public abstract class AbstractSqlAstTranslator implemen } } - protected void renderSetClause(UpdateStatement statement, Stack clauseStack) { + protected void renderSetClause(List assignments) { appendSql( " set" ); char separator = ' '; try { clauseStack.push( Clause.SET ); - for ( Assignment assignment : statement.getAssignments() ) { + for ( Assignment assignment : assignments ) { appendSql( separator ); separator = COMMA_SEPARATOR_CHAR; visitSetAssignment( assignment ); @@ -1232,8 +1259,9 @@ public abstract class AbstractSqlAstTranslator implemen } protected void visitInsertStatementOnly(InsertSelectStatement statement) { + clauseStack.push( Clause.INSERT ); appendSql( "insert into " ); - appendSql( statement.getTargetTable().getTableExpression() ); + renderDmlTargetTableExpression( statement.getTargetTable() ); appendSql( OPEN_PARENTHESIS ); boolean firstPass = true; @@ -1256,16 +1284,292 @@ public abstract class AbstractSqlAstTranslator implemen } appendSql( ") " ); + clauseStack.pop(); + visitInsertSource( statement ); + visitConflictClause( statement.getConflictClause() ); + visitReturningColumns( statement.getReturningColumns() ); + } + + protected void visitInsertSource(InsertSelectStatement statement) { if ( statement.getSourceSelectStatement() != null ) { statement.getSourceSelectStatement().accept( this ); } else { visitValuesList( statement.getValuesList() ); } + } + + protected void visitInsertStatementEmulateMerge(InsertSelectStatement statement) { + assert statement.getConflictClause() != null; + + final ConflictClause conflictClause = statement.getConflictClause(); + final String constraintName = conflictClause.getConstraintName(); + if ( constraintName != null ) { + throw new IllegalQueryOperationException( "Dialect does not support constraint name in conflict clause" ); + } + + appendSql( "merge into " ); + clauseStack.push( Clause.MERGE ); + renderNamedTableReference( statement.getTargetTable(), LockMode.NONE ); + clauseStack.pop(); + appendSql(" using " ); + + final List targetColumnReferences = statement.getTargetColumns(); + final List columnNames = new ArrayList<>( targetColumnReferences.size() ); + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + columnNames.add( targetColumnReference.getColumnExpression() ); + } + + final DerivedTableReference derivedTableReference; + if ( statement.getSourceSelectStatement() != null ) { + derivedTableReference = new QueryPartTableReference( + new SelectStatement( statement.getSourceSelectStatement() ), + "excluded", + columnNames, + false, + sessionFactory + ); + } + else { + derivedTableReference = new ValuesTableReference( + statement.getValuesList(), + "excluded", + columnNames, + sessionFactory + ); + } + clauseStack.push( Clause.FROM ); + derivedTableReference.accept( this ); + appendSql( " on (" ); + + String separator = ""; + for ( String constraintColumnName : conflictClause.getConstraintColumnNames() ) { + appendSql( separator ); + appendSql( statement.getTargetTable().getIdentificationVariable() ); + appendSql( '.' ); + appendSql( constraintColumnName ); + appendSql( "=excluded." ); + appendSql( constraintColumnName ); + separator = " and "; + } + appendSql( ')' ); + + final List assignments = conflictClause.getAssignments(); + if ( !assignments.isEmpty() ) { + appendSql( " when matched" ); + renderMergeUpdateClause( assignments, conflictClause.getPredicate() ); + } + + appendSql( " when not matched then insert " ); + char separatorChar = OPEN_PARENTHESIS; + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + appendSql( separatorChar ); + appendSql( targetColumnReference.getColumnExpression() ); + separatorChar = COMMA_SEPARATOR_CHAR; + } + clauseStack.pop(); + + clauseStack.push( Clause.VALUES ); + appendSql( ") values " ); + separatorChar = OPEN_PARENTHESIS; + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + appendSql( separatorChar ); + appendSql( "excluded." ); + appendSql( targetColumnReference.getColumnExpression() ); + separatorChar = COMMA_SEPARATOR_CHAR; + } + clauseStack.pop(); + + appendSql( ')' ); + visitReturningColumns( statement.getReturningColumns() ); } + protected void visitUpdateStatementEmulateMerge(UpdateStatement statement) { + appendSql( "merge into " ); + clauseStack.push( Clause.MERGE ); + appendSql( statement.getTargetTable().getTableExpression() ); + registerAffectedTable( statement.getTargetTable() ); + appendSql( " as t" ); + clauseStack.pop(); + + final QueryPartTableReference inlineView = updateSourceAsSubquery( statement ); + appendSql( " using " ); + clauseStack.push( Clause.FROM ); + visitQueryPartTableReference( inlineView ); + clauseStack.pop(); + appendSql( " on " ); + final String rowIdExpression = dialect.rowId( null ); + if ( rowIdExpression == null ) { + final TableGroup dmlTargetTableGroup = statement.getFromClause().getRoots().get( 0 ); + assert dmlTargetTableGroup.getPrimaryTableReference() == statement.getTargetTable(); + createRowMatchingPredicate( dmlTargetTableGroup, "t", "s" ).accept( this ); + } + else { + appendSql( "t." ); + appendSql( rowIdExpression ); + appendSql( "=s.c" ); + appendSql( inlineView.getColumnNames().size() - 1 ); + } + appendSql( " when matched then update set" ); + char separator = ' '; + int column = 0; + for ( Assignment assignment : statement.getAssignments() ) { + final List columnReferences = assignment.getAssignable().getColumnReferences(); + for ( int j = 0; j < columnReferences.size(); j++ ) { + appendSql( separator ); + columnReferences.get( j ).appendColumnForWrite( this, "t" ); + appendSql( "=s.c" ); + appendSql( column++ ); + separator = ','; + } + } + + visitReturningColumns( statement.getReturningColumns() ); + } + + private QueryPartTableReference updateSourceAsSubquery(UpdateStatement statement) { + final QuerySpec inlineView = new QuerySpec( true ); + final SelectClause selectClause = inlineView.getSelectClause(); + final List assignments = statement.getAssignments(); + final List columnNames = new ArrayList<>( assignments.size() ); + for ( Assignment assignment : assignments ) { + final List columnReferences = assignment.getAssignable().getColumnReferences(); + final Expression assignedValue = assignment.getAssignedValue(); + if ( columnReferences.size() == 1 ) { + selectClause.addSqlSelection( new SqlSelectionImpl( assignedValue ) ); + columnNames.add( "c" + columnNames.size() ); + } + else if ( assignedValue instanceof SqlTuple ) { + final List expressions = ( (SqlTuple) assignedValue ).getExpressions(); + for ( int i = 0; i < columnReferences.size(); i++ ) { + selectClause.addSqlSelection( new SqlSelectionImpl( expressions.get( i ) ) ); + columnNames.add( "c" + columnNames.size() ); + } + } + else { + throw new IllegalQueryOperationException( "Unsupported tuple assignment in update query with joins." ); + } + } + final String rowIdExpression = dialect.rowId( null ); + if ( rowIdExpression == null ) { + final TableGroup dmlTargetTableGroup = statement.getFromClause().getRoots().get( 0 ); + assert dmlTargetTableGroup.getPrimaryTableReference() == statement.getTargetTable(); + final EntityIdentifierMapping identifierMapping = dmlTargetTableGroup.getModelPart() + .asEntityMappingType() + .getIdentifierMapping(); + identifierMapping.forEachSelectable( + 0, + (selectionIndex, selectableMapping) -> { + selectClause.addSqlSelection( new SqlSelectionImpl( + new ColumnReference( statement.getTargetTable(), selectableMapping ) + ) ); + columnNames.add( selectableMapping.getSelectionExpression() ); + } + ); + } + else { + selectClause.addSqlSelection( new SqlSelectionImpl( + new ColumnReference( + statement.getTargetTable(), + rowIdExpression, + sessionFactory.getTypeConfiguration().getBasicTypeRegistry() + .resolve( Object.class, dialect.rowIdSqlType() ) + ) + ) ); + columnNames.add( "c" + columnNames.size() ); + } + for ( TableGroup root : statement.getFromClause().getRoots() ) { + inlineView.getFromClause().addRoot( root ); + } + inlineView.applyPredicate( statement.getRestriction() ); + + return new QueryPartTableReference( + new SelectStatement( inlineView ), + "s", + columnNames, + false, + getSessionFactory() + ); + } + + protected void visitUpdateStatementEmulateInlineView(UpdateStatement statement) { + appendSql( "update " ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.UPDATE ); + final QueryPartTableReference inlineView = updateSourceAsInlineView( statement ); + visitQueryPartTableReference( inlineView ); + appendSql( " set" ); + char separator = ' '; + for ( int i = 0; i < inlineView.getColumnNames().size(); i += 2 ) { + appendSql( separator ); + appendSql( "t.c" ); + appendSql( i ); + appendSql( "=t.c" ); + appendSql( i + 1 ); + separator = ','; + } + } + finally { + clauseStack.pop(); + } + visitReturningColumns( statement.getReturningColumns() ); + } + + private QueryPartTableReference updateSourceAsInlineView(UpdateStatement statement) { + final QuerySpec inlineView = new QuerySpec( true ); + final SelectClause selectClause = inlineView.getSelectClause(); + final List assignments = statement.getAssignments(); + final List columnNames = new ArrayList<>( assignments.size() ); + for ( Assignment assignment : assignments ) { + final List columnReferences = assignment.getAssignable().getColumnReferences(); + final Expression assignedValue = assignment.getAssignedValue(); + if ( columnReferences.size() == 1 ) { + selectClause.addSqlSelection( new SqlSelectionImpl( columnReferences.get( 0 ) ) ); + selectClause.addSqlSelection( new SqlSelectionImpl( assignedValue ) ); + columnNames.add( "c" + columnNames.size() ); + columnNames.add( "c" + columnNames.size() ); + } + else if ( assignedValue instanceof SqlTuple ) { + final List expressions = ( (SqlTuple) assignedValue ).getExpressions(); + for ( int i = 0; i < columnReferences.size(); i++ ) { + selectClause.addSqlSelection( new SqlSelectionImpl( columnReferences.get( i ) ) ); + selectClause.addSqlSelection( new SqlSelectionImpl( expressions.get( i ) ) ); + columnNames.add( "c" + columnNames.size() ); + columnNames.add( "c" + columnNames.size() ); + } + } + else { + throw new IllegalQueryOperationException( "Unsupported tuple assignment in update query with joins." ); + } + } + for ( TableGroup root : statement.getFromClause().getRoots() ) { + inlineView.getFromClause().addRoot( root ); + } + inlineView.applyPredicate( statement.getRestriction() ); + + return new QueryPartTableReference( + new SelectStatement( inlineView ), + "t", + columnNames, + false, + getSessionFactory() + ); + } + + protected void renderMergeUpdateClause(List assignments, Predicate wherePredicate) { + if ( wherePredicate != null ) { + appendSql( " and " ); + clauseStack.push( Clause.WHERE ); + wherePredicate.accept( this ); + clauseStack.pop(); + } + appendSql( " then update" ); + renderSetClause( assignments ); + } + private void renderImplicitTargetColumnSpec() { } @@ -1302,25 +1606,18 @@ public abstract class AbstractSqlAstTranslator implemen } protected void visitValuesListEmulateSelectUnion(List valuesList) { - if ( valuesList.size() < 2 ) { - visitValuesListStandard( valuesList ); + String separator = ""; + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.VALUES ); + for ( int i = 0; i < valuesList.size(); i++ ) { + appendSql( separator ); + renderExpressionsAsSubquery( valuesList.get( i ).getExpressions() ); + separator = " union all "; + } } - else { - // Oracle doesn't support a multi-values insert - // So we render a select union emulation instead - String separator = ""; - final Stack clauseStack = getClauseStack(); - try { - clauseStack.push( Clause.VALUES ); - for ( int i = 0; i < valuesList.size(); i++ ) { - appendSql( separator ); - renderExpressionsAsSubquery( valuesList.get( i ).getExpressions() ); - separator = " union all "; - } - } - finally { - clauseStack.pop(); - } + finally { + clauseStack.pop(); } } @@ -1494,7 +1791,10 @@ public abstract class AbstractSqlAstTranslator implemen } protected LockMode getEffectiveLockMode(String alias) { - return getEffectiveLockMode( alias, getQueryPartStack().getCurrent().isRoot() ); + final QueryPart currentQueryPart = getQueryPartStack().getCurrent(); + return currentQueryPart == null + ? LockMode.NONE + : getEffectiveLockMode( alias, currentQueryPart.isRoot() ); } protected LockMode getEffectiveLockMode(String alias, boolean isRoot) { @@ -1605,6 +1905,131 @@ public abstract class AbstractSqlAstTranslator implemen return strategy; } + protected void visitConflictClause(ConflictClause conflictClause) { + if ( conflictClause != null ) { + // By default, we only support do nothing with an optional constraint name + if ( !conflictClause.getConstraintColumnNames().isEmpty() ) { + throw new IllegalQueryOperationException( "Insert conflict clause with constraint column names is not supported" ); + } + if ( conflictClause.isDoUpdate() ) { + throw new IllegalQueryOperationException( "Insert conflict do update clause is not supported" ); + } + } + } + + protected void visitStandardConflictClause(ConflictClause conflictClause) { + if ( conflictClause == null ) { + return; + } + + clauseStack.push( Clause.CONFLICT ); + appendSql( " on conflict" ); + final String constraintName = conflictClause.getConstraintName(); + if ( constraintName != null ) { + appendSql( " on constraint " ); + appendSql( constraintName ); + } + else if ( !conflictClause.getConstraintColumnNames().isEmpty() ) { + char separator = '('; + for ( String columnName : conflictClause.getConstraintColumnNames() ) { + appendSql( separator ); + appendSql( columnName ); + separator = ','; + } + appendSql( ')' ); + } + final List assignments = conflictClause.getAssignments(); + if ( assignments.isEmpty() ) { + appendSql( " do nothing" ); + } + else { + appendSql( " do update" ); + renderSetClause( assignments ); + + final Predicate predicate = conflictClause.getPredicate(); + if ( predicate != null ) { + clauseStack.push( Clause.WHERE ); + appendSql( " where " ); + predicate.accept( this ); + clauseStack.pop(); + } + } + clauseStack.pop(); + } + + protected void visitOnDuplicateKeyConflictClause(ConflictClause conflictClause) { + if ( conflictClause == null ) { + return; + } + // The duplicate key clause does not support specifying the constraint name or constraint column names, + // but to allow compatibility, we have to require the user to specify either one in the SQM conflict clause. + // To allow meaningful usage, we simply ignore the constraint column names in this emulation. + // A possible problem with this is when the constraint column names contain the primary key columns, + // but the insert fails due to a unique constraint violation. This emulation will not cause a failure to be + // propagated, but instead will run the respective conflict action. todo: document this + final String constraintName = conflictClause.getConstraintName(); + if ( constraintName != null ) { + throw new IllegalQueryOperationException( "Dialect does not support constraint name in conflict clause" ); + } +// final List constraintColumnNames = conflictClause.getConstraintColumnNames(); +// if ( !constraintColumnNames.isEmpty() ) { +// throw new IllegalQueryOperationException( "Dialect does not support constraint column names in conflict clause" ); +// } + + final InsertSelectStatement statement = (InsertSelectStatement) statementStack.getCurrent(); + clauseStack.push( Clause.CONFLICT ); + appendSql( " on duplicate key update" ); + final List assignments = conflictClause.getAssignments(); + if ( assignments.isEmpty() ) { + // Emulate do nothing by setting the first column to itself + final ColumnReference columnReference = statement.getTargetColumns().get( 0 ); + try { + clauseStack.push( Clause.SET ); + appendSql( ' ' ); + appendSql( columnReference.getColumnExpression() ); + appendSql( '=' ); + visitColumnReference( columnReference ); + } + finally { + clauseStack.pop(); + } + } + else { + renderPredicatedSetAssignments( assignments, conflictClause.getPredicate() ); + } + clauseStack.pop(); + } + + private void renderPredicatedSetAssignments(List assignments, Predicate predicate) { + char separator = ' '; + try { + clauseStack.push( Clause.SET ); + for ( Assignment assignment : assignments ) { + appendSql( separator ); + separator = COMMA_SEPARATOR_CHAR; + if ( predicate == null ) { + visitSetAssignment( assignment ); + } + else { + assert assignment.getAssignable().getColumnReferences().size() == 1; + final Expression expression = new CaseSearchedExpression( + (MappingModelExpressible) assignment.getAssignedValue().getExpressionType(), + List.of( + new CaseSearchedExpression.WhenFragment( + predicate, assignment.getAssignedValue() + ) + ), + assignment.getAssignable().getColumnReferences().get( 0 ) + ); + visitSetAssignment( new Assignment( assignment.getAssignable(), expression ) ); + } + } + } + finally { + clauseStack.pop(); + } + } + protected void visitReturningColumns(Supplier> returningColumnsAccess) { final List returningColumns = returningColumnsAccess.get(); @@ -3514,9 +3939,9 @@ public abstract class AbstractSqlAstTranslator implemen appendSql( CLOSE_PARENTHESIS ); } else { - appendSql( "exists (select 1" ); - appendSql( getFromDual() ); - appendSql( " where (" ); + appendSql( "exists (select 1 from " ); + appendSql( getDual() ); + appendSql( " d_ where (" ); String separator = NO_SEPARATOR; for ( int i = 0; i < size; i++ ) { appendSql( separator ); @@ -5298,12 +5723,39 @@ public abstract class AbstractSqlAstTranslator implemen appendSql( getFromDualForSelectOnly() ); } else { + appendSql( " from " ); + renderFromClauseSpaces( fromClause ); + } + } + + protected void renderFromClauseSpaces(FromClause fromClause) { + try { + clauseStack.push( Clause.FROM ); + String separator = NO_SEPARATOR; + for ( TableGroup root : fromClause.getRoots() ) { + separator = renderFromClauseRoot( root, separator ); + } + } + finally { + clauseStack.pop(); + } + } + + protected void renderFromClauseAfterUpdateSet(UpdateStatement statement) { + // No-op. Subclasses have to override this + } + + protected void renderFromClauseExcludingDmlTargetReference(UpdateStatement statement) { + final FromClause fromClause = statement.getFromClause(); + if ( hasNonTrivialFromClause( fromClause ) ) { appendSql( " from " ); try { clauseStack.push( Clause.FROM ); - String separator = NO_SEPARATOR; - for ( TableGroup root : fromClause.getRoots() ) { - separator = renderFromClauseRoot( root, separator ); + final List roots = fromClause.getRoots(); + renderDmlTargetTableGroup( roots.get( 0 ) ); + for ( int i = 1; i < roots.size(); i++ ) { + TableGroup root = roots.get( i ); + renderFromClauseRoot( root, COMMA_SEPARATOR ); } } finally { @@ -5312,12 +5764,94 @@ public abstract class AbstractSqlAstTranslator implemen } } + protected void renderFromClauseJoiningDmlTargetReference(UpdateStatement statement) { + final FromClause fromClause = statement.getFromClause(); + if ( hasNonTrivialFromClause( fromClause ) ) { + visitFromClause( fromClause ); + final TableGroup dmlTargetTableGroup = statement.getFromClause().getRoots().get( 0 ); + assert dmlTargetTableGroup.getPrimaryTableReference() == statement.getTargetTable(); + addAdditionalWherePredicate( + // Render the match predicate like `table.ctid=alias.ctid` + createRowMatchingPredicate( + dmlTargetTableGroup, + statement.getTargetTable().getTableExpression(), + statement.getTargetTable().getIdentificationVariable() + ) + ); + } + } + + protected Predicate createRowMatchingPredicate(TableGroup dmlTargetTableGroup, String lhsAlias, String rhsAlias) { + final String rowIdExpression = dialect.rowId( null ); + if ( rowIdExpression == null ) { + final EntityIdentifierMapping identifierMapping = dmlTargetTableGroup.getModelPart() + .asEntityMappingType() + .getIdentifierMapping(); + final int jdbcTypeCount = identifierMapping.getJdbcTypeCount(); + final List targetExpressions = new ArrayList<>( jdbcTypeCount ); + final List sourceExpressions = new ArrayList<>( jdbcTypeCount ); + identifierMapping.forEachSelectable( + 0, + (selectionIndex, selectableMapping) -> { + targetExpressions.add( new ColumnReference( + lhsAlias, + selectableMapping.getSelectionExpression(), + selectableMapping.isFormula(), + selectableMapping.getCustomReadExpression(), + selectableMapping.getJdbcMapping() + ) ); + sourceExpressions.add( new ColumnReference( + rhsAlias, + selectableMapping.getSelectionExpression(), + selectableMapping.isFormula(), + selectableMapping.getCustomReadExpression(), + selectableMapping.getJdbcMapping() + ) ); + } + ); + return new ComparisonPredicate( + targetExpressions.size() == 1 + ? targetExpressions.get( 0 ) + : new SqlTuple( targetExpressions, identifierMapping ), + ComparisonOperator.EQUAL, + sourceExpressions.size() == 1 + ? sourceExpressions.get( 0 ) + : new SqlTuple( sourceExpressions, identifierMapping ) + ); + } + else { + return new SelfRenderingPredicate( + new SelfRenderingSqlFragmentExpression( + lhsAlias + "." + rowIdExpression + "=" + rhsAlias + "." + rowIdExpression + ) + ); + } + } + + protected void renderDmlTargetTableGroup(TableGroup tableGroup) { + assert getStatementStack().getCurrent() instanceof UpdateStatement + && ( (UpdateStatement) getStatementStack().getCurrent() ).getTargetTable() == tableGroup.getPrimaryTableReference(); + appendSql( getDual() ); + renderTableReferenceJoins( tableGroup ); + processNestedTableGroupJoins( tableGroup, null ); + processTableGroupJoins( tableGroup ); + ModelPartContainer modelPart = tableGroup.getModelPart(); + if ( modelPart instanceof AbstractEntityPersister ) { + String[] querySpaces = (String[]) ( (AbstractEntityPersister) modelPart ).getQuerySpaces(); + for ( int i = 0; i < querySpaces.length; i++ ) { + registerAffectedTable( querySpaces[i] ); + } + } + } + private String renderFromClauseRoot(TableGroup root, String separator) { if ( root.isVirtual() ) { for ( TableGroupJoin tableGroupJoin : root.getTableGroupJoins() ) { + addAdditionalWherePredicate( tableGroupJoin.getPredicate() ); separator = renderFromClauseRoot( tableGroupJoin.getJoinedGroup(), separator ); } for ( TableGroupJoin tableGroupJoin : root.getNestedTableGroupJoins() ) { + addAdditionalWherePredicate( tableGroupJoin.getPredicate() ); separator = renderFromClauseRoot( tableGroupJoin.getJoinedGroup(), separator ); } } @@ -5572,10 +6106,7 @@ public abstract class AbstractSqlAstTranslator implemen protected boolean renderNamedTableReference(NamedTableReference tableReference, LockMode lockMode) { appendSql( tableReference.getTableExpression() ); registerAffectedTable( tableReference ); - final Clause currentClause = clauseStack.getCurrent(); - if ( rendersTableReferenceAlias( currentClause ) ) { - renderTableReferenceIdentificationVariable( tableReference ); - } + renderTableReferenceIdentificationVariable( tableReference ); return false; } @@ -5674,7 +6205,7 @@ public abstract class AbstractSqlAstTranslator implemen } } - protected final void renderTableReferenceIdentificationVariable(TableReference tableReference) { + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { final String identificationVariable = tableReference.getIdentificationVariable(); if ( identificationVariable != null ) { append( WHITESPACE ); @@ -5682,17 +6213,6 @@ public abstract class AbstractSqlAstTranslator implemen } } - protected boolean rendersTableReferenceAlias(Clause clause) { - // todo (6.0) : For now we just skip the alias rendering in the delete and update clauses - // We need some dialect support if we want to support joins in delete and update statements - switch ( clause ) { - case DELETE: - case UPDATE: - return getDialect().getDmlTargetColumnQualifierSupport() == DmlTargetColumnQualifierSupport.TABLE_ALIAS; - } - return true; - } - protected void registerAffectedTable(NamedTableReference tableReference) { registerAffectedTable( tableReference.getTableExpression() ); } @@ -6204,65 +6724,52 @@ public abstract class AbstractSqlAstTranslator implemen @Override public void visitColumnReference(ColumnReference columnReference) { - final String dmlTargetTableAlias = getDmlTargetTableAlias(); - if ( dmlTargetTableAlias != null && dmlTargetTableAlias.equals( columnReference.getQualifier() ) ) { - final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); - final String qualifier; - if ( qualifierSupport == DmlTargetColumnQualifierSupport.TABLE_ALIAS ) { - qualifier = dmlTargetTableAlias; - } - // Qualify the column reference with the table expression also when in subqueries - else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !queryPartStack.isEmpty() ) { - qualifier = getCurrentDmlStatement().getTargetTable().getTableExpression(); + final String qualifier = determineColumnReferenceQualifier( columnReference ); + if ( columnReference.isColumnExpressionFormula() ) { + // For formulas, we have to replace the qualifier as the alias was already rendered into the formula + // This is fine for now as this is only temporary anyway until we render aliases for table references + final String replacement; + if ( qualifier != null ) { + replacement = "$1" + qualifier + ".$3"; } else { - qualifier = null; - } - if ( columnReference.isColumnExpressionFormula() ) { - // For formulas, we have to replace the qualifier as the alias was already rendered into the formula - // This is fine for now as this is only temporary anyway until we render aliases for table references - final String replacement; - if ( qualifier != null ) { - replacement = "$1" + qualifier + ".$3"; - } - else { - replacement = "$1$3"; - } - appendSql( - columnReference.getColumnExpression() - .replaceAll( "(\\b)(" + dmlTargetTableAlias + "\\.)(\\b)", replacement ) - ); - } - else { - columnReference.appendReadExpression( this, qualifier ); + replacement = "$1$3"; } + appendSql( + columnReference.getColumnExpression() + .replaceAll( "(\\b)(" + columnReference.getQualifier() + "\\.)(\\b)", replacement ) + ); } else { - columnReference.appendReadExpression( this ); + columnReference.appendReadExpression( this, qualifier ); } } @Override public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) { - final String dmlTargetTableAlias = getDmlTargetTableAlias(); - final ColumnReference columnReference = aggregateColumnWriteExpression.getColumnReference(); - if ( dmlTargetTableAlias != null && dmlTargetTableAlias.equals( columnReference.getQualifier() ) ) { - final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); - final String qualifier; - if ( qualifierSupport == DmlTargetColumnQualifierSupport.TABLE_ALIAS ) { - qualifier = dmlTargetTableAlias; - } - // Qualify the column reference with the table expression also when in subqueries - else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !queryPartStack.isEmpty() ) { - qualifier = getCurrentDmlStatement().getTargetTable().getTableExpression(); - } - else { - qualifier = null; - } - aggregateColumnWriteExpression.appendWriteExpression( this, this, qualifier ); + aggregateColumnWriteExpression.appendWriteExpression( + this, + this, + determineColumnReferenceQualifier( aggregateColumnWriteExpression.getColumnReference() ) + ); + } + + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + if ( qualifierSupport == DmlTargetColumnQualifierSupport.TABLE_ALIAS + || ( currentDmlStatement = getCurrentDmlStatement() ) == null + || ( dmlAlias = currentDmlStatement.getTargetTable().getIdentificationVariable() ) == null + || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !queryPartStack.isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); } else { - aggregateColumnWriteExpression.appendWriteExpression( this, this ); + return null; } } @@ -6453,11 +6960,18 @@ public abstract class AbstractSqlAstTranslator implemen @Override public void visitTuple(SqlTuple tuple) { - appendSql( OPEN_PARENTHESIS ); + // A tuple in a values clause of an insert-select statement must be unwrapped, + // since the assignment target is also unwrapped to the individual column references + final boolean wrap = clauseStack.getCurrent() != Clause.VALUES; + if ( wrap ) { + appendSql( OPEN_PARENTHESIS ); + } renderCommaSeparated( tuple.getExpressions() ); - appendSql( CLOSE_PARENTHESIS ); + if ( wrap ) { + appendSql( CLOSE_PARENTHESIS ); + } } protected final void renderCommaSeparated(Iterable expressions) { @@ -7510,8 +8024,8 @@ public abstract class AbstractSqlAstTranslator implemen } return; } - else if ( expression instanceof EntityValuedPathInterpretation ) { - final AbstractUpdateOrDeleteStatement statement = getCurrentOrParentUpdateOrDeleteStatement( !supportsJoinInMutationStatementSubquery() ); + else if ( !supportsJoinInMutationStatementSubquery() && expression instanceof EntityValuedPathInterpretation ) { + final AbstractUpdateOrDeleteStatement statement = getCurrentOrParentUpdateOrDeleteStatement( true ); if ( statement != null ) { final TableGroup tableGroup = ( (EntityValuedPathInterpretation) expression ).getTableGroup(); final TableGroupJoin tableGroupJoin = findTableGroupJoin( @@ -7911,9 +8425,20 @@ public abstract class AbstractSqlAstTranslator implemen * there are no tables in the from clause. * * @return the SQL equivalent to Oracle's {@code from dual}. + * @deprecated Use {@link #getDual()} instead */ + @Deprecated(forRemoval = true) protected String getFromDual() { - return " from (values (0)) dual"; + return " from " + getDual() + " d_"; + } + + /** + * Returns a table expression that has one row. + * + * @return the SQL equivalent to Oracle's {@code dual}. + */ + protected String getDual() { + return "(values(0))"; } protected String getFromDualForSelectOnly() { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/SqlTuple.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/SqlTuple.java index f2b6683c16..7545b71fe7 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/SqlTuple.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/SqlTuple.java @@ -15,6 +15,7 @@ import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.sql.internal.DomainResultProducer; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.SqlTreeCreationLogger; +import org.hibernate.sql.ast.tree.update.Assignable; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.tuple.TupleResult; @@ -23,7 +24,7 @@ import org.hibernate.type.descriptor.java.JavaType; /** * @author Steve Ebersole */ -public class SqlTuple implements Expression, SqlTupleContainer, DomainResultProducer { +public class SqlTuple implements Expression, SqlTupleContainer, DomainResultProducer, Assignable { private final List expressions; private final MappingModelExpressible valueMapping; @@ -51,6 +52,11 @@ public class SqlTuple implements Expression, SqlTupleContainer, DomainResultProd return expressions; } + @Override + public List getColumnReferences() { + return (List) expressions; + } + @Override public void accept(SqlAstWalker sqlTreeWalker) { sqlTreeWalker.visitTuple( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java index ef75a5df17..80a05d4c50 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java @@ -209,13 +209,13 @@ public interface TableGroup extends SqlAstNode, ColumnReferenceQualifier, SqmPat default boolean hasRealJoins() { for ( TableGroupJoin join : getTableGroupJoins() ) { final TableGroup joinedGroup = join.getJoinedGroup(); - if ( !joinedGroup.isVirtual() || joinedGroup.hasRealJoins() ) { + if ( joinedGroup.isInitialized() && !joinedGroup.isVirtual() || joinedGroup.hasRealJoins() ) { return true; } } for ( TableGroupJoin join : getNestedTableGroupJoins() ) { final TableGroup joinedGroup = join.getJoinedGroup(); - if ( !joinedGroup.isVirtual() || joinedGroup.hasRealJoins() ) { + if ( joinedGroup.isInitialized() && !joinedGroup.isVirtual() || joinedGroup.hasRealJoins() ) { return true; } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/ConflictClause.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/ConflictClause.java new file mode 100644 index 0000000000..6400e30b50 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/ConflictClause.java @@ -0,0 +1,59 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.ast.tree.insert; + +import java.util.List; + +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.update.Assignment; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * @since 6.5 + */ +public class ConflictClause { + private final @Nullable String constraintName; + private final List constraintColumnNames; + private final List assignments; + private final @Nullable Predicate predicate; + + public ConflictClause( + @Nullable String constraintName, + List constraintColumnNames, + List assignments, + @Nullable Predicate predicate) { + this.constraintName = constraintName; + this.constraintColumnNames = constraintColumnNames; + this.assignments = assignments; + this.predicate = predicate; + } + + public @Nullable String getConstraintName() { + return constraintName; + } + + public List getConstraintColumnNames() { + return constraintColumnNames; + } + + public List getAssignments() { + return assignments; + } + + public boolean isDoNothing() { + return assignments.isEmpty(); + } + + public boolean isDoUpdate() { + return !assignments.isEmpty(); + } + + public @Nullable Predicate getPredicate() { + return predicate; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertSelectStatement.java index aae4601b0f..b08e5485df 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/InsertSelectStatement.java @@ -36,6 +36,7 @@ public class InsertSelectStatement extends AbstractMutationStatement implements private List targetColumnReferences; private QueryPart sourceSelectStatement; private List valuesList = new ArrayList<>(); + private ConflictClause conflictClause; public InsertSelectStatement(NamedTableReference targetTable) { this( null, targetTable, Collections.emptyList() ); @@ -100,6 +101,14 @@ public class InsertSelectStatement extends AbstractMutationStatement implements this.valuesList = valuesList; } + public ConflictClause getConflictClause() { + return conflictClause; + } + + public void setConflictClause(ConflictClause conflictClause) { + this.conflictClause = conflictClause; + } + @Override public void accept(SqlAstWalker walker) { walker.visitInsertStatement( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/Values.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/Values.java index 7dc691404c..e9abac3d73 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/Values.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/insert/Values.java @@ -14,10 +14,6 @@ import java.util.List; public class Values { private final List expressions; - public Values() { - this.expressions = new ArrayList<>(); - } - public Values(List expressions) { this.expressions = expressions; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/PredicateCollector.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/PredicateCollector.java index 9ff173ff0b..3e7a004ffb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/PredicateCollector.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/predicate/PredicateCollector.java @@ -6,7 +6,9 @@ */ package org.hibernate.sql.ast.tree.predicate; -public class PredicateCollector { +import java.util.function.Consumer; + +public class PredicateCollector implements Consumer { private Predicate predicate; public PredicateCollector() { @@ -20,6 +22,11 @@ public class PredicateCollector { this.predicate = Predicate.combinePredicates( this.predicate, incomingPredicate ); } + @Override + public void accept(Predicate predicate) { + applyPredicate( predicate ); + } + public Predicate getPredicate() { return predicate; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java index 9dee283b53..70dcbb3555 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcOperationQueryInsert.java @@ -6,7 +6,6 @@ */ package org.hibernate.sql.exec.internal; -import java.util.Collections; import java.util.List; import java.util.Set; @@ -20,10 +19,20 @@ import org.hibernate.sql.exec.spi.JdbcParameterBinder; * @author Steve Ebersole */ public class AbstractJdbcOperationQueryInsert extends AbstractJdbcOperationQuery implements JdbcOperationQueryInsert { + + private final String uniqueConstraintNameThatMayFail; + public AbstractJdbcOperationQueryInsert( String sql, List parameterBinders, - Set affectedTableNames) { + Set affectedTableNames, + String uniqueConstraintNameThatMayFail) { super( sql, parameterBinders, affectedTableNames ); + this.uniqueConstraintNameThatMayFail = uniqueConstraintNameThatMayFail; + } + + @Override + public String getUniqueConstraintNameThatMayFail() { + return uniqueConstraintNameThatMayFail; } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryInsertImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryInsertImpl.java index d42dfab2f8..d5d81bb6d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryInsertImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/JdbcOperationQueryInsertImpl.java @@ -20,10 +20,20 @@ import org.hibernate.sql.exec.spi.JdbcParameterBinder; public class JdbcOperationQueryInsertImpl extends AbstractJdbcOperationQueryInsert implements JdbcOperationQueryMutation { + + public JdbcOperationQueryInsertImpl( String sql, List parameterBinders, Set affectedTableNames) { - super( sql, parameterBinders, affectedTableNames ); + super( sql, parameterBinders, affectedTableNames, null ); + } + + public JdbcOperationQueryInsertImpl( + String sql, + List parameterBinders, + Set affectedTableNames, + String uniqueConstraintNameThatMayFail) { + super( sql, parameterBinders, affectedTableNames, uniqueConstraintNameThatMayFail ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StandardJdbcMutationExecutor.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StandardJdbcMutationExecutor.java index 062683fb8d..47ff5eb5ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StandardJdbcMutationExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/StandardJdbcMutationExecutor.java @@ -8,17 +8,21 @@ package org.hibernate.sql.exec.internal; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.SQLIntegrityConstraintViolationException; import java.util.function.BiConsumer; import java.util.function.Function; +import org.hibernate.JDBCException; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventManager; import org.hibernate.event.spi.HibernateMonitoringEvent; +import org.hibernate.exception.ConstraintViolationException; import org.hibernate.query.spi.QueryOptions; import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcMutationExecutor; +import org.hibernate.sql.exec.spi.JdbcOperationQueryInsert; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -98,10 +102,23 @@ public class StandardJdbcMutationExecutor implements JdbcMutationExecutor { } } catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( + final JDBCException exception = jdbcServices.getSqlExceptionHelper().convert( e, "JDBC exception executing SQL [" + finalSql + "]" ); + if ( exception instanceof ConstraintViolationException && jdbcMutation instanceof JdbcOperationQueryInsert ) { + final ConstraintViolationException constraintViolationException = (ConstraintViolationException) exception; + if ( constraintViolationException.getKind() == ConstraintViolationException.ConstraintKind.UNIQUE ) { + final String uniqueConstraintNameThatMayFail = ( (JdbcOperationQueryInsert) jdbcMutation ).getUniqueConstraintNameThatMayFail(); + if ( uniqueConstraintNameThatMayFail != null ) { + if ( uniqueConstraintNameThatMayFail.isEmpty() + || uniqueConstraintNameThatMayFail.equalsIgnoreCase( constraintViolationException.getConstraintName() ) ) { + return 0; + } + } + } + } + throw exception; } finally { executionContext.afterStatement( logicalConnection ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryInsert.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryInsert.java index 2179002438..dfcefec816 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryInsert.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/JdbcOperationQueryInsert.java @@ -12,4 +12,5 @@ package org.hibernate.sql.exec.spi; * @author Steve Ebersole */ public interface JdbcOperationQueryInsert extends JdbcOperationQueryMutation { + String getUniqueConstraintNameThatMayFail(); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/HANADialectTestCase.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/HANADialectTestCase.java index 16616929e5..5386dd4b9d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/HANADialectTestCase.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/HANADialectTestCase.java @@ -15,7 +15,7 @@ import org.hibernate.MappingException; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.cfg.AvailableSettings; -import org.hibernate.dialect.HANAColumnStoreDialect; +import org.hibernate.dialect.HANADialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.testing.TestForIssue; @@ -39,7 +39,7 @@ public class HANADialectTestCase extends BaseUnitTestCase { public void testSqlGeneratedForIdentityInsertNoColumns() { ServiceRegistryScope.using( () -> ServiceRegistryUtil.serviceRegistryBuilder() - .applySetting( AvailableSettings.DIALECT, HANAColumnStoreDialect.class ) + .applySetting( AvailableSettings.DIALECT, HANADialect.class ) .build(), registryScope -> { final StandardServiceRegistry registry = registryScope.getRegistry(); @@ -52,7 +52,7 @@ public class HANADialectTestCase extends BaseUnitTestCase { } } ).getMessage(); assertThat( errorMessage ) - .matches( "The INSERT statement for table \\[EntityWithIdentity\\] contains no column, and this is not supported by \\[" + HANAColumnStoreDialect.class.getName() + ", version: [\\d\\.]+\\]" ); + .matches( "The INSERT statement for table \\[EntityWithIdentity\\] contains no column, and this is not supported by \\[" + HANADialect.class.getName() + ", version: [\\d\\.]+\\]" ); } ); } @@ -83,7 +83,7 @@ public class HANADialectTestCase extends BaseUnitTestCase { @Test @TestForIssue(jiraKey = "HHH-13239") public void testLockWaitTimeout() { - HANAColumnStoreDialect dialect = new HANAColumnStoreDialect(); + HANADialect dialect = new HANADialect(); String sql = "select dummy from sys.dummy"; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/HANASearchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/HANASearchTest.java index 7df88d3f89..345b1a6df9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/HANASearchTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/HANASearchTest.java @@ -9,7 +9,7 @@ package org.hibernate.orm.test.dialect.functional; import java.sql.PreparedStatement; import org.hibernate.Transaction; -import org.hibernate.dialect.HANAColumnStoreDialect; +import org.hibernate.dialect.HANADialect; import org.hibernate.query.Query; import org.hibernate.testing.TestForIssue; @@ -38,8 +38,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; annotatedClasses = { HANASearchTest.SearchEntity.class } ) @SessionFactory(exportSchema = false) -@RequiresDialect(HANAColumnStoreDialect.class) -@SkipForDialect(dialectClass = HANAColumnStoreDialect.class, majorVersion = 4) +@RequiresDialect(HANADialect.class) +@SkipForDialect(dialectClass = HANADialect.class, majorVersion = 4) public class HANASearchTest { private static final String ENTITY_NAME = "SearchEntity"; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java index 5176390ee3..d4f6676df9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/locktimeout/HANALockTimeoutTest.java @@ -9,7 +9,7 @@ package org.hibernate.orm.test.dialect.unit.locktimeout; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.HANARowStoreDialect; +import org.hibernate.dialect.HANADialect; import org.hibernate.testing.junit4.BaseUnitTestCase; import org.junit.Test; @@ -25,7 +25,7 @@ import static org.junit.Assert.assertEquals; */ public class HANALockTimeoutTest extends BaseUnitTestCase { - private final Dialect dialect = new HANARowStoreDialect(); + private final Dialect dialect = new HANADialect(); @Test public void testLockTimeoutNoAliasNoTimeout() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/HANAOptimisticLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/HANAOptimisticLockingTest.java index a6a64c9b53..a91c04a96b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/HANAOptimisticLockingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/HANAOptimisticLockingTest.java @@ -6,8 +6,7 @@ */ package org.hibernate.orm.test.locking; -import org.hibernate.dialect.HANAColumnStoreDialect; -import org.hibernate.dialect.HANARowStoreDialect; +import org.hibernate.dialect.AbstractHANADialect; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -18,7 +17,6 @@ import jakarta.persistence.Version; import org.hibernate.testing.TestForIssue; 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; @@ -36,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; ) @SessionFactory @TestForIssue(jiraKey = "HHH-11656") -@RequiresDialects({ @RequiresDialect(HANAColumnStoreDialect.class), @RequiresDialect(HANARowStoreDialect.class) }) +@RequiresDialect(AbstractHANADialect.class) public class HANAOptimisticLockingTest { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/AttributeConverterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/AttributeConverterTest.java index 97656d00d4..1ca03daab3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/AttributeConverterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/AttributeConverterTest.java @@ -23,7 +23,6 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; -import org.hibernate.dialect.HANAColumnStoreDialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.ConfigHelper; import org.hibernate.mapping.BasicValue; @@ -473,17 +472,7 @@ public class AttributeConverterTest extends BaseUnitTestCase { final JdbcMapping jdbcMapping = (JdbcMapping) type; assertThat( jdbcMapping.getJavaTypeDescriptor(), instanceOf( EnumJavaType.class ) ); - - final int expectedJdbcTypeCode; - if ( metadata.getDatabase().getDialect() instanceof HANAColumnStoreDialect - // Only for SAP HANA Cloud - && metadata.getDatabase().getDialect().getVersion().isSameOrAfter( 4 ) ) { - expectedJdbcTypeCode = Types.NVARCHAR; - } - else { - expectedJdbcTypeCode = Types.VARCHAR; - } - assertThat( jdbcMapping.getJdbcType(), is( jdbcTypeRegistry.getDescriptor( expectedJdbcTypeCode ) ) ); + assertThat( jdbcMapping.getJdbcType(), is( jdbcTypeRegistry.getDescriptor( Types.VARCHAR ) ) ); // then lets build the SF and verify its use... final SessionFactory sf = metadata.buildSessionFactory(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java index aab7138578..69937a770b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java @@ -12,7 +12,9 @@ import java.util.List; import java.util.Map; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.AbstractTransactSQLDialect; import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.SybaseDialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.query.NativeQuery; import org.hibernate.query.Query; @@ -70,17 +72,26 @@ public class QueryTimeOutTest extends BaseNonConfigCoreFunctionalTestCase { ); final String baseQuery; if ( DialectContext.getDialect() instanceof OracleDialect ) { - baseQuery = "update AnEntity ae1_0 set ae1_0.name="; + baseQuery = "update AnEntity ae1_0 set ae1_0.name=?"; + } + else if ( DialectContext.getDialect() instanceof SybaseDialect ) { + baseQuery = "update AnEntity set name=? from AnEntity ae1_0"; + } + else if ( DialectContext.getDialect() instanceof AbstractTransactSQLDialect ) { + baseQuery = "update ae1_0 set name=? from AnEntity ae1_0"; } else { - baseQuery = "update AnEntity set name="; + baseQuery = "update AnEntity ae1_0 set name=?"; } - expectedSqlQuery = baseQuery + jdbcType.getJdbcLiteralFormatter( StringJavaType.INSTANCE ) - .toJdbcLiteral( - "abc", - sessionFactory().getJdbcServices().getDialect(), - sessionFactory().getWrapperOptions() - ); + expectedSqlQuery = baseQuery.replace( + "?", + jdbcType.getJdbcLiteralFormatter( StringJavaType.INSTANCE ) + .toJdbcLiteral( + "abc", + sessionFactory().getJdbcServices().getDialect(), + sessionFactory().getWrapperOptions() + ) + ); } @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java new file mode 100644 index 0000000000..d4a08fc326 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java @@ -0,0 +1,264 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.hql; + +import java.time.LocalDate; + +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaConflictClause; +import org.hibernate.query.criteria.JpaCriteriaInsertValues; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.contacts.Contact; +import org.hibernate.testing.orm.domain.contacts.Contact.Name; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DialectFeatureCheck; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ServiceRegistry +@DomainModel(standardModels = { StandardDomainModel.GAMBIT, StandardDomainModel.CONTACTS }) +@SessionFactory +@JiraKey("HHH-17506") +public class InsertConflictTests { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( new Contact( + 1, + new Name( "A", "B" ), + Contact.Gender.FEMALE, + LocalDate.of( 2000, 1, 1 ) + ) ); + session.persist( new BasicEntity( 1, "data" ) ); + } + ); + } + + @AfterEach + public void cleanupData(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from Contact" ).executeUpdate(); + session.createMutationQuery( "delete from BasicEntity" ).executeUpdate(); + } ); + } + + @Test + public void testOnConflictDoNothing(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + int updated = session.createMutationQuery( + "insert into BasicEntity (id, data) " + + "values (1, 'John') " + + "on conflict do nothing" + ).executeUpdate(); + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { + // Since JDBC set the MySQL CLIENT_FOUND_ROWS flag, the updated count is 1 even if values didn't change + // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html + assertEquals( 1, updated ); + } + else { + assertEquals( 0, updated ); + } + final BasicEntity basicEntity = session.find( BasicEntity.class, 1 ); + assertEquals( "data", basicEntity.getData() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUpsertOrMerge.class) + public void testOnConflictDoUpdate(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + int updated = session.createMutationQuery( + "insert into BasicEntity (id, data) " + + "values (1, 'John') " + + "on conflict(id) do update " + + "set data = excluded.data" + ).executeUpdate(); + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { + // Strange MySQL returns 2 if the conflict action updates a row + // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html + assertEquals( 2, updated ); + } + else { + assertEquals( 1, updated ); + } + final BasicEntity basicEntity = session.find( BasicEntity.class, 1 ); + assertEquals( "John", basicEntity.getData() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUpsertOrMerge.class) + public void testOnConflictDoUpdateWithWhere(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + int updated = session.createMutationQuery( + "insert into BasicEntity (id, data) " + + "values (1, 'John') " + + "on conflict(id) do update " + + "set data = excluded.data " + + "where id > 1" + ).executeUpdate(); + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { + // Since JDBC set the MySQL CLIENT_FOUND_ROWS flag, the updated count is 1 even if values didn't change + // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html + assertEquals( 1, updated ); + } + else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof SybaseASEDialect ) { + // Sybase seems to report all matched rows as affected and ignores additional predicates + assertEquals( 1, updated ); + } + else { + assertEquals( 0, updated ); + } + final BasicEntity basicEntity = session.find( BasicEntity.class, 1 ); + assertEquals( "data", basicEntity.getData() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUpsertOrMerge.class) + public void testOnConflictDoUpdateWithWhereCriteria(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaInsertValues insert = cb.createCriteriaInsertValues( BasicEntity.class ); + insert.setInsertionTargetPaths( + insert.getTarget().get( "id" ), + insert.getTarget().get( "data" ) + ); + insert.values( cb.values( cb.value( 1 ), cb.value( "John" ) ) ); + final JpaConflictClause conflictClause = insert.onConflict(); + conflictClause.conflictOnConstraintPaths( insert.getTarget().get( "id" ) ) + .onConflictDoUpdate() + .set( "data", conflictClause.getExcludedRoot().get( "data" ) ) + .where( cb.gt( insert.getTarget().get( "id" ), 1 ) ); + int updated = session.createMutationQuery( insert ).executeUpdate(); + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { + // Since JDBC set the MySQL CLIENT_FOUND_ROWS flag, the updated count is 1 even if values didn't change + // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html + assertEquals( 1, updated ); + } + else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof SybaseASEDialect ) { + // Sybase seems to report all matched rows as affected and ignores additional predicates + assertEquals( 1, updated ); + } + else { + assertEquals( 0, updated ); + } + final BasicEntity basicEntity = session.find( BasicEntity.class, 1 ); + assertEquals( "data", basicEntity.getData() ); + } + ); + } + + @Test + public void testOnConflictDoNothingMultiTable(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + int updated = session.createMutationQuery( + "insert into Contact (id, name) " + + "values (1, ('John', 'Doe')) " + + "on conflict do nothing" + ).executeUpdate(); + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { + // Since JDBC set the MySQL CLIENT_FOUND_ROWS flag, the updated count is 1 even if values didn't change + // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html + assertEquals( 1, updated ); + } + else { + assertEquals( 0, updated ); + } + final Contact contact = session.find( Contact.class, 1 ); + assertEquals( "A", contact.getName().getFirst() ); + assertEquals( "B", contact.getName().getLast() ); + assertEquals( Contact.Gender.FEMALE, contact.getGender() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUpsertOrMerge.class) + @SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "MERGE into a table that has a self-referential FK does not work") + public void testOnConflictDoUpdateMultiTable(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + int updated = session.createMutationQuery( + "insert into Contact (id, name, gender) " + + "values (1, ('John', 'Doe'), MALE) " + + "on conflict(id) do update " + + "set name = excluded.name, gender = excluded.gender" + ).executeUpdate(); + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { + // Strange MySQL returns 2 if the conflict action updates a row + // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html + assertEquals( 2, updated ); + } + else { + assertEquals( 1, updated ); + } + final Contact contact = session.find( Contact.class, 1 ); + assertEquals( "John", contact.getName().getFirst() ); + assertEquals( "Doe", contact.getName().getLast() ); + assertEquals( Contact.Gender.MALE, contact.getGender() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUpsertOrMerge.class) + @SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "MERGE into a table that has a self-referential FK does not work") + public void testOnConflictDoUpdateWithWhereMultiTable(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + int updated = session.createMutationQuery( + "insert into Contact (id, name, gender) " + + "values (1, ('John', 'Doe'), FEMALE) " + + "on conflict(id) do update " + + "set name = excluded.name, gender = excluded.gender " + + "where id > 1" + ).executeUpdate(); + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { + // Since JDBC set the MySQL CLIENT_FOUND_ROWS flag, the updated count is 1 even if values didn't change + // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html + assertEquals( 1, updated ); + } + else if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof SybaseASEDialect ) { + // Sybase seems to report all matched rows as affected and ignores additional predicates + assertEquals( 1, updated ); + } + else { + assertEquals( 0, updated ); + } + final Contact contact = session.find( Contact.class, 1 ); + assertEquals( "A", contact.getName().getFirst() ); + assertEquals( "B", contact.getName().getLast() ); + assertEquals( Contact.Gender.FEMALE, contact.getGender() ); + } + ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/strategyselectors/DefaultDialectSelectorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/strategyselectors/DefaultDialectSelectorTest.java index 6d93e09f31..0a6927a62a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/strategyselectors/DefaultDialectSelectorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/strategyselectors/DefaultDialectSelectorTest.java @@ -37,6 +37,7 @@ public class DefaultDialectSelectorTest { testDialectNamingResolution( DerbyTenSevenDialect.class ); testDialectNamingResolution( H2Dialect.class ); + testDialectNamingResolution( HANADialect.class ); testDialectNamingResolution( HANAColumnStoreDialect.class ); testDialectNamingResolution( HANARowStoreDialect.class ); testDialectNamingResolution( HSQLDialect.class ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java index 80ddb51313..e059d8208e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/typeoverride/TypeOverrideTest.java @@ -12,6 +12,7 @@ import org.hibernate.boot.MetadataBuilder; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; @@ -22,6 +23,7 @@ import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.Assert.assertEquals; @@ -72,8 +74,8 @@ public class TypeOverrideTest extends BaseSessionFactoryFunctionalTest { ); } else if ( AbstractHANADialect.class.isInstance( dialect ) ) { - assertSame( - ( (AbstractHANADialect) dialect ).getBlobTypeDescriptor(), + Assertions.assertInstanceOf( + AbstractHANADialect.HANABlobType.class, jdbcTypeRegistry.getDescriptor( Types.BLOB ) ); } diff --git a/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/hana/HANASpatialDialect.java b/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/hana/HANASpatialDialect.java index 31830b3fec..358660c39d 100644 --- a/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/hana/HANASpatialDialect.java +++ b/hibernate-spatial/src/main/java/org/hibernate/spatial/dialect/hana/HANASpatialDialect.java @@ -9,5 +9,9 @@ package org.hibernate.spatial.dialect.hana; import org.hibernate.dialect.HANAColumnStoreDialect; import org.hibernate.spatial.SpatialDialect; +/** + * @deprecated Spatial dialects are no longer needed + */ +@Deprecated public class HANASpatialDialect extends HANAColumnStoreDialect implements SpatialDialect { } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java index 0e998cc19a..0cf49ba89f 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java @@ -21,12 +21,13 @@ import jakarta.persistence.SecondaryTable; import jakarta.persistence.Table; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; +import jakarta.persistence.UniqueConstraint; /** * @author Steve Ebersole */ @Entity -@Table( name = "contacts" ) +@Table( name = "contacts", uniqueConstraints = @UniqueConstraint( name = "contacts_pk", columnNames = "id" ) ) @SecondaryTable( name="contact_supp" ) public class Contact { private Integer id; diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index f97b1d0cda..48171606c6 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -678,4 +678,10 @@ abstract public class DialectFeatureChecks { return dialect.getPreferredSqlTypeCodeForArray() != SqlTypes.VARBINARY; } } + + public static class SupportsUpsertOrMerge implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return !( dialect instanceof DerbyDialect ); + } + } }