22$docstring_select
33"""
44macro 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
3831end
3932
33+
4034"""
4135$docstring_filter
4236"""
@@ -181,25 +175,113 @@ end
181175
182176function 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
198188end
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+ #=
203285macro 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
250332end
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)\b AS\b " , part)
457+ bits = split (part, r" (?i)\b AS\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)\b AS\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
401495end
402496
497+
403498mutable struct DBQuery
404499 val:: String
405500end
0 commit comments