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]; EXP : [eE] [xX] [pP];
EXTRACT : [eE] [xX] [tT] [rR] [aA] [cC] [tT]; EXTRACT : [eE] [xX] [tT] [rR] [aA] [cC] [tT];
FETCH : [fF] [eE] [tT] [cC] [hH]; FETCH : [fF] [eE] [tT] [cC] [hH];
FIRST : [fF] [iI] [rR] [sS] [tT];
FLOOR : [fF] [lL] [oO] [oO] [rR]; FLOOR : [fF] [lL] [oO] [oO] [rR];
FROM : [fF] [rR] [oO] [mM]; FROM : [fF] [rR] [oO] [mM];
FOR : [fF] [oO] [rR]; FOR : [fF] [oO] [rR];
@ -200,6 +201,7 @@ INTO : [iI] [nN] [tT] [oO];
IS : [iI] [sS]; IS : [iI] [sS];
JOIN : [jJ] [oO] [iI] [nN]; JOIN : [jJ] [oO] [iI] [nN];
KEY : [kK] [eE] [yY]; KEY : [kK] [eE] [yY];
LAST : [lL] [aA] [sS] [tT];
LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG]; LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG];
LEAST : [lL] [eE] [aA] [sS] [tT]; LEAST : [lL] [eE] [aA] [sS] [tT];
LEFT : [lL] [eE] [fF] [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]; NEW : [nN] [eE] [wW];
NOT : [nN] [oO] [tT]; NOT : [nN] [oO] [tT];
NULLIF : [nN] [uU] [lL] [lL] [iI] [fF]; NULLIF : [nN] [uU] [lL] [lL] [iI] [fF];
NULLS : [nN] [uU] [lL] [lL] [sS];
OBJECT : [oO] [bB] [jJ] [eE] [cC] [tT]; OBJECT : [oO] [bB] [jJ] [eE] [cC] [tT];
OF : [oO] [fF]; OF : [oO] [fF];
OFFSET : [oO] [fF] [fF] [sS] [eE] [tT]; OFFSET : [oO] [fF] [fF] [sS] [eE] [tT];

View File

@ -312,14 +312,12 @@ orderByFragment
; ;
sortSpecification sortSpecification
// todo (6.0) : null precedence : sortExpression collationSpecification? orderingSpecification? nullsPrecedence?
// : sortExpression collationSpecification? orderingSpecification? nullsPrecedence?
: sortExpression collationSpecification? orderingSpecification?
; ;
//nullsPrecedence nullsPrecedence
// : NULLS (FIRST | LAST) : NULLS (FIRST | LAST)
// ; ;
sortExpression sortExpression
: identifier : identifier

View File

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

View File

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

View File

@ -603,6 +603,11 @@ public class MySQLDialect extends Dialect {
return true; return true;
} }
@Override
public boolean supportsNullPrecedence() {
return false;
}
@Override @Override
public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) { public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) {
final StringBuilder orderByElement = new StringBuilder(); final StringBuilder orderByElement = new StringBuilder();

View File

@ -391,6 +391,11 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
return sql; return sql;
} }
@Override
public boolean supportsNullPrecedence() {
return getVersion() < 10;
}
@Override @Override
public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) { public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) {
if ( getVersion() < 10 ) { if ( getVersion() < 10 ) {

View File

@ -771,8 +771,13 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implements SqmCre
sortOrder = null; sortOrder = null;
} }
// todo (6.0) : NullPrecedence final NullPrecedence nullPrecedence;
final NullPrecedence nullPrecedence = null; if ( ctx.nullsPrecedence() != null ) {
nullPrecedence = ctx.nullsPrecedence().FIRST() != null ? NullPrecedence.FIRST : NullPrecedence.LAST;
}
else {
nullPrecedence = null;
}
return new SqmSortSpecification( sortExpression, collation, sortOrder, nullPrecedence ); return new SqmSortSpecification( sortExpression, collation, sortOrder, nullPrecedence );
} }

View File

@ -481,7 +481,8 @@ public abstract class BaseSqmToSqlAstConverter
return new SortSpecification( return new SortSpecification(
(Expression) sortSpecification.getSortExpression().accept( this ), (Expression) sortSpecification.getSortExpression().accept( this ),
sortSpecification.getCollation(), 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 * @author Steve Ebersole
*/ */
public class SqmSortSpecification implements JpaOrder { public class SqmSortSpecification implements JpaOrder {
private SqmExpression sortExpression; private final SqmExpression sortExpression;
private String collation; private final String collation;
private SortOrder sortOrder; private SortOrder sortOrder;
private final NullPrecedence nullPrecedence; private NullPrecedence nullPrecedence;
private NullPrecedence precedence;
public SqmSortSpecification( public SqmSortSpecification(
SqmExpression sortExpression, SqmExpression sortExpression,
@ -63,14 +61,14 @@ public class SqmSortSpecification implements JpaOrder {
// JPA // JPA
@Override @Override
public JpaOrder nullPrecedence(NullPrecedence precedence) { public JpaOrder nullPrecedence(NullPrecedence nullPrecedence) {
this.precedence = precedence; this.nullPrecedence = nullPrecedence;
return this; return this;
} }
@Override @Override
public NullPrecedence getNullPrecedence() { public NullPrecedence getNullPrecedence() {
return precedence; return nullPrecedence;
} }
@Override @Override

View File

@ -9,9 +9,11 @@ package org.hibernate.sql.ast.spi;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
import org.hibernate.NotYetImplementedFor6Exception; import org.hibernate.NotYetImplementedFor6Exception;
import org.hibernate.NullPrecedence;
import org.hibernate.SortOrder; import org.hibernate.SortOrder;
import org.hibernate.dialect.Dialect; import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.jdbc.spi.JdbcServices;
@ -231,6 +233,22 @@ public abstract class AbstractSqlAstWalker
@Override @Override
public void visitSortSpecification(SortSpecification sortSpecification) { 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 ); sortSpecification.getSortExpression().accept( this );
final String collation = sortSpecification.getCollation(); final String collation = sortSpecification.getCollation();
@ -247,7 +265,10 @@ public abstract class AbstractSqlAstWalker
appendSql( " desc" ); 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; package org.hibernate.sql.ast.tree.select;
import org.hibernate.NullPrecedence;
import org.hibernate.SortOrder; import org.hibernate.SortOrder;
import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.SqlAstNode;
@ -18,11 +19,17 @@ public class SortSpecification implements SqlAstNode {
private final Expression sortExpression; private final Expression sortExpression;
private final String collation; private final String collation;
private final SortOrder sortOrder; private final SortOrder sortOrder;
private final NullPrecedence nullPrecedence;
public SortSpecification(Expression sortExpression, String collation, SortOrder sortOrder) { 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.sortExpression = sortExpression;
this.collation = collation; this.collation = collation;
this.sortOrder = sortOrder; this.sortOrder = sortOrder;
this.nullPrecedence = nullPrecedence;
} }
public Expression getSortExpression() { public Expression getSortExpression() {
@ -37,6 +44,10 @@ public class SortSpecification implements SqlAstNode {
return sortOrder; return sortOrder;
} }
public NullPrecedence getNullPrecedence() {
return nullPrecedence;
}
@Override @Override
public void accept(SqlAstWalker sqlTreeWalker) { public void accept(SqlAstWalker sqlTreeWalker) {
sqlTreeWalker.visitSortSpecification( this ); sqlTreeWalker.visitSortSpecification( this );

View File

@ -126,7 +126,7 @@ public class ParameterTests extends BaseSqmUnitTest {
@Test @Test
public void testEmbeddableUseInPredicates() { 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 ) ); assertThat( sqm.getSqmParameters().size(), equalTo( 1 ) );
final SqmParameter<?> parameter = sqm.getSqmParameters().iterator().next(); final SqmParameter<?> parameter = sqm.getSqmParameters().iterator().next();
// assertThat( parameter.getAnticipatedType(), instanceOf( BasicSqmPathSource.class ) ); // assertThat( parameter.getAnticipatedType(), instanceOf( BasicSqmPathSource.class ) );
@ -145,7 +145,7 @@ public class ParameterTests extends BaseSqmUnitTest {
public void testNullParamValues() { public void testNullParamValues() {
inTransaction( inTransaction(
session -> { 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.name = :p" ).setParameter( "p", null ).list();
session.createQuery( "from Person p where p.pk = :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 { public static class Person {
@Embeddable @Embeddable
public static class Name { public static class Name {
public String first; public String firstName;
public String last; public String lastName;
} }
@Id @Id

View File

@ -70,7 +70,7 @@ public class SelectClauseTests extends BaseSqmUnitTest {
@Test @Test
public void testCompoundAttributeSelection() { 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() ); assertEquals( 2, statement.getQuerySpec().getSelectClause().getSelections().size() );
assertThat( assertThat(
statement.getQuerySpec().getSelectClause().getSelections().get( 0 ).getSelectableNode(), 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 @Embeddable
public static class Name { public static class Name {
public String first; public String firstName;
public String last; public String lastName;
public String getFirst() { public String getFirstName() {
return first; return firstName;
} }
public void setFirst(String first) { public void setFirstName(String firstName) {
this.first = first; this.firstName = firstName;
} }
public String getLast() { public String getLastName() {
return last; return lastName;
} }
public void setLast(String last) { public void setLastName(String lastName) {
this.last = last; this.lastName = lastName;
} }
} }
} }

View File

@ -196,4 +196,16 @@ abstract public class DialectFeatureChecks {
return dialect.dropConstraints(); 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();
}
}
} }