From 34dc005af14b72e4927780cb76288f2499fbabeb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Nov 2024 16:07:14 +0100 Subject: [PATCH] Consider CONFLICT clause on INSERT in HQL. Closes: #3689 Original Pull Request: #3691 --- .../data/jpa/repository/query/Hql.g4 | 24 ++++++- .../repository/query/HqlQueryRenderer.java | 65 +++++++++++++++++++ .../query/HqlSpecificationTests.java | 33 +++++++--- 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 index 54a93e9eb..42c159876 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 @@ -147,7 +147,7 @@ deleteStatement // https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#hql-insert insertStatement - : INSERT INTO? targetEntity targetFields (queryExpression | valuesList) + : INSERT INTO? targetEntity targetFields (queryExpression | valuesList) conflictClause? ; // Already defined underneath updateStatement @@ -167,6 +167,23 @@ values : '(' expression (',' expression)* ')' ; +/** + * a 'conflict' clause in an 'insert' statement + */ +conflictClause + : ON CONFLICT conflictTarget? DO conflictAction + ; + +conflictTarget + : ON CONSTRAINT identifier + | '(' simplePath (',' simplePath)* ')' + ; + +conflictAction + : NOTHING + | UPDATE setClause whereClause? + ; + instantiation : NEW instantiationTarget '(' instantiationArguments ')' ; @@ -921,7 +938,11 @@ CURRENT_DATE : C U R R E N T '_' D A T E; CURRENT_INSTANT : C U R R E N T '_' I N S T A N T; CURRENT_TIME : C U R R E N T '_' T I M E; CURRENT_TIMESTAMP : C U R R E N T '_' T I M E S T A M P; +CONFLICT : C O N F L I C T; +CONSTRAINT : C O N S T R A I N T; +COLUMN : C O L U M N; CYCLE : C Y C L E; +DO : D O; DATE : D A T E; DATETIME : D A T E T I M E ; DAY : D A Y; @@ -1009,6 +1030,7 @@ NEW : N E W; NEXT : N E X T; NO : N O; NOT : N O T; +NOTHING : N O T H I N G; NULL : N U L L; NULLS : N U L L S; OBJECT : O B J E C T; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 9976347f1..127068f4a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -24,6 +24,7 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder; +import org.springframework.util.ObjectUtils; /** * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an HQL query without making any changes. @@ -545,6 +546,10 @@ class HqlQueryRenderer extends HqlBaseVisitor { builder.appendExpression(visit(ctx.valuesList())); } + if (ctx.conflictClause() != null) { + builder.appendExpression(visit(ctx.conflictClause())); + } + return builder; } @@ -583,6 +588,66 @@ class HqlQueryRenderer extends HqlBaseVisitor { return builder; } + @Override + public QueryTokenStream visitConflictClause(HqlParser.ConflictClauseContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(QueryTokens.expression(ctx.ON())); + builder.append(QueryTokens.expression(ctx.CONFLICT())); + + if (ctx.conflictTarget() != null) { + builder.appendExpression(visit(ctx.conflictTarget())); + } + + builder.append(QueryTokens.expression(ctx.DO())); + builder.appendExpression(visit(ctx.conflictAction())); + + return builder; + } + + @Override + public QueryTokenStream visitConflictTarget(HqlParser.ConflictTargetContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.identifier() != null) { + + builder.append(QueryTokens.expression(ctx.ON())); + builder.append(QueryTokens.expression(ctx.CONSTRAINT())); + builder.appendExpression(visit(ctx.identifier())); + } + + if (!ObjectUtils.isEmpty(ctx.simplePath())) { + + builder.append(TOKEN_OPEN_PAREN); + builder.append(QueryTokenStream.concat(ctx.simplePath(), this::visit, TOKEN_COMMA)); + + builder.append(TOKEN_CLOSE_PAREN); + } + + return builder; + } + + @Override + public QueryTokenStream visitConflictAction(HqlParser.ConflictActionContext ctx) { + + QueryRendererBuilder builder = QueryRenderer.builder(); + + if (ctx.NOTHING() != null) { + builder.append(QueryTokens.expression(ctx.NOTHING())); + } else { + builder.append(QueryTokens.expression(ctx.UPDATE())); + builder.appendExpression(visit(ctx.setClause())); + + if (ctx.whereClause() != null) { + builder.appendExpression(visit(ctx.whereClause())); + } + } + + return builder; + } + @Override public QueryTokenStream visitInstantiation(HqlParser.InstantiationContext ctx) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java index 62efc2fdc..3e459fa26 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java @@ -983,6 +983,29 @@ class HqlSpecificationTests { """); } + @Test // GH-3689 + void insertQueries() { + + assertQuery("insert Person (id, name) values (100L, 'Jane Doe')"); + + assertQuery("insert Person (id, name) values " + // + "(101L, 'J A Doe III'), " + // + "(102L, 'J X Doe'), " + // + "(103L, 'John Doe, Jr')"); + + assertQuery("insert into Partner (id, name) " + // + "select p.id, p.name from Person p "); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT (range) DO UPDATE SET price = :price, type = :priceType"); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT ON CONSTRAINT foo DO UPDATE SET price = :price, type = :priceType"); + + assertQuery("INSERT INTO AggregationPrice (range, price, type) " + "VALUES (:range, :price, :priceType) " + + "ON CONFLICT ON CONSTRAINT foo DO NOTHING"); + } + @Test void hqlQueries() { @@ -1000,15 +1023,7 @@ class HqlSpecificationTests { assertQuery("update versioned Person " + // "set name = :newName " + // "where name = :oldName"); - assertQuery("insert Person (id, name) " + // - "values (100L, 'Jane Doe')"); - assertQuery("insert Person (id, name) " + // - "values (101L, 'J A Doe III'), " + // - "(102L, 'J X Doe'), " + // - "(103L, 'John Doe, Jr')"); - assertQuery("insert into Partner (id, name) " + // - "select p.id, p.name " + // - "from Person p "); + assertQuery("select p " + // "from Person p " + // "where p.name like 'Joe'");