diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index 63b0ec0b7e..5ce71985bc 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -180,6 +180,7 @@ EXISTS : [eE] [xX] [iI] [sS] [tT] [sS]; EXP : [eE] [xX] [pP]; EXTRACT : [eE] [xX] [tT] [rR] [aA] [cC] [tT]; FETCH : [fF] [eE] [tT] [cC] [hH]; +FIRST : [fF] [iI] [rR] [sS] [tT]; FLOOR : [fF] [lL] [oO] [oO] [rR]; FROM : [fF] [rR] [oO] [mM]; FOR : [fF] [oO] [rR]; @@ -200,6 +201,7 @@ INTO : [iI] [nN] [tT] [oO]; IS : [iI] [sS]; JOIN : [jJ] [oO] [iI] [nN]; KEY : [kK] [eE] [yY]; +LAST : [lL] [aA] [sS] [tT]; LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG]; LEAST : [lL] [eE] [aA] [sS] [tT]; LEFT : [lL] [eE] [fF] [tT]; @@ -231,6 +233,7 @@ NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD]; NEW : [nN] [eE] [wW]; NOT : [nN] [oO] [tT]; NULLIF : [nN] [uU] [lL] [lL] [iI] [fF]; +NULLS : [nN] [uU] [lL] [lL] [sS]; OBJECT : [oO] [bB] [jJ] [eE] [cC] [tT]; OF : [oO] [fF]; OFFSET : [oO] [fF] [fF] [sS] [eE] [tT]; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 1850b3dd58..d5f3c8916b 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -312,14 +312,12 @@ orderByFragment ; sortSpecification -// todo (6.0) : null precedence -// : sortExpression collationSpecification? orderingSpecification? nullsPrecedence? - : sortExpression collationSpecification? orderingSpecification? + : sortExpression collationSpecification? orderingSpecification? nullsPrecedence? ; -//nullsPrecedence -// : NULLS (FIRST | LAST) -// ; +nullsPrecedence + : NULLS (FIRST | LAST) + ; sortExpression : identifier diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index b894602519..cdafac81f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -578,6 +578,10 @@ public class DB2Dialect extends Dialect { return limitHandler; } + @Override + public boolean supportsNullPrecedence() { + return false; + } /** * Handle DB2 "support" for null precedence... * diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index ab7b39eb1a..8d3ad2ba32 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -2578,6 +2578,9 @@ public abstract class Dialect implements ConversionContext { return false; } + public boolean supportsNullPrecedence() { + return true; + } /** * Renders an ordering fragment * diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index a09f3278be..b62c086cff 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -603,6 +603,11 @@ public class MySQLDialect extends Dialect { return true; } + @Override + public boolean supportsNullPrecedence() { + return false; + } + @Override public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) { final StringBuilder orderByElement = new StringBuilder(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index ac5f2957b5..5584f14598 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -391,6 +391,11 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { return sql; } + @Override + public boolean supportsNullPrecedence() { + return getVersion() < 10; + } + @Override public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) { if ( getVersion() < 10 ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 3f29f92141..e824039f3d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -771,8 +771,13 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implements SqmCre sortOrder = null; } - // todo (6.0) : NullPrecedence - final NullPrecedence nullPrecedence = null; + final NullPrecedence nullPrecedence; + if ( ctx.nullsPrecedence() != null ) { + nullPrecedence = ctx.nullsPrecedence().FIRST() != null ? NullPrecedence.FIRST : NullPrecedence.LAST; + } + else { + nullPrecedence = null; + } return new SqmSortSpecification( sortExpression, collation, sortOrder, nullPrecedence ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 31b46998d3..ded5b0309d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -481,7 +481,8 @@ public abstract class BaseSqmToSqlAstConverter return new SortSpecification( (Expression) sortSpecification.getSortExpression().accept( this ), sortSpecification.getCollation(), - sortSpecification.getSortOrder() + sortSpecification.getSortOrder(), + sortSpecification.getNullPrecedence() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSortSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSortSpecification.java index d44b9d6680..6361dc8958 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSortSpecification.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSortSpecification.java @@ -16,12 +16,10 @@ import org.hibernate.query.sqm.tree.expression.SqmExpression; * @author Steve Ebersole */ public class SqmSortSpecification implements JpaOrder { - private SqmExpression sortExpression; - private String collation; + private final SqmExpression sortExpression; + private final String collation; private SortOrder sortOrder; - private final NullPrecedence nullPrecedence; - - private NullPrecedence precedence; + private NullPrecedence nullPrecedence; public SqmSortSpecification( SqmExpression sortExpression, @@ -63,14 +61,14 @@ public class SqmSortSpecification implements JpaOrder { // JPA @Override - public JpaOrder nullPrecedence(NullPrecedence precedence) { - this.precedence = precedence; + public JpaOrder nullPrecedence(NullPrecedence nullPrecedence) { + this.nullPrecedence = nullPrecedence; return this; } @Override public NullPrecedence getNullPrecedence() { - return precedence; + return nullPrecedence; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java index 9d61d2ee9a..3a36f260f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstWalker.java @@ -9,9 +9,11 @@ package org.hibernate.sql.ast.spi; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.NullPrecedence; import org.hibernate.SortOrder; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.spi.JdbcServices; @@ -231,6 +233,22 @@ public abstract class AbstractSqlAstWalker @Override public void visitSortSpecification(SortSpecification sortSpecification) { + NullPrecedence nullPrecedence = sortSpecification.getNullPrecedence(); + final boolean hasNullPrecedence = nullPrecedence != null && nullPrecedence != NullPrecedence.NONE; + if ( hasNullPrecedence && ! dialect.supportsNullPrecedence() ) { + appendSql( "case when (" ); + sortSpecification.getSortExpression().accept( this ); + appendSql( ") is null then " ); + if ( nullPrecedence == NullPrecedence.FIRST ) { + appendSql( "0 else 1" ); + } + else { + appendSql( "1 else 0" ); + } + appendSql( " end" ); + appendSql( COMA_SEPARATOR ); + } + sortSpecification.getSortExpression().accept( this ); final String collation = sortSpecification.getCollation(); @@ -247,7 +265,10 @@ public abstract class AbstractSqlAstWalker appendSql( " desc" ); } - // TODO: null precedence handling + if ( hasNullPrecedence && dialect.supportsNullPrecedence() ) { + appendSql( " nulls " ); + appendSql( nullPrecedence.name().toLowerCase( Locale.ROOT ) ); + } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SortSpecification.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SortSpecification.java index 9579c3c433..606bf46d54 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SortSpecification.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/select/SortSpecification.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.ast.tree.select; +import org.hibernate.NullPrecedence; import org.hibernate.SortOrder; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.tree.SqlAstNode; @@ -18,11 +19,17 @@ public class SortSpecification implements SqlAstNode { private final Expression sortExpression; private final String collation; private final SortOrder sortOrder; + private final NullPrecedence nullPrecedence; public SortSpecification(Expression sortExpression, String collation, SortOrder sortOrder) { + this( sortExpression, collation, sortOrder, NullPrecedence.NONE ); + } + + public SortSpecification(Expression sortExpression, String collation, SortOrder sortOrder, NullPrecedence nullPrecedence) { this.sortExpression = sortExpression; this.collation = collation; this.sortOrder = sortOrder; + this.nullPrecedence = nullPrecedence; } public Expression getSortExpression() { @@ -37,6 +44,10 @@ public class SortSpecification implements SqlAstNode { return sortOrder; } + public NullPrecedence getNullPrecedence() { + return nullPrecedence; + } + @Override public void accept(SqlAstWalker sqlTreeWalker) { sqlTreeWalker.visitSortSpecification( this ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ParameterTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ParameterTests.java index c65db52d85..eecbffbf39 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ParameterTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ParameterTests.java @@ -126,7 +126,7 @@ public class ParameterTests extends BaseSqmUnitTest { @Test public void testEmbeddableUseInPredicates() { { - final SqmSelectStatement sqm = interpretSelect( "select p.id from Person p where p.name.first = :fname" ); + final SqmSelectStatement sqm = interpretSelect( "select p.id from Person p where p.name.firstName = :fname" ); assertThat( sqm.getSqmParameters().size(), equalTo( 1 ) ); final SqmParameter parameter = sqm.getSqmParameters().iterator().next(); // assertThat( parameter.getAnticipatedType(), instanceOf( BasicSqmPathSource.class ) ); @@ -145,7 +145,7 @@ public class ParameterTests extends BaseSqmUnitTest { public void testNullParamValues() { inTransaction( session -> { - session.createQuery( "from Person p where p.name.first = :p" ).setParameter( "p", null ).list(); + session.createQuery( "from Person p where p.name.firstName = :p" ).setParameter( "p", null ).list(); session.createQuery( "from Person p where p.name = :p" ).setParameter( "p", null ).list(); session.createQuery( "from Person p where p.pk = :p" ).setParameter( "p", null ).list(); } @@ -168,8 +168,8 @@ public class ParameterTests extends BaseSqmUnitTest { public static class Person { @Embeddable public static class Name { - public String first; - public String last; + public String firstName; + public String lastName; } @Id diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/SelectClauseTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/SelectClauseTests.java index f9731e1be4..3460e54048 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/SelectClauseTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/SelectClauseTests.java @@ -70,7 +70,7 @@ public class SelectClauseTests extends BaseSqmUnitTest { @Test public void testCompoundAttributeSelection() { - SqmSelectStatement statement = interpretSelect( "select p.nickName, p.name.first from Person p" ); + SqmSelectStatement statement = interpretSelect( "select p.nickName, p.name.firstName from Person p" ); assertEquals( 2, statement.getQuerySpec().getSelectClause().getSelections().size() ); assertThat( statement.getQuerySpec().getSelectClause().getSelections().get( 0 ).getSelectableNode(), diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/AbstractNullPrecedenceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/AbstractNullPrecedenceTest.java new file mode 100644 index 0000000000..69f116d16b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/AbstractNullPrecedenceTest.java @@ -0,0 +1,64 @@ +/* + * 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.query.hql.nullPrecedence; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.hibernate.query.Query; + +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * @author Nathan Xu + */ +abstract class AbstractNullPrecedenceTest { + + @BeforeEach + void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + ExampleEntity entity1 = new ExampleEntity( 1L ); + entity1.setName( "name1" ); + session.save( entity1 ); + + ExampleEntity entity2 = new ExampleEntity( 2L ); + session.save( entity2 ); + + ExampleEntity entity3 = new ExampleEntity( 3L ); + entity3.setName( "name3" ); + session.save( entity3 ); + + } ); + } + + @Test + void testNullPrecedenceInOrdering(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query query = session.createQuery( "select e from ExampleEntity e order by e.name asc nulls first", ExampleEntity.class ); + List exampleEntities = query.getResultList(); + assertThat( exampleEntities.stream().map( ExampleEntity::getName ).collect( Collectors.toList() ), + equalTo( Arrays.asList( null, "name1", "name3" ) ) ); + + query = session.createQuery( "select e from ExampleEntity e order by e.name asc nulls last", ExampleEntity.class ); + exampleEntities = query.getResultList(); + assertThat( exampleEntities.stream().map( ExampleEntity::getName ).collect( Collectors.toList() ), + equalTo( Arrays.asList( "name1", "name3", null ) ) ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createQuery( "delete from ExampleEntity" ).executeUpdate() ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/ExampleEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/ExampleEntity.java new file mode 100644 index 0000000000..76182ffa42 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/ExampleEntity.java @@ -0,0 +1,38 @@ +/* + * 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.query.hql.nullPrecedence; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author Nathan Xu + */ +@Entity( name = "ExampleEntity" ) +@Table( name = "ExampleEntity" ) +public class ExampleEntity { + @Id + private Long id; + + private String name; + + public ExampleEntity() { + } + + public ExampleEntity(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/SupportingNativelyDialectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/SupportingNativelyDialectTest.java new file mode 100644 index 0000000000..ecb512f5f8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/SupportingNativelyDialectTest.java @@ -0,0 +1,21 @@ +/* + * 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.query.hql.nullPrecedence; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; + +/** + * @author Nathan Xu + */ +@DomainModel( annotatedClasses = ExampleEntity.class ) +@SessionFactory +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportNullPrecedence.class ) +public class SupportingNativelyDialectTest extends AbstractNullPrecedenceTest { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/SupportingNotNativelyDialectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/SupportingNotNativelyDialectTest.java new file mode 100644 index 0000000000..b93cd7969e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/nullPrecedence/SupportingNotNativelyDialectTest.java @@ -0,0 +1,21 @@ +/* + * 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.query.hql.nullPrecedence; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; + +/** + * @author Nathan Xu + */ +@DomainModel( annotatedClasses = ExampleEntity.class ) +@SessionFactory +@RequiresDialectFeature( feature = DialectFeatureChecks.DoesNotSupportNullPrecedence.class ) +public class SupportingNotNativelyDialectTest extends AbstractNullPrecedenceTest { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/domain/Person.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/domain/Person.java index e28c82218c..7102b14195 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/domain/Person.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/domain/Person.java @@ -82,23 +82,23 @@ public class Person { @Embeddable public static class Name { - public String first; - public String last; + public String firstName; + public String lastName; - public String getFirst() { - return first; + public String getFirstName() { + return firstName; } - public void setFirst(String first) { - this.first = first; + public void setFirstName(String firstName) { + this.firstName = firstName; } - public String getLast() { - return last; + public String getLastName() { + return lastName; } - public void setLast(String last) { - this.last = last; + public void setLastName(String lastName) { + this.lastName = lastName; } } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 872e2cbaf2..ff61889e9b 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -196,4 +196,16 @@ abstract public class DialectFeatureChecks { return dialect.dropConstraints(); } } + + public static class SupportNullPrecedence implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect.supportsNullPrecedence(); + } + } + + public static class DoesNotSupportNullPrecedence implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return !dialect.supportsNullPrecedence(); + } + } }