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:
Steve Ebersole 2014-04-08 13:02:03 -04:00 committed by Brett Meyer
parent ca8bcf58c5
commit 6cef805a32
9 changed files with 337 additions and 19 deletions

View File

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

View File

@ -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" )?

View File

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

View File

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

View File

@ -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();
}

View File

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

View File

@ -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.
*/

View File

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

View File

@ -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();
}
}