Skip to content

Commit

Permalink
HHH-465 - Support for NULLS FIRST/LAST
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasz-antoniak committed Dec 19, 2012
1 parent caf2ee4 commit add6aea
Show file tree
Hide file tree
Showing 27 changed files with 868 additions and 44 deletions.
Expand Up @@ -119,7 +119,7 @@ List cats = sess.createCriteria(Cat.class)

<programlisting role="JAVA"><![CDATA[List cats = sess.createCriteria(Cat.class)
.add( Restrictions.like("name", "F%")
.addOrder( Order.asc("name") )
.addOrder( Order.asc("name").nulls(NullPrecedence.LAST) )
.addOrder( Order.desc("age") )
.setMaxResults(50)
.list();]]></programlisting>
Expand Down
Expand Up @@ -125,6 +125,12 @@
<entry>Forces Hibernate to order SQL updates by the primary key value of the items being updated. This
reduces the likelihood of transaction deadlocks in highly-concurrent systems.</entry>
</row>
<row>
<entry>hibernate.order_by.default_null_ordering</entry>
<entry><para><literal>none</literal>, <literal>first</literal> or <literal>last</literal></para></entry>
<entry>Defines precedence of null values in <literal>ORDER BY</literal> clause. Defaults to
<literal>none</literal> which varies between RDBMS implementation.</entry>
</row>
<row>
<entry>hibernate.generate_statistics</entry>
<entry><para><literal>true</literal> or <literal>false</literal></para></entry>
Expand Down
Expand Up @@ -1427,7 +1427,9 @@
</para>
<para>
Individual expressions in the order-by can be qualified with either <literal>ASC</literal> (ascending) or
<literal>DESC</literal> (descending) to indicated the desired ordering direction.
<literal>DESC</literal> (descending) to indicated the desired ordering direction. Null values can be placed
in front or at the end of sorted set using <literal>NULLS FIRST</literal> or <literal>NULLS LAST</literal>
clause respectively.
</para>
<example>
<title>Order-by examples</title>
Expand Down
Expand Up @@ -483,8 +483,8 @@ public class Part {
<literal>@javax.persistence.OrderBy</literal> to your property. This
annotation takes as parameter a list of comma separated properties (of
the target entity) and orders the collection accordingly (eg
<code>firstname asc, age desc</code>), if the string is empty, the
collection will be ordered by the primary key of the target
<code>firstname asc, age desc, weight asc nulls last</code>), if the string
is empty, the collection will be ordered by the primary key of the target
entity.</para>

<example>
Expand Down
Expand Up @@ -855,12 +855,17 @@ WHERE prod.name = 'widget'
</para>

<programlisting><![CDATA[from DomesticCat cat
order by cat.name asc, cat.weight desc, cat.birthdate]]></programlisting>
order by cat.name asc, cat.weight desc nulls first, cat.birthdate]]></programlisting>

<para>
The optional <literal>asc</literal> or <literal>desc</literal> indicate ascending or descending order
respectively.
</para>

<para>
The optional <literal>nulls first</literal> or <literal>nulls last</literal> indicate precedence of null
values while sorting.
</para>
</section>

<section xml:id="queryhql-grouping" revision="1">
Expand Down
11 changes: 10 additions & 1 deletion hibernate-core/src/main/antlr/hql-sql.g
Expand Up @@ -348,9 +348,18 @@ orderClause
;
orderExprs
: orderExpr ( ASCENDING | DESCENDING )? (orderExprs)?
: orderExpr ( ASCENDING | DESCENDING )? ( nullOrdering )? (orderExprs)?
;
nullOrdering
: NULLS nullPrecedence
;
nullPrecedence
: FIRST
| LAST
;
orderExpr
: { isOrderExpressionResultVariableRef( _t ) }? resultVariableRef
| expr
Expand Down
23 changes: 22 additions & 1 deletion hibernate-core/src/main/antlr/hql.g
Expand Up @@ -78,6 +78,9 @@ tokens
UPDATE="update";
VERSIONED="versioned";
WHERE="where";
NULLS="nulls";
FIRST;
LAST;

// -- SQL tokens --
// These aren't part of HQL, but the SQL fragment parser uses the HQL lexer, so they need to be declared here.
Expand Down Expand Up @@ -439,14 +442,32 @@ orderByClause
;
orderElement
: expression ( ascendingOrDescending )?
: expression ( ascendingOrDescending )? ( nullOrdering )?
;
ascendingOrDescending
: ( "asc" | "ascending" ) { #ascendingOrDescending.setType(ASCENDING); }
| ( "desc" | "descending") { #ascendingOrDescending.setType(DESCENDING); }
;
nullOrdering
: NULLS nullPrecedence
;
nullPrecedence
: IDENT {
if ( "first".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
#nullPrecedence.setType( FIRST );
}
else if ( "last".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
#nullPrecedence.setType( LAST );
}
else {
throw new SemanticException( "Expecting 'first' or 'last', but found '" + #nullPrecedence.getText() + "' as null ordering precedence." );
}
}
;
//## havingClause:
//## HAVING logicalExpression;
Expand Down
39 changes: 22 additions & 17 deletions hibernate-core/src/main/antlr/order-by-render.g
Expand Up @@ -30,6 +30,7 @@ package org.hibernate.sql.ordering.antlr;
* Antlr grammar for rendering <tt>ORDER_BY</tt> trees as described by the {@link OrderByFragmentParser}

* @author Steve Ebersole
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
class GeneratedOrderByFragmentRenderer extends TreeParser;

Expand All @@ -53,6 +54,13 @@ options {
/*package*/ String getRenderedFragment() {
return buffer.toString();
}

/**
* Implementation note: This is just a stub. OrderByFragmentRenderer contains the effective implementation.
*/
protected String renderOrderByElement(String expression, String collation, String order, String nulls) {
throw new UnsupportedOperationException("Concrete ORDER BY renderer should override this method.");
}
}

orderByFragment
Expand All @@ -61,32 +69,29 @@ orderByFragment
)
;

sortSpecification
sortSpecification { String sortKeySpec = null; String collSpec = null; String ordSpec = null; String nullOrd = null; }
: #(
SORT_SPEC sortKeySpecification (collationSpecification)? (orderingSpecification)?
SORT_SPEC sortKeySpec=sortKeySpecification (collSpec=collationSpecification)? (ordSpec=orderingSpecification)? (nullOrd=nullOrdering)?
{ out( renderOrderByElement( sortKeySpec, collSpec, ordSpec, nullOrd ) ); }
)
;

sortKeySpecification
: #(SORT_KEY sortKey)
sortKeySpecification returns [String sortKeyExp = null]
: #(SORT_KEY s:sortKey) { sortKeyExp = #s.getText(); }
;

sortKey
: i:IDENT {
out( #i );
}
: IDENT
;

collationSpecification
: c:COLLATE {
out( " collate " );
out( c );
}
collationSpecification returns [String collSpecExp = null]
: c:COLLATE { collSpecExp = "collate " + #c.getText(); }
;

orderingSpecification
: o:ORDER_SPEC {
out( " " );
out( #o );
}
orderingSpecification returns [String ordSpecExp = null]
: o:ORDER_SPEC { ordSpecExp = #o.getText(); }
;

nullOrdering returns [String nullOrdExp = null]
: n:NULL_ORDER { nullOrdExp = #n.getText(); }
;
32 changes: 30 additions & 2 deletions hibernate-core/src/main/antlr/order-by.g
Expand Up @@ -46,6 +46,7 @@ tokens
ORDER_BY;
SORT_SPEC;
ORDER_SPEC;
NULL_ORDER;
SORT_KEY;
EXPR_LIST;
DOT;
Expand All @@ -55,6 +56,9 @@ tokens
COLLATE="collate";
ASCENDING="asc";
DESCENDING="desc";
NULLS="nulls";
FIRST;
LAST;
}


Expand All @@ -76,7 +80,7 @@ tokens
* @return The text.
*/
protected final String extractText(AST ast) {
// for some reason, within AST creation blocks "[]" I am somtimes unable to refer to the AST.getText() method
// for some reason, within AST creation blocks "[]" I am sometimes unable to refer to the AST.getText() method
// using #var (the #var is not interpreted as the rule's output AST).
return ast.getText();
}
Expand Down Expand Up @@ -168,7 +172,7 @@ orderByFragment { trace("orderByFragment"); }
* the results should be sorted.
*/
sortSpecification { trace("sortSpecification"); }
: sortKey (collationSpecification)? (orderingSpecification)? {
: sortKey (collationSpecification)? (orderingSpecification)? (nullOrdering)? {
#sortSpecification = #( [SORT_SPEC, "{sort specification}"], #sortSpecification );
#sortSpecification = postProcessSortSpecification( #sortSpecification );
}
Expand Down Expand Up @@ -290,6 +294,30 @@ orderingSpecification! { trace("orderingSpecification"); }
}
;
/**
* Recognition rule for what SQL-2003 terms the <tt>null ordering</tt>; <tt>NULLS FIRST</tt> or
* <tt>NULLS LAST</tt>.
*/
nullOrdering! { trace("nullOrdering"); }
: NULLS n:nullPrecedence {
#nullOrdering = #( [NULL_ORDER, extractText( #n )] );
}
;
nullPrecedence { trace("nullPrecedence"); }
: IDENT {
if ( "first".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
#nullPrecedence.setType( FIRST );
}
else if ( "last".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
#nullPrecedence.setType( LAST );
}
else {
throw new SemanticException( "Expecting 'first' or 'last', but found '" + #nullPrecedence.getText() + "' as null ordering precedence." );
}
}
;
/**
* A simple-property-path is an IDENT followed by one or more (DOT IDENT) sequences
*/
Expand Down
46 changes: 42 additions & 4 deletions hibernate-core/src/main/antlr/sql-gen.g
Expand Up @@ -27,8 +27,11 @@ options {
/** the buffer resulting SQL statement is written to */
private StringBuilder buf = new StringBuilder();
private boolean captureExpression = false;
private StringBuilder expr = new StringBuilder();
protected void out(String s) {
buf.append(s);
getStringBuilder().append( s );
}
/**
Expand Down Expand Up @@ -72,7 +75,7 @@ options {
}

protected StringBuilder getStringBuilder() {
return buf;
return captureExpression ? expr : buf;
}

protected void nyi(AST n) {
Expand All @@ -92,6 +95,27 @@ options {
protected void commaBetweenParameters(String comma) {
out(comma);
}

protected void captureExpressionStart() {
captureExpression = true;
}

protected void captureExpressionFinish() {
captureExpression = false;
}

protected String resetCapture() {
final String expression = expr.toString();
expr = new StringBuilder();
return expression;
}

/**
* Implementation note: This is just a stub. SqlGenerator contains the effective implementation.
*/
protected String renderOrderByElement(String expression, String order, String nulls) {
throw new UnsupportedOperationException("Concrete SQL generator should override this method.");
}
}

statement
Expand Down Expand Up @@ -152,9 +176,14 @@ whereClauseExpr
| booleanExpr[ false ]
;
orderExprs
orderExprs { String ordExp = null; String ordDir = null; String ordNul = null; }
// TODO: remove goofy space before the comma when we don't have to regression test anymore.
: ( expr ) (dir:orderDirection { out(" "); out(dir); })? ( {out(", "); } orderExprs)?
// Dialect is provided a hook to render each ORDER BY element, so the expression is being captured instead of
// printing to the SQL output directly. See Dialect#renderOrderByElement(String, String, String, NullPrecedence).
: { captureExpressionStart(); } ( expr ) { captureExpressionFinish(); ordExp = resetCapture(); }
(dir:orderDirection { ordDir = #dir.getText(); })? (ordNul=nullOrdering)?
{ out( renderOrderByElement( ordExp, ordDir, ordNul ) ); }
( {out(", "); } orderExprs )?
;

groupExprs
Expand All @@ -167,6 +196,15 @@ orderDirection
| DESCENDING
;
nullOrdering returns [String nullOrdExp = null]
: NULLS fl:nullPrecedence { nullOrdExp = #fl.getText(); }
;
nullPrecedence
: FIRST
| LAST
;
whereExpr
// Expect the filter subtree, followed by the theta join subtree, followed by the HQL condition subtree.
// Might need parens around the HQL condition if there is more than one subtree.
Expand Down
41 changes: 41 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/NullPrecedence.java
@@ -0,0 +1,41 @@
package org.hibernate;

/**
* Defines precedence of null values within {@code ORDER BY} clause.
*
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public enum NullPrecedence {
/**
* Null precedence not specified. Relies on the RDBMS implementation.
*/
NONE,

/**
* Null values appear at the beginning of the sorted collection.
*/
FIRST,

/**
* Null values appear at the end of the sorted collection.
*/
LAST;

public static NullPrecedence parse(String type) {
if ( "none".equalsIgnoreCase( type ) ) {
return NullPrecedence.NONE;
}
else if ( "first".equalsIgnoreCase( type ) ) {
return NullPrecedence.FIRST;
}
else if ( "last".equalsIgnoreCase( type ) ) {
return NullPrecedence.LAST;
}
return null;
}

public static NullPrecedence parse(String type, NullPrecedence defaultValue) {
final NullPrecedence value = parse( type );
return value != null ? value : defaultValue;
}
}

0 comments on commit add6aea

Please sign in to comment.