implement 'NULLS (FIRST | LAST)' in HQL

This commit is contained in:
Nathan Xu 2020-05-05 16:11:06 -04:00 committed by Steve Ebersole
parent bbac6ed571
commit 2250b7f84f
19 changed files with 243 additions and 33 deletions

View File

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

View File

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

View File

@ -578,6 +578,10 @@ public class DB2Dialect extends Dialect {
return limitHandler;
}
@Override
public boolean supportsNullPrecedence() {
return false;
}
/**
* Handle DB2 "support" for null precedence...
*

View File

@ -2578,6 +2578,9 @@ public abstract class Dialect implements ConversionContext {
return false;
}
public boolean supportsNullPrecedence() {
return true;
}
/**
* Renders an ordering fragment
*

View File

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

View File

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

View File

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

View File

@ -481,7 +481,8 @@ public abstract class BaseSqmToSqlAstConverter
return new SortSpecification(
(Expression) sortSpecification.getSortExpression().accept( this ),
sortSpecification.getCollation(),
sortSpecification.getSortOrder()
sortSpecification.getSortOrder(),
sortSpecification.getNullPrecedence()
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),

View File

@ -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<ExampleEntity> query = session.createQuery( "select e from ExampleEntity e order by e.name asc nulls first", ExampleEntity.class );
List<ExampleEntity> 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() );
}
}

View File

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

View File

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

View File

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

View File

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

View File

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