Consider CONFLICT clause on INSERT in HQL.

Closes: #3689
Original Pull Request: #3691
This commit is contained in:
Mark Paluch
2024-11-26 16:07:14 +01:00
committed by Christoph Strobl
parent 7afb8d4747
commit 34dc005af1
3 changed files with 112 additions and 10 deletions

View File

@@ -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;

View File

@@ -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<QueryTokenStream> {
builder.appendExpression(visit(ctx.valuesList()));
}
if (ctx.conflictClause() != null) {
builder.appendExpression(visit(ctx.conflictClause()));
}
return builder;
}
@@ -583,6 +588,66 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryTokenStream> {
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) {

View File

@@ -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'");