diff --git a/hibernate-core/src/main/antlr/hql-sql.g b/hibernate-core/src/main/antlr/hql-sql.g index 3a4c382056..bc0b7adec4 100644 --- a/hibernate-core/src/main/antlr/hql-sql.g +++ b/hibernate-core/src/main/antlr/hql-sql.g @@ -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 { } @@ -626,6 +628,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 ) ; diff --git a/hibernate-core/src/main/antlr/hql.g b/hibernate-core/src/main/antlr/hql.g index 9c6104399d..6522203ac9 100644 --- a/hibernate-core/src/main/antlr/hql.g +++ b/hibernate-core/src/main/antlr/hql.g @@ -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; @@ -666,6 +667,7 @@ quantifiedExpression // * function : differentiated from method call via explicit keyword atom : { validateSoftKeyword("function") && LA(2) == OPEN && LA(3) == QUOTED_STRING }? jpaFunctionSyntax + | { validateSoftKeyword("cast") && LA(2) == OPEN }? castFunction | primaryExpression ( DOT^ identifier @@ -677,12 +679,38 @@ atom jpaFunctionSyntax! : i:IDENT OPEN n:QUOTED_STRING COMMA a:exprList CLOSE { - #i.setType( METHOD_CALL ); - #i.setText( #i.getText() + " (" + #n.getText() + ")" ); - #jpaFunctionSyntax = #( #i, [IDENT, unquote( #n.getText() )], #a ); + final String functionName = unquote( #n.getText() ); + + if ( functionName.equalsIgnoreCase( "cast" ) ) { + #i.setType( CAST ); + #i.setText( #i.getText() + " (" + functionName + ")" ); + final AST expression = #a.getFirstChild(); + final AST type = expression.getNextSibling(); + #jpaFunctionSyntax = #( #i, expression, type ); + } + else { + #i.setType( METHOD_CALL ); + #i.setText( #i.getText() + " (" + functionName + ")" ); + #jpaFunctionSyntax = #( #i, [IDENT, unquote( #n.getText() )], #a ); + } } ; +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" )? diff --git a/hibernate-core/src/main/antlr/sql-gen.g b/hibernate-core/src/main/antlr/sql-gen.g index 29279301e7..99726b8f11 100644 --- a/hibernate-core/src/main/antlr/sql-gen.g +++ b/hibernate-core/src/main/antlr/sql-gen.g @@ -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); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CastFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CastFunction.java index d145854bc7..7a0a1fbc9c 100755 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CastFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CastFunction.java @@ -36,6 +36,11 @@ import org.hibernate.type.Type; * @author Gavin King */ public class CastFunction implements SQLFunction { + /** + * Singleton access + */ + public static final CastFunction INSTANCE = new CastFunction(); + @Override public boolean hasArguments() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java index eb7bf0b9f6..9ce4794de6 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java @@ -47,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; @@ -1076,6 +1077,12 @@ public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, Par methodNode.resolve( inSelect ); } + @Override + 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; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlASTFactory.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlASTFactory.java index 76d6db2ef7..9e40176f44 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlASTFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlASTFactory.java @@ -32,6 +32,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; @@ -133,6 +134,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; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java index 183c73c720..26adf6da58 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java @@ -208,7 +208,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(); + } } } @@ -222,7 +227,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 ) ); } @@ -246,11 +251,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 args = new ArrayList( 3 ); @@ -274,6 +283,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 args = new ArrayList( 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. */ diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CastFunctionNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CastFunctionNode.java new file mode 100644 index 0000000000..766554de63 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CastFunctionNode.java @@ -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 ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/CastFunctionTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/CastFunctionTest.java new file mode 100644 index 0000000000..e72ede47e7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/CastFunctionTest.java @@ -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(); + } +}