Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/order-direction-case.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- A lowercase or mixed-case sort direction in an `order` string is now honoured: `order="createdAt desc"` (or `Desc`, or with extra spaces or a tab before it) sorts descending in `findAll()`, paginated finders, `table.column` items and the query builder's `orderBy()`. It used to sort ascending, and a `table.column desc` item threw `Wheels.InvalidOrderClause`
2 changes: 1 addition & 1 deletion vendor/wheels/model/query/QueryBuilder.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ component output="false" {
public any function orderBy(required string property, string direction = "ASC") {
$validatePropertyName(arguments.property);
$validateDirection(arguments.direction);
ArrayAppend(variables.orderClauses, "#arguments.property# #arguments.direction#");
ArrayAppend(variables.orderClauses, "#arguments.property# #UCase(arguments.direction)#");
return this;
}

Expand Down
20 changes: 16 additions & 4 deletions vendor/wheels/model/sql.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@
return arguments.sql;
}

/**
* Internal function. One `order` item as `<item> ASC` or `<item> DESC`: whitespace runs (spaces,
* tabs) become single spaces, and a trailing direction is read whatever its case and emitted in
* upper case. An item with no direction sorts ascending. A lowercase direction used to be
* missed, so `order="id desc"` sorted ascending.
*/
public string function $normalizeOrderItem(required string item) {
local.text = Trim(ReReplace(arguments.item, "\s+", " ", "all"));
local.last = ListLast(local.text, " ");
if (ListLen(local.text, " ") > 1 && ListFindNoCase("ASC,DESC", local.last)) {
return Left(local.text, Len(local.text) - Len(local.last) - 1) & " " & UCase(local.last);
}
return local.text & " ASC";
}

/**
* Internal function.
*/
Expand All @@ -258,10 +273,7 @@
local.orderArray = ListToArray(arguments.order);
local.iEnd = ArrayLen(local.orderArray);
for (local.i = 1; local.i <= local.iEnd; local.i++) {
local.iItem = Trim(local.orderArray[local.i]);
if (!Find(" ASC", local.iItem) && !Find(" DESC", local.iItem)) {
local.iItem &= " ASC";
}
local.iItem = $normalizeOrderItem(local.orderArray[local.i]);
if (Find("(", local.iItem)) {
// Reject raw SQL expressions — calculated properties should be referenced by name
local.property = Trim(SpanExcluding(local.iItem, " "));
Expand Down
64 changes: 64 additions & 0 deletions vendor/wheels/tests/specs/model/orderDirectionCaseSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* The sort direction in an `order` string is read whatever its case or the whitespace before it:
* `desc`, `Desc` and `DESC` all sort descending, through findAll(), paginated finders, dot-notation
* items and the query builder's orderBy(). A lowercase direction used to sort ascending.
*/
component extends="wheels.WheelsTest" {

function beforeAll() {
variables.g = application.wo;
var ids = variables.g.model("author").findAll(select = "id", order = "id ASC", returnAs = "query");
variables.minId = ids.id[1];
variables.maxId = ids.id[ids.recordCount];
}

function firstId(required query result) {
return arguments.result.id[1];
}

function run() {

describe("The direction in an order string", () => {

it("sorts descending for desc in any case", () => {
for (var dir in ["DESC", "desc", "Desc", "dEsC"]) {
expect(firstId(variables.g.model("author").findAll(order = "id #dir#", maxRows = 1))).toBe(variables.maxId, "order=""id #dir#""");
}
});

it("sorts ascending for asc in any case, and when no direction is given", () => {
for (var dir in ["ASC", "asc", "Asc", ""]) {
expect(firstId(variables.g.model("author").findAll(order = Trim("id #dir#"), maxRows = 1))).toBe(variables.minId, "order=""id #dir#""");
}
});

it("reads the direction after repeated spaces or a tab", () => {
expect(firstId(variables.g.model("author").findAll(order = "id desc", maxRows = 1))).toBe(variables.maxId);
expect(firstId(variables.g.model("author").findAll(order = "id#Chr(9)#desc", maxRows = 1))).toBe(variables.maxId);
expect(firstId(variables.g.model("author").findAll(order = " id desc ", maxRows = 1))).toBe(variables.maxId);
});

it("reads each item's direction in a multi-column order", () => {
var rows = variables.g.model("author").findAll(order = "lastName asc, id desc", returnAs = "query");
var expected = variables.g.model("author").findAll(order = "lastName ASC, id DESC", returnAs = "query");
expect(ValueList(rows.id)).toBe(ValueList(expected.id));
});

it("reads a lowercase direction on a table.column item", () => {
var tableName = variables.g.model("author").tableName();
expect(firstId(variables.g.model("author").findAll(order = "#tableName#.id desc", maxRows = 1))).toBe(variables.maxId);
});

it("reads a lowercase direction in a paginated finder", () => {
expect(firstId(variables.g.model("author").findAll(order = "id desc", page = 1, perPage = 2))).toBe(variables.maxId);
});

it("reads a lowercase direction passed to the query builder's orderBy()", () => {
expect(firstId(variables.g.model("author").orderBy("id", "desc").limit(1).get())).toBe(variables.maxId);
});

});

}

}
Loading