HHH-15106 - fk() HQL function
This commit is contained in:
parent
3679db0455
commit
cf1bf5f823
|
@ -244,6 +244,15 @@ tokens
|
|||
return dot;
|
||||
}
|
||||
|
||||
protected AST lookupFkRefSource(AST path) throws SemanticException {
|
||||
if ( path.getType() == DOT ) {
|
||||
return lookupProperty( path, true, isInSelect() );
|
||||
}
|
||||
else {
|
||||
return lookupNonQualifiedProperty( path );
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isNonQualifiedPropertyRef(AST ident) { return false; }
|
||||
|
||||
protected AST lookupNonQualifiedProperty(AST property) throws SemanticException { return property; }
|
||||
|
@ -713,12 +722,15 @@ identifier
|
|||
;
|
||||
|
||||
addrExpr! [ boolean root ]
|
||||
: #(d:DOT lhs:addrExprLhs rhs:propertyName ) {
|
||||
: #(d:DOT lhs:addrExprLhs rhs:propertyName ) {
|
||||
// This gives lookupProperty() a chance to transform the tree
|
||||
// to process collection properties (.elements, etc).
|
||||
#addrExpr = #(#d, #lhs, #rhs);
|
||||
#addrExpr = lookupProperty(#addrExpr,root,false);
|
||||
}
|
||||
| fk_ref:fkRef {
|
||||
#addrExpr = #fk_ref;
|
||||
}
|
||||
| #(i:INDEX_OP lhs2:addrExprLhs rhs2:expr [ null ]) {
|
||||
#addrExpr = #(#i, #lhs2, #rhs2);
|
||||
processIndex(#addrExpr);
|
||||
|
@ -743,6 +755,12 @@ addrExpr! [ boolean root ]
|
|||
}
|
||||
;
|
||||
|
||||
fkRef
|
||||
: #( r:FK_REF p:propertyRef ) {
|
||||
#p = lookupProperty( #p, false, isInSelect() );
|
||||
}
|
||||
;
|
||||
|
||||
addrExprLhs
|
||||
: addrExpr [ false ]
|
||||
;
|
||||
|
@ -764,8 +782,7 @@ propertyRef!
|
|||
#propertyRef = #(#d, #lhs, #rhs);
|
||||
#propertyRef = lookupProperty(#propertyRef,false,true);
|
||||
}
|
||||
|
|
||||
p:identifier {
|
||||
| p:identifier {
|
||||
// In many cases, things other than property-refs are recognized
|
||||
// by this propertyRef rule. Some of those I have seen:
|
||||
// 1) select-clause from-aliases
|
||||
|
|
|
@ -46,6 +46,7 @@ tokens
|
|||
EXISTS="exists";
|
||||
FALSE="false";
|
||||
FETCH="fetch";
|
||||
FK_REF;
|
||||
FROM="from";
|
||||
FULL="full";
|
||||
GROUP="group";
|
||||
|
@ -721,7 +722,8 @@ atom
|
|||
|
||||
// level 0 - the basic element of an expression
|
||||
primaryExpression
|
||||
: { validateSoftKeyword("function") && LA(2) == OPEN && LA(3) == QUOTED_STRING }? jpaFunctionSyntax
|
||||
: { validateSoftKeyword("fk") && LA(2) == OPEN }? fkRefPath
|
||||
| { validateSoftKeyword("function") && LA(2) == OPEN && LA(3) == QUOTED_STRING }? jpaFunctionSyntax
|
||||
| { validateSoftKeyword("cast") && LA(2) == OPEN }? castFunction
|
||||
| identPrimary ( options {greedy=true;} : DOT^ "class" )?
|
||||
| constant
|
||||
|
@ -729,6 +731,12 @@ primaryExpression
|
|||
| OPEN! (expressionOrVector | subQuery) CLOSE!
|
||||
;
|
||||
|
||||
fkRefPath!
|
||||
: "fk" OPEN p:identPrimary CLOSE {
|
||||
#fkRefPath = #( [FK_REF], #p );
|
||||
}
|
||||
;
|
||||
|
||||
jpaFunctionSyntax!
|
||||
: i:IDENT OPEN n:QUOTED_STRING (COMMA a:exprList)? CLOSE {
|
||||
final String functionName = unquote( #n.getText() );
|
||||
|
|
|
@ -500,6 +500,7 @@ parameter
|
|||
|
||||
addrExpr
|
||||
: #(r:DOT . .) { out(r); }
|
||||
| #(fk:FK_REF .) { out(fk); }
|
||||
| i:ALIAS_REF { out(i); }
|
||||
| j:INDEX_OP { out(j); }
|
||||
| v:RESULT_VARIABLE_REF { out(v); }
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.hibernate.hql.internal.ast.tree.BinaryArithmeticOperatorNode;
|
|||
import org.hibernate.hql.internal.ast.tree.BinaryLogicOperatorNode;
|
||||
import org.hibernate.hql.internal.ast.tree.BooleanLiteralNode;
|
||||
import org.hibernate.hql.internal.ast.tree.CastFunctionNode;
|
||||
import org.hibernate.hql.internal.ast.tree.FkRefNode;
|
||||
import org.hibernate.hql.internal.ast.tree.NullNode;
|
||||
import org.hibernate.hql.internal.ast.tree.SearchedCaseNode;
|
||||
import org.hibernate.hql.internal.ast.tree.SimpleCaseNode;
|
||||
|
@ -196,6 +197,9 @@ public class SqlASTFactory extends ASTFactory implements HqlSqlTokenTypes {
|
|||
case NULL : {
|
||||
return NullNode.class;
|
||||
}
|
||||
case FK_REF: {
|
||||
return FkRefNode.class;
|
||||
}
|
||||
default:
|
||||
return SqlNode.class;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ package org.hibernate.hql.internal.ast.tree;
|
|||
import org.hibernate.QueryException;
|
||||
import org.hibernate.engine.internal.JoinSequence;
|
||||
import org.hibernate.hql.internal.CollectionProperties;
|
||||
import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes;
|
||||
import org.hibernate.hql.internal.antlr.SqlTokenTypes;
|
||||
import org.hibernate.hql.internal.ast.util.ASTUtil;
|
||||
import org.hibernate.hql.internal.ast.util.ColumnHelper;
|
||||
|
@ -260,7 +261,7 @@ public class DotNode extends FromReferenceNode implements DisplayableNode, Selec
|
|||
}
|
||||
|
||||
private Type prepareLhs() throws SemanticException {
|
||||
FromReferenceNode lhs = getLhs();
|
||||
final FromReferenceNode lhs = getLhs();
|
||||
lhs.prepareForDot( propertyName );
|
||||
return getDataType();
|
||||
}
|
||||
|
@ -390,10 +391,11 @@ public class DotNode extends FromReferenceNode implements DisplayableNode, Selec
|
|||
final boolean joinIsNeeded;
|
||||
|
||||
if ( isDotNode( parent ) ) {
|
||||
// our parent is another dot node, meaning we are being further dereferenced.
|
||||
// thus we need to generate a join unless the association is non-nullable and
|
||||
// parent refers to the associated entity's PK (because 'our' table would know the FK).
|
||||
parentAsDotNode = (DotNode) parent;
|
||||
|
||||
// our parent is another dot node, meaning we are being further de-referenced.
|
||||
// depending on the exact de-reference we may need to generate a physical join.
|
||||
|
||||
property = parentAsDotNode.propertyName;
|
||||
joinIsNeeded = generateJoin && (
|
||||
entityType.isNullable() ||
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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.hql.internal.ast.tree;
|
||||
|
||||
import org.hibernate.QueryException;
|
||||
import org.hibernate.hql.internal.ast.InvalidPathException;
|
||||
import org.hibernate.type.BasicType;
|
||||
import org.hibernate.type.CompositeType;
|
||||
import org.hibernate.type.ManyToOneType;
|
||||
import org.hibernate.type.Type;
|
||||
|
||||
import antlr.SemanticException;
|
||||
import antlr.collections.AST;
|
||||
|
||||
/**
|
||||
* Represents a `fk()` pseudo-function
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class FkRefNode
|
||||
extends HqlSqlWalkerNode
|
||||
implements ResolvableNode, DisplayableNode, PathNode {
|
||||
private FromReferenceNode toOnePath;
|
||||
|
||||
private Type fkType;
|
||||
private String[] columns;
|
||||
|
||||
private FromReferenceNode resolveToOnePath() {
|
||||
if ( toOnePath == null ) {
|
||||
try {
|
||||
resolve( false, true );
|
||||
}
|
||||
catch (SemanticException e) {
|
||||
final String msg = "Unable to resolve to-one path `fk(" + toOnePath.getPath() + "`)";
|
||||
throw new QueryException( msg, new InvalidPathException( msg ) );
|
||||
}
|
||||
}
|
||||
|
||||
assert toOnePath != null;
|
||||
return toOnePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
final FromReferenceNode toOnePath = resolveToOnePath();
|
||||
return "fk(`" + toOnePath.getDisplayText() + "` )";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return toOnePath.getDisplayText() + ".{fk}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(
|
||||
boolean generateJoin,
|
||||
boolean implicitJoin) throws SemanticException {
|
||||
if ( toOnePath != null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AST firstChild = getFirstChild();
|
||||
assert firstChild instanceof FromReferenceNode;
|
||||
|
||||
toOnePath = (FromReferenceNode) firstChild;
|
||||
toOnePath.resolve( false, true, null, toOnePath.getFromElement() );
|
||||
|
||||
final Type sourcePathDataType = toOnePath.getDataType();
|
||||
if ( ! ( sourcePathDataType instanceof ManyToOneType ) ) {
|
||||
throw new InvalidPathException(
|
||||
"Argument to fk() function must be a to-one path, but found " + sourcePathDataType
|
||||
);
|
||||
}
|
||||
final ManyToOneType toOneType = (ManyToOneType) sourcePathDataType;
|
||||
final FromElement fromElement = toOnePath.getFromElement();
|
||||
|
||||
fkType = toOneType.getIdentifierOrUniqueKeyType( getSessionFactoryHelper().getFactory() );
|
||||
assert fkType instanceof BasicType
|
||||
|| fkType instanceof CompositeType;
|
||||
|
||||
columns = fromElement.toColumns(
|
||||
fromElement.getTableAlias(),
|
||||
toOneType.getPropertyName(),
|
||||
getWalker().isInSelect()
|
||||
);
|
||||
assert columns != null && columns.length > 0;
|
||||
|
||||
setText( String.join( ", ", columns ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(
|
||||
boolean generateJoin,
|
||||
boolean implicitJoin,
|
||||
String classAlias,
|
||||
AST parent,
|
||||
AST parentPredicate) throws SemanticException {
|
||||
resolve( false, true );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(
|
||||
boolean generateJoin,
|
||||
boolean implicitJoin,
|
||||
String classAlias,
|
||||
AST parent) throws SemanticException {
|
||||
resolve( false, true );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(
|
||||
boolean generateJoin,
|
||||
boolean implicitJoin,
|
||||
String classAlias) throws SemanticException {
|
||||
resolve( false, true );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveInFunctionCall(
|
||||
boolean generateJoin,
|
||||
boolean implicitJoin) throws SemanticException {
|
||||
resolve( false, true );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveIndex(AST parent) throws SemanticException {
|
||||
throw new InvalidPathException( "fk() paths cannot be de-referenced as indexed path" );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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.notfound;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import javax.persistence.ConstraintMode;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.ForeignKey;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.OneToOne;
|
||||
|
||||
import org.hibernate.QueryException;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
import org.hibernate.annotations.NotFoundAction;
|
||||
import org.hibernate.boot.SessionFactoryBuilder;
|
||||
|
||||
import org.hibernate.testing.TestForIssue;
|
||||
import org.hibernate.testing.jdbc.SQLStatementInterceptor;
|
||||
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.CoreMatchers.nullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* Tests for the new `{fk}` HQL token
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class FkRefTests extends BaseNonConfigCoreFunctionalTestCase {
|
||||
|
||||
private SQLStatementInterceptor statementInspector;
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getAnnotatedClasses() {
|
||||
return new Class[] {
|
||||
Coin.class, Currency.class
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureSessionFactoryBuilder(SessionFactoryBuilder sfb) {
|
||||
statementInspector = new SQLStatementInterceptor( sfb );
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@TestForIssue(jiraKey = "HHH-15106")
|
||||
public void testSimplePredicateUse() {
|
||||
statementInspector.clear();
|
||||
|
||||
// there is a Coin which has a currency_fk = 1
|
||||
inTransaction( (session) -> {
|
||||
final String hql = "select c from Coin c where fk(c.currency) = 1";
|
||||
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
|
||||
assertThat( coins.size(), is( 1 ) );
|
||||
assertThat( coins.get( 0 ), notNullValue() );
|
||||
assertThat( coins.get( 0 ).getCurrency(), nullValue() );
|
||||
|
||||
assertThat( statementInspector.getSqlQueries().size(), is( 2 ) );
|
||||
assertThat( statementInspector.getSqlQueries().get( 0 ), not( containsString( " join " ) ) );
|
||||
} );
|
||||
|
||||
statementInspector.clear();
|
||||
|
||||
// However, the "matching" Currency does not exist
|
||||
inTransaction( (session) -> {
|
||||
final String hql = "select c from Coin c where c.currency.id = 1";
|
||||
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
|
||||
assertThat( coins.size(), is( 0 ) );
|
||||
} );
|
||||
|
||||
statementInspector.clear();
|
||||
|
||||
// check using `currency` as a naked "property-ref"
|
||||
inTransaction( (session) -> {
|
||||
final String hql = "select c from Coin c where fk(currency) = 1";
|
||||
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
|
||||
assertThat( coins.size(), is( 1 ) );
|
||||
assertThat( coins.get( 0 ), notNullValue() );
|
||||
assertThat( coins.get( 0 ).getCurrency(), nullValue() );
|
||||
|
||||
assertThat( statementInspector.getSqlQueries().size(), is( 2 ) );
|
||||
assertThat( statementInspector.getSqlQueries().get( 0 ), not( containsString( " join " ) ) );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestForIssue(jiraKey = "HHH-15106")
|
||||
public void testNullnessPredicateUse() {
|
||||
statementInspector.clear();
|
||||
|
||||
// there is one Coin (id=3) which has a null currency_fk
|
||||
inTransaction( (session) -> {
|
||||
final String hql = "select c from Coin c where fk(c.currency) is null";
|
||||
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
|
||||
assertThat( coins.size(), is( 1 ) );
|
||||
assertThat( coins.get( 0 ), notNullValue() );
|
||||
assertThat( coins.get( 0 ).getId(), is( 3 ) );
|
||||
assertThat( coins.get( 0 ).getCurrency(), nullValue() );
|
||||
|
||||
assertThat( statementInspector.getSqlQueries().size(), is( 1 ) );
|
||||
assertThat( statementInspector.getSqlQueries().get( 0 ), not( containsString( " join " ) ));
|
||||
} );
|
||||
|
||||
statementInspector.clear();
|
||||
|
||||
// check using `currency` as a naked "property-ref"
|
||||
inTransaction( (session) -> {
|
||||
final String hql = "select c from Coin c where fk(currency) is null";
|
||||
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
|
||||
assertThat( coins.size(), is( 1 ) );
|
||||
assertThat( coins.get( 0 ), notNullValue() );
|
||||
assertThat( coins.get( 0 ).getId(), is( 3 ) );
|
||||
assertThat( coins.get( 0 ).getCurrency(), nullValue() );
|
||||
|
||||
assertThat( statementInspector.getSqlQueries().size(), is( 1 ) );
|
||||
assertThat( statementInspector.getSqlQueries().get( 0 ), not( containsString( " join " ) ));
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestForIssue(jiraKey = "HHH-15106")
|
||||
public void testFkRefDereferenceNotAllowed() {
|
||||
statementInspector.clear();
|
||||
|
||||
inTransaction( (session) -> {
|
||||
try {
|
||||
final String hql = "select c from Coin c where fk(c.currency).something";
|
||||
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
|
||||
fail( "Expecting failure" );
|
||||
}
|
||||
catch (IllegalArgumentException expected) {
|
||||
assertThat( expected.getCause(), instanceOf( QueryException.class ) );
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail( "Unexpected failure type : " + e );
|
||||
}
|
||||
} );
|
||||
|
||||
inTransaction( (session) -> {
|
||||
try {
|
||||
final String hql = "select c from Coin c where currency.{fk}.something";
|
||||
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
|
||||
}
|
||||
catch (IllegalArgumentException expected) {
|
||||
assertThat( expected.getCause(), instanceOf( QueryException.class ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@Before
|
||||
public void prepareTestData() {
|
||||
inTransaction( (session) -> {
|
||||
Currency euro = new Currency( 1, "Euro" );
|
||||
Coin fiveC = new Coin( 1, "Five cents", euro );
|
||||
session.persist( euro );
|
||||
session.persist( fiveC );
|
||||
|
||||
Currency usd = new Currency( 2, "USD" );
|
||||
Coin penny = new Coin( 2, "Penny", usd );
|
||||
session.persist( usd );
|
||||
session.persist( penny );
|
||||
|
||||
Coin noCurrency = new Coin( 3, "N/A", null );
|
||||
session.persist( noCurrency );
|
||||
} );
|
||||
|
||||
inTransaction( (session) -> {
|
||||
session.createQuery( "delete Currency where id = 1" ).executeUpdate();
|
||||
} );
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanupTest() throws Exception {
|
||||
inTransaction( (session) -> {
|
||||
session.createQuery( "delete Coin" ).executeUpdate();
|
||||
session.createQuery( "delete Currency" ).executeUpdate();
|
||||
} );
|
||||
}
|
||||
|
||||
@Entity(name = "Coin")
|
||||
public static class Coin {
|
||||
private Integer id;
|
||||
private String name;
|
||||
private Currency currency;
|
||||
|
||||
public Coin() {
|
||||
}
|
||||
|
||||
public Coin(Integer id, String name, Currency currency) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
@Id
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@NotFound(action = NotFoundAction.IGNORE)
|
||||
@JoinColumn(name = "currency_fk", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
|
||||
public Currency getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public void setCurrency(Currency currency) {
|
||||
this.currency = currency;
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(name = "Currency")
|
||||
public static class Currency implements Serializable {
|
||||
private Integer id;
|
||||
private String name;
|
||||
|
||||
public Currency() {
|
||||
}
|
||||
|
||||
public Currency(Integer id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Id
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue