HHH-9100 - Improve CAST function support
(cherry picked from commit 6ffbf8f3f3
)
Conflicts:
hibernate-core/src/main/antlr/hql.g
hibernate-core/src/main/java/org/hibernate/dialect/function/CastFunction.java
hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java
hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java
This commit is contained in:
parent
ca8bcf58c5
commit
6cef805a32
|
@ -204,6 +204,8 @@ tokens
|
|||
|
||||
protected void processFunction(AST functionCall,boolean inSelect) throws SemanticException { }
|
||||
|
||||
protected void processCastFunction(AST functionCall,boolean inSelect) throws SemanticException { }
|
||||
|
||||
protected void processAggregation(AST node, boolean inSelect) throws SemanticException { }
|
||||
|
||||
protected void processConstructor(AST constructor) throws SemanticException { }
|
||||
|
@ -625,6 +627,10 @@ functionCall
|
|||
: #(METHOD_CALL {inFunctionCall=true;} pathAsIdent ( #(EXPR_LIST (exprOrSubquery)* ) )? ) {
|
||||
processFunction( #functionCall, inSelect );
|
||||
inFunctionCall=false;
|
||||
}
|
||||
| #(CAST {inFunctionCall=true;} expr pathAsIdent) {
|
||||
processCastFunction( #functionCall, inSelect );
|
||||
inFunctionCall=false;
|
||||
}
|
||||
| #(AGGREGATE aggregateExpr )
|
||||
;
|
||||
|
|
|
@ -84,7 +84,7 @@ tokens
|
|||
|
||||
// -- SQL tokens --
|
||||
// These aren't part of HQL, but the SQL fragment parser uses the HQL lexer, so they need to be declared here.
|
||||
CASE="case";
|
||||
CASE="case"; // a "searched case statement", whereas CASE2 represents a "simple case statement"
|
||||
END="end";
|
||||
ELSE="else";
|
||||
THEN="then";
|
||||
|
@ -92,7 +92,7 @@ tokens
|
|||
ON="on";
|
||||
WITH="with";
|
||||
|
||||
// -- EJBQL tokens --
|
||||
// -- JPAQL tokens --
|
||||
BOTH="both";
|
||||
EMPTY="empty";
|
||||
LEADING="leading";
|
||||
|
@ -108,7 +108,8 @@ tokens
|
|||
AGGREGATE; // One of the aggregate functions (e.g. min, max, avg)
|
||||
ALIAS;
|
||||
CONSTRUCTOR;
|
||||
CASE2;
|
||||
CASE2; // a "simple case statement", whereas CASE represents a "searched case statement"
|
||||
CAST;
|
||||
EXPR_LIST;
|
||||
FILTER_ENTITY; // FROM element injected because of a filter expression (happens during compilation phase 2)
|
||||
IN_LIST;
|
||||
|
@ -197,6 +198,20 @@ tokens
|
|||
public void weakKeywords() throws TokenStreamException { }
|
||||
|
||||
public void processMemberOf(Token n,AST p,ASTPair currentAST) { }
|
||||
|
||||
protected boolean validateSoftKeyword(String text) throws TokenStreamException {
|
||||
return validateLookAheadText(1, text);
|
||||
}
|
||||
|
||||
protected boolean validateLookAheadText(int lookAheadPosition, String text) throws TokenStreamException {
|
||||
String text2Validate = retrieveLookAheadText( lookAheadPosition );
|
||||
return text2Validate == null ? false : text2Validate.equalsIgnoreCase( text );
|
||||
}
|
||||
|
||||
protected String retrieveLookAheadText(int lookAheadPosition) throws TokenStreamException {
|
||||
Token token = LT(lookAheadPosition);
|
||||
return token == null ? null : token.getText();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -618,7 +633,8 @@ quantifiedExpression
|
|||
// ident qualifier ('.' ident ), array index ( [ expr ] ),
|
||||
// method call ( '.' ident '(' exprList ') )
|
||||
atom
|
||||
: primaryExpression
|
||||
: { validateSoftKeyword("cast") && LA(2) == OPEN }? castFunction
|
||||
| primaryExpression
|
||||
(
|
||||
DOT^ identifier
|
||||
( options { greedy=true; } :
|
||||
|
@ -627,6 +643,20 @@ atom
|
|||
)*
|
||||
;
|
||||
|
||||
castFunction!
|
||||
: c:IDENT OPEN e:expression (AS)? t:castTargetType CLOSE {
|
||||
#c.setType( CAST );
|
||||
#castFunction = #( #c, #e, #t );
|
||||
}
|
||||
;
|
||||
|
||||
castTargetType
|
||||
// the cast target type is Hibernate type name which is either:
|
||||
// 1) a simple identifier
|
||||
// 2) a simple identifier-(dot-identifier)* sequence
|
||||
: identifier { handleDotIdent(); } ( options { greedy=true; } : DOT^ identifier )*
|
||||
;
|
||||
|
||||
// level 0 - the basic element of an expression
|
||||
primaryExpression
|
||||
: identPrimary ( options {greedy=true;} : DOT^ "class" )?
|
||||
|
|
|
@ -465,12 +465,17 @@ methodCall
|
|||
: #(m:METHOD_CALL i:METHOD_NAME { beginFunctionTemplate(m,i); }
|
||||
( #(EXPR_LIST (arguments)? ) )?
|
||||
{ endFunctionTemplate(m); } )
|
||||
| #( c:CAST { beginFunctionTemplate(c,c); } expr castTargetType { endFunctionTemplate(c); } )
|
||||
;
|
||||
|
||||
arguments
|
||||
: expr ( { commaBetweenParameters(", "); } expr )*
|
||||
;
|
||||
|
||||
castTargetType
|
||||
: i:IDENT { out(i); }
|
||||
;
|
||||
|
||||
parameter
|
||||
: n:NAMED_PARAM { out(n); }
|
||||
| p:PARAM { out(p); }
|
||||
|
|
|
@ -35,6 +35,11 @@ import org.hibernate.type.Type;
|
|||
* @author Gavin King
|
||||
*/
|
||||
public class CastFunction implements SQLFunction {
|
||||
/**
|
||||
* Singleton access
|
||||
*/
|
||||
public static final CastFunction INSTANCE = new CastFunction();
|
||||
|
||||
public boolean hasArguments() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -36,12 +36,6 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import antlr.ASTFactory;
|
||||
import antlr.RecognitionException;
|
||||
import antlr.SemanticException;
|
||||
import antlr.collections.AST;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.QueryException;
|
||||
import org.hibernate.engine.internal.JoinSequence;
|
||||
|
@ -53,6 +47,7 @@ import org.hibernate.hql.internal.antlr.HqlTokenTypes;
|
|||
import org.hibernate.hql.internal.antlr.SqlTokenTypes;
|
||||
import org.hibernate.hql.internal.ast.tree.AggregateNode;
|
||||
import org.hibernate.hql.internal.ast.tree.AssignmentSpecification;
|
||||
import org.hibernate.hql.internal.ast.tree.CastFunctionNode;
|
||||
import org.hibernate.hql.internal.ast.tree.CollectionFunction;
|
||||
import org.hibernate.hql.internal.ast.tree.ConstructorNode;
|
||||
import org.hibernate.hql.internal.ast.tree.DeleteStatement;
|
||||
|
@ -104,6 +99,12 @@ import org.hibernate.type.DbTimestampType;
|
|||
import org.hibernate.type.Type;
|
||||
import org.hibernate.type.VersionType;
|
||||
import org.hibernate.usertype.UserVersionType;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import antlr.ASTFactory;
|
||||
import antlr.RecognitionException;
|
||||
import antlr.SemanticException;
|
||||
import antlr.collections.AST;
|
||||
|
||||
/**
|
||||
* Implements methods used by the HQL->SQL tree transform grammar (a.k.a. the second phase).
|
||||
|
@ -1009,8 +1010,14 @@ public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, Par
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void processAggregation(AST node, boolean inSelect) throws SemanticException {
|
||||
AggregateNode aggregateNode = ( AggregateNode ) node;
|
||||
protected void processCastFunction(AST castFunctionCall, boolean inSelect) throws SemanticException {
|
||||
CastFunctionNode castFunctionNode = (CastFunctionNode) castFunctionCall;
|
||||
castFunctionNode.resolve( inSelect );
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processAggregation(AST node, boolean inSelect) throws SemanticException {
|
||||
AggregateNode aggregateNode = (AggregateNode) node;
|
||||
aggregateNode.resolve();
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.hibernate.hql.internal.ast.tree.BetweenOperatorNode;
|
|||
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.SearchedCaseNode;
|
||||
import org.hibernate.hql.internal.ast.tree.SimpleCaseNode;
|
||||
import org.hibernate.hql.internal.ast.tree.CollectionFunction;
|
||||
|
@ -131,6 +132,8 @@ public class SqlASTFactory extends ASTFactory implements HqlSqlTokenTypes {
|
|||
return SqlFragment.class;
|
||||
case METHOD_CALL:
|
||||
return MethodNode.class;
|
||||
case CAST:
|
||||
return CastFunctionNode.class;
|
||||
case ELEMENTS:
|
||||
case INDICES:
|
||||
return CollectionFunction.class;
|
||||
|
|
|
@ -29,10 +29,6 @@ import java.util.Arrays;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import antlr.RecognitionException;
|
||||
import antlr.collections.AST;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import org.hibernate.NullPrecedence;
|
||||
import org.hibernate.QueryException;
|
||||
import org.hibernate.dialect.function.SQLFunction;
|
||||
|
@ -49,6 +45,10 @@ import org.hibernate.internal.CoreMessageLogger;
|
|||
import org.hibernate.internal.util.StringHelper;
|
||||
import org.hibernate.param.ParameterSpecification;
|
||||
import org.hibernate.type.Type;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import antlr.RecognitionException;
|
||||
import antlr.collections.AST;
|
||||
|
||||
/**
|
||||
* Generates SQL by overriding callback methods in the base class, which does
|
||||
|
@ -201,7 +201,12 @@ public class SqlGenerator extends SqlGeneratorBase implements ErrorReporter {
|
|||
else {
|
||||
// this function has a registered SQLFunction -> redirect output and catch the arguments
|
||||
outputStack.addFirst( writer );
|
||||
writer = new FunctionArguments();
|
||||
if ( node.getType() == CAST ) {
|
||||
writer = new CastFunctionArguments();
|
||||
}
|
||||
else {
|
||||
writer = new StandardFunctionArguments();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,7 +220,7 @@ public class SqlGenerator extends SqlGeneratorBase implements ErrorReporter {
|
|||
else {
|
||||
final Type functionType = functionNode.getFirstArgumentType();
|
||||
// this function has a registered SQLFunction -> redirect output and catch the arguments
|
||||
FunctionArguments functionArguments = ( FunctionArguments ) writer;
|
||||
FunctionArgumentsCollectingWriter functionArguments = (FunctionArgumentsCollectingWriter) writer;
|
||||
writer = outputStack.removeFirst();
|
||||
out( sqlFunction.render( functionType, functionArguments.getArgs(), sessionFactory ) );
|
||||
}
|
||||
|
@ -239,11 +244,15 @@ public class SqlGenerator extends SqlGeneratorBase implements ErrorReporter {
|
|||
void commaBetweenParameters(String comma);
|
||||
}
|
||||
|
||||
interface FunctionArgumentsCollectingWriter extends SqlWriter {
|
||||
public List getArgs();
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL function processing code redirects generated SQL output to an instance of this class
|
||||
* which catches function arguments.
|
||||
*/
|
||||
class FunctionArguments implements SqlWriter {
|
||||
class StandardFunctionArguments implements FunctionArgumentsCollectingWriter {
|
||||
private int argInd;
|
||||
private final List<String> args = new ArrayList<String>(3);
|
||||
|
||||
|
@ -265,6 +274,28 @@ public class SqlGenerator extends SqlGeneratorBase implements ErrorReporter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL function processing code redirects generated SQL output to an instance of this class
|
||||
* which catches function arguments.
|
||||
*/
|
||||
class CastFunctionArguments implements FunctionArgumentsCollectingWriter {
|
||||
private final List<String> args = new ArrayList<String>( 3 );
|
||||
|
||||
@Override
|
||||
public void clause(String clause) {
|
||||
args.add( clause );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commaBetweenParameters(String comma) {
|
||||
// todo : should this be an exception? Its not likely to end well if this method is called here...
|
||||
}
|
||||
|
||||
public List getArgs() {
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default SQL writer.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* Copyright (c) 2014, Red Hat Inc. or third-party contributors as
|
||||
* indicated by the @author tags or express copyright attribution
|
||||
* statements applied by the authors. All third-party contributions are
|
||||
* distributed under license by Red Hat Inc.
|
||||
*
|
||||
* This copyrighted material is made available to anyone wishing to use, modify,
|
||||
* copy, or redistribute it subject to the terms and conditions of the GNU
|
||||
* Lesser General Public License, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this distribution; if not, write to:
|
||||
* Free Software Foundation, Inc.
|
||||
* 51 Franklin Street, Fifth Floor
|
||||
* Boston, MA 02110-1301 USA
|
||||
*/
|
||||
package org.hibernate.hql.internal.ast.tree;
|
||||
|
||||
import org.hibernate.QueryException;
|
||||
import org.hibernate.dialect.function.CastFunction;
|
||||
import org.hibernate.dialect.function.SQLFunction;
|
||||
import org.hibernate.hql.internal.ast.util.ColumnHelper;
|
||||
import org.hibernate.type.Type;
|
||||
|
||||
import antlr.SemanticException;
|
||||
|
||||
/**
|
||||
* Represents a cast function call. We handle this specially because its type
|
||||
* argument has a semantic meaning to the HQL query (its not just pass through).
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class CastFunctionNode extends AbstractSelectExpression implements FunctionNode {
|
||||
private SQLFunction dialectCastFunction;
|
||||
|
||||
private Node expressionNode;
|
||||
|
||||
private IdentNode typeNode;
|
||||
private Type castType;
|
||||
|
||||
|
||||
/**
|
||||
* Called from the hql-sql grammar after the children of the CAST have been resolved.
|
||||
*
|
||||
* @param inSelect Is this call part of the SELECT clause?
|
||||
*/
|
||||
public void resolve(boolean inSelect) {
|
||||
this.dialectCastFunction = getSessionFactoryHelper().findSQLFunction( "cast" );
|
||||
if ( dialectCastFunction == null ) {
|
||||
dialectCastFunction = CastFunction.INSTANCE;
|
||||
}
|
||||
|
||||
this.expressionNode = (Node) getFirstChild();
|
||||
if ( expressionNode == null ) {
|
||||
throw new QueryException( "Could not resolve expression to CAST" );
|
||||
}
|
||||
if ( SqlNode.class.isInstance( expressionNode ) ) {
|
||||
final Type expressionType = ( (SqlNode) expressionNode ).getDataType();
|
||||
if ( expressionType != null ) {
|
||||
if ( expressionType.isEntityType() ) {
|
||||
throw new QueryException( "Expression to CAST cannot be an entity : " + expressionNode.getText() );
|
||||
}
|
||||
if ( expressionType.isComponentType() ) {
|
||||
throw new QueryException( "Expression to CAST cannot be a composite : " + expressionNode.getText() );
|
||||
}
|
||||
if ( expressionType.isCollectionType() ) {
|
||||
throw new QueryException( "Expression to CAST cannot be a collection : " + expressionNode.getText() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.typeNode = (IdentNode) expressionNode.getNextSibling();
|
||||
if ( typeNode == null ) {
|
||||
throw new QueryException( "Could not resolve requested type for CAST" );
|
||||
}
|
||||
|
||||
final String typeName = typeNode.getText();
|
||||
this.castType = getSessionFactoryHelper().getFactory().getTypeResolver().heuristicType( typeName );
|
||||
if ( castType == null ) {
|
||||
throw new QueryException( "Could not resolve requested type for CAST : " + typeName );
|
||||
}
|
||||
if ( castType.isEntityType() ) {
|
||||
throw new QueryException( "CAST target type cannot be an entity : " + expressionNode.getText() );
|
||||
}
|
||||
if ( castType.isComponentType() ) {
|
||||
throw new QueryException( "CAST target type cannot be a composite : " + expressionNode.getText() );
|
||||
}
|
||||
if ( castType.isCollectionType() ) {
|
||||
throw new QueryException( "CAST target type cannot be a collection : " + expressionNode.getText() );
|
||||
}
|
||||
setDataType( castType );
|
||||
}
|
||||
|
||||
@Override
|
||||
public SQLFunction getSQLFunction() {
|
||||
return dialectCastFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getFirstArgumentType() {
|
||||
return castType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScalarColumnText(int i) throws SemanticException {
|
||||
ColumnHelper.generateSingleScalarColumn( this, i );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* Copyright (c) 2014, Red Hat Inc. or third-party contributors as
|
||||
* indicated by the @author tags or express copyright attribution
|
||||
* statements applied by the authors. All third-party contributions are
|
||||
* distributed under license by Red Hat Inc.
|
||||
*
|
||||
* This copyrighted material is made available to anyone wishing to use, modify,
|
||||
* copy, or redistribute it subject to the terms and conditions of the GNU
|
||||
* Lesser General Public License, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this distribution; if not, write to:
|
||||
* Free Software Foundation, Inc.
|
||||
* 51 Franklin Street, Fifth Floor
|
||||
* Boston, MA 02110-1301 USA
|
||||
*/
|
||||
package org.hibernate.test.hql;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
|
||||
import org.hibernate.Session;
|
||||
|
||||
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class CastFunctionTest extends BaseCoreFunctionalTestCase {
|
||||
@Entity( name="MyEntity" )
|
||||
public static class MyEntity {
|
||||
@Id
|
||||
private Integer id;
|
||||
private String name;
|
||||
private Number theLostNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getAnnotatedClasses() {
|
||||
return new Class[] { MyEntity.class };
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringCasting() {
|
||||
Session s = openSession();
|
||||
s.beginTransaction();
|
||||
|
||||
// using the short name
|
||||
s.createQuery( "select cast(e.theLostNumber as string) from MyEntity e" ).list();
|
||||
// using the java class name
|
||||
s.createQuery( "select cast(e.theLostNumber as java.lang.String) from MyEntity e" ).list();
|
||||
// using the fqn Hibernate Type name
|
||||
s.createQuery( "select cast(e.theLostNumber as org.hibernate.type.StringType) from MyEntity e" ).list();
|
||||
|
||||
s.getTransaction().commit();
|
||||
s.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegerCasting() {
|
||||
Session s = openSession();
|
||||
s.beginTransaction();
|
||||
|
||||
// using the short name
|
||||
s.createQuery( "select cast(e.theLostNumber as integer) from MyEntity e" ).list();
|
||||
// using the java class name (primitive)
|
||||
s.createQuery( "select cast(e.theLostNumber as int) from MyEntity e" ).list();
|
||||
// using the java class name
|
||||
s.createQuery( "select cast(e.theLostNumber as java.lang.Integer) from MyEntity e" ).list();
|
||||
// using the fqn Hibernate Type name
|
||||
s.createQuery( "select cast(e.theLostNumber as org.hibernate.type.IntegerType) from MyEntity e" ).list();
|
||||
|
||||
s.getTransaction().commit();
|
||||
s.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongCasting() {
|
||||
Session s = openSession();
|
||||
s.beginTransaction();
|
||||
|
||||
// using the short name (also the primitive name)
|
||||
s.createQuery( "select cast(e.theLostNumber as long) from MyEntity e" ).list();
|
||||
// using the java class name
|
||||
s.createQuery( "select cast(e.theLostNumber as java.lang.Long) from MyEntity e" ).list();
|
||||
// using the fqn Hibernate Type name
|
||||
s.createQuery( "select cast(e.theLostNumber as org.hibernate.type.LongType) from MyEntity e" ).list();
|
||||
|
||||
s.getTransaction().commit();
|
||||
s.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloatCasting() {
|
||||
Session s = openSession();
|
||||
s.beginTransaction();
|
||||
|
||||
// using the short name (also the primitive name)
|
||||
s.createQuery( "select cast(e.theLostNumber as float) from MyEntity e" ).list();
|
||||
// using the java class name
|
||||
s.createQuery( "select cast(e.theLostNumber as java.lang.Float) from MyEntity e" ).list();
|
||||
// using the fqn Hibernate Type name
|
||||
s.createQuery( "select cast(e.theLostNumber as org.hibernate.type.FloatType) from MyEntity e" ).list();
|
||||
|
||||
s.getTransaction().commit();
|
||||
s.close();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue