Skip to content

Commit c2db273

Browse files
authored
Merge pull request #147 from TidierOrg/join_selectfix
fix join select issue
2 parents ed77af1 + 0757774 commit c2db273

22 files changed

Lines changed: 663 additions & 136 deletions

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# TidierDB.jl updates
2+
## v.8.8 - 2025-08-05
3+
- fixes issue when grouping on a join id
4+
- fixes cte construction issue
5+
- fixes select before join order execution issue
6+
- add `~` support for aggregate functions in mutate, similar to TidierData syntax
7+
- additional AWS/Athena Backend improvements
8+
29
## v.8.7 - 2025-07-07
310
- AWS Athena backend bug fixes
411
- add `temp` option to `@create_table`, default is `true`

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "TidierDB"
22
uuid = "86993f9b-bbba-4084-97c5-ee15961ad48b"
33
authors = ["Daniel Rizk <rizk.daniel.12@gmail.com> and contributors"]
4-
version = "0.8.7"
4+
version = "0.8.8"
55

66
[deps]
77
Arrow = "69666777-d1a9-59fb-9406-91d4454c9d45"

docs/examples/UserGuide/agg_window.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ end
1818
# ## Aggregate Functions in `@mutate`
1919
# By default, `@mutate`/`@transmute` supports (however, you can easily expand this list)
2020
# - `maximum`, `minimum`, `mean`, `std`, `sum`, `cumsum`
21-
# To use aggregate sql functions that are built in to any database backend, but exist outside of the TidierDB parser list above, simply wrap the function call in `agg()`
21+
# To use aggregate sql functions that are built in to any database backend, but exist outside of the TidierDB parser list above, simply prefix the function with `~` or wrap the function call in `agg()`
2222
@chain mtcars begin
2323
@group_by(cyl)
24-
@mutate(kurt = agg(kurtosis(mpg)))
24+
@mutate(kurt = ~kurtosis(mpg))
2525
@select cyl mpg kurt
2626
@head()
2727
@collect

docs/examples/UserGuide/ex_joining.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ end;
4949
query2 = @chain mtcars @filter(mpg>20) @mutate(mpg = mpg *4);
5050

5151
@chain query begin
52-
@left_join(t(query2), cyl == cyl)
52+
@left_join(query2, cyl == cyl)
5353
@summarize(avg_mean = mean(mpg), _by = efficiency)
5454
@mutate(mean = avg_mean / 4 )
5555
@collect
@@ -63,7 +63,7 @@ end
6363
# mt2 = dt(db, "ducks_db.mt2")
6464
# other_db = @chain dt(db, "ducks_db.mt2") @filter(!str_detect(car, "M"))
6565
# @chain mtcars begin
66-
# @left_join(t(other_db), model == car)
66+
# @left_join(other_db, model == car)
6767
# @select(model, fuel_efficiency)
6868
# @head(5)
6969
# @collect
@@ -120,7 +120,7 @@ end
120120
end;
121121

122122
@chain dt(db, "viewer") begin # access the view like any other table
123-
@left_join(t(query2), cyl == cyl)
123+
@left_join(query2, cyl == cyl)
124124
@summarize(avg_mean = mean(mpg), _by = efficiency)
125125
@mutate(mean = avg_mean / 4 )
126126
@collect
@@ -131,7 +131,7 @@ end
131131
prices = dt(db, "https://duckdb.org/data/prices.csv", "prices");
132132
holdings = dt(db, "https://duckdb.org/data/holdings.csv", "holdings");
133133
@chain holdings begin
134-
@inner_join(t(prices), ticker = ticker, closest(when >= when))
134+
@inner_join(prices, ticker = ticker, closest(when >= when))
135135
@select(holdings.ticker, holdings.when)
136136
@mutate(value = price * shares)
137137
@collect

src/TidierDB.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ using GZip
4141
struct databricks <: SQLBackend end
4242

4343
const _warning_ = Ref(false)
44-
const window_agg_fxns = [:lead, :lag, :dense_rank, :nth_value, :ntile, :rank_dense, :row_number, :first_value, :last_value, :cume_dist, :count]
44+
const window_agg_fxns = [:lead, :lag, :dense_rank, :nth_value, :ntile, :rank_dense, :row_number, :first_value, :last_value, :cume_dist, :count, :first, :last]
4545
current_sql_mode = Ref{SQLBackend}(duckdb())
4646
const color = Ref{Bool}(true)
4747
function set_sql_mode(mode::SQLBackend) current_sql_mode[] = mode end

src/TidierDB_macros.jl

Lines changed: 142 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
$docstring_select
33
"""
44
macro select(sqlquery, exprs...)
5-
65
exprs = parse_blocks(exprs...)
7-
86
return quote
97
exprs_str = map(expr -> isa(expr, Symbol) ? string(expr) : expr, $exprs)
108
sq = t($(esc(sqlquery)))
@@ -17,26 +15,22 @@ macro select(sqlquery, exprs...)
1715
if occursin(".", col)
1816
table_col_split = split(col, ".")
1917
table_name, col_name = table_col_split[1], table_col_split[2]
20-
21-
# Iterate and update current_selxn based on matches
2218
for idx in eachindex(sq.metadata.current_selxn)
23-
if sq.metadata.table_name[idx] == table_name &&
24-
sq.metadata.name[idx] == col_name
19+
if sq.metadata.table_name[idx] == table_name && sq.metadata.name[idx] == col_name
2520
sq.metadata.current_selxn[idx] = 2
2621
end
2722
end
2823
else
29-
# Direct matching for columns without 'table.' prefix
3024
matching_indices = findall(sq.metadata.name .== col)
3125
sq.metadata.current_selxn[matching_indices] .= 1
3226
end
3327
end
3428
end
35-
3629
sq
3730
end
3831
end
3932

33+
4034
"""
4135
$docstring_filter
4236
"""
@@ -181,25 +175,113 @@ end
181175

182176
function groupby_exp(expr, sq)
183177
if isa(expr, Expr) && expr.head == :(=) && isa(expr.args[1], Symbol)
184-
# Extract column alias name as string
185178
col_name = string(expr.args[1])
186179
if current_sql_mode[] == snowflake()
187180
col_name = uppercase(col_name) # COV_EXCL_LINE
188181
end
189182
push!(sq.metadata, Dict("name" => col_name, "type" => "UNKNOWN", "current_selxn" => 1, "table_name" => "table"))
190-
# Convert the right-hand side expression to a SQL expression
191-
col_expr = expr_to_sql(expr.args[2], sq)
192-
col_expr = string(col_expr)
193-
# Return the alias and the SQL snippet (wrapped in parentheses to be safe)
183+
col_expr = expr_to_sql(expr.args[2], sq) |> string
194184
return col_name, "(" * col_expr * ") AS " * col_name
195185
else
196186
error("Unsupported expression in @group_by: $(expr)")
197187
end
198188
end
199189

190+
# Single helper to get the expression behind an alias in the current SELECT
191+
function _expr_for_alias(select_sql::AbstractString, alias::AbstractString)
192+
s = strip(String(select_sql))
193+
body = startswith(uppercase(s), "SELECT ") ? s[8:end] : s
194+
195+
m = findfirst(Regex("(?i)\\bAS\\s+$alias\\b"), body)
196+
m === nothing && return nothing
197+
198+
as_start = first(m) - 1
199+
depth = 0
200+
i = as_start
201+
start_idx = firstindex(body)
202+
203+
while i >= firstindex(body)
204+
c = body[i]
205+
if c == ')'
206+
depth += 1
207+
elseif c == '('
208+
depth = max(depth - 1, 0)
209+
elseif c == ',' && depth == 0
210+
start_idx = nextind(body, i)
211+
break
212+
end
213+
i = prevind(body, i)
214+
end
215+
216+
expr = strip(body[start_idx:as_start])
217+
return expr == "" ? nothing : expr
218+
end
219+
200220
"""
201221
$docstring_group_by
202222
"""
223+
macro group_by(sqlquery, columns...)
224+
columns = parse_blocks(columns...)
225+
return quote
226+
columns_str = map(col -> isa(col, Symbol) ? string(col) : col, $columns)
227+
sq = t($(esc(sqlquery)))
228+
if isa(sq, SQLQuery)
229+
try
230+
# Build GROUP BY items; if a name matches a SELECT alias, use its expression
231+
group_items = String[]
232+
for c in columns_str
233+
expr = _expr_for_alias(sq.select, c)
234+
if expr !== nothing
235+
push!(group_items, expr) # e.g., COALESCE(t2.id, t1.id)
236+
else
237+
for nm in parse_tidy_db([c], sq.metadata)
238+
push!(group_items, nm) # qualified name
239+
end
240+
end
241+
end
242+
sq.groupBy = "GROUP BY " * join(group_items, ", ")
243+
244+
# If no projection yet or it's SELECT *, project only the grouping columns
245+
sel = strip(String(sq.select))
246+
if isempty(sel) || occursin(r"(?i)^SELECT\s+\*$", sel)
247+
sq.select = "SELECT " * join(group_items, ", ")
248+
end
249+
catch
250+
# Handle expression/alias form: group_by(alias = expr, ...)
251+
sq.groupBy_exprs = true
252+
local group_expressions = String[] # "(expr) AS alias"
253+
local group_aliases = String[] # "alias"
254+
255+
for col in $columns
256+
if isa(col, Expr) && col.head == :(=)
257+
let tup = groupby_exp(col, sq)
258+
push!(group_expressions, tup[2])
259+
push!(group_aliases, tup[1])
260+
end
261+
else
262+
for nm in parse_tidy_db([col], sq.metadata)
263+
push!(group_expressions, nm)
264+
push!(group_aliases, nm)
265+
end
266+
end
267+
end
268+
269+
# FIX #1: GROUP BY aliases (not "(expr) AS alias")
270+
sq.groupBy = "GROUP BY " * join(group_aliases, ", ")
271+
272+
# FIX #2: If no projection yet or it's SELECT *, expose the aliased select items
273+
sel = strip(String(sq.select))
274+
if isempty(sel) || occursin(r"(?i)^SELECT\s+\*$", sel)
275+
sq.select = "SELECT " * join(group_expressions, ", ")
276+
end
277+
end
278+
else
279+
error("Expected sqlquery to be an instance of SQLQuery")
280+
end
281+
sq
282+
end
283+
end
284+
#=
203285
macro group_by(sqlquery, columns...)
204286
columns = parse_blocks(columns...)
205287
return quote
@@ -248,6 +330,9 @@ macro group_by(sqlquery, columns...)
248330
sq
249331
end
250332
end
333+
=#
334+
335+
251336

252337

253338
"""
@@ -339,67 +424,77 @@ macro rename(sqlquery, renamings...)
339424
renamings = parse_blocks(renamings...)
340425

341426
return quote
342-
# Prepare the renaming rules from the macro arguments
427+
# Build old->new map from "new = old" pairs
343428
renamings_dict = Dict{String, String}()
344-
for renaming in $(esc(renamings))
345-
if isa(renaming, Expr) && renaming.head == :(=) && isa(renaming.args[1], Symbol)
346-
# Map original column names to new names for renaming
347-
renamings_dict[string(renaming.args[2])] = string(renaming.args[1])
429+
for r in $(esc(renamings))
430+
if isa(r, Expr) && r.head == :(=) && isa(r.args[1], Symbol)
431+
# value11 = value1 => "value1" => "value11"
432+
renamings_dict[string(r.args[2])] = string(r.args[1])
348433
else
349-
throw("Unsupported renaming format in @rename: $(renaming)")
434+
throw("Unsupported renaming format in @rename: $(r)")
350435
end
351436
end
352437

353438
sq = t($(esc(sqlquery)))
354-
355439
if isa(sq, SQLQuery)
356-
# Generate a new CTE name
440+
# New CTE wrapper
357441
new_cte_name = "cte_" * string(sq.cte_count + 1)
358442
sq.cte_count += 1
359-
360-
# Determine the select clause for the new CTE
361-
select_clause = if isempty(sq.select) || sq.select == "SELECT *"
362-
# If select is *, list all columns with renaming applied
363-
all_columns = sq.metadata[!, :name]
364-
join([haskey(renamings_dict, col) ? col * " AS " * renamings_dict[col] : col for col in all_columns], ", ")
443+
444+
# Build projection with renaming
445+
select_clause = if isempty(strip(sq.select)) || strip(uppercase(sq.select)) == "SELECT *" || strip(sq.select) == "*"
446+
# Only include columns with current_selxn != 0
447+
mask = sq.metadata.current_selxn .!= 0
448+
cols = Vector{String}(sq.metadata.name[mask])
449+
join([haskey(renamings_dict, c) ? string(c, " AS ", renamings_dict[c]) : c for c in cols], ", ")
365450
else
366-
367-
select_parts = split(sq.select[8:end], ", ")
368-
updated_parts = map(select_parts) do part
369-
# Identify the base column name for potential renaming
370-
col = strip(split(part, " AS ")[1])
371-
if haskey(renamings_dict, col)
372-
# Apply renaming to the base column name
373-
string(renamings_dict[col]) * " AS " * col
451+
# Edit existing SELECT list (adjust aliases, don't add new columns)
452+
s = String(sq.select)
453+
body = startswith(uppercase(s), "SELECT ") ? s[8:end] : s
454+
parts = split(body, ", ")
455+
updated = map(parts) do part
456+
if occursin(r"(?i)\bAS\b", part)
457+
bits = split(part, r"(?i)\bAS\b")
458+
expr = strip(bits[1])
459+
alias = strip(bits[end])
460+
new_alias = get(renamings_dict, alias, alias)
461+
string(expr, " AS ", new_alias)
374462
else
375-
# No renaming needed; keep the original part
376-
part
463+
base = strip(split(part, ".")[end])
464+
if haskey(renamings_dict, base)
465+
string(part, " AS ", renamings_dict[base])
466+
else
467+
part
468+
end
377469
end
378470
end
379-
sq.select = " " * join(updated_parts, ", ")
380-
381-
end
382-
for (old_name, new_name) in renamings_dict
383-
sq.metadata[!, :name] = replace.(sq.metadata[!, :name], old_name => new_name)
471+
replace(join(updated, ", "), r"(?i)\bAS\s+AS\b" => " AS ")
384472
end
385473

386-
if isempty(sq.select)
387-
sq.select == "SELECT *"
474+
# Update metadata names only for selected cols (current_selxn != 0)
475+
for (old_name, new_name) in renamings_dict
476+
for i in eachindex(sq.metadata.name)
477+
if sq.metadata.current_selxn[i] != 0 && sq.metadata[i, :name] == old_name
478+
sq.metadata[i, :name] = new_name
479+
end
480+
end
388481
end
389482

390-
# Create the new CTE with the select clause
483+
# Emit CTE and re-point FROM
391484
new_cte = CTE(name=new_cte_name, select=select_clause, from=sq.from)
392485
push!(sq.ctes, new_cte)
393-
394-
# Update the from clause of the SQLQuery to the new CTE
395486
sq.from = new_cte_name
487+
488+
# Clear sq.select so subsequent steps project from the new CTE
489+
sq.select = ""
396490
else
397491
error("Expected sqlquery to be an instance of SQLQuery")
398492
end
399493
sq
400494
end
401495
end
402496

497+
403498
mutable struct DBQuery
404499
val::String
405500
end

src/db_parsing.jl

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,19 @@ function parse_tidy_db(exprs, metadata::DataFrame)
159159

160160
# If no columns are explicitly included, default to all columns (with current_selxn == 1) minus any exclusions
161161
if isempty(included_columns)
162-
included_columns = metadata.name[metadata.current_selxn .== 1]
163-
included_columns = setdiff(included_columns, excluded_columns)
162+
idx = findall(metadata.current_selxn .>= 1)
163+
included_columns = String[]
164+
for i in idx
165+
nm = metadata.name[i]
166+
if nm in excluded_columns
167+
continue
168+
end
169+
if metadata.current_selxn[i] == 2
170+
push!(included_columns, string(metadata.table_name[i], ".", nm))
171+
else
172+
push!(included_columns, nm)
173+
end
174+
end
164175
else
165176
included_columns = setdiff(included_columns, excluded_columns)
166177
end

0 commit comments

Comments
 (0)