From e0b9b63b79d7618d3d2c2ce633e22ccdad80eaf0 Mon Sep 17 00:00:00 2001 From: Tim Brown Date: Thu, 23 Mar 2017 03:20:55 -0600 Subject: [PATCH] First stab at implementing the various requirements for issue #8 to accept 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. --- models/Query/Builder.cfc | 115 ++++++++++++++- tests/specs/Query/Builder+GrammarSpec.cfc | 167 ++++++++++++++++++++++ 2 files changed, 275 insertions(+), 7 deletions(-) diff --git a/models/Query/Builder.cfc b/models/Query/Builder.cfc index d372f3e3..ec332113 100644 --- a/models/Query/Builder.cfc +++ b/models/Query/Builder.cfc @@ -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; } diff --git a/tests/specs/Query/Builder+GrammarSpec.cfc b/tests/specs/Query/Builder+GrammarSpec.cfc index 3f61eb55..e5d49d99 100644 --- a/tests/specs/Query/Builder+GrammarSpec.cfc +++ b/tests/specs/Query/Builder+GrammarSpec.cfc @@ -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() {