implement 'NULLS (FIRST | LAST)' in HQL
This commit is contained in:
parent
bbac6ed571
commit
2250b7f84f
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -578,6 +578,10 @@ public class DB2Dialect extends Dialect {
|
|||
return limitHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsNullPrecedence() {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Handle DB2 "support" for null precedence...
|
||||
*
|
||||
|
|
|
@ -2578,6 +2578,9 @@ public abstract class Dialect implements ConversionContext {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsNullPrecedence() {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Renders an ordering fragment
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -481,7 +481,8 @@ public abstract class BaseSqmToSqlAstConverter
|
|||
return new SortSpecification(
|
||||
(Expression) sortSpecification.getSortExpression().accept( this ),
|
||||
sortSpecification.getCollation(),
|
||||
sortSpecification.getSortOrder()
|
||||
sortSpecification.getSortOrder(),
|
||||
sortSpecification.getNullPrecedence()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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() );
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue