Skip to content

Commit 7e2a369

Browse files
committed
bump version to 0.1.7 and add support for ORDER BY on SELECT aliases in sql_query_generator
1 parent 527617e commit 7e2a369

3 files changed

Lines changed: 82 additions & 8 deletions

File tree

sqlquery.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Package
22

3-
version = "0.1.6"
3+
version = "0.1.7"
44
author = "ThomasTJdev"
55
description = "SQL query builder and validator - opiniated"
66
license = "MIT"

src/sqlquery/sql_query_generator.nim

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,25 @@ proc parseTable(table: string): string =
132132
return table
133133

134134

135-
proc parseOrderBy(orderBy: seq[OrderSpec], table = ""): seq[string] =
135+
proc selectAliasesFromSelectList(select: seq[string]): seq[string] =
136+
## Extracts explicit SELECT aliases (e.g. "cnt" from "COUNT(*) AS cnt") for use in ORDER BY.
137+
for item in select:
138+
let lower = item.toLowerAscii()
139+
if " as " in lower:
140+
let parts = lower.split(" as ", 1)
141+
if parts.len == 2:
142+
result.add(parts[1].strip())
143+
144+
proc parseOrderBy(orderBy: seq[OrderSpec], table = "", selectAliases: seq[string] = @[]): seq[string] =
136145
for order in orderBy:
137-
var field = order.field
138-
if "." notin field and table != "":
146+
let rawField = order.field
147+
let isSelectAlias = rawField.toLowerAscii() in selectAliases
148+
var field = rawField
149+
if not isSelectAlias and "." notin field and table != "":
139150
field = table & "." & field
140151

141152
let direction = order.direction
142-
if direction != QueryDirection.IGNORE:
153+
if direction != QueryDirection.IGNORE and not isSelectAlias:
143154
let validation = validateFieldExists(field)
144155
if not validation.valid:
145156
sqlError("[ORDER BY] Field '" & validation.fieldName & "' does not exist in table '" & validation.tableName & "'", order)
@@ -343,6 +354,10 @@ proc parseSelect(select: seq[string], requireTableName = true, table = "", table
343354
if fieldStr.contains(" "):
344355
result.add(fieldLower)
345356
continue
357+
# Inner content may be a literal (e.g. percentile_cont(0.5)) — skip validation if not a known field.
358+
if not validateFieldExists(fieldStr).valid:
359+
result.add(fieldLower)
360+
continue
346361

347362
if not requireTableName and "." notin fieldStr and table != "":
348363
if not validateFieldExists(fieldStr).valid and not validateFieldExists(table & "." & fieldStr).valid:
@@ -538,7 +553,8 @@ proc selectQueryRuntime*(
538553
whereParsed = parseWhere(where, requireTableName = false, table = tableParsed)
539554
groupByParsed = parseGroupBy(groupBy, table = tableParsed)
540555

541-
let orderParsed = parseOrderBy(order, table = tableParsed)
556+
let orderSelectAliases = selectAliasesFromSelectList(select)
557+
let orderParsed = parseOrderBy(order, table = tableParsed, selectAliases = orderSelectAliases)
542558
let limitParsed = parseLimit(limit)
543559
let offsetParsed = parseOffset(offset)
544560

@@ -817,6 +833,18 @@ macro selectQuery*(
817833
else:
818834
compileError("[SELECT] Field '" & fieldStr & "' does not exist 2")
819835

836+
# Collect SELECT aliases (e.g. "cnt" from "COUNT(*) AS cnt") for ORDER BY validation
837+
var selectAliases: seq[string] = @[]
838+
if processedSelect != nil and processedSelect.kind == nnkPrefix and processedSelect[0].eqIdent("@"):
839+
let bracketExpr = processedSelect[1]
840+
if bracketExpr.kind == nnkBracket:
841+
for selectExpr in bracketExpr:
842+
if selectExpr.kind == nnkStrLit:
843+
let item = selectExpr.strVal.toLowerAscii()
844+
if " as " in item:
845+
let parts = item.split(" as ", 1)
846+
if parts.len == 2:
847+
selectAliases.add(parts[1].strip())
820848

821849
#
822850
# :== Validate WHERE clause fields
@@ -879,12 +907,17 @@ macro selectQuery*(
879907
if orderExpr.kind == nnkTupleConstr and orderExpr.len > 0:
880908
let fieldNode = orderExpr[0]
881909
if fieldNode.kind == nnkStrLit:
882-
var fieldStr = fieldNode.strVal
910+
let rawFieldStr = fieldNode.strVal
911+
var fieldStr = rawFieldStr
883912
if "." notin fieldStr and joins == nil:
884913
fieldStr = table & "." & fieldStr
885914

886915
if not validateFieldExists(fieldStr).valid:
887-
compileError("[ORDER BY] Order by field '" & fieldStr & "' does not exist in table '" & table & "'")
916+
# Allow ORDER BY on SELECT aliases (e.g. "cnt" from "COUNT(*) AS cnt").
917+
# When select is a variable we cannot extract aliases at compile time (selectAliases empty);
918+
# allow the field so runtime validation can accept it if it is an alias.
919+
if selectAliases.len > 0 and rawFieldStr.toLowerAscii() notin selectAliases:
920+
compileError("[ORDER BY] Order by field '" & fieldStr & "' does not exist in table '" & table & "'")
888921

889922
elif order == nil:
890923
# Handle nil case - create empty order

tests/test_sqlquery_generator.nim

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,37 @@ suite "ORDER BY extended":
498498

499499
check query.sql == "SELECT actions.id FROM actions WHERE actions.id = ? AND actions.is_deleted IS NULL ORDER BY actions.name"
500500

501+
test "ORDER BY SELECT alias (e.g. COUNT(*) AS cnt, order by cnt DESC) 1":
502+
let query = selectQuery(
503+
table = "actions",
504+
select = @["COUNT(*) AS cnt"],
505+
where = @[("actions.project_id", "=", "123")],
506+
order = @[("cnt", DESC)]
507+
)
508+
check query.sql == "SELECT count(*) as cnt FROM actions WHERE actions.project_id = ? AND actions.is_deleted IS NULL ORDER BY cnt DESC"
509+
check query.params == @["123"]
510+
511+
test "ORDER BY SELECT alias (e.g. COUNT(*) AS cnt, order by cnt DESC) 2":
512+
let query = selectQuery(
513+
table = "actions",
514+
select = @["actions.id", "COUNT(*) AS cnt"],
515+
where = @[("actions.project_id", "=", "123")],
516+
order = @[("cnt", DESC)]
517+
)
518+
check query.sql == "SELECT actions.id, count(*) as cnt FROM actions WHERE actions.project_id = ? AND actions.is_deleted IS NULL ORDER BY cnt DESC"
519+
check query.params == @["123"]
520+
521+
test "ORDER BY SELECT alias (e.g. COUNT(*) AS cnt, order by cnt DESC) 3":
522+
let s = @["actions.id", "COUNT(*) AS cnt"]
523+
let query = selectQuery(
524+
table = "actions",
525+
select = s,
526+
where = @[("actions.project_id", "=", "123")],
527+
order = @[("cnt", DESC)]
528+
)
529+
check query.sql == "SELECT actions.id, count(*) as cnt FROM actions WHERE actions.project_id = ? AND actions.is_deleted IS NULL ORDER BY cnt DESC"
530+
check query.params == @["123"]
531+
501532

502533
# ============================================================================
503534
# Extended Tests - WHERE Operators
@@ -596,6 +627,16 @@ suite "runtime functions extended":
596627
check query.sql == "SELECT actions.id, actions.name FROM actions WHERE actions.project_id = ? AND actions.is_deleted IS NULL"
597628
check query.params == @["123"]
598629

630+
test "selectQueryRuntime ORDER BY SELECT alias (e.g. cnt from COUNT(*) AS cnt)":
631+
let query = selectQueryRuntime(
632+
table = "actions",
633+
select = @["COUNT(*) AS cnt"],
634+
where = @[("actions.project_id", "=", "123")],
635+
order = @[("cnt", DESC)]
636+
)
637+
check query.sql == "SELECT count(*) as cnt FROM actions WHERE actions.project_id = ? AND actions.is_deleted IS NULL ORDER BY cnt DESC"
638+
check query.params == @["123"]
639+
599640
test "deleteQueryRuntime basic":
600641
let query = deleteQueryRuntime(
601642
table = "actions",

0 commit comments

Comments
 (0)