Skip to content

Commit

Permalink
First stab at implementing the various requirements for issue #8 to a…
Browse files Browse the repository at this point in the history
…ccept an array or list as the column argument's value. The array can accept a variety of value formats that can be intermingled if desired. All scenarios will inherit eithe the default direction or the supplied value for the direction argument.
  • Loading branch information
Tim Brown authored and elpete committed Mar 23, 2017
1 parent 5eb4561 commit e0b9b63
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 7 deletions.
115 changes: 108 additions & 7 deletions models/Query/Builder.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1108,19 +1108,120 @@ component displayname="Builder" accessors="true" {
* Add an order by clause to the query. To order by multiple columns, call `orderBy` multiple times.
* The order in which `orderBy` is called matters and is the order it appears in the SQL.
*
* @column The name of the column to order by. An expression (`builder.raw()`) can be passed as well.
* @direction The direction by which to order the query. Accepts "asc" OR "desc". Default: "asc".
* @column The name of the column(s) to order by. An expression (`builder.raw()`) can be passed as well. An array can be passed with any combination of simple values, array, struct, or list for each entry in the array (an example with all possible value styles: column = [ "last_name", [ "age", "desc" ], { column = "favorite_color", direction = "desc" }, "height|desc" ];. The column argument can also just accept a comman delimited list with a pipe ( | ) as the secondary delimiter denoting the direction of the order by. The pipe delimiter is also used when parsing the column argument when it is passed as an array and the entry in the array is a pipe delimited string.
* @direction The direction by which to order the query. Accepts "asc" OR "desc". Default: "asc". If column argument is an array this argument will be used as the default value for all entries in the column list or array that fail to specify a direction for a speicifc column.
*
* @return qb.models.Query.Builder
*/
public Builder function orderBy( required any column, string direction = "asc" ) {
var validDirections = [ "asc", "desc" ];

if ( getUtils().isExpression( column ) ) {
direction = "raw";
variables.orders.append( {
direction = "raw",
column = column
} );
}
variables.orders.append( {
direction = direction,
column = column
} );
// if the column argument is an array
else if ( isArray( column ) ) {

for ( var col in column ) {
//check the value of the current iteration to determine what blend of column def they went with
// ex: "DATE(created_at)" -- RAW expression
if ( getUtils().isExpression( col ) ) {
variables.orders.append( {
direction = "raw",
column = col
} );
}
// ex: "favorite_color|desc,height|asc,weight|desc"
else if ( isSimpleValue( col ) && listLen( col ) > 1 ) {
//convert list to array for easier looping and access to vals
var arCols = listToArray( col );

for ( var c in arCols ) {
var colName = listFirst( c, "|" );

if ( listLen( c, "|" ) == 2 ) {
var dir = ( arrayFindNoCase( validDirections, listLast( c, "|" ) ) ) ? listLast( c, "|" ) : direction;
} else {
var dir = direction;
}

variables.orders.append( {
direction = dir,
column = colName
} );
}
}
// ex: "age|desc" || "last_name"
else if ( isSimpleValue( col ) ) {
var colName = listFirst( col, "|" );
// ex: "age|desc"
if ( listLen( col, "|" ) == 2 ) {
var dir = ( arrayFindNoCase( validDirections, listLast( col, "|" ) ) ) ? listLast( col, "|" ) : direction;
} else {
var dir = direction;
}

// now append the simple value column name and determined direction
variables.orders.append( {
direction = dir,
column = colName
} );
}
// ex: { "column" = "favorite_color" } || { "column" = "favorite_color", direction = "desc" }
else if ( isStruct( col ) && structKeyExists( col, "column" ) ) {
//as long as the struct provided contains the column keyName then we can append it. If the direction column is omitted we will assume direction argument's value
if ( getUtils().isExpression( col.column ) ) {
var dir = "raw";
} else {
var dir = ( structKeyExists( col, "direction") && arrayFindNoCase( validDirections, col.direction ) ) ? col.direction : direction;
}
variables.orders.append({
direction = dir,
column = col.column
});
}
// ex: [ "age", "desc" ]
else if ( isArray( col ) ) {
//assume position 1 is the column name and position 2 if it exists and is a valid direction ( asc | desc ) use it.
variables.orders.append({
direction = ( arrayLen( col ) == 2 && arrayFindNoCase( validDirections, col[2] ) ) ? col[2] : direction,
column = col[1]
});
}

}

}
// ex: "last_name|asc,age|desc"
else if ( listLen( column ) > 1 ) {
//convert list to array for easier looping and access to vals
var arCols = listToArray( column );

for ( var col in arCols ) {
var colName = listFirst( col, "|" );

if ( listLen( col, "|" ) == 2 ) {
var dir = ( arrayFindNoCase( validDirections, listLast( col, "|" ) ) ) ? listLast( col, "|" ) : direction;
} else {
var dir = direction;
}

variables.orders.append( {
direction = dir,
column = colName
} );
}
}
else {
variables.orders.append( {
direction = direction,
column = column
} );
}

return this;
}

Expand Down
167 changes: 167 additions & 0 deletions tests/specs/Query/Builder+GrammarSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,173 @@ component extends="testbox.system.BaseSpec" {
);
expect( getTestBindings( builder ) ).toBe( [] );
} );

describe( "can accept an array for the column argument", function(){

describe( "with the array values", function() {

it( "as simple strings", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [ "last_name", "age", "favorite_color" ] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" ASC, ""age"" ASC, ""favorite_color"" ASC"
);
});

it( "as pipe delimited strings", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [ "last_name|desc", "age|asc", "favorite_color|desc" ] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" ASC, ""favorite_color"" DESC"
);
});

it( "as a nested positional array", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [ [ "last_name", "desc" ], [ "age", "asc" ], [ "favorite_color" ] ] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" ASC, ""favorite_color"" ASC"
);
});

it( "as a nested positional array with leniency for arrays of length 1 or longer than 2 which assumes position 1 is column name and position 2 is the direction and ignores other entries in the nested array", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [ [ "last_name", "desc" ], [ "age", "asc" ], [ "favorite_color" ], [ "height", "asc", "will", "be", "ignored" ] ] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" ASC, ""favorite_color"" ASC, ""height"" ASC"
);
});

it( "as a any combo of values and ignores then inherits the direction's argument value if an invalid direction is supplied (anything other than (asc|desc)", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [
[ "last_name", "desc" ],
[ "age", "forward" ],
"favorite_color|backward",
"favorite_food|desc",
{ column = "height", direction = "tallest" },
{ column = "weight", direction = "desc" },
builder.raw( "DATE(created_at)" ),
{ column = builder.raw( "DATE(modified_at)" ), direction = "desc" } //desc will be ignored in this case because it's an expression
] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" ASC, ""favorite_color"" ASC, ""favorite_food"" DESC, ""height"" ASC, ""weight"" DESC, DATE(created_at), DATE(modified_at)"
);
});

it( "as raw expressions", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [ "last_name|desc", "age|asc", "favorite_color|desc" ] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" ASC, ""favorite_color"" DESC"
);
});

it( "as simple strings OR pipe delimited strings intermingled", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [ "last_name", "age|desc", "favorite_color" ] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" ASC, ""age"" DESC, ""favorite_color"" ASC"
);
});

it( "can accept a struct with a column key and optionally the direction key", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [ { column = "last_name" } , { column = "age", direction = "asc" },{ column = "favorite_color", direction = "desc" } ], "desc" );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" ASC, ""favorite_color"" DESC"
);
});

it( "as any combo of any valid values intermingled", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( [
"last_name",
"age|desc",
"favorite_color|desc,height|asc,weight|desc",
[ "eye_color", "desc" ],
[ "hair_color" ],
{ column = "is_musical" },
{ column = "is_athletic", direction = "desc", extraKey = "ignored" },
builder.raw( "DATE(created_at)" ),
{ column = builder.raw( "DATE(modified_at)" ), direction = "desc" } // direction is ignored because it should be RAW
] );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" ASC, ""age"" DESC, ""favorite_color"" DESC, ""height"" ASC, ""weight"" DESC, ""eye_color"" DESC, ""hair_color"" ASC, ""is_musical"" ASC, ""is_athletic"" DESC, DATE(created_at), DATE(modified_at)"
);
});

});

});

describe( "can accept a comma delimited list for the column argument", function(){

describe( "with the list values", function() {

it( "as simple column names that inherit the default direction", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( "last_name,age,favorite_color");

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" ASC, ""age"" ASC, ""favorite_color"" ASC"
);
});

it( "as simple column names while inheriting the direction argument's supplied value", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( "last_name,age,favorite_color", "desc" );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" DESC, ""favorite_color"" DESC"
);
});

it( "as column names with secondary piped delimited value representing the direction for each column", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( "last_name|desc,age|desc,favorite_color|asc" );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" DESC, ""age"" DESC, ""favorite_color"" ASC"
);
});

it( "as column names with secondary piped delimited value representing the direction for each column and inheriting direction from the direction argument's value when supplied", function(){
var builder = getBuilder();
builder.select( "*").from( "users" )
.orderBy( "last_name|asc,age,favorite_color|asc", "desc" );

expect( builder.toSql() ).toBeWithCase(
"SELECT * FROM ""users"" ORDER BY ""last_name"" ASC, ""age"" DESC, ""favorite_color"" ASC"
);
});

});

});


} );

describe( "limits", function() {
Expand Down

0 comments on commit e0b9b63

Please sign in to comment.