From b3a400f2bc84b60b63949c8a08fa7dc442e0b28b Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Mon, 10 Mar 2025 18:55:40 -0500 Subject: [PATCH 01/17] @dissect: use "local" declaration to indicate a binding --- src/dissect.jl | 31 +++++++++++++--------- src/link.jl | 22 ++++++++-------- src/nodes.jl | 68 ++++++++++++++++++++++++------------------------ src/resolve.jl | 10 +++---- src/serialize.jl | 18 ++++++------- src/translate.jl | 68 ++++++++++++++++++++++++------------------------ 6 files changed, 111 insertions(+), 106 deletions(-) diff --git a/src/dissect.jl b/src/dissect.jl index e512534a..a1ea2eb5 100644 --- a/src/dissect.jl +++ b/src/dissect.jl @@ -12,23 +12,24 @@ function dissect(@nospecialize(val), @nospecialize(pat)) end function dissect(scr::Symbol, @nospecialize(pat)) - if pat isa Symbol - if pat === :_ - :(true) - elseif pat === :nothing || pat === :missing - :($scr === $pat) - else - :(local $pat = $scr; true) - end + if pat === :_ + :(true) + elseif pat === :nothing || pat === :missing + :($scr === $pat) elseif pat isa Bool :($scr === $pat) elseif pat isa QuoteNode && pat.value isa Symbol :($scr === $pat) elseif pat isa Expr nargs = length(pat.args) - if pat.head === :(:=) && nargs == 2 - ex1 = dissect(scr, pat.args[1]) - ex2 = dissect(scr, pat.args[2]) + if pat.head === :local && nargs == 1 && + (local var = pat.args[1]) isa Symbol + :(local $var = $scr; true) + elseif pat.head === :local && nargs === 1 && + (local pat′ = pat.args[1]) isa Expr && pat′.head === :(=) && + length(pat′.args) == 2 && (local var = pat′.args[1]) isa Symbol + ex1 = :(local $var = $scr; true) + ex2 = dissect(scr, pat′.args[2]) :($ex2 && $ex1) elseif pat.head === :(::) && nargs == 1 :($scr isa $(pat.args[1])) @@ -41,8 +42,10 @@ function dissect(scr::Symbol, @nospecialize(pat)) elseif pat.head === :kw && nargs == 2 dissect(:($scr.$(pat.args[1])), pat.args[2]) elseif pat.head === :call && nargs >= 1 && - (local f = pat.args[1]; f isa Symbol) + (local f = pat.args[1]) isa Symbol dissect(scr, getfield(FunSQL, f), pat.args[2:end]) + elseif pat.head === :($) && nargs == 1 + :(isequal($scr, $(pat.args[1]))) else error("invalid pattern: $(repr(pat))") end @@ -114,7 +117,9 @@ end function dissect(scr::Symbol, ::Type{GlobalRef}, pats::Vector{Any}) if length(pats) == 2 - return :($scr isa GlobalRef && $scr.mod == $(pats[1]) && $scr.name === $(pats[2])) + ex1 = dissect(:($scr.mod), pats[1]) + ex2 = dissect(:($scr.name), pats[2]) + return :($scr isa GlobalRef && $ex1 && $ex2) end error("invalid pattern: $(repr(pats))") end diff --git a/src/link.jl b/src/link.jl index d4d12487..812c57a4 100644 --- a/src/link.jl +++ b/src/link.jl @@ -23,7 +23,7 @@ struct LinkContext end function link(n::SQLNode) - @dissect(n, WithContext(over = over, catalog = catalog)) || throw(ILLFormedError()) + @dissect(n, WithContext(over = (local over), catalog = (local catalog))) || throw(ILLFormedError()) ctx = LinkContext(catalog) t = row_type(over) refs = SQLNode[] @@ -221,7 +221,7 @@ end function link(n::AsNode, ctx) refs = SQLNode[] for ref in ctx.refs - if @dissect(ref, over |> Nested(name = name)) + if @dissect(ref, (local over) |> Nested(name = (local name))) @assert name == n.name push!(refs, over) else @@ -241,7 +241,7 @@ function link(n::DefineNode, ctx) refs = SQLNode[] seen = Set{Symbol}() for ref in ctx.refs - if @dissect(ref, nothing |> Get(name = name)) && name in keys(n.label_map) + if @dissect(ref, nothing |> Get(name = (local name))) && name in keys(n.label_map) push!(seen, name) else push!(refs, ref) @@ -300,13 +300,13 @@ function link(n::GroupNode, ctx) if has_aggregates ctx′ = LinkContext(ctx, refs = refs) for ref in ctx.refs - if (@dissect(ref, nothing |> Agg(args = args, filter = filter) |> Nested(name = name)) && name === n.name) || - (@dissect(ref, nothing |> Agg(args = args, filter = filter)) && n.name === nothing) + if (@dissect(ref, nothing |> Agg(args = (local args), filter = (local filter)) |> Nested(name = (local name))) && name === n.name) || + (@dissect(ref, nothing |> Agg(args = (local args), filter = (local filter))) && n.name === nothing) gather!(args, ctx′) if filter !== nothing gather!(filter, ctx′) end - elseif @dissect(ref, nothing |> Get(name = name)) && name in keys(n.label_map) + elseif @dissect(ref, nothing |> Get(name = (local name))) && name in keys(n.label_map) # Force evaluation in a nested subquery. push!(refs, n.by[n.label_map[name]]) end @@ -352,13 +352,13 @@ function link(n::IterateNode, ctx) end function route(r::JoinRouter, ref::SQLNode) - if @dissect(ref, over |> Nested(name = name)) && name in r.label_set + if @dissect(ref, (local over) |> Nested(name = (local name))) && name in r.label_set return 1 end - if @dissect(ref, Get(name = name)) && name in r.label_set + if @dissect(ref, Get(name = (local name))) && name in r.label_set return 1 end - if @dissect(ref, over |> Agg()) && r.group + if @dissect(ref, (local over) |> Agg()) && r.group return 1 end return -1 @@ -424,8 +424,8 @@ function link(n::PartitionNode, ctx) ctx′ = LinkContext(ctx, refs = imm_refs) has_aggregates = false for ref in ctx.refs - if (@dissect(ref, nothing |> Agg(args = args, filter = filter) |> Nested(name = name)) && name === n.name) || - (@dissect(ref, nothing |> Agg(args = args, filter = filter)) && n.name === nothing) + if (@dissect(ref, nothing |> Agg(args = (local args), filter = (local filter)) |> Nested(name = (local name))) && name === n.name) || + (@dissect(ref, nothing |> Agg(args = (local args), filter = (local filter))) && n.name === nothing) gather!(args, ctx′) if filter !== nothing gather!(filter, ctx′) diff --git a/src/nodes.jl b/src/nodes.jl index 58d955a8..574938e0 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -506,23 +506,23 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) else return QuoteNode(ex) end - elseif @dissect(ex, QuoteNode(name::Symbol)) + elseif @dissect(ex, QuoteNode((local name)::Symbol)) # :name return :(Var($ex)) elseif ex isa Expr - if @dissect(ex, Expr(:($), arg)) + if @dissect(ex, Expr(:($), (local arg))) # $(...) return esc(arg) - elseif @dissect(ex, Expr(:macrocall, ref := GlobalRef(Core, Symbol("@doc")), ln::LineNumberNode, doc, arg)) + elseif @dissect(ex, Expr(:macrocall, (local ref = GlobalRef($Core, $(Symbol("@doc")))), (local ln)::LineNumberNode, (local doc), (local arg))) # "..." ... - if @dissect(arg, name::Symbol || Expr(:macrocall, GlobalRef(Core, Symbol("@cmd")), ::LineNumberNode, name::String)) + if @dissect(arg, (local name)::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String)) arg = Symbol("funsql_$name") else ctx = TransliterateContext(ctx, src = ln) arg = transliterate(arg, ctx) end return Expr(:macrocall, ref, ln, doc, arg) - elseif @dissect(ex, Expr(:(=), Expr(:call, name::Symbol || Expr(:macrocall, GlobalRef(Core, Symbol("@cmd")), ::LineNumberNode, name::String), args...), body)) + elseif @dissect(ex, Expr(:(=), Expr(:call, (local name)::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String), (local args)...), (local body))) # name(args...) = body ctx = TransliterateContext(ctx, decl = true) trs = Any[transliterate(arg, ctx) for arg in args] @@ -530,109 +530,109 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) return Expr(:(=), :($(esc(Symbol("funsql_$name")))($(trs...))), transliterate(body, ctx)) - elseif @dissect(ex, Expr(:(=), name::Symbol, arg)) + elseif @dissect(ex, Expr(:(=), (local name)::Symbol, (local arg))) # name = arg return Expr(:(=), esc(name), transliterate(arg, ctx)) elseif ctx.decl && @dissect(ex, Expr(:(::), _::Symbol, _)) # name::t return esc(ex) - elseif @dissect(ex, Expr(:kw, key, arg)) + elseif @dissect(ex, Expr(:kw, (local key), (local arg))) # key = arg ctx = TransliterateContext(ctx, decl = true) ctx′ = TransliterateContext(ctx, decl = false) return Expr(:kw, transliterate(key, ctx), transliterate(arg, ctx′)) - elseif @dissect(ex, Expr(:(...), arg)) + elseif @dissect(ex, Expr(:(...), (local arg))) # arg... return Expr(:(...), transliterate(arg, ctx)) - elseif @dissect(ex, Expr(:parameters, args...)) + elseif @dissect(ex, Expr(:parameters, (local args)...)) # ; args... return Expr(:parameters, Any[transliterate(arg, ctx) for arg in args]...) - elseif @dissect(ex, Expr(:macrocall, GlobalRef(Core, Symbol("@cmd")), ::LineNumberNode, name::String)) + elseif @dissect(ex, Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String)) # `name` return QuoteNode(Symbol(name)) - elseif @dissect(ex, Expr(:call, Expr(:., over, QuoteNode(name)), args...)) + elseif @dissect(ex, Expr(:call, Expr(:., (local over), QuoteNode(local name)), (local args)...)) # over.name(args...) tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:call, name, args...), ctx) return :(Chain($tr1, $tr2)) - elseif @dissect(ex, Expr(:macrocall, Expr(:., over, Expr(:quote, ex′)), args...)) + elseif @dissect(ex, Expr(:macrocall, Expr(:., (local over), Expr(:quote, (local ex′))), (local args)...)) # over.`name` tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:macrocall, ex′, args...), ctx) return :(Chain($tr1, $tr2)) - elseif @dissect(ex, Expr(:., over, Expr(:quote, arg))) + elseif @dissect(ex, Expr(:., (local over), Expr(:quote, (local arg)))) # over.`name` (Julia ≥ 1.10) tr1 = transliterate(over, ctx) tr2 = transliterate(arg, ctx) return :(Chain($tr1, $tr2)) - elseif @dissect(ex, Expr(:call, Expr(:macrocall, Expr(:., over, Expr(:quote, ex′)), args...), args′...)) + elseif @dissect(ex, Expr(:call, Expr(:macrocall, Expr(:., (local over), Expr(:quote, (local ex′))), (local args)...), (local args′)...)) # over.`name`(args...) tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:call, Expr(:macrocall, ex′, args...), args′...), ctx) return :(Chain($tr1, $tr2)) - elseif @dissect(ex, Expr(:call, Expr(:., over, Expr(:quote, arg)), args...)) + elseif @dissect(ex, Expr(:call, Expr(:., (local over), Expr(:quote, (local arg))), (local args)...)) # over.`name`(args...) (Julia ≥ 1.10) tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:call, arg, args...), ctx) return :(Chain($tr1, $tr2)) - elseif @dissect(ex, Expr(:., over, QuoteNode(name))) + elseif @dissect(ex, Expr(:., (local over), QuoteNode((local name)))) # over.name tr1 = transliterate(over, ctx) tr2 = transliterate(name, ctx) return :(Chain($tr1, $tr2)) - elseif @dissect(ex, Expr(:call, :(=>), name := QuoteNode(_::Symbol), arg)) + elseif @dissect(ex, Expr(:call, :(=>), (local name = QuoteNode(_::Symbol)), (local arg))) # :name => arg tr = transliterate(arg, ctx) return :($name => $tr) - elseif @dissect(ex, Expr(:call, :(=>), name, arg)) + elseif @dissect(ex, Expr(:call, :(=>), (local name), (local arg))) # name => arg tr1 = transliterate(name, ctx) tr2 = transliterate(arg, ctx) return :($tr1 => $tr2) - elseif @dissect(ex, Expr(:call, :(:), arg1, arg2)) + elseif @dissect(ex, Expr(:call, :(:), (local arg1), (local arg2))) tr1 = transliterate(arg1, ctx) tr2 = transliterate(arg2, ctx) return :($tr1:$tr2) - elseif @dissect(ex, Expr(:vect, args...)) + elseif @dissect(ex, Expr(:vect, (local args)...)) # [args...] return Expr(:vect, Any[transliterate(arg, ctx) for arg in args]...) - elseif @dissect(ex, Expr(:tuple, args...)) + elseif @dissect(ex, Expr(:tuple, (local args)...)) # (args...) return Expr(:tuple, Any[transliterate(arg, ctx) for arg in args]...) - elseif @dissect(ex, Expr(:comparison, arg1, arg2::Symbol, arg3)) + elseif @dissect(ex, Expr(:comparison, (local arg1), (local arg2)::Symbol, (local arg3))) # Chained comparison. tr1 = transliterate(arg1, ctx) tr2 = transliterate(arg3, ctx) return :($(esc(Symbol("funsql_$arg2")))($tr1, $tr2)) - elseif @dissect(ex, Expr(:comparison, arg1, arg2::Symbol, arg3, args...)) + elseif @dissect(ex, Expr(:comparison, (local arg1), (local arg2)::Symbol, (local arg3), (local args)...)) # Chained comparison. tr1 = transliterate(arg1, ctx) tr2 = transliterate(arg3, ctx) tr3 = transliterate(Expr(:comparison, arg3, args...), ctx) return :(Fun(:and, $(esc(Symbol("funsql_$arg2")))($tr1, $tr2), $tr3)) - elseif @dissect(ex, Expr(:(&&), args...)) + elseif @dissect(ex, Expr(:(&&), (local args)...)) # &&(args...) trs = Any[transliterate(arg, ctx) for arg in args] return :(Fun(:and, args = [$(trs...)])) - elseif @dissect(ex, Expr(:(||), args...)) + elseif @dissect(ex, Expr(:(||), (local args)...)) # ||(args...) trs = Any[transliterate(arg, ctx) for arg in args] return :(Fun(:or, args = [$(trs...)])) - elseif @dissect(ex, Expr(:call, op := :+ || :-, arg := :Inf)) + elseif @dissect(ex, Expr(:call, (local op = :+ || :-), (local arg = :Inf))) # ±Inf tr = transliterate(arg, ctx) return Expr(:call, op, tr) - elseif @dissect(ex, Expr(:call, name::Symbol, args...)) + elseif @dissect(ex, Expr(:call, (local name)::Symbol, (local args)...)) # name(args...) trs = Any[transliterate(arg, ctx) for arg in args] return :($(esc(Symbol("funsql_$name")))($(trs...))) - elseif @dissect(ex, Expr(:call, Expr(:macrocall, GlobalRef(Core, Symbol("@cmd")), ::LineNumberNode, name::String), args...)) + elseif @dissect(ex, Expr(:call, Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String), (local args)...)) # `name`(args...) trs = Any[transliterate(arg, ctx) for arg in args] return :($(esc(Symbol("funsql_$name")))($(trs...))) - elseif @dissect(ex, Expr(:block, args...)) + elseif @dissect(ex, Expr(:block, (local args)...)) # begin; args...; end - if all(@dissect(arg, ::LineNumberNode || Expr(:(=), _...) || Expr(:macrocall, GlobalRef(Core, Symbol("@doc")), _...)) + if all(@dissect(arg, ::LineNumberNode || Expr(:(=), _...) || Expr(:macrocall, GlobalRef($Core, $(Symbol("@doc"))), _...)) for arg in args) trs = Any[] for arg in args @@ -656,20 +656,20 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) end return tr end - elseif @dissect(ex, Expr(:if, arg1, arg2)) + elseif @dissect(ex, Expr(:if, (local arg1), (local arg2))) tr1 = transliterate(arg1, ctx) tr2 = transliterate(arg2, ctx) return :(Fun(:case, $tr1, $tr2)) - elseif @dissect(ex, Expr(:if, arg1, arg2, arg3)) + elseif @dissect(ex, Expr(:if, (local arg1), (local arg2), (local arg3))) trs = Any[transliterate(arg1, ctx), transliterate(arg2, ctx)] - while @dissect(arg3, Expr(:if || :elseif, arg1′, arg2′, arg3′)) + while @dissect(arg3, Expr(:if || :elseif, (local arg1′), (local arg2′), (local arg3′))) push!(trs, transliterate(arg1′, ctx), transliterate(arg2′, ctx)) arg3 = arg3′ end - if @dissect(arg3, Expr(:if || :elseif, arg1′, arg2′)) + if @dissect(arg3, Expr(:if || :elseif, (local arg1′), (local arg2′))) push!(trs, transliterate(arg1′, ctx), transliterate(arg2′, ctx)) diff --git a/src/resolve.jl b/src/resolve.jl index b2097429..a7b0c3f9 100644 --- a/src/resolve.jl +++ b/src/resolve.jl @@ -38,22 +38,22 @@ get_path(ctx::ResolveContext) = copy(ctx.path) function row_type(n::SQLNode) - @dissect(n, Resolved(type = type::RowType)) || throw(IllFormedError()) + @dissect(n, Resolved(type = (local type)::RowType)) || throw(IllFormedError()) type end function scalar_type(n::SQLNode) - @dissect(n, Resolved(type = type::ScalarType)) || throw(IllFormedError()) + @dissect(n, Resolved(type = (local type)::ScalarType)) || throw(IllFormedError()) type end function type(n::SQLNode) - @dissect(n, Resolved(type = t)) || throw(IllFormedError()) + @dissect(n, Resolved(type = (local t))) || throw(IllFormedError()) t end function resolve(n::SQLNode) - @dissect(n, WithContext(over = n′, catalog = catalog)) || throw(IllFormedError()) + @dissect(n, WithContext(over = (local n′), catalog = (local catalog))) || throw(IllFormedError()) ctx = ResolveContext(catalog) WithContext(over = resolve(n′, ctx), catalog = catalog) end @@ -107,7 +107,7 @@ function resolve_scalar(n::TabularNode, ctx) end function unnest(node, base, ctx) - while @dissect(node, over |> Get(name = name)) + while @dissect(node, (local over) |> Get(name = (local name))) base = Nested(over = base, name = name) node = over end diff --git a/src/serialize.jl b/src/serialize.jl index bebb4dda..d9657c4f 100644 --- a/src/serialize.jl +++ b/src/serialize.jl @@ -12,7 +12,7 @@ mutable struct SerializeContext <: IO end function serialize(c::SQLClause) - @dissect(c, WITH_CONTEXT(over = c′, dialect = dialect, columns = columns)) || throw(IllFormedError()) + @dissect(c, WITH_CONTEXT(over = (local c′), dialect = (local dialect), columns = (local columns))) || throw(IllFormedError()) ctx = SerializeContext(dialect) serialize!(c′, ctx) raw = String(take!(ctx.io)) @@ -384,7 +384,7 @@ end arity(::Val{:count_distinct}) = 0:1 function serialize!(::Val{:cast}, args::Vector{SQLClause}, ctx) - if length(args) == 2 && @dissect(args[2], LIT(val = t)) && t isa AbstractString + if length(args) == 2 && @dissect(args[2], LIT(val = (local t))) && t isa AbstractString print(ctx, "CAST(") serialize!(args[1], ctx) print(ctx, " AS ", t, ')') @@ -407,7 +407,7 @@ end arity(::Val{:concat}) = 2 function serialize!(::Val{:extract}, args::Vector{SQLClause}, ctx) - if length(args) == 2 && @dissect(args[1], LIT(val = f)) && f isa AbstractString + if length(args) == 2 && @dissect(args[1], LIT(val = (local f))) && f isa AbstractString print(ctx, "EXTRACT(", f, " FROM ") serialize!(args[2], ctx) print(ctx, ')') @@ -577,9 +577,9 @@ function serialize!(c::HavingClause, ctx) end newline(ctx) print(ctx, "HAVING") - if @dissect(c.condition, FUN(name = :and, args = args)) && length(args) >= 2 + if @dissect(c.condition, FUN(name = :and, args = (local args))) && length(args) >= 2 serialize_lines!(args, ctx, sep = "AND") - elseif @dissect(c.condition, FUN(name = :or, args = args)) && length(args) >= 2 + elseif @dissect(c.condition, FUN(name = :or, args = (local args))) && length(args) >= 2 serialize_lines!(args, ctx, sep = "OR") else print(ctx, ' ') @@ -807,7 +807,7 @@ function serialize!(c::SelectClause, ctx) limit = top.limit with_ties = top.with_ties elseif ctx.dialect.limit_style === LIMIT_STYLE.SQLSERVER - if @dissect(over, limit_over |> LIMIT(offset = nothing, limit = limit, with_ties = with_ties)) + if @dissect(over, (local limit_over) |> LIMIT(offset = nothing, limit = (local limit), with_ties = (local with_ties))) over = limit_over elseif nested && @dissect(over, ORDER()) offset_0_rows = true @@ -966,9 +966,9 @@ function serialize!(c::WhereClause, ctx) end newline(ctx) print(ctx, "WHERE") - if @dissect(c.condition, FUN(name = :and, args = args)) && length(args) >= 2 + if @dissect(c.condition, FUN(name = :and, args = (local args))) && length(args) >= 2 serialize_lines!(args, ctx, sep = "AND") - elseif @dissect(c.condition, FUN(name = :or, args = args)) && length(args) >= 2 + elseif @dissect(c.condition, FUN(name = :or, args = (local args))) && length(args) >= 2 serialize_lines!(args, ctx, sep = "OR") else print(ctx, ' ') @@ -1001,7 +1001,7 @@ function serialize!(c::WithClause, ctx) else first = false end - if @dissect(arg, AS(name = name, columns = columns, over = arg)) + if @dissect(arg, AS(name = (local name), columns = (local columns), over = (local arg))) serialize!(name, ctx) if columns !== nothing print(ctx, " (") diff --git a/src/translate.jl b/src/translate.jl index cc2f2128..cc997a26 100644 --- a/src/translate.jl +++ b/src/translate.jl @@ -16,7 +16,7 @@ end function complete(cols::OrderedDict{Symbol, SQLClause}) args = SQLClause[] for (name, col) in cols - if !(@dissect(col, ID(name = id_name)) && id_name == name) + if !(@dissect(col, ID(name = (local id_name))) && id_name == name) col = AS(over = col, name = name) end push!(args, col) @@ -139,7 +139,7 @@ end function aligned_columns(refs, repl, args) length(refs) == length(args) || return false for (ref, arg) in zip(refs, args) - if !(@dissect(arg, ID(name = name) || AS(name = name)) && name === repl[ref]) + if !(@dissect(arg, ID(name = (local name)) || AS(name = (local name))) && name === repl[ref]) return false end end @@ -208,7 +208,7 @@ function allocate_alias(ctx::TranslateContext, alias::Symbol) end function translate(n::SQLNode) - @dissect(n, WithContext(over = Linked(over = n′, refs = refs), catalog = catalog, defs = defs)) || throw(IllFormedError()) + @dissect(n, WithContext(over = Linked(over = (local n′), refs = (local refs)), catalog = (local catalog), defs = (local defs))) || throw(IllFormedError()) ctx = TranslateContext(catalog = catalog, defs = defs) ctx′ = TranslateContext(ctx, refs = refs) base = assemble(n′, ctx′) @@ -288,7 +288,7 @@ function translate(n::FunctionNode, ctx) args′ = SQLClause[] for arg in args if @dissect(arg, LIT(val = true)) - elseif @dissect(arg, FUN(name = :and, args = args′′)) + elseif @dissect(arg, FUN(name = :and, args = (local args′′))) append!(args′, args′′) else push!(args′, arg) @@ -304,7 +304,7 @@ function translate(n::FunctionNode, ctx) args′ = SQLClause[] for arg in args if @dissect(arg, LIT(val = false)) - elseif @dissect(arg, FUN(name = :or, args = args′′)) + elseif @dissect(arg, FUN(name = :or, args = (local args′′))) append!(args′, args′′) else push!(args′, arg) @@ -317,7 +317,7 @@ function translate(n::FunctionNode, ctx) return args[1] end elseif n.name === :not - if length(args) == 1 && @dissect(args[1], LIT(val = val)) && val isa Bool + if length(args) == 1 && @dissect(args[1], LIT(val = (local val))) && val isa Bool return LIT(!val) end end @@ -394,7 +394,7 @@ function assemble(n::AppendNode, ctx) if a.name !== a_name a_name = :union end - if @dissect(a.clause, over |> SELECT(args = args)) && aligned_columns(urefs, repl, args) && !@dissect(over, ORDER() || LIMIT()) + if @dissect(a.clause, (local over) |> SELECT(args = (local args))) && aligned_columns(urefs, repl, args) && !@dissect(over, ORDER() || LIMIT()) push!(cs, a.clause) continue elseif !@dissect(a.clause, SELECT() || UNION() || ORDER() || LIMIT()) @@ -420,7 +420,7 @@ end function assemble(n::AsNode, ctx) refs′ = SQLNode[] for ref in ctx.refs - if @dissect(ref, over |> Nested()) + if @dissect(ref, (local over) |> Nested()) push!(refs′, over) else push!(refs′, ref) @@ -429,7 +429,7 @@ function assemble(n::AsNode, ctx) base = assemble(n.over, TranslateContext(ctx, refs = refs′)) repl′ = Dict{SQLNode, Symbol}() for ref in ctx.refs - if @dissect(ref, over |> Nested()) + if @dissect(ref, (local over) |> Nested()) repl′[ref] = base.repl[over] else repl′[ref] = base.repl[ref] @@ -465,7 +465,7 @@ function assemble(n::DefineNode, ctx) repl = Dict{SQLNode, Symbol}() trns = Pair{SQLNode, SQLClause}[] for ref in ctx.refs - if @dissect(ref, nothing |> Get(name = name)) && name in keys(tr_cache) + if @dissect(ref, nothing |> Get(name = (local name))) && name in keys(tr_cache) push!(trns, ref => tr_cache[name]) else push!(trns, ref => subs[ref]) @@ -479,7 +479,7 @@ function assemble(n::FromFunctionNode, ctx) seen = Set{Symbol}() column_set = Set(n.columns) for ref in ctx.refs - @dissect(ref, nothing |> Get(name = name)) && name in column_set || error() + @dissect(ref, nothing |> Get(name = (local name))) && name in column_set || error() if !(name in seen) push!(seen, name) end @@ -494,7 +494,7 @@ function assemble(n::FromFunctionNode, ctx) end repl = Dict{SQLNode, Symbol}() for ref in ctx.refs - if @dissect(ref, nothing |> Get(name = name)) + if @dissect(ref, nothing |> Get(name = (local name))) repl[ref] = name end end @@ -522,7 +522,7 @@ assemble(::FromNothingNode, ctx) = function unwrap_repl(a::Assemblage) repl′ = Dict{SQLNode, Symbol}() for (ref, name) in a.repl - @dissect(ref, over |> Nested()) || error() + @dissect(ref, (local over) |> Nested()) || error() repl′[over] = name end Assemblage(a.name, a.clause, cols = a.cols, repl = repl′) @@ -545,7 +545,7 @@ end function assemble(n::FromTableNode, ctx) seen = Set{Symbol}() for ref in ctx.refs - @dissect(ref, nothing |> Get(name = name)) && name in keys(n.table.columns) || error() + @dissect(ref, nothing |> Get(name = (local name))) && name in keys(n.table.columns) || error() if !(name in seen) push!(seen, name) end @@ -560,7 +560,7 @@ function assemble(n::FromTableNode, ctx) end repl = Dict{SQLNode, Symbol}() for ref in ctx.refs - if @dissect(ref, nothing |> Get(name = name)) + if @dissect(ref, nothing |> Get(name = (local name))) repl[ref] = name end end @@ -572,7 +572,7 @@ function assemble(n::FromValuesNode, ctx) column_set = Set{Symbol}(columns) seen = Set{Symbol}() for ref in ctx.refs - @dissect(ref, nothing |> Get(name = name)) && name in column_set || error() + @dissect(ref, nothing |> Get(name = (local name))) && name in column_set || error() if !(name in seen) push!(seen, name) end @@ -615,7 +615,7 @@ function assemble(n::FromValuesNode, ctx) end repl = Dict{SQLNode, Symbol}() for ref in ctx.refs - if @dissect(ref, nothing |> Get(name = name)) + if @dissect(ref, nothing |> Get(name = (local name))) repl[ref] = name end end @@ -628,7 +628,7 @@ function assemble(n::GroupNode, ctx) return assemble(nothing, ctx) end base = assemble(n.over, ctx) - if @dissect(base.clause, tail := nothing || FROM() || JOIN() || WHERE()) + if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE()) base_alias = nothing else base_alias = allocate_alias(ctx, base) @@ -638,12 +638,12 @@ function assemble(n::GroupNode, ctx) by = SQLClause[subs[key] for key in n.by] trns = Pair{SQLNode, SQLClause}[] for ref in ctx.refs - if @dissect(ref, nothing |> Get(name = name)) + if @dissect(ref, nothing |> Get(name = (local name))) @assert name in keys(n.label_map) push!(trns, ref => by[n.label_map[name]]) elseif @dissect(ref, nothing |> Agg()) push!(trns, ref => translate(ref, ctx, subs)) - elseif @dissect(ref, (over := nothing |> Agg()) |> Nested()) + elseif @dissect(ref, (local over = nothing |> Agg()) |> Nested()) push!(trns, ref => translate(over, ctx, subs)) end end @@ -695,7 +695,7 @@ function assemble(n::IterateNode, ctx) end cs = SQLClause[] for (arg, a) in (n.over => left, n.iterator => right) - if @dissect(a.clause, over |> SELECT(args = args)) && aligned_columns(urefs, repl, args) && !@dissect(over, ORDER() || LIMIT()) + if @dissect(a.clause, (local over) |> SELECT(args = (local args))) && aligned_columns(urefs, repl, args) && !@dissect(over, ORDER() || LIMIT()) push!(cs, a.clause) continue elseif !@dissect(a.clause, SELECT() || UNION() || ORDER() || LIMIT()) @@ -736,7 +736,7 @@ end function assemble(n::LimitNode, ctx) base = assemble(n.over, ctx) - if @dissect(base.clause, tail := nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING() || ORDER()) + if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING() || ORDER()) base_alias = nothing else base_alias = allocate_alias(ctx, base) @@ -780,7 +780,7 @@ end function assemble(n::OrderNode, ctx) base = assemble(n.over, ctx) @assert !isempty(n.by) - if @dissect(base.clause, tail := nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) + if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) base_alias = nothing else base_alias = allocate_alias(ctx, base) @@ -821,7 +821,7 @@ end function assemble(n::PartitionNode, ctx) base = assemble(n.over, ctx) - if @dissect(base.clause, tail := nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) + if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) base_alias = nothing else base_alias = allocate_alias(ctx, base) @@ -839,7 +839,7 @@ function assemble(n::PartitionNode, ctx) if @dissect(ref, nothing |> Agg()) && n.name === nothing push!(trns, ref => partition |> translate(ref, ctx′)) has_aggregates = true - elseif @dissect(ref, (over := nothing |> Agg()) |> Nested(name = name)) && name === n.name + elseif @dissect(ref, (local over = nothing |> Agg()) |> Nested(name = (local name))) && name === n.name push!(trns, ref => partition |> translate(over, ctx′)) has_aggregates = true else @@ -856,7 +856,7 @@ _outer_safe(a::Assemblage) = function assemble(n::RoutedJoinNode, ctx) left = assemble(n.over, ctx) - if @dissect(left.clause, tail := FROM() || JOIN()) && (!n.right || _outer_safe(left)) + if @dissect(left.clause, local tail = FROM() || JOIN()) && (!n.right || _outer_safe(left)) left_alias = nothing else left_alias = allocate_alias(ctx, left) @@ -869,7 +869,7 @@ function assemble(n::RoutedJoinNode, ctx) else right = assemble(n.joinee, ctx) end - if @dissect(right.clause, (joinee := (ID() || AS())) |> FROM()) && (!n.left || _outer_safe(right)) + if @dissect(right.clause, (local joinee = (ID() || AS())) |> FROM()) && (!n.left || _outer_safe(right)) for (ref, name) in right.repl subs[ref] = right.cols[name] end @@ -915,20 +915,20 @@ function assemble(n::SelectNode, ctx) cols = OrderedDict{Symbol, SQLClause}([name => ID(name) for name in keys(cols)]) repl = Dict{SQLNode, Symbol}() for ref in ctx.refs - @dissect(ref, nothing |> Get(name = name)) || error() + @dissect(ref, nothing |> Get(name = (local name))) || error() repl[ref] = name end Assemblage(base.name, c, cols = cols, repl = repl) end function merge_conditions(c1, c2) - if @dissect(c1, FUN(name = :and, args = args1)) - if @dissect(c2, FUN(name = :and, args = args2)) + if @dissect(c1, FUN(name = :and, args = (local args1))) + if @dissect(c2, FUN(name = :and, args = (local args2))) return FUN(:and, args1..., args2...) else return FUN(:and, args1..., c2) end - elseif @dissect(c2, FUN(name = :and, args = args2)) + elseif @dissect(c2, FUN(name = :and, args = (local args2))) return FUN(:and, c1, args2...) else return FUN(:and, c1, c2) @@ -938,18 +938,18 @@ end function assemble(n::WhereNode, ctx) base = assemble(n.over, ctx) if @dissect(base.clause, nothing || FROM() || JOIN() || WHERE() || HAVING()) || - @dissect(base.clause, GROUP(by = by)) && !isempty(by) + @dissect(base.clause, GROUP(by = (local by))) && !isempty(by) subs = make_subs(base, nothing) condition = translate(n.condition, ctx, subs) if @dissect(condition, LIT(val = true)) return base end - if @dissect(base.clause, tail |> WHERE(condition = tail_condition)) + if @dissect(base.clause, (local tail) |> WHERE(condition = (local tail_condition))) condition = merge_conditions(tail_condition, condition) c = WHERE(over = tail, condition = condition) elseif @dissect(base.clause, GROUP()) c = HAVING(over = base.clause, condition = condition) - elseif @dissect(base.clause, tail |> HAVING(condition = tail_condition)) + elseif @dissect(base.clause, (local tail) |> HAVING(condition = (local tail_condition))) condition = merge_conditions(tail_condition, condition) c = HAVING(over = tail, condition = condition) else From c04578fe3ff55c92cc299c0a9e5c813bb4cdf8d5 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sun, 18 May 2025 17:12:46 -0500 Subject: [PATCH 02/17] Bump the minimum Julia version to the current LTS --- .github/workflows/ci.yml | 4 ++-- Project.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c67caa40..b2b69708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: matrix: include: - name: Julia LTS / Linux 64bit - version: '1.6' + version: '1.10' os: ubuntu-latest arch: x64 - name: Julia LTS / Linux 32bit - version: '1.6' + version: '1.10' os: ubuntu-latest arch: x86 - name: Julia Stable / Linux 64bit diff --git a/Project.toml b/Project.toml index 89ef4d80..2d76c69e 100644 --- a/Project.toml +++ b/Project.toml @@ -19,4 +19,4 @@ LRUCache = "1.3" OrderedCollections = "1.4" PrettyPrinting = "0.3.2, 0.4" Tables = "1.6" -julia = "1.6" +julia = "1.10" From 0202920047c3f46002dd6466b67abafd4cf538da Mon Sep 17 00:00:00 2001 From: "Clark C. Evans" Date: Sun, 18 May 2025 17:35:30 -0500 Subject: [PATCH 03/17] FunSQL.reflect() includes table_catalog for DuckDB (#87) * reflect includes table_catalog * Update reflect() docstring * DBInterface.connect(): add catalog parameter --------- Co-authored-by: Kyrylo Simonov --- src/connections.jl | 5 ++-- src/reflect.jl | 58 ++++++++++++++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/connections.jl b/src/connections.jl index fa068f25..dc1da7af 100644 --- a/src/connections.jl +++ b/src/connections.jl @@ -62,6 +62,7 @@ const DB = SQLConnection """ DBInterface.connect(DB{RawConnType}, args...; + catalog = nothing, schema = nothing, dialect = nothing, cache = $default_cache_maxsize, @@ -75,13 +76,13 @@ Extra parameters `args` and `kws` are passed to the call: DBInterface.connect(RawConnType, args...; kws...) """ function DBInterface.connect(::Type{SQLConnection{RawConnType}}, args...; + catalog = nothing, schema = nothing, dialect = nothing, cache = default_cache_maxsize, kws...) where {RawConnType} raw = DBInterface.connect(RawConnType, args...; kws...) - catalog = reflect(raw, schema = schema, dialect = dialect, cache = cache) - SQLConnection{RawConnType}(raw, catalog = catalog) + SQLConnection{RawConnType}(raw; catalog = reflect(raw; catalog, schema, dialect, cache)) end """ diff --git a/src/reflect.jl b/src/reflect.jl index d90a7570..69c5270c 100644 --- a/src/reflect.jl +++ b/src/reflect.jl @@ -2,25 +2,31 @@ const default_reflect_clause = FROM(:c => (:information_schema, :columns)) |> - WHERE(FUN("=", (:c, :table_schema), VAR(:schema))) |> - ORDER((:c, :table_schema), (:c, :table_name), (:c, :ordinal_position)) |> - SELECT(:schema => (:c, :table_schema), + WHERE(FUN(:and, FUN("=", (:c, :table_catalog), VAR(:catalog)), + FUN("=", (:c, :table_schema), VAR(:schema)) + )) |> + ORDER((:c, :table_catalog), (:c, :table_schema), (:c, :table_name), (:c, :ordinal_position)) |> + SELECT(:catalog => (:c, :table_catalog), + :schema => (:c, :table_schema), :name => (:c, :table_name), :column => (:c, :column_name)) const duckdb_reflect_clause = FROM(:c => (:information_schema, :columns)) |> - WHERE(FUN(:and, FUN("=", (:c, :table_schema), FUN(:coalesce, VAR(:schema), "main")), + WHERE(FUN(:and, FUN("=", (:c, :table_catalog), FUN(:coalesce, VAR(:catalog), FUN(:current_catalog))), + FUN("=", (:c, :table_schema), FUN(:coalesce, VAR(:schema), FUN(:current_schema))), FUN(:not_like, (:c, :table_name), "sqlite_%"), FUN(:not_like, (:c, :table_name), "pragma_database_list"))) |> - ORDER((:c, :table_schema), (:c, :table_name), (:c, :ordinal_position)) |> - SELECT(:schema => (:c, :table_schema), + ORDER((:c, :table_catalog), (:c, :table_schema), (:c, :table_name), (:c, :ordinal_position)) |> + SELECT(:catalog => (:c, :table_catalog), + :schema => (:c, :table_schema), :name => (:c, :table_name), :column => (:c, :column_name)) const mysql_reflect_clause = FROM(:c => (:information_schema, :columns)) |> WHERE(FUN("=", (:c, :table_schema), FUN(:coalesce, VAR(:schema), FUN("DATABASE")))) |> ORDER((:c, :table_schema), (:c, :table_name), (:c, :ordinal_position)) |> - SELECT(:schema => (:c, :table_schema), + SELECT(:catalog => missing, + :schema => (:c, :table_schema), :name => (:c, :table_name), :column => (:c, :column_name)) const postgresql_reflect_clause = @@ -32,7 +38,8 @@ const postgresql_reflect_clause = FUN(">", (:a, :attnum), 0), FUN(:not, (:a, :attisdropped)))) |> ORDER((:n, :nspname), (:c, :relname), (:a, :attnum)) |> - SELECT(:schema => (:n, :nspname), + SELECT(:catalog => missing, + :schema => (:n, :nspname), :name => (:c, :relname), :column => (:a, :attname)) const redshift_reflect_clause = postgresql_reflect_clause @@ -42,7 +49,8 @@ const sqlite_reflect_clause = WHERE(FUN(:and, FUN(:in, (:sm, :type), "table", "view"), FUN(:not_like, (:sm, :name), "sqlite_%"))) |> ORDER((:sm, :name), (:pti, :cid)) |> - SELECT(:schema => missing, + SELECT(:catalog => missing, + :schema => missing, :name => (:sm, :name), :column => (:pti, :name)) const sqlserver_reflect_clause = @@ -52,7 +60,8 @@ const sqlserver_reflect_clause = WHERE(FUN(:and, FUN("=", (:s, :name), FUN(:coalesce, VAR(:schema), "dbo")), FUN(:in, (:o, :type), "U", "V"))) |> ORDER((:s, :name), (:o, :name), (:c, :column_id)) |> - SELECT(:schema => (:s, :name), + SELECT(:catalog => missing, + :schema => (:s, :name), :name => (:o, :name), :column => (:c, :name)) const standard_reflect_clauses = [ @@ -77,25 +86,26 @@ reflect_sql(d::SQLDialect) = """ reflect(conn; + catalog = nothing, schema = nothing, dialect = nothing, cache = $default_cache_maxsize)::SQLCatalog Retrieve the information about available database tables. -The function returns a [`SQLCatalog`](@ref) object. The catalog -will be populated with the tables from the given database `schema`, or, -if parameter `schema` is not set, from the default database schema +The function returns a [`SQLCatalog`](@ref) object. The catalog will be +populated with the tables from the given database `catalog` and `schema`. +If these parameters are not set, the default catalog and schema are assumed (e.g., schema `public` for PostgreSQL). Parameter `dialect` specifies the target [`SQLDialect`](@ref). If not set, `dialect` will be inferred from the type of the connection object. """ -function reflect(conn; schema = nothing, dialect = nothing, cache = default_cache_maxsize) +function reflect(conn; catalog = nothing, schema = nothing, dialect = nothing, cache = default_cache_maxsize) dialect = dialect === nothing ? SQLDialect(typeof(conn)) : convert(SQLDialect, dialect) sql = reflect_sql(dialect) - params = pack(sql, (; schema = something(schema, missing))) + params = pack(sql, (; catalog = something(catalog, missing), schema = something(schema, missing))) stmt = DBInterface.prepare(conn, String(sql)) cr = DBInterface.execute(stmt, params) SQLCatalog(tables = tables_from_column_list(Tables.rows(cr)), @@ -106,22 +116,30 @@ end function tables_from_column_list(rows) tables = SQLTable[] qualifiers = Symbol[] - schema = name = nothing + catalog = schema = name = nothing columns = Symbol[] - for (s, n, c) in rows + for (g, s, n, c) in rows + g = g !== missing ? Symbol(g) : nothing s = s !== missing ? Symbol(s) : nothing n = Symbol(n) c = Symbol(c) - if s === schema && n === name + if g === catalog && s === schema && n === name push!(columns, c) else if !isempty(columns) t = SQLTable(qualifiers = qualifiers, name = name, columns = columns) push!(tables, t) end - if s !== schema - qualifiers = s !== nothing ? [s] : Symbol[] + if s !== schema || g !== catalog + qualifiers = Symbol[] + if !isnothing(g) + push!(qualifiers, g) + end + if !isnothing(s) + push!(qualifiers, s) + end end + catalog = g schema = s name = n columns = [c] From becdb3d3c37560727a1355f71f44e2d41e0771b4 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sun, 20 Apr 2025 14:48:37 -0500 Subject: [PATCH 04/17] Convert syntax nodes to an explicit linked list --- docs/src/test/clauses.md | 599 ++++++++++++++++++++------------------ docs/src/test/nodes.md | 68 +++-- src/clauses.jl | 130 +++++---- src/clauses/aggregate.jl | 37 +-- src/clauses/as.jl | 40 +-- src/clauses/from.jl | 35 +-- src/clauses/function.jl | 34 +-- src/clauses/group.jl | 35 +-- src/clauses/having.jl | 40 +-- src/clauses/identifier.jl | 83 +++--- src/clauses/internal.jl | 13 +- src/clauses/join.jl | 42 +-- src/clauses/limit.jl | 43 +-- src/clauses/literal.jl | 24 +- src/clauses/note.jl | 32 +- src/clauses/order.jl | 36 +-- src/clauses/partition.jl | 44 +-- src/clauses/select.jl | 37 +-- src/clauses/sort.jl | 43 +-- src/clauses/union.jl | 33 +-- src/clauses/values.jl | 14 +- src/clauses/variable.jl | 14 +- src/clauses/where.jl | 40 +-- src/clauses/window.jl | 36 +-- src/clauses/with.jl | 37 +-- src/connections.jl | 12 +- src/dissect.jl | 2 +- src/nodes.jl | 8 + src/render.jl | 28 +- src/serialize.jl | 194 ++++++------ src/translate.jl | 358 ++++++++++++----------- 31 files changed, 991 insertions(+), 1200 deletions(-) diff --git a/docs/src/test/clauses.md b/docs/src/test/clauses.md index e147c6dd..9cbe298e 100644 --- a/docs/src/test/clauses.md +++ b/docs/src/test/clauses.md @@ -3,33 +3,33 @@ using FunSQL: AGG, AS, ASC, DESC, FROM, FUN, GROUP, HAVING, ID, JOIN, LIMIT, LIT, NOTE, ORDER, PARTITION, SELECT, SORT, UNION, VALUES, VAR, WHERE, - WINDOW, WITH, pack, render + WINDOW, WITH, SQLTable, pack, render -The syntactic structure of a SQL query is represented as a tree of `SQLClause` -objects. Different types of clauses are created by specialized constructors +The syntactic structure of a SQL query is represented as a tree of `SQLSyntax` +objects. Different types of syntax nodes are created by specialized constructors and connected using the chain (`|>`) operator. - c = FROM(:person) |> + s = FROM(:person) |> SELECT(:person_id, :year_of_birth) #-> (…) |> SELECT(…) -Displaying a `SQLClause` object shows how it was constructed. +Displaying a `SQLSyntax` object shows how it was constructed. - display(c) + display(s) #-> ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth)) -A `SQLClause` object wraps a concrete clause object, which can be accessed -using the indexing operator. +A `SQLSyntax` object is a linked list consisting of a concrete `head` clause +and an optional `tail`. - c[] - #-> ((…) |> SELECT(…))[] + display(s.head) + #-> SELECT(ID(:person_id), ID(:year_of_birth)).head - display(c[]) - #-> (ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth)))[] + display(s.tail) + #-> ID(:person) |> FROM() To generate SQL, we use function `render()`. - print(render(c)) + print(render(s)) #=> SELECT "person_id", @@ -42,17 +42,17 @@ To generate SQL, we use function `render()`. A SQL literal is created using a `LIT()` constructor. - c = LIT("SQL is fun!") + s = LIT("SQL is fun!") #-> LIT("SQL is fun!") Values of certain Julia data types are automatically converted to SQL -literals when they are used in the context of a SQL clause. +literals when they are used as arguments of clause constructors. using Dates - c = SELECT(missing, true, 42, "SQL is fun!", Date(2000)) + s = SELECT(missing, true, 42, "SQL is fun!", Date(2000)) - display(c) + display(s) #=> SELECT(LIT(missing), LIT(true), @@ -61,7 +61,7 @@ literals when they are used in the context of a SQL clause. LIT(Dates.Date("2000-01-01"))) =# - print(render(c)) + print(render(s)) #=> SELECT NULL, @@ -73,21 +73,21 @@ literals when they are used in the context of a SQL clause. Some values may render differently depending on the dialect. - c = LIT(false) + s = LIT(false) - print(render(c, dialect = :sqlserver)) + print(render(s, dialect = :sqlserver)) #-> (1 = 0) A quote character in a string literal is represented by a pair of quotes. - c = LIT("O'Hare") + s = LIT("O'Hare") - print(render(c)) + print(render(s)) #-> 'O''Hare' Some dialects use backslash to escape quote characters. - print(render(c, dialect = :spark)) + print(render(s, dialect = :spark)) #-> 'O\'Hare' @@ -95,46 +95,65 @@ Some dialects use backslash to escape quote characters. A SQL identifier is created with `ID()` constructor. - c = ID(:person) + s = ID(:person) #-> ID(:person) - display(c) + display(s) #-> ID(:person) - print(render(c)) + print(render(s)) #-> "person" Serialization of an identifier depends on the SQL dialect. - print(render(c, dialect = :sqlserver)) + print(render(s, dialect = :sqlserver)) #-> [person] A quote character in an identifier is properly escaped. - c = ID("year of \"birth\"") + s = ID("year of \"birth\"") - print(render(c)) + print(render(s)) #-> "year of ""birth""" A qualified identifier is created using the chain operator. - c = ID(:person) |> ID(:year_of_birth) + s = ID(:person) |> ID(:year_of_birth) #-> (…) |> ID(:year_of_birth) - display(c) + display(s) #-> ID(:person) |> ID(:year_of_birth) - print(render(c)) + print(render(s)) #-> "person"."year_of_birth" +There are several shorthands for creating qualified identifiers. + + display(ID(:public, :person)) + #-> ID(:public) |> ID(:person) + + display(ID(:public, :person, :year_of_birth)) + #-> ID(:public) |> ID(:person) |> ID(:year_of_birth) + + display(ID([:public, :person], :year_of_birth)) + #-> ID(:public) |> ID(:person) |> ID(:year_of_birth) + + t = SQLTable(qualifiers = [:public], :person, :person_id, :year_of_birth) + + display(ID(t)) + #-> ID(:public) |> ID(:person) + + display(FROM(t)) + #-> ID(:public) |> ID(:person) |> FROM() + Symbols and pairs of symbols are automatically converted to SQL identifiers -when they are used in the context of a SQL clause. +when they are used as arguments of clause constructors. - c = FROM(:p => :person) |> SELECT((:p, :person_id)) - display(c) + s = FROM(:p => :person) |> SELECT((:p, :person_id)) + display(s) #-> ID(:person) |> AS(:p) |> FROM() |> SELECT(ID(:p) |> ID(:person_id)) - print(render(c)) + print(render(s)) #=> SELECT "p"."person_id" FROM "person" AS "p" @@ -145,34 +164,34 @@ when they are used in the context of a SQL clause. Placeholder parameters to a SQL query are created with `VAR()` constructor. - c = VAR(:YEAR) + s = VAR(:YEAR) #-> VAR(:YEAR) - display(c) + display(s) #-> VAR(:YEAR) - print(render(c)) + print(render(s)) #-> :YEAR Rendering of a SQL parameter depends on the chosen dialect. - print(render(c, dialect = :sqlite)) + print(render(s, dialect = :sqlite)) #-> ?1 - print(render(c, dialect = :postgresql)) + print(render(s, dialect = :postgresql)) #-> $1 - print(render(c, dialect = :mysql)) + print(render(s, dialect = :mysql)) #-> ? Function `pack()` converts named parameters to a positional form. - c = FROM(:person) |> + s = FROM(:person) |> WHERE(FUN(:or, FUN("=", :gender_concept_id, VAR(:GENDER)), FUN("=", :gender_source_concept_id, VAR(:GENDER)))) |> SELECT(:person_id) - sql = render(c, dialect = :sqlite) + sql = render(s, dialect = :sqlite) print(sql) #=> @@ -195,7 +214,7 @@ Function `pack()` converts named parameters to a positional form. If the dialect does not support numbered parameters, `pack()` may need to duplicate parameter values. - sql = render(c, dialect = :mysql) + sql = render(s, dialect = :mysql) print(sql) #=> @@ -214,151 +233,151 @@ duplicate parameter values. An application of a SQL function is created with `FUN()` constructor. - c = FUN(:concat, :city, ", ", :state) + s = FUN(:concat, :city, ", ", :state) #-> FUN("concat", …) - display(c) + display(s) #-> FUN("concat", ID(:city), LIT(", "), ID(:state)) - print(render(c)) + print(render(s)) #-> concat("city", ', ', "state") - c = FUN(:now) + s = FUN(:now) #-> FUN("now") - print(render(c)) + print(render(s)) #-> now() `FUN()` with an empty name generates a comma-separated list of values. - c = FUN("", "60614", "60615") + s = FUN("", "60614", "60615") - print(render(c)) + print(render(s)) #-> ('60614', '60615') A name that contains only symbol characters is considered an operator. - c = FUN("||", :city, ", ", :state) + s = FUN("||", :city, ", ", :state) - print(render(c)) + print(render(s)) #-> ("city" || ', ' || "state") To create an operator containing alphabetical characters, add a leading or a trailing space to its name. - c = FUN(" IS DISTINCT FROM ", :zip, missing) + s = FUN(" IS DISTINCT FROM ", :zip, missing) - print(render(c)) + print(render(s)) #-> ("zip" IS DISTINCT FROM NULL) - c = FUN(" IS DISTINCT FROM", :zip, missing) + s = FUN(" IS DISTINCT FROM", :zip, missing) - print(render(c)) + print(render(s)) #-> ("zip" IS DISTINCT FROM NULL) - c = FUN(" COLLATE \"C\"", :zip) + s = FUN(" COLLATE \"C\"", :zip) - print(render(c)) + print(render(s)) #-> ("zip" COLLATE "C") - c = FUN("DATE ", "2000-01-01") + s = FUN("DATE ", "2000-01-01") - print(render(c)) + print(render(s)) #-> (DATE '2000-01-01') - c = FUN("CURRENT_TIME ") + s = FUN("CURRENT_TIME ") - print(render(c)) + print(render(s)) #-> CURRENT_TIME - c = FUN(" CURRENT_TIME") + s = FUN(" CURRENT_TIME") - print(render(c)) + print(render(s)) #-> CURRENT_TIME To create a SQL expression with irregular syntax, supply `FUN()` with a *template* string. - c = FUN("SUBSTRING(? FROM ? FOR ?)", :zip, 1, 3) + s = FUN("SUBSTRING(? FROM ? FOR ?)", :zip, 1, 3) - print(render(c)) + print(render(s)) #-> SUBSTRING("zip" FROM 1 FOR 3) - c = FUN("?::date", "2000-01-01") + s = FUN("?::date", "2000-01-01") - print(render(c)) + print(render(s)) #-> '2000-01-01'::date Write `??` to use `?` in an operator name or a template. - c = FUN("??-", "(1,0)", "(0,0)") + s = FUN("??-", "(1,0)", "(0,0)") - print(render(c)) + print(render(s)) #-> ('(1,0)' ?- '(0,0)') - c = FUN("('(?,?)'::point ??| '(?,?)'::point)", 0, 1, 0, 0) + s = FUN("('(?,?)'::point ??| '(?,?)'::point)", 0, 1, 0, 0) - print(render(c)) + print(render(s)) #-> ('(0,1)'::point ?| '(0,0)'::point) Some functions and operators have specialized serializers. - c = FUN(:and) + s = FUN(:and) - print(render(c)) + print(render(s)) #-> TRUE - c = FUN(:and, true) + s = FUN(:and, true) - print(render(c)) + print(render(s)) #-> TRUE - c = FUN(:and, true, false) + s = FUN(:and, true, false) - print(render(c)) + print(render(s)) #-> (TRUE AND FALSE) - c = FUN(:or) + s = FUN(:or) - print(render(c)) + print(render(s)) #-> FALSE - c = FUN(:or, true) + s = FUN(:or, true) - print(render(c)) + print(render(s)) #-> TRUE - c = FUN(:or, true, false) + s = FUN(:or, true, false) - print(render(c)) + print(render(s)) #-> (TRUE OR FALSE) - c = FUN(:not, true) + s = FUN(:not, true) - print(render(c)) + print(render(s)) #-> (NOT TRUE) - c = FUN(:concat, :city, ", ", :state) + s = FUN(:concat, :city, ", ", :state) - print(render(c)) + print(render(s)) #-> concat("city", ', ', "state") - print(render(c, dialect = :sqlite)) + print(render(s, dialect = :sqlite)) #-> ("city" || ', ' || "state") - c = FUN(:in, :zip) + s = FUN(:in, :zip) - print(render(c)) + print(render(s)) #-> FALSE - c = FUN(:in, :zip, "60614", "60615") + s = FUN(:in, :zip, "60614", "60615") - print(render(c)) + print(render(s)) #-> ("zip" IN ('60614', '60615')) - c = SELECT(FUN(:in, "60615", FROM(:location) |> SELECT(:zip))) + s = SELECT(FUN(:in, "60615", FROM(:location) |> SELECT(:zip))) - print(render(c)) + print(render(s)) #=> SELECT ('60615' IN ( SELECT "zip" @@ -366,19 +385,19 @@ Some functions and operators have specialized serializers. )) =# - c = FUN(:not_in, :zip) + s = FUN(:not_in, :zip) - print(render(c)) + print(render(s)) #-> TRUE - c = FUN(:not_in, :zip, "60614", "60615") + s = FUN(:not_in, :zip, "60614", "60615") - print(render(c)) + print(render(s)) #-> ("zip" NOT IN ('60614', '60615')) - c = SELECT(FUN(:not_in, "60615", FROM(:location) |> SELECT(:zip))) + s = SELECT(FUN(:not_in, "60615", FROM(:location) |> SELECT(:zip))) - print(render(c)) + print(render(s)) #=> SELECT ('60615' NOT IN ( SELECT "zip" @@ -386,11 +405,11 @@ Some functions and operators have specialized serializers. )) =# - c = SELECT(FUN(:exists, FROM(:location) |> + s = SELECT(FUN(:exists, FROM(:location) |> WHERE(FUN("=", :zip, "60615")) |> SELECT(missing))) - print(render(c)) + print(render(s)) #=> SELECT (EXISTS ( SELECT NULL @@ -399,11 +418,11 @@ Some functions and operators have specialized serializers. )) =# - c = SELECT(FUN(:not_exists, FROM(:location) |> + s = SELECT(FUN(:not_exists, FROM(:location) |> WHERE(FUN("=", :zip, "60615")) |> SELECT(missing))) - print(render(c)) + print(render(s)) #=> SELECT (NOT EXISTS ( SELECT NULL @@ -412,69 +431,69 @@ Some functions and operators have specialized serializers. )) =# - c = FUN(:is_null, :zip) + s = FUN(:is_null, :zip) - print(render(c)) + print(render(s)) #-> ("zip" IS NULL) - c = FUN(:is_not_null, :zip) + s = FUN(:is_not_null, :zip) - print(render(c)) + print(render(s)) #-> ("zip" IS NOT NULL) - c = FUN(:like, :zip, "606%") + s = FUN(:like, :zip, "606%") - print(render(c)) + print(render(s)) #-> ("zip" LIKE '606%') - c = FUN(:not_like, :zip, "606%") + s = FUN(:not_like, :zip, "606%") - print(render(c)) + print(render(s)) #-> ("zip" NOT LIKE '606%') - c = FUN(:case, FUN("<", :year_of_birth, 1970), "boomer") + s = FUN(:case, FUN("<", :year_of_birth, 1970), "boomer") - print(render(c)) + print(render(s)) #-> (CASE WHEN ("year_of_birth" < 1970) THEN 'boomer' END) - c = FUN(:case, FUN("<", :year_of_birth, 1970), "boomer", "millenial") + s = FUN(:case, FUN("<", :year_of_birth, 1970), "boomer", "millenial") - print(render(c)) + print(render(s)) #-> (CASE WHEN ("year_of_birth" < 1970) THEN 'boomer' ELSE 'millenial' END) - c = FUN(:cast, "2020-01-01", "DATE") + s = FUN(:cast, "2020-01-01", "DATE") - print(render(c)) + print(render(s)) #-> CAST('2020-01-01' AS DATE) - c = FUN(:extract, "YEAR", c) + s = FUN(:extract, "YEAR", s) - print(render(c)) + print(render(s)) #-> EXTRACT(YEAR FROM CAST('2020-01-01' AS DATE)) - c = FUN(:between, :year_of_birth, 1950, 2000) + s = FUN(:between, :year_of_birth, 1950, 2000) - print(render(c)) + print(render(s)) #-> ("year_of_birth" BETWEEN 1950 AND 2000) - c = FUN(:not_between, :year_of_birth, 1950, 2000) + s = FUN(:not_between, :year_of_birth, 1950, 2000) - print(render(c)) + print(render(s)) #-> ("year_of_birth" NOT BETWEEN 1950 AND 2000) - c = FUN(:current_date) + s = FUN(:current_date) - print(render(c)) + print(render(s)) #-> CURRENT_DATE - c = FUN(:current_date, 1) + s = FUN(:current_date, 1) - print(render(c)) + print(render(s)) #-> CURRENT_DATE(1) - c = FUN(:current_timestamp) + s = FUN(:current_timestamp) - print(render(c)) + print(render(s)) #-> CURRENT_TIMESTAMP @@ -482,112 +501,111 @@ Some functions and operators have specialized serializers. Aggregate SQL functions have a specialized `AGG()` constructor. - c = AGG(:max, :year_of_birth) + s = AGG(:max, :year_of_birth) #-> AGG("max", …) - display(c) + display(s) #-> AGG("max", ID(:year_of_birth)) - print(render(c)) + print(render(s)) #-> max("year_of_birth") Some well-known aggregate functions with irregular syntax are supported. - c = AGG(:count) + s = AGG(:count) #-> AGG("count") - display(c) + display(s) #-> AGG("count") - print(render(c)) + print(render(s)) #-> count(*) - c = AGG(:count_distinct, :zip) + s = AGG(:count_distinct, :zip) - print(render(c)) + print(render(s)) #-> count(DISTINCT "zip") Otherwise, a template name can be used. - c = AGG("string_agg(DISTINCT ?, ',' ORDER BY ?)", :zip, :zip) + s = AGG("string_agg(DISTINCT ?, ',' ORDER BY ?)", :zip, :zip) - print(render(c)) + print(render(s)) #-> string_agg(DISTINCT "zip", ',' ORDER BY "zip") An aggregate function may have a `FILTER` modifier. - c = AGG(:count, filter = FUN(">", :year_of_birth, 1970)) + s = AGG(:count, filter = FUN(">", :year_of_birth, 1970)) - display(c) + display(s) #-> AGG("count", filter = FUN(">", ID(:year_of_birth), LIT(1970))) - print(render(c)) + print(render(s)) #-> (count(*) FILTER (WHERE ("year_of_birth" > 1970))) A window function can be created by adding an `OVER` modifier. - c = PARTITION(:year_of_birth, order_by = [:month_of_birth, :day_of_birth]) |> - AGG("row_number") + s = AGG("row_number", over = PARTITION(:year_of_birth, order_by = [:month_of_birth, :day_of_birth])) - display(c) + display(s) #=> AGG("row_number", over = PARTITION(ID(:year_of_birth), order_by = [ID(:month_of_birth), ID(:day_of_birth)])) =# - print(render(c)) + print(render(s)) #-> (row_number() OVER (PARTITION BY "year_of_birth" ORDER BY "month_of_birth", "day_of_birth")) - c = AGG("row_number", over = :w) + s = AGG("row_number", over = :w) - print(render(c)) + print(render(s)) #-> (row_number() OVER ("w")) The `PARTITION` clause may contain a frame specification including the frame mode, frame endpoints, and frame exclusion. - c = PARTITION(order_by = [:year_of_birth], frame = :groups) + s = PARTITION(order_by = [:year_of_birth], frame = :groups) #-> PARTITION(order_by = […], frame = :GROUPS) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" GROUPS UNBOUNDED PRECEDING - c = PARTITION(order_by = [:year_of_birth], frame = (mode = :rows,)) + s = PARTITION(order_by = [:year_of_birth], frame = (mode = :rows,)) #-> PARTITION(order_by = […], frame = :ROWS) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" ROWS UNBOUNDED PRECEDING - c = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -1, finish = 1, exclude = :current_row)) + s = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -1, finish = 1, exclude = :current_row)) #-> PARTITION(order_by = […], frame = (mode = :RANGE, start = -1, finish = 1, exclude = :CURRENT_ROW)) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE CURRENT ROW - c = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -Inf, finish = 0)) + s = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -Inf, finish = 0)) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - c = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = 0, finish = Inf)) + s = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = 0, finish = Inf)) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING - c = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :no_others)) + s = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :no_others)) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" RANGE UNBOUNDED PRECEDING EXCLUDE NO OTHERS - c = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :group)) + s = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :group)) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" RANGE UNBOUNDED PRECEDING EXCLUDE GROUP - c = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :ties)) + s = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :ties)) - print(render(c)) + print(render(s)) #-> ORDER BY "year_of_birth" RANGE UNBOUNDED PRECEDING EXCLUDE TIES @@ -595,22 +613,22 @@ mode, frame endpoints, and frame exclusion. An `AS` clause is created with `AS()` constructor. - c = ID(:person) |> AS(:p) + s = ID(:person) |> AS(:p) #-> (…) |> AS(:p) - display(c) + display(s) #-> ID(:person) |> AS(:p) - print(render(c)) + print(render(s)) #-> "person" AS "p" A pair expression is automatically converted to an `AS` clause. - c = FROM(:p => :person) - display(c) + s = FROM(:p => :person) + display(s) #-> ID(:person) |> AS(:p) |> FROM() - print(render(c |> SELECT((:p, :person_id)))) + print(render(s |> SELECT((:p, :person_id)))) #=> SELECT "p"."person_id" FROM "person" AS "p" @@ -621,13 +639,13 @@ A pair expression is automatically converted to an `AS` clause. A `FROM` clause is created with `FROM()` constructor. - c = FROM(:person) + s = FROM(:person) #-> (…) |> FROM() - display(c) + display(s) #-> ID(:person) |> FROM() - print(render(c |> SELECT(:person_id))) + print(render(s |> SELECT(:person_id))) #=> SELECT "person_id" FROM "person" @@ -640,13 +658,13 @@ A `SELECT` clause is created with `SELECT()` constructor. While in SQL, `SELECT` typically opens a query, in FunSQL, `SELECT()` should be placed at the end of a clause chain. - c = :person |> FROM() |> SELECT(:person_id, :year_of_birth) + s = :person |> FROM() |> SELECT(:person_id, :year_of_birth) #-> (…) |> SELECT(…) - display(c) + display(s) #-> ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth)) - print(render(c)) + print(render(s)) #=> SELECT "person_id", @@ -656,13 +674,13 @@ at the end of a clause chain. The `DISTINCT` modifier can be added from the constructor. - c = FROM(:location) |> SELECT(distinct = true, :zip) + s = FROM(:location) |> SELECT(distinct = true, :zip) #-> (…) |> SELECT(…) - display(c) + display(s) #-> ID(:location) |> FROM() |> SELECT(distinct = true, ID(:zip)) - print(render(c)) + print(render(s)) #=> SELECT DISTINCT "zip" FROM "location" @@ -670,22 +688,22 @@ The `DISTINCT` modifier can be added from the constructor. A `TOP` modifier could be specified. - c = FROM(:person) |> SELECT(top = 1, :person_id) + s = FROM(:person) |> SELECT(top = 1, :person_id) - display(c) + display(s) #-> ID(:person) |> FROM() |> SELECT(top = 1, ID(:person_id)) - print(render(c)) + print(render(s)) #=> SELECT TOP 1 "person_id" FROM "person" =# - c = FROM(:person) |> + s = FROM(:person) |> ORDER(:year_of_birth) |> SELECT(top = (limit = 1, with_ties = true), :person_id) - display(c) + display(s) #=> ID(:person) |> FROM() |> @@ -693,7 +711,7 @@ A `TOP` modifier could be specified. SELECT(top = (limit = 1, with_ties = true), ID(:person_id)) =# - print(render(c)) + print(render(s)) #=> SELECT TOP 1 WITH TIES "person_id" FROM "person" @@ -702,14 +720,14 @@ A `TOP` modifier could be specified. A `SELECT` clause with an empty list of arguments can be created explicitly. - c = SELECT(args = []) + s = SELECT(args = []) #-> SELECT(…) Rendering a nested `SELECT` clause adds parentheses around it. - c = :location |> FROM() |> SELECT(:state, :zip) |> FROM() |> SELECT(:zip) + s = :location |> FROM() |> SELECT(:state, :zip) |> FROM() |> SELECT(:zip) - print(render(c)) + print(render(s)) #=> SELECT "zip" FROM ( @@ -725,13 +743,13 @@ Rendering a nested `SELECT` clause adds parentheses around it. A `WHERE` clause is created with `WHERE()` constructor. - c = FROM(:person) |> WHERE(FUN(">", :year_of_birth, 2000)) + s = FROM(:person) |> WHERE(FUN(">", :year_of_birth, 2000)) #-> (…) |> WHERE(…) - display(c) + display(s) #-> ID(:person) |> FROM() |> WHERE(FUN(">", ID(:year_of_birth), LIT(2000))) - print(render(c |> SELECT(:person_id))) + print(render(s |> SELECT(:person_id))) #=> SELECT "person_id" FROM "person" @@ -744,13 +762,13 @@ A `WHERE` clause is created with `WHERE()` constructor. A `LIMIT/OFFSET` (or `OFFSET/FETCH`) clause is created with `LIMIT()` constructor. - c = FROM(:person) |> LIMIT(10) + s = FROM(:person) |> LIMIT(10) #-> (…) |> LIMIT(10) - display(c) + display(s) #-> ID(:person) |> FROM() |> LIMIT(10) - print(render(c |> SELECT(:person_id))) + print(render(s |> SELECT(:person_id))) #=> SELECT "person_id" FROM "person" @@ -759,28 +777,28 @@ constructor. Many SQL dialects represent `LIMIT` clause with a non-standard syntax. - print(render(c |> SELECT(:person_id), dialect = :mysql)) + print(render(s |> SELECT(:person_id), dialect = :mysql)) #=> SELECT `person_id` FROM `person` LIMIT 10 =# - print(render(c |> SELECT(:person_id), dialect = :postgresql)) + print(render(s |> SELECT(:person_id), dialect = :postgresql)) #=> SELECT "person_id" FROM "person" LIMIT 10 =# - print(render(c |> SELECT(:person_id), dialect = :sqlite)) + print(render(s |> SELECT(:person_id), dialect = :sqlite)) #=> SELECT "person_id" FROM "person" LIMIT 10 =# - print(render(c |> SELECT(:person_id), dialect = :sqlserver)) + print(render(s |> SELECT(:person_id), dialect = :sqlserver)) #=> SELECT TOP 10 [person_id] FROM [person] @@ -789,12 +807,12 @@ Many SQL dialects represent `LIMIT` clause with a non-standard syntax. Both limit (the number of rows) and offset (number of rows to skip) can be specified. - c = FROM(:person) |> LIMIT(100, 10) |> SELECT(:person_id) + s = FROM(:person) |> LIMIT(100, 10) |> SELECT(:person_id) - display(c) + display(s) #-> ID(:person) |> FROM() |> LIMIT(100, 10) |> SELECT(ID(:person_id)) - print(render(c)) + print(render(s)) #=> SELECT "person_id" FROM "person" @@ -802,14 +820,14 @@ be specified. FETCH NEXT 10 ROWS ONLY =# - print(render(c, dialect = :mysql)) + print(render(s, dialect = :mysql)) #=> SELECT `person_id` FROM `person` LIMIT 100, 10 =# - print(render(c, dialect = :postgresql)) + print(render(s, dialect = :postgresql)) #=> SELECT "person_id" FROM "person" @@ -817,7 +835,7 @@ be specified. OFFSET 100 =# - print(render(c, dialect = :sqlite)) + print(render(s, dialect = :sqlite)) #=> SELECT "person_id" FROM "person" @@ -825,7 +843,7 @@ be specified. OFFSET 100 =# - print(render(c, dialect = :sqlserver)) + print(render(s, dialect = :sqlserver)) #=> SELECT [person_id] FROM [person] @@ -835,9 +853,9 @@ be specified. Alternatively, both limit and offset can be specified as a unit range. - c = FROM(:person) |> LIMIT(101:110) + s = FROM(:person) |> LIMIT(101:110) - print(render(c |> SELECT(:person_id))) + print(render(s |> SELECT(:person_id))) #=> SELECT "person_id" FROM "person" @@ -847,33 +865,33 @@ Alternatively, both limit and offset can be specified as a unit range. It is possible to specify the offset without the limit. - c = FROM(:person) |> LIMIT(offset = 100) |> SELECT(:person_id) + s = FROM(:person) |> LIMIT(offset = 100) |> SELECT(:person_id) - display(c) + display(s) #-> ID(:person) |> FROM() |> LIMIT(100, nothing) |> SELECT(ID(:person_id)) - print(render(c)) + print(render(s)) #=> SELECT "person_id" FROM "person" OFFSET 100 ROWS =# - print(render(c, dialect = :mysql)) + print(render(s, dialect = :mysql)) #=> SELECT `person_id` FROM `person` LIMIT 100, 18446744073709551615 =# - print(render(c, dialect = :postgresql)) + print(render(s, dialect = :postgresql)) #=> SELECT "person_id" FROM "person" OFFSET 100 =# - print(render(c, dialect = :sqlite)) + print(render(s, dialect = :sqlite)) #=> SELECT "person_id" FROM "person" @@ -881,7 +899,7 @@ It is possible to specify the offset without the limit. OFFSET 100 =# - print(render(c, dialect = :sqlserver)) + print(render(s, dialect = :sqlserver)) #=> SELECT [person_id] FROM [person] @@ -890,12 +908,12 @@ It is possible to specify the offset without the limit. It is possible to specify the limit with ties. - c = FROM(:person) |> + s = FROM(:person) |> ORDER(:year_of_birth) |> LIMIT(10, with_ties = true) |> SELECT(:person_id) - display(c) + display(s) #=> ID(:person) |> FROM() |> @@ -904,7 +922,7 @@ It is possible to specify the limit with ties. SELECT(ID(:person_id)) =# - print(render(c)) + print(render(s)) #=> SELECT "person_id" FROM "person" @@ -915,7 +933,7 @@ It is possible to specify the limit with ties. SQL Server prohibits `ORDER BY` without limiting in a nested query, so FunSQL automatically adds `OFFSET 0` clause to the query. - c = FROM(:person) |> + s = FROM(:person) |> ORDER(:year_of_birth) |> SELECT(:person_id, :gender_concept_id) |> AS(:person) |> @@ -923,7 +941,7 @@ automatically adds `OFFSET 0` clause to the query. WHERE(FUN("=", :gender_concept_id, 8507)) |> SELECT(:person_id) - print(render(c, dialect = :sqlserver)) + print(render(s, dialect = :sqlserver)) #=> SELECT [person_id] FROM ( @@ -942,11 +960,11 @@ automatically adds `OFFSET 0` clause to the query. A `JOIN` clause is created with `JOIN()` constructor. - c = FROM(:p => :person) |> + s = FROM(:p => :person) |> JOIN(:l => :location, FUN("=", (:p, :location_id), (:l, :location_id)), left = true) #-> (…) |> JOIN(…) - display(c) + display(s) #=> ID(:person) |> AS(:p) |> @@ -956,7 +974,7 @@ A `JOIN` clause is created with `JOIN()` constructor. left = true) =# - print(render(c |> SELECT((:p, :person_id), (:l, :state)))) + print(render(s |> SELECT((:p, :person_id), (:l, :state)))) #=> SELECT "p"."person_id", @@ -967,11 +985,11 @@ A `JOIN` clause is created with `JOIN()` constructor. Different types of `JOIN` are supported. - c = FROM(:p => :person) |> + s = FROM(:p => :person) |> JOIN(:op => :observation_period, on = FUN("=", (:p, :person_id), (:op, :person_id))) - display(c) + display(s) #=> ID(:person) |> AS(:p) |> @@ -980,7 +998,7 @@ Different types of `JOIN` are supported. FUN("=", ID(:p) |> ID(:person_id), ID(:op) |> ID(:person_id))) =# - print(render(c |> SELECT((:p, :person_id), (:op, :observation_period_start_date)))) + print(render(s |> SELECT((:p, :person_id), (:op, :observation_period_start_date)))) #=> SELECT "p"."person_id", @@ -989,12 +1007,12 @@ Different types of `JOIN` are supported. JOIN "observation_period" AS "op" ON ("p"."person_id" = "op"."person_id") =# - c = FROM(:l => :location) |> + s = FROM(:l => :location) |> JOIN(:cs => :care_site, on = FUN("=", (:l, :location_id), (:cs, :location_id)), right = true) - display(c) + display(s) #=> ID(:location) |> AS(:l) |> @@ -1004,7 +1022,7 @@ Different types of `JOIN` are supported. right = true) =# - print(render(c |> SELECT((:cs, :care_site_name), (:l, :state)))) + print(render(s |> SELECT((:cs, :care_site_name), (:l, :state)))) #=> SELECT "cs"."care_site_name", @@ -1013,13 +1031,13 @@ Different types of `JOIN` are supported. RIGHT JOIN "care_site" AS "cs" ON ("l"."location_id" = "cs"."location_id") =# - c = FROM(:p => :person) |> + s = FROM(:p => :person) |> JOIN(:pr => :provider, on = FUN("=", (:p, :provider_id), (:pr, :provider_id)), left = true, right = true) - display(c) + display(s) #=> ID(:person) |> AS(:p) |> @@ -1030,7 +1048,7 @@ Different types of `JOIN` are supported. right = true) =# - print(render(c |> SELECT((:p, :person_id), (:pr, :npi)))) + print(render(s |> SELECT((:p, :person_id), (:pr, :npi)))) #=> SELECT "p"."person_id", @@ -1041,11 +1059,11 @@ Different types of `JOIN` are supported. To render a `CROSS JOIN`, set the join condition to `true`. - c = FROM(:p1 => :person) |> + s = FROM(:p1 => :person) |> JOIN(:p2 => :person, on = true) - print(render(c |> SELECT((:p1, :person_id), (:p2, :person_id)))) + print(render(s |> SELECT((:p1, :person_id), (:p2, :person_id)))) #=> SELECT "p1"."person_id", @@ -1056,7 +1074,7 @@ To render a `CROSS JOIN`, set the join condition to `true`. A `JOIN LATERAL` clause can be created. - c = FROM(:p => :person) |> + s = FROM(:p => :person) |> JOIN(:vo => FROM(:vo => :visit_occurrence) |> WHERE(FUN("=", (:p, :person_id), (:vo, :person_id))) |> ORDER((:vo, :visit_start_date) |> DESC()) |> @@ -1066,7 +1084,7 @@ A `JOIN LATERAL` clause can be created. left = true, lateral = true) - display(c) + display(s) #=> ID(:person) |> AS(:p) |> @@ -1084,7 +1102,7 @@ A `JOIN LATERAL` clause can be created. lateral = true) =# - print(render(c |> SELECT((:p, :person_id), (:vo, :visit_start_date)))) + print(render(s |> SELECT((:p, :person_id), (:vo, :visit_start_date)))) #=> SELECT "p"."person_id", @@ -1104,13 +1122,13 @@ A `JOIN LATERAL` clause can be created. A `GROUP BY` clause is created with `GROUP` constructor. - c = FROM(:person) |> GROUP(:year_of_birth) + s = FROM(:person) |> GROUP(:year_of_birth) #-> (…) |> GROUP(…) - display(c) + display(s) #-> ID(:person) |> FROM() |> GROUP(ID(:year_of_birth)) - print(render(c |> SELECT(:year_of_birth, AGG(:count)))) + print(render(s |> SELECT(:year_of_birth, AGG(:count)))) #=> SELECT "year_of_birth", @@ -1122,10 +1140,10 @@ A `GROUP BY` clause is created with `GROUP` constructor. A `GROUP` constructor accepts an empty partition list, in which case, it is not rendered. - c = FROM(:person) |> GROUP() + s = FROM(:person) |> GROUP() #-> (…) |> GROUP() - print(render(c |> SELECT(AGG(:count)))) + print(render(s |> SELECT(AGG(:count)))) #=> SELECT count(*) FROM "person" @@ -1133,10 +1151,10 @@ rendered. `GROUP` can accept the grouping mode or a vector of grouping sets. - c = FROM(:person) |> GROUP(:year_of_birth, sets = :ROLLUP) + s = FROM(:person) |> GROUP(:year_of_birth, sets = :ROLLUP) #-> (…) |> GROUP(…, sets = :ROLLUP) - print(render(c |> SELECT(:year_of_birth, AGG(:count)))) + print(render(s |> SELECT(:year_of_birth, AGG(:count)))) #=> SELECT "year_of_birth", @@ -1145,10 +1163,10 @@ rendered. GROUP BY ROLLUP("year_of_birth") =# - c = FROM(:person) |> GROUP(:year_of_birth, sets = :CUBE) + s = FROM(:person) |> GROUP(:year_of_birth, sets = :CUBE) #-> (…) |> GROUP(…, sets = :CUBE) - print(render(c |> SELECT(:year_of_birth, AGG(:count)))) + print(render(s |> SELECT(:year_of_birth, AGG(:count)))) #=> SELECT "year_of_birth", @@ -1157,10 +1175,10 @@ rendered. GROUP BY CUBE("year_of_birth") =# - c = FROM(:person) |> GROUP(:year_of_birth, sets = [[1], Int[]]) + s = FROM(:person) |> GROUP(:year_of_birth, sets = [[1], Int[]]) #-> (…) |> GROUP(…, sets = [[1], Int64[]]) - print(render(c |> SELECT(:year_of_birth, AGG(:count)))) + print(render(s |> SELECT(:year_of_birth, AGG(:count)))) #=> SELECT "year_of_birth", @@ -1182,12 +1200,12 @@ rendered. A `HAVING` clause is created with `HAVING()` constructor. - c = FROM(:person) |> + s = FROM(:person) |> GROUP(:year_of_birth) |> HAVING(FUN(">", AGG(:count), 10)) #-> (…) |> HAVING(…) - display(c) + display(s) #=> ID(:person) |> FROM() |> @@ -1195,7 +1213,7 @@ A `HAVING` clause is created with `HAVING()` constructor. HAVING(FUN(">", AGG("count"), LIT(10))) =# - print(render(c |> SELECT(:person_id))) + print(render(s |> SELECT(:person_id))) #=> SELECT "person_id" FROM "person" @@ -1208,13 +1226,13 @@ A `HAVING` clause is created with `HAVING()` constructor. An `ORDER BY` clause is created with `ORDER` constructor. - c = FROM(:person) |> ORDER(:year_of_birth) + s = FROM(:person) |> ORDER(:year_of_birth) #-> (…) |> ORDER(…) - display(c) + display(s) #-> ID(:person) |> FROM() |> ORDER(ID(:year_of_birth)) - print(render(c |> SELECT(:person_id))) + print(render(s |> SELECT(:person_id))) #=> SELECT "person_id" FROM "person" @@ -1224,10 +1242,10 @@ An `ORDER BY` clause is created with `ORDER` constructor. An `ORDER` constructor accepts an empty list, in which case, it is not rendered. - c = FROM(:person) |> ORDER() + s = FROM(:person) |> ORDER() #-> (…) |> ORDER() - print(render(c |> SELECT(:person_id))) + print(render(s |> SELECT(:person_id))) #=> SELECT "person_id" FROM "person" @@ -1235,12 +1253,12 @@ rendered. It is possible to specify ascending or descending order of the sort column. - c = FROM(:person) |> + s = FROM(:person) |> ORDER(:year_of_birth |> DESC(nulls = :first), :person_id |> ASC()) |> SELECT(:person_id) - display(c) + display(s) #=> ID(:person) |> FROM() |> @@ -1249,7 +1267,7 @@ It is possible to specify ascending or descending order of the sort column. SELECT(ID(:person_id)) =# - print(render(c)) + print(render(s)) #=> SELECT "person_id" FROM "person" @@ -1260,12 +1278,12 @@ It is possible to specify ascending or descending order of the sort column. Instead of `ASC` and `DESC`, a generic `SORT` constructor can be used. - c = FROM(:person) |> + s = FROM(:person) |> ORDER(:year_of_birth |> SORT(:desc, nulls = :last), :person_id |> SORT(:asc)) |> SELECT(:person_id) - print(render(c)) + print(render(s)) #=> SELECT "person_id" FROM "person" @@ -1279,14 +1297,14 @@ Instead of `ASC` and `DESC`, a generic `SORT` constructor can be used. `UNION` and `UNION ALL` clauses are created with `UNION()` constructor. - c = FROM(:measurement) |> + s = FROM(:measurement) |> SELECT(:person_id, :date => :measurement_date) |> UNION(all = true, FROM(:observation) |> SELECT(:person_id, :date => :observation_date)) #-> (…) |> UNION(all = true, …) - display(c) + display(s) #=> ID(:measurement) |> FROM() |> @@ -1297,7 +1315,7 @@ Instead of `ASC` and `DESC`, a generic `SORT` constructor can be used. SELECT(ID(:person_id), ID(:observation_date) |> AS(:date))) =# - print(render(c)) + print(render(s)) #=> SELECT "person_id", @@ -1317,7 +1335,7 @@ A `UNION` clause with no subqueries can be created explicitly. Rendering a nested `UNION` clause adds parentheses around it. - c = FROM(:measurement) |> + s = FROM(:measurement) |> SELECT(:person_id, :date => :measurement_date) |> UNION(all = true, FROM(:observation) |> @@ -1327,7 +1345,7 @@ Rendering a nested `UNION` clause adds parentheses around it. WHERE(FUN(">", ID(:date), Date(2000))) |> SELECT(ID(:person_id)) - print(render(c)) + print(render(s)) #=> SELECT "person_id" FROM ( @@ -1349,13 +1367,13 @@ Rendering a nested `UNION` clause adds parentheses around it. A `VALUES` clause is created with `VALUES()` constructor. - c = VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]) + s = VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]) #-> VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]) - display(c) + display(s) #-> VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]) - print(render(c)) + print(render(s)) #=> VALUES ('SQL', 1974), @@ -1365,7 +1383,7 @@ A `VALUES` clause is created with `VALUES()` constructor. MySQL has special syntax for rows. - print(render(c, dialect = :mysql)) + print(render(s, dialect = :mysql)) #=> VALUES ROW('SQL', 1974), @@ -1376,16 +1394,16 @@ MySQL has special syntax for rows. When `VALUES` clause contains a single row, it is emitted on the same line. - c = VALUES([("SQL", 1974)]) + s = VALUES([("SQL", 1974)]) - print(render(c)) + print(render(s)) #-> VALUES ('SQL', 1974) `VALUES` accepts a vector of scalar values. - c = VALUES(["SQL", "Julia", "FunSQL"]) + s = VALUES(["SQL", "Julia", "FunSQL"]) - print(render(c)) + print(render(s)) #=> VALUES 'SQL', @@ -1395,12 +1413,12 @@ line. When `VALUES` is nested in a `FROM` clause, it is wrapped in parentheses. - c = VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]) |> + s = VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]) |> AS(:values, columns = [:name, :year]) |> FROM() |> SELECT(FUN("*")) - print(render(c)) + print(render(s)) #=> SELECT * FROM ( @@ -1416,12 +1434,12 @@ When `VALUES` is nested in a `FROM` clause, it is wrapped in parentheses. A `WINDOW` clause is created with `WINDOW()` constructor. - c = FROM(:person) |> + s = FROM(:person) |> WINDOW(:w1 => PARTITION(:gender_concept_id), :w2 => :w1 |> PARTITION(:year_of_birth, order_by = [:month_of_birth, :day_of_birth])) #-> (…) |> WINDOW(…) - display(c) + display(s) #=> ID(:person) |> FROM() |> @@ -1432,7 +1450,7 @@ A `WINDOW` clause is created with `WINDOW()` constructor. AS(:w2)) =# - print(render(c |> SELECT(:w1 |> AGG("row_number"), :w2 |> AGG("row_number")))) + print(render(s |> SELECT(AGG("row_number", over = :w1), AGG("row_number", over = :w2)))) #=> SELECT (row_number() OVER ("w1")), @@ -1446,13 +1464,13 @@ A `WINDOW` clause is created with `WINDOW()` constructor. The `WINDOW()` constructor accepts an empty list of partitions, in which case, it is not rendered. - c = FROM(:person) |> + s = FROM(:person) |> WINDOW(args = []) - display(c) + display(s) #-> ID(:person) |> FROM() |> WINDOW(args = []) - print(render(c |> SELECT(AGG("row_number", over = PARTITION())))) + print(render(s |> SELECT(AGG("row_number", over = PARTITION())))) #=> SELECT (row_number() OVER ()) FROM "person" @@ -1488,12 +1506,12 @@ The `AS` clause that defines a common table expression is created using the The `WITH` clause is created using the `WITH()` constructor. - c = FROM(:essential_hypertension_with_descendants) |> + s = FROM(:essential_hypertension_with_descendants) |> SELECT(*) |> WITH(recursive = true, cte1, cte2) #-> (…) |> WITH(recursive = true, …) - display(c) + display(s) #=> ID(:essential_hypertension_with_descendants) |> FROM() |> @@ -1525,7 +1543,7 @@ The `WITH` clause is created using the `WITH()` constructor. columns = [:concept_id, :concept_name])) =# - print(render(c)) + print(render(s)) #=> WITH RECURSIVE "essential_hypertension" AS ( SELECT @@ -1585,14 +1603,13 @@ The `MATERIALIZED` annotation can be added using `NOTE`. A `WITH` clause without any common table expressions will be omitted. - c = FROM(:condition_occurrence) |> + s = FROM(:condition_occurrence) |> SELECT(*) |> WITH(args = []) #-> (…) |> WITH(args = []) - print(render(c)) + print(render(s)) #=> SELECT * FROM "condition_occurrence" =# - diff --git a/docs/src/test/nodes.md b/docs/src/test/nodes.md index 32a44cf2..415a06fb 100644 --- a/docs/src/test/nodes.md +++ b/docs/src/test/nodes.md @@ -2519,7 +2519,7 @@ for a `CREATE TABLE AS` or `SELECT INTO` statement. with_external_handler((tbl, def)) = println("CREATE TEMP TABLE ", - render(ID(tbl.qualifiers, tbl.name)), + render(ID(tbl)), " (", join([render(ID(c.name)) for (n, c) in tbl.columns], ", "), ") AS\n", render(def), ";\n") @@ -4266,40 +4266,38 @@ On the next stage, the query object is converted to a SQL syntax tree. end; #=> ┌ Debug: FunSQL.translate - │ WITH_CONTEXT( - │ over = ID(:person) |> - │ AS(:person_1) |> - │ FROM() |> - │ WHERE(FUN("<=", ID(:person_1) |> ID(:year_of_birth), LIT(2000))) |> - │ SELECT(ID(:person_1) |> ID(:person_id), - │ ID(:person_1) |> ID(:location_id)) |> - │ AS(:person_2) |> - │ FROM() |> - │ JOIN(ID(:location) |> - │ AS(:location_1) |> - │ FROM() |> - │ WHERE(FUN("=", ID(:location_1) |> ID(:state), LIT("IL"))) |> - │ SELECT(ID(:location_1) |> ID(:location_id)) |> - │ AS(:location_2), - │ FUN("=", - │ ID(:person_2) |> ID(:location_id), - │ ID(:location_2) |> ID(:location_id))) |> - │ JOIN(ID(:visit_occurrence) |> - │ AS(:visit_occurrence_1) |> - │ FROM() |> - │ GROUP(ID(:visit_occurrence_1) |> ID(:person_id)) |> - │ SELECT(AGG("max", - │ ID(:visit_occurrence_1) |> ID(:visit_start_date)) |> - │ AS(:max), - │ ID(:visit_occurrence_1) |> ID(:person_id)) |> - │ AS(:visit_group_1), - │ FUN("=", - │ ID(:person_2) |> ID(:person_id), - │ ID(:visit_group_1) |> ID(:person_id)), - │ left = true) |> - │ SELECT(ID(:person_2) |> ID(:person_id), - │ ID(:visit_group_1) |> ID(:max) |> AS(:max_visit_start_date)), - │ columns = [SQLColumn(:person_id), SQLColumn(:max_visit_start_date)]) + │ ID(:person) |> + │ AS(:person_1) |> + │ FROM() |> + │ WHERE(FUN("<=", ID(:person_1) |> ID(:year_of_birth), LIT(2000))) |> + │ SELECT(ID(:person_1) |> ID(:person_id), ID(:person_1) |> ID(:location_id)) |> + │ AS(:person_2) |> + │ FROM() |> + │ JOIN(ID(:location) |> + │ AS(:location_1) |> + │ FROM() |> + │ WHERE(FUN("=", ID(:location_1) |> ID(:state), LIT("IL"))) |> + │ SELECT(ID(:location_1) |> ID(:location_id)) |> + │ AS(:location_2), + │ FUN("=", + │ ID(:person_2) |> ID(:location_id), + │ ID(:location_2) |> ID(:location_id))) |> + │ JOIN(ID(:visit_occurrence) |> + │ AS(:visit_occurrence_1) |> + │ FROM() |> + │ GROUP(ID(:visit_occurrence_1) |> ID(:person_id)) |> + │ SELECT(AGG("max", ID(:visit_occurrence_1) |> ID(:visit_start_date)) |> + │ AS(:max), + │ ID(:visit_occurrence_1) |> ID(:person_id)) |> + │ AS(:visit_group_1), + │ FUN("=", + │ ID(:person_2) |> ID(:person_id), + │ ID(:visit_group_1) |> ID(:person_id)), + │ left = true) |> + │ SELECT(ID(:person_2) |> ID(:person_id), + │ ID(:visit_group_1) |> ID(:max) |> AS(:max_visit_start_date)) |> + │ WITH_CONTEXT(columns = [SQLColumn(:person_id), + │ SQLColumn(:max_visit_start_date)]) └ @ FunSQL … =# diff --git a/src/clauses.jl b/src/clauses.jl index 53561e4f..1b42c2a8 100644 --- a/src/clauses.jl +++ b/src/clauses.jl @@ -1,7 +1,7 @@ # Syntactic structure of a SQL query. -# Base type. +# Base abstract type of a node in a SQL syntax tree. """ A component of a SQL syntax tree. @@ -9,60 +9,43 @@ A component of a SQL syntax tree. abstract type AbstractSQLClause end -function dissect(scr::Symbol, ClauseType::Type{<:AbstractSQLClause}, pats::Vector{Any}) - scr_core = gensym(:scr_core) - ex = Expr(:&&, :($scr_core isa $ClauseType), Any[dissect(scr_core, pat) for pat in pats]...) - :($scr isa SQLClause && (local $scr_core = $scr[]; $ex)) -end +terminal(::Type{<:AbstractSQLClause}) = + false + +terminal(c::C) where {C <: AbstractSQLClause} = + terminal(C) -# Opaque wrapper that serves as a specialization barrier. +# Opaque linked list of SQL clauses. """ -An opaque wrapper over an arbitrary SQL clause. +SQL syntax tree represented as a linked list of SQL clauses. """ -struct SQLClause <: AbstractSQLClause - core::AbstractSQLClause - - SQLClause(@nospecialize core::AbstractSQLClause) = - new(core) -end - -Base.getindex(c::SQLClause) = - c.core +struct SQLSyntax + tail::Union{SQLSyntax, Nothing} + head::AbstractSQLClause -Base.convert(::Type{SQLClause}, c::SQLClause) = - c + SQLSyntax(@nospecialize head::AbstractSQLClause) = + new(nothing, head) -Base.convert(::Type{SQLClause}, @nospecialize c::AbstractSQLClause) = - SQLClause(c) - -Base.convert(::Type{SQLClause}, obj) = - convert(SQLClause, convert(AbstractSQLClause, obj)::AbstractSQLClause) - -(c::AbstractSQLClause)(c′) = - c(convert(SQLClause, c′)) + SQLSyntax(tail, @nospecialize head::AbstractSQLClause) = + new(tail, head) +end -(c::AbstractSQLClause)(c′::SQLClause) = - rebase(c, c′) +Base.convert(::Type{SQLSyntax}, @nospecialize c::AbstractSQLClause) = + SQLSyntax(c) -rebase(c::SQLClause, c′) = - convert(SQLClause, rebase(c[], c′)) +terminal(s::SQLSyntax) = + s.tail !== nothing ? terminal(s.tail) : terminal(s.head) -rebase(::Nothing, c′) = - c′ +(s::SQLSyntax)(s′) = + SQLSyntax(s.tail !== nothing ? s.tail(s′) : s′, s.head) Base.:(==)(@nospecialize(::AbstractSQLClause), @nospecialize(::AbstractSQLClause)) = false -Base.:(==)(c1::SQLClause, c2::SQLClause) = - c1[] == c2[] - -Base.:(==)(c1::SQLClause, @nospecialize(c2::AbstractSQLClause)) = - c1[] == c2 - -Base.:(==)(@nospecialize(c1::AbstractSQLClause), c2::SQLClause) = - c1 == c2[] +Base.:(==)(s1::SQLSyntax, s2::SQLSyntax) = + s1.head == s2.head && s1.tail == s2.tail @generated function Base.:(==)(c1::C, c2::C) where {C <: AbstractSQLClause} exs = Expr[] @@ -72,8 +55,8 @@ Base.:(==)(@nospecialize(c1::AbstractSQLClause), c2::SQLClause) = Expr(:||, :(c1 === c2), Expr(:&&, exs...)) end -Base.hash(c::SQLClause, h::UInt) = - hash(c[], h) +Base.hash(s::SQLSyntax, h::UInt) = + s.tail !== nothing ? hash(s.tail, hash(s.head, h)) : hash(s.head, h) @generated function Base.hash(c::AbstractSQLClause, h::UInt) ex = :(h + $(hash(c))) @@ -86,36 +69,37 @@ end # Pretty-printing. -Base.show(io::IO, c::AbstractSQLClause) = +Base.show(io::IO, c::Union{AbstractSQLClause, SQLSyntax}) = print(io, quoteof(c, limit = true)) -Base.show(io::IO, ::MIME"text/plain", c::AbstractSQLClause) = +Base.show(io::IO, ::MIME"text/plain", c::Union{AbstractSQLClause, SQLSyntax}) = pprint(io, c) -function PrettyPrinting.quoteof(c::SQLClause; limit::Bool = false, unwrap::Bool = false) +function PrettyPrinting.quoteof(s::SQLSyntax; limit::Bool = false, head_only::Bool = false) ctx = QuoteContext(limit = limit) - ex = quoteof(c[], ctx) - if unwrap - ex = Expr(:ref, ex) + ex = quoteof(s, ctx) + if head_only + ex = Expr(:., ex, QuoteNode(:head)) end ex end PrettyPrinting.quoteof(c::AbstractSQLClause; limit::Bool = false) = - quoteof(convert(SQLClause, c), limit = limit, unwrap = true) + quoteof(convert(SQLSyntax, c), limit = limit, head_only = true) -PrettyPrinting.quoteof(c::SQLClause, ctx::QuoteContext) = - if !ctx.limit - quoteof(c[], ctx) - else - :… +function PrettyPrinting.quoteof(s::SQLSyntax, ctx::QuoteContext) + ex = quoteof(s.head, ctx) + if s.tail !== nothing + ex = Expr(:call, :|>, !ctx.limit ? quoteof(s.tail, ctx) : :…, ex) end + ex +end -PrettyPrinting.quoteof(cs::Vector{SQLClause}, ctx::QuoteContext) = - if isempty(cs) +PrettyPrinting.quoteof(ss::Vector{SQLSyntax}, ctx::QuoteContext) = + if isempty(ss) Any[] elseif !ctx.limit - Any[quoteof(c, ctx) for c in cs] + Any[quoteof(s, ctx) for s in ss] else Any[:…] end @@ -130,6 +114,36 @@ PrettyPrinting.quoteof(names::Vector{Symbol}, ctx::QuoteContext) = end +# Support for clause constructors. + +abstract type SQLSyntaxCtor{T} +end + +SQLSyntaxCtor{C}(args...; tail = nothing, kws...) where {C<:AbstractSQLClause} = + SQLSyntax(tail, C(args...; kws...)) + +function dissect(scr::Symbol, ::Type{SQLSyntaxCtor{C}}, pats::Vector{Any}) where {C<:AbstractSQLClause} + head_pats = Any[] + tail_pats = Any[] + for pat in pats + if pat isa Expr && pat.head === :kw && length(pat.args) == 2 && pat.args[1] === :tail + push!(tail_pats, pat.args[2]) + else + push!(head_pats, pat) + end + end + scr_head = gensym(:scr_head) + head_ex = Expr(:&&, :($scr_head isa $C), Any[dissect(scr_head, pat) for pat in head_pats]...) + ex = Expr(:&&, :($scr isa SQLSyntax), :(local $scr_head = $scr.head; $head_ex)) + if !isempty(tail_pats) + scr_tail = gensym(:scr_tail) + tail_ex = Expr(:&&, Any[dissect(scr_tail, pat) for pat in tail_pats]...) + push!(ex.args, :(local $scr_tail = $scr.tail; $tail_ex)) + end + ex +end + + # Concrete clause types. include("clauses/aggregate.jl") diff --git a/src/clauses/aggregate.jl b/src/clauses/aggregate.jl index 81ff08d4..fc385796 100644 --- a/src/clauses/aggregate.jl +++ b/src/clauses/aggregate.jl @@ -2,23 +2,23 @@ mutable struct AggregateClause <: AbstractSQLClause name::Symbol - args::Vector{SQLClause} - filter::Union{SQLClause, Nothing} - over::Union{SQLClause, Nothing} + args::Vector{SQLSyntax} + filter::Union{SQLSyntax, Nothing} + over::Union{SQLSyntax, Nothing} AggregateClause(; name::Union{Symbol, AbstractString}, - args = SQLClause[], + args = SQLSyntax[], filter = nothing, over = nothing) = new(Symbol(name), args, filter, over) end -AggregateClause(name; args = SQLClause[], filter = nothing, over = nothing) = - AggregateClause(name = name, args = args, filter = filter, over = over) +AggregateClause(name; args = SQLSyntax[], filter = nothing, over = nothing) = + AggregateClause(; name, args, filter, over) AggregateClause(name, args...; filter = nothing, over = nothing) = - AggregateClause(name, args = SQLClause[args...], filter = filter, over = over) + AggregateClause(; name, args = SQLSyntax[args...], filter, over) """ AGG(; name, args = [], filter = nothing, over = nothing) @@ -30,34 +30,30 @@ An application of an aggregate function. # Examples ```jldoctest -julia> c = AGG(:max, :year_of_birth); +julia> s = AGG(:max, :year_of_birth); -julia> print(render(c)) +julia> print(render(s)) max("year_of_birth") ``` ```jldoctest -julia> c = AGG(:count, filter = FUN(">", :year_of_birth, 1970)); +julia> s = AGG(:count, filter = FUN(">", :year_of_birth, 1970)); -julia> print(render(c)) +julia> print(render(s)) (count(*) FILTER (WHERE ("year_of_birth" > 1970))) ``` ```jldoctest -julia> c = AGG(:row_number, over = PARTITION(:year_of_birth)); +julia> s = AGG(:row_number, over = PARTITION(:year_of_birth)); -julia> print(render(c)) +julia> print(render(s)) (row_number() OVER (PARTITION BY "year_of_birth")) ``` """ -AGG(args...; kws...) = - AggregateClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(AGG), pats::Vector{Any}) = - dissect(scr, AggregateClause, pats) +const AGG = SQLSyntaxCtor{AggregateClause} function PrettyPrinting.quoteof(c::AggregateClause, ctx::QuoteContext) - ex = Expr(:call, nameof(AGG), string(c.name)) + ex = Expr(:call, :AGG, string(c.name)) append!(ex.args, quoteof(c.args, ctx)) if c.filter !== nothing push!(ex.args, Expr(:kw, :filter, quoteof(c.filter, ctx))) @@ -67,6 +63,3 @@ function PrettyPrinting.quoteof(c::AggregateClause, ctx::QuoteContext) end ex end - -rebase(c::AggregateClause, c′) = - AggregateClause(name = c.name, args = c.args, filter = c.filter, over = rebase(c.over, c′)) diff --git a/src/clauses/as.jl b/src/clauses/as.jl index 58369f5b..bff645aa 100644 --- a/src/clauses/as.jl +++ b/src/clauses/as.jl @@ -1,65 +1,51 @@ # AS clause. mutable struct AsClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} name::Symbol columns::Union{Vector{Symbol}, Nothing} AsClause(; - over = nothing, name::Union{Symbol, AbstractString}, columns::Union{AbstractVector{<:Union{Symbol, AbstractString}}, Nothing} = nothing) = - new(over, - Symbol(name), + new(Symbol(name), !(columns === nothing || columns isa Vector{Symbol}) ? Symbol[Symbol(col) for col in columns] : columns) end -AsClause(name; over = nothing, columns = nothing) = - AsClause(over = over, name = name, columns = columns) +AsClause(name; columns = nothing) = + AsClause(; name, columns) """ - AS(; over = nothing, name, columns = nothing) - AS(name; over = nothing, columns = nothing) + AS(; name, columns = nothing, tail = nothing) + AS(name; columns = nothing, tail = nothing) An `AS` clause. # Examples ```jldoctest -julia> c = ID(:person) |> AS(:p); +julia> s = ID(:person) |> AS(:p); -julia> print(render(c)) +julia> print(render(s)) "person" AS "p" ``` ```jldoctest -julia> c = ID(:person) |> AS(:p, columns = [:person_id, :year_of_birth]); +julia> s = ID(:person) |> AS(:p, columns = [:person_id, :year_of_birth]); -julia> print(render(c)) +julia> print(render(s)) "person" AS "p" ("person_id", "year_of_birth") ``` """ -AS(args...; kws...) = - AsClause(args...; kws...) |> SQLClause +const AS = SQLSyntaxCtor{AsClause} -dissect(scr::Symbol, ::typeof(AS), pats::Vector{Any}) = - dissect(scr, AsClause, pats) - -Base.convert(::Type{AbstractSQLClause}, p::Pair{<:Union{Symbol, AbstractString}}) = - AsClause(name = first(p), over = convert(SQLClause, last(p))) +Base.convert(::Type{SQLSyntax}, p::Pair{Symbol}) = + SQLSyntax(last(p), AsClause(name = first(p))) function PrettyPrinting.quoteof(c::AsClause, ctx::QuoteContext) - ex = Expr(:call, nameof(AS), quoteof(c.name)) + ex = Expr(:call, :AS, quoteof(c.name)) if c.columns !== nothing push!(ex.args, Expr(:kw, :columns, Expr(:vect, quoteof(c.columns, ctx)...))) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::AsClause, c′) = - AsClause(over = rebase(c.over, c′), name = c.name, columns = c.columns) - diff --git a/src/clauses/from.jl b/src/clauses/from.jl index 5b05f99e..19856e34 100644 --- a/src/clauses/from.jl +++ b/src/clauses/from.jl @@ -1,45 +1,28 @@ # FROM clause. mutable struct FromClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - - FromClause(; over = nothing) = - new(over) end -FromClause(over) = - FromClause(over = over) - """ - FROM(; over = nothing) - FROM(over) + FROM(; tail = nothing) + FROM(source) A `FROM` clause. # Examples ```jldoctest -julia> c = ID(:person) |> AS(:p) |> FROM() |> SELECT((:p, :person_id)); +julia> s = ID(:person) |> AS(:p) |> FROM() |> SELECT((:p, :person_id)); -julia> print(render(c)) +julia> print(render(s)) SELECT "p"."person_id" FROM "person" AS "p" ``` """ -FROM(args...; kws...) = - FromClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(FROM), pats::Vector{Any}) = - dissect(scr, FromClause, pats) - -function PrettyPrinting.quoteof(c::FromClause, ctx::QuoteContext) - ex = Expr(:call, nameof(FROM)) - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end - ex -end +const FROM = SQLSyntaxCtor{FromClause} -rebase(c::FromClause, c′) = - FromClause(over = rebase(c.over, c′)) +FROM(source) = + FROM(tail = source) +PrettyPrinting.quoteof(c::FromClause, ctx::QuoteContext) = + Expr(:call, :FROM) diff --git a/src/clauses/function.jl b/src/clauses/function.jl index 5dc30417..e868ddba 100644 --- a/src/clauses/function.jl +++ b/src/clauses/function.jl @@ -2,19 +2,19 @@ mutable struct FunctionClause <: AbstractSQLClause name::Symbol - args::Vector{SQLClause} + args::Vector{SQLSyntax} FunctionClause(; name::Union{Symbol, AbstractString}, - args = SQLClause[]) = + args = SQLSyntax[]) = new(Symbol(name), args) end -FunctionClause(name; args = SQLClause[]) = - FunctionClause(name = name, args = args) +FunctionClause(name; args = SQLSyntax[]) = + FunctionClause(; name, args) FunctionClause(name, args...) = - FunctionClause(name, args = SQLClause[args...]) + FunctionClause(; name, args = SQLSyntax[args...]) """ FUN(; name, args = []) @@ -26,34 +26,30 @@ An invocation of a SQL function or a SQL operator. # Examples ```jldoctest -julia> c = FUN(:concat, :city, ", ", :state); +julia> s = FUN(:concat, :city, ", ", :state); -julia> print(render(c)) +julia> print(render(s)) concat("city", ', ', "state") ``` ```jldoctest -julia> c = FUN("||", :city, ", ", :state); +julia> s = FUN("||", :city, ", ", :state); -julia> print(render(c)) +julia> print(render(s)) ("city" || ', ' || "state") ``` ```jldoctest -julia> c = FUN("SUBSTRING(? FROM ? FOR ?)", :zip, 1, 3); +julia> s = FUN("SUBSTRING(? FROM ? FOR ?)", :zip, 1, 3); -julia> print(render(c)) +julia> print(render(s)) SUBSTRING("zip" FROM 1 FOR 3) ``` """ -FUN(args...; kws...) = - FunctionClause(args...; kws...) |> SQLClause +const FUN = SQLSyntaxCtor{FunctionClause} -Base.convert(::Type{AbstractSQLClause}, ::typeof(*)) = - FunctionClause(:*) - -dissect(scr::Symbol, ::typeof(FUN), pats::Vector{Any}) = - dissect(scr, FunctionClause, pats) +Base.convert(::Type{SQLSyntax}, ::typeof(*)) = + FUN(:*) PrettyPrinting.quoteof(c::FunctionClause, ctx::QuoteContext) = - Expr(:call, nameof(FUN), string(c.name), quoteof(c.args, ctx)...) + Expr(:call, :FUN, string(c.name), quoteof(c.args, ctx)...) diff --git a/src/clauses/group.jl b/src/clauses/group.jl index c32d905d..d4035e89 100644 --- a/src/clauses/group.jl +++ b/src/clauses/group.jl @@ -19,15 +19,13 @@ end import .GROUPING_MODE.GroupingMode mutable struct GroupClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - by::Vector{SQLClause} + by::Vector{SQLSyntax} sets::Union{Vector{Vector{Int}}, GroupingMode, Nothing} function GroupClause(; - over = nothing, - by = SQLClause[], + by = SQLSyntax[], sets = nothing) - c = new(over, by, sets isa Symbol ? convert(GroupingMode, sets) : sets) + c = new(by, sets isa Symbol ? convert(GroupingMode, sets) : sets) s = c.sets if s isa Vector{Vector{Int}} && !checkbounds(Bool, c.by, s) throw(DomainError(s, "sets are out of bounds")) @@ -36,23 +34,23 @@ mutable struct GroupClause <: AbstractSQLClause end end -GroupClause(by...; over = nothing, sets = nothing) = - GroupClause(over = over, by = SQLClause[by...], sets = sets) +GroupClause(by...; sets = nothing) = + GroupClause(; by = SQLSyntax[by...], sets) """ - GROUP(; over = nothing, by = [], sets = nothing) - GROUP(by...; over = nothing, sets = nothing) + GROUP(; by = [], sets = nothing, tail = nothing) + GROUP(by...; sets = nothing, tail = nothing) A `GROUP BY` clause. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> GROUP(:year_of_birth) |> SELECT(:year_of_birth, AGG(:count)); -julia> print(render(c)) +julia> print(render(s)) SELECT "year_of_birth", count(*) @@ -60,25 +58,14 @@ FROM "person" GROUP BY "year_of_birth" ``` """ -GROUP(args...; kws...) = - GroupClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(GROUP), pats::Vector{Any}) = - dissect(scr, GroupClause, pats) +const GROUP = SQLSyntaxCtor{GroupClause} function PrettyPrinting.quoteof(c::GroupClause, ctx::QuoteContext) - ex = Expr(:call, nameof(GROUP)) + ex = Expr(:call, :GROUP) append!(ex.args, quoteof(c.by, ctx)) s = c.sets if s !== nothing push!(ex.args, Expr(:kw, :sets, s isa GroupingMode ? QuoteNode(Symbol(s)) : s)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::GroupClause, c′) = - GroupClause(over = rebase(c.over, c′), by = c.by, sets = c.sets) - diff --git a/src/clauses/having.jl b/src/clauses/having.jl index 982a9b88..d014ec99 100644 --- a/src/clauses/having.jl +++ b/src/clauses/having.jl @@ -1,53 +1,37 @@ # HAVING clause. mutable struct HavingClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - condition::SQLClause + condition::SQLSyntax - HavingClause(; - over = nothing, - condition) = - new(over, condition) + HavingClause(; condition) = + new(condition) end -HavingClause(condition; over = nothing) = - HavingClause(over = over, condition = condition) +HavingClause(condition) = + HavingClause(; condition) """ - HAVING(; over = nothing, condition) - HAVING(condition; over = nothing) + HAVING(; condition, tail = nothing) + HAVING(condition, tail = nothing) A `HAVING` clause. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> GROUP(:year_of_birth) |> HAVING(FUN(">", AGG(:count), 10)) |> SELECT(:person_id); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id" FROM "person" GROUP BY "year_of_birth" HAVING (count(*) > 10) ``` """ -HAVING(args...; kws...) = - HavingClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(HAVING), pats::Vector{Any}) = - dissect(scr, HavingClause, pats) - -function PrettyPrinting.quoteof(c::HavingClause, ctx::QuoteContext) - ex = Expr(:call, nameof(HAVING), quoteof(c.condition, ctx)) - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end - ex -end - -rebase(c::HavingClause, c′) = - HavingClause(over = rebase(c.over, c′), condition = c.condition) +const HAVING = SQLSyntaxCtor{HavingClause} +PrettyPrinting.quoteof(c::HavingClause, ctx::QuoteContext) = + Expr(:call, :HAVING, quoteof(c.condition, ctx)) diff --git a/src/clauses/identifier.jl b/src/clauses/identifier.jl index 23020890..e265ba66 100644 --- a/src/clauses/identifier.jl +++ b/src/clauses/identifier.jl @@ -1,77 +1,74 @@ -# SQL identifier (possibly qualified). +# SQL identifier. mutable struct IdentifierClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} name::Symbol - IdentifierClause(; - over = nothing, - name::Union{Symbol, AbstractString}) = - new(over, Symbol(name)) + IdentifierClause(; name::Union{Symbol, AbstractString}) = + new(Symbol(name)) end -IdentifierClause(name; over = nothing) = - IdentifierClause(over = over, name = name) - -function IdentifierClause(qualifiers, name) - over = nothing - for ql in qualifiers - over = IdentifierClause(over = over, name = ql) |> SQLClause - end - IdentifierClause(over = over, name = name) -end +IdentifierClause(name) = + IdentifierClause(; name) """ - ID(; over = nothing, name) - ID(name; over = nothing) - ID(qualifiers, name) + ID(; name, tail = nothing) + ID(name; tail = nothing) + ID(qualifiers..., name; tail = nothing) -A SQL identifier. Specify `over` or use the `|>` operator to make a qualified -identifier. +A SQL identifier. Use the `|>` operator to make a qualified identifier. # Examples ```jldoctest -julia> c = ID(:person); +julia> s = ID(:person); -julia> print(render(c)) +julia> print(render(s)) "person" ``` ```jldoctest -julia> c = ID(:p) |> ID(:birth_datetime); +julia> s = ID(:p) |> ID(:birth_datetime); -julia> print(render(c)) +julia> print(render(s)) "p"."birth_datetime" ``` ```jldoctest -julia> c = ID([:pg_catalog], :pg_database); +julia> s = ID([:pg_catalog], :pg_database); -julia> print(render(c)) +julia> print(render(s)) "pg_catalog"."pg_database" ``` """ -ID(args...; kws...) = - IdentifierClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(ID), pats::Vector{Any}) = - dissect(scr, IdentifierClause, pats) +const ID = SQLSyntaxCtor{IdentifierClause} -Base.convert(::Type{AbstractSQLClause}, name::Symbol) = - IdentifierClause(name) +ID(qualifier::Union{Symbol, AbstractString}, name::Union{Symbol, AbstractString}; tail = nothing) = + ID(tail = ID(qualifier; tail), name = name) -Base.convert(::Type{AbstractSQLClause}, qname::Tuple{Symbol, Symbol}) = - IdentifierClause(qname[2], over = IdentifierClause(qname[1])) +ID(name1::Union{Symbol, AbstractString}, name2::Union{Symbol, AbstractString}, names::Union{Symbol, AbstractString}...; tail = nothing) = + ID(tail = ID(name1; tail), name2, names...) -function PrettyPrinting.quoteof(c::IdentifierClause, ctx::QuoteContext) - ex = Expr(:call, nameof(ID), quoteof(c.name)) - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) +function ID(qualifiers::AbstractVector{<:Union{Symbol, AbstractString}}, name::Union{Symbol, AbstractString}; tail = nothing) + for q in qualifiers + tail = ID(tail = tail, name = q) end - ex + ID(; tail, name) end -rebase(c::IdentifierClause, c′) = - IdentifierClause(over = rebase(c.over, c′), name = c.name) +ID(t::SQLTable) = + ID(t.qualifiers, t.name) + +Base.convert(::Type{SQLSyntax}, name::Symbol) = + ID(name) + +Base.convert(::Type{SQLSyntax}, qname::Tuple{Symbol, Vararg{Symbol}}) = + ID(qname...) + +Base.convert(::Type{SQLSyntax}, qname::Tuple{Vector{Symbol}, Symbol}) = + ID(qname...) + +Base.convert(::Type{SQLSyntax}, t::SQLTable) = + ID(t) +PrettyPrinting.quoteof(c::IdentifierClause, ctx::QuoteContext) = + Expr(:call, :ID, quoteof(c.name)) diff --git a/src/clauses/internal.jl b/src/clauses/internal.jl index 4e88cb7c..53ec6d1c 100644 --- a/src/clauses/internal.jl +++ b/src/clauses/internal.jl @@ -3,22 +3,17 @@ # Context holder for the serialize pass. mutable struct WithContextClause <: AbstractSQLClause - over::SQLClause dialect::SQLDialect columns::Union{Vector{SQLColumn}, Nothing} - WithContextClause(; over, dialect, columns = nothing) = - new(over, dialect, columns) + WithContextClause(; dialect, columns = nothing) = + new(dialect, columns) end -WITH_CONTEXT(args...; kws...) = - WithContextClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(WITH_CONTEXT), pats::Vector{Any}) = - dissect(scr, WithContextClause, pats) +const WITH_CONTEXT = SQLSyntaxCtor{WithContextClause} function PrettyPrinting.quoteof(c::WithContextClause, ctx::QuoteContext) - ex = Expr(:call, nameof(WITH_CONTEXT), Expr(:kw, :over, quoteof(c.over, ctx))) + ex = Expr(:call, :WITH_CONTEXT) if c.dialect !== default_dialect push!(ex.args, Expr(:kw, :dialect, quoteof(c.dialect))) end diff --git a/src/clauses/join.jl b/src/clauses/join.jl index b00245b8..19a2fdc7 100644 --- a/src/clauses/join.jl +++ b/src/clauses/join.jl @@ -1,46 +1,44 @@ # JOIN clause. mutable struct JoinClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - joinee::SQLClause - on::SQLClause + joinee::SQLSyntax + on::SQLSyntax left::Bool right::Bool lateral::Bool JoinClause(; - over = nothing, joinee, on, left = false, right = false, lateral = false) = - new(over, joinee, on, left, right, lateral) + new(joinee, on, left, right, lateral) end -JoinClause(joinee; over = nothing, on, left = false, right = false, lateral = false) = - JoinClause(over = over, joinee = joinee, on = on, left = left, right = right, lateral = lateral) +JoinClause(joinee; on, left = false, right = false, lateral = false) = + JoinClause(; joinee, on, left, right, lateral) -JoinClause(joinee, on; over = nothing, left = false, right = false, lateral = false) = - JoinClause(over = over, joinee = joinee, on = on, left = left, right = right, lateral = lateral) +JoinClause(joinee, on; left = false, right = false, lateral = false) = + JoinClause(; joinee, on, left, right, lateral) """ - JOIN(; over = nothing, joinee, on, left = false, right = false, lateral = false) - JOIN(joinee; over = nothing, on, left = false, right = false, lateral = false) - JOIN(joinee, on; over = nothing, left = false, right = false, lateral = false) + JOIN(; joinee, on, left = false, right = false, lateral = false, tail = nothing) + JOIN(joinee; on, left = false, right = false, lateral = false, tail = nothing) + JOIN(joinee, on; left = false, right = false, lateral = false, tail = nothing) A `JOIN` clause. # Examples ```jldoctest -julia> c = FROM(:p => :person) |> +julia> s = FROM(:p => :person) |> JOIN(:l => :location, on = FUN("=", (:p, :location_id), (:l, :location_id)), left = true) |> SELECT((:p, :person_id), (:l, :state)); -julia> print(render(c)) +julia> print(render(s)) SELECT "p"."person_id", "l"."state" @@ -48,14 +46,10 @@ FROM "person" AS "p" LEFT JOIN "location" AS "l" ON ("p"."location_id" = "l"."location_id") ``` """ -JOIN(args...; kws...) = - JoinClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(JOIN), pats::Vector{Any}) = - dissect(scr, JoinClause, pats) +const JOIN = SQLSyntaxCtor{JoinClause} function PrettyPrinting.quoteof(c::JoinClause, ctx::QuoteContext) - ex = Expr(:call, nameof(JOIN), quoteof([c.joinee, c.on], ctx)...) + ex = Expr(:call, :JOIN, quoteof([c.joinee, c.on], ctx)...) if c.left push!(ex.args, Expr(:kw, :left, c.left)) end @@ -65,13 +59,5 @@ function PrettyPrinting.quoteof(c::JoinClause, ctx::QuoteContext) if c.lateral push!(ex.args, Expr(:kw, :lateral, c.lateral)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::JoinClause, c′) = - JoinClause(over = rebase(c.over, c′), - joinee = c.joinee, on = c.on, left = c.left, right = c.right, lateral = c.lateral) - diff --git a/src/clauses/limit.jl b/src/clauses/limit.jl index be2e3d25..9fca98a7 100644 --- a/src/clauses/limit.jl +++ b/src/clauses/limit.jl @@ -1,57 +1,51 @@ # LIMIT clause. mutable struct LimitClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} offset::Union{Int, Nothing} limit::Union{Int, Nothing} with_ties::Bool LimitClause(; - over = nothing, offset = nothing, limit = nothing, with_ties = false) = - new(over, offset, limit, with_ties) + new(offset, limit, with_ties) end -LimitClause(limit; over = nothing, offset = nothing, with_ties = false) = - LimitClause(over = over, offset = offset, limit = limit, with_ties = with_ties) +LimitClause(limit; offset = nothing, with_ties = false) = + LimitClause(; offset, limit, with_ties = with_ties) -LimitClause(offset, limit; over = nothing, with_ties = false) = - LimitClause(over = over, offset = offset, limit = limit, with_ties = with_ties) +LimitClause(offset, limit; with_ties = false) = + LimitClause(; offset, limit, with_ties) -LimitClause(range::UnitRange, over = nothing, with_ties = false) = - LimitClause(over = over, offset = first(range) - 1, limit = length(range), with_ties = with_ties) +LimitClause(range::UnitRange; with_ties = false) = + LimitClause(; offset = first(range) - 1, limit = length(range), with_ties) """ - LIMIT(; over = nothing, offset = nothing, limit = nothing, with_ties = false) - LIMIT(limit; over = nothing, offset = nothing, with_ties = false) - LIMIT(offset, limit; over = nothing, with_ties = false) - LIMIT(start:stop; over = nothing, with_ties = false) + LIMIT(; offset = nothing, limit = nothing, with_ties = false, tail = nothing) + LIMIT(limit; offset = nothing, with_ties = false, tail = nothing) + LIMIT(offset, limit; with_ties = false, tail = nothing) + LIMIT(start:stop; with_ties = false, tail = nothing) A `LIMIT` clause. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> LIMIT(1) |> SELECT(:person_id); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id" FROM "person" FETCH FIRST 1 ROW ONLY ``` """ -LIMIT(args...; kws...) = - LimitClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(LIMIT), pats::Vector{Any}) = - dissect(scr, LimitClause, pats) +const LIMIT = SQLSyntaxCtor{LimitClause} function PrettyPrinting.quoteof(c::LimitClause, ctx::QuoteContext) - ex = Expr(:call, nameof(LIMIT)) + ex = Expr(:call, :LIMIT) if c.offset !== nothing push!(ex.args, c.offset) end @@ -59,12 +53,5 @@ function PrettyPrinting.quoteof(c::LimitClause, ctx::QuoteContext) if c.with_ties push!(ex.args, Expr(:kw, :with_ties, c.with_ties)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::LimitClause, c′) = - LimitClause(over = rebase(c.over, c′), offset = c.offset, limit = c.limit, with_ties = c.with_ties) - diff --git a/src/clauses/literal.jl b/src/clauses/literal.jl index 4364431e..868a94ca 100644 --- a/src/clauses/literal.jl +++ b/src/clauses/literal.jl @@ -8,7 +8,7 @@ mutable struct LiteralClause <: AbstractSQLClause end LiteralClause(val) = - LiteralClause(val = val) + LiteralClause(; val) """ LIT(; val) @@ -22,28 +22,26 @@ are automatically converted to SQL literals. # Examples ```jldoctest -julia> c = LIT(missing); +julia> s = LIT(missing); -julia> print(render(c)) +julia> print(render(s)) NULL ``` ```jldoctest -julia> c = LIT("SQL is fun!"); +julia> s = LIT("SQL is fun!"); -julia> print(render(c)) +julia> print(render(s)) 'SQL is fun!' ``` """ -LIT(args...; kws...) = - LiteralClause(args...; kws...) |> SQLClause +LIT = SQLSyntaxCtor{LiteralClause} -dissect(scr::Symbol, ::typeof(LIT), pats::Vector{Any}) = - dissect(scr, LiteralClause, pats) +Base.convert(::Type{SQLSyntax}, val::SQLLiteralType) = + LIT(val) -Base.convert(::Type{AbstractSQLClause}, val::SQLLiteralType) = - LiteralClause(val) +terminal(::Type{LiteralClause}) = + true PrettyPrinting.quoteof(c::LiteralClause, ::QuoteContext) = - Expr(:call, nameof(LIT), c.val) - + Expr(:call, :LIT, c.val) diff --git a/src/clauses/note.jl b/src/clauses/note.jl index b1c02d0f..48e06f52 100644 --- a/src/clauses/note.jl +++ b/src/clauses/note.jl @@ -1,52 +1,40 @@ # A free-form annotation. mutable struct NoteClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} text::String postfix::Bool - NoteClause(; over = nothing, text, postfix = false) = - new(over, text, postfix) + NoteClause(; text, postfix = false) = + new(text, postfix) end -NoteClause(text; over = nothing, postfix = false) = - NoteClause(over = over, text = text, postfix = postfix) +NoteClause(text; postfix = false) = + NoteClause(; text, postfix) """ - NOTE(; over = nothing, text, postfix = false) - NOTE(text; over = nothing, postfix = false) + NOTE(; text, postfix = false, tail = nothing) + NOTE(text; postfix = false, tail = nothing) A free-form prefix of postfix annotation. # Examples ```jldoctest -julia> c = FROM(:p => :person) |> +julia> s = FROM(:p => :person) |> NOTE("TABLESAMPLE SYSTEM (50)", postfix = true) |> SELECT((:p, :person_id)); -julia> print(render(c)) +julia> print(render(s)) SELECT "p"."person_id" FROM "person" AS "p" TABLESAMPLE SYSTEM (50) ``` """ -NOTE(args...; kws...) = - NoteClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(NOTE), pats::Vector{Any}) = - dissect(scr, NoteClause, pats) +const NOTE = SQLSyntaxCtor{NoteClause} function PrettyPrinting.quoteof(c::NoteClause, ctx::QuoteContext) - ex = Expr(:call, nameof(NOTE), quoteof(c.text)) + ex = Expr(:call, :NOTE, quoteof(c.text)) if c.postfix push!(ex.args, Expr(:kw, :postfix, c.postfix)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::NoteClause, c′) = - NoteClause(over = rebase(c.over, c′), text = c.text, postfix = c.postfix) - diff --git a/src/clauses/order.jl b/src/clauses/order.jl index cb6504d2..1c2c9c1e 100644 --- a/src/clauses/order.jl +++ b/src/clauses/order.jl @@ -1,52 +1,38 @@ # ORDER BY clause. mutable struct OrderClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - by::Vector{SQLClause} + by::Vector{SQLSyntax} - OrderClause(; - over = nothing, - by = SQLClause[]) = - new(over, by) + OrderClause(; by = SQLSyntax[]) = + new(by) end -OrderClause(by...; over = nothing) = - OrderClause(over = over, by = SQLClause[by...]) +OrderClause(by...) = + OrderClause(by = SQLSyntax[by...]) """ - ORDER(; over = nothing, by = []) - ORDER(by...; over = nothing) + ORDER(; by = [], tail = nothing) + ORDER(by...; tail = nothing) An `ORDER BY` clause. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> ORDER(:year_of_birth) |> SELECT(:person_id); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id" FROM "person" ORDER BY "year_of_birth" ``` """ -ORDER(args...; kws...) = - OrderClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(ORDER), pats::Vector{Any}) = - dissect(scr, OrderClause, pats) +const ORDER = SQLSyntaxCtor{OrderClause} function PrettyPrinting.quoteof(c::OrderClause, ctx::QuoteContext) - ex = Expr(:call, nameof(ORDER)) + ex = Expr(:call, :ORDER) append!(ex.args, quoteof(c.by, ctx)) - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::OrderClause, c′) = - OrderClause(over = rebase(c.over, c′), by = c.by) - diff --git a/src/clauses/partition.jl b/src/clauses/partition.jl index fdd9549d..80c0838f 100644 --- a/src/clauses/partition.jl +++ b/src/clauses/partition.jl @@ -81,32 +81,31 @@ function PrettyPrinting.quoteof(f::PartitionFrame) end mutable struct PartitionClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - by::Vector{SQLClause} - order_by::Vector{SQLClause} + by::Vector{SQLSyntax} + order_by::Vector{SQLSyntax} frame::Union{PartitionFrame, Nothing} - PartitionClause(; over = nothing, by = SQLClause[], order_by = SQLClause[], frame = nothing) = - new(over, by, order_by, frame) + PartitionClause(; by = SQLSyntax[], order_by = SQLSyntax[], frame = nothing) = + new(by, order_by, frame) end -PartitionClause(by...; over = nothing, order_by = SQLClause[], frame = nothing) = - PartitionClause(over = over, by = SQLClause[by...], order_by = order_by, frame = frame) +PartitionClause(by...; order_by = SQLSyntax[], frame = nothing) = + PartitionClause(; by = SQLSyntax[by...], order_by, frame) """ - PARTITION(; over = nothing, by = [], order_by = [], frame = nothing) - PARTITION(by...; over = nothing, order_by = [], frame = nothing) + PARTITION(; by = [], order_by = [], frame = nothing, tail = nothing) + PARTITION(by...; order_by = [], frame = nothing, tail = nothing) A window definition clause. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> SELECT(:person_id, AGG(:row_number, over = PARTITION(:year_of_birth))); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id", (row_number() OVER (PARTITION BY "year_of_birth")) @@ -114,12 +113,12 @@ FROM "person" ``` ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> WINDOW(:w1 => PARTITION(:year_of_birth), :w2 => :w1 |> PARTITION(order_by = [:month_of_birth, :day_of_birth])) |> SELECT(:person_id, AGG(:row_number, over = :w2)); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id", (row_number() OVER ("w2")) @@ -130,7 +129,7 @@ WINDOW ``` ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> GROUP(:year_of_birth) |> SELECT(:year_of_birth, AGG(:avg, @@ -138,7 +137,7 @@ julia> c = FROM(:person) |> over = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -1, finish = 1)))); -julia> print(render(c)) +julia> print(render(s)) SELECT "year_of_birth", (avg(count(*)) OVER (ORDER BY "year_of_birth" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)) @@ -146,14 +145,10 @@ FROM "person" GROUP BY "year_of_birth" ``` """ -PARTITION(args...; kws...) = - PartitionClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(PARTITION), pats::Vector{Any}) = - dissect(scr, PartitionClause, pats) +const PARTITION = SQLSyntaxCtor{PartitionClause} function PrettyPrinting.quoteof(c::PartitionClause, ctx::QuoteContext) - ex = Expr(:call, nameof(PARTITION)) + ex = Expr(:call, :PARTITION) append!(ex.args, quoteof(c.by, ctx)) if !isempty(c.order_by) push!(ex.args, Expr(:kw, :order_by, Expr(:vect, quoteof(c.order_by, ctx)...))) @@ -161,12 +156,5 @@ function PrettyPrinting.quoteof(c::PartitionClause, ctx::QuoteContext) if c.frame !== nothing push!(ex.args, Expr(:kw, :frame, quoteof(c.frame))) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::PartitionClause, c′) = - PartitionClause(over = rebase(c.over, c′), by = c.by, order_by = c.order_by, frame = c.frame) - diff --git a/src/clauses/select.jl b/src/clauses/select.jl index 02a2d547..848d1a9e 100644 --- a/src/clauses/select.jl +++ b/src/clauses/select.jl @@ -24,25 +24,23 @@ function PrettyPrinting.quoteof(t::SelectTop) end mutable struct SelectClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} top::Union{SelectTop, Nothing} distinct::Bool - args::Vector{SQLClause} + args::Vector{SQLSyntax} SelectClause(; - over = nothing, top = nothing, distinct = false, args) = - new(over, top, distinct, args) + new(top, distinct, args) end -SelectClause(args...; over = nothing, top = nothing, distinct = false) = - SelectClause(over = over, top = top, distinct = distinct, args = SQLClause[args...]) +SelectClause(args...; top = nothing, distinct = false) = + SelectClause(; top, distinct, args = SQLSyntax[args...]) """ - SELECT(; over = nothing, top = nothing, distinct = false, args) - SELECT(args...; over = nothing, top = nothing, distinct = false) + SELECT(; top = nothing, distinct = false, args, tail = nothing) + SELECT(args...; top = nothing, distinct = false, tail = nothing) A `SELECT` clause. Unlike raw SQL, `SELECT()` should be placed at the end of a clause chain. @@ -52,31 +50,27 @@ Set `distinct` to `true` to add a `DISTINCT` modifier. # Examples ```jldoctest -julia> c = SELECT(true, false); +julia> s = SELECT(true, false); -julia> print(render(c)) +julia> print(render(s)) SELECT TRUE, FALSE ``` ```jldoctest -julia> c = FROM(:location) |> +julia> s = FROM(:location) |> SELECT(distinct = true, :zip); -julia> print(render(c)) +julia> print(render(s)) SELECT DISTINCT "zip" FROM "location" ``` """ -SELECT(args...; kws...) = - SelectClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(SELECT), pats::Vector{Any}) = - dissect(scr, SelectClause, pats) +const SELECT = SQLSyntaxCtor{SelectClause} function PrettyPrinting.quoteof(c::SelectClause, ctx::QuoteContext) - ex = Expr(:call, nameof(SELECT)) + ex = Expr(:call, :SELECT) if c.top !== nothing push!(ex.args, Expr(:kw, :top, quoteof(c.top))) end @@ -88,12 +82,5 @@ function PrettyPrinting.quoteof(c::SelectClause, ctx::QuoteContext) else append!(ex.args, quoteof(c.args, ctx)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::SelectClause, c′) = - SelectClause(over = rebase(c.over, c′), top = c.top, distinct = c.distinct, args = c.args) - diff --git a/src/clauses/sort.jl b/src/clauses/sort.jl index 4bea8839..e02b0cd0 100644 --- a/src/clauses/sort.jl +++ b/src/clauses/sort.jl @@ -39,46 +39,43 @@ end import .NULLS_ORDER.NullsOrder mutable struct SortClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} value::ValueOrder nulls::Union{NullsOrder, Nothing} SortClause(; - over = nothing, value, nulls = nothing) = - new(over, value, nulls) + new(value, nulls) end -SortClause(value; over = nothing, nulls = nothing) = - SortClause(over = over, value = value, nulls = nulls) +SortClause(value; nulls = nothing) = + SortClause(; value, nulls) """ - SORT(; over = nothing, value, nulls = nothing) - SORT(value; over = nothing, nulls = nothing) - ASC(; over = nothing, nulls = nothing) - DESC(; over = nothing, nulls = nothing) + SORT(; value, nulls = nothing, tail = nothing) + SORT(value; nulls = nothing, tail = nothing) + ASC(; nulls = nothing, tail = nothing) + DESC(; nulls = nothing, tail = nothing) Sort order options. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> ORDER(:year_of_birth |> DESC()) |> SELECT(:person_id); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id" FROM "person" ORDER BY "year_of_birth" DESC ``` """ -SORT(args...; kws...) = - SortClause(args...; kws...) |> SQLClause +SORT = SQLSyntaxCtor{SortClause} """ - ASC(; over = nothing, nulls = nothing) + ASC(; over = nothing, nulls = nothing, tail = nothing) Ascending order indicator. """ @@ -86,33 +83,23 @@ ASC(; kws...) = SORT(VALUE_ORDER.ASC; kws...) """ - DESC(; over = nothing, nulls = nothing) + DESC(; over = nothing, nulls = nothing, tail = nothing) Descending order indicator. """ DESC(; kws...) = SORT(VALUE_ORDER.DESC; kws...) -dissect(scr::Symbol, ::typeof(SORT), pats::Vector{Any}) = - dissect(scr, SortClause, pats) - function PrettyPrinting.quoteof(c::SortClause, ctx::QuoteContext) if c.value == VALUE_ORDER.ASC - ex = Expr(:call, nameof(ASC)) + ex = Expr(:call, :ASC) elseif c.value == VALUE_ORDER.DESC - ex = Expr(:call, nameof(DESC)) + ex = Expr(:call, :DESC) else - ex = Expr(:call, nameof(SORT), QuoteNode(Symbol(c.value))) + ex = Expr(:call, :SORT, QuoteNode(Symbol(c.value))) end if c.nulls !== nothing push!(ex.args, Expr(:kw, :nulls, QuoteNode(Symbol(c.nulls)))) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::SortClause, c′) = - SortClause(over = rebase(c.over, c′), value = c.value, nulls = c.nulls) - diff --git a/src/clauses/union.jl b/src/clauses/union.jl index 620aebf7..b25ea237 100644 --- a/src/clauses/union.jl +++ b/src/clauses/union.jl @@ -1,36 +1,34 @@ # UNION clause. mutable struct UnionClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} all::Bool - args::Vector{SQLClause} + args::Vector{SQLSyntax} UnionClause(; - over = nothing, all = false, args) = - new(over, all, args) + new(all, args) end -UnionClause(args...; over = nothing, all = false) = - UnionClause(over = over, all = all, args = SQLClause[args...]) +UnionClause(args...; all = false) = + UnionClause(; all, args = SQLSyntax[args...]) """ - UNION(; over = nothing, all = false, args) - UNION(args...; over = nothing, all = false) + UNION(; all = false, args, tail = nothing) + UNION(args...; all = false, tail = nothing) A `UNION` clause. # Examples ```jldoctest -julia> c = FROM(:measurement) |> +julia> s = FROM(:measurement) |> SELECT(:person_id, :date => :measurement_date) |> UNION(all = true, FROM(:observation) |> SELECT(:person_id, :date => :observation_date)); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id", "measurement_date" AS "date" @@ -42,14 +40,10 @@ SELECT FROM "observation" ``` """ -UNION(args...; kws...) = - UnionClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(UNION), pats::Vector{Any}) = - dissect(scr, UnionClause, pats) +const UNION = SQLSyntaxCtor{UnionClause} function PrettyPrinting.quoteof(c::UnionClause, ctx::QuoteContext) - ex = Expr(:call, nameof(UNION)) + ex = Expr(:call, :UNION) if c.all !== false push!(ex.args, Expr(:kw, :all, c.all)) end @@ -58,12 +52,5 @@ function PrettyPrinting.quoteof(c::UnionClause, ctx::QuoteContext) else append!(ex.args, quoteof(c.args, ctx)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::UnionClause, c′) = - UnionClause(over = rebase(c.over, c′), args = c.args, all = c.all) - diff --git a/src/clauses/values.jl b/src/clauses/values.jl index 34480941..305af25c 100644 --- a/src/clauses/values.jl +++ b/src/clauses/values.jl @@ -19,21 +19,19 @@ A `VALUES` clause. # Examples ```jldoctest -julia> c = VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]); +julia> s = VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]); -julia> print(render(c)) +julia> print(render(s)) VALUES ('SQL', 1974), ('Julia', 2012), ('FunSQL', 2021) ``` """ -VALUES(args...; kws...) = - ValuesClause(args...; kws...) |> SQLClause +VALUES = SQLSyntaxCtor{ValuesClause} -dissect(scr::Symbol, ::typeof(VALUES), pats::Vector{Any}) = - dissect(scr, ValuesClause, pats) +terminal(::Type{ValuesClause}) = + true PrettyPrinting.quoteof(c::ValuesClause, ::QuoteContext) = - Expr(:call, nameof(VALUES), c.rows) - + Expr(:call, :VALUES, c.rows) diff --git a/src/clauses/variable.jl b/src/clauses/variable.jl index ba91ea1c..c943d8ff 100644 --- a/src/clauses/variable.jl +++ b/src/clauses/variable.jl @@ -19,18 +19,16 @@ A placeholder in a parameterized query. # Examples ```jldoctest -julia> c = VAR(:year); +julia> s = VAR(:year); -julia> print(render(c)) +julia> print(render(s)) :year ``` """ -VAR(args...; kws...) = - VariableClause(args...; kws...) |> SQLClause +const VAR = SQLSyntaxCtor{VariableClause} -dissect(scr::Symbol, ::typeof(VAR), pats::Vector{Any}) = - dissect(scr, VariableClause, pats) +terminal(::Type{VariableClause}) = + true PrettyPrinting.quoteof(c::VariableClause, ctx::QuoteContext) = - Expr(:call, nameof(VAR), quoteof(c.name)) - + Expr(:call, :VAR, quoteof(c.name)) diff --git a/src/clauses/where.jl b/src/clauses/where.jl index c5484ab3..5b73d3ff 100644 --- a/src/clauses/where.jl +++ b/src/clauses/where.jl @@ -1,51 +1,35 @@ # WHERE clause. mutable struct WhereClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - condition::SQLClause + condition::SQLSyntax - WhereClause(; - over = nothing, - condition) = - new(over, condition) + WhereClause(; condition) = + new(condition) end -WhereClause(condition; over = nothing) = - WhereClause(over = over, condition = condition) +WhereClause(condition) = + WhereClause(; condition) """ - WHERE(; over = nothing, condition) - WHERE(condition; over = nothing) + WHERE(; condition, tail = nothing) + WHERE(condition; tail = nothing) A `WHERE` clause. # Examples ```jldoctest -julia> c = FROM(:location) |> +julia> s = FROM(:location) |> WHERE(FUN("=", :zip, "60614")) |> SELECT(:location_id); -julia> print(render(c)) +julia> print(render(s)) SELECT "location_id" FROM "location" WHERE ("zip" = '60614') ``` """ -WHERE(args...; kws...) = - WhereClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(WHERE), pats::Vector{Any}) = - dissect(scr, WhereClause, pats) - -function PrettyPrinting.quoteof(c::WhereClause, ctx::QuoteContext) - ex = Expr(:call, nameof(WHERE), quoteof(c.condition, ctx)) - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end - ex -end - -rebase(c::WhereClause, c′) = - WhereClause(over = rebase(c.over, c′), condition = c.condition) +const WHERE = SQLSyntaxCtor{WhereClause} +PrettyPrinting.quoteof(c::WhereClause, ctx::QuoteContext) = + Expr(:call, :WHERE, quoteof(c.condition, ctx)) diff --git a/src/clauses/window.jl b/src/clauses/window.jl index 4df1ae06..62fc8a59 100644 --- a/src/clauses/window.jl +++ b/src/clauses/window.jl @@ -1,33 +1,30 @@ # WINDOW clause. mutable struct WindowClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} - args::Vector{SQLClause} + args::Vector{SQLSyntax} - WindowClause(; - over = nothing, - args) = - new(over, args) + WindowClause(; args) = + new(args) end -WindowClause(args...; over = nothing) = - WindowClause(over = over, args = SQLClause[args...]) +WindowClause(args...) = + WindowClause(args = SQLSyntax[args...]) """ - WINDOW(; over = nothing, args) - WINDOW(args...; over = nothing) + WINDOW(; args, tail = nothing) + WINDOW(args...; tail = nothing) A `WINDOW` clause. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> WINDOW(:w1 => PARTITION(:year_of_birth), :w2 => :w1 |> PARTITION(order_by = [:month_of_birth, :day_of_birth])) |> SELECT(:person_id, AGG("row_number", over = :w2)); -julia> print(render(c)) +julia> print(render(s)) SELECT "person_id", (row_number() OVER ("w2")) @@ -37,25 +34,14 @@ WINDOW "w2" AS ("w1" ORDER BY "month_of_birth", "day_of_birth") ``` """ -WINDOW(args...; kws...) = - WindowClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(WINDOW), pats::Vector{Any}) = - dissect(scr, WindowClause, pats) +const WINDOW = SQLSyntaxCtor{WindowClause} function PrettyPrinting.quoteof(c::WindowClause, ctx::QuoteContext) - ex = Expr(:call, nameof(WINDOW)) + ex = Expr(:call, :WINDOW) if isempty(c.args) push!(ex.args, Expr(:kw, :args, Expr(:vect))) else append!(ex.args, quoteof(c.args, ctx)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::WindowClause, c′) = - WindowClause(over = rebase(c.over, c′), args = c.args) - diff --git a/src/clauses/with.jl b/src/clauses/with.jl index e2f857b4..5b770786 100644 --- a/src/clauses/with.jl +++ b/src/clauses/with.jl @@ -1,30 +1,28 @@ # WITH clause. mutable struct WithClause <: AbstractSQLClause - over::Union{SQLClause, Nothing} recursive::Bool - args::Vector{SQLClause} + args::Vector{SQLSyntax} WithClause(; - over = nothing, recursive = false, args) = - new(over, recursive, args) + new(recursive, args) end -WithClause(args...; over = nothing, recursive = false) = - WithClause(over = over, recursive = recursive, args = SQLClause[args...]) +WithClause(args...; recursive = false) = + WithClause(; recursive, args = SQLSyntax[args...]) """ - WITH(; over = nothing, recursive = false, args) - WITH(args...; over = nothing, recursive = false) + WITH(; recursive = false, args, tail = nothing) + WITH(args...; recursive = false, tail = nothing) A `WITH` clause. # Examples ```jldoctest -julia> c = FROM(:person) |> +julia> s = FROM(:person) |> WHERE(FUN(:in, :person_id, FROM(:essential_hypertension) |> SELECT(:person_id))) |> @@ -34,7 +32,7 @@ julia> c = FROM(:person) |> SELECT(:person_id) |> AS(:essential_hypertension)); -julia> print(render(c)) +julia> print(render(s)) WITH "essential_hypertension" AS ( SELECT "person_id" FROM "condition_occurrence" @@ -51,7 +49,7 @@ WHERE ("person_id" IN ( ``` ```jldoctest -julia> c = FROM(:essential_hypertension) |> +julia> s = FROM(:essential_hypertension) |> SELECT(*) |> WITH(recursive = true, FROM(:concept) |> @@ -67,7 +65,7 @@ julia> c = FROM(:essential_hypertension) |> SELECT((:c, :concept_id), (:c, :concept_name))) |> AS(:essential_hypertension, columns = [:concept_id, :concept_name])); -julia> print(render(c)) +julia> print(render(s)) WITH RECURSIVE "essential_hypertension" ("concept_id", "concept_name") AS ( SELECT "concept_id", @@ -87,14 +85,10 @@ SELECT * FROM "essential_hypertension" ``` """ -WITH(args...; kws...) = - WithClause(args...; kws...) |> SQLClause - -dissect(scr::Symbol, ::typeof(WITH), pats::Vector{Any}) = - dissect(scr, WithClause, pats) +const WITH = SQLSyntaxCtor{WithClause} function PrettyPrinting.quoteof(c::WithClause, ctx::QuoteContext) - ex = Expr(:call, nameof(WITH)) + ex = Expr(:call, :WITH) if c.recursive !== false push!(ex.args, Expr(:kw, :recursive, c.recursive)) end @@ -103,12 +97,5 @@ function PrettyPrinting.quoteof(c::WithClause, ctx::QuoteContext) else append!(ex.args, quoteof(c.args, ctx)) end - if c.over !== nothing - ex = Expr(:call, :|>, quoteof(c.over, ctx), ex) - end ex end - -rebase(c::WithClause, c′) = - WithClause(over = rebase(c.over, c′), args = c.args, recursive = c.recursive) - diff --git a/src/connections.jl b/src/connections.jl index dc1da7af..473a77d9 100644 --- a/src/connections.jl +++ b/src/connections.jl @@ -87,11 +87,11 @@ end """ DBInterface.prepare(conn::SQLConnection, sql::SQLNode)::SQLStatement - DBInterface.prepare(conn::SQLConnection, sql::SQLClause)::SQLStatement + DBInterface.prepare(conn::SQLConnection, sql::SQLSyntax)::SQLStatement Serialize the query node and return a prepared SQL statement. """ -DBInterface.prepare(conn::SQLConnection, sql::Union{AbstractSQLNode, AbstractSQLClause}) = +DBInterface.prepare(conn::SQLConnection, sql::Union{SQLNode, SQLSyntax}) = DBInterface.prepare(conn, render(conn, sql)) """ @@ -107,20 +107,20 @@ DBInterface.prepare(conn::SQLConnection, str::AbstractString) = """ DBInterface.execute(conn::SQLConnection, sql::SQLNode; params...) - DBInterface.execute(conn::SQLConnection, sql::SQLClause; params...) + DBInterface.execute(conn::SQLConnection, sql::SQLSyntax; params...) Serialize and execute the query node. """ -DBInterface.execute(conn::SQLConnection, sql::Union{AbstractSQLNode, AbstractSQLClause}; params...) = +DBInterface.execute(conn::SQLConnection, sql::Union{SQLNode, SQLSyntax}; params...) = DBInterface.execute(conn, sql, values(params)) """ DBInterface.execute(conn::SQLConnection, sql::SQLNode, params) - DBInterface.execute(conn::SQLConnection, sql::SQLClause, params) + DBInterface.execute(conn::SQLConnection, sql::SQLSyntax, params) Serialize and execute the query node. """ -DBInterface.execute(conn::SQLConnection, sql::Union{AbstractSQLNode, AbstractSQLClause}, params) = +DBInterface.execute(conn::SQLConnection, sql::Union{SQLNode, SQLSyntax}, params) = DBInterface.execute(DBInterface.prepare(conn, sql), params) DBInterface.close!(conn::SQLConnection) = diff --git a/src/dissect.jl b/src/dissect.jl index a1ea2eb5..3eba2056 100644 --- a/src/dissect.jl +++ b/src/dissect.jl @@ -57,7 +57,7 @@ end function dissect(scr::Symbol, ::typeof(|>), pats::Vector{Any}) if length(pats) == 2 && (local call = pats[2]; call isa Expr) if call.head === :call - pat = Expr(:call, call.args..., Expr(:kw, :over, pats[1])) + pat = Expr(:call, call.args..., Expr(:kw, :tail, pats[1])) return dissect(scr, pat) elseif call.head === :|| return Expr(call.head, Any[dissect(scr, Expr(:call, :|>, pats[1], arg)) diff --git a/src/nodes.jl b/src/nodes.jl index 574938e0..c2abafe9 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -16,6 +16,11 @@ abstract type TabularNode <: AbstractSQLNode end function dissect(scr::Symbol, NodeType::Type{<:AbstractSQLNode}, pats::Vector{Any}) + for pat in pats + if pat isa Expr && pat.head === :kw && length(pat.args) == 2 && pat.args[1] === :tail + pat.args[1] = :over + end + end scr_core = gensym(:scr_core) ex = Expr(:&&, :($scr_core isa $NodeType), Any[dissect(scr_core, pat) for pat in pats]...) :($scr isa SQLNode && (local $scr_core = $scr[]; $ex)) @@ -66,6 +71,9 @@ label(::Nothing) = end end +rebase(::Nothing, n′) = + n′ + rebase(n::SQLNode, n′) = convert(SQLNode, rebase(n[], n′)) diff --git a/src/render.jl b/src/render.jl index e20b5327..254dcc71 100644 --- a/src/render.jl +++ b/src/render.jl @@ -10,10 +10,7 @@ Create a [`SQLCatalog`](@ref) object and serialize the query node. render(n; tables = Dict{Symbol, SQLTable}(), dialect = :default, cache = nothing) = render(SQLCatalog(tables = tables, dialect = dialect, cache = cache), n) -render(conn::SQLConnection, n) = - render(conn.catalog, n) - -render(catalog::SQLCatalog, n) = +render(catalog::Union{SQLConnection, SQLCatalog}, n) = render(catalog, convert(SQLNode, n)) """ @@ -71,21 +68,26 @@ function render(catalog::SQLCatalog, n::SQLNode) sql end -render(catalog::SQLCatalog, c::AbstractSQLClause) = - render(catalog.dialect, c) - -render(dialect::SQLDialect, c::AbstractSQLClause) = - render(dialect, convert(SQLClause, c)) +render(conn::SQLConnection, n::SQLNode) = + render(conn.catalog, n) """ render(dialect::Union{SQLConnection, SQLCatalog, SQLDialect}, - clause::SQLClause)::SQLString + syntax::SQLSyntax)::SQLString Serialize the syntax tree of a SQL query. """ -function render(dialect::SQLDialect, c::SQLClause) - c = WITH_CONTEXT(over = c, dialect = dialect) - sql = serialize(c) +function render(dialect::SQLDialect, s::SQLSyntax) + s = WITH_CONTEXT(tail = s, dialect = dialect) + sql = serialize(s) sql end +render(conn::SQLConnection, s::SQLSyntax) = + render(conn.catalog, s) + +render(catalog::SQLCatalog, s::SQLSyntax) = + render(catalog.dialect, s) + +render(dialect::Union{SQLConnection, SQLCatalog, SQLDialect}, s) = + render(dialect, convert(SQLSyntax, s)) diff --git a/src/serialize.jl b/src/serialize.jl index d9657c4f..16d8a8b5 100644 --- a/src/serialize.jl +++ b/src/serialize.jl @@ -6,15 +6,16 @@ mutable struct SerializeContext <: IO level::Int nested::Bool vars::Vector{Symbol} + tail::Union{SQLSyntax, Nothing} - SerializeContext(dialect) = - new(dialect, IOBuffer(), 0, false, Symbol[]) + SerializeContext(dialect, tail) = + new(dialect, IOBuffer(), 0, false, Symbol[], tail) end -function serialize(c::SQLClause) - @dissect(c, WITH_CONTEXT(over = (local c′), dialect = (local dialect), columns = (local columns))) || throw(IllFormedError()) - ctx = SerializeContext(dialect) - serialize!(c′, ctx) +function serialize(s::SQLSyntax) + @dissect(s, WITH_CONTEXT(tail = (local s′), dialect = (local dialect), columns = (local columns))) || throw(IllFormedError()) + ctx = SerializeContext(dialect, s′) + serialize!(ctx) raw = String(take!(ctx.io)) SQLString(raw, columns = columns, vars = ctx.vars) end @@ -82,14 +83,27 @@ end serialize!(val::Dates.AbstractTime, ctx) = print(ctx, '\'', val, '\'') -function serialize!(c::SQLClause, ctx::SerializeContext) - serialize!(c[], ctx) +function serialize!(ctx::SerializeContext) + tail = ctx.tail + if tail !== nothing + ctx.tail = tail.tail + serialize!(tail.head, ctx) + ctx.tail = tail + end nothing end -function serialize!(cs::AbstractVector{SQLClause}, ctx; sep = nothing) +function serialize!(s::SQLSyntax, ctx::SerializeContext) + tail = ctx.tail + ctx.tail = s.tail + serialize!(s.head, ctx) + ctx.tail = tail + nothing +end + +function serialize!(ss::AbstractVector{SQLSyntax}, ctx; sep = nothing) first = true - for c in cs + for s in ss if !first if sep === nothing print(ctx, ", ") @@ -99,20 +113,20 @@ function serialize!(cs::AbstractVector{SQLClause}, ctx; sep = nothing) else first = false end - serialize!(c, ctx) + serialize!(s, ctx) end end -function serialize_lines!(cs::AbstractVector{SQLClause}, ctx; sep = nothing) - !isempty(cs) || return - if length(cs) == 1 +function serialize_lines!(ss::AbstractVector{SQLSyntax}, ctx; sep = nothing) + !isempty(ss) || return + if length(ss) == 1 print(ctx, ' ') - serialize!(cs[1], ctx) + serialize!(ss[1], ctx) else ctx.level += 1 newline(ctx) first = true - for c in cs + for s in ss if !first if sep === nothing print(ctx, ',') @@ -123,7 +137,7 @@ function serialize_lines!(cs::AbstractVector{SQLClause}, ctx; sep = nothing) else first = false end - serialize!(c, ctx) + serialize!(s, ctx) end ctx.level -= 1 end @@ -200,7 +214,7 @@ macro serialize!(tmpl, args, ctx) end end -function serialize_operator!(op::String, args::Vector{SQLClause}, ctx) +function serialize_operator!(op::String, args::Vector{SQLSyntax}, ctx) if isempty(args) print(ctx, op) elseif length(args) == 1 @@ -214,7 +228,7 @@ function serialize_operator!(op::String, args::Vector{SQLClause}, ctx) end end -function serialize_postfix_operator!(op::String, args::Vector{SQLClause}, ctx) +function serialize_postfix_operator!(op::String, args::Vector{SQLSyntax}, ctx) if isempty(args) print(ctx, op) elseif length(args) == 1 @@ -228,13 +242,13 @@ function serialize_postfix_operator!(op::String, args::Vector{SQLClause}, ctx) end end -function serialize_function!(name::String, args::Vector{SQLClause}, ctx) +function serialize_function!(name::String, args::Vector{SQLSyntax}, ctx) print(ctx, name, '(') serialize!(args, ctx) print(ctx, ')') end -function serialize_template!(@nospecialize(chunks::Tuple{Vararg{String}}), args::Vector{SQLClause}, ctx) +function serialize_template!(@nospecialize(chunks::Tuple{Vararg{String}}), args::Vector{SQLSyntax}, ctx) for k = 1:lastindex(chunks)-1 print(ctx, chunks[k]) if k <= length(args) @@ -244,7 +258,7 @@ function serialize_template!(@nospecialize(chunks::Tuple{Vararg{String}}), args: print(ctx, chunks[end]) end -@generated serialize!(::Val{N}, args::Vector{SQLClause}, ctx) where {N} = +@generated serialize!(::Val{N}, args::Vector{SQLSyntax}, ctx) where {N} = :(@serialize! $(string(N)) args ctx) arity(name::Symbol) = @@ -274,7 +288,7 @@ end for (name, op, default) in ((:and, "AND", true), (:or, "OR", false)) @eval begin - function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLClause}, ctx) + function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLSyntax}, ctx) if isempty(args) serialize!($default, ctx) elseif length(args) == 1 @@ -293,7 +307,7 @@ end for (name, op, default) in ((:in, "IN", false), (:not_in, "NOT IN", true)) @eval begin - function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLClause}, ctx) + function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLSyntax}, ctx) if length(args) <= 1 serialize!($default, ctx) else @@ -315,51 +329,51 @@ for (name, op, default) in ((:in, "IN", false), end end -serialize!(::Val{Symbol("not in")}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{Symbol("not in")}, args::Vector{SQLSyntax}, ctx) = serialize!(Val(:not_in), args, ctx) -serialize!(::Val{:not}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{:not}, args::Vector{SQLSyntax}, ctx) = @serialize! "NOT " args ctx arity(::Val{:not}) = 1:1 -serialize!(::Val{:exists}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{:exists}, args::Vector{SQLSyntax}, ctx) = @serialize! "EXISTS " args ctx arity(::Val{:exists}) = 1:1 -serialize!(::Val{:not_exists}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{:not_exists}, args::Vector{SQLSyntax}, ctx) = @serialize! "NOT EXISTS " args ctx arity(::Val{:not_exists}) = 1:1 -serialize!(::Val{:is_null}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{:is_null}, args::Vector{SQLSyntax}, ctx) = @serialize! " IS NULL" args ctx arity(::Val{:is_null}) = 1:1 -serialize!(::Val{Symbol("is null")}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{Symbol("is null")}, args::Vector{SQLSyntax}, ctx) = serialize!(Val(:is_null), args, ctx) -serialize!(::Val{:is_not_null}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{:is_not_null}, args::Vector{SQLSyntax}, ctx) = @serialize! " IS NOT NULL" args ctx arity(::Val{:is_not_null}) = 1:1 -serialize!(::Val{Symbol("is not null")}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{Symbol("is not null")}, args::Vector{SQLSyntax}, ctx) = serialize!(Val(:is_not_null), args, ctx) -serialize!(::Val{:like}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{:like}, args::Vector{SQLSyntax}, ctx) = @serialize! " LIKE " args ctx arity(::Val{:like}) = 2:2 -serialize!(::Val{:not_like}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{:not_like}, args::Vector{SQLSyntax}, ctx) = @serialize! " NOT LIKE " args ctx arity(::Val{:not_like}) = 2:2 -function serialize!(::Val{:count}, args::Vector{SQLClause}, ctx) +function serialize!(::Val{:count}, args::Vector{SQLSyntax}, ctx) print(ctx, "count(") if isempty(args) print(ctx, "*") @@ -371,7 +385,7 @@ end arity(::Val{:count}) = 0:1 -function serialize!(::Val{:count_distinct}, args::Vector{SQLClause}, ctx) +function serialize!(::Val{:count_distinct}, args::Vector{SQLSyntax}, ctx) print(ctx, "count(DISTINCT ") if isempty(args) print(ctx, "*") @@ -383,7 +397,7 @@ end arity(::Val{:count_distinct}) = 0:1 -function serialize!(::Val{:cast}, args::Vector{SQLClause}, ctx) +function serialize!(::Val{:cast}, args::Vector{SQLSyntax}, ctx) if length(args) == 2 && @dissect(args[2], LIT(val = (local t))) && t isa AbstractString print(ctx, "CAST(") serialize!(args[1], ctx) @@ -395,7 +409,7 @@ end arity(::Val{:cast}) = 2:2 -function serialize!(::Val{:concat}, args::Vector{SQLClause}, ctx) +function serialize!(::Val{:concat}, args::Vector{SQLSyntax}, ctx) concat_operator = ctx.dialect.concat_operator if concat_operator !== nothing serialize_operator!(string(concat_operator), args, ctx) @@ -406,7 +420,7 @@ end arity(::Val{:concat}) = 2 -function serialize!(::Val{:extract}, args::Vector{SQLClause}, ctx) +function serialize!(::Val{:extract}, args::Vector{SQLSyntax}, ctx) if length(args) == 2 && @dissect(args[1], LIT(val = (local f))) && f isa AbstractString print(ctx, "EXTRACT(", f, " FROM ") serialize!(args[2], ctx) @@ -421,7 +435,7 @@ arity(::Val{:extract}) = 2:2 for (name, op) in ((:between, " BETWEEN "), (:not_between, " NOT BETWEEN ")) @eval begin - function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLClause}, ctx) + function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLSyntax}, ctx) if length(args) == 3 print(ctx, '(') serialize!(args[1], ctx) @@ -439,13 +453,13 @@ for (name, op) in ((:between, " BETWEEN "), end end -serialize!(::Val{Symbol("not between")}, args::Vector{SQLClause}, ctx) = +serialize!(::Val{Symbol("not between")}, args::Vector{SQLSyntax}, ctx) = serialize!(Val(:not_between), args, ctx) for (name, op) in ((:current_date, "CURRENT_DATE"), (:current_timestamp, "CURRENT_TIMESTAMP")) @eval begin - function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLClause}, ctx) + function serialize!(::Val{$(QuoteNode(name))}, args::Vector{SQLSyntax}, ctx) if isempty(args) print(ctx, $op) else @@ -459,7 +473,7 @@ for (name, op) in ((:current_date, "CURRENT_DATE"), end end -function serialize!(::Val{:case}, args::Vector{SQLClause}, ctx) +function serialize!(::Val{:case}, args::Vector{SQLSyntax}, ctx) print(ctx, "(CASE") nargs = length(args) for (i, arg) in enumerate(args) @@ -496,16 +510,16 @@ function serialize!(c::AggregateClause, ctx) end function serialize!(c::AsClause, ctx) - over = c.over + tail = ctx.tail columns = c.columns - if @dissect(over, PARTITION()) + if @dissect(tail, PARTITION()) @assert columns === nothing serialize!(c.name, ctx) print(ctx, " AS (") - serialize!(over, ctx) + serialize!(ctx) print(ctx, ')') - elseif over !== nothing - serialize!(over, ctx) + elseif tail !== nothing + serialize!(ctx) print(ctx, " AS ") serialize!(c.name, ctx) if columns !== nothing @@ -519,10 +533,9 @@ end function serialize!(c::FromClause, ctx) newline(ctx) print(ctx, "FROM") - over = c.over - if over !== nothing + if ctx.tail !== nothing print(ctx, ' ') - serialize!(over, ctx) + serialize!(ctx) end end @@ -531,10 +544,7 @@ function serialize!(c::FunctionClause, ctx) end function serialize!(c::GroupClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) !isempty(c.by) || return newline(ctx) print(ctx, "GROUP BY") @@ -571,10 +581,7 @@ function serialize!(c::GroupClause, ctx) end function serialize!(c::HavingClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) newline(ctx) print(ctx, "HAVING") if @dissect(c.condition, FUN(name = :and, args = (local args))) && length(args) >= 2 @@ -588,19 +595,15 @@ function serialize!(c::HavingClause, ctx) end function serialize!(c::IdentifierClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) + if ctx.tail !== nothing + serialize!(ctx) print(ctx, '.') end serialize!(c.name, ctx) end function serialize!(c::JoinClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) newline(ctx) cross = !c.left && !c.right && @dissect(c.on, LIT(val = true)) if cross @@ -625,10 +628,7 @@ function serialize!(c::JoinClause, ctx) end function serialize!(c::LimitClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) start = c.offset count = c.limit start !== nothing || count !== nothing || return @@ -673,23 +673,19 @@ serialize!(c::LiteralClause, ctx) = serialize!(c.val, ctx) function serialize!(c::NoteClause, ctx) - over = c.over - if over === nothing + if ctx.tail === nothing print(ctx, c.text) elseif c.postfix - serialize!(over, ctx) + serialize!(ctx) print(ctx, ' ', c.text) else print(ctx, c.text, ' ') - serialize!(over, ctx) + serialize!(ctx) end end function serialize!(c::OrderClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) !isempty(c.by) || return newline(ctx) print(ctx, "ORDER BY") @@ -761,8 +757,8 @@ end function serialize!(c::PartitionClause, ctx) need_space = false - if c.over !== nothing - serialize!(c.over, ctx) + if ctx.tail !== nothing + serialize!(ctx) need_space = true end if !isempty(c.by) @@ -798,7 +794,7 @@ function serialize!(c::SelectClause, ctx) newline(ctx) end ctx.nested = true - over = c.over + tail = ctx.tail limit = nothing with_ties = false offset_0_rows = false @@ -807,9 +803,9 @@ function serialize!(c::SelectClause, ctx) limit = top.limit with_ties = top.with_ties elseif ctx.dialect.limit_style === LIMIT_STYLE.SQLSERVER - if @dissect(over, (local limit_over) |> LIMIT(offset = nothing, limit = (local limit), with_ties = (local with_ties))) - over = limit_over - elseif nested && @dissect(over, ORDER()) + if @dissect(tail, (local limit_tail) |> LIMIT(offset = nothing, limit = (local limit), with_ties = (local with_ties))) + tail = limit_tail + elseif nested && @dissect(tail, ORDER()) offset_0_rows = true end end @@ -824,8 +820,8 @@ function serialize!(c::SelectClause, ctx) print(ctx, " DISTINCT") end serialize_lines!(c.args, ctx) - if over !== nothing - serialize!(over, ctx) + if tail !== nothing + serialize!(tail, ctx) end if offset_0_rows newline(ctx) @@ -840,10 +836,7 @@ function serialize!(c::SelectClause, ctx) end function serialize!(c::SortClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) if c.value == VALUE_ORDER.ASC print(ctx, " ASC") elseif c.value == VALUE_ORDER.DESC @@ -864,10 +857,7 @@ function serialize!(c::UnionClause, ctx) newline(ctx) end ctx.nested = false - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) for arg in c.args newline(ctx) print(ctx, "UNION") @@ -960,10 +950,7 @@ function serialize!(c::VariableClause, ctx) end function serialize!(c::WhereClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) newline(ctx) print(ctx, "WHERE") if @dissect(c.condition, FUN(name = :and, args = (local args))) && length(args) >= 2 @@ -977,10 +964,7 @@ function serialize!(c::WhereClause, ctx) end function serialize!(c::WindowClause, ctx) - over = c.over - if over !== nothing - serialize!(over, ctx) - end + serialize!(ctx) !isempty(c.args) || return newline(ctx) print(ctx, "WINDOW") @@ -1001,7 +985,7 @@ function serialize!(c::WithClause, ctx) else first = false end - if @dissect(arg, AS(name = (local name), columns = (local columns), over = (local arg))) + if @dissect(arg, AS(name = (local name), columns = (local columns), tail = (local arg))) serialize!(name, ctx) if columns !== nothing print(ctx, " (") @@ -1017,7 +1001,5 @@ function serialize!(c::WithClause, ctx) end newline(ctx) end - if c.over !== nothing - serialize!(c.over, ctx) - end + serialize!(ctx) end diff --git a/src/translate.jl b/src/translate.jl index cc997a26..14c3c61a 100644 --- a/src/translate.jl +++ b/src/translate.jl @@ -1,41 +1,41 @@ -# Translating a SQL node graph to a SQL statement. +# Translating a SQL node graph to a SQL syntax tree. # Partially constructed query. struct Assemblage name::Symbol # Base name for the alias. - clause::Union{SQLClause, Nothing} # A SQL subquery (possibly without SELECT clause). - cols::OrderedDict{Symbol, SQLClause} # SELECT arguments, if necessary. + syntax::Union{SQLSyntax, Nothing} # A SQL subquery (possibly without SELECT clause). + cols::OrderedDict{Symbol, SQLSyntax} # SELECT arguments, if necessary. repl::Dict{SQLNode, Symbol} # Maps a reference node to a column alias. - Assemblage(name, clause; cols = OrderedDict{Symbol, SQLClause}(), repl = Dict{SQLNode, Symbol}()) = - new(name, clause, cols, repl) + Assemblage(name, syntax; cols = OrderedDict{Symbol, SQLSyntax}(), repl = Dict{SQLNode, Symbol}()) = + new(name, syntax, cols, repl) end # Pack SELECT arguments. -function complete(cols::OrderedDict{Symbol, SQLClause}) - args = SQLClause[] +function complete(cols::OrderedDict{Symbol, SQLSyntax}) + args = SQLSyntax[] for (name, col) in cols if !(@dissect(col, ID(name = (local id_name))) && id_name == name) - col = AS(over = col, name = name) + col = AS(tail = col, name = name) end push!(args, col) end if isempty(args) - push!(args, AS(over = LIT(missing), name = :_)) + push!(args, AS(tail = LIT(missing), name = :_)) end args end # Add a SELECT clause to a partially assembled subquery (if necessary). function complete(a::Assemblage) - clause = a.clause - if !@dissect(clause, SELECT() || UNION()) + syntax = a.syntax + if !@dissect(syntax, SELECT() || UNION()) args = complete(a.cols) - clause = SELECT(over = clause, args = args) + syntax = SELECT(tail = syntax, args = args) end - @assert clause !== nothing - clause + @assert syntax !== nothing + syntax end # Add a SELECT clause aligned with the exported references. @@ -44,49 +44,49 @@ function complete_aligned(a::Assemblage, ctx) length(a.cols) == length(ctx.refs) && all(a.repl[ref] === name for (name, ref) in zip(keys(a.cols), ctx.refs)) !aligned || return complete(a) - if !@dissect(a.clause, SELECT() || UNION()) + if !@dissect(a.syntax, SELECT() || UNION()) alias = nothing - clause = a.clause + syntax = a.syntax else alias = allocate_alias(ctx, a) - clause = FROM(AS(over = a.clause, name = alias)) + syntax = FROM(AS(tail = a.syntax, name = alias)) end subs = make_subs(a, alias) repl = Dict{SQLNode, Symbol}() - cols = OrderedDict{Symbol, SQLClause}() + cols = OrderedDict{Symbol, SQLSyntax}() for ref in ctx.refs name = repl[ref] = a.repl[ref] cols[name] = subs[ref] end - a′ = Assemblage(a.name, clause, repl = repl, cols = cols) + a′ = Assemblage(a.name, syntax, repl = repl, cols = cols) complete(a′) end -# Build node->clause map assuming that the assemblage will be extended. -function make_subs(a::Assemblage, ::Nothing)::Dict{SQLNode, SQLClause} - subs = Dict{SQLNode, SQLClause}() +# Build node->syntax map assuming that the assemblage will be extended. +function make_subs(a::Assemblage, ::Nothing)::Dict{SQLNode, SQLSyntax} + subs = Dict{SQLNode, SQLSyntax}() for (ref, name) in a.repl subs[ref] = a.cols[name] end subs end -# Build node->clause map assuming that the assemblage will be completed. +# Build node->syntax map assuming that the assemblage will be completed. function make_subs(a::Assemblage, alias::Symbol) - subs = Dict{SQLNode, SQLClause}() - cache = Dict{Symbol, SQLClause}() + subs = Dict{SQLNode, SQLSyntax}() + cache = Dict{Symbol, SQLSyntax}() for (ref, name) in a.repl subs[ref] = get(cache, name) do - ID(over = alias, name = name) + ID(tail = alias, name = name) end end subs end # Build a node->alias map and implicit SELECT columns for a UNION query. -function make_repl_cols(refs::Vector{SQLNode})::Tuple{Dict{SQLNode, Symbol}, OrderedDict{Symbol, SQLClause}} +function make_repl_cols(refs::Vector{SQLNode})::Tuple{Dict{SQLNode, Symbol}, OrderedDict{Symbol, SQLSyntax}} repl = Dict{SQLNode, Symbol}() - cols = OrderedDict{Symbol, SQLClause}() + cols = OrderedDict{Symbol, SQLSyntax}() dups = Dict{Symbol, Int}() for ref in refs name′ = name = label(ref) @@ -107,11 +107,11 @@ function make_repl_cols(refs::Vector{SQLNode})::Tuple{Dict{SQLNode, Symbol}, Ord end # Build a node->alias map and SELECT columns. -function make_repl_cols(trns::Vector{Pair{SQLNode, SQLClause}})::Tuple{Dict{SQLNode, Symbol}, OrderedDict{Symbol, SQLClause}} +function make_repl_cols(trns::Vector{Pair{SQLNode, SQLSyntax}})::Tuple{Dict{SQLNode, Symbol}, OrderedDict{Symbol, SQLSyntax}} repl = Dict{SQLNode, Symbol}() - cols = OrderedDict{Symbol, SQLClause}() + cols = OrderedDict{Symbol, SQLSyntax}() dups = Dict{Symbol, Int}() - renames = Dict{Tuple{Symbol, SQLClause}, Symbol}() + renames = Dict{Tuple{Symbol, SQLSyntax}, Symbol}() for (ref, c) in trns name′ = name = label(ref) k = get(dups, name, 0) + 1 @@ -169,8 +169,8 @@ struct TranslateContext cte_map::Base.ImmutableDict{Tuple{Symbol, Int}, Int} knot::Int refs::Vector{SQLNode} - vars::Base.ImmutableDict{Tuple{Symbol, Int}, SQLClause} - subs::Dict{SQLNode, SQLClause} + vars::Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax} + subs::Dict{SQLNode, SQLSyntax} TranslateContext(; catalog, defs) = new(catalog, @@ -181,8 +181,8 @@ struct TranslateContext Base.ImmutableDict{Tuple{Symbol, Int}, Int}(), 0, SQLNode[], - Base.ImmutableDict{Tuple{Symbol, Int}, SQLClause}(), - Dict{Int, SQLClause}()) + Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax}(), + Dict{Int, SQLSyntax}()) function TranslateContext(ctx::TranslateContext; cte_map = ctx.cte_map, knot = ctx.knot, refs = ctx.refs, vars = ctx.vars, subs = ctx.subs) new(ctx.catalog, @@ -217,43 +217,43 @@ function translate(n::SQLNode) columns = [SQLColumn(base.repl[ref]) for ref in refs] end c = complete_aligned(base, ctx′) - with_args = SQLClause[] + with_args = SQLSyntax[] for cte_a in ctx.ctes !cte_a.external || continue cols = Symbol[name for name in keys(cte_a.a.cols)] if isempty(cols) push!(cols, :_) end - over = complete(cte_a.a) + tail = complete(cte_a.a) materialized = cte_a.materialized if materialized !== nothing - over = NOTE(materialized ? "MATERIALIZED" : "NOT MATERIALIZED", over = over) + tail = NOTE(materialized ? "MATERIALIZED" : "NOT MATERIALIZED", tail = tail) end - arg = AS(name = cte_a.name, columns = cols, over = over) + arg = AS(name = cte_a.name, columns = cols, tail = tail) push!(with_args, arg) end if !isempty(with_args) - c = WITH(over = c, args = with_args, recursive = ctx.recursive[]) + c = WITH(tail = c, args = with_args, recursive = ctx.recursive[]) end - WITH_CONTEXT(over = c, dialect = ctx.catalog.dialect, columns = columns) + WITH_CONTEXT(tail = c, dialect = ctx.catalog.dialect, columns = columns) end function translate(n::SQLNode, ctx) c = get(ctx.subs, n, nothing) if c === nothing - c = convert(SQLClause, translate(n[], ctx)) + c = convert(SQLSyntax, translate(n[], ctx)) end c end function translate(ns::Vector{SQLNode}, ctx) - SQLClause[translate(n, ctx) for n in ns] + SQLSyntax[translate(n, ctx) for n in ns] end translate(::Nothing, ctx) = nothing -function translate(n, ctx::TranslateContext, subs::Dict{SQLNode, SQLClause}) +function translate(n, ctx::TranslateContext, subs::Dict{SQLNode, SQLSyntax}) ctx′ = TranslateContext(ctx, subs = subs) translate(n, ctx′) end @@ -285,7 +285,7 @@ end function translate(n::FunctionNode, ctx) args = translate(n.args, ctx) if n.name === :and - args′ = SQLClause[] + args′ = SQLSyntax[] for arg in args if @dissect(arg, LIT(val = true)) elseif @dissect(arg, FUN(name = :and, args = (local args′′))) @@ -301,7 +301,7 @@ function translate(n::FunctionNode, ctx) return args[1] end elseif n.name === :or - args′ = SQLClause[] + args′ = SQLSyntax[] for arg in args if @dissect(arg, LIT(val = false)) elseif @dissect(arg, FUN(name = :or, args = (local args′′))) @@ -342,7 +342,7 @@ function translate(n::ResolvedNode, ctx) end translate(n::SortNode, ctx) = - SORT(over = translate(n.over, ctx), value = n.value, nulls = n.nulls) + SORT(tail = translate(n.over, ctx), value = n.value, nulls = n.nulls) function translate(n::VariableNode, ctx) VAR(n.name) @@ -389,32 +389,32 @@ function assemble(n::AppendNode, ctx) repl[ref] = repl[uref] end a_name = base.name - cs = SQLClause[] + ss = SQLSyntax[] for (arg, a) in branches if a.name !== a_name a_name = :union end - if @dissect(a.clause, (local over) |> SELECT(args = (local args))) && aligned_columns(urefs, repl, args) && !@dissect(over, ORDER() || LIMIT()) - push!(cs, a.clause) + if @dissect(a.syntax, (local tail) |> SELECT(args = (local args))) && aligned_columns(urefs, repl, args) && !@dissect(tail, ORDER() || LIMIT()) + push!(ss, a.syntax) continue - elseif !@dissect(a.clause, SELECT() || UNION() || ORDER() || LIMIT()) + elseif !@dissect(a.syntax, SELECT() || UNION() || ORDER() || LIMIT()) alias = nothing - tail = a.clause + tail = a.syntax else alias = allocate_alias(ctx, a) - tail = FROM(AS(over = complete(a), name = alias)) + tail = FROM(AS(tail = complete(a), name = alias)) end subs = make_subs(a, alias) - cols = OrderedDict{Symbol, SQLClause}() + cols = OrderedDict{Symbol, SQLSyntax}() for ref in urefs name = repl[ref] cols[name] = subs[ref] end - c = SELECT(over = tail, args = complete(cols)) - push!(cs, c) + s = SELECT(tail = tail, args = complete(cols)) + push!(ss, s) end - c = UNION(over = cs[1], all = true, args = cs[2:end]) - Assemblage(a_name, c, repl = repl, cols = dummy_cols) + s = UNION(tail = ss[1], all = true, args = ss[2:end]) + Assemblage(a_name, s, repl = repl, cols = dummy_cols) end function assemble(n::AsNode, ctx) @@ -435,7 +435,7 @@ function assemble(n::AsNode, ctx) repl′[ref] = base.repl[ref] end end - Assemblage(n.name, base.clause, cols = base.cols, repl = repl′) + Assemblage(n.name, base.syntax, cols = base.cols, repl = repl′) end function assemble(n::BindNode, ctx) @@ -450,20 +450,20 @@ end function assemble(n::DefineNode, ctx) base = assemble(n.over, ctx) - if !@dissect(base.clause, SELECT() || UNION()) + if !@dissect(base.syntax, SELECT() || UNION()) base_alias = nothing - c = base.clause + s = base.syntax else base_alias = allocate_alias(ctx, base) - c = FROM(AS(over = complete(base), name = base_alias)) + s = FROM(AS(tail = complete(base), name = base_alias)) end subs = make_subs(base, base_alias) - tr_cache = Dict{Symbol, SQLClause}() + tr_cache = Dict{Symbol, SQLSyntax}() for (f, i) in n.label_map tr_cache[f] = translate(n.args[i], ctx, subs) end repl = Dict{SQLNode, Symbol}() - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) && name in keys(tr_cache) push!(trns, ref => tr_cache[name]) @@ -472,7 +472,7 @@ function assemble(n::DefineNode, ctx) end end repl, cols = make_repl_cols(trns) - Assemblage(base.name, c, cols = cols, repl = repl) + Assemblage(base.name, s, cols = cols, repl = repl) end function assemble(n::FromFunctionNode, ctx) @@ -486,11 +486,11 @@ function assemble(n::FromFunctionNode, ctx) end over = translate(n.over, ctx) alias = allocate_alias(ctx, label(n.over)) - c = FROM(AS(over = over, name = alias, columns = n.columns)) - cols = OrderedDict{Symbol, SQLClause}() + s = FROM(AS(tail = over, name = alias, columns = n.columns)) + cols = OrderedDict{Symbol, SQLSyntax}() for col in n.columns col in seen || continue - cols[col] = ID(over = alias, name = col) + cols[col] = ID(tail = alias, name = col) end repl = Dict{SQLNode, Symbol}() for ref in ctx.refs @@ -498,22 +498,22 @@ function assemble(n::FromFunctionNode, ctx) repl[ref] = name end end - Assemblage(label(n.over), c, cols = cols, repl = repl) + Assemblage(label(n.over), s, cols = cols, repl = repl) end function assemble(n::FromIterateNode, ctx) cte_a = ctx.ctes[ctx.knot] name = cte_a.a.name alias = allocate_alias(ctx, name) - tbl = ID(cte_a.qualifiers, cte_a.name) - c = FROM(AS(over = tbl, name = alias)) + tbl = convert(SQLSyntax, (cte_a.qualifiers, cte_a.name)) + s = FROM(AS(tail = tbl, name = alias)) subs = make_subs(cte_a.a, alias) - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - return Assemblage(name, c, cols = cols, repl = repl) + return Assemblage(name, s, cols = cols, repl = repl) end assemble(::FromNothingNode, ctx) = @@ -525,21 +525,21 @@ function unwrap_repl(a::Assemblage) @dissect(ref, (local over) |> Nested()) || error() repl′[over] = name end - Assemblage(a.name, a.clause, cols = a.cols, repl = repl′) + Assemblage(a.name, a.syntax, cols = a.cols, repl = repl′) end function assemble(n::FromTableExpressionNode, ctx) cte_a = ctx.ctes[ctx.cte_map[(n.name, n.depth)]] alias = allocate_alias(ctx, n.name) - tbl = ID(cte_a.qualifiers, cte_a.name) - c = FROM(AS(over = tbl, name = alias)) + tbl = convert(SQLSyntax, (cte_a.qualifiers, cte_a.name)) + s = FROM(AS(tail = tbl, name = alias)) subs = make_subs(unwrap_repl(cte_a.a), alias) - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - return Assemblage(n.name, c, cols = cols, repl = repl) + return Assemblage(n.name, s, cols = cols, repl = repl) end function assemble(n::FromTableNode, ctx) @@ -551,12 +551,12 @@ function assemble(n::FromTableNode, ctx) end end alias = allocate_alias(ctx, n.table.name) - tbl = ID(n.table.qualifiers, n.table.name) - c = FROM(AS(over = tbl, name = alias)) - cols = OrderedDict{Symbol, SQLClause}() + tbl = convert(SQLSyntax, (n.table.qualifiers, n.table.name)) + s = FROM(AS(tail = tbl, name = alias)) + cols = OrderedDict{Symbol, SQLSyntax}() for (name, col) in n.table.columns name in seen || continue - cols[name] = ID(over = alias, name = col.name) + cols[name] = ID(tail = alias, name = col.name) end repl = Dict{SQLNode, Symbol}() for ref in ctx.refs @@ -564,7 +564,7 @@ function assemble(n::FromTableNode, ctx) repl[ref] = name end end - Assemblage(n.table.name, c, cols = cols, repl = repl) + Assemblage(n.table.name, s, cols = cols, repl = repl) end function assemble(n::FromValuesNode, ctx) @@ -588,28 +588,28 @@ function assemble(n::FromValuesNode, ctx) column_aliases = [:_] end alias = allocate_alias(ctx, :values) - cols = OrderedDict{Symbol, SQLClause}() + cols = OrderedDict{Symbol, SQLSyntax}() if isempty(rows) - c = WHERE(false) + s = WHERE(false) for col in columns col in seen || continue cols[col] = LIT(missing) end elseif ctx.catalog.dialect.has_as_columns - c = FROM(AS(alias, columns = column_aliases, over = VALUES(rows))) + s = FROM(AS(alias, columns = column_aliases, tail = VALUES(rows))) for col in columns col in seen || continue - cols[col] = ID(over = alias, name = col) + cols[col] = ID(tail = alias, name = col) end else column_prefix = ctx.catalog.dialect.values_column_prefix column_index = ctx.catalog.dialect.values_column_index column_prefix !== nothing || error() - c = FROM(AS(alias, over = VALUES(rows))) + s = FROM(AS(alias, tail = VALUES(rows))) for col in columns col in seen || continue name = Symbol(column_prefix, column_index) - cols[col] = ID(over = alias, name = name) + cols[col] = ID(tail = alias, name = name) column_index += 1 end end @@ -619,7 +619,7 @@ function assemble(n::FromValuesNode, ctx) repl[ref] = name end end - Assemblage(:values, c, cols = cols, repl = repl) + Assemblage(:values, s, cols = cols, repl = repl) end function assemble(n::GroupNode, ctx) @@ -628,15 +628,15 @@ function assemble(n::GroupNode, ctx) return assemble(nothing, ctx) end base = assemble(n.over, ctx) - if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE()) + if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(over = complete(base), name = base_alias)) + tail = FROM(AS(tail = complete(base), name = base_alias)) end subs = make_subs(base, base_alias) - by = SQLClause[subs[key] for key in n.by] - trns = Pair{SQLNode, SQLClause}[] + by = SQLSyntax[subs[key] for key in n.by] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) @assert name in keys(n.label_map) @@ -655,17 +655,17 @@ function assemble(n::GroupNode, ctx) repl, cols = make_repl_cols(trns) @assert !isempty(cols) if has_aggregates || n.sets !== nothing - c = GROUP(over = tail, by = by, sets = n.sets) + s = GROUP(tail = tail, by = by, sets = n.sets) else args = complete(cols) - c = SELECT(over = tail, distinct = true, args = args) - cols = OrderedDict{Symbol, SQLClause}([name => ID(name) for name in keys(cols)]) + s = SELECT(tail = tail, distinct = true, args = args) + cols = OrderedDict{Symbol, SQLSyntax}([name => ID(name) for name in keys(cols)]) end - return Assemblage(base.name, c, cols = cols, repl = repl) + return Assemblage(base.name, s, cols = cols, repl = repl) end function assemble(n::IterateNode, ctx) - ctx′ = TranslateContext(ctx, vars = Base.ImmutableDict{Tuple{Symbol, Int}, SQLClause}()) + ctx′ = TranslateContext(ctx, vars = Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax}()) left = assemble(n.over, ctx) repl = Dict{SQLNode, Symbol}() dups = Dict{SQLNode, SQLNode}() @@ -680,7 +680,7 @@ function assemble(n::IterateNode, ctx) seen[name] = ref end end - temp_union = Assemblage(label(n.iterator), left.clause, cols = left.cols, repl = repl) + temp_union = Assemblage(label(n.iterator), left.syntax, cols = left.cols, repl = repl) union_alias = allocate_alias(ctx, temp_union) cte = CTEAssemblage(temp_union, name = union_alias) push!(ctx.ctes, cte) @@ -693,63 +693,63 @@ function assemble(n::IterateNode, ctx) dups[ref] = ref push!(urefs, ref) end - cs = SQLClause[] + ss = SQLSyntax[] for (arg, a) in (n.over => left, n.iterator => right) - if @dissect(a.clause, (local over) |> SELECT(args = (local args))) && aligned_columns(urefs, repl, args) && !@dissect(over, ORDER() || LIMIT()) - push!(cs, a.clause) + if @dissect(a.syntax, (local tail) |> SELECT(args = (local args))) && aligned_columns(urefs, repl, args) && !@dissect(tail, ORDER() || LIMIT()) + push!(ss, a.syntax) continue - elseif !@dissect(a.clause, SELECT() || UNION() || ORDER() || LIMIT()) + elseif !@dissect(a.syntax, SELECT() || UNION() || ORDER() || LIMIT()) alias = nothing - tail = a.clause + tail = a.syntax else alias = allocate_alias(ctx, a) - tail = FROM(AS(over = complete(a), name = alias)) + tail = FROM(AS(tail = complete(a), name = alias)) end subs = make_subs(a, alias) - cols = OrderedDict{Symbol, SQLClause}() + cols = OrderedDict{Symbol, SQLSyntax}() for ref in urefs name = left.repl[ref] cols[name] = subs[ref] end - c = SELECT(over = tail, args = complete(cols)) - push!(cs, c) + s = SELECT(tail = tail, args = complete(cols)) + push!(ss, s) end - union_clause = UNION(over = cs[1], all = true, args = cs[2:end]) - cols = OrderedDict{Symbol, SQLClause}() + union_syntax = UNION(tail = ss[1], all = true, args = ss[2:end]) + cols = OrderedDict{Symbol, SQLSyntax}() for ref in urefs name = left.repl[ref] cols[name] = ID(name) end - union = Assemblage(right.name, union_clause, cols = cols, repl = repl) + union = Assemblage(right.name, union_syntax, cols = cols, repl = repl) ctx.ctes[knot] = CTEAssemblage(union, name = union_alias) ctx.recursive[] = true alias = allocate_alias(ctx, union) - c = FROM(AS(over = ID(union_alias), name = alias)) + s = FROM(AS(tail = ID(union_alias), name = alias)) subs = make_subs(union, alias) - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - return Assemblage(union.name, c, cols = cols, repl = repl) + return Assemblage(union.name, s, cols = cols, repl = repl) end function assemble(n::LimitNode, ctx) base = assemble(n.over, ctx) - if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING() || ORDER()) + if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING() || ORDER()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(over = complete(base), name = base_alias)) + tail = FROM(AS(tail = complete(base), name = base_alias)) end - c = LIMIT(over = tail, offset = n.offset, limit = n.limit) + s = LIMIT(tail = tail, offset = n.offset, limit = n.limit) subs = make_subs(base, base_alias) - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - Assemblage(base.name, c, cols = cols, repl = repl) + Assemblage(base.name, s, cols = cols, repl = repl) end function assemble(n::LinkedNode, ctx) @@ -761,14 +761,14 @@ function assemble(n::LinkedNode, ctx) if col in dups if k > n.n_ext_refs alias = allocate_alias(ctx, a) - c = FROM(AS(over = complete(a), name = alias)) + s = FROM(AS(tail = complete(a), name = alias)) subs = make_subs(a, alias) - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in n.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - return Assemblage(a.name, c, cols = cols, repl = repl) + return Assemblage(a.name, s, cols = cols, repl = repl) end elseif !@dissect(a.cols[col], (nothing |> ID() || nothing |> ID() |> ID() || VAR() || LIT())) push!(dups, col) @@ -780,21 +780,21 @@ end function assemble(n::OrderNode, ctx) base = assemble(n.over, ctx) @assert !isempty(n.by) - if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) + if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(over = complete(base), name = base_alias)) + tail = FROM(AS(tail = complete(base), name = base_alias)) end subs = make_subs(base, base_alias) by = translate(n.by, ctx, subs) - c = ORDER(over = tail, by = by) - trns = Pair{SQLNode, SQLClause}[] + s = ORDER(tail = tail, by = by) + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - Assemblage(base.name, c, cols = cols, repl = repl) + Assemblage(base.name, s, cols = cols, repl = repl) end function assemble(n::PaddingNode, ctx) @@ -802,45 +802,47 @@ function assemble(n::PaddingNode, ctx) if isempty(ctx.refs) return base end - if !@dissect(base.clause, SELECT() || UNION()) + if !@dissect(base.syntax, SELECT() || UNION()) base_alias = nothing - c = base.clause + s = base.syntax else base_alias = allocate_alias(ctx, base) - c = FROM(AS(over = complete(base), name = base_alias)) + s = FROM(AS(tail = complete(base), name = base_alias)) end subs = make_subs(base, base_alias) repl = Dict{SQLNode, Symbol}() - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => translate(ref, ctx, subs)) end repl, cols = make_repl_cols(trns) - Assemblage(base.name, c, cols = cols, repl = repl) + Assemblage(base.name, s, cols = cols, repl = repl) end function assemble(n::PartitionNode, ctx) base = assemble(n.over, ctx) - if @dissect(base.clause, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) + if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(over = complete(base), name = base_alias)) + tail = FROM(AS(tail = complete(base), name = base_alias)) end - c = WINDOW(over = tail, args = []) + s = WINDOW(tail = tail, args = []) subs = make_subs(base, base_alias) ctx′ = TranslateContext(ctx, subs = subs) by = translate(n.by, ctx′) order_by = translate(n.order_by, ctx′) partition = PARTITION(by = by, order_by = order_by, frame = n.frame) - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] has_aggregates = false for ref in ctx.refs if @dissect(ref, nothing |> Agg()) && n.name === nothing - push!(trns, ref => partition |> translate(ref, ctx′)) + @dissect(translate(ref, ctx′), AGG(name = (local name), args = (local args), filter = (local filter))) || error() + push!(trns, ref => AGG(; name, args, filter, over = partition)) has_aggregates = true elseif @dissect(ref, (local over = nothing |> Agg()) |> Nested(name = (local name))) && name === n.name - push!(trns, ref => partition |> translate(over, ctx′)) + @dissect(translate(over, ctx′), AGG(name = (local name), args = (local args), filter = (local filter))) || error() + push!(trns, ref => AGG(; name, args, filter, over = partition)) has_aggregates = true else push!(trns, ref => subs[ref]) @@ -848,7 +850,7 @@ function assemble(n::PartitionNode, ctx) end @assert has_aggregates repl, cols = make_repl_cols(trns) - Assemblage(base.name, c, cols = cols, repl = repl) + Assemblage(base.name, s, cols = cols, repl = repl) end _outer_safe(a::Assemblage) = @@ -856,11 +858,11 @@ _outer_safe(a::Assemblage) = function assemble(n::RoutedJoinNode, ctx) left = assemble(n.over, ctx) - if @dissect(left.clause, local tail = FROM() || JOIN()) && (!n.right || _outer_safe(left)) + if @dissect(left.syntax, local tail = FROM() || JOIN()) && (!n.right || _outer_safe(left)) left_alias = nothing else left_alias = allocate_alias(ctx, left) - tail = FROM(AS(over = complete(left), name = left_alias)) + tail = FROM(AS(tail = complete(left), name = left_alias)) end lateral = n.lateral subs = make_subs(left, left_alias) @@ -869,7 +871,7 @@ function assemble(n::RoutedJoinNode, ctx) else right = assemble(n.joinee, ctx) end - if @dissect(right.clause, (local joinee = (ID() || AS())) |> FROM()) && (!n.left || _outer_safe(right)) + if @dissect(right.syntax, (local joinee = (ID() || AS())) |> FROM()) && (!n.left || _outer_safe(right)) for (ref, name) in right.repl subs[ref] = right.cols[name] end @@ -878,105 +880,105 @@ function assemble(n::RoutedJoinNode, ctx) end else right_alias = allocate_alias(ctx, right) - joinee = AS(over = complete(right), name = right_alias) - right_cache = Dict{Symbol, SQLClause}() + joinee = AS(tail = complete(right), name = right_alias) + right_cache = Dict{Symbol, SQLSyntax}() for (ref, name) in right.repl subs[ref] = get(right_cache, name) do - ID(over = right_alias, name = name) + ID(tail = right_alias, name = name) end end end on = translate(n.on, ctx, subs) - c = JOIN(over = tail, joinee = joinee, on = on, left = n.left, right = n.right, lateral = lateral) - trns = Pair{SQLNode, SQLClause}[] + s = JOIN(tail = tail, joinee = joinee, on = on, left = n.left, right = n.right, lateral = lateral) + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - Assemblage(left.name, c, cols = cols, repl = repl) + Assemblage(left.name, s, cols = cols, repl = repl) end function assemble(n::SelectNode, ctx) base = assemble(n.over, ctx) - if !@dissect(base.clause, SELECT() || UNION()) + if !@dissect(base.syntax, SELECT() || UNION()) base_alias = nothing - tail = base.clause + tail = base.syntax else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(over = complete(base), name = base_alias)) + tail = FROM(AS(tail = complete(base), name = base_alias)) end subs = make_subs(base, base_alias) - cols = OrderedDict{Symbol, SQLClause}() + cols = OrderedDict{Symbol, SQLSyntax}() for (name, i) in n.label_map col = n.args[i] cols[name] = translate(col, ctx, subs) end - c = SELECT(over = tail, args = complete(cols)) - cols = OrderedDict{Symbol, SQLClause}([name => ID(name) for name in keys(cols)]) + s = SELECT(tail = tail, args = complete(cols)) + cols = OrderedDict{Symbol, SQLSyntax}([name => ID(name) for name in keys(cols)]) repl = Dict{SQLNode, Symbol}() for ref in ctx.refs @dissect(ref, nothing |> Get(name = (local name))) || error() repl[ref] = name end - Assemblage(base.name, c, cols = cols, repl = repl) + Assemblage(base.name, s, cols = cols, repl = repl) end -function merge_conditions(c1, c2) - if @dissect(c1, FUN(name = :and, args = (local args1))) - if @dissect(c2, FUN(name = :and, args = (local args2))) +function merge_conditions(s1, s2) + if @dissect(s1, FUN(name = :and, args = (local args1))) + if @dissect(s2, FUN(name = :and, args = (local args2))) return FUN(:and, args1..., args2...) else - return FUN(:and, args1..., c2) + return FUN(:and, args1..., s2) end - elseif @dissect(c2, FUN(name = :and, args = (local args2))) - return FUN(:and, c1, args2...) + elseif @dissect(s2, FUN(name = :and, args = (local args2))) + return FUN(:and, s1, args2...) else - return FUN(:and, c1, c2) + return FUN(:and, s1, s2) end end function assemble(n::WhereNode, ctx) base = assemble(n.over, ctx) - if @dissect(base.clause, nothing || FROM() || JOIN() || WHERE() || HAVING()) || - @dissect(base.clause, GROUP(by = (local by))) && !isempty(by) + if @dissect(base.syntax, nothing || FROM() || JOIN() || WHERE() || HAVING()) || + @dissect(base.syntax, GROUP(by = (local by))) && !isempty(by) subs = make_subs(base, nothing) condition = translate(n.condition, ctx, subs) if @dissect(condition, LIT(val = true)) return base end - if @dissect(base.clause, (local tail) |> WHERE(condition = (local tail_condition))) + if @dissect(base.syntax, (local tail) |> WHERE(condition = (local tail_condition))) condition = merge_conditions(tail_condition, condition) - c = WHERE(over = tail, condition = condition) - elseif @dissect(base.clause, GROUP()) - c = HAVING(over = base.clause, condition = condition) - elseif @dissect(base.clause, (local tail) |> HAVING(condition = (local tail_condition))) + s = WHERE(tail = tail, condition = condition) + elseif @dissect(base.syntax, GROUP()) + s = HAVING(tail = base.syntax, condition = condition) + elseif @dissect(base.syntax, (local tail) |> HAVING(condition = (local tail_condition))) condition = merge_conditions(tail_condition, condition) - c = HAVING(over = tail, condition = condition) + s = HAVING(tail = tail, condition = condition) else - c = WHERE(over = base.clause, condition = condition) + s = WHERE(tail = base.syntax, condition = condition) end else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(over = complete(base), name = base_alias)) + tail = FROM(AS(tail = complete(base), name = base_alias)) subs = make_subs(base, base_alias) condition = translate(n.condition, ctx, subs) if @dissect(condition, LIT(val = true)) return base end - c = WHERE(over = tail, condition = condition) + s = WHERE(tail = tail, condition = condition) end - trns = Pair{SQLNode, SQLClause}[] + trns = Pair{SQLNode, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end repl, cols = make_repl_cols(trns) - return Assemblage(base.name, c, cols = cols, repl = repl) + return Assemblage(base.name, s, cols = cols, repl = repl) end function assemble(n::WithNode, ctx) cte_map′ = ctx.cte_map # FIXME: variable pushed into a CTE - ctx′ = TranslateContext(ctx, vars = Base.ImmutableDict{Tuple{Symbol, Int}, SQLClause}()) + ctx′ = TranslateContext(ctx, vars = Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax}()) for (name, i) in n.label_map a = assemble(n.args[i], ctx) alias = allocate_alias(ctx, a) @@ -990,7 +992,7 @@ end function assemble(n::WithExternalNode, ctx) cte_map′ = ctx.cte_map - ctx′ = TranslateContext(ctx, vars = Base.ImmutableDict{Tuple{Symbol, Int}, SQLClause}()) + ctx′ = TranslateContext(ctx, vars = Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax}()) for (name, i) in n.label_map a = assemble(n.args[i], ctx) table_name = a.name From 1047ad7bd8210aba96a66b5a90f1b2a19b81908b Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sun, 20 Apr 2025 21:20:49 -0500 Subject: [PATCH 05/17] Make clause objects immutable structs --- src/clauses.jl | 2 +- src/clauses/aggregate.jl | 2 +- src/clauses/as.jl | 2 +- src/clauses/from.jl | 2 +- src/clauses/function.jl | 2 +- src/clauses/group.jl | 2 +- src/clauses/having.jl | 2 +- src/clauses/identifier.jl | 2 +- src/clauses/internal.jl | 2 +- src/clauses/join.jl | 2 +- src/clauses/limit.jl | 2 +- src/clauses/literal.jl | 2 +- src/clauses/note.jl | 2 +- src/clauses/order.jl | 2 +- src/clauses/partition.jl | 2 +- src/clauses/select.jl | 2 +- src/clauses/sort.jl | 2 +- src/clauses/union.jl | 2 +- src/clauses/values.jl | 2 +- src/clauses/variable.jl | 2 +- src/clauses/where.jl | 2 +- src/clauses/window.jl | 2 +- src/clauses/with.jl | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/clauses.jl b/src/clauses.jl index 1b42c2a8..5e5e5f8b 100644 --- a/src/clauses.jl +++ b/src/clauses.jl @@ -45,7 +45,7 @@ Base.:(==)(@nospecialize(::AbstractSQLClause), @nospecialize(::AbstractSQLClause false Base.:(==)(s1::SQLSyntax, s2::SQLSyntax) = - s1.head == s2.head && s1.tail == s2.tail + s1 === s2 || s1.head == s2.head && s1.tail == s2.tail @generated function Base.:(==)(c1::C, c2::C) where {C <: AbstractSQLClause} exs = Expr[] diff --git a/src/clauses/aggregate.jl b/src/clauses/aggregate.jl index fc385796..6f0c501b 100644 --- a/src/clauses/aggregate.jl +++ b/src/clauses/aggregate.jl @@ -1,6 +1,6 @@ # Aggregate functions. -mutable struct AggregateClause <: AbstractSQLClause +struct AggregateClause <: AbstractSQLClause name::Symbol args::Vector{SQLSyntax} filter::Union{SQLSyntax, Nothing} diff --git a/src/clauses/as.jl b/src/clauses/as.jl index bff645aa..ebc8a298 100644 --- a/src/clauses/as.jl +++ b/src/clauses/as.jl @@ -1,6 +1,6 @@ # AS clause. -mutable struct AsClause <: AbstractSQLClause +struct AsClause <: AbstractSQLClause name::Symbol columns::Union{Vector{Symbol}, Nothing} diff --git a/src/clauses/from.jl b/src/clauses/from.jl index 19856e34..2f459722 100644 --- a/src/clauses/from.jl +++ b/src/clauses/from.jl @@ -1,6 +1,6 @@ # FROM clause. -mutable struct FromClause <: AbstractSQLClause +struct FromClause <: AbstractSQLClause end """ diff --git a/src/clauses/function.jl b/src/clauses/function.jl index e868ddba..00adb5a3 100644 --- a/src/clauses/function.jl +++ b/src/clauses/function.jl @@ -1,6 +1,6 @@ # SQL functions. -mutable struct FunctionClause <: AbstractSQLClause +struct FunctionClause <: AbstractSQLClause name::Symbol args::Vector{SQLSyntax} diff --git a/src/clauses/group.jl b/src/clauses/group.jl index d4035e89..34365c22 100644 --- a/src/clauses/group.jl +++ b/src/clauses/group.jl @@ -18,7 +18,7 @@ end import .GROUPING_MODE.GroupingMode -mutable struct GroupClause <: AbstractSQLClause +struct GroupClause <: AbstractSQLClause by::Vector{SQLSyntax} sets::Union{Vector{Vector{Int}}, GroupingMode, Nothing} diff --git a/src/clauses/having.jl b/src/clauses/having.jl index d014ec99..8d7af8d3 100644 --- a/src/clauses/having.jl +++ b/src/clauses/having.jl @@ -1,6 +1,6 @@ # HAVING clause. -mutable struct HavingClause <: AbstractSQLClause +struct HavingClause <: AbstractSQLClause condition::SQLSyntax HavingClause(; condition) = diff --git a/src/clauses/identifier.jl b/src/clauses/identifier.jl index e265ba66..541ea57b 100644 --- a/src/clauses/identifier.jl +++ b/src/clauses/identifier.jl @@ -1,6 +1,6 @@ # SQL identifier. -mutable struct IdentifierClause <: AbstractSQLClause +struct IdentifierClause <: AbstractSQLClause name::Symbol IdentifierClause(; name::Union{Symbol, AbstractString}) = diff --git a/src/clauses/internal.jl b/src/clauses/internal.jl index 53ec6d1c..80ed3d00 100644 --- a/src/clauses/internal.jl +++ b/src/clauses/internal.jl @@ -2,7 +2,7 @@ # Context holder for the serialize pass. -mutable struct WithContextClause <: AbstractSQLClause +struct WithContextClause <: AbstractSQLClause dialect::SQLDialect columns::Union{Vector{SQLColumn}, Nothing} diff --git a/src/clauses/join.jl b/src/clauses/join.jl index 19a2fdc7..65e42e72 100644 --- a/src/clauses/join.jl +++ b/src/clauses/join.jl @@ -1,6 +1,6 @@ # JOIN clause. -mutable struct JoinClause <: AbstractSQLClause +struct JoinClause <: AbstractSQLClause joinee::SQLSyntax on::SQLSyntax left::Bool diff --git a/src/clauses/limit.jl b/src/clauses/limit.jl index 9fca98a7..d5ea6446 100644 --- a/src/clauses/limit.jl +++ b/src/clauses/limit.jl @@ -1,6 +1,6 @@ # LIMIT clause. -mutable struct LimitClause <: AbstractSQLClause +struct LimitClause <: AbstractSQLClause offset::Union{Int, Nothing} limit::Union{Int, Nothing} with_ties::Bool diff --git a/src/clauses/literal.jl b/src/clauses/literal.jl index 868a94ca..ed7b144b 100644 --- a/src/clauses/literal.jl +++ b/src/clauses/literal.jl @@ -1,6 +1,6 @@ # Literal value. -mutable struct LiteralClause <: AbstractSQLClause +struct LiteralClause <: AbstractSQLClause val LiteralClause(; val) = diff --git a/src/clauses/note.jl b/src/clauses/note.jl index 48e06f52..b3f40fd7 100644 --- a/src/clauses/note.jl +++ b/src/clauses/note.jl @@ -1,6 +1,6 @@ # A free-form annotation. -mutable struct NoteClause <: AbstractSQLClause +struct NoteClause <: AbstractSQLClause text::String postfix::Bool diff --git a/src/clauses/order.jl b/src/clauses/order.jl index 1c2c9c1e..ec9a5748 100644 --- a/src/clauses/order.jl +++ b/src/clauses/order.jl @@ -1,6 +1,6 @@ # ORDER BY clause. -mutable struct OrderClause <: AbstractSQLClause +struct OrderClause <: AbstractSQLClause by::Vector{SQLSyntax} OrderClause(; by = SQLSyntax[]) = diff --git a/src/clauses/partition.jl b/src/clauses/partition.jl index 80c0838f..e39bedbf 100644 --- a/src/clauses/partition.jl +++ b/src/clauses/partition.jl @@ -80,7 +80,7 @@ function PrettyPrinting.quoteof(f::PartitionFrame) ex end -mutable struct PartitionClause <: AbstractSQLClause +struct PartitionClause <: AbstractSQLClause by::Vector{SQLSyntax} order_by::Vector{SQLSyntax} frame::Union{PartitionFrame, Nothing} diff --git a/src/clauses/select.jl b/src/clauses/select.jl index 848d1a9e..e4782528 100644 --- a/src/clauses/select.jl +++ b/src/clauses/select.jl @@ -23,7 +23,7 @@ function PrettyPrinting.quoteof(t::SelectTop) end end -mutable struct SelectClause <: AbstractSQLClause +struct SelectClause <: AbstractSQLClause top::Union{SelectTop, Nothing} distinct::Bool args::Vector{SQLSyntax} diff --git a/src/clauses/sort.jl b/src/clauses/sort.jl index e02b0cd0..e36a18e7 100644 --- a/src/clauses/sort.jl +++ b/src/clauses/sort.jl @@ -38,7 +38,7 @@ end import .NULLS_ORDER.NullsOrder -mutable struct SortClause <: AbstractSQLClause +struct SortClause <: AbstractSQLClause value::ValueOrder nulls::Union{NullsOrder, Nothing} diff --git a/src/clauses/union.jl b/src/clauses/union.jl index b25ea237..8132cbed 100644 --- a/src/clauses/union.jl +++ b/src/clauses/union.jl @@ -1,6 +1,6 @@ # UNION clause. -mutable struct UnionClause <: AbstractSQLClause +struct UnionClause <: AbstractSQLClause all::Bool args::Vector{SQLSyntax} diff --git a/src/clauses/values.jl b/src/clauses/values.jl index 305af25c..4f8b4888 100644 --- a/src/clauses/values.jl +++ b/src/clauses/values.jl @@ -1,6 +1,6 @@ # VALUES clause. -mutable struct ValuesClause <: AbstractSQLClause +struct ValuesClause <: AbstractSQLClause rows::Vector ValuesClause(; rows) = diff --git a/src/clauses/variable.jl b/src/clauses/variable.jl index c943d8ff..f58c4217 100644 --- a/src/clauses/variable.jl +++ b/src/clauses/variable.jl @@ -1,6 +1,6 @@ # SQL placeholder parameter. -mutable struct VariableClause <: AbstractSQLClause +struct VariableClause <: AbstractSQLClause name::Symbol VariableClause(; name::Union{Symbol, AbstractString}) = diff --git a/src/clauses/where.jl b/src/clauses/where.jl index 5b73d3ff..5e83753f 100644 --- a/src/clauses/where.jl +++ b/src/clauses/where.jl @@ -1,6 +1,6 @@ # WHERE clause. -mutable struct WhereClause <: AbstractSQLClause +struct WhereClause <: AbstractSQLClause condition::SQLSyntax WhereClause(; condition) = diff --git a/src/clauses/window.jl b/src/clauses/window.jl index 62fc8a59..6b7917bf 100644 --- a/src/clauses/window.jl +++ b/src/clauses/window.jl @@ -1,6 +1,6 @@ # WINDOW clause. -mutable struct WindowClause <: AbstractSQLClause +struct WindowClause <: AbstractSQLClause args::Vector{SQLSyntax} WindowClause(; args) = diff --git a/src/clauses/with.jl b/src/clauses/with.jl index 5b770786..c985dbc6 100644 --- a/src/clauses/with.jl +++ b/src/clauses/with.jl @@ -1,6 +1,6 @@ # WITH clause. -mutable struct WithClause <: AbstractSQLClause +struct WithClause <: AbstractSQLClause recursive::Bool args::Vector{SQLSyntax} From 8ea6afd2dffa4973a1d4043287e9106d53cbfb5a Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Thu, 1 May 2025 05:02:30 -0500 Subject: [PATCH 06/17] Convert query nodes to linked list structure --- docs/src/examples/index.md | 4 +- docs/src/guide/index.md | 19 +- docs/src/test/nodes.md | 108 ++++------- src/FunSQL.jl | 1 - src/catalogs.jl | 8 +- src/clauses.jl | 16 +- src/clauses/aggregate.jl | 2 +- src/clauses/as.jl | 2 +- src/clauses/from.jl | 4 +- src/clauses/function.jl | 2 +- src/clauses/group.jl | 2 +- src/clauses/having.jl | 2 +- src/clauses/identifier.jl | 10 +- src/clauses/internal.jl | 2 +- src/clauses/join.jl | 2 +- src/clauses/limit.jl | 2 +- src/clauses/literal.jl | 2 +- src/clauses/note.jl | 2 +- src/clauses/order.jl | 2 +- src/clauses/partition.jl | 2 +- src/clauses/select.jl | 2 +- src/clauses/sort.jl | 2 +- src/clauses/union.jl | 2 +- src/clauses/values.jl | 2 +- src/clauses/variable.jl | 2 +- src/clauses/where.jl | 2 +- src/clauses/window.jl | 2 +- src/clauses/with.jl | 2 +- src/connections.jl | 13 +- src/link.jl | 322 +++++++++++++++++---------------- src/nodes.jl | 359 +++++++++++++++++++++++-------------- src/nodes/aggregate.jl | 52 +++--- src/nodes/append.jl | 36 ++-- src/nodes/as.jl | 32 ++-- src/nodes/bind.jl | 28 ++- src/nodes/define.jl | 30 ++-- src/nodes/from.jl | 39 ++-- src/nodes/function.jl | 81 +++++---- src/nodes/get.jl | 62 ++----- src/nodes/group.jl | 34 ++-- src/nodes/highlight.jl | 34 +--- src/nodes/internal.jl | 229 +++++++++++------------ src/nodes/iterate.jl | 30 ++-- src/nodes/join.jl | 36 ++-- src/nodes/limit.jl | 38 ++-- src/nodes/literal.jl | 19 +- src/nodes/order.jl | 28 ++- src/nodes/over.jl | 30 ++-- src/nodes/partition.jl | 31 ++-- src/nodes/select.jl | 30 ++-- src/nodes/sort.jl | 33 ++-- src/nodes/variable.jl | 11 +- src/nodes/where.jl | 32 ++-- src/nodes/with.jl | 30 ++-- src/nodes/with_external.jl | 24 +-- src/render.jl | 49 +++-- src/resolve.jl | 280 +++++++++++++++-------------- src/translate.jl | 265 ++++++++++++++------------- 58 files changed, 1185 insertions(+), 1342 deletions(-) diff --git a/docs/src/examples/index.md b/docs/src/examples/index.md index bb041871..48fd6541 100644 --- a/docs/src/examples/index.md +++ b/docs/src/examples/index.md @@ -445,7 +445,7 @@ however we must ensure that all column names are unique. q = q |> Select(Get.(keys(person_table.columns))..., - Get.(keys(visit_occurrence_table.columns), over = Get.visit)...) + Get.(keys(visit_occurrence_table.columns), tail = Get.visit)...) #=> ERROR: FunSQL.DuplicateLabelError: `person_id` is used more than once in: ⋮ @@ -454,7 +454,7 @@ however we must ensure that all column names are unique. q = q |> Select(Get.(keys(person_table.columns))..., Get.(filter(!in(keys(person_table.columns)), collect(keys(visit_occurrence_table.columns))), - over = Get.visit)...) + tail = Get.visit)...) render(conn, q) |> print #=> diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 63d59b8f..76c59b97 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -571,7 +571,7 @@ pipelines. The first pipeline is attached using the `|>` operator and the second one is provided as an argument to the `Join` constructor. Alternatively, both input pipelines can be specified as keyword arguments: - Join(over = From(:person), + Join(tail = From(:person), joinee = :location => From(:location), on = Get.location_id .== Get.location.location_id, left = true) |> @@ -645,22 +645,7 @@ admits several equivalent forms: Get("year_of_birth") Such column references are resolved at the place of use against the input -dataset. As we mentioned earlier, sometimes column references cannot be -resolved unambiguously. To alleviate this problem, we can bind the column -reference to the node that produces it: - -*Show all patients with their state of residence.* - - qₚ = From(:person) - qₗ = From(:location) - q = qₚ |> - LeftJoin(qₗ, on = qₚ.location_id .== qₗ.location_id) |> - Select(qₚ.person_id, qₗ.state) - -The notation `qₚ.location_id` and `qₗ.location_id` is a syntax sugar for - - Get(:location_id, over = qₚ) - Get(:location_id, over = qₗ) +dataset. ## `Fun`: SQL Functions and Operators diff --git a/docs/src/test/nodes.md b/docs/src/test/nodes.md index 415a06fb..1767ce54 100644 --- a/docs/src/test/nodes.md +++ b/docs/src/test/nodes.md @@ -5,7 +5,7 @@ using FunSQL: Agg, Append, As, Asc, Bind, CrossJoin, Define, Desc, Fun, From, Get, Group, Highlight, Iterate, Join, LeftJoin, Limit, Lit, Order, Over, - Partition, SQLNode, SQLTable, Select, Sort, Var, Where, With, + Partition, SQLQuery, SQLTable, Select, Sort, Var, Where, With, WithExternal, ID, render We start with specifying the database model. @@ -28,8 +28,8 @@ We start with specifying the database model. const observation = SQLTable(:observation, columns = [:observation_id, :person_id, :observation_concept_id, :observation_date]) -In FunSQL, a SQL query is generated from a tree of `SQLNode` objects. The -nodes are created using constructors with familiar SQL names and connected +In FunSQL, a SQL query is generated from a tree of `SQLQuery` objects. +The nodes are created using constructors with familiar SQL names and connected together using the chain (`|>`) operator. q = From(person) |> @@ -37,7 +37,7 @@ together using the chain (`|>`) operator. Select(Get.person_id) #-> (…) |> Select(…) -Displaying a `SQLNode` object shows how it was constructed. +Displaying a `SQLQuery` object shows how it was constructed. display(q) #=> @@ -49,21 +49,17 @@ Displaying a `SQLNode` object shows how it was constructed. end =# -Each node wraps a concrete node object, which can be accessed using the -indexing operator. +A `SQLQuery` object is represented as a linked list, whose components +could be accessed using attributes `head` and `tail`. - q[] - #-> ((…) |> Select(…))[] + q.head + #-> (Select(…)).head - display(q[]) - #=> - let person = SQLTable(:person, …), - q1 = From(person), - q2 = q1 |> Where(Fun.">"(Get.year_of_birth, 2000)), - q3 = q2 |> Select(Get.person_id) - q3[] - end - =# + display(q.head) + #-> Select(Get.person_id).head + + q.tail + #-> (…) |> Where(…) The SQL query is generated using the function `render()`. @@ -390,33 +386,6 @@ Use backticks to represent a name that is not a valid identifier. @funsql `p`.`person_id` #-> Get.p.person_id -`Get` can also create bound references. - - q = From(person) - - e = Get(over = q, :year_of_birth) - #-> (…) |> Get.year_of_birth - - display(e) - #=> - let person = SQLTable(:person, …), - q1 = From(person) - q1.year_of_birth - end - =# - - q.person_id - #-> (…) |> Get.person_id - - q."person_id" - #-> (…) |> Get.person_id - - q[:person_id] - #-> (…) |> Get.person_id - - q["person_id"] - #-> (…) |> Get.person_id - `Get` is used for dereferencing an alias created with `As`. q = From(person) |> @@ -514,20 +483,6 @@ reference, will result in an error. end =# -A reference bound to any node other than `Get` will cause an error. - - q = (qₚ = From(person)) |> Select(qₚ.person_id) - - print(render(q)) - #=> - ERROR: FunSQL.IllFormedError in: - let person = SQLTable(:person, …), - q1 = From(person), - q2 = q1 |> Select(q1.person_id) - q2 - end - =# - Any expression could be given a name and attached to a query using the `Define` constructor. @@ -849,7 +804,7 @@ are replaced with their SQL equivalents. A vector of arguments could be passed directly. - Fun.">"(args = SQLNode[Get.year_of_birth, 2000]) + Fun.">"(args = SQLQuery[Get.year_of_birth, 2000]) #-> Fun.:(">")(…) `Fun` nodes can be generated in `@funsql` notation. @@ -2662,8 +2617,8 @@ used more than once. Join(:visit_group => From(visit_occurrence) |> Group(Get.person_id), on = Get.person_id .== Get.visit_group.person_id) |> - Where(Agg.count(over = Get.visit_group) .>= 2) |> - Select(Get.person_id, Agg.count(over = Get.visit_group)) + Where(Get.visit_group |> Agg.count() .>= 2) |> + Select(Get.person_id, Get.visit_group |> Agg.count()) print(render(q)) #=> @@ -4211,24 +4166,23 @@ and determines node types. │ let person = SQLTable(:person, …), │ location = SQLTable(:location, …), │ visit_occurrence = SQLTable(:visit_occurrence, …), - │ q1 = FromTable(table = person), - │ q2 = Resolved(RowType(:person_id => ScalarType(), + │ q1 = FromTable(person), + │ q2 = q1 |> + │ Resolved(RowType(:person_id => ScalarType(), │ :gender_concept_id => ScalarType(), │ :year_of_birth => ScalarType(), │ :month_of_birth => ScalarType(), │ :day_of_birth => ScalarType(), │ :birth_datetime => ScalarType(), - │ :location_id => ScalarType()), - │ over = q1) |> - │ Where(Resolved(ScalarType(), - │ over = Fun."<="(Resolved(ScalarType(), - │ over = Get.year_of_birth), - │ Resolved(ScalarType(), over = 2000)))), + │ :location_id => ScalarType())) |> + │ Where(Fun."<="(Get.year_of_birth |> Resolved(ScalarType()), + │ 2000 |> Resolved(ScalarType())) |> + │ Resolved(ScalarType())), ⋮ - │ WithContext(over = Resolved(RowType(:person_id => ScalarType(), - │ :max_visit_start_date => ScalarType()), - │ over = q9), - │ catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing)) + │ q9 |> + │ Resolved(RowType(:person_id => ScalarType(), + │ :max_visit_start_date => ScalarType())) |> + │ WithContext(catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing)) │ end └ @ FunSQL … =# @@ -4245,15 +4199,15 @@ produce. │ let person = SQLTable(:person, …), │ location = SQLTable(:location, …), │ visit_occurrence = SQLTable(:visit_occurrence, …), - │ q1 = FromTable(table = person), + │ q1 = FromTable(person), │ q2 = Get.person_id, │ q3 = Get.person_id, │ q4 = Get.location_id, │ q5 = Get.year_of_birth, - │ q6 = Linked([q2, q3, q4, q5], 3, over = q1), + │ q6 = q1 |> Linked([q2, q3, q4, q5], 3), ⋮ - │ WithContext(over = q33, - │ catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing)) + │ q33 |> + │ WithContext(catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing)) │ end └ @ FunSQL … =# diff --git a/src/FunSQL.jl b/src/FunSQL.jl index ca799261..d3f00af6 100644 --- a/src/FunSQL.jl +++ b/src/FunSQL.jl @@ -111,7 +111,6 @@ include("strings.jl") include("clauses.jl") include("nodes.jl") include("connections.jl") -#include("annotate.jl") include("resolve.jl") include("link.jl") include("translate.jl") diff --git a/src/catalogs.jl b/src/catalogs.jl index a59992f6..0fbd446a 100644 --- a/src/catalogs.jl +++ b/src/catalogs.jl @@ -267,13 +267,13 @@ SQLCatalog(SQLTable(:location, SQLColumn(:location_id), SQLColumn(:state)), struct SQLCatalog <: AbstractDict{Symbol, SQLTable} tables::Dict{Symbol, SQLTable} dialect::SQLDialect - cache::Any # Union{AbstractDict{SQLNode, SQLString}, Nothing} + cache::Any # Union{AbstractDict{SQLQuery, SQLString}, Nothing} metadata::SQLMetadata function SQLCatalog(; tables = Dict{Symbol, SQLTable}(), dialect = :default, cache = default_cache_maxsize, metadata = nothing) table_map = _table_map(tables) if cache isa Number - cache = LRU{SQLNode, SQLString}(maxsize = cache) + cache = LRU{SQLQuery, SQLString}(maxsize = cache) end new(table_map, dialect, cache, _metadata(metadata)) end @@ -311,7 +311,7 @@ function PrettyPrinting.quoteof(c::SQLCatalog) cache = c.cache if cache === nothing push!(ex.args, Expr(:kw, :cache, nothing)) - elseif cache isa LRU{SQLNode, SQLString} + elseif cache isa LRU{SQLQuery, SQLString} if cache.maxsize != default_cache_maxsize push!(ex.args, Expr(:kw, :cache, cache.maxsize)) end @@ -336,7 +336,7 @@ function Base.show(io::IO, c::SQLCatalog) cache = c.cache if cache === nothing print(io, ", cache = nothing") - elseif cache isa LRU{SQLNode, SQLString} + elseif cache isa LRU{SQLQuery, SQLString} if cache.maxsize != default_cache_maxsize print(io, ", cache = ", cache.maxsize) end diff --git a/src/clauses.jl b/src/clauses.jl index 5e5e5f8b..71e7e829 100644 --- a/src/clauses.jl +++ b/src/clauses.jl @@ -28,8 +28,12 @@ struct SQLSyntax SQLSyntax(@nospecialize head::AbstractSQLClause) = new(nothing, head) - SQLSyntax(tail, @nospecialize head::AbstractSQLClause) = + function SQLSyntax(tail, head::AbstractSQLClause) + if tail !== nothing && terminal(head) + throw(RebaseError(path = [SQLSyntax(head)])) + end new(tail, head) + end end Base.convert(::Type{SQLSyntax}, @nospecialize c::AbstractSQLClause) = @@ -116,13 +120,17 @@ PrettyPrinting.quoteof(names::Vector{Symbol}, ctx::QuoteContext) = # Support for clause constructors. -abstract type SQLSyntaxCtor{T} +struct SQLSyntaxCtor{C<:AbstractSQLClause} + id::Symbol end -SQLSyntaxCtor{C}(args...; tail = nothing, kws...) where {C<:AbstractSQLClause} = +Base.show(io::IO, @nospecialize ctor::SQLSyntaxCtor{C}) where {C} = + print(io, ctor.id) + +(::SQLSyntaxCtor{C})(args...; tail = nothing, kws...) where {C} = SQLSyntax(tail, C(args...; kws...)) -function dissect(scr::Symbol, ::Type{SQLSyntaxCtor{C}}, pats::Vector{Any}) where {C<:AbstractSQLClause} +function dissect(scr::Symbol, ::SQLSyntaxCtor{C}, pats::Vector{Any}) where {C<:AbstractSQLClause} head_pats = Any[] tail_pats = Any[] for pat in pats diff --git a/src/clauses/aggregate.jl b/src/clauses/aggregate.jl index 6f0c501b..245eb3ec 100644 --- a/src/clauses/aggregate.jl +++ b/src/clauses/aggregate.jl @@ -50,7 +50,7 @@ julia> print(render(s)) (row_number() OVER (PARTITION BY "year_of_birth")) ``` """ -const AGG = SQLSyntaxCtor{AggregateClause} +const AGG = SQLSyntaxCtor{AggregateClause}(:AGG) function PrettyPrinting.quoteof(c::AggregateClause, ctx::QuoteContext) ex = Expr(:call, :AGG, string(c.name)) diff --git a/src/clauses/as.jl b/src/clauses/as.jl index ebc8a298..0444f4dd 100644 --- a/src/clauses/as.jl +++ b/src/clauses/as.jl @@ -37,7 +37,7 @@ julia> print(render(s)) "person" AS "p" ("person_id", "year_of_birth") ``` """ -const AS = SQLSyntaxCtor{AsClause} +const AS = SQLSyntaxCtor{AsClause}(:AS) Base.convert(::Type{SQLSyntax}, p::Pair{Symbol}) = SQLSyntax(last(p), AsClause(name = first(p))) diff --git a/src/clauses/from.jl b/src/clauses/from.jl index 2f459722..700c4ee3 100644 --- a/src/clauses/from.jl +++ b/src/clauses/from.jl @@ -19,9 +19,9 @@ SELECT "p"."person_id" FROM "person" AS "p" ``` """ -const FROM = SQLSyntaxCtor{FromClause} +const FROM = SQLSyntaxCtor{FromClause}(:FROM) -FROM(source) = +(::typeof(FROM))(source) = FROM(tail = source) PrettyPrinting.quoteof(c::FromClause, ctx::QuoteContext) = diff --git a/src/clauses/function.jl b/src/clauses/function.jl index 00adb5a3..9a2e229b 100644 --- a/src/clauses/function.jl +++ b/src/clauses/function.jl @@ -46,7 +46,7 @@ julia> print(render(s)) SUBSTRING("zip" FROM 1 FOR 3) ``` """ -const FUN = SQLSyntaxCtor{FunctionClause} +const FUN = SQLSyntaxCtor{FunctionClause}(:FUN) Base.convert(::Type{SQLSyntax}, ::typeof(*)) = FUN(:*) diff --git a/src/clauses/group.jl b/src/clauses/group.jl index 34365c22..56786819 100644 --- a/src/clauses/group.jl +++ b/src/clauses/group.jl @@ -58,7 +58,7 @@ FROM "person" GROUP BY "year_of_birth" ``` """ -const GROUP = SQLSyntaxCtor{GroupClause} +const GROUP = SQLSyntaxCtor{GroupClause}(:GROUP) function PrettyPrinting.quoteof(c::GroupClause, ctx::QuoteContext) ex = Expr(:call, :GROUP) diff --git a/src/clauses/having.jl b/src/clauses/having.jl index 8d7af8d3..76b90cbf 100644 --- a/src/clauses/having.jl +++ b/src/clauses/having.jl @@ -31,7 +31,7 @@ GROUP BY "year_of_birth" HAVING (count(*) > 10) ``` """ -const HAVING = SQLSyntaxCtor{HavingClause} +const HAVING = SQLSyntaxCtor{HavingClause}(:HAVING) PrettyPrinting.quoteof(c::HavingClause, ctx::QuoteContext) = Expr(:call, :HAVING, quoteof(c.condition, ctx)) diff --git a/src/clauses/identifier.jl b/src/clauses/identifier.jl index 541ea57b..ecac1a2e 100644 --- a/src/clauses/identifier.jl +++ b/src/clauses/identifier.jl @@ -40,22 +40,22 @@ julia> print(render(s)) "pg_catalog"."pg_database" ``` """ -const ID = SQLSyntaxCtor{IdentifierClause} +const ID = SQLSyntaxCtor{IdentifierClause}(:ID) -ID(qualifier::Union{Symbol, AbstractString}, name::Union{Symbol, AbstractString}; tail = nothing) = +(::typeof(ID))(qualifier::Union{Symbol, AbstractString}, name::Union{Symbol, AbstractString}; tail = nothing) = ID(tail = ID(qualifier; tail), name = name) -ID(name1::Union{Symbol, AbstractString}, name2::Union{Symbol, AbstractString}, names::Union{Symbol, AbstractString}...; tail = nothing) = +(::typeof(ID))(name1::Union{Symbol, AbstractString}, name2::Union{Symbol, AbstractString}, names::Union{Symbol, AbstractString}...; tail = nothing) = ID(tail = ID(name1; tail), name2, names...) -function ID(qualifiers::AbstractVector{<:Union{Symbol, AbstractString}}, name::Union{Symbol, AbstractString}; tail = nothing) +function (::typeof(ID))(qualifiers::AbstractVector{<:Union{Symbol, AbstractString}}, name::Union{Symbol, AbstractString}; tail = nothing) for q in qualifiers tail = ID(tail = tail, name = q) end ID(; tail, name) end -ID(t::SQLTable) = +(::typeof(ID))(t::SQLTable) = ID(t.qualifiers, t.name) Base.convert(::Type{SQLSyntax}, name::Symbol) = diff --git a/src/clauses/internal.jl b/src/clauses/internal.jl index 80ed3d00..8eea0184 100644 --- a/src/clauses/internal.jl +++ b/src/clauses/internal.jl @@ -10,7 +10,7 @@ struct WithContextClause <: AbstractSQLClause new(dialect, columns) end -const WITH_CONTEXT = SQLSyntaxCtor{WithContextClause} +const WITH_CONTEXT = SQLSyntaxCtor{WithContextClause}(:WITH_CONTEXT) function PrettyPrinting.quoteof(c::WithContextClause, ctx::QuoteContext) ex = Expr(:call, :WITH_CONTEXT) diff --git a/src/clauses/join.jl b/src/clauses/join.jl index 65e42e72..1129cb80 100644 --- a/src/clauses/join.jl +++ b/src/clauses/join.jl @@ -46,7 +46,7 @@ FROM "person" AS "p" LEFT JOIN "location" AS "l" ON ("p"."location_id" = "l"."location_id") ``` """ -const JOIN = SQLSyntaxCtor{JoinClause} +const JOIN = SQLSyntaxCtor{JoinClause}(:JOIN) function PrettyPrinting.quoteof(c::JoinClause, ctx::QuoteContext) ex = Expr(:call, :JOIN, quoteof([c.joinee, c.on], ctx)...) diff --git a/src/clauses/limit.jl b/src/clauses/limit.jl index d5ea6446..67252487 100644 --- a/src/clauses/limit.jl +++ b/src/clauses/limit.jl @@ -42,7 +42,7 @@ FROM "person" FETCH FIRST 1 ROW ONLY ``` """ -const LIMIT = SQLSyntaxCtor{LimitClause} +const LIMIT = SQLSyntaxCtor{LimitClause}(:LIMIT) function PrettyPrinting.quoteof(c::LimitClause, ctx::QuoteContext) ex = Expr(:call, :LIMIT) diff --git a/src/clauses/literal.jl b/src/clauses/literal.jl index ed7b144b..7c9f7b25 100644 --- a/src/clauses/literal.jl +++ b/src/clauses/literal.jl @@ -35,7 +35,7 @@ julia> print(render(s)) 'SQL is fun!' ``` """ -LIT = SQLSyntaxCtor{LiteralClause} +const LIT = SQLSyntaxCtor{LiteralClause}(:LIT) Base.convert(::Type{SQLSyntax}, val::SQLLiteralType) = LIT(val) diff --git a/src/clauses/note.jl b/src/clauses/note.jl index b3f40fd7..9e7a84bb 100644 --- a/src/clauses/note.jl +++ b/src/clauses/note.jl @@ -29,7 +29,7 @@ SELECT "p"."person_id" FROM "person" AS "p" TABLESAMPLE SYSTEM (50) ``` """ -const NOTE = SQLSyntaxCtor{NoteClause} +const NOTE = SQLSyntaxCtor{NoteClause}(:NOTE) function PrettyPrinting.quoteof(c::NoteClause, ctx::QuoteContext) ex = Expr(:call, :NOTE, quoteof(c.text)) diff --git a/src/clauses/order.jl b/src/clauses/order.jl index ec9a5748..2c193816 100644 --- a/src/clauses/order.jl +++ b/src/clauses/order.jl @@ -29,7 +29,7 @@ FROM "person" ORDER BY "year_of_birth" ``` """ -const ORDER = SQLSyntaxCtor{OrderClause} +const ORDER = SQLSyntaxCtor{OrderClause}(:ORDER) function PrettyPrinting.quoteof(c::OrderClause, ctx::QuoteContext) ex = Expr(:call, :ORDER) diff --git a/src/clauses/partition.jl b/src/clauses/partition.jl index e39bedbf..a7b3b221 100644 --- a/src/clauses/partition.jl +++ b/src/clauses/partition.jl @@ -145,7 +145,7 @@ FROM "person" GROUP BY "year_of_birth" ``` """ -const PARTITION = SQLSyntaxCtor{PartitionClause} +const PARTITION = SQLSyntaxCtor{PartitionClause}(:PARTITION) function PrettyPrinting.quoteof(c::PartitionClause, ctx::QuoteContext) ex = Expr(:call, :PARTITION) diff --git a/src/clauses/select.jl b/src/clauses/select.jl index e4782528..32529105 100644 --- a/src/clauses/select.jl +++ b/src/clauses/select.jl @@ -67,7 +67,7 @@ SELECT DISTINCT "zip" FROM "location" ``` """ -const SELECT = SQLSyntaxCtor{SelectClause} +const SELECT = SQLSyntaxCtor{SelectClause}(:SELECT) function PrettyPrinting.quoteof(c::SelectClause, ctx::QuoteContext) ex = Expr(:call, :SELECT) diff --git a/src/clauses/sort.jl b/src/clauses/sort.jl index e36a18e7..4648b0bc 100644 --- a/src/clauses/sort.jl +++ b/src/clauses/sort.jl @@ -72,7 +72,7 @@ FROM "person" ORDER BY "year_of_birth" DESC ``` """ -SORT = SQLSyntaxCtor{SortClause} +SORT = SQLSyntaxCtor{SortClause}(:SORT) """ ASC(; over = nothing, nulls = nothing, tail = nothing) diff --git a/src/clauses/union.jl b/src/clauses/union.jl index 8132cbed..a1288c42 100644 --- a/src/clauses/union.jl +++ b/src/clauses/union.jl @@ -40,7 +40,7 @@ SELECT FROM "observation" ``` """ -const UNION = SQLSyntaxCtor{UnionClause} +const UNION = SQLSyntaxCtor{UnionClause}(:UNION) function PrettyPrinting.quoteof(c::UnionClause, ctx::QuoteContext) ex = Expr(:call, :UNION) diff --git a/src/clauses/values.jl b/src/clauses/values.jl index 4f8b4888..40626b88 100644 --- a/src/clauses/values.jl +++ b/src/clauses/values.jl @@ -28,7 +28,7 @@ VALUES ('FunSQL', 2021) ``` """ -VALUES = SQLSyntaxCtor{ValuesClause} +const VALUES = SQLSyntaxCtor{ValuesClause}(:VALUES) terminal(::Type{ValuesClause}) = true diff --git a/src/clauses/variable.jl b/src/clauses/variable.jl index f58c4217..77c63c96 100644 --- a/src/clauses/variable.jl +++ b/src/clauses/variable.jl @@ -25,7 +25,7 @@ julia> print(render(s)) :year ``` """ -const VAR = SQLSyntaxCtor{VariableClause} +const VAR = SQLSyntaxCtor{VariableClause}(:VAR) terminal(::Type{VariableClause}) = true diff --git a/src/clauses/where.jl b/src/clauses/where.jl index 5e83753f..0e99f08f 100644 --- a/src/clauses/where.jl +++ b/src/clauses/where.jl @@ -29,7 +29,7 @@ FROM "location" WHERE ("zip" = '60614') ``` """ -const WHERE = SQLSyntaxCtor{WhereClause} +const WHERE = SQLSyntaxCtor{WhereClause}(:WHERE) PrettyPrinting.quoteof(c::WhereClause, ctx::QuoteContext) = Expr(:call, :WHERE, quoteof(c.condition, ctx)) diff --git a/src/clauses/window.jl b/src/clauses/window.jl index 6b7917bf..1a031402 100644 --- a/src/clauses/window.jl +++ b/src/clauses/window.jl @@ -34,7 +34,7 @@ WINDOW "w2" AS ("w1" ORDER BY "month_of_birth", "day_of_birth") ``` """ -const WINDOW = SQLSyntaxCtor{WindowClause} +const WINDOW = SQLSyntaxCtor{WindowClause}(:WINDOW) function PrettyPrinting.quoteof(c::WindowClause, ctx::QuoteContext) ex = Expr(:call, :WINDOW) diff --git a/src/clauses/with.jl b/src/clauses/with.jl index c985dbc6..f77cbb7f 100644 --- a/src/clauses/with.jl +++ b/src/clauses/with.jl @@ -85,7 +85,7 @@ SELECT * FROM "essential_hypertension" ``` """ -const WITH = SQLSyntaxCtor{WithClause} +const WITH = SQLSyntaxCtor{WithClause}(:WITH) function PrettyPrinting.quoteof(c::WithClause, ctx::QuoteContext) ex = Expr(:call, :WITH) diff --git a/src/connections.jl b/src/connections.jl index 473a77d9..d4bf0431 100644 --- a/src/connections.jl +++ b/src/connections.jl @@ -86,12 +86,12 @@ function DBInterface.connect(::Type{SQLConnection{RawConnType}}, args...; end """ - DBInterface.prepare(conn::SQLConnection, sql::SQLNode)::SQLStatement + DBInterface.prepare(conn::SQLConnection, sql::SQLQuery)::SQLStatement DBInterface.prepare(conn::SQLConnection, sql::SQLSyntax)::SQLStatement Serialize the query node and return a prepared SQL statement. """ -DBInterface.prepare(conn::SQLConnection, sql::Union{SQLNode, SQLSyntax}) = +DBInterface.prepare(conn::SQLConnection, sql::Union{SQLQuery, SQLSyntax}) = DBInterface.prepare(conn, render(conn, sql)) """ @@ -106,21 +106,21 @@ DBInterface.prepare(conn::SQLConnection, str::AbstractString) = DBInterface.prepare(conn.raw, str) """ - DBInterface.execute(conn::SQLConnection, sql::SQLNode; params...) + DBInterface.execute(conn::SQLConnection, sql::SQLQuery; params...) DBInterface.execute(conn::SQLConnection, sql::SQLSyntax; params...) Serialize and execute the query node. """ -DBInterface.execute(conn::SQLConnection, sql::Union{SQLNode, SQLSyntax}; params...) = +DBInterface.execute(conn::SQLConnection, sql::Union{SQLQuery, SQLSyntax}; params...) = DBInterface.execute(conn, sql, values(params)) """ - DBInterface.execute(conn::SQLConnection, sql::SQLNode, params) + DBInterface.execute(conn::SQLConnection, sql::SQLQuery, params) DBInterface.execute(conn::SQLConnection, sql::SQLSyntax, params) Serialize and execute the query node. """ -DBInterface.execute(conn::SQLConnection, sql::Union{SQLNode, SQLSyntax}, params) = +DBInterface.execute(conn::SQLConnection, sql::Union{SQLQuery, SQLSyntax}, params) = DBInterface.execute(DBInterface.prepare(conn, sql), params) DBInterface.close!(conn::SQLConnection) = @@ -139,4 +139,3 @@ DBInterface.getconnection(stmt::SQLStatement) = DBInterface.close!(stmt::SQLStatement) = DBInterface.close!(stmt.raw) - diff --git a/src/link.jl b/src/link.jl index 812c57a4..496d2418 100644 --- a/src/link.jl +++ b/src/link.jl @@ -2,54 +2,63 @@ struct LinkContext catalog::SQLCatalog - defs::Vector{SQLNode} - refs::Vector{SQLNode} - cte_refs::Base.ImmutableDict{Tuple{Symbol, Int}, Vector{SQLNode}} - knot_refs::Union{Vector{SQLNode}, Nothing} + tail::Union{SQLQuery, Nothing} + defs::Vector{SQLQuery} + refs::Vector{SQLQuery} + cte_refs::Base.ImmutableDict{Tuple{Symbol, Int}, Vector{SQLQuery}} + knot_refs::Union{Vector{SQLQuery}, Nothing} LinkContext(catalog) = new(catalog, - SQLNode[], - SQLNode[], - Base.ImmutableDict{Tuple{Symbol, Int}, Vector{SQLNode}}(), + nothing, + SQLQuery[], + SQLQuery[], + Base.ImmutableDict{Tuple{Symbol, Int}, Vector{SQLQuery}}(), nothing) - LinkContext(ctx::LinkContext; refs = ctx.refs, cte_refs = ctx.cte_refs, knot_refs = ctx.knot_refs) = + LinkContext(ctx::LinkContext; tail = ctx.tail, refs = ctx.refs, cte_refs = ctx.cte_refs, knot_refs = ctx.knot_refs) = new(ctx.catalog, + tail, ctx.defs, refs, cte_refs, knot_refs) end -function link(n::SQLNode) - @dissect(n, WithContext(over = (local over), catalog = (local catalog))) || throw(ILLFormedError()) +function link(q::SQLQuery) + @dissect(q, (local tail) |> WithContext(catalog = (local catalog))) || throw(ILLFormedError()) ctx = LinkContext(catalog) - t = row_type(over) - refs = SQLNode[] + t = row_type(tail) + refs = SQLQuery[] for (f, ft) in t.fields if ft isa ScalarType push!(refs, Get(f)) end end - over′ = Linked(refs, over = link(dismantle(over, ctx), ctx, refs)) - WithContext(over = over′, catalog = catalog, defs = ctx.defs) + tail′ = Linked(refs, tail = link(dismantle(tail, ctx), ctx, refs)) + WithContext(tail = tail′, catalog = catalog, defs = ctx.defs) end -function dismantle(n::SQLNode, ctx) - convert(SQLNode, dismantle(n[], ctx)) +function dismantle(q::SQLQuery, ctx) + convert(SQLQuery, dismantle(q.head, LinkContext(ctx, tail = q.tail))) end -function dismantle(ns::Vector{SQLNode}, ctx) - SQLNode[dismantle(n, ctx) for n in ns] +dismantle(ctx::LinkContext) = + dismantle(ctx.tail, ctx) + +function dismantle(qs::Vector{SQLQuery}, ctx) + SQLQuery[dismantle(q, ctx) for q in qs] end -function dismantle_scalar(n::SQLNode, ctx) - convert(SQLNode, dismantle_scalar(n[], ctx)) +function dismantle_scalar(q::SQLQuery, ctx) + convert(SQLQuery, dismantle_scalar(q.head, LinkContext(ctx, tail = q.tail))) end -function dismantle_scalar(ns::Vector{SQLNode}, ctx) - SQLNode[dismantle_scalar(n, ctx) for n in ns] +dismantle_scalar(ctx::LinkContext) = + dismantle_scalar(ctx.tail, ctx) + +function dismantle_scalar(qs::Vector{SQLQuery}, ctx) + SQLQuery[dismantle_scalar(q, ctx) for q in qs] end function dismantle_scalar(n::AggregateNode, ctx) @@ -59,49 +68,49 @@ function dismantle_scalar(n::AggregateNode, ctx) end function dismantle(n::AppendNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) args′ = dismantle(n.args, ctx) - Append(over = over′, args = args′) + Append(args = args′, tail = tail′) end function dismantle(n::AsNode, ctx) - over′ = dismantle(n.over, ctx) - As(over = over′, name = n.name) + tail′ = dismantle(ctx) + As(name = n.name, tail = tail′) end function dismantle_scalar(n::AsNode, ctx) - over′ = dismantle_scalar(n.over, ctx) - As(over = over′, name = n.name) + tail′ = dismantle_scalar(ctx) + As(name = n.name, tail = tail′) end function dismantle(n::BindNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) args′ = dismantle_scalar(n.args, ctx) - BindNode(over = over′, args = args′, label_map = n.label_map) + Bind(args = args′, label_map = n.label_map, tail = tail′) end function dismantle_scalar(n::BindNode, ctx) - over′ = dismantle_scalar(n.over, ctx) + tail′ = dismantle_scalar(ctx) args′ = dismantle_scalar(n.args, ctx) - BindNode(over = over′, args = args′, label_map = n.label_map) + Bind(args = args′, label_map = n.label_map, tail = tail′) end dismantle_scalar(n::Union{BoundVariableNode, GetNode, LiteralNode, VariableNode}, ctx) = - convert(SQLNode, n) + SQLQuery(ctx.tail, n) function dismantle(n::DefineNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) args′ = dismantle_scalar(n.args, ctx) - Define(over = over′, args = args′, label_map = n.label_map) + Define(args = args′, label_map = n.label_map, tail = tail′) end function dismantle(n::FromFunctionNode, ctx) - over′ = dismantle_scalar(n.over, ctx) - FromFunction(over = over′, columns = n.columns) + tail′ = dismantle_scalar(ctx) + FromFunction(columns = n.columns, tail = tail′) end dismantle(n::Union{FromIterateNode, FromNothingNode, FromTableExpressionNode, FromTableNode, FromValuesNode}, ctx) = - convert(SQLNode, n) + SQLQuery(ctx.tail, n) function dismantle_scalar(n::FunctionNode, ctx) args′ = dismantle_scalar(n.args, ctx) @@ -109,136 +118,140 @@ function dismantle_scalar(n::FunctionNode, ctx) end function dismantle(n::GroupNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) by′ = dismantle_scalar(n.by, ctx) - Group(over = over′, by = by′, sets = n.sets, name = n.name, label_map = n.label_map) + Group(by = by′, sets = n.sets, name = n.name, label_map = n.label_map, tail = tail′) end function dismantle(n::IterateNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) iterator′ = dismantle(n.iterator, ctx) - Iterate(over = over′, iterator = iterator′) + Iterate(iterator = iterator′, tail = tail′) end function dismantle(n::JoinNode, ctx) rt = row_type(n.joinee) router = JoinRouter(Set(keys(rt.fields)), !isa(rt.group, EmptyType)) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) joinee′ = dismantle(n.joinee, ctx) on′ = dismantle_scalar(n.on, ctx) - RoutedJoin(over = over′, joinee = joinee′, on = on′, router = router, left = n.left, right = n.right, optional = n.optional) + RoutedJoin(joinee = joinee′, on = on′, router = router, left = n.left, right = n.right, optional = n.optional, tail = tail′) end function dismantle(n::LimitNode, ctx) - over′ = dismantle(n.over, ctx) - Limit(over = over′, offset = n.offset, limit = n.limit) + tail′ = dismantle(ctx) + Limit(offset = n.offset, limit = n.limit, tail = tail′) end function dismantle_scalar(n::NestedNode, ctx) - over′ = dismantle_scalar(n.over, ctx) - NestedNode(over = over′, name = n.name) + tail′ = dismantle_scalar(ctx) + Nested(name = n.name, tail = tail′) end function dismantle(n::OrderNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) by′ = dismantle_scalar(n.by, ctx) - Order(over = over′, by = by′) + Order(by = by′, tail = tail′) end function dismantle(n::PaddingNode, ctx) - over′ = dismantle(n.over, ctx) - Padding(over = over′) + tail′ = dismantle(ctx) + Padding(tail = tail′) end function dismantle(n::PartitionNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) by′ = dismantle_scalar(n.by, ctx) order_by′ = dismantle_scalar(n.order_by, ctx) - Partition(over = over′, by = by′, order_by = order_by′, frame = n.frame, name = n.name) + Partition(by = by′, order_by = order_by′, frame = n.frame, name = n.name, tail = tail′) end dismantle(n::ResolvedNode, ctx) = - dismantle(n.over, ctx) + dismantle(ctx) function dismantle_scalar(n::ResolvedNode, ctx) t = n.type if t isa RowType - n′ = dismantle(n.over, ctx) + n′ = dismantle(ctx) push!(ctx.defs, n′) ref = lastindex(ctx.defs) Isolated(ref, t) else - dismantle_scalar(n.over, ctx) + dismantle_scalar(ctx) end end function dismantle(n::SelectNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) args′ = dismantle_scalar(n.args, ctx) - Select(over = over′, args = args′, label_map = n.label_map) + Select(args = args′, label_map = n.label_map, tail = tail′) end function dismantle_scalar(n::SortNode, ctx) - over′ = dismantle_scalar(n.over, ctx) - Sort(over = over′, value = n.value, nulls = n.nulls) + tail′ = dismantle_scalar(ctx) + Sort(value = n.value, nulls = n.nulls, tail = tail′) end function dismantle(n::WhereNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) condition′ = dismantle_scalar(n.condition, ctx) - Where(over = over′, condition = condition′) + Where(condition = condition′, tail = tail′) end function dismantle(n::WithNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) args′ = dismantle(n.args, ctx) - With(over = over′, args = args′, materialized = n.materialized, label_map = n.label_map) + With(args = args′, materialized = n.materialized, label_map = n.label_map, tail = tail′) end function dismantle(n::WithExternalNode, ctx) - over′ = dismantle(n.over, ctx) + tail′ = dismantle(ctx) args′ = dismantle(n.args, ctx) - WithExternal(over = over′, args = args′, qualifiers = n.qualifiers, handler = n.handler, label_map = n.label_map) + WithExternal(args = args′, qualifiers = n.qualifiers, handler = n.handler, label_map = n.label_map, tail = tail′) +end + +function link(ctx::LinkContext) + link(ctx.tail, ctx) end -function link(n::SQLNode, ctx) - convert(SQLNode, link(n[], ctx)) +function link(q::SQLQuery, ctx) + convert(SQLQuery, link(q.head, LinkContext(ctx, tail = q.tail))) end -function link(ns::Vector{SQLNode}, ctx) - SQLNode[link(n, ctx) for n in ns] +function link(qs::Vector{SQLQuery}, ctx) + SQLQuery[link(q, ctx) for q in qs] end link(n, ctx, refs) = link(n, LinkContext(ctx, refs = refs)) function link(n::AppendNode, ctx) - over′ = Linked(ctx.refs, over = link(n.over, ctx)) - args′ = SQLNode[Linked(ctx.refs, over = link(arg, ctx)) for arg in n.args] - Append(over = over′, args = args′) + tail′ = Linked(ctx.refs, tail = link(ctx)) + args′ = SQLQuery[Linked(ctx.refs, tail = link(arg, ctx)) for arg in n.args] + Append(args = args′, tail = tail′) end function link(n::AsNode, ctx) - refs = SQLNode[] + refs = SQLQuery[] for ref in ctx.refs - if @dissect(ref, (local over) |> Nested(name = (local name))) + if @dissect(ref, (local tail) |> Nested(name = (local name))) @assert name == n.name - push!(refs, over) + push!(refs, tail) else error() end end - over′ = link(n.over, ctx, refs) - As(over = over′, name = n.name) + tail′ = link(ctx.tail, ctx, refs) + As(name = n.name, tail = tail′) end function link(n::BindNode, ctx) - over′ = link(n.over, ctx) - Bind(over = over′, args = n.args, label_map = n.label_map) + tail′ = link(ctx) + Bind(args = n.args, label_map = n.label_map, tail = tail′) end function link(n::DefineNode, ctx) - refs = SQLNode[] + refs = SQLQuery[] seen = Set{Symbol}() for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) && name in keys(n.label_map) @@ -248,10 +261,10 @@ function link(n::DefineNode, ctx) end end if isempty(seen) - return link(n.over, ctx) + return link(ctx) end n_ext_refs = length(refs) - args′ = SQLNode[] + args′ = SQLQuery[] label_map′ = OrderedDict{Symbol, Int}() for (f, i) in n.label_map f in seen || continue @@ -260,15 +273,15 @@ function link(n::DefineNode, ctx) push!(args′, arg′) label_map′[f] = lastindex(args′) end - over′ = Linked(refs, n_ext_refs, over = link(n.over, ctx, refs)) + tail′ = Linked(refs, n_ext_refs, tail = link(ctx.tail, ctx, refs)) Define( - over = over′, args = args′, - label_map = label_map′) + label_map = label_map′, + tail = tail′) end link(n::Union{FromFunctionNode, FromNothingNode, FromTableNode, FromValuesNode}, ctx) = - convert(SQLNode, n) + SQLQuery(ctx.tail, n) function link(n::FromIterateNode, ctx) append!(ctx.knot_refs, ctx.refs) @@ -278,7 +291,7 @@ end function link(n::FromTableExpressionNode, ctx) refs = ctx.cte_refs[(n.name, n.depth)] for ref in ctx.refs - push!(refs, Nested(over = ref, name = n.name)) + push!(refs, Nested(name = n.name, tail = ref)) end n end @@ -290,7 +303,7 @@ function link(n::GroupNode, ctx) end # Some group keys are added both to SELECT and to GROUP BY. # To avoid duplicate SQL, they must be evaluated in a nested subquery. - refs = SQLNode[] + refs = SQLQuery[] append!(refs, n.by) if n.sets !== nothing # Force evaluation in a nested subquery. @@ -312,25 +325,25 @@ function link(n::GroupNode, ctx) end end end - over = n.over + tail = ctx.tail if !isempty(n.by) - over = Padding(over = over) + tail = Padding(tail = tail) end - over′ = Linked(refs, 0, over = link(over, ctx, refs)) - Group(over = over′, by = n.by, sets = n.sets, name = n.name, label_map = n.label_map) + tail′ = Linked(refs, 0, tail = link(tail, ctx, refs)) + Group(by = n.by, sets = n.sets, name = n.name, label_map = n.label_map, tail = tail′) end function link(n::IterateNode, ctx) iterator′ = n.iterator defs = copy(ctx.defs) cte_refs = [(v, length(v)) for (k, v) in ctx.cte_refs] - refs = SQLNode[] - knot_refs = SQLNode[] + refs = SQLQuery[] + knot_refs = SQLQuery[] repeat = true while repeat refs = copy(ctx.refs) append!(refs, knot_refs) - knot_refs = SQLNode[] + knot_refs = SQLQuery[] for (v, l) in cte_refs resize!(v, l) end @@ -345,39 +358,39 @@ function link(n::IterateNode, ctx) end end end - iterator′ = Linked(refs, over = iterator′) - over′ = Linked(refs, over = link(n.over, ctx, refs)) - n′ = Linked(refs, over = Iterate(over = over′, iterator = iterator′)) - Padding(over = n′) + iterator′ = Linked(refs, tail = iterator′) + tail′ = Linked(refs, tail = link(ctx.tail, ctx, refs)) + q′ = Linked(refs, tail = Iterate(iterator = iterator′, tail = tail′)) + Padding(tail = q′) end -function route(r::JoinRouter, ref::SQLNode) - if @dissect(ref, (local over) |> Nested(name = (local name))) && name in r.label_set +function route(r::JoinRouter, ref::SQLQuery) + if @dissect(ref, Nested(name = (local name))) && name in r.label_set return 1 end if @dissect(ref, Get(name = (local name))) && name in r.label_set return 1 end - if @dissect(ref, (local over) |> Agg()) && r.group + if @dissect(ref, Agg()) && r.group return 1 end return -1 end function link(n::RoutedJoinNode, ctx) - lrefs = SQLNode[] - rrefs = SQLNode[] + lrefs = SQLQuery[] + rrefs = SQLQuery[] for ref in ctx.refs turn = route(n.router, ref) push!(turn < 0 ? lrefs : rrefs, ref) end if n.optional && isempty(rrefs) - return link(n.over, ctx) + return link(ctx) end ln_ext_refs = length(lrefs) rn_ext_refs = length(rrefs) - refs′ = SQLNode[] - lateral_refs = SQLNode[] + refs′ = SQLQuery[] + lateral_refs = SQLQuery[] gather!(n.joinee, ctx, lateral_refs) append!(lrefs, lateral_refs) lateral = !isempty(lateral_refs) @@ -386,41 +399,41 @@ function link(n::RoutedJoinNode, ctx) turn = route(n.router, ref) push!(turn < 0 ? lrefs : rrefs, ref) end - over′ = Linked(lrefs, ln_ext_refs, over = link(n.over, ctx, lrefs)) - joinee′ = Linked(rrefs, rn_ext_refs, over = link(n.joinee, ctx, rrefs)) - RoutedJoinNode( - over = over′, + tail′ = Linked(lrefs, ln_ext_refs, tail = link(ctx.tail, ctx, lrefs)) + joinee′ = Linked(rrefs, rn_ext_refs, tail = link(n.joinee, ctx, rrefs)) + RoutedJoin( joinee = joinee′, on = n.on, router = n.router, left = n.left, right = n.right, - lateral = lateral) + lateral = lateral, + tail = tail′) end function link(n::LimitNode, ctx) - over′ = Linked(ctx.refs, over = link(n.over, ctx)) - Limit(over = over′, offset = n.offset, limit = n.limit) + tail′ = Linked(ctx.refs, tail = link(ctx)) + Limit(offset = n.offset, limit = n.limit, tail = tail′) end function link(n::OrderNode, ctx) refs = copy(ctx.refs) n_ext_refs = length(refs) gather!(n.by, ctx, refs) - over′ = Linked(refs, n_ext_refs, over = link(n.over, ctx, refs)) - Order(over = over′, by = n.by) + tail′ = Linked(refs, n_ext_refs, tail = link(ctx.tail, ctx, refs)) + Order(by = n.by, tail = tail′) end function link(n::PaddingNode, ctx) - refs = SQLNode[] + refs = SQLQuery[] gather!(ctx.refs, ctx, refs) - over′ = Linked(refs, 0, over = link(n.over, ctx, refs)) - Padding(over = over′) + tail′ = Linked(refs, 0, tail = link(ctx.tail, ctx, refs)) + Padding(tail = tail′) end function link(n::PartitionNode, ctx) - refs = SQLNode[] - imm_refs = SQLNode[] + refs = SQLQuery[] + imm_refs = SQLQuery[] ctx′ = LinkContext(ctx, refs = imm_refs) has_aggregates = false for ref in ctx.refs @@ -436,29 +449,29 @@ function link(n::PartitionNode, ctx) end end if !has_aggregates - return link(n.over, ctx) + return link(ctx) end gather!(n.by, ctx′) gather!(n.order_by, ctx′) n_ext_refs = length(refs) append!(refs, imm_refs) - over′ = Linked(refs, n_ext_refs, over = link(n.over, ctx, refs)) - Partition(over = over′, by = n.by, order_by = n.order_by, frame = n.frame, name = n.name) + tail′ = Linked(refs, n_ext_refs, tail = link(ctx.tail, ctx, refs)) + Partition(by = n.by, order_by = n.order_by, frame = n.frame, name = n.name, tail = tail′) end function link(n::SelectNode, ctx) - refs = SQLNode[] + refs = SQLQuery[] gather!(n.args, ctx, refs) - over′ = Linked(refs, 0, over = link(n.over, ctx, refs)) - Select(over = over′, args = n.args, label_map = n.label_map) + tail′ = Linked(refs, 0, tail = link(ctx.tail, ctx, refs)) + Select(args = n.args, label_map = n.label_map, tail = tail′) end function link(n::WhereNode, ctx) refs = copy(ctx.refs) n_ext_refs = length(refs) gather!(n.condition, ctx, refs) - over′ = Linked(refs, n_ext_refs, over = link(n.over, ctx, refs)) - Where(n.condition, over = over′) + tail′ = Linked(refs, n_ext_refs, tail = link(ctx.tail, ctx, refs)) + Where(n.condition, tail = tail′) end function _cte_depth(dict, name) @@ -472,38 +485,46 @@ end function link(n::Union{WithNode, WithExternalNode}, ctx) cte_refs′ = ctx.cte_refs - refs_map = Vector{SQLNode}[] + refs_map = Vector{SQLQuery}[] for name in keys(n.label_map) depth = _cte_depth(ctx.cte_refs, name) + 1 - refs = SQLNode[] + refs = SQLQuery[] cte_refs′ = Base.ImmutableDict(cte_refs′, (name, depth) => refs) push!(refs_map, refs) end ctx′ = LinkContext(ctx, cte_refs = cte_refs′) - over′ = Linked(ctx′.refs, over = link(n.over, ctx′)) - args′ = SQLNode[] + tail′ = Linked(ctx′.refs, tail = link(ctx′)) + args′ = SQLQuery[] label_map′ = OrderedDict{Symbol, Int}() for (f, i) in n.label_map arg = n.args[i] refs = refs_map[i] - arg′ = Linked(refs, over = link(arg, ctx, refs)) + arg′ = Linked(refs, tail = link(arg, ctx, refs)) push!(args′, arg′) label_map′[f] = lastindex(args′) end if n isa WithNode - With(over = over′, args = args′, materialized = n.materialized, label_map = label_map′) + With(args = args′, materialized = n.materialized, label_map = label_map′, tail = tail′) else - WithExternal(over = over′, args = args′, qualifiers = n.qualifiers, handler = n.handler, label_map = label_map′) + WithExternal(args = args′, qualifiers = n.qualifiers, handler = n.handler, label_map = label_map′, tail = tail′) end end -function gather!(n::SQLNode, ctx) - gather!(n[], ctx) +function gather!(q::SQLQuery, ctx) + if @dissect(q, Agg() || Get() || Nested()) + push!(ctx.refs, q) + else + gather!(q.head, LinkContext(ctx, tail = q.tail)) + end end -function gather!(ns::Vector{SQLNode}, ctx) - for n in ns - gather!(n, ctx) +function gather!(ctx::LinkContext) + gather!(ctx.tail, ctx) +end + +function gather!(qs::Vector{SQLQuery}, ctx) + for q in qs + gather!(q, ctx) end end @@ -513,18 +534,13 @@ gather!(n::AbstractSQLNode, ctx) = gather!(n, ctx, refs) = gather!(n, LinkContext(ctx, refs = refs)) -function gather!(n::Union{AggregateNode, GetNode, NestedNode}, ctx) - push!(ctx.refs, n) - nothing -end - function gather!(n::Union{AsNode, FromFunctionNode, ResolvedNode, SortNode}, ctx) - gather!(n.over, ctx) + gather!(ctx) end function gather!(n::BindNode, ctx) - gather!(n.over, ctx) - refs′ = SQLNode[] + gather!(ctx) + refs′ = SQLQuery[] gather!(n.args, ctx, refs′) append!(ctx.refs, refs′) # Force aggregates and other complex definitions to be wrapped @@ -540,14 +556,14 @@ end function gather!(n::IsolatedNode, ctx) def = ctx.defs[n.idx] !@dissect(def, Linked()) || return - refs = SQLNode[] + refs = SQLQuery[] for (f, ft) in n.type.fields if ft isa ScalarType push!(refs, Get(f)) break end end - def′ = Linked(refs, over = link(def, ctx, refs)) + def′ = Linked(refs, tail = link(def, ctx, refs)) ctx.defs[n.idx] = def′ nothing end diff --git a/src/nodes.jl b/src/nodes.jl index c2abafe9..ec9f9be1 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -4,110 +4,138 @@ # Base node type. """ -A tabular or a scalar operation that can be expressed as a SQL query. +A component of a SQL query tree. """ abstract type AbstractSQLNode end +terminal(::Type{<:AbstractSQLNode}) = + false + +terminal(n::N) where {N <: AbstractSQLNode} = + terminal(N) + """ A node that produces tabular output. """ abstract type TabularNode <: AbstractSQLNode end -function dissect(scr::Symbol, NodeType::Type{<:AbstractSQLNode}, pats::Vector{Any}) - for pat in pats - if pat isa Expr && pat.head === :kw && length(pat.args) == 2 && pat.args[1] === :tail - pat.args[1] = :over - end - end - scr_core = gensym(:scr_core) - ex = Expr(:&&, :($scr_core isa $NodeType), Any[dissect(scr_core, pat) for pat in pats]...) - :($scr isa SQLNode && (local $scr_core = $scr[]; $ex)) -end - -# Specialization barrier node. +# Opaque linked list of SQL nodes. """ -An opaque wrapper over an arbitrary SQL node. +SQL query represented as a linked list of SQL nodes. """ -struct SQLNode <: AbstractSQLNode - core::AbstractSQLNode +mutable struct SQLQuery + const tail::Union{SQLQuery, Nothing} + const head::AbstractSQLNode - SQLNode(@nospecialize core::AbstractSQLNode) = - new(core) -end + SQLQuery(@nospecialize head::AbstractSQLNode) = + new(nothing, head) -Base.getindex(n::SQLNode) = - getfield(n, :core) + function SQLQuery(tail, head::AbstractSQLNode) + if tail !== nothing && terminal(head) + throw(RebaseError(path = [SQLQuery(head)])) + end + new(tail, head) + end +end -Base.convert(::Type{SQLNode}, n::SQLNode) = - n +Base.convert(::Type{SQLQuery}, @nospecialize n::AbstractSQLNode) = + SQLQuery(n) -Base.convert(::Type{SQLNode}, @nospecialize n::AbstractSQLNode) = - SQLNode(n) +terminal(q::SQLQuery) = + q.tail !== nothing ? terminal(q.tail) : terminal(q.head) -Base.convert(::Type{SQLNode}, obj) = - convert(SQLNode, convert(AbstractSQLNode, obj)::AbstractSQLNode) +(q::SQLQuery)(q′) = + SQLQuery(q.tail !== nothing ? q.tail(q′) : q′, q.head) -(n::AbstractSQLNode)(n′) = - n(convert(SQLNode, n′)) +Chain(q′, q) = + convert(SQLQuery, q)(q′) -(n::AbstractSQLNode)(n′::SQLNode) = - rebase(n, n′) +label(q::SQLQuery) = + @something label(q.head) label(q.tail) -label(n::SQLNode) = - label(n[])::Symbol +label(n::AbstractSQLNode) = + nothing label(::Nothing) = :_ -@generated function label(n::AbstractSQLNode) - if :over in fieldnames(n) - return :(label(n.over)) - else - return :(:_) - end +label(q) = + label(convert(SQLQuery, q)) + + +# A variant of SQLQuery for assembling a chain of identifiers. + +struct SQLGetQuery + tail::Union{SQLGetQuery, Nothing} + head::Symbol end -rebase(::Nothing, n′) = - n′ +Base.getproperty(q::SQLGetQuery, name::Symbol) = + SQLGetQuery(q, Symbol(name)) -rebase(n::SQLNode, n′) = - convert(SQLNode, rebase(n[], n′)) +Base.getproperty(q::SQLGetQuery, name::AbstractString) = + SQLGetQuery(q, Symbol(name)) -@generated function rebase(n::AbstractSQLNode, n′) - fs = fieldnames(n) - if !in(:over, fs) - return quote - throw(RebaseError(path = [n])) - end +Base.getindex(q::SQLGetQuery, name::Union{Symbol, AbstractString}) = + SQLGetQuery(q, Symbol(name)) + +(q::SQLGetQuery)(::Nothing) = + q + +(q::SQLGetQuery)(q′::SQLGetQuery) = + let head = getfield(q, :head), tail = getfield(q, :tail) + SQLGetQuery(tail !== nothing ? tail(q′) : q′, head) + end + +(q::SQLGetQuery)(q′) = + convert(SQLQuery, q)(q′) + +Base.show(io::IO, q::SQLGetQuery) = + print(io, quoteof(q)) + +function PrettyPrinting.quoteof(q::SQLGetQuery) + path = Symbol[] + while q !== nothing + push!(path, getfield(q, :head)) + q = getfield(q, :tail) end - return quote - $n($(Any[Expr(:kw, f, f === :over ? :(rebase(n.$(f), n′)) : :(n.$(f))) - for f in fs]...)) + ex = :Get + while !isempty(path) + ex = Expr(:., ex, QuoteNode(pop!(path))) end + ex end -Chain(n′, n) = - rebase(convert(SQLNode, n), n′) +function _tosqlgetquery(q) + q.head isa GetNode || return + tail′ = nothing + if q.tail !== nothing + tail′ = _tosqlgetquery(q.tail) + tail′ !== nothing || return + end + SQLGetQuery(tail′, q.head.name) +end # Generic traversal and substitution. -function visit(f, n::SQLNode, visiting = Set{SQLNode}()) - !(n in visiting) || return - push!(visiting, n) - visit(f, n[], visiting) - f(n) - pop!(visiting, n) +function visit(f, q::SQLQuery, visiting = Set{SQLQuery}()) + !(q in visiting) || return + push!(visiting, q) + visit(f, q.tail, visiting) + visit(f, q.head, visiting) + f(q) + pop!(visiting, q) nothing end -function visit(f, ns::Vector{SQLNode}, visiting) - for n in ns - visit(f, n, visiting) +function visit(f, qs::Vector{SQLQuery}, visiting) + for q in qs + visit(f, q, visiting) end end @@ -118,7 +146,7 @@ visit(f, ::Nothing, visiting) = exs = Expr[] for f in fieldnames(n) t = fieldtype(n, f) - if t === SQLNode || t === Union{SQLNode, Nothing} || t === Vector{SQLNode} + if t === SQLQuery || t === Union{SQLQuery, Nothing} || t === Vector{SQLQuery} ex = quote visit(f, n.$(f), visiting) end @@ -129,26 +157,30 @@ visit(f, ::Nothing, visiting) = Expr(:block, exs...) end -substitute(n::SQLNode, c::SQLNode, c′::SQLNode) = - SQLNode(substitute(n[], c, c′)) +substitute(q::SQLQuery, c::SQLQuery, c′::SQLQuery) = + if q.tail === c + SQLQuery(c′, q.head) + else + SQLQuery(q.tail, substitute(q.head, c, c′)) + end -function substitute(ns::Vector{SQLNode}, c::SQLNode, c′::SQLNode) - i = findfirst(isequal(c), ns) - i !== nothing || return ns - ns′ = copy(ns) - ns′[i] = c′ - ns′ +function substitute(qs::Vector{SQLQuery}, c::SQLQuery, c′::SQLQuery) + i = findfirst(q -> q === c, qs) + i !== nothing || return qs + qs′ = copy(qs) + qs′[i] = c′ + qs′ end -substitute(::Nothing, ::SQLNode, ::SQLNode) = +substitute(::Nothing, ::SQLQuery, ::SQLQuery) = nothing -@generated function substitute(n::AbstractSQLNode, c::SQLNode, c′::SQLNode) +@generated function substitute(n::AbstractSQLNode, c::SQLQuery, c′::SQLQuery) exs = Expr[] fs = fieldnames(n) for f in fs t = fieldtype(n, f) - if t === SQLNode || t === Union{SQLNode, Nothing} + if t === SQLQuery || t === Union{SQLQuery, Nothing} ex = quote if n.$(f) === c return $n($(Any[Expr(:kw, f′, f′ !== f ? :(n.$(f′)) : :(c′)) @@ -156,7 +188,7 @@ substitute(::Nothing, ::SQLNode, ::SQLNode) = end end push!(exs, ex) - elseif t === Vector{SQLNode} + elseif t === Vector{SQLQuery} ex = quote let cs′ = substitute(n.$(f), c, c′) if cs′ !== n.$(f) @@ -175,49 +207,55 @@ end # Pretty-printing. -Base.show(io::IO, n::AbstractSQLNode) = +Base.show(io::IO, n::Union{AbstractSQLNode, SQLQuery}) = print(io, quoteof(n, limit = true)) -Base.show(io::IO, ::MIME"text/plain", n::AbstractSQLNode) = +Base.show(io::IO, ::MIME"text/plain", n::Union{AbstractSQLNode, SQLQuery}) = pprint(io, n) -function PrettyPrinting.quoteof(n::SQLNode; - limit::Bool = false, - unwrap::Bool = false) +function PrettyPrinting.quoteof(q::SQLQuery; limit::Bool = false, head_only::Bool = false) + if q.head isa GetNode && !head_only + q′ = _tosqlgetquery(q) + if q′ !== nothing + return quoteof(q′) + end + end if limit ctx = QuoteContext(limit = true) - ex = quoteof(n[], ctx) - if unwrap - ex = Expr(:ref, ex) + ex = quoteof(q.head, ctx) + if head_only + ex = Expr(:., ex, QuoteNode(:head)) + elseif q.tail !== nothing + ex = Expr(:call, :|>, quoteof(q.tail, ctx), ex) end return ex end tables_seen = OrderedSet{SQLTable}() - nodes_seen = OrderedSet{SQLNode}() - nodes_toplevel = Set{SQLNode}() - visit(n) do n - core = n[] - if core isa FromNode - source = core.source + queries_seen = OrderedSet{SQLQuery}() + queries_toplevel = Set{SQLQuery}() + visit(q) do q + head = q.head + if head isa FromNode + source = head.source if source isa SQLTable push!(tables_seen, source) end end - if core isa FromTableNode - push!(tables_seen, core.table) + if head isa FromTableNode + push!(tables_seen, head.table) end - if core isa TabularNode - push!(nodes_seen, n) - push!(nodes_toplevel, n) - elseif n in nodes_seen - push!(nodes_toplevel, n) + if head isa TabularNode + push!(queries_seen, q) + push!(queries_toplevel, q) + elseif q in queries_seen + push!(queries_toplevel, q) else - push!(nodes_seen, n) + push!(queries_seen, q) end end ctx = QuoteContext() defs = Any[] - if length(nodes_toplevel) >= 2 || (length(nodes_toplevel) == 1 && !(n in nodes_toplevel)) + if length(queries_toplevel) >= 2 || (length(queries_toplevel) == 1 && !(q in queries_toplevel)) for t in tables_seen def = quoteof(t, limit = true) name = t.name @@ -225,23 +263,23 @@ function PrettyPrinting.quoteof(n::SQLNode; ctx.vars[t] = name end qidx = 0 - for n in nodes_seen - n in nodes_toplevel || continue + for q in queries_seen + q in queries_toplevel || continue qidx += 1 - ctx.vars[n] = Symbol('q', qidx) + ctx.vars[q] = Symbol('q', qidx) end qidx = 0 - for n in nodes_seen - n in nodes_toplevel || continue + for q in queries_seen + q in queries_toplevel || continue qidx += 1 name = Symbol('q', qidx) - def = quoteof(n, ctx, true, true) + def = quoteof(q, ctx, true, true) push!(defs, Expr(:(=), name, def)) end end - ex = quoteof(n, ctx, true, false) - if unwrap - ex = Expr(:ref, ex) + ex = quoteof(q, ctx, true, false) + if head_only + ex = Expr(:., ex, QuoteNode(:head)) end if !isempty(defs) ex = Expr(:let, Expr(:block, defs...), ex) @@ -250,28 +288,39 @@ function PrettyPrinting.quoteof(n::SQLNode; end PrettyPrinting.quoteof(n::AbstractSQLNode; limit::Bool = false) = - quoteof(convert(SQLNode, n), limit = limit, unwrap = true) + quoteof(convert(SQLQuery, n), limit = limit, head_only = true) -PrettyPrinting.quoteof(n::SQLNode, ctx::QuoteContext, top::Bool = false, full::Bool = false) = +function PrettyPrinting.quoteof(q::SQLQuery, ctx::QuoteContext, top::Bool = false, full::Bool = false) if !ctx.limit - !full || return quoteof(n[], ctx) - var = get(ctx.vars, n, nothing) - if var !== nothing + if q.head isa HighlightNode && q.tail !== nothing + color = q.head.color + push!(ctx.colors, color) + ex = quoteof(q.tail, ctx) + pop!(ctx.colors) + EscWrapper(ex, color, copy(ctx.colors)) + elseif !full && (local var = get(ctx.vars, q, nothing); var !== nothing) var - elseif !top && (local over = n[]; over isa LiteralNode) && (local val = over.val; val isa SQLLiteralType) - quoteof(val) + elseif !full && !top && q.tail === nothing && q.head isa LiteralNode && q.head.val isa SQLLiteralType + quoteof(q.head.val) + elseif q.head isa GetNode && (local q′ = _tosqlgetquery(q); q′) !== nothing + quoteof(q′) else - quoteof(n[], ctx) + ex = quoteof(q.head, ctx) + if q.tail !== nothing + ex = Expr(:call, :|>, quoteof(q.tail, ctx), ex) + end + ex end else :… end +end -PrettyPrinting.quoteof(ns::Vector{SQLNode}, ctx::QuoteContext) = - if isempty(ns) +PrettyPrinting.quoteof(qs::Vector{SQLQuery}, ctx::QuoteContext) = + if isempty(qs) Any[] elseif !ctx.limit - Any[quoteof(n, ctx) for n in ns] + Any[quoteof(q, ctx) for q in qs] else Any[:…] end @@ -284,9 +333,9 @@ A duplicate label where unique labels are expected. """ struct DuplicateLabelError <: FunSQLError name::Symbol - path::Vector{SQLNode} + path::Vector{SQLQuery} - DuplicateLabelError(name; path = SQLNode[]) = + DuplicateLabelError(name; path = SQLQuery[]) = new(name, path) end @@ -302,9 +351,9 @@ struct InvalidArityError <: FunSQLError name::Symbol expected::Union{Int, UnitRange{Int}} actual::Int - path::Vector{SQLNode} + path::Vector{SQLQuery} - InvalidArityError(name, expected, actual; path = SQLNode[]) = + InvalidArityError(name, expected, actual; path = SQLQuery[]) = new(name, expected, actual, path) end @@ -331,7 +380,7 @@ function checkarity!(n) expected = arity(n.name) actual = length(n.args) if !(expected isa Int ? actual >= expected : actual in expected) - throw(InvalidArityError(n.name, expected, actual, path = SQLNode[n])) + throw(InvalidArityError(n.name, expected, actual, path = SQLQuery[n])) end end @@ -339,9 +388,9 @@ end A scalar operation where a tabular operation is expected. """ struct IllFormedError <: FunSQLError - path::Vector{SQLNode} + path::Vector{SQLQuery} - IllFormedError(; path = SQLNode[]) = + IllFormedError(; path = SQLQuery[]) = new(path) end @@ -354,9 +403,9 @@ end A node that cannot be rebased. """ struct RebaseError <: FunSQLError - path::Vector{SQLNode} + path::Vector{SQLQuery} - RebaseError(; path = SQLNode[]) = + RebaseError(; path = SQLQuery[]) = new(path) end @@ -370,9 +419,9 @@ Grouping sets are specified incorrectly. """ struct InvalidGroupingSetsError <: FunSQLError value::Union{Int, Symbol, Vector{Symbol}} - path::Vector{SQLNode} + path::Vector{SQLQuery} - InvalidGroupingSetsError(value; path = SQLNode[]) = + InvalidGroupingSetsError(value; path = SQLQuery[]) = new(value, path) end @@ -411,9 +460,9 @@ An undefined or an invalid reference. struct ReferenceError <: FunSQLError type::ReferenceErrorType name::Union{Symbol, Nothing} - path::Vector{SQLNode} + path::Vector{SQLQuery} - ReferenceError(type; name = nothing, path = SQLNode[]) = + ReferenceError(type; name = nothing, path = SQLQuery[]) = new(type, name, path) end @@ -437,7 +486,7 @@ function Base.showerror(io::IO, err::ReferenceError) showpath(io, err.path) end -function showpath(io, path::Vector{SQLNode}) +function showpath(io, path::Vector{SQLQuery}) if !isempty(path) q = highlight(path) println(io, " in:") @@ -445,13 +494,13 @@ function showpath(io, path::Vector{SQLNode}) end end -function highlight(path::Vector{SQLNode}, color = Base.error_color()) +function highlight(path::Vector{SQLQuery}, color = Base.error_color()) @assert !isempty(path) - n = Highlight(over = path[end], color = color) + q = Highlight(tail = path[end], color = color) for k = lastindex(path):-1:2 - n = substitute(path[k - 1], path[k], n) + q = substitute(path[k - 1], path[k], q) end - n + q end """ @@ -473,7 +522,7 @@ function populate_label_map!(n, args = n.args, label_map = n.label_map, group_na for (i, arg) in enumerate(args) name = label(arg) if name === group_name || name in keys(label_map) - err = DuplicateLabelError(name, path = [n, arg]) + err = DuplicateLabelError(name, path = SQLQuery[n, arg]) throw(err) end label_map[name] = i @@ -482,6 +531,40 @@ function populate_label_map!(n, args = n.args, label_map = n.label_map, group_na end +# Support for query constructors. + +struct SQLQueryCtor{N<:AbstractSQLNode} + id::Symbol +end + +Base.show(io::IO, @nospecialize ctor::SQLQueryCtor{N}) where {N} = + print(io, ctor.id) + +(::SQLQueryCtor{N})(args...; tail = nothing, kws...) where {N} = + SQLQuery(tail, N(args...; kws...)) + +function dissect(scr::Symbol, ::SQLQueryCtor{N}, pats::Vector{Any}) where {N<:AbstractSQLNode} + head_pats = Any[] + tail_pats = Any[] + for pat in pats + if pat isa Expr && pat.head === :kw && length(pat.args) == 2 && pat.args[1] === :tail + push!(tail_pats, pat.args[2]) + else + push!(head_pats, pat) + end + end + scr_head = gensym(:scr_head) + head_ex = Expr(:&&, :($scr_head isa $N), Any[dissect(scr_head, pat) for pat in head_pats]...) + ex = Expr(:&&, :($scr isa SQLQuery), :(local $scr_head = $scr.head; $head_ex)) + if !isempty(tail_pats) + scr_tail = gensym(:scr_tail) + tail_ex = Expr(:&&, Any[dissect(scr_tail, pat) for pat in tail_pats]...) + push!(ex.args, :(local $scr_tail = $scr.tail; $tail_ex)) + end + ex +end + + # The @funsql macro. struct TransliterateContext diff --git a/src/nodes/aggregate.jl b/src/nodes/aggregate.jl index 87cd4824..f63a15b7 100644 --- a/src/nodes/aggregate.jl +++ b/src/nodes/aggregate.jl @@ -1,33 +1,31 @@ # Aggregate expression. -mutable struct AggregateNode <: AbstractSQLNode - over::Union{SQLNode, Nothing} +struct AggregateNode <: AbstractSQLNode name::Symbol - args::Vector{SQLNode} - filter::Union{SQLNode, Nothing} + args::Vector{SQLQuery} + filter::Union{SQLQuery, Nothing} function AggregateNode(; - over = nothing, name::Union{Symbol, AbstractString}, - args = SQLNode[], + args = SQLQuery[], filter = nothing) - n = new(over, Symbol(name), args, filter) + n = new(Symbol(name), args, filter) checkarity!(n) n end end -AggregateNode(name; over = nothing, args = SQLNode[], filter = nothing) = - AggregateNode(over = over, name = name, args = args, filter = filter) +AggregateNode(name; args = SQLQuery[], filter = nothing) = + AggregateNode(name = name, args = args, filter = filter) -AggregateNode(name, args...; over = nothing, filter = nothing) = - AggregateNode(over = over, name = name, args = SQLNode[args...], filter = filter) +AggregateNode(name, args...; filter = nothing) = + AggregateNode(name = name, args = SQLQuery[args...], filter = filter) """ - Agg(; over = nothing, name, args = [], filter = nothing) - Agg(name; over = nothing, args = [], filter = nothing) - Agg(name, args...; over = nothing, filter = nothing) - Agg.name(args...; over = nothing, filter = nothing) + Agg(; name, args = [], filter = nothing, tail = nothing) + Agg(name; args = [], filter = nothing, tail = nothing) + Agg(name, args...; filter = nothing, tail = nothing) + Agg.name(args...; filter = nothing, tail = nothing) An application of an aggregate function. @@ -120,25 +118,18 @@ SELECT FROM "visit_occurrence" AS "visit_occurrence_1" ``` """ -Agg(args...; kws...) = - AggregateNode(args...; kws...) |> SQLNode +const Agg = SQLQueryCtor{AggregateNode}(:Agg) const funsql_agg = Agg -dissect(scr::Symbol, ::typeof(Agg), pats::Vector{Any}) = - dissect(scr, AggregateNode, pats) - function PrettyPrinting.quoteof(n::AggregateNode, ctx::QuoteContext) ex = Expr(:call, - Expr(:., nameof(Agg), + Expr(:., :Agg, QuoteNode(Base.isidentifier(n.name) ? n.name : string(n.name)))) append!(ex.args, quoteof(n.args, ctx)) if n.filter !== nothing push!(ex.args, Expr(:kw, :filter, quoteof(n.filter, ctx))) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end @@ -155,9 +146,8 @@ end AggClosure(name::AbstractString) = AggClosure(Symbol(name)) -Base.show(io::IO, f::AggClosure) = - print(io, Expr(:., nameof(Agg), - QuoteNode(Base.isidentifier(f.name) ? f.name : string(f.name)))) +Base.show(io::IO, ctor::AggClosure) = + print(io, Expr(:., :Agg, QuoteNode(Base.isidentifier(ctor.name) ? ctor.name : string(ctor.name)))) Base.getproperty(::typeof(Agg), name::Symbol) = AggClosure(name) @@ -165,11 +155,11 @@ Base.getproperty(::typeof(Agg), name::Symbol) = Base.getproperty(::typeof(Agg), name::AbstractString) = AggClosure(name) -(f::AggClosure)(args...; over = nothing, filter = nothing) = - Agg(over = over, name = f.name, args = SQLNode[args...], filter = filter) +(ctor::AggClosure)(args...; filter = nothing, tail = nothing) = + Agg(name = ctor.name, args = SQLQuery[args...], filter = filter, tail = tail) -(f::AggClosure)(; over = nothing, args = SQLNode[], filter = nothing) = - Agg(over = over, name = f.name, args = args, filter = filter) +(ctor::AggClosure)(; args = SQLQuery[], filter = nothing, tail = nothing) = + Agg(name = ctor.name, args = args, filter = filter, tail = tail) # Common aggregate and window functions. diff --git a/src/nodes/append.jl b/src/nodes/append.jl index f837169c..4129d29c 100644 --- a/src/nodes/append.jl +++ b/src/nodes/append.jl @@ -1,19 +1,18 @@ # Append (UNION ALL) node. -mutable struct AppendNode <: TabularNode - over::Union{SQLNode, Nothing} - args::Vector{SQLNode} +struct AppendNode <: TabularNode + args::Vector{SQLQuery} - AppendNode(; over = nothing, args) = - new(over, args) + AppendNode(; args) = + new(args) end -AppendNode(args...; over = nothing) = - AppendNode(over = over, args = SQLNode[args...]) +AppendNode(args...) = + AppendNode(args = SQLQuery[args...]) """ - Append(; over = nothing, args) - Append(args...; over = nothing) + Append(; args, tail = nothing) + Append(args...; tail = nothing) `Append` concatenates input datasets. @@ -57,31 +56,16 @@ SELECT FROM "observation" AS "observation_1" ``` """ -Append(args...; kws...) = - AppendNode(args...; kws...) |> SQLNode +const Append = SQLQueryCtor{AppendNode}(:Append) const funsql_append = Append -dissect(scr::Symbol, ::typeof(Append), pats::Vector{Any}) = - dissect(scr, AppendNode, pats) - function PrettyPrinting.quoteof(n::AppendNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Append)) + ex = Expr(:call, :Append) if isempty(n.args) push!(ex.args, Expr(:kw, :args, Expr(:vect))) else append!(ex.args, quoteof(n.args, ctx)) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end - -function label(n::AppendNode) - lbl = label(n.over) - for l in n.args - label(l) === lbl || return :union - end - lbl -end diff --git a/src/nodes/as.jl b/src/nodes/as.jl index ca52308e..3a4a5db7 100644 --- a/src/nodes/as.jl +++ b/src/nodes/as.jl @@ -1,22 +1,20 @@ # AS wrapper. -mutable struct AsNode <: AbstractSQLNode - over::Union{SQLNode, Nothing} +struct AsNode <: AbstractSQLNode name::Symbol AsNode(; - over = nothing, name::Union{Symbol, AbstractString}) = - new(over, Symbol(name)) + new(Symbol(name)) end -AsNode(name; over = nothing) = - AsNode(over = over, name = name) +AsNode(name) = + AsNode(name = name) """ - As(; over = nothing, name) - As(name; over = nothing) - name => over + As(; name, tail = nothing) + As(name; tail = nothing) + name => tail In a scalar context, `As` specifies the name of the output column. When applied to tabular data, `As` wraps the data in a nested record. @@ -57,23 +55,15 @@ FROM "person" AS "person_1" JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id") ``` """ -As(args...; kws...) = - AsNode(args...; kws...) |> SQLNode +const As = SQLQueryCtor{AsNode}(:As) const funsql_as = As -dissect(scr::Symbol, ::typeof(As), pats::Vector{Any}) = - dissect(scr, AsNode, pats) - -Base.convert(::Type{AbstractSQLNode}, p::Pair{<:Union{Symbol, AbstractString}}) = - AsNode(name = first(p), over = convert(SQLNode, last(p))) +Base.convert(::Type{SQLQuery}, p::Pair{<:Union{Symbol, AbstractString}}) = + SQLQuery(last(p), AsNode(name = first(p))) function PrettyPrinting.quoteof(n::AsNode, ctx::QuoteContext) - ex = Expr(:call, nameof(As), quoteof(n.name)) - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end - ex + Expr(:call, :As, quoteof(n.name)) end label(n::AsNode) = diff --git a/src/nodes/bind.jl b/src/nodes/bind.jl index ccc7551f..56059660 100644 --- a/src/nodes/bind.jl +++ b/src/nodes/bind.jl @@ -1,27 +1,26 @@ # Binding query parameters. mutable struct BindNode <: AbstractSQLNode - over::Union{SQLNode, Nothing} - args::Vector{SQLNode} + args::Vector{SQLQuery} label_map::OrderedDict{Symbol, Int} - function BindNode(; over = nothing, args, label_map = nothing) + function BindNode(; args, label_map = nothing) if label_map !== nothing - new(over, args, label_map) + new(args, label_map) else - n = new(over, args, OrderedDict{Symbol, Int}()) + n = new(args, OrderedDict{Symbol, Int}()) populate_label_map!(n) n end end end -BindNode(args...; over = nothing) = - BindNode(over = over, args = SQLNode[args...]) +BindNode(args...) = + BindNode(args = SQLQuery[args...]) """ - Bind(; over = nothing; args) - Bind(args...; over = nothing) + Bind(; args, tail = nothing) + Bind(args...; tail = nothing) The `Bind` node evaluates a query with parameters. Specifically, `Bind` provides the values for [`Var`](@ref) parameters contained in the `over` node. @@ -87,23 +86,16 @@ LEFT JOIN LATERAL ( ) AS "visit_1" ON TRUE ``` """ -Bind(args...; kws...) = - BindNode(args...; kws...) |> SQLNode +const Bind = SQLQueryCtor{BindNode}(:Bind) const funsql_bind = Bind -dissect(scr::Symbol, ::typeof(Bind), pats::Vector{Any}) = - dissect(scr, BindNode, pats) - function PrettyPrinting.quoteof(n::BindNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Bind)) + ex = Expr(:call, :Bind) if isempty(n.args) push!(ex.args, Expr(:kw, :args, Expr(:vect))) else append!(ex.args, quoteof(n.args, ctx)) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/define.jl b/src/nodes/define.jl index 6ba32e6a..fab058de 100644 --- a/src/nodes/define.jl +++ b/src/nodes/define.jl @@ -1,17 +1,16 @@ # Defining calculated columns. -mutable struct DefineNode <: TabularNode - over::Union{SQLNode, Nothing} - args::Vector{SQLNode} +struct DefineNode <: TabularNode + args::Vector{SQLQuery} before::Union{Symbol, Bool} after::Union{Symbol, Bool} label_map::OrderedDict{Symbol, Int} - function DefineNode(; over = nothing, args = [], before = nothing, after = nothing, label_map = nothing) + function DefineNode(; args = [], before = nothing, after = nothing, label_map = nothing) if label_map !== nothing - n = new(over, args, something(before, false), something(after, false), label_map) + n = new(args, something(before, false), something(after, false), label_map) else - n = new(over, args, something(before, false), something(after, false), OrderedDict{Symbol, Int}()) + n = new(args, something(before, false), something(after, false), OrderedDict{Symbol, Int}()) populate_label_map!(n) end if (n.before isa Symbol || n.before) && (n.after isa Symbol || n.after) @@ -21,12 +20,12 @@ mutable struct DefineNode <: TabularNode end end -DefineNode(args...; over = nothing, before = nothing, after = nothing) = - DefineNode(over = over, args = SQLNode[args...], before = before, after = after) +DefineNode(args...; before = nothing, after = nothing) = + DefineNode(args = SQLQuery[args...], before = before, after = after) """ - Define(; over; args = [], before = nothing, after = nothing) - Define(args...; over, before = nothing, after = nothing) + Define(; args = [], before = nothing, after = nothing, tail = nothing) + Define(args...; before = nothing, after = nothing, tail = nothing) The `Define` node adds or replaces output columns. @@ -79,24 +78,17 @@ SELECT FROM "person" AS "person_1" ``` """ -Define(args...; kws...) = - DefineNode(args...; kws...) |> SQLNode +const Define = SQLQueryCtor{DefineNode}(:Define) const funsql_define = Define -dissect(scr::Symbol, ::typeof(Define), pats::Vector{Any}) = - dissect(scr, DefineNode, pats) - function PrettyPrinting.quoteof(n::DefineNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Define), quoteof(n.args, ctx)...) + ex = Expr(:call, :Define, quoteof(n.args, ctx)...) if n.before !== false push!(ex.args, Expr(:kw, :before, n.before isa Symbol ? QuoteNode(n.before) : n.before)) end if n.after !== false push!(ex.args, Expr(:kw, :after, n.after isa Symbol ? QuoteNode(n.after) : n.after)) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/from.jl b/src/nodes/from.jl index 9a5b1362..1b8159c6 100644 --- a/src/nodes/from.jl +++ b/src/nodes/from.jl @@ -11,7 +11,7 @@ struct ValuesSource <: AbstractSource end struct FunctionSource <: AbstractSource - node::SQLNode + query::SQLQuery columns::Vector{Symbol} end @@ -27,9 +27,9 @@ _from_source(source::Symbol) = _from_source(::typeof(^)) = IterateSource() -function _from_source(node::AbstractSQLNode; +function _from_source(query::SQLQuery; columns::AbstractVector{<:Union{Symbol, AbstractString}}) - source = FunctionSource(node, + source = FunctionSource(query, !isa(columns, Vector{Symbol}) ? Symbol[Symbol(col) for col in columns] : columns) @@ -50,7 +50,7 @@ function _from_source(source) ValuesSource(columns) end -mutable struct FromNode <: TabularNode +struct FromNode <: TabularNode source::Union{SQLTable, Symbol, IterateSource, ValuesSource, FunctionSource, Nothing} FromNode(; source, kws...) = @@ -66,7 +66,7 @@ FromNode(source; kws...) = From(name::Symbol) From(^) From(df) - From(f::SQLNode; columns::Vector{Symbol}) + From(f::SQLQuery; columns::Vector{Symbol}) From(::Nothing) `From` outputs the content of a database table. @@ -76,7 +76,7 @@ The parameter `source` could be one of: * a `Symbol` value; * a `^` object; * a `DataFrame` or any Tables.jl-compatible dataset; -* A `SQLNode` representing a table-valued function. In this case, `From` +* A `SQLQuery` representing a table-valued function. In this case, `From` also requires a keyword parameter `columns` with a list of output columns produced by the function. * `nothing`. @@ -209,16 +209,15 @@ SELECT CAST("regexp_matches_1"."captures"[1] AS INTEGER) AS "_" FROM regexp_matches('2,3,5,7,11', '(\\d+)', 'g') AS "regexp_matches_1" ("captures") ``` """ -From(args...; kws...) = - FromNode(args...; kws...) |> SQLNode +const From = SQLQueryCtor{FromNode}(:From) const funsql_from = From -dissect(scr::Symbol, ::typeof(From), pats::Vector{Any}) = - dissect(scr, FromNode, pats) +Base.convert(::Type{SQLQuery}, source::SQLTable) = + SQLQuery(FromNode(source)) -Base.convert(::Type{AbstractSQLNode}, source::SQLTable) = - FromNode(source) +terminal(::Type{FromNode}) = + true function PrettyPrinting.quoteof(n::FromNode, ctx::QuoteContext) source = n.source @@ -227,19 +226,19 @@ function PrettyPrinting.quoteof(n::FromNode, ctx::QuoteContext) if tex === nothing tex = quoteof(source, limit = true) end - Expr(:call, nameof(From), tex) + Expr(:call, :From, tex) elseif source isa Symbol - Expr(:call, nameof(From), QuoteNode(source)) + Expr(:call, :From, QuoteNode(source)) elseif source isa IterateSource - Expr(:call, nameof(From), :^) + Expr(:call, :From, :^) elseif source isa ValuesSource - Expr(:call, nameof(From), quoteof(source.columns, ctx)) + Expr(:call, :From, quoteof(source.columns, ctx)) elseif source isa FunctionSource - Expr(:call, nameof(From), - quoteof(source.node, ctx), + Expr(:call, :From, + quoteof(source.query, ctx), Expr(:kw, :columns, Expr(:vect, [QuoteNode(col) for col in source.columns]...))) else - Expr(:call, nameof(From), source) + Expr(:call, :From, source) end end @@ -252,7 +251,7 @@ function label(n::FromNode) elseif source isa ValuesSource :values elseif source isa FunctionSource - label(source.node) + label(source.query) else :_ end diff --git a/src/nodes/function.jl b/src/nodes/function.jl index 2706df22..f450bcc8 100644 --- a/src/nodes/function.jl +++ b/src/nodes/function.jl @@ -1,39 +1,40 @@ # Function and operator calls. -mutable struct FunctionNode <: AbstractSQLNode +struct FunctionNode <: AbstractSQLNode name::Symbol - args::Vector{SQLNode} + args::Vector{SQLQuery} function FunctionNode(; name::Union{Symbol, AbstractString}, - args = SQLNode[]) - n = new(Symbol(name), args) - renameoperators!(n) + args = SQLQuery[]) + n = new(_renameoperators(Symbol(name)), args) checkarity!(n) n end end # Rename Julia operators to SQL equivalents. -function renameoperators!(n::FunctionNode) - if n.name === :(==) - n.name = Symbol("=") - elseif n.name === :(!=) - n.name = Symbol("<>") - elseif n.name === :(||) - n.name = :or - elseif n.name === :(&&) - n.name = :and - elseif n.name === :(!) - n.name = :not +function _renameoperators(name::Symbol) + if name === :(==) + Symbol("=") + elseif name === :(!=) + Symbol("<>") + elseif name === :(||) + :or + elseif name === :(&&) + :and + elseif name === :(!) + :not + else + name end end -FunctionNode(name; args = SQLNode[]) = +FunctionNode(name; args = SQLQuery[]) = FunctionNode(name = name, args = args) FunctionNode(name, args...) = - FunctionNode(name = name, args = SQLNode[args...]) + FunctionNode(name = name, args = SQLQuery[args...]) """ Fun(; name, args = []) @@ -152,20 +153,19 @@ SELECT SUBSTRING("location_1"."zip" FROM 1 FOR 3) AS "_" FROM "location" AS "location_1" ``` """ -Fun(args...; kws...) = - FunctionNode(args...; kws...) |> SQLNode +const Fun = SQLQueryCtor{FunctionNode}(:Fun) const funsql_fun = Fun -dissect(scr::Symbol, ::typeof(Fun), pats::Vector{Any}) = - dissect(scr, FunctionNode, pats) - transliterate(::typeof(Fun), name::Symbol, ctx::TransliterateContext, @nospecialize(args...)) = - Fun(name, args = [transliterate(SQLNode, arg, ctx) for arg in args]) + Fun(name, args = [transliterate(SQLQuery, arg, ctx) for arg in args]) + +terminal(::Type{FunctionNode}) = + true PrettyPrinting.quoteof(n::FunctionNode, ctx::QuoteContext) = Expr(:call, - Expr(:., nameof(Fun), + Expr(:., :Fun, QuoteNode(Base.isidentifier(n.name) ? n.name : string(n.name))), quoteof(n.args, ctx)...) @@ -183,8 +183,7 @@ FunClosure(name::AbstractString) = FunClosure(Symbol(name)) Base.show(io::IO, f::FunClosure) = - print(io, Expr(:., nameof(Fun), - QuoteNode(Base.isidentifier(f.name) ? f.name : string(f.name)))) + print(io, Expr(:., :Fun, QuoteNode(Base.isidentifier(f.name) ? f.name : string(f.name)))) Base.getproperty(::typeof(Fun), name::Symbol) = FunClosure(name) @@ -192,11 +191,11 @@ Base.getproperty(::typeof(Fun), name::Symbol) = Base.getproperty(::typeof(Fun), name::AbstractString) = FunClosure(name) -(f::FunClosure)(args...) = - Fun(f.name, args = SQLNode[args...]) +(ctor::FunClosure)(args...) = + Fun(ctor.name, args = SQLQuery[args...]) -(f::FunClosure)(; args = SQLNode[]) = - Fun(f.name, args = args) +(ctor::FunClosure)(; args = SQLQuery[]) = + Fun(ctor.name, args = args) # Common SQL functions and operators. @@ -246,23 +245,23 @@ const funsql_not_like = FunClosure(:not_like) struct FunStyle <: Base.BroadcastStyle end -Base.BroadcastStyle(::Type{<:AbstractSQLNode}) = +Base.BroadcastStyle(::Type{<:Union{SQLQuery, SQLGetQuery}}) = FunStyle() Base.BroadcastStyle(::FunStyle, ::Base.Broadcast.DefaultArrayStyle{0}) = FunStyle() -Base.broadcastable(n::AbstractSQLNode) = - n +Base.broadcastable(q::Union{SQLQuery, SQLGetQuery}) = + q Base.Broadcast.instantiate(bc::Base.Broadcast.Broadcasted{FunStyle}) = bc Base.copy(bc::Base.Broadcast.Broadcasted{FunStyle}) = - Fun(nameof(bc.f), args = SQLNode[bc.args...]) + Fun(nameof(bc.f), args = SQLQuery[bc.args...]) -Base.convert(::Type{AbstractSQLNode}, bc::Base.Broadcast.Broadcasted{FunStyle}) = - FunctionNode(nameof(bc.f), args = SQLNode[bc.args...]) +Base.convert(::Type{SQLQuery}, bc::Base.Broadcast.Broadcasted{FunStyle}) = + Fun(nameof(bc.f), args = SQLQuery[bc.args...]) # Broadcasting over && and ||. @@ -275,12 +274,12 @@ end if VERSION >= v"1.7" Base.Broadcast.broadcasted(::Base.Broadcast.AndAnd, - arg1::Union{Base.Broadcast.Broadcasted{FunStyle}, AbstractSQLNode}, - arg2::Union{Base.Broadcast.Broadcasted{FunStyle}, AbstractSQLNode}) = + arg1::Union{Base.Broadcast.Broadcasted{FunStyle}, SQLQuery, SQLGetQuery}, + arg2::Union{Base.Broadcast.Broadcasted{FunStyle}, SQLQuery, SQLGetQuery}) = Base.Broadcast.broadcasted(DUMMY_CONNECTIVES.var"&&", arg1, arg2) Base.Broadcast.broadcasted(::Base.Broadcast.OrOr, - arg1::Union{Base.Broadcast.Broadcasted{FunStyle}, AbstractSQLNode}, - arg2::Union{Base.Broadcast.Broadcasted{FunStyle}, AbstractSQLNode}) = + arg1::Union{Base.Broadcast.Broadcasted{FunStyle}, SQLQuery, SQLGetQuery}, + arg2::Union{Base.Broadcast.Broadcasted{FunStyle}, SQLQuery, SQLGetQuery}) = Base.Broadcast.broadcasted(DUMMY_CONNECTIVES.var"||", arg1, arg2) end diff --git a/src/nodes/get.jl b/src/nodes/get.jl index d8dcb45e..5f33146e 100644 --- a/src/nodes/get.jl +++ b/src/nodes/get.jl @@ -1,23 +1,20 @@ # Attribute lookup. -mutable struct GetNode <: AbstractSQLNode - over::Union{SQLNode, Nothing} +struct GetNode <: AbstractSQLNode name::Symbol GetNode(; - over = nothing, name::Union{Symbol, AbstractString}) = - new(over, Symbol(name)) + new(Symbol(name)) end -GetNode(name; over = nothing) = - GetNode(over = over, name = name) +GetNode(name) = + GetNode(name = name) """ - Get(; over, name) - Get(name; over) + Get(; name, tail = nothing) + Get(name; tail = nothing) Get.name Get."name" Get[name] Get["name"] - over.name over."name" over[name] over["name"] name A reference to a column of the input dataset. @@ -61,54 +58,25 @@ FROM "person" AS "person_1" JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id") ``` """ -Get(args...; kws...) = - GetNode(args...; kws...) |> SQLNode +const Get = SQLQueryCtor{GetNode}(:Get) -dissect(scr::Symbol, ::typeof(Get), pats::Vector{Any}) = - dissect(scr, GetNode, pats) +Base.convert(::Type{SQLQuery}, name::Symbol) = + SQLQuery(GetNode(name)) -Base.convert(::Type{AbstractSQLNode}, name::Symbol) = - GetNode(name) +Base.convert(::Type{SQLQuery}, q::SQLGetQuery) = + SQLQuery(getfield(q, :tail), GetNode(getfield(q, :head))) Base.getproperty(::typeof(Get), name::Symbol) = - Get(name) + SQLGetQuery(nothing, Symbol(name)) Base.getproperty(::typeof(Get), name::AbstractString) = - Get(name) + SQLGetQuery(nothing, Symbol(name)) Base.getindex(::typeof(Get), name::Union{Symbol, AbstractString}) = - Get(name) - -Base.getproperty(n::SQLNode, name::Symbol) = - Get(name, over = n) - -Base.getproperty(n::SQLNode, name::AbstractString) = - Get(name, over = n) - -Base.getindex(n::SQLNode, name::Union{Symbol, AbstractString}) = - Get(name, over = n) + SQLGetQuery(nothing, Symbol(name)) function PrettyPrinting.quoteof(n::GetNode, ctx::QuoteContext) - path = Symbol[n.name] - over = n.over - while over !== nothing && (nested = over[]; nested isa GetNode) && !(over in keys(ctx.vars)) - push!(path, nested.name) - over = nested.over - end - if over !== nothing && over in keys(ctx.vars) - ex = ctx.vars[over] - over = nothing - else - ex = nameof(Get) - end - while !isempty(path) - name = pop!(path) - ex = Expr(:., ex, quoteof(name)) - end - if over !== nothing - ex = Expr(:call, :|>, quoteof(over, ctx), ex) - end - ex + Expr(:., :Get, QuoteNode(n.name)) end label(n::GetNode) = diff --git a/src/nodes/group.jl b/src/nodes/group.jl index 2c98aae1..1fe216db 100644 --- a/src/nodes/group.jl +++ b/src/nodes/group.jl @@ -1,15 +1,13 @@ # Grouping. function populate_grouping_sets!(n, sets) - sets′ = Vector{Int}[] for set in sets set′ = Int[] for el in set push!(set′, _grouping_index(el, n)) end - push!(sets′, set′) + push!(n.sets, set′) end - n.sets = sets′ end _grouping_index(el::Integer, n) = @@ -26,24 +24,21 @@ end _grouping_index(el::AbstractString, n) = _grouping_index(Symbol(el), n) -mutable struct GroupNode <: TabularNode - over::Union{SQLNode, Nothing} - by::Vector{SQLNode} +struct GroupNode <: TabularNode + by::Vector{SQLQuery} sets::Union{Vector{Vector{Int}}, GroupingMode, Nothing} name::Union{Symbol, Nothing} label_map::OrderedDict{Symbol, Int} function GroupNode(; - over = nothing, - by = SQLNode[], + by = SQLQuery[], sets = nothing, name::Union{Symbol, AbstractString, Nothing} = nothing, label_map = nothing) need_to_populate_sets = !(sets isa Union{Vector{Vector{Int}}, GroupingMode, Symbol, Nothing}) n = new( - over, by, - need_to_populate_sets ? nothing : sets isa Symbol ? convert(GroupingMode, sets) : sets, + need_to_populate_sets ? Vector{Int}[] : sets isa Symbol ? convert(GroupingMode, sets) : sets, name !== nothing ? Symbol(name) : nothing, label_map !== nothing ? label_map : OrderedDict{Symbol, Int}()) if label_map === nothing @@ -66,12 +61,12 @@ mutable struct GroupNode <: TabularNode end end -GroupNode(by...; over = nothing, sets = nothing, name = nothing) = - GroupNode(over = over, by = SQLNode[by...], sets = sets, name = name) +GroupNode(by...; sets = nothing, name = nothing) = + GroupNode(by = SQLQuery[by...], sets = sets, name = name) """ - Group(; over, by = [], sets = sets, name = nothing) - Group(by...; over, sets = sets, name = nothing) + Group(; by = [], sets = sets, name = nothing, tail = nothing) + Group(by...; sets = sets, name = nothing, tail = nothing) The `Group` node summarizes the input dataset. @@ -169,16 +164,12 @@ SELECT DISTINCT "location_1"."state" FROM "location" AS "location_1" ``` """ -Group(args...; kws...) = - GroupNode(args...; kws...) |> SQLNode +const Group = SQLQueryCtor{GroupNode}(:Group) const funsql_group = Group -dissect(scr::Symbol, ::typeof(Group), pats::Vector{Any}) = - dissect(scr, GroupNode, pats) - function PrettyPrinting.quoteof(n::GroupNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Group), quoteof(n.by, ctx)...) + ex = Expr(:call, :Group, quoteof(n.by, ctx)...) s = n.sets if s !== nothing push!(ex.args, Expr(:kw, :sets, s isa GroupingMode ? QuoteNode(Symbol(s)) : s)) @@ -186,8 +177,5 @@ function PrettyPrinting.quoteof(n::GroupNode, ctx::QuoteContext) if n.name !== nothing push!(ex.args, Expr(:kw, :name, QuoteNode(n.name))) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/highlight.jl b/src/nodes/highlight.jl index bc22224c..be1aa259 100644 --- a/src/nodes/highlight.jl +++ b/src/nodes/highlight.jl @@ -28,24 +28,22 @@ end PrettyPrinting.tile_expr_or_repr(w::EscWrapper, pr = -1) = PrettyPrinting.tile(w) -mutable struct HighlightNode <: AbstractSQLNode - over::Union{SQLNode, Nothing} +struct HighlightNode <: AbstractSQLNode color::Symbol HighlightNode(; - over = nothing, color::Union{Symbol, AbstractString}) = - new(over, Symbol(color)) + new(Symbol(color)) end -HighlightNode(color; over = nothing) = - HighlightNode(over = over, color = color) +HighlightNode(color) = + HighlightNode(color = color) """ - Highlight(; over = nothing; color) - Highlight(color; over = nothing) + Highlight(; color, tail = nothing) + Highlight(color; tail = nothing) -Highlight `over` with the given `color`. +Highlight `tail` with the given `color`. The highlighted node is printed with the selected color when the query containing it is displayed. @@ -63,24 +61,10 @@ let q1 = From(:person), end ``` """ -Highlight(args...; kws...) = - HighlightNode(args...; kws...) |> SQLNode +const Highlight = SQLQueryCtor{HighlightNode}(:Highlight) const funsql_highlight = Highlight -dissect(scr::Symbol, ::typeof(Highlight), pats::Vector{Any}) = - dissect(scr, HighlightNode, pats) - function PrettyPrinting.quoteof(n::HighlightNode, ctx::QuoteContext) - if ctx.limit - ex = Expr(:call, nameof(Highlight), quoteof(n.color)) - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end - return ex - end - push!(ctx.colors, n.color) - ex = quoteof(n.over, ctx) - pop!(ctx.colors) - EscWrapper(ex, n.color, copy(ctx.colors)) + Expr(:call, :Highlight, quoteof(n.color)) end diff --git a/src/nodes/internal.jl b/src/nodes/internal.jl index 8a5f1025..d929460e 100644 --- a/src/nodes/internal.jl +++ b/src/nodes/internal.jl @@ -1,23 +1,18 @@ # Auxiliary nodes. # Preserve context between rendering passes. -mutable struct WithContextNode <: AbstractSQLNode - over::SQLNode +struct WithContextNode <: AbstractSQLNode catalog::SQLCatalog - defs::Vector{SQLNode} + defs::Vector{SQLQuery} - WithContextNode(; over, catalog = SQLCatalog(), defs = SQLNode[]) = - new(over, catalog, defs) + WithContextNode(; catalog = SQLCatalog(), defs = SQLQuery[]) = + new(catalog, defs) end -WithContext(args...; kws...) = - WithContextNode(args...; kws...) |> SQLNode - -dissect(scr::Symbol, ::typeof(WithContext), pats::Vector{Any}) = - dissect(scr, WithContextNode, pats) +const WithContext = SQLQueryCtor{WithContextNode}(:WithContext) function PrettyPrinting.quoteof(n::WithContextNode, ctx::QuoteContext) - ex = Expr(:call, nameof(WithContext), Expr(:kw, :over, quoteof(n.over, ctx))) + ex = Expr(:call, :WithContext) push!(ex.args, Expr(:kw, :catalog, quoteof(n.catalog))) if !isempty(n.defs) push!(ex.args, Expr(:kw, :defs, Expr(:vect, Any[quoteof(def, ctx) for def in n.defs]...))) @@ -26,80 +21,66 @@ function PrettyPrinting.quoteof(n::WithContextNode, ctx::QuoteContext) end # Annotations added by "resolve" pass. -mutable struct ResolvedNode <: AbstractSQLNode - over::SQLNode +struct ResolvedNode <: AbstractSQLNode type::AbstractSQLType - ResolvedNode(; over, type) = - new(over, type) + ResolvedNode(; type) = + new(type) end -ResolvedNode(type; over) = - ResolvedNode(over = over, type = type) - -Resolved(args...; kws...) = - ResolvedNode(args...; kws...) |> SQLNode +ResolvedNode(type) = + ResolvedNode(type = type) -dissect(scr::Symbol, ::typeof(Resolved), pats::Vector{Any}) = - dissect(scr, ResolvedNode, pats) +const Resolved = SQLQueryCtor{ResolvedNode}(:Resolved) function PrettyPrinting.quoteof(n::ResolvedNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Resolved), quoteof(n.type)) - push!(ex.args, Expr(:kw, :over, quoteof(n.over, ctx))) - ex + Expr(:call, :Resolved, quoteof(n.type)) end # Annotations added by "link" pass. -mutable struct LinkedNode <: TabularNode - over::SQLNode - refs::Vector{SQLNode} +struct LinkedNode <: TabularNode + refs::Vector{SQLQuery} n_ext_refs::Int - LinkedNode(; over, refs = SQLNode[], n_ext_refs = length(refs)) = - new(over, refs, n_ext_refs) + LinkedNode(; refs = SQLQuery[], n_ext_refs = length(refs)) = + new(refs, n_ext_refs) end -LinkedNode(refs, n_ext_refs = length(refs); over) = - LinkedNode(over = over, refs = refs, n_ext_refs = n_ext_refs) - -Linked(args...; kws...) = - LinkedNode(args...; kws...) |> SQLNode +LinkedNode(refs, n_ext_refs = length(refs)) = + LinkedNode(refs = refs, n_ext_refs = n_ext_refs) -dissect(scr::Symbol, ::typeof(Linked), pats::Vector{Any}) = - dissect(scr, LinkedNode, pats) +const Linked = SQLQueryCtor{LinkedNode}(:Linked) function PrettyPrinting.quoteof(n::LinkedNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Linked)) + ex = Expr(:call, :Linked) if !isempty(n.refs) push!(ex.args, Expr(:vect, Any[quoteof(ref, ctx) for ref in n.refs]...)) end if n.n_ext_refs != length(n.refs) push!(ex.args, n.n_ext_refs) end - push!(ex.args, Expr(:kw, :over, quoteof(n.over, ctx))) ex end -# Get(over = Get(:a), name = :b) => Nested(over = Get(:b), name = :a) -mutable struct NestedNode <: AbstractSQLNode - over::SQLNode +# @funsql(a.b) == Get(tail = Get(:a), name = :b) => Nested(tail = Get(:b), name = :a) +struct NestedNode <: AbstractSQLNode name::Symbol - NestedNode(; over, name) = - new(over, name) + NestedNode(; name) = + new(name) end -Nested(args...; kws...) = - NestedNode(args...; kws...) |> SQLNode +NestedNode(name) = + NestedNode(; name) -dissect(scr::Symbol, ::typeof(Nested), pats::Vector{Any}) = - dissect(scr, NestedNode, pats) +const Nested = SQLQueryCtor{NestedNode}(:Nested) PrettyPrinting.quoteof(n::NestedNode, ctx::QuoteContext) = - Expr(:call, nameof(Nested), Expr(:kw, :over, quoteof(n.over, ctx)), Expr(:kw, :name, QuoteNode(n.name))) + Expr(:call, :Nested, QuoteNode(n.name)) + # Var() that found the corresponding Bind() -mutable struct BoundVariableNode <: AbstractSQLNode +struct BoundVariableNode <: AbstractSQLNode name::Symbol depth::Int @@ -108,44 +89,54 @@ mutable struct BoundVariableNode <: AbstractSQLNode end BoundVariableNode(name, depth) = - BoundVariableNode(name = name, depth = depth) + BoundVariableNode(; name, depth) + +const BoundVariable = SQLQueryCtor{BoundVariableNode}(:BoundVariable) -BoundVariable(args...; kws...) = - BoundVariableNode(args...; kws...) |> SQLNode +terminal(::Type{BoundVariableNode}) = + true PrettyPrinting.quoteof(n::BoundVariableNode, ctx::QuoteContext) = - Expr(:call, nameof(BoundVariable), QuoteNode(n.name), n.depth) + Expr(:call, :BoundVariable, QuoteNode(n.name), n.depth) + # A generic From node is specialized to FromNothing, FromTable, # FromTableExpression, FromIterate, FromValues, or FromFunction. -mutable struct FromNothingNode <: TabularNode +struct FromNothingNode <: TabularNode end -FromNothing(args...; kws...) = - FromNothingNode(args...; kws...) |> SQLNode +const FromNothing = SQLQueryCtor{FromNothingNode}(:FromNothing) + +terminal(::Type{FromNothingNode}) = + true PrettyPrinting.quoteof(::FromNothingNode, ::QuoteContext) = - Expr(:call, nameof(FromNothing)) + Expr(:call, :FromNothing) -mutable struct FromTableNode <: TabularNode +struct FromTableNode <: TabularNode table::SQLTable FromTableNode(; table) = new(table) end -FromTable(args...; kws...) = - FromTableNode(args...; kws...) |> SQLNode +FromTableNode(table) = + FromTableNode(; table) + +const FromTable = SQLQueryCtor{FromTableNode}(:FromTable) + +terminal(::Type{FromTableNode}) = + true function PrettyPrinting.quoteof(n::FromTableNode, ctx::QuoteContext) tex = get(ctx.vars, n.table, nothing) if tex === nothing tex = quoteof(n.table, limit = true) end - Expr(:call, nameof(FromTable), Expr(:kw, :table, tex)) + Expr(:call, :FromTable, tex) end -mutable struct FromTableExpressionNode <: TabularNode +struct FromTableExpressionNode <: TabularNode name::Symbol depth::Int @@ -154,52 +145,62 @@ mutable struct FromTableExpressionNode <: TabularNode end FromTableExpressionNode(name, depth) = - FromTableExpressionNode(name = name, depth = depth) + FromTableExpressionNode(; name, depth) -FromTableExpression(args...; kws...) = - FromTableExpressionNode(args...; kws...) |> SQLNode +const FromTableExpression = SQLQueryCtor{FromTableExpressionNode}(:FromTableExpression) + +terminal(::Type{FromTableExpressionNode}) = + true PrettyPrinting.quoteof(n::FromTableExpressionNode, ctx::QuoteContext) = - Expr(:call, nameof(FromTableExpression), QuoteNode(n.name), n.depth) + Expr(:call, :FromTableExpression, QuoteNode(n.name), n.depth) -mutable struct FromIterateNode <: TabularNode +struct FromIterateNode <: TabularNode end -FromIterate(args...; kws...) = - FromIterateNode(args...; kws...) |> SQLNode +const FromIterate = SQLQueryCtor{FromIterateNode}(:FromIterate) + +terminal(::Type{FromIterateNode}) = + true PrettyPrinting.quoteof(n::FromIterateNode, ctx::QuoteContext) = - Expr(:call, nameof(FromIterate)) + Expr(:call, :FromIterate) -mutable struct FromValuesNode <: TabularNode +struct FromValuesNode <: TabularNode columns::NamedTuple FromValuesNode(; columns) = new(columns) end -FromValues(args...; kws...) = - FromValuesNode(args...; kws...) |> SQLNode +FromValuesNode(columns) = + FromValuesNode(; columns) + +const FromValues = SQLQueryCtor{FromValuesNode}(:FromValues) + +terminal(::Type{FromValuesNode}) = + true PrettyPrinting.quoteof(n::FromValuesNode, ctx::QuoteContext) = - Expr(:call, nameof(FromValues), Expr(:kw, :columns, quoteof(n.columns, ctx))) + Expr(:call, :FromValues, quoteof(n.columns, ctx)) -mutable struct FromFunctionNode <: TabularNode - over::SQLNode +struct FromFunctionNode <: TabularNode columns::Vector{Symbol} - FromFunctionNode(; over, columns) = - new(over, columns) + FromFunctionNode(; columns) = + new(columns) end -FromFunction(args...; kws...) = - FromFunctionNode(args...; kws...) |> SQLNode +FromFunctionNode(columns) = + FromFunctionNode(; columns) + +const FromFunction = SQLQueryCtor{FromFunctionNode}(:FromFunction) PrettyPrinting.quoteof(n::FromFunctionNode, ctx::QuoteContext) = Expr(:call, - nameof(FromFunction), - Expr(:kw, :over, quoteof(n.over, ctx)), - Expr(:kw, :columns, Expr(:vect, [QuoteNode(col) for col in n.columns]...))) + :FromFunction, + Expr(:vect, [QuoteNode(col) for col in n.columns]...)) + # Annotated Join node. struct JoinRouter @@ -208,30 +209,28 @@ struct JoinRouter end PrettyPrinting.quoteof(r::JoinRouter) = - Expr(:call, nameof(JoinRouter), quoteof(r.label_set), quoteof(r.group)) + Expr(:call, :JoinRouter, quoteof(r.label_set), quoteof(r.group)) -mutable struct RoutedJoinNode <: TabularNode - over::Union{SQLNode, Nothing} - joinee::SQLNode - on::SQLNode +struct RoutedJoinNode <: TabularNode + joinee::SQLQuery + on::SQLQuery router::JoinRouter left::Bool right::Bool lateral::Bool optional::Bool - RoutedJoinNode(; over, joinee, on, router, left, right, lateral = false, optional = false) = - new(over, joinee, on, router, left, right, lateral, optional) + RoutedJoinNode(; joinee, on, router, left, right, lateral = false, optional = false) = + new(joinee, on, router, left, right, lateral, optional) end -RoutedJoinNode(joinee, on; over = nothing, router, left = false, right = false, lateral = false, optional = false) = - RoutedJoinNode(over = over, joinee = joinee, on = on, router, left = left, right = right, lateral = lateral, optional = optional) +RoutedJoinNode(joinee, on; router, left = false, right = false, lateral = false, optional = false) = + RoutedJoinNode(joinee = joinee, on = on, router, left = left, right = right, lateral = lateral, optional = optional) -RoutedJoin(args...; kws...) = - RoutedJoinNode(args...; kws...) |> SQLNode +const RoutedJoin = SQLQueryCtor{RoutedJoinNode}(:RoutedJoin) function PrettyPrinting.quoteof(n::RoutedJoinNode, ctx::QuoteContext) - ex = Expr(:call, nameof(RoutedJoin)) + ex = Expr(:call, :RoutedJoin) if !ctx.limit push!(ex.args, quoteof(n.joinee, ctx)) push!(ex.args, quoteof(n.on, ctx)) @@ -251,33 +250,22 @@ function PrettyPrinting.quoteof(n::RoutedJoinNode, ctx::QuoteContext) else push!(ex.args, :…) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end -# Calculates the keys of a Group node. Also used by Iterate. -mutable struct PaddingNode <: TabularNode - over::Union{SQLNode, Nothing} - PaddingNode(; over = nothing) = - new(over) +# Calculates the keys of a Group node. Also used by Iterate. +struct PaddingNode <: TabularNode end -Padding(args...; kws...) = - PaddingNode(args...; kws...) |> SQLNode +const Padding = SQLQueryCtor{PaddingNode}(:Padding) + +PrettyPrinting.quoteof(n::PaddingNode, ctx::QuoteContext) = + Expr(:call, :Padding) -function PrettyPrinting.quoteof(n::PaddingNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Padding)) - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end - ex -end # Isolated subquery. -mutable struct IsolatedNode <: AbstractSQLNode +struct IsolatedNode <: AbstractSQLNode idx::Int type::RowType @@ -286,13 +274,12 @@ mutable struct IsolatedNode <: AbstractSQLNode end IsolatedNode(idx, type) = - IsolatedNode(idx = idx, type = type) + IsolatedNode(; idx, type) -Isolated(args...; kws...) = - IsolatedNode(args...; kws...) |> SQLNode +const Isolated = SQLQueryCtor{IsolatedNode}(:Isolated) -PrettyPrinting.quoteof(n::IsolatedNode, ctx::QuoteContext) = - Expr(:call, nameof(Isolated), n.idx, quoteof(n.type)) +terminal(::Type{IsolatedNode}) = + true -dissect(scr::Symbol, ::typeof(Isolated), pats::Vector{Any}) = - dissect(scr, IsolatedNode, pats) +PrettyPrinting.quoteof(n::IsolatedNode, ctx::QuoteContext) = + Expr(:call, :Isolated, n.idx, quoteof(n.type)) diff --git a/src/nodes/iterate.jl b/src/nodes/iterate.jl index aec63a76..c60c0d1a 100644 --- a/src/nodes/iterate.jl +++ b/src/nodes/iterate.jl @@ -1,24 +1,23 @@ # Recursive UNION ALL node. mutable struct IterateNode <: TabularNode - over::Union{SQLNode, Nothing} - iterator::SQLNode + iterator::SQLQuery - IterateNode(; over = nothing, iterator) = - new(over, iterator) + IterateNode(; iterator) = + new(iterator) end -IterateNode(iterator; over = nothing) = - IterateNode(over = over, iterator = iterator) +IterateNode(iterator) = + IterateNode(; iterator) """ - Iterate(; over = nothing, iterator) - Iterate(iterator; over = nothing) + Iterate(; iterator, tail = nothing) + Iterate(iterator; tail = nothing) `Iterate` generates the concatenated output of an iterated query. -The `over` query is evaluated first. Then the `iterator` query is repeatedly -applied: to the output of `over`, then to the output of its previous run, and +The `tail` query is evaluated first. Then the `iterator` query is repeatedly +applied: to the output of `tail`, then to the output of its previous run, and so on, until the iterator produces no data. All these outputs are concatenated to generate the output of `Iterate`. @@ -91,23 +90,16 @@ SELECT FROM "__1" AS "__3" ``` """ -Iterate(args...; kws...) = - IterateNode(args...; kws...) |> SQLNode +const Iterate = SQLQueryCtor{IterateNode}(:Iterate) const funsql_iterate = Iterate -dissect(scr::Symbol, ::typeof(Iterate), pats::Vector{Any}) = - dissect(scr, IterateNode, pats) - function PrettyPrinting.quoteof(n::IterateNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Iterate)) + ex = Expr(:call, :Iterate) if !ctx.limit push!(ex.args, quoteof(n.iterator, ctx)) else push!(ex.args, :…) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/join.jl b/src/nodes/join.jl index b5e56536..c1bc2eb4 100644 --- a/src/nodes/join.jl +++ b/src/nodes/join.jl @@ -1,27 +1,26 @@ # Join node. mutable struct JoinNode <: TabularNode - over::Union{SQLNode, Nothing} - joinee::SQLNode - on::SQLNode + joinee::SQLQuery + on::SQLQuery left::Bool right::Bool optional::Bool - JoinNode(; over = nothing, joinee, on, left = false, right = false, optional = false) = - new(over, joinee, on, left, right, optional) + JoinNode(; joinee, on, left = false, right = false, optional = false) = + new(joinee, on, left, right, optional) end -JoinNode(joinee; over = nothing, on, left = false, right = false, optional = false) = - JoinNode(over = over, joinee = joinee, on = on, left = left, right = right, optional = optional) +JoinNode(joinee; on, left = false, right = false, optional = false) = + JoinNode(; joinee, on, left, right, optional) -JoinNode(joinee, on; over = nothing, left = false, right = false, optional = false) = - JoinNode(over = over, joinee = joinee, on = on, left = left, right = right, optional = optional) +JoinNode(joinee, on; left = false, right = false, optional = false) = + JoinNode(; joinee, on, left, right, optional) """ - Join(; over = nothing, joinee, on, left = false, right = false, optional = false) - Join(joinee; over = nothing, on, left = false, right = false, optional = false) - Join(joinee, on; over = nothing, left = false, right = false, optional = false) + Join(; joinee, on, left = false, right = false, optional = false) + Join(joinee; on, left = false, right = false, optional = false) + Join(joinee, on; left = false, right = false, optional = false) `Join` correlates two input datasets. @@ -68,9 +67,8 @@ SELECT FROM "person" AS "person_1" JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id") ``` -""" -Join(args...; kws...) = - JoinNode(args...; kws...) |> SQLNode + """ +const Join = SQLQueryCtor{JoinNode}(:Join) """ An alias for `Join(...; ..., left = true)`. @@ -90,11 +88,8 @@ const funsql_left_join = LeftJoin const funsql_cross_join = CrossJoin -dissect(scr::Symbol, ::typeof(Join), pats::Vector{Any}) = - dissect(scr, JoinNode, pats) - function PrettyPrinting.quoteof(n::JoinNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Join)) + ex = Expr(:call, :Join) if !ctx.limit push!(ex.args, quoteof(n.joinee, ctx)) push!(ex.args, quoteof(n.on, ctx)) @@ -110,8 +105,5 @@ function PrettyPrinting.quoteof(n::JoinNode, ctx::QuoteContext) else push!(ex.args, :…) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/limit.jl b/src/nodes/limit.jl index 143a728c..f3087646 100644 --- a/src/nodes/limit.jl +++ b/src/nodes/limit.jl @@ -1,28 +1,27 @@ # Truncating. -mutable struct LimitNode <: TabularNode - over::Union{SQLNode, Nothing} +struct LimitNode <: TabularNode offset::Union{Int, Nothing} limit::Union{Int, Nothing} - LimitNode(; over = nothing, offset = nothing, limit = nothing) = - new(over, offset, limit) + LimitNode(; offset = nothing, limit = nothing) = + new(offset, limit) end -LimitNode(limit; over = nothing, offset = nothing) = - LimitNode(over = over, offset = offset, limit = limit) +LimitNode(limit; offset = nothing) = + LimitNode(; offset, limit) -LimitNode(offset, limit; over = nothing) = - LimitNode(over = over, offset = offset, limit = limit) +LimitNode(offset, limit) = + LimitNode(; offset, limit) -LimitNode(range::UnitRange; over = nothing) = - LimitNode(over = over, offset = first(range) - 1, limit = length(range)) +LimitNode(range::UnitRange) = + LimitNode(offset = first(range) - 1, limit = length(range)) """ - Limit(; over = nothing, offset = nothing, limit = nothing) - Limit(limit; over = nothing, offset = nothing) - Limit(offset, limit; over = nothing) - Limit(start:stop; over = nothing) + Limit(; offset = nothing, limit = nothing, tail = nothing) + Limit(limit; offset = nothing, tail = nothing) + Limit(offset, limit; tail = nothing) + Limit(start:stop; tail = nothing) The `Limit` node skips the first `offset` rows and then emits the next `limit` rows. @@ -58,22 +57,15 @@ ORDER BY "person_1"."year_of_birth" FETCH FIRST 1 ROW ONLY ``` """ -Limit(args...; kws...) = - LimitNode(args...; kws...) |> SQLNode +const Limit = SQLQueryCtor{LimitNode}(:Limit) const funsql_limit = Limit -dissect(scr::Symbol, ::typeof(Limit), pats::Vector{Any}) = - dissect(scr, LimitNode, pats) - function PrettyPrinting.quoteof(n::LimitNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Limit)) + ex = Expr(:call, :Limit) if n.offset !== nothing push!(ex.args, n.offset) end push!(ex.args, n.limit) - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/literal.jl b/src/nodes/literal.jl index 12ae01b8..f4ff534e 100644 --- a/src/nodes/literal.jl +++ b/src/nodes/literal.jl @@ -8,7 +8,7 @@ mutable struct LiteralNode <: AbstractSQLNode end LiteralNode(val) = - LiteralNode(val = val) + LiteralNode(; val) """ Lit(; val) @@ -37,17 +37,16 @@ SELECT '2000-01-01' AS "date" ``` """ -Lit(args...; kws...) = - LiteralNode(args...; kws...) |> SQLNode +const Lit = SQLQueryCtor{LiteralNode}(:Lit) -dissect(scr::Symbol, ::typeof(Lit), pats::Vector{Any}) = - dissect(scr, LiteralNode, pats) +Base.convert(::Type{SQLQuery}, val::SQLLiteralType) = + Lit(val) -Base.convert(::Type{AbstractSQLNode}, val::SQLLiteralType) = - LiteralNode(val) +Base.convert(::Type{SQLQuery}, ref::Base.RefValue) = + Lit(ref.x) -Base.convert(::Type{AbstractSQLNode}, ref::Base.RefValue) = - LiteralNode(ref.x) +terminal(::Type{LiteralNode}) = + true PrettyPrinting.quoteof(n::LiteralNode, ctx::QuoteContext) = - Expr(:call, nameof(Lit), n.val) + Expr(:call, :Lit, n.val) diff --git a/src/nodes/order.jl b/src/nodes/order.jl index 37599872..0f8a387c 100644 --- a/src/nodes/order.jl +++ b/src/nodes/order.jl @@ -1,19 +1,18 @@ # Sorting. -mutable struct OrderNode <: TabularNode - over::Union{SQLNode, Nothing} - by::Vector{SQLNode} +struct OrderNode <: TabularNode + by::Vector{SQLQuery} - OrderNode(; over = nothing, by) = - new(over, by) + OrderNode(; by) = + new(by) end -OrderNode(by...; over = nothing) = - OrderNode(over = over, by = SQLNode[by...]) +OrderNode(by...) = + OrderNode(by = SQLQuery[by...]) """ - Order(; over = nothing, by) - Order(by...; over = nothing) + Order(; by, tail = nothing) + Order(by...; tail = nothing) `Order` sorts the input rows `by` the given key. @@ -44,23 +43,16 @@ FROM "person" AS "person_1" ORDER BY "person_1"."year_of_birth" ``` """ -Order(args...; kws...) = - OrderNode(args...; kws...) |> SQLNode +const Order = SQLQueryCtor{OrderNode}(:Order) const funsql_order = Order -dissect(scr::Symbol, ::typeof(Order), pats::Vector{Any}) = - dissect(scr, OrderNode, pats) - function PrettyPrinting.quoteof(n::OrderNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Order)) + ex = Expr(:call, :Order) if isempty(n.by) push!(ex.args, Expr(:kw, :by, Expr(:vect))) else append!(ex.args, quoteof(n.by, ctx)) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/over.jl b/src/nodes/over.jl index 09e66b67..ce01e636 100644 --- a/src/nodes/over.jl +++ b/src/nodes/over.jl @@ -1,22 +1,21 @@ # Over node. -mutable struct OverNode <: TabularNode - over::Union{SQLNode, Nothing} - arg::SQLNode +struct OverNode <: TabularNode + arg::SQLQuery materialized::Union{Bool, Nothing} - OverNode(; over = nothing, arg, materialized = nothing) = - new(over, arg, materialized) + OverNode(; arg, materialized = nothing) = + new(arg, materialized) end -OverNode(arg; over = nothing, materialized = nothing) = - OverNode(over = over, arg = arg, materialized = materialized) +OverNode(arg; materialized = nothing) = + OverNode(arg = arg, materialized = materialized) """ - Over(; over = nothing, arg, materialized = nothing) - Over(arg; over = nothing, materialized = nothing) + Over(; arg, materialized = nothing, tail = nothing) + Over(arg; materialized = nothing, tail = nothing) -`base |> Over(arg)` is an alias for `With(base, over = arg)`. +`base |> Over(arg)` is an alias for `With(base, tail = arg)`. # Examples @@ -51,22 +50,15 @@ WHERE ("person_1"."person_id" IN ( )) ``` """ -Over(args...; kws...) = - OverNode(args...; kws...) |> SQLNode +const Over = SQLQueryCtor{OverNode}(:Over) const funsql_over = Over -dissect(scr::Symbol, ::typeof(Over), pats::Vector{Any}) = - dissect(scr, OverNode, pats) - function PrettyPrinting.quoteof(n::OverNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Over), quoteof(n.arg, ctx)) + ex = Expr(:call, :Over, quoteof(n.arg, ctx)) if n.materialized !== nothing push!(ex.args, Expr(:kw, :materialized, n.materialized)) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/partition.jl b/src/nodes/partition.jl index c0b68b2f..243fb984 100644 --- a/src/nodes/partition.jl +++ b/src/nodes/partition.jl @@ -1,27 +1,25 @@ # Window partition. mutable struct PartitionNode <: TabularNode - over::Union{SQLNode, Nothing} - by::Vector{SQLNode} - order_by::Vector{SQLNode} + by::Vector{SQLQuery} + order_by::Vector{SQLQuery} frame::Union{PartitionFrame, Nothing} name::Union{Symbol, Nothing} PartitionNode(; - over = nothing, - by = SQLNode[], - order_by = SQLNode[], + by = SQLQuery[], + order_by = SQLQuery[], frame = nothing, name::Union{Symbol, AbstractString, Nothing} = nothing) = - new(over, by, order_by, frame, name !== nothing ? Symbol(name) : nothing) + new(by, order_by, frame, name !== nothing ? Symbol(name) : nothing) end -PartitionNode(by...; over = nothing, order_by = SQLNode[], frame = nothing, name = nothing) = - PartitionNode(over = over, by = SQLNode[by...], order_by = order_by, frame = frame, name = name) +PartitionNode(by...; order_by = SQLQuery[], frame = nothing, name = nothing) = + PartitionNode(; by = SQLQuery[by...], order_by, frame, name) """ - Partition(; over, by = [], order_by = [], frame = nothing, name = nothing) - Partition(by...; over, order_by = [], frame = nothing, name = nothing) + Partition(; by = [], order_by = [], frame = nothing, name = nothing, tail = nothing) + Partition(by...; order_by = [], frame = nothing, name = nothing, tail = nothing) The `Partition` node relates adjacent rows. @@ -94,16 +92,12 @@ FROM "person" AS "person_1" GROUP BY "person_1"."year_of_birth" ``` """ -Partition(args...; kws...) = - PartitionNode(args...; kws...) |> SQLNode +const Partition = SQLQueryCtor{PartitionNode}(:Partition) const funsql_partition = Partition -dissect(scr::Symbol, ::typeof(Partition), pats::Vector{Any}) = - dissect(scr, PartitionNode, pats) - function PrettyPrinting.quoteof(n::PartitionNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Partition), quoteof(n.by, ctx)...) + ex = Expr(:call, :Partition, quoteof(n.by, ctx)...) if !isempty(n.order_by) push!(ex.args, Expr(:kw, :order_by, Expr(:vect, quoteof(n.order_by, ctx)...))) end @@ -113,8 +107,5 @@ function PrettyPrinting.quoteof(n::PartitionNode, ctx::QuoteContext) if n.name !== nothing push!(ex.args, Expr(:kw, :name, QuoteNode(n.name))) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/select.jl b/src/nodes/select.jl index dc6f9c16..31a3118c 100644 --- a/src/nodes/select.jl +++ b/src/nodes/select.jl @@ -1,27 +1,26 @@ # Selecting. -mutable struct SelectNode <: TabularNode - over::Union{SQLNode, Nothing} - args::Vector{SQLNode} +struct SelectNode <: TabularNode + args::Vector{SQLQuery} label_map::OrderedDict{Symbol, Int} - function SelectNode(; over = nothing, args, label_map = nothing) + function SelectNode(; args, label_map = nothing) if label_map !== nothing - new(over, args, label_map) + new(args, label_map) else - n = new(over, args, OrderedDict{Symbol, Int}()) + n = new(args, OrderedDict{Symbol, Int}()) populate_label_map!(n) n end end end -SelectNode(args...; over = nothing) = - SelectNode(over = over, args = SQLNode[args...]) +SelectNode(args...) = + SelectNode(args = SQLQuery[args...]) """ - Select(; over; args) - Select(args...; over) + Select(; args, tail = nothing) + Select(args...; tail = nothing) The `Select` node specifies the output columns. @@ -50,23 +49,16 @@ SELECT FROM "person" AS "person_1" ``` """ -Select(args...; kws...) = - SelectNode(args...; kws...) |> SQLNode +const Select = SQLQueryCtor{SelectNode}(:Select) const funsql_select = Select -dissect(scr::Symbol, ::typeof(Select), pats::Vector{Any}) = - dissect(scr, SelectNode, pats) - function PrettyPrinting.quoteof(n::SelectNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Select)) + ex = Expr(:call, :Select) if isempty(n.args) push!(ex.args, Expr(:kw, :args, Expr(:vect))) else append!(ex.args, quoteof(n.args, ctx)) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/sort.jl b/src/nodes/sort.jl index 4e88a067..f4279144 100644 --- a/src/nodes/sort.jl +++ b/src/nodes/sort.jl @@ -1,25 +1,23 @@ # Sort ordering. -mutable struct SortNode <: AbstractSQLNode - over::Union{SQLNode, Nothing} +struct SortNode <: AbstractSQLNode value::ValueOrder nulls::Union{NullsOrder, Nothing} SortNode(; - over = nothing, value, nulls = nothing) = - new(over, value, nulls) + new(value, nulls) end -SortNode(value; over = nothing, nulls = nothing) = - SortNode(over = over, value = value, nulls = nulls) +SortNode(value; nulls = nothing) = + SortNode(; value, nulls) """ - Sort(; over = nothing, value, nulls = nothing) - Sort(value; over = nothing, nulls = nothing) - Asc(; over = nothing, nulls = nothing) - Desc(; over = nothing, nulls = nothing) + Sort(; value, nulls = nothing, tail = nothing) + Sort(value; nulls = nothing, tail = nothing) + Asc(; nulls = nothing, tail = nothing) + Desc(; nulls = nothing, tail = nothing) Sort order indicator. @@ -43,8 +41,7 @@ FROM "person" AS "person_1" ORDER BY "person_1"."year_of_birth" DESC NULLS FIRST ``` """ -Sort(args...; kws...) = - SortNode(args...; kws...) |> SQLNode +const Sort = SQLQueryCtor{SortNode}(:Sort) """ Asc(; over = nothing, nulls = nothing) @@ -68,22 +65,16 @@ const funsql_asc = Asc const funsql_desc = Desc -dissect(scr::Symbol, ::typeof(Sort), pats::Vector{Any}) = - dissect(scr, SortNode, pats) - function PrettyPrinting.quoteof(n::SortNode, ctx::QuoteContext) if n.value == VALUE_ORDER.ASC - ex = Expr(:call, nameof(Asc)) + ex = Expr(:call, :Asc) elseif n.value == VALUE_ORDER.DESC - ex = Expr(:call, nameof(Desc)) + ex = Expr(:call, :Desc) else - ex = Expr(:call, nameof(Sort), QuoteNode(Symbol(n.value))) + ex = Expr(:call, :Sort, QuoteNode(Symbol(n.value))) end if n.nulls !== nothing push!(ex.args, Expr(:kw, :nulls, QuoteNode(Symbol(n.nulls)))) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/variable.jl b/src/nodes/variable.jl index 1f9d0706..dc150886 100644 --- a/src/nodes/variable.jl +++ b/src/nodes/variable.jl @@ -36,11 +36,7 @@ FROM "person" AS "person_1" WHERE ("person_1"."year_of_birth" > :YEAR) ``` """ -Var(args...; kws...) = - VariableNode(args...; kws...) |> SQLNode - -dissect(scr::Symbol, ::typeof(Var), pats::Vector{Any}) = - dissect(scr, VariableNode, pats) +const Var = SQLQueryCtor{VariableNode}(:Var) Base.getproperty(::typeof(Var), name::Symbol) = Var(name) @@ -51,8 +47,11 @@ Base.getproperty(::typeof(Var), name::AbstractString) = Base.getindex(::typeof(Var), name::Union{Symbol, AbstractString}) = Var(name) +terminal(::Type{VariableNode}) = + true + PrettyPrinting.quoteof(n::VariableNode, ctx::QuoteContext) = - Expr(:., nameof(Var), quoteof(n.name)) + Expr(:., :Var, quoteof(n.name)) label(n::VariableNode) = n.name diff --git a/src/nodes/where.jl b/src/nodes/where.jl index bc1c71bb..3c3beb37 100644 --- a/src/nodes/where.jl +++ b/src/nodes/where.jl @@ -1,19 +1,18 @@ # Where node. -mutable struct WhereNode <: TabularNode - over::Union{SQLNode, Nothing} - condition::SQLNode +struct WhereNode <: TabularNode + condition::SQLQuery - WhereNode(; over = nothing, condition) = - new(over, condition) + WhereNode(; condition) = + new(condition) end -WhereNode(condition; over = nothing) = - WhereNode(over = over, condition = condition) +WhereNode(condition) = + WhereNode(; condition) """ - Where(; over = nothing, condition) - Where(condition; over = nothing) + Where(; condition, tail = nothing) + Where(condition; tail = nothing) The `Where` node filters the input rows by the given `condition`. @@ -40,18 +39,9 @@ FROM "person" AS "person_1" WHERE ("person_1"."year_of_birth" > 2000) ``` """ -Where(args...; kws...) = - WhereNode(args...; kws...) |> SQLNode +const Where = SQLQueryCtor{WhereNode}(:Where) const funsql_filter = Where -dissect(scr::Symbol, ::typeof(Where), pats::Vector{Any}) = - dissect(scr, WhereNode, pats) - -function PrettyPrinting.quoteof(n::WhereNode, ctx::QuoteContext) - ex = Expr(:call, nameof(Where), quoteof(n.condition, ctx)) - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end - ex -end +PrettyPrinting.quoteof(n::WhereNode, ctx::QuoteContext) = + Expr(:call, :Where, quoteof(n.condition, ctx)) diff --git a/src/nodes/with.jl b/src/nodes/with.jl index 08c24f4c..e0f9f3e3 100644 --- a/src/nodes/with.jl +++ b/src/nodes/with.jl @@ -1,28 +1,27 @@ # With node. -mutable struct WithNode <: TabularNode - over::Union{SQLNode, Nothing} - args::Vector{SQLNode} +struct WithNode <: TabularNode + args::Vector{SQLQuery} materialized::Union{Bool, Nothing} label_map::OrderedDict{Symbol, Int} - function WithNode(; over = nothing, args, materialized = nothing, label_map = nothing) + function WithNode(; args, materialized = nothing, label_map = nothing) if label_map !== nothing - new(over, args, materialized, label_map) + new(args, materialized, label_map) else - n = new(over, args, materialized, OrderedDict{Symbol, Int}()) + n = new(args, materialized, OrderedDict{Symbol, Int}()) populate_label_map!(n) n end end end -WithNode(args...; over = nothing, materialized = nothing) = - WithNode(over = over, args = SQLNode[args...], materialized = materialized) +WithNode(args...; materialized = nothing) = + WithNode(args = SQLQuery[args...], materialized = materialized) """ - With(; over = nothing, args, materialized = nothing) - With(args...; over = nothing, materialized = nothing) + With(; args, materialized = nothing, tail = nothing) + With(args...; materialized = nothing, tail = nothing) `With` assigns a name to a temporary dataset. The dataset content can be retrieved within the `over` query using the [`From`](@ref) node. @@ -67,16 +66,12 @@ WHERE ("person_1"."person_id" IN ( )) ``` """ -With(args...; kws...) = - WithNode(args...; kws...) |> SQLNode +const With = SQLQueryCtor{WithNode}(:With) const funsql_with = With -dissect(scr::Symbol, ::typeof(With), pats::Vector{Any}) = - dissect(scr, WithNode, pats) - function PrettyPrinting.quoteof(n::WithNode, ctx::QuoteContext) - ex = Expr(:call, nameof(With)) + ex = Expr(:call, :With) if isempty(n.args) push!(ex.args, Expr(:kw, :args, Expr(:vect))) else @@ -85,8 +80,5 @@ function PrettyPrinting.quoteof(n::WithNode, ctx::QuoteContext) if n.materialized !== nothing push!(ex.args, Expr(:kw, :materialized, n.materialized)) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/nodes/with_external.jl b/src/nodes/with_external.jl index 9dca4570..1aaba794 100644 --- a/src/nodes/with_external.jl +++ b/src/nodes/with_external.jl @@ -1,29 +1,28 @@ # A With-like node that creates definitions for SELECT INTO statements. mutable struct WithExternalNode <: TabularNode - over::Union{SQLNode, Nothing} - args::Vector{SQLNode} + args::Vector{SQLQuery} qualifiers::Vector{Symbol} handler label_map::OrderedDict{Symbol, Int} - function WithExternalNode(; over = nothing, args, qualifiers = Symbol[], handler = nothing, label_map = nothing) + function WithExternalNode(; args, qualifiers = Symbol[], handler = nothing, label_map = nothing) if label_map !== nothing - new(over, args, qualifiers, handler, label_map) + new(args, qualifiers, handler, label_map) else qualifiers = !isa(qualifiers, Vector{Symbol}) ? Symbol[Symbol(ql) for ql in qualifiers] : qualifiers - n = new(over, args, qualifiers, handler, OrderedDict{Symbol, Int}()) + n = new(args, qualifiers, handler, OrderedDict{Symbol, Int}()) populate_label_map!(n) n end end end -WithExternalNode(args...; over = nothing, qualifiers = Symbol[], handler = nothing) = - WithExternalNode(over = over, args = SQLNode[args...], qualifiers = qualifiers, handler = handler) +WithExternalNode(args...; qualifiers = Symbol[], handler = nothing) = + WithExternalNode(args = SQLQuery[args...], qualifiers = qualifiers, handler = handler) """ WithExternal(; over = nothing, args, qualifiers = [], handler = nothing) @@ -74,14 +73,10 @@ WHERE ("person_1"."person_id" IN ( )) ``` """ -WithExternal(args...; kws...) = - WithExternalNode(args...; kws...) |> SQLNode - -dissect(scr::Symbol, ::typeof(WithExternal), pats::Vector{Any}) = - dissect(scr, WithExternalNode, pats) +const WithExternal = SQLQueryCtor{WithExternalNode}(:WithExternal) function PrettyPrinting.quoteof(n::WithExternalNode, ctx::QuoteContext) - ex = Expr(:call, nameof(WithExternal)) + ex = Expr(:call, :WithExternal) if isempty(n.args) push!(ex.args, Expr(:kw, :args, Expr(:vect))) else @@ -93,8 +88,5 @@ function PrettyPrinting.quoteof(n::WithExternalNode, ctx::QuoteContext) if n.handler !== nothing push!(ex.args, Expr(:kw, :handler, nameof(n.handler))) end - if n.over !== nothing - ex = Expr(:call, :|>, quoteof(n.over, ctx), ex) - end ex end diff --git a/src/render.jl b/src/render.jl index 254dcc71..96d8d035 100644 --- a/src/render.jl +++ b/src/render.jl @@ -1,20 +1,20 @@ # Rendering SQL. """ - render(node; tables = Dict{Symbol, SQLTable}(), - dialect = :default, - cache = nothing)::SQLString + render(query; tables = Dict{Symbol, SQLTable}(), + dialect = :default, + cache = nothing)::SQLString Create a [`SQLCatalog`](@ref) object and serialize the query node. """ -render(n; tables = Dict{Symbol, SQLTable}(), dialect = :default, cache = nothing) = - render(SQLCatalog(tables = tables, dialect = dialect, cache = cache), n) +render(q; tables = Dict{Symbol, SQLTable}(), dialect = :default, cache = nothing) = + render(SQLCatalog(tables = tables, dialect = dialect, cache = cache), q) -render(catalog::Union{SQLConnection, SQLCatalog}, n) = - render(catalog, convert(SQLNode, n)) +render(catalog::Union{SQLConnection, SQLCatalog}, q) = + render(catalog, convert(SQLQuery, q)) """ - render(catalog::Union{SQLConnection, SQLCatalog}, node::SQLNode)::SQLString + render(catalog::Union{SQLConnection, SQLCatalog}, query::SQLQuery)::SQLString Serialize the query node as a SQL statement. @@ -22,10 +22,10 @@ Parameter `catalog` of [`SQLCatalog`](@ref) type encapsulates available database tables and the target SQL dialect. A [`SQLConnection`](@ref) object is also accepted. -Parameter `node` is a composite [`SQLNode`](@ref) object. +Parameter `query` is a [`SQLQuery`](@ref) object. The function returns a [`SQLString`](@ref) value. The result is also cached -(with the identity of `node` serving as the key) in the catalog cache. +(with the identity of `query` serving as the key) in the catalog cache. # Examples @@ -45,31 +45,30 @@ FROM "person" AS "person_1" WHERE ("person_1"."year_of_birth" >= 1950) ``` """ -function render(catalog::SQLCatalog, n::SQLNode) +function render(catalog::SQLCatalog, q::SQLQuery) cache = catalog.cache if cache !== nothing - sql = get(cache, n, nothing) + sql = get(cache, q, nothing) if sql !== nothing return sql end end - n = WithContext(over = n, catalog = catalog) - n = resolve(n) - @debug "FunSQL.resolve\n" * sprint(pprint, n) _group = Symbol("FunSQL.resolve") - n = link(n) - @debug "FunSQL.link\n" * sprint(pprint, n) _group = Symbol("FunSQL.link") - c = translate(n) - @debug "FunSQL.translate\n" * sprint(pprint, c) _group = Symbol("FunSQL.translate") - sql = serialize(c) + q′ = resolve(WithContext(catalog = catalog, tail = q)) + @debug "FunSQL.resolve\n" * sprint(pprint, q′) _group = Symbol("FunSQL.resolve") + q′′ = link(q′) + @debug "FunSQL.link\n" * sprint(pprint, q′′) _group = Symbol("FunSQL.link") + s = translate(q′′) + @debug "FunSQL.translate\n" * sprint(pprint, s) _group = Symbol("FunSQL.translate") + sql = serialize(s) @debug "FunSQL.serialize\n" * sprint(pprint, sql) _group = Symbol("FunSQL.serialize") if cache !== nothing - cache[n] = sql + cache[q] = sql end sql end -render(conn::SQLConnection, n::SQLNode) = - render(conn.catalog, n) +render(conn::SQLConnection, q::SQLQuery) = + render(conn.catalog, q) """ render(dialect::Union{SQLConnection, SQLCatalog, SQLDialect}, @@ -78,9 +77,7 @@ render(conn::SQLConnection, n::SQLNode) = Serialize the syntax tree of a SQL query. """ function render(dialect::SQLDialect, s::SQLSyntax) - s = WITH_CONTEXT(tail = s, dialect = dialect) - sql = serialize(s) - sql + serialize(WITH_CONTEXT(dialect = dialect, tail = s)) end render(conn::SQLConnection, s::SQLSyntax) = diff --git a/src/resolve.jl b/src/resolve.jl index a7b0c3f9..1eb7085a 100644 --- a/src/resolve.jl +++ b/src/resolve.jl @@ -2,7 +2,8 @@ struct ResolveContext catalog::SQLCatalog - path::Vector{SQLNode} + tail::Union{SQLQuery, Nothing} + path::Vector{SQLQuery} row_type::RowType cte_types::Base.ImmutableDict{Symbol, Tuple{Int, RowType}} var_types::Base.ImmutableDict{Symbol, Tuple{Int, ScalarType}} @@ -11,7 +12,8 @@ struct ResolveContext ResolveContext(catalog) = new(catalog, - SQLNode[], + nothing, + SQLQuery[], EMPTY_ROW, Base.ImmutableDict{Symbol, Tuple{Int, RowType}}(), Base.ImmutableDict{Symbol, Tuple{Int, ScalarType}}(), @@ -20,12 +22,14 @@ struct ResolveContext ResolveContext( ctx::ResolveContext; + tail = ctx.tail, row_type = ctx.row_type, cte_types = ctx.cte_types, var_types = ctx.var_types, knot_type = ctx.knot_type, implicit_knot = ctx.implicit_knot) = new(ctx.catalog, + tail, ctx.path, row_type, cte_types, @@ -37,90 +41,101 @@ end get_path(ctx::ResolveContext) = copy(ctx.path) -function row_type(n::SQLNode) - @dissect(n, Resolved(type = (local type)::RowType)) || throw(IllFormedError()) +function row_type(q::SQLQuery) + @dissect(q, Resolved(type = (local type)::RowType)) || throw(IllFormedError()) type end -function scalar_type(n::SQLNode) - @dissect(n, Resolved(type = (local type)::ScalarType)) || throw(IllFormedError()) +function scalar_type(q::SQLQuery) + @dissect(q, Resolved(type = (local type)::ScalarType)) || throw(IllFormedError()) type end -function type(n::SQLNode) - @dissect(n, Resolved(type = (local t))) || throw(IllFormedError()) +function type(q::SQLQuery) + @dissect(q, Resolved(type = (local t))) || throw(IllFormedError()) t end -function resolve(n::SQLNode) - @dissect(n, WithContext(over = (local n′), catalog = (local catalog))) || throw(IllFormedError()) +function resolve(q::SQLQuery) + @dissect(q, (local q′) |> WithContext(catalog = (local catalog))) || throw(IllFormedError()) ctx = ResolveContext(catalog) - WithContext(over = resolve(n′, ctx), catalog = catalog) + WithContext(tail = resolve(q′, ctx), catalog = catalog) +end + +function resolve(ctx::ResolveContext) + resolve(ctx.tail, ctx) end -function resolve(n::SQLNode, ctx) - push!(ctx.path, n) +function resolve(q::SQLQuery, ctx) + !@dissect(q, Resolved()) || return q + push!(ctx.path, q) try - convert(SQLNode, resolve(n[], ctx)) + convert(SQLQuery, resolve(q.head, ResolveContext(ctx, tail = q.tail))) finally pop!(ctx.path) end end -resolve(ns::Vector{SQLNode}, ctx) = - SQLNode[resolve(n, ctx) for n in ns] +resolve(qs::Vector{SQLQuery}, ctx) = + SQLQuery[resolve(q, ctx) for q in qs] function resolve(::Nothing, ctx) t = ctx.knot_type if t !== nothing && ctx.implicit_knot - n = FromIterate() + q = FromIterate() else - n = FromNothing() + q = FromNothing() t = EMPTY_ROW end - Resolved(t, over = n) + Resolved(t, tail = q) end -resolve(n, ctx, t) = - resolve(n, ResolveContext(ctx, row_type = t)) +resolve(q, ctx, t) = + resolve(q, ResolveContext(ctx, row_type = t)) resolve(n::AbstractSQLNode, ctx) = throw(IllFormedError(path = get_path(ctx))) -function resolve_scalar(n::SQLNode, ctx) - push!(ctx.path, n) - n′ = convert(SQLNode, resolve_scalar(n[], ctx)) - pop!(ctx.path) - n′ +function resolve_scalar(ctx::ResolveContext) + resolve_scalar(ctx.tail, ctx) +end + +function resolve_scalar(q::SQLQuery, ctx) + push!(ctx.path, q) + try + convert(SQLQuery, resolve_scalar(q.head, ResolveContext(ctx, tail = q.tail))) + finally + pop!(ctx.path) + end end -function resolve_scalar(ns::Vector{SQLNode}, ctx) - SQLNode[resolve_scalar(n, ctx) for n in ns] +function resolve_scalar(qs::Vector{SQLQuery}, ctx) + SQLQuery[resolve_scalar(q, ctx) for q in qs] end -resolve_scalar(n, ctx, t) = - resolve_scalar(n, ResolveContext(ctx, row_type = t)) +resolve_scalar(q, ctx, t) = + resolve_scalar(q, ResolveContext(ctx, row_type = t)) function resolve_scalar(n::TabularNode, ctx) - n′ = resolve(n, ResolveContext(ctx, implicit_knot = false)) - Resolved(ScalarType(), over = n′) + q′ = resolve(n, ResolveContext(ctx, implicit_knot = false)) + Resolved(ScalarType(), tail = q′) end -function unnest(node, base, ctx) - while @dissect(node, (local over) |> Get(name = (local name))) - base = Nested(over = base, name = name) - node = over +function unnest(q, base, ctx) + while @dissect(q, (local tail) |> Get(name = (local name))) + base = Nested(tail = base, name = name) + q = tail end - if node !== nothing + if q !== nothing throw(IllFormedError(path = get_path(ctx))) end base end function resolve_scalar(n::AggregateNode, ctx) - if n.over !== nothing - n′ = unnest(n.over, Agg(name = n.name, args = n.args, filter = n.filter), ctx) - return resolve_scalar(n′, ctx) + if ctx.tail !== nothing + q′ = unnest(ctx.tail, Agg(name = n.name, args = n.args, filter = n.filter), ctx) + return resolve_scalar(q′, ctx) end t = ctx.row_type.group if !(t isa RowType) @@ -133,42 +148,42 @@ function resolve_scalar(n::AggregateNode, ctx) if n.filter !== nothing filter′ = resolve_scalar(n.filter, ctx′) end - n′ = Agg(name = n.name, args = args′, filter = filter′) - Resolved(ScalarType(), over = n′) + q′ = Agg(name = n.name, args = args′, filter = filter′) + Resolved(ScalarType(), tail = q′) end function resolve(n::AppendNode, ctx) - over = n.over + tail = ctx.tail args = n.args - if over === nothing && !ctx.implicit_knot + if tail === nothing && !ctx.implicit_knot if !isempty(args) - over = args[1] + tail = args[1] args = args[2:end] else - over = Where(false) + tail = Where(false) end end - over′ = resolve(over, ctx) + tail′ = resolve(tail, ctx) args′ = resolve(args, ResolveContext(ctx, implicit_knot = false)) - n′ = Append(over = over′, args = args′) - t = row_type(over′) + q′ = Append(args = args′, tail = tail′) + t = row_type(tail′) for arg in args′ t = intersect(t, row_type(arg)) end - Resolved(t, over = n′) + Resolved(t, tail = q′) end function resolve(n::AsNode, ctx) - over′ = resolve(n.over, ctx) - t = row_type(over′) - n′ = As(name = n.name, over = over′) - Resolved(RowType(FieldTypeMap(n.name => t)), over = n′) + tail′ = resolve(ctx) + t = row_type(tail′) + q′ = As(name = n.name, tail = tail′) + Resolved(RowType(FieldTypeMap(n.name => t)), tail = q′) end function resolve_scalar(n::AsNode, ctx) - over′ = resolve_scalar(n.over, ctx) - n′ = As(name = n.name, over = over′) - Resolved(type(over′), over = n′) + tail′ = resolve_scalar(ctx) + q′ = As(name = n.name, tail = tail′) + Resolved(type(tail′), tail = q′) end function resolve(n::BindNode, ctx, scalar = false) @@ -181,9 +196,9 @@ function resolve(n::BindNode, ctx, scalar = false) var_types′ = Base.ImmutableDict(var_types′, name => (depth, t)) end ctx′ = ResolveContext(ctx, var_types = var_types′) - over′ = !scalar ? resolve(n.over, ctx′) : resolve_scalar(n.over, ctx′) - n′ = Bind(over = over′, args = args′, label_map = n.label_map) - Resolved(type(over′), over = n′) + tail′ = !scalar ? resolve(ctx′) : resolve_scalar(ctx′) + q′ = Bind(args = args′, label_map = n.label_map, tail = tail′) + Resolved(type(tail′), tail = q′) end resolve_scalar(n::BindNode, ctx) = @@ -198,14 +213,14 @@ function resolve_scalar(n::NestedNode, ctx) REFERENCE_ERROR_TYPE.UNEXPECTED_SCALAR_TYPE throw(ReferenceError(error_type, name = n.name, path = get_path(ctx))) end - over′ = resolve_scalar(n.over, ctx, t) - n′ = NestedNode(over = over′, name = n.name) - Resolved(type(over′), over = n′) + tail′ = resolve_scalar(ctx.tail, ctx, t) + q′ = Nested(name = n.name, tail = tail′) + Resolved(type(tail′), tail = q′) end function resolve(n::DefineNode, ctx) - over′ = resolve(n.over, ctx) - t = row_type(over′) + tail′ = resolve(ctx) + t = row_type(tail′) anchor = n.before isa Symbol ? n.before : n.before && !isempty(t.fields) ? first(first(t.fields)) : @@ -246,8 +261,8 @@ function resolve(n::DefineNode, ctx) end end end - n′ = Define(over = over′, args = args′, label_map = n.label_map) - Resolved(RowType(fields, t.group), over = n′) + q′ = Define(args = args′, label_map = n.label_map, tail = tail′) + Resolved(RowType(fields, t.group), tail = q′) end function RowType(table::SQLTable) @@ -261,13 +276,13 @@ end function resolve(n::FromNode, ctx) source = n.source if source isa SQLTable - n′ = FromTable(table = source) + q′ = FromTable(table = source) t = RowType(source) elseif source isa Symbol v = get(ctx.cte_types, source, nothing) if v !== nothing (depth, t) = v - n′ = FromTableExpression(source, depth) + q′ = FromTableExpression(source, depth) else table = get(ctx.catalog, source, nothing) if table === nothing @@ -277,7 +292,7 @@ function resolve(n::FromNode, ctx) name = source, path = get_path(ctx))) end - n′ = FromTable(table = table) + q′ = FromTable(table = table) t = RowType(table) end elseif source isa IterateSource @@ -288,40 +303,40 @@ function resolve(n::FromNode, ctx) REFERENCE_ERROR_TYPE.INVALID_SELF_REFERENCE, path = get_path(ctx))) end - n′ = FromIterate() + q′ = FromIterate() elseif source isa ValuesSource - n′ = FromValues(columns = source.columns) + q′ = FromValues(columns = source.columns) fields = FieldTypeMap() for f in keys(source.columns) fields[f] = ScalarType() end t = RowType(fields) elseif source isa FunctionSource - n′ = FromFunction(over = resolve_scalar(source.node, ctx), columns = source.columns) + q′ = FromFunction(columns = source.columns, tail = resolve_scalar(source.query, ctx)) fields = FieldTypeMap() for f in source.columns fields[f] = ScalarType() end t = RowType(fields) elseif source === nothing - n′ = FromNothing() + q′ = FromNothing() t = RowType() else error() end - Resolved(t, over = n′) + Resolved(t, tail = q′) end function resolve_scalar(n::FunctionNode, ctx) args′ = resolve_scalar(n.args, ctx) - n′ = Fun(name = n.name, args = args′) - Resolved(ScalarType(), over = n′) + q′ = Fun(name = n.name, args = args′) + Resolved(ScalarType(), tail = q′) end function resolve_scalar(n::GetNode, ctx) - if n.over !== nothing - n′ = unnest(n.over, Get(name = n.name), ctx) - return resolve_scalar(n′, ctx) + if ctx.tail !== nothing + q′ = unnest(ctx.tail, Get(n.name), ctx) + return resolve_scalar(q′, ctx) end t = get(ctx.row_type.fields, n.name, EmptyType()) if !(t isa ScalarType) @@ -331,12 +346,12 @@ function resolve_scalar(n::GetNode, ctx) REFERENCE_ERROR_TYPE.UNEXPECTED_ROW_TYPE throw(ReferenceError(error_type, name = n.name, path = get_path(ctx))) end - Resolved(t, over = n) + Resolved(t, tail = convert(SQLQuery, n)) end function resolve(n::GroupNode, ctx) - over′ = resolve(n.over, ctx) - t = row_type(over′) + tail′ = resolve(ctx) + t = row_type(tail′) by′ = resolve_scalar(n.by, ctx, t) fields = FieldTypeMap() for (name, i) in n.label_map @@ -347,19 +362,19 @@ function resolve(n::GroupNode, ctx) fields[n.name] = RowType(FieldTypeMap(), group) group = EmptyType() end - n′ = Group(over = over′, by = by′, sets = n.sets, label_map = n.label_map) - Resolved(RowType(fields, group), over = n′) + q′ = Group(by = by′, sets = n.sets, label_map = n.label_map, tail = tail′) + Resolved(RowType(fields, group), tail = q′) end -resolve(n::HighlightNode, ctx) = - resolve(n.over, ctx) +resolve(::HighlightNode, ctx) = + resolve(ctx) -resolve_scalar(n::HighlightNode, ctx) = - resolve_scalar(n.over, ctx) +resolve_scalar(::HighlightNode, ctx) = + resolve_scalar(ctx) function resolve(n::IterateNode, ctx) - over′ = resolve(n.over, ResolveContext(ctx, knot_type = nothing, implicit_knot = false)) - t = row_type(over′) + tail′ = resolve(ResolveContext(ctx, knot_type = nothing, implicit_knot = false)) + t = row_type(tail′) iterator′ = resolve(n.iterator, ResolveContext(ctx, knot_type = t, implicit_knot = true)) iterator_t = row_type(iterator′) while !issubset(t, iterator_t) @@ -367,13 +382,13 @@ function resolve(n::IterateNode, ctx) iterator′ = resolve(n.iterator, ResolveContext(ctx, knot_type = t, implicit_knot = true)) iterator_t = row_type(iterator′) end - n′ = IterateNode(over = over′, iterator = iterator′) - Resolved(t, over = n′) + q′ = Iterate(iterator = iterator′, tail = tail′) + Resolved(t, tail = q′) end function resolve(n::JoinNode, ctx) - over′ = resolve(n.over, ctx) - lt = row_type(over′) + tail′ = resolve(ctx) + lt = row_type(tail′) joinee′ = resolve(n.joinee, ResolveContext(ctx, row_type = lt, implicit_knot = false)) rt = row_type(joinee′) fields = FieldTypeMap() @@ -388,41 +403,41 @@ function resolve(n::JoinNode, ctx) group = rt.group isa EmptyType ? lt.group : rt.group t = RowType(fields, group) on′ = resolve_scalar(n.on, ctx, t) - n′ = Join(over = over′, joinee = joinee′, on = on′, left = n.left, right = n.right, optional = n.optional) - Resolved(t, over = n′) + q′ = Join(joinee = joinee′, on = on′, left = n.left, right = n.right, optional = n.optional, tail = tail′) + Resolved(t, tail = q′) end function resolve(n::LimitNode, ctx) - over′ = resolve(n.over, ctx) + tail′ = resolve(ctx) if n.offset === nothing && n.limit === nothing - return over′ + return tail′ end - t = row_type(over′) - n′ = Limit(over = over′, offset = n.offset, limit = n.limit) - Resolved(t, over = n′) + t = row_type(tail′) + q′ = Limit(offset = n.offset, limit = n.limit, tail = tail′) + Resolved(t, tail = q′) end function resolve_scalar(n::LiteralNode, ctx) - Resolved(ScalarType(), over = n) + Resolved(ScalarType(), tail = convert(SQLQuery, n)) end function resolve(n::OrderNode, ctx) - over′ = resolve(n.over, ctx) + tail′ = resolve(ctx) if isempty(n.by) - return over′ + return tail′ end - t = row_type(over′) + t = row_type(tail′) by′ = resolve_scalar(n.by, ctx, t) - n′ = Order(over = over′, by = by′) - Resolved(t, over = n′) + q′ = Order(by = by′, tail = tail′) + Resolved(t, tail = q′) end resolve(n::OverNode, ctx) = - resolve(With(over = n.arg, args = n.over !== nothing ? SQLNode[n.over] : SQLNode[]), ctx) + resolve(With(tail = n.arg, args = ctx.tail !== nothing ? SQLQuery[ctx.tail] : SQLQuery[]), ctx) function resolve(n::PartitionNode, ctx) - over′ = resolve(n.over, ctx) - t = row_type(over′) + tail′ = resolve(ctx) + t = row_type(tail′) ctx′ = ResolveContext(ctx, row_type = t) by′ = resolve_scalar(n.by, ctx′) order_by′ = resolve_scalar(n.order_by, ctx′) @@ -439,48 +454,45 @@ function resolve(n::PartitionNode, ctx) end fields[n.name] = RowType(FieldTypeMap(), t) end - n′ = Partition(over = over′, by = by′, order_by = order_by′, frame = n.frame, name = n.name) - Resolved(RowType(fields, group), over = n′) + q′ = Partition(by = by′, order_by = order_by′, frame = n.frame, name = n.name, tail = tail′) + Resolved(RowType(fields, group), tail = q′) end -resolve(n::ResolvedNode, ctx) = - n - function resolve(n::SelectNode, ctx) - over′ = resolve(n.over, ctx) - t = row_type(over′) + tail′ = resolve(ctx) + t = row_type(tail′) args′ = resolve_scalar(n.args, ctx, t) fields = FieldTypeMap() for (name, i) in n.label_map fields[name] = type(args′[i]) end - n′ = Select(over = over′, args = args′, label_map = n.label_map) - Resolved(RowType(fields), over = n′) + q′ = Select(args = args′, label_map = n.label_map, tail = tail′) + Resolved(RowType(fields), tail = q′) end function resolve_scalar(n::SortNode, ctx) - over′ = resolve_scalar(n.over, ctx) - n′ = Sort(over = over′, value = n.value, nulls = n.nulls) - Resolved(type(over′), over = n′) + tail′ = resolve_scalar(ctx) + q′ = Sort(value = n.value, nulls = n.nulls, tail = tail′) + Resolved(type(tail′), tail = q′) end function resolve_scalar(n::VariableNode, ctx) v = get(ctx.var_types, n.name, nothing) if v !== nothing depth, t = v - n′ = BoundVariable(n.name, depth) - Resolved(t, over = n′) + q′ = BoundVariable(n.name, depth) + Resolved(t, tail = q′) else - Resolved(ScalarType(), over = n) + Resolved(ScalarType(), tail = convert(SQLQuery, n)) end end function resolve(n::WhereNode, ctx) - over′ = resolve(n.over, ctx) - t = row_type(over′) + tail′ = resolve(ctx) + t = row_type(tail′) condition′ = resolve_scalar(n.condition, ctx, t) - n′ = Where(over = over′, condition = condition′) - Resolved(t, over = n′) + q′ = Where(condition = condition′, tail = tail′) + Resolved(t, tail = q′) end function resolve(n::Union{WithNode, WithExternalNode}, ctx) @@ -503,11 +515,11 @@ function resolve(n::Union{WithNode, WithExternalNode}, ctx) cte_types′ = Base.ImmutableDict(cte_types′, name => (depth, cte_t)) end ctx′ = ResolveContext(ctx, cte_types = cte_types′) - over′ = resolve(n.over, ctx′) + tail′ = resolve(ctx′) if n isa WithNode - n′ = With(over = over′, args = args′, materialized = n.materialized, label_map = n.label_map) + q′ = With(args = args′, materialized = n.materialized, label_map = n.label_map, tail = tail′) else - n′ = WithExternal(over = over′, args = args′, qualifiers = n.qualifiers, handler = n.handler, label_map = n.label_map) + q′ = WithExternal(args = args′, qualifiers = n.qualifiers, handler = n.handler, label_map = n.label_map, tail = tail′) end - Resolved(row_type(over′), over = n′) + Resolved(row_type(tail′), tail = q′) end diff --git a/src/translate.jl b/src/translate.jl index 14c3c61a..84b3bd79 100644 --- a/src/translate.jl +++ b/src/translate.jl @@ -6,9 +6,9 @@ struct Assemblage name::Symbol # Base name for the alias. syntax::Union{SQLSyntax, Nothing} # A SQL subquery (possibly without SELECT clause). cols::OrderedDict{Symbol, SQLSyntax} # SELECT arguments, if necessary. - repl::Dict{SQLNode, Symbol} # Maps a reference node to a column alias. + repl::Dict{SQLQuery, Symbol} # Maps a reference node to a column alias. - Assemblage(name, syntax; cols = OrderedDict{Symbol, SQLSyntax}(), repl = Dict{SQLNode, Symbol}()) = + Assemblage(name, syntax; cols = OrderedDict{Symbol, SQLSyntax}(), repl = Dict{SQLQuery, Symbol}()) = new(name, syntax, cols, repl) end @@ -52,7 +52,7 @@ function complete_aligned(a::Assemblage, ctx) syntax = FROM(AS(tail = a.syntax, name = alias)) end subs = make_subs(a, alias) - repl = Dict{SQLNode, Symbol}() + repl = Dict{SQLQuery, Symbol}() cols = OrderedDict{Symbol, SQLSyntax}() for ref in ctx.refs name = repl[ref] = a.repl[ref] @@ -63,8 +63,8 @@ function complete_aligned(a::Assemblage, ctx) end # Build node->syntax map assuming that the assemblage will be extended. -function make_subs(a::Assemblage, ::Nothing)::Dict{SQLNode, SQLSyntax} - subs = Dict{SQLNode, SQLSyntax}() +function make_subs(a::Assemblage, ::Nothing)::Dict{SQLQuery, SQLSyntax} + subs = Dict{SQLQuery, SQLSyntax}() for (ref, name) in a.repl subs[ref] = a.cols[name] end @@ -73,7 +73,7 @@ end # Build node->syntax map assuming that the assemblage will be completed. function make_subs(a::Assemblage, alias::Symbol) - subs = Dict{SQLNode, SQLSyntax}() + subs = Dict{SQLQuery, SQLSyntax}() cache = Dict{Symbol, SQLSyntax}() for (ref, name) in a.repl subs[ref] = get(cache, name) do @@ -84,8 +84,8 @@ function make_subs(a::Assemblage, alias::Symbol) end # Build a node->alias map and implicit SELECT columns for a UNION query. -function make_repl_cols(refs::Vector{SQLNode})::Tuple{Dict{SQLNode, Symbol}, OrderedDict{Symbol, SQLSyntax}} - repl = Dict{SQLNode, Symbol}() +function make_repl_cols(refs::Vector{SQLQuery})::Tuple{Dict{SQLQuery, Symbol}, OrderedDict{Symbol, SQLSyntax}} + repl = Dict{SQLQuery, Symbol}() cols = OrderedDict{Symbol, SQLSyntax}() dups = Dict{Symbol, Int}() for ref in refs @@ -107,8 +107,8 @@ function make_repl_cols(refs::Vector{SQLNode})::Tuple{Dict{SQLNode, Symbol}, Ord end # Build a node->alias map and SELECT columns. -function make_repl_cols(trns::Vector{Pair{SQLNode, SQLSyntax}})::Tuple{Dict{SQLNode, Symbol}, OrderedDict{Symbol, SQLSyntax}} - repl = Dict{SQLNode, Symbol}() +function make_repl_cols(trns::Vector{Pair{SQLQuery, SQLSyntax}})::Tuple{Dict{SQLQuery, Symbol}, OrderedDict{Symbol, SQLSyntax}} + repl = Dict{SQLQuery, Symbol}() cols = OrderedDict{Symbol, SQLSyntax}() dups = Dict{Symbol, Int}() renames = Dict{Tuple{Symbol, SQLSyntax}, Symbol}() @@ -162,30 +162,33 @@ end struct TranslateContext catalog::SQLCatalog - defs::Vector{SQLNode} + tail::Union{SQLQuery, Nothing} + defs::Vector{SQLQuery} aliases::Dict{Symbol, Int} recursive::Ref{Bool} ctes::Vector{CTEAssemblage} cte_map::Base.ImmutableDict{Tuple{Symbol, Int}, Int} knot::Int - refs::Vector{SQLNode} + refs::Vector{SQLQuery} vars::Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax} - subs::Dict{SQLNode, SQLSyntax} + subs::Dict{SQLQuery, SQLSyntax} TranslateContext(; catalog, defs) = new(catalog, + nothing, defs, Dict{Symbol, Int}(), Ref(false), CTEAssemblage[], Base.ImmutableDict{Tuple{Symbol, Int}, Int}(), 0, - SQLNode[], + SQLQuery[], Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax}(), Dict{Int, SQLSyntax}()) - function TranslateContext(ctx::TranslateContext; cte_map = ctx.cte_map, knot = ctx.knot, refs = ctx.refs, vars = ctx.vars, subs = ctx.subs) + function TranslateContext(ctx::TranslateContext; tail = ctx.tail, cte_map = ctx.cte_map, knot = ctx.knot, refs = ctx.refs, vars = ctx.vars, subs = ctx.subs) new(ctx.catalog, + tail, ctx.defs, ctx.aliases, ctx.recursive, @@ -207,11 +210,11 @@ function allocate_alias(ctx::TranslateContext, alias::Symbol) Symbol(alias, '_', n) end -function translate(n::SQLNode) - @dissect(n, WithContext(over = Linked(over = (local n′), refs = (local refs)), catalog = (local catalog), defs = (local defs))) || throw(IllFormedError()) +function translate(q::SQLQuery) + @dissect(q, (local q′) |> Linked(refs = (local refs)) |> WithContext(catalog = (local catalog), defs = (local defs))) || throw(IllFormedError()) ctx = TranslateContext(catalog = catalog, defs = defs) ctx′ = TranslateContext(ctx, refs = refs) - base = assemble(n′, ctx′) + base = assemble(q′, ctx′) columns = nothing if !isempty(refs) columns = [SQLColumn(base.repl[ref]) for ref in refs] @@ -238,24 +241,28 @@ function translate(n::SQLNode) WITH_CONTEXT(tail = c, dialect = ctx.catalog.dialect, columns = columns) end -function translate(n::SQLNode, ctx) - c = get(ctx.subs, n, nothing) +function translate(q::SQLQuery, ctx) + c = get(ctx.subs, q, nothing) if c === nothing - c = convert(SQLSyntax, translate(n[], ctx)) + c = convert(SQLSyntax, translate(q.head, TranslateContext(ctx, tail = q.tail))) end c end -function translate(ns::Vector{SQLNode}, ctx) - SQLSyntax[translate(n, ctx) for n in ns] +function translate(ctx::TranslateContext) + translate(ctx.tail, ctx) +end + +function translate(qs::Vector{SQLQuery}, ctx) + SQLSyntax[translate(q, ctx) for q in qs] end translate(::Nothing, ctx) = nothing -function translate(n, ctx::TranslateContext, subs::Dict{SQLNode, SQLSyntax}) +function translate(q, ctx::TranslateContext, subs::Dict{SQLQuery, SQLSyntax}) ctx′ = TranslateContext(ctx, subs = subs) - translate(n, ctx′) + translate(q, ctx′) end function translate(n::AggregateNode, ctx) @@ -265,7 +272,7 @@ function translate(n::AggregateNode, ctx) end function translate(n::AsNode, ctx) - translate(n.over, ctx) + translate(ctx) end function translate(n::BindNode, ctx) @@ -275,7 +282,7 @@ function translate(n::BindNode, ctx) vars′ = Base.ImmutableDict(vars′, (name, depth) => translate(n.args[i], ctx)) end ctx′ = TranslateContext(ctx, vars = vars′) - translate(n.over, ctx′) + translate(ctx′) end function translate(n::BoundVariableNode, ctx) @@ -338,33 +345,36 @@ function translate(n::LiteralNode, ctx) end function translate(n::ResolvedNode, ctx) - translate(n.over, ctx) + translate(ctx) end translate(n::SortNode, ctx) = - SORT(tail = translate(n.over, ctx), value = n.value, nulls = n.nulls) + SORT(value = n.value, nulls = n.nulls, tail = translate(ctx)) function translate(n::VariableNode, ctx) VAR(n.name) end -function assemble(n::SQLNode, ctx) - assemble(n[], ctx) +function assemble(q::SQLQuery, ctx) + assemble(q.head, TranslateContext(ctx, tail = q.tail)) end +assemble(ctx::TranslateContext) = + assemble(ctx.tail, ctx) + function assemble(::Nothing, ctx) @assert isempty(ctx.refs) Assemblage(:_, nothing) end function assemble(n::AppendNode, ctx) - base = assemble(n.over, ctx) - branches = [n.over => base] + base = assemble(ctx) + branches = [ctx.tail => base] for arg in n.args push!(branches, arg => assemble(arg, ctx)) end - dups = Dict{SQLNode, SQLNode}() - seen = Dict{Symbol, SQLNode}() + dups = Dict{SQLQuery, SQLQuery}() + seen = Dict{Symbol, SQLQuery}() for ref in ctx.refs name = base.repl[ref] if name in keys(seen) @@ -377,7 +387,7 @@ function assemble(n::AppendNode, ctx) seen[name] = ref end end - urefs = SQLNode[] + urefs = SQLQuery[] for ref in ctx.refs if !(ref in keys(dups)) push!(urefs, ref) @@ -402,7 +412,7 @@ function assemble(n::AppendNode, ctx) tail = a.syntax else alias = allocate_alias(ctx, a) - tail = FROM(AS(tail = complete(a), name = alias)) + tail = FROM(AS(name = alias, tail = complete(a))) end subs = make_subs(a, alias) cols = OrderedDict{Symbol, SQLSyntax}() @@ -410,27 +420,27 @@ function assemble(n::AppendNode, ctx) name = repl[ref] cols[name] = subs[ref] end - s = SELECT(tail = tail, args = complete(cols)) + s = SELECT(args = complete(cols), tail = tail) push!(ss, s) end - s = UNION(tail = ss[1], all = true, args = ss[2:end]) + s = UNION(all = true, args = ss[2:end], tail = ss[1]) Assemblage(a_name, s, repl = repl, cols = dummy_cols) end function assemble(n::AsNode, ctx) - refs′ = SQLNode[] + refs′ = SQLQuery[] for ref in ctx.refs - if @dissect(ref, (local over) |> Nested()) - push!(refs′, over) + if @dissect(ref, (local tail) |> Nested()) + push!(refs′, tail) else push!(refs′, ref) end end - base = assemble(n.over, TranslateContext(ctx, refs = refs′)) - repl′ = Dict{SQLNode, Symbol}() + base = assemble(TranslateContext(ctx, refs = refs′)) + repl′ = Dict{SQLQuery, Symbol}() for ref in ctx.refs - if @dissect(ref, (local over) |> Nested()) - repl′[ref] = base.repl[over] + if @dissect(ref, (local tail) |> Nested()) + repl′[ref] = base.repl[tail] else repl′[ref] = base.repl[ref] end @@ -445,25 +455,25 @@ function assemble(n::BindNode, ctx) vars′ = Base.ImmutableDict(vars′, (name, depth) => translate(n.args[i], ctx)) end ctx′ = TranslateContext(ctx, vars = vars′) - assemble(n.over, ctx′) + assemble(ctx′) end function assemble(n::DefineNode, ctx) - base = assemble(n.over, ctx) + base = assemble(ctx) if !@dissect(base.syntax, SELECT() || UNION()) base_alias = nothing s = base.syntax else base_alias = allocate_alias(ctx, base) - s = FROM(AS(tail = complete(base), name = base_alias)) + s = FROM(AS(name = base_alias, tail = complete(base))) end subs = make_subs(base, base_alias) tr_cache = Dict{Symbol, SQLSyntax}() for (f, i) in n.label_map tr_cache[f] = translate(n.args[i], ctx, subs) end - repl = Dict{SQLNode, Symbol}() - trns = Pair{SQLNode, SQLSyntax}[] + repl = Dict{SQLQuery, Symbol}() + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) && name in keys(tr_cache) push!(trns, ref => tr_cache[name]) @@ -484,21 +494,22 @@ function assemble(n::FromFunctionNode, ctx) push!(seen, name) end end - over = translate(n.over, ctx) - alias = allocate_alias(ctx, label(n.over)) - s = FROM(AS(tail = over, name = alias, columns = n.columns)) + tail = translate(ctx) + lbl = label(ctx.tail) + alias = allocate_alias(ctx, lbl) + s = FROM(AS(name = alias, columns = n.columns, tail = tail)) cols = OrderedDict{Symbol, SQLSyntax}() for col in n.columns col in seen || continue - cols[col] = ID(tail = alias, name = col) + cols[col] = ID(name = col, tail = alias) end - repl = Dict{SQLNode, Symbol}() + repl = Dict{SQLQuery, Symbol}() for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) repl[ref] = name end end - Assemblage(label(n.over), s, cols = cols, repl = repl) + Assemblage(lbl, s, cols = cols, repl = repl) end function assemble(n::FromIterateNode, ctx) @@ -506,9 +517,9 @@ function assemble(n::FromIterateNode, ctx) name = cte_a.a.name alias = allocate_alias(ctx, name) tbl = convert(SQLSyntax, (cte_a.qualifiers, cte_a.name)) - s = FROM(AS(tail = tbl, name = alias)) + s = FROM(AS(name = alias, tail = tbl)) subs = make_subs(cte_a.a, alias) - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end @@ -520,10 +531,10 @@ assemble(::FromNothingNode, ctx) = assemble(nothing, ctx) function unwrap_repl(a::Assemblage) - repl′ = Dict{SQLNode, Symbol}() + repl′ = Dict{SQLQuery, Symbol}() for (ref, name) in a.repl - @dissect(ref, (local over) |> Nested()) || error() - repl′[over] = name + @dissect(ref, (local tail) |> Nested()) || error() + repl′[tail] = name end Assemblage(a.name, a.syntax, cols = a.cols, repl = repl′) end @@ -532,9 +543,9 @@ function assemble(n::FromTableExpressionNode, ctx) cte_a = ctx.ctes[ctx.cte_map[(n.name, n.depth)]] alias = allocate_alias(ctx, n.name) tbl = convert(SQLSyntax, (cte_a.qualifiers, cte_a.name)) - s = FROM(AS(tail = tbl, name = alias)) + s = FROM(AS(name = alias, tail = tbl)) subs = make_subs(unwrap_repl(cte_a.a), alias) - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end @@ -552,13 +563,13 @@ function assemble(n::FromTableNode, ctx) end alias = allocate_alias(ctx, n.table.name) tbl = convert(SQLSyntax, (n.table.qualifiers, n.table.name)) - s = FROM(AS(tail = tbl, name = alias)) + s = FROM(AS(name = alias, tail = tbl)) cols = OrderedDict{Symbol, SQLSyntax}() for (name, col) in n.table.columns name in seen || continue - cols[name] = ID(tail = alias, name = col.name) + cols[name] = ID(name = col.name, tail = alias) end - repl = Dict{SQLNode, Symbol}() + repl = Dict{SQLQuery, Symbol}() for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) repl[ref] = name @@ -599,7 +610,7 @@ function assemble(n::FromValuesNode, ctx) s = FROM(AS(alias, columns = column_aliases, tail = VALUES(rows))) for col in columns col in seen || continue - cols[col] = ID(tail = alias, name = col) + cols[col] = ID(name = col, tail = alias) end else column_prefix = ctx.catalog.dialect.values_column_prefix @@ -609,11 +620,11 @@ function assemble(n::FromValuesNode, ctx) for col in columns col in seen || continue name = Symbol(column_prefix, column_index) - cols[col] = ID(tail = alias, name = name) + cols[col] = ID(name = name, tail = alias) column_index += 1 end end - repl = Dict{SQLNode, Symbol}() + repl = Dict{SQLQuery, Symbol}() for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) repl[ref] = name @@ -627,24 +638,24 @@ function assemble(n::GroupNode, ctx) if isempty(n.by) && !has_aggregates # NOOP: already processed in link() return assemble(nothing, ctx) end - base = assemble(n.over, ctx) + base = assemble(ctx) if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(tail = complete(base), name = base_alias)) + tail = FROM(AS(name = base_alias, tail = complete(base))) end subs = make_subs(base, base_alias) by = SQLSyntax[subs[key] for key in n.by] - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs if @dissect(ref, nothing |> Get(name = (local name))) @assert name in keys(n.label_map) push!(trns, ref => by[n.label_map[name]]) elseif @dissect(ref, nothing |> Agg()) push!(trns, ref => translate(ref, ctx, subs)) - elseif @dissect(ref, (local over = nothing |> Agg()) |> Nested()) - push!(trns, ref => translate(over, ctx, subs)) + elseif @dissect(ref, (local tail = nothing |> Agg()) |> Nested()) + push!(trns, ref => translate(tail, ctx, subs)) end end if !has_aggregates && n.sets === nothing @@ -655,10 +666,10 @@ function assemble(n::GroupNode, ctx) repl, cols = make_repl_cols(trns) @assert !isempty(cols) if has_aggregates || n.sets !== nothing - s = GROUP(tail = tail, by = by, sets = n.sets) + s = GROUP(by = by, sets = n.sets, tail = tail) else args = complete(cols) - s = SELECT(tail = tail, distinct = true, args = args) + s = SELECT(distinct = true, args = args, tail = tail) cols = OrderedDict{Symbol, SQLSyntax}([name => ID(name) for name in keys(cols)]) end return Assemblage(base.name, s, cols = cols, repl = repl) @@ -666,10 +677,10 @@ end function assemble(n::IterateNode, ctx) ctx′ = TranslateContext(ctx, vars = Base.ImmutableDict{Tuple{Symbol, Int}, SQLSyntax}()) - left = assemble(n.over, ctx) - repl = Dict{SQLNode, Symbol}() - dups = Dict{SQLNode, SQLNode}() - seen = Dict{Symbol, SQLNode}() + left = assemble(ctx) + repl = Dict{SQLQuery, Symbol}() + dups = Dict{SQLQuery, SQLQuery}() + seen = Dict{Symbol, SQLQuery}() for ref in ctx.refs !in(ref, keys(repl)) || continue name = left.repl[ref] @@ -687,14 +698,14 @@ function assemble(n::IterateNode, ctx) knot = lastindex(ctx.ctes) ctx = TranslateContext(ctx, knot = knot) right = assemble(n.iterator, ctx) - urefs = SQLNode[] + urefs = SQLQuery[] for ref in ctx.refs !(ref in keys(dups)) || continue dups[ref] = ref push!(urefs, ref) end ss = SQLSyntax[] - for (arg, a) in (n.over => left, n.iterator => right) + for (arg, a) in (ctx.tail => left, n.iterator => right) if @dissect(a.syntax, (local tail) |> SELECT(args = (local args))) && aligned_columns(urefs, repl, args) && !@dissect(tail, ORDER() || LIMIT()) push!(ss, a.syntax) continue @@ -703,7 +714,7 @@ function assemble(n::IterateNode, ctx) tail = a.syntax else alias = allocate_alias(ctx, a) - tail = FROM(AS(tail = complete(a), name = alias)) + tail = FROM(AS(name = alias, tail = complete(a))) end subs = make_subs(a, alias) cols = OrderedDict{Symbol, SQLSyntax}() @@ -711,10 +722,10 @@ function assemble(n::IterateNode, ctx) name = left.repl[ref] cols[name] = subs[ref] end - s = SELECT(tail = tail, args = complete(cols)) + s = SELECT(args = complete(cols), tail = tail) push!(ss, s) end - union_syntax = UNION(tail = ss[1], all = true, args = ss[2:end]) + union_syntax = UNION(all = true, args = ss[2:end], tail = ss[1]) cols = OrderedDict{Symbol, SQLSyntax}() for ref in urefs name = left.repl[ref] @@ -724,9 +735,9 @@ function assemble(n::IterateNode, ctx) ctx.ctes[knot] = CTEAssemblage(union, name = union_alias) ctx.recursive[] = true alias = allocate_alias(ctx, union) - s = FROM(AS(tail = ID(union_alias), name = alias)) + s = FROM(AS(name = alias, tail = ID(union_alias))) subs = make_subs(union, alias) - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end @@ -735,16 +746,16 @@ function assemble(n::IterateNode, ctx) end function assemble(n::LimitNode, ctx) - base = assemble(n.over, ctx) + base = assemble(ctx) if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING() || ORDER()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(tail = complete(base), name = base_alias)) + tail = FROM(AS(name = base_alias, tail = complete(base))) end - s = LIMIT(tail = tail, offset = n.offset, limit = n.limit) + s = LIMIT(offset = n.offset, limit = n.limit, tail = tail) subs = make_subs(base, base_alias) - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end @@ -753,7 +764,7 @@ function assemble(n::LimitNode, ctx) end function assemble(n::LinkedNode, ctx) - a = assemble(n.over, TranslateContext(ctx, refs = n.refs)) + a = assemble(TranslateContext(ctx, refs = n.refs)) n.n_ext_refs < length(n.refs) || return a dups = Set{Symbol}() for (k, ref) in enumerate(n.refs) @@ -761,9 +772,9 @@ function assemble(n::LinkedNode, ctx) if col in dups if k > n.n_ext_refs alias = allocate_alias(ctx, a) - s = FROM(AS(tail = complete(a), name = alias)) + s = FROM(AS(name = alias, tail = complete(a))) subs = make_subs(a, alias) - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] for ref in n.refs push!(trns, ref => subs[ref]) end @@ -778,18 +789,18 @@ function assemble(n::LinkedNode, ctx) end function assemble(n::OrderNode, ctx) - base = assemble(n.over, ctx) + base = assemble(ctx) @assert !isempty(n.by) if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(tail = complete(base), name = base_alias)) + tail = FROM(AS(name = base_alias, tail = complete(base))) end subs = make_subs(base, base_alias) by = translate(n.by, ctx, subs) - s = ORDER(tail = tail, by = by) - trns = Pair{SQLNode, SQLSyntax}[] + s = ORDER(by = by, tail = tail) + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end @@ -798,7 +809,7 @@ function assemble(n::OrderNode, ctx) end function assemble(n::PaddingNode, ctx) - base = assemble(n.over, ctx) + base = assemble(ctx) if isempty(ctx.refs) return base end @@ -807,11 +818,11 @@ function assemble(n::PaddingNode, ctx) s = base.syntax else base_alias = allocate_alias(ctx, base) - s = FROM(AS(tail = complete(base), name = base_alias)) + s = FROM(AS(name = base_alias, tail = complete(base))) end subs = make_subs(base, base_alias) - repl = Dict{SQLNode, Symbol}() - trns = Pair{SQLNode, SQLSyntax}[] + repl = Dict{SQLQuery, Symbol}() + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => translate(ref, ctx, subs)) end @@ -820,28 +831,28 @@ function assemble(n::PaddingNode, ctx) end function assemble(n::PartitionNode, ctx) - base = assemble(n.over, ctx) + base = assemble(ctx) if @dissect(base.syntax, local tail = nothing || FROM() || JOIN() || WHERE() || GROUP() || HAVING()) base_alias = nothing else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(tail = complete(base), name = base_alias)) + tail = FROM(AS(name = base_alias, tail = complete(base))) end - s = WINDOW(tail = tail, args = []) + s = WINDOW(args = [], tail = tail) subs = make_subs(base, base_alias) ctx′ = TranslateContext(ctx, subs = subs) by = translate(n.by, ctx′) order_by = translate(n.order_by, ctx′) partition = PARTITION(by = by, order_by = order_by, frame = n.frame) - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] has_aggregates = false for ref in ctx.refs if @dissect(ref, nothing |> Agg()) && n.name === nothing @dissect(translate(ref, ctx′), AGG(name = (local name), args = (local args), filter = (local filter))) || error() push!(trns, ref => AGG(; name, args, filter, over = partition)) has_aggregates = true - elseif @dissect(ref, (local over = nothing |> Agg()) |> Nested(name = (local name))) && name === n.name - @dissect(translate(over, ctx′), AGG(name = (local name), args = (local args), filter = (local filter))) || error() + elseif @dissect(ref, (local tail = nothing |> Agg()) |> Nested(name = (local name))) && name === n.name + @dissect(translate(tail, ctx′), AGG(name = (local name), args = (local args), filter = (local filter))) || error() push!(trns, ref => AGG(; name, args, filter, over = partition)) has_aggregates = true else @@ -857,12 +868,12 @@ _outer_safe(a::Assemblage) = all(@dissect(col, (nothing |> ID() |> ID())) for col in values(a.cols)) function assemble(n::RoutedJoinNode, ctx) - left = assemble(n.over, ctx) + left = assemble(ctx) if @dissect(left.syntax, local tail = FROM() || JOIN()) && (!n.right || _outer_safe(left)) left_alias = nothing else left_alias = allocate_alias(ctx, left) - tail = FROM(AS(tail = complete(left), name = left_alias)) + tail = FROM(AS(name = left_alias, tail = complete(left))) end lateral = n.lateral subs = make_subs(left, left_alias) @@ -880,17 +891,17 @@ function assemble(n::RoutedJoinNode, ctx) end else right_alias = allocate_alias(ctx, right) - joinee = AS(tail = complete(right), name = right_alias) + joinee = AS(name = right_alias, tail = complete(right)) right_cache = Dict{Symbol, SQLSyntax}() for (ref, name) in right.repl subs[ref] = get(right_cache, name) do - ID(tail = right_alias, name = name) + ID(name = name, tail = right_alias) end end end on = translate(n.on, ctx, subs) - s = JOIN(tail = tail, joinee = joinee, on = on, left = n.left, right = n.right, lateral = lateral) - trns = Pair{SQLNode, SQLSyntax}[] + s = JOIN(joinee = joinee, on = on, left = n.left, right = n.right, lateral = lateral, tail = tail) + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end @@ -899,13 +910,13 @@ function assemble(n::RoutedJoinNode, ctx) end function assemble(n::SelectNode, ctx) - base = assemble(n.over, ctx) + base = assemble(ctx) if !@dissect(base.syntax, SELECT() || UNION()) base_alias = nothing tail = base.syntax else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(tail = complete(base), name = base_alias)) + tail = FROM(AS(name = base_alias, tail = complete(base))) end subs = make_subs(base, base_alias) cols = OrderedDict{Symbol, SQLSyntax}() @@ -913,9 +924,9 @@ function assemble(n::SelectNode, ctx) col = n.args[i] cols[name] = translate(col, ctx, subs) end - s = SELECT(tail = tail, args = complete(cols)) + s = SELECT(args = complete(cols), tail = tail) cols = OrderedDict{Symbol, SQLSyntax}([name => ID(name) for name in keys(cols)]) - repl = Dict{SQLNode, Symbol}() + repl = Dict{SQLQuery, Symbol}() for ref in ctx.refs @dissect(ref, nothing |> Get(name = (local name))) || error() repl[ref] = name @@ -938,7 +949,7 @@ function merge_conditions(s1, s2) end function assemble(n::WhereNode, ctx) - base = assemble(n.over, ctx) + base = assemble(ctx) if @dissect(base.syntax, nothing || FROM() || JOIN() || WHERE() || HAVING()) || @dissect(base.syntax, GROUP(by = (local by))) && !isempty(by) subs = make_subs(base, nothing) @@ -953,21 +964,21 @@ function assemble(n::WhereNode, ctx) s = HAVING(tail = base.syntax, condition = condition) elseif @dissect(base.syntax, (local tail) |> HAVING(condition = (local tail_condition))) condition = merge_conditions(tail_condition, condition) - s = HAVING(tail = tail, condition = condition) + s = HAVING(condition = condition, tail = tail) else - s = WHERE(tail = base.syntax, condition = condition) + s = WHERE(condition = condition, tail = base.syntax) end else base_alias = allocate_alias(ctx, base) - tail = FROM(AS(tail = complete(base), name = base_alias)) + tail = FROM(AS(name = base_alias, tail = complete(base))) subs = make_subs(base, base_alias) condition = translate(n.condition, ctx, subs) if @dissect(condition, LIT(val = true)) return base end - s = WHERE(tail = tail, condition = condition) + s = WHERE(condition = condition, tail = tail) end - trns = Pair{SQLNode, SQLSyntax}[] + trns = Pair{SQLQuery, SQLSyntax}[] for ref in ctx.refs push!(trns, ref => subs[ref]) end @@ -987,7 +998,7 @@ function assemble(n::WithNode, ctx) depth = _cte_depth(ctx.cte_map, name) + 1 cte_map′ = Base.ImmutableDict(cte_map′, (name, depth) => lastindex(ctx.ctes)) end - assemble(n.over, TranslateContext(ctx, cte_map = cte_map′)) + assemble(TranslateContext(ctx, cte_map = cte_map′)) end function assemble(n::WithExternalNode, ctx) @@ -1009,5 +1020,5 @@ function assemble(n::WithExternalNode, ctx) depth = _cte_depth(ctx.cte_map, name) + 1 cte_map′ = Base.ImmutableDict(cte_map′, (name, depth) => lastindex(ctx.ctes)) end - assemble(n.over, TranslateContext(ctx, cte_map = cte_map′)) + assemble(TranslateContext(ctx, cte_map = cte_map′)) end From f3d22a374b88a883992f94f0e32357a0183b7788 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sat, 17 May 2025 11:52:04 -0500 Subject: [PATCH 07/17] Preserve funsql macro nodes in the query tree --- docs/src/test/nodes.md | 406 ++++++++++++++++++++------------------- src/nodes.jl | 418 ++++++++++++++++++++++++----------------- src/nodes/from.jl | 8 +- src/nodes/highlight.jl | 10 + src/nodes/internal.jl | 72 ++++++- src/quote.jl | 8 +- src/resolve.jl | 23 ++- 7 files changed, 555 insertions(+), 390 deletions(-) diff --git a/docs/src/test/nodes.md b/docs/src/test/nodes.md index 1767ce54..1d1aa14d 100644 --- a/docs/src/test/nodes.md +++ b/docs/src/test/nodes.md @@ -6,7 +6,7 @@ Agg, Append, As, Asc, Bind, CrossJoin, Define, Desc, Fun, From, Get, Group, Highlight, Iterate, Join, LeftJoin, Limit, Lit, Order, Over, Partition, SQLQuery, SQLTable, Select, Sort, Var, Where, With, - WithExternal, ID, render + WithExternal, ID, render, unwrap_funsql_macro We start with specifying the database model. @@ -56,7 +56,7 @@ could be accessed using attributes `head` and `tail`. #-> (Select(…)).head display(q.head) - #-> Select(Get.person_id).head + #-> Select(…).head q.tail #-> (…) |> Where(…) @@ -77,8 +77,8 @@ Ill-formed queries are detected. #=> ERROR: FunSQL.IllFormedError in: let person = SQLTable(:person, …), - q1 = From(person), - q2 = q1 |> Agg.count() |> Select(Get.person_id) + q1 = From(person) |> Agg.count(), + q2 = q1 |> Select(Get.person_id) q2 end =# @@ -92,16 +92,25 @@ Ill-formed queries are detected. ## `@funsql` -The `@funsql` macro provides alternative notation for specifying FunSQL queries. +The `@funsql` macro provides alternative notation for assembling FunSQL queries. q = @funsql begin from(person) filter(year_of_birth > 2000) select(person_id) end + #-> #= … =# @funsql … display(q) #=> + #= … =# + @funsql (from(person); filter(year_of_birth > 2000); select(person_id)) + =# + +We can access the underlying structure of a query: + + display(unwrap_funsql_macro(q)) + #=> let q1 = From(:person), q2 = q1 |> Where(Fun.">"(Get.year_of_birth, 2000)), q3 = q2 |> Select(Get.person_id) @@ -119,11 +128,8 @@ We can combine `@funsql` notation with regular Julia code. display(q) #=> - let q1 = From(:person), - q2 = q1 |> Where(Fun.">"(Get.year_of_birth, 2000)), - q3 = q2 |> Select(Get.person_id) - q3 - end + #= … =# + @funsql (from(person); $(Where(Get.year_of_birth .> 2000)); select(person_id)) =# q = From(:person) |> @@ -133,7 +139,7 @@ We can combine `@funsql` notation with regular Julia code. display(q) #=> let q1 = From(:person), - q2 = q1 |> Where(Fun.">"(Get.year_of_birth, 2000)), + q2 = q1 |> @funsql(filter(year_of_birth > 2000)), q3 = q2 |> Select(Get.person_id) q3 end @@ -146,12 +152,13 @@ functions. display(@funsql adults()) #=> - let q1 = From(:person), - q2 = q1 |> Where(Fun.">="(Fun."-"(2020, Get.year_of_birth), 16)) - q2 - end + #= … =# + @funsql adults() =# + display(unwrap_funsql_macro(@funsql(adults()), depth = 1)) + #-> @funsql from(person).filter(2020 - year_of_birth >= 16) + Query functions defined with `@funsql` can accept parameters. @funsql concept_by_code(v, c) = @@ -160,7 +167,7 @@ Query functions defined with `@funsql` can accept parameters. filter(vocabulary_id == $v && concept_code == $c) end - display(@funsql concept_by_code("SNOMED", "22298006")) + display(unwrap_funsql_macro(@funsql concept_by_code("SNOMED", "22298006"))) #=> let q1 = From(:concept), q2 = q1 |> @@ -178,7 +185,7 @@ Query functions support `...` notation. filter(vocabulary_id == $v && in(concept_code, $(cs...))) end - display(@funsql concept_by_code("Visit", "IP", "ER")) + display(unwrap_funsql_macro(@funsql concept_by_code("Visit", "IP", "ER"))) #=> let q1 = From(:concept), q2 = q1 |> @@ -200,7 +207,7 @@ Query functions support keyword arguments and default values. age_in_2000 => age(at = 2000)) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> @@ -220,23 +227,11 @@ A parameter of a query function accepts a type declaration. @funsql concept(id::Int) = from(concept).filter(concept_id == $id) - display(@funsql concept("22298006")) - #=> - let q1 = From(:concept), - q2 = q1 |> - Where(Fun.and(Fun."="(Get.vocabulary_id, "SNOMED"), - Fun."="(Get.concept_code, "22298006"))) - q2 - end - =# + display(unwrap_funsql_macro(@funsql(concept("22298006")), depth = 1)) + #-> @funsql concept_by_code($(v), $(c)) - display(@funsql concept(4329847)) - #=> - let q1 = From(:concept), - q2 = q1 |> Where(Fun."="(Get.concept_id, 4329847)) - q2 - end - =# + display(unwrap_funsql_macro(@funsql(concept(4329847)), depth = 1)) + #-> @funsql from(concept).filter(concept_id == $(id)) A single `@funsql` macro can wrap multiple definitions. @@ -246,15 +241,8 @@ A single `@funsql` macro can wrap multiple definitions. `MYOCARDIAL INFARCTION`() = SNOMED("22298006") end - display(@funsql `MYOCARDIAL INFARCTION`()) - #=> - let q1 = From(:concept), - q2 = q1 |> - Where(Fun.and(Fun."="(Get.vocabulary_id, "SNOMED"), - Fun."="(Get.concept_code, "22298006"))) - q2 - end - =# + display(unwrap_funsql_macro(@funsql(`MYOCARDIAL INFARCTION`()), depth = 1)) + #-> @funsql SNOMED("22298006") A query function may have a docstring. @@ -276,7 +264,7 @@ An ill-formed `@funsql` query triggers an error. @funsql for p in person; end #=> - ERROR: LoadError: FunSQL.TransliterationError: ill-formed @funsql notation: + ERROR: LoadError: FunSQL.TransliterationError: ill-formed @funsql notation at …: quote for p = person end @@ -320,7 +308,7 @@ Such plain literals could also be used in `@funsql` notation. text => "SQL is fun!", date => $(Date(2000))) - display(q) + display(unwrap_funsql_macro(q)) #=> Select(missing |> As(:null), true |> As(:boolean), @@ -375,7 +363,9 @@ this symbol will be translated to a column reference. `@funsql` notation supports hierarchical references. - @funsql p.person_id + e = @funsql p.person_id + + display(unwrap_funsql_macro(e)) #-> Get.p.person_id Use backticks to represent a name that is not a valid identifier. @@ -383,7 +373,9 @@ Use backticks to represent a name that is not a valid identifier. @funsql `person_id` #-> :person_id - @funsql `p`.`person_id` + e = @funsql `p`.`person_id` + + display(unwrap_funsql_macro(e)) #-> Get.p.person_id `Get` is used for dereferencing an alias created with `As`. @@ -433,8 +425,8 @@ When `Get` refers to an unknown attribute, an error is reported. #=> ERROR: FunSQL.ReferenceError: cannot find `q` in: let person = SQLTable(:person, …), - q1 = From(person), - q2 = q1 |> As(:p) |> Select(Get.q.person_id) + q1 = From(person) |> As(:p), + q2 = q1 |> Select(Get.q.person_id) q2 end =# @@ -464,8 +456,8 @@ reference, will result in an error. #=> ERROR: FunSQL.ReferenceError: incomplete reference `p` in: let person = SQLTable(:person, …), - q1 = From(person), - q2 = q1 |> As(:p) |> Select(Get.p) + q1 = From(person) |> As(:p), + q2 = q1 |> Select(Get.p) q2 end =# @@ -546,7 +538,7 @@ A `Define` node can be created using `@funsql` notation. q = @funsql from(person).define(age => 2000 - year_of_birth) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Define(Fun."-"(2000, Get.year_of_birth) |> As(:age)) @@ -714,9 +706,7 @@ column. print(render(q)) #=> ERROR: FunSQL.ReferenceError: cannot find `person_id` in: - let q1 = Define(before = :person_id) - q1 - end + Define(before = :person_id) =# `Define` has no effect if none of the defined fields are used in the query. @@ -811,7 +801,7 @@ A vector of arguments could be passed directly. e = @funsql fun(>, year_of_birth, 2000) - display(e) + display(unwrap_funsql_macro(e)) #-> Fun.">"(Get.year_of_birth, 2000) In order to generate `Fun` nodes using regular function and operator calls, @@ -819,22 +809,22 @@ we need to declare these functions and operators in advance. e = @funsql concat(location.city, ", ", location.state) - display(e) + display(unwrap_funsql_macro(e)) #-> Fun.concat(Get.location.city, ", ", Get.location.state) e = @funsql 1950 < year_of_birth < 1990 - display(e) + display(unwrap_funsql_macro(e)) #-> Fun.and(Fun."<"(1950, Get.year_of_birth), Fun."<"(Get.year_of_birth, 1990)) e = @funsql location.state != "IL" || location.zip != 60615 - display(e) + display(unwrap_funsql_macro(e)) #-> Fun.or(Fun."<>"(Get.location.state, "IL"), Fun."<>"(Get.location.zip, 60615)) e = @funsql location.state == "IL" && location.zip == 60615 - display(e) + display(unwrap_funsql_macro(e)) #-> Fun.and(Fun."="(Get.location.state, "IL"), Fun."="(Get.location.zip, 60615)) In `@funsql` notation, use backticks to represent a name that is not @@ -842,12 +832,12 @@ a valid identifier. e = @funsql fun(`SUBSTRING(? FROM ? FOR ?)`, city, 1, 1) - display(e) + display(unwrap_funsql_macro(e)) #-> Fun."SUBSTRING(? FROM ? FOR ?)"(Get.city, 1, 1) q = @funsql `from`(person).`filter`(year_of_birth <= 1964) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Where(Fun."<="(Get.year_of_birth, 1964)) @@ -859,13 +849,13 @@ In `@funsql` notation, an `if` statement is converted to a `CASE` expression. e = @funsql year_of_birth <= 1964 ? "Boomers" : "Millenials" - display(e) + display(unwrap_funsql_macro(e)) #-> Fun.case(Fun."<="(Get.year_of_birth, 1964), "Boomers", "Millenials") e = @funsql year_of_birth <= 1964 ? "Boomers" : year_of_birth <= 1980 ? "Generation X" : "Millenials" - display(e) + display(unwrap_funsql_macro(e)) #=> Fun.case(Fun."<="(Get.year_of_birth, 1964), "Boomers", @@ -876,7 +866,7 @@ In `@funsql` notation, an `if` statement is converted to a `CASE` expression. e = @funsql if year_of_birth <= 1964; "Boomers"; end - display(e) + display(unwrap_funsql_macro(e)) #-> Fun.case(Fun."<="(Get.year_of_birth, 1964), "Boomers") e = @funsql begin @@ -887,7 +877,7 @@ In `@funsql` notation, an `if` statement is converted to a `CASE` expression. end end - display(e) + display(unwrap_funsql_macro(e)) #=> Fun.case(Fun."<="(Get.year_of_birth, 1964), "Boomers", @@ -907,7 +897,7 @@ In `@funsql` notation, an `if` statement is converted to a `CASE` expression. end end - display(e) + display(unwrap_funsql_macro(e)) #=> Fun.case(Fun."<="(Get.year_of_birth, 1964), "Boomers", @@ -1285,7 +1275,9 @@ Alternatively, use shorthand notation. A variable could be created with `@funsql` notation. - @funsql :YEAR + e = @funsql :YEAR + + display(unwrap_funsql_macro(e)) #-> Var.YEAR Unbound query variables are serialized as query parameters. @@ -1322,8 +1314,10 @@ Query variables could be bound using the `Bind` constructor. #=> let visit_occurrence = SQLTable(:visit_occurrence, …), q1 = From(visit_occurrence), - q2 = q1 |> Where(Fun."="(Get.person_id, Var.PERSON_ID)) - q2 |> Bind(1 |> As(:PERSON_ID)) + q2 = q1 |> + Where(Fun."="(Get.person_id, Var.PERSON_ID)) |> + Bind(1 |> As(:PERSON_ID)) + q2 end =# @@ -1347,11 +1341,12 @@ A `Bind` node can be created with `@funsql` notation. bind(:PERSON_ID => person_id) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:visit_occurrence), - q2 = q1 |> Where(Fun."="(Get.person_id, Var.PERSON_ID)) - q2 |> Bind(Get.person_id |> As(:PERSON_ID)) + q2 = q1 |> Where(Fun."="(Get.person_id, Var.PERSON_ID)), + q3 = q2 |> Bind(Get.person_id |> As(:PERSON_ID)) + q3 end =# @@ -1438,9 +1433,8 @@ multiple queries. q1 = From(measurement), q2 = q1 |> Define(Get.measurement_date |> As(:date)), q3 = From(observation), - q4 = q3 |> Define(Get.observation_date |> As(:date)), - q5 = q2 |> Append(q4) - q5 + q4 = q2 |> Append(q3 |> Define(Get.observation_date |> As(:date))) + q4 end =# @@ -1495,14 +1489,13 @@ An `Append` node can be created using `@funsql` notation. append(from(observation).define(date => observation_date)) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:measurement), q2 = q1 |> Define(Get.measurement_date |> As(:date)), q3 = From(:observation), - q4 = q3 |> Define(Get.observation_date |> As(:date)), - q5 = q2 |> Append(q4) - q5 + q4 = q2 |> Append(q3 |> Define(Get.observation_date |> As(:date))) + q4 end =# @@ -1704,9 +1697,8 @@ We could use `Iterate` and `From(^)` to create a factorial table. q3 = q2 |> Define(Fun."+"(Get.n, 1) |> As(:n), Fun."*"(Get.f, Fun."+"(Get.n, 1)) |> As(:f)), - q4 = q3 |> Where(Fun."<="(Get.n, 10)), - q5 = q1 |> Iterate(q4) - q5 + q4 = q1 |> Iterate(q3 |> Where(Fun."<="(Get.n, 10))) + q4 end =# @@ -1741,14 +1733,13 @@ An `Iterate` node can be created using `@funsql` notation. iterate(define(n => n + 1, f => f * (n + 1)).filter(n <= 10)) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = Define(1 |> As(:n), 1 |> As(:f)), q2 = Define(Fun."+"(Get.n, 1) |> As(:n), Fun."*"(Get.f, Fun."+"(Get.n, 1)) |> As(:f)), - q3 = q2 |> Where(Fun."<="(Get.n, 10)), - q4 = q1 |> Iterate(q3) - q4 + q3 = q1 |> Iterate(q2 |> Where(Fun."<="(Get.n, 10))) + q3 end =# @@ -1826,9 +1817,7 @@ It is an error to use `From(^)` outside of `Iterate`. print(render(q)) #=> ERROR: FunSQL.ReferenceError: self-reference outside of Iterate in: - let q1 = From(^) - q1 - end + From(^) =# The set of columns produced by `Iterate` is the intersection of the columns @@ -1903,7 +1892,7 @@ An alias to an expression can be added with the `As` constructor. e = @funsql (42).as(integer) - display(e) + display(unwrap_funsql_macro(e)) #-> 42 |> As(:integer) The `=>` shorthand is supported by `@funsql`. @@ -1945,7 +1934,12 @@ given table. #-> From(…) display(q) - #-> From(SQLTable(:person, …)) + #=> + let person = SQLTable(:person, …), + q1 = From(person) + q1 + end + =# By default, `From` selects all columns from the table. @@ -2144,34 +2138,39 @@ A `From` node can be created with `@funsql` notation. q = @funsql from(person) - display(q) + display(unwrap_funsql_macro(q)) #-> From(:person) q = @funsql from(nothing) - display(q) + display(unwrap_funsql_macro(q)) #-> From(nothing) q = @funsql from(^) - display(q) + display(unwrap_funsql_macro(q)) #-> From(^) q = @funsql from($person) - display(q) - #-> From(SQLTable(:person, …)) + display(unwrap_funsql_macro(q)) + #=> + let person = SQLTable(:person, …), + q1 = From(person) + q1 + end + =# q = @funsql from($df) - display(q) + display(unwrap_funsql_macro(q)) #-> From((name = ["SQL", …], year = [1974, …])) funsql_generate_series = FunSQL.FunClosure(:generate_series) q = @funsql from(generate_series(0, 100, 10), columns = [value]) - display(q) + display(unwrap_funsql_macro(q)) #-> From(Fun.generate_series(0, 100, 10), columns = [:value]) When `From` with a tabular function is attached to the right branch of @@ -2200,10 +2199,8 @@ All the columns of a tabular function must have distinct names. columns = [:index, :index]) #=> ERROR: FunSQL.DuplicateLabelError: `index` is used more than once in: - let q1 = From(Fun."? WITH ORDINALITY"(Fun.generate_series(0, 100, 10)), - columns = [:index, :index]) - q1 - end + From(Fun."? WITH ORDINALITY"(Fun.generate_series(0, 100, 10)), + columns = [:index, :index]) =# `From(nothing)` will generate a *unit* dataset with one row. @@ -2233,9 +2230,9 @@ We can create a temporary dataset using `With` and refer to it with `From`. let person = SQLTable(:person, …), q1 = From(:male), q2 = From(person), - q3 = q2 |> Where(Fun."="(Get.gender_concept_id, 8507)), - q4 = q1 |> With(q3 |> As(:male)) - q4 + q3 = q1 |> + With(q2 |> Where(Fun."="(Get.gender_concept_id, 8507)) |> As(:male)) + q3 end =# @@ -2362,13 +2359,14 @@ A `With` node can be created using `@funsql`. materialized = false) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:male), q2 = From(:person), - q3 = q2 |> Where(Fun."="(Get.gender_concept_id, 8507)), - q4 = q1 |> With(q3 |> As(:male), materialized = false) - q4 + q3 = q1 |> + With(q2 |> Where(Fun."="(Get.gender_concept_id, 8507)) |> As(:male), + materialized = false) + q3 end =# @@ -2382,9 +2380,8 @@ A dataset defined by `With` must have an explicit label assigned to it. ERROR: FunSQL.ReferenceError: table reference `person` requires As in: let person = SQLTable(:person, …), q1 = From(:person), - q2 = From(person), - q3 = q1 |> With(q2) - q3 + q2 = q1 |> With(From(person)) + q2 end =# @@ -2396,10 +2393,8 @@ Datasets defined by `With` must have a unique label. #=> ERROR: FunSQL.DuplicateLabelError: `p` is used more than once in: let person = SQLTable(:person, …), - q1 = From(person), - q2 = From(person), - q3 = With(q1 |> As(:p), q2 |> As(:p)) - q3 + q1 = With(From(person) |> As(:p), From(person) |> As(:p)) + q1 end =# @@ -2410,9 +2405,7 @@ It is an error for `From` to refer to an undefined dataset. print(render(q)) #=> ERROR: FunSQL.ReferenceError: cannot find `p` in: - let q1 = From(:p) - q1 - end + From(:p) =# A variant of `With` called `Over` exchanges the positions of the definition @@ -2428,10 +2421,9 @@ and the query that uses it. #=> let person = SQLTable(:person, …), q1 = From(person), - q2 = q1 |> Where(Fun."="(Get.gender_concept_id, 8507)), - q3 = From(:male), - q4 = q2 |> As(:male) |> Over(q3) - q4 + q2 = q1 |> Where(Fun."="(Get.gender_concept_id, 8507)) |> As(:male), + q3 = q2 |> Over(From(:male)) + q3 end =# @@ -2459,13 +2451,12 @@ An `Over` node can be created using `@funsql`. over(from(male), materialized = true) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), - q2 = q1 |> Where(Fun."="(Get.gender_concept_id, 8507)), - q3 = From(:male), - q4 = q2 |> As(:male) |> Over(q3, materialized = true) - q4 + q2 = q1 |> Where(Fun."="(Get.gender_concept_id, 8507)) |> As(:male), + q3 = q2 |> Over(From(:male), materialized = true) + q3 end =# @@ -2511,10 +2502,8 @@ Datasets defined by `WithExternal` must have a unique label. #=> ERROR: FunSQL.DuplicateLabelError: `p` is used more than once in: let person = SQLTable(:person, …), - q1 = From(person), - q2 = From(person), - q3 = WithExternal(q1 |> As(:p), q2 |> As(:p)) - q3 + q1 = WithExternal(From(person) |> As(:p), From(person) |> As(:p)) + q1 end =# @@ -2547,7 +2536,7 @@ A `Group` node can be created using `@funsql` notation. q = @funsql from(person).group(year_of_birth) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Group(Get.year_of_birth) @@ -2577,37 +2566,37 @@ Aggregate functions can be created with `@funsql`. e = @funsql agg(min, year_of_birth) - display(e) + display(unwrap_funsql_macro(e)) #-> Agg.min(Get.year_of_birth) e = @funsql min(year_of_birth) - display(e) + display(unwrap_funsql_macro(e)) #-> Agg.min(Get.year_of_birth) e = @funsql count(filter = year_of_birth > 1950) - display(e) + display(unwrap_funsql_macro(e)) #-> Agg.count(filter = Fun.">"(Get.year_of_birth, 1950)) e = @funsql visit_group.count() - display(e) + display(unwrap_funsql_macro(e)) #-> Get.visit_group |> Agg.count() e = @funsql `count`() - display(e) + display(unwrap_funsql_macro(e)) #-> Agg.count() e = @funsql visit_group.`count`() - display(e) + display(unwrap_funsql_macro(e)) #-> Get.visit_group |> Agg.count() e = @funsql `visit_group`.`count`() - display(e) + display(unwrap_funsql_macro(e)) #-> Get.visit_group |> Agg.count() `Group` will create a single instance of an aggregate function even if it is @@ -2809,9 +2798,7 @@ indicators `:cube` or `:rollup`, or by explicit enumeration. Group(Get.year_of_birth, sets = [[1, 2], [1], []]) #=> ERROR: FunSQL.InvalidGroupingSetsError: `2` is out of bounds in: - let q1 = Group(Get.year_of_birth, sets = [[1, 2], [1], []]) - q1 - end + Group(Get.year_of_birth, sets = [[1, 2], [1], []]) =# From(person) |> @@ -2819,9 +2806,7 @@ indicators `:cube` or `:rollup`, or by explicit enumeration. sets = [[1], []]) #=> ERROR: FunSQL.InvalidGroupingSetsError: missing keys `[:year_of_birth]` in: - let q1 = Group(Get.year_of_birth, Get.gender_concept_id, sets = [[1], []]) - q1 - end + Group(Get.year_of_birth, Get.gender_concept_id, sets = [[1], []]) =# `Group` allows specifying the name of a group field. @@ -3018,7 +3003,7 @@ A `Partition` node can be created with `@funsql` notation. partition(year_of_birth, order_by = [month_of_birth, day_of_birth]) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> @@ -3107,12 +3092,12 @@ A window frame can be specified in `@funsql` notation. q = @funsql partition(order_by = [year_of_birth], frame = groups) - display(q) + display(unwrap_funsql_macro(q)) #-> Partition(order_by = [Get.year_of_birth], frame = :GROUPS) q = @funsql partition(order_by = [year_of_birth], frame = (mode = range, start = -1, finish = 1)) - display(q) + display(unwrap_funsql_macro(q)) #=> Partition(order_by = [Get.year_of_birth], frame = (mode = :RANGE, start = -1, finish = 1)) @@ -3120,7 +3105,7 @@ A window frame can be specified in `@funsql` notation. q = @funsql partition(; order_by = [year_of_birth], frame = (mode = range, start = -Inf, finish = Inf, exclude = current_row)) - display(q) + display(unwrap_funsql_macro(q)) #=> Partition( order_by = [Get.year_of_birth], @@ -3237,12 +3222,11 @@ The `Join` constructor creates a subquery that correlates two nested subqueries. let person = SQLTable(:person, …), location = SQLTable(:location, …), q1 = From(person), - q2 = From(location), - q3 = q1 |> - Join(q2 |> As(:location), + q2 = q1 |> + Join(From(location) |> As(:location), Fun."="(Get.location_id, Get.location.location_id), left = true) - q3 + q2 end =# @@ -3267,12 +3251,11 @@ The `Join` constructor creates a subquery that correlates two nested subqueries. let person = SQLTable(:person, …), location = SQLTable(:location, …), q1 = From(person), - q2 = From(location), - q3 = q1 |> - Join(q2 |> As(:location), + q2 = q1 |> + Join(From(location) |> As(:location), Fun."="(Get.location_id, Get.location.location_id), left = true) - q3 + q2 end =# @@ -3285,15 +3268,14 @@ Various `Join` nodes can be created with `@funsql` notation. left = true) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), - q2 = From(:location), - q3 = q1 |> - Join(q2 |> As(:location), + q2 = q1 |> + Join(From(:location) |> As(:location), Fun."="(Get.location_id, Get.location.location_id), left = true) - q3 + q2 end =# @@ -3303,15 +3285,14 @@ Various `Join` nodes can be created with `@funsql` notation. location_id == location.location_id) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), - q2 = From(:location), - q3 = q1 |> - Join(q2 |> As(:location), + q2 = q1 |> + Join(From(:location) |> As(:location), Fun."="(Get.location_id, Get.location.location_id), left = true) - q3 + q2 end =# @@ -3320,12 +3301,11 @@ Various `Join` nodes can be created with `@funsql` notation. cross_join(other => from(person)) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), - q2 = From(:person), - q3 = q1 |> Join(q2 |> As(:other), true) - q3 + q2 = q1 |> Join(From(:person) |> As(:other), true) + q2 end =# @@ -3567,13 +3547,12 @@ its right branch. let person = SQLTable(:person, …), location = SQLTable(:location, …), q1 = From(person), - q2 = From(location), - q3 = q1 |> - Join(q2 |> As(:location), + q2 = q1 |> + Join(From(location) |> As(:location), Fun."="(Get.location_id, Get.location.location_id), left = true, optional = true) - q3 + q2 end =# @@ -3627,7 +3606,7 @@ An `Order` node can be created with `@funsql` notation. order(year_of_birth) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Order(Get.year_of_birth) @@ -3730,7 +3709,7 @@ Sort decorations can be created with `@funsql`. order(year_of_birth.desc(nulls = first), person_id.asc()) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> @@ -3745,7 +3724,7 @@ Sort decorations can be created with `@funsql`. order(year_of_birth.sort(desc, nulls = first), person_id.sort(asc)) end - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> @@ -3868,7 +3847,7 @@ A `Limit` node can be created with `@funsql` notation. q = @funsql from(person).order(person_id).limit(10) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Order(Get.person_id), @@ -3879,7 +3858,7 @@ A `Limit` node can be created with `@funsql` notation. q = @funsql from(person).order(person_id).limit(100, 10) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Order(Get.person_id), @@ -3890,7 +3869,7 @@ A `Limit` node can be created with `@funsql` notation. q = @funsql from(person).order(person_id).limit(101:110) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Order(Get.person_id), @@ -3927,7 +3906,7 @@ A `Select` node can be created with `@funsql` notation. q = @funsql from(person).select(person_id) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Select(Get.person_id) @@ -3993,7 +3972,7 @@ A `Where` node can be created with `@funsql` notation. q = @funsql from(person).filter(year_of_birth > 2000) - display(q) + display(unwrap_funsql_macro(q)) #=> let q1 = From(:person), q2 = q1 |> Where(Fun.">"(Get.year_of_birth, 2000)) @@ -4125,12 +4104,8 @@ A `Highlight` node can be created with `@funsql` notation. q = @funsql from(person).highlight(red) - display(q) - #=> - let q1 = From(:person) - q1 - end - =# + display(unwrap_funsql_macro(q)) + #-> From(:person) ## Debugging @@ -4166,23 +4141,39 @@ and determines node types. │ let person = SQLTable(:person, …), │ location = SQLTable(:location, …), │ visit_occurrence = SQLTable(:visit_occurrence, …), - │ q1 = FromTable(person), - │ q2 = q1 |> + │ q1 = FromTable(person) |> │ Resolved(RowType(:person_id => ScalarType(), │ :gender_concept_id => ScalarType(), │ :year_of_birth => ScalarType(), │ :month_of_birth => ScalarType(), │ :day_of_birth => ScalarType(), │ :birth_datetime => ScalarType(), - │ :location_id => ScalarType())) |> + │ :location_id => ScalarType())), + │ q2 = q1 |> │ Where(Fun."<="(Get.year_of_birth |> Resolved(ScalarType()), │ 2000 |> Resolved(ScalarType())) |> - │ Resolved(ScalarType())), + │ Resolved(ScalarType())) |> + │ Resolved(RowType(:person_id => ScalarType(), + │ :gender_concept_id => ScalarType(), + │ :year_of_birth => ScalarType(), + │ :month_of_birth => ScalarType(), + │ :day_of_birth => ScalarType(), + │ :birth_datetime => ScalarType(), + │ :location_id => ScalarType())), ⋮ - │ q9 |> - │ Resolved(RowType(:person_id => ScalarType(), - │ :max_visit_start_date => ScalarType())) |> - │ WithContext(catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing)) + │ q7 = q6 |> + │ Select(Get.person_id |> Resolved(ScalarType()), + │ Agg.max(Get.visit_start_date |> Resolved(ScalarType())) |> + │ Resolved(ScalarType()) |> + │ Nested(:visit_group) |> + │ Resolved(ScalarType()) |> + │ As(:max_visit_start_date) |> + │ Resolved(ScalarType())) |> + │ Resolved(RowType(:person_id => ScalarType(), + │ :max_visit_start_date => ScalarType())) |> + │ WithContext(catalog = SQLCatalog(dialect = SQLDialect(), + │ cache = nothing)) + │ q7 │ end └ @ FunSQL … =# @@ -4199,15 +4190,18 @@ produce. │ let person = SQLTable(:person, …), │ location = SQLTable(:location, …), │ visit_occurrence = SQLTable(:visit_occurrence, …), - │ q1 = FromTable(person), + │ q1 = Get.person_id, │ q2 = Get.person_id, - │ q3 = Get.person_id, - │ q4 = Get.location_id, - │ q5 = Get.year_of_birth, - │ q6 = q1 |> Linked([q2, q3, q4, q5], 3), + │ q3 = Get.location_id, + │ q4 = Get.year_of_birth, + │ q5 = FromTable(person) |> Linked([q1, q2, q3, q4], 3), ⋮ - │ q33 |> - │ WithContext(catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing)) + │ q19 = q18 |> + │ Select(q1, q16 |> As(:max_visit_start_date)) |> + │ Linked([Get.person_id, Get.max_visit_start_date]) |> + │ WithContext(catalog = SQLCatalog(dialect = SQLDialect(), + │ cache = nothing)) + │ q19 │ end └ @ FunSQL … =# diff --git a/src/nodes.jl b/src/nodes.jl index ec9f9be1..886472f6 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -105,181 +105,195 @@ function PrettyPrinting.quoteof(q::SQLGetQuery) end ex = :Get while !isempty(path) - ex = Expr(:., ex, QuoteNode(pop!(path))) + name = pop!(path) + ex = Expr(:., ex, QuoteNode(Base.isidentifier(name) ? name : string(name))) end ex end -function _tosqlgetquery(q) - q.head isa GetNode || return - tail′ = nothing - if q.tail !== nothing - tail′ = _tosqlgetquery(q.tail) - tail′ !== nothing || return - end - SQLGetQuery(tail′, q.head.name) -end +# Traversing query tree. -# Generic traversal and substitution. - -function visit(f, q::SQLQuery, visiting = Set{SQLQuery}()) - !(q in visiting) || return - push!(visiting, q) - visit(f, q.tail, visiting) - visit(f, q.head, visiting) - f(q) - pop!(visiting, q) - nothing -end - -function visit(f, qs::Vector{SQLQuery}, visiting) - for q in qs - visit(f, q, visiting) +function children(q::SQLQuery) + cs = SQLQuery[] + if q.tail !== nothing + push!(cs, q.tail) end + children!(q.head, cs) + cs end -visit(f, ::Nothing, visiting) = - nothing - -@generated function visit(f, n::AbstractSQLNode, visiting) +@generated function children!(n::AbstractSQLNode, children::Vector{SQLQuery}) exs = Expr[] for f in fieldnames(n) t = fieldtype(n, f) - if t === SQLQuery || t === Union{SQLQuery, Nothing} || t === Vector{SQLQuery} - ex = quote - visit(f, n.$(f), visiting) - end - push!(exs, ex) + if t === SQLQuery + push!(exs, quote push!(children, n.$(f)) end) + elseif t === Union{SQLQuery, Nothing} + push!( + exs, + quote + if n.$(f) !== nothing + push!(children, n.$(f)) + end + end) + elseif t === Vector{SQLQuery} + push!(exs, quote append!(children, n.$(f)) end) end end push!(exs, :(return nothing)) Expr(:block, exs...) end -substitute(q::SQLQuery, c::SQLQuery, c′::SQLQuery) = - if q.tail === c - SQLQuery(c′, q.head) - else - SQLQuery(q.tail, substitute(q.head, c, c′)) - end -function substitute(qs::Vector{SQLQuery}, c::SQLQuery, c′::SQLQuery) - i = findfirst(q -> q === c, qs) - i !== nothing || return qs - qs′ = copy(qs) - qs′[i] = c′ - qs′ -end +# Pretty-printing. -substitute(::Nothing, ::SQLQuery, ::SQLQuery) = - nothing +Base.show(io::IO, q::SQLQuery) = + print(io, quoteof(q, limit = true)) -@generated function substitute(n::AbstractSQLNode, c::SQLQuery, c′::SQLQuery) - exs = Expr[] - fs = fieldnames(n) - for f in fs - t = fieldtype(n, f) - if t === SQLQuery || t === Union{SQLQuery, Nothing} - ex = quote - if n.$(f) === c - return $n($(Any[Expr(:kw, f′, f′ !== f ? :(n.$(f′)) : :(c′)) - for f′ in fs]...)) - end - end - push!(exs, ex) - elseif t === Vector{SQLQuery} - ex = quote - let cs′ = substitute(n.$(f), c, c′) - if cs′ !== n.$(f) - return $n($(Any[Expr(:kw, f′, f′ !== f ? :(n.$(f′)) : :(cs′)) - for f′ in fs]...)) - end - end - end - push!(exs, ex) - end +function Base.show(io::IO, ::MIME"text/plain", q::SQLQuery) + if q isa SQLQuery && q.tail === nothing && q.head isa FunSQLMacroNode + println(io, q.head.line) end - push!(exs, :(return n)) - Expr(:block, exs...) + pprint(io, q) end - -# Pretty-printing. - -Base.show(io::IO, n::Union{AbstractSQLNode, SQLQuery}) = - print(io, quoteof(n, limit = true)) - -Base.show(io::IO, ::MIME"text/plain", n::Union{AbstractSQLNode, SQLQuery}) = - pprint(io, n) - -function PrettyPrinting.quoteof(q::SQLQuery; limit::Bool = false, head_only::Bool = false) - if q.head isa GetNode && !head_only - q′ = _tosqlgetquery(q) - if q′ !== nothing - return quoteof(q′) - end - end +function PrettyPrinting.quoteof(q::SQLQuery; limit::Bool = false) if limit ctx = QuoteContext(limit = true) ex = quoteof(q.head, ctx) - if head_only - ex = Expr(:., ex, QuoteNode(:head)) - elseif q.tail !== nothing + if q.tail !== nothing ex = Expr(:call, :|>, quoteof(q.tail, ctx), ex) end return ex end + skip_to = Dict{SQLQuery, SQLQuery}() tables_seen = OrderedSet{SQLTable}() queries_seen = OrderedSet{SQLQuery}() queries_toplevel = Set{SQLQuery}() - visit(q) do q - head = q.head - if head isa FromNode - source = head.source - if source isa SQLTable - push!(tables_seen, source) + highlight_targets = Dict{SQLQuery, Symbol}() + highlight_colors = Dict{SQLQuery, Vector{Symbol}}() + color_stack = [:normal] + unwrap_depth = [0] + unwrapped = Set{SQLQuery}() + stack = [(q, true)] + while !isempty(stack) + top, fwd = pop!(stack) + head = top.head + if fwd + push!(stack, (top, false)) + if head isa HighlightNode && top.tail !== nothing + highlight_targets[top.tail] = head.color + elseif head isa HighlightTargetNode && head.target isa SQLQuery + highlight_targets[head.target] = head.color + elseif head isa UnwrapFunSQLMacroNode + push!(unwrap_depth, head.depth) + elseif head isa FunSQLMacroNode + push!(unwrap_depth, unwrap_depth[end] != 0 ? unwrap_depth[end]-1 : 0) + end + color = get(highlight_targets, top, nothing) + if color !== nothing + push!(color_stack, color) + highlight_colors[top] = copy(color_stack) + end + if !(top in queries_seen) + if head isa FunSQLMacroNode + if unwrap_depth[end-1] != 0 + push!(stack, (head.query, true)) + end + if top.tail !== nothing + push!(stack, (top.tail, true)) + end + else + for c in reverse(children(top)) + push!(stack, (c, true)) + end + end end - end - if head isa FromTableNode - push!(tables_seen, head.table) - end - if head isa TabularNode - push!(queries_seen, q) - push!(queries_toplevel, q) - elseif q in queries_seen - push!(queries_toplevel, q) else - push!(queries_seen, q) + if head isa Union{HighlightNode, HighlightTargetNode, UnwrapFunSQLMacroNode} && top.tail !== nothing + skip_to[top] = get(skip_to, top.tail, top.tail) + if top in keys(highlight_colors) + highlight_colors[skip_to[top]] = highlight_colors[top] + end + end + if head isa Union{FunSQLMacroNode, UnwrapFunSQLMacroNode} + pop!(unwrap_depth) + end + if head isa FunSQLMacroNode && unwrap_depth[end] != 0 + push!(unwrapped, top) + if top.tail === nothing + skip_to[top] = get(skip_to, head.query, head.query) + end + end + if top in keys(highlight_targets) + pop!(color_stack) + end + if head isa FromNode && head.source isa SQLTable + push!(tables_seen, head.source) + elseif head isa FromTableNode + push!(tables_seen, head.table) + end + if head isa FunSQLMacroNode + push!(queries_toplevel, top) + if top.tail !== nothing + push!(queries_toplevel, get(skip_to, top.tail, top.tail)) + end + elseif head isa TabularNode && top.tail !== nothing + push!(queries_toplevel, get(skip_to, top.tail, top.tail)) + end + if top in queries_seen || isempty(stack) + push!(queries_toplevel, get(skip_to, top, top)) + end + push!(queries_seen, top) end end ctx = QuoteContext() defs = Any[] - if length(queries_toplevel) >= 2 || (length(queries_toplevel) == 1 && !(q in queries_toplevel)) - for t in tables_seen - def = quoteof(t, limit = true) - name = t.name - push!(defs, Expr(:(=), name, def)) - ctx.vars[t] = name - end - qidx = 0 - for q in queries_seen - q in queries_toplevel || continue - qidx += 1 - ctx.vars[q] = Symbol('q', qidx) - end - qidx = 0 - for q in queries_seen - q in queries_toplevel || continue - qidx += 1 - name = Symbol('q', qidx) - def = quoteof(q, ctx, true, true) - push!(defs, Expr(:(=), name, def)) + for t in collect(tables_seen) + def = quoteof(t, limit = true) + name = t.name + push!(defs, Expr(:(=), name, def)) + ctx.repl[t] = name + end + qidx = 0 + for q in queries_seen + if q in keys(skip_to) + ex = ctx.repl[skip_to[q]] + ctx.repl[q] = ex + else + toplevel = q in queries_toplevel + if !toplevel && q.tail === nothing && q.head isa LiteralNode && q.head.val isa SQLLiteralType + ex = quoteof(q.head.val) + elseif q.head isa GetNode && q.tail !== nothing && (local tail_ex = ctx.repl[q.tail]; tail_ex isa Expr && tail_ex.head === :.) + ex = Expr(:., tail_ex, QuoteNode(Base.isidentifier(q.head.name) ? q.head.name : string(q.head.name))) + elseif q.head isa FunSQLMacroNode && q in unwrapped + ex = ctx.repl[q.head.query] + if q.tail !== nothing + ex = Expr(:call, :|>, quoteof(q.tail, ctx), ex) + end + else + ex = quoteof(q, ctx) + end + colors = get(highlight_colors, q, nothing) + if colors !== nothing + ex = EscWrapper(ex, colors[end], colors[1:end-1]) + if toplevel + ex = NormalWrapper(ex) + end + end + if toplevel + qidx += 1 + name = Symbol('q', qidx) + push!(defs, Expr(:(=), name, ex)) + ex = name + end + ctx.repl[q] = ex end end - ex = quoteof(q, ctx, true, false) - if head_only - ex = Expr(:., ex, QuoteNode(:head)) + ex = ctx.repl[q] + if length(defs) == 1 + ex = pop!(defs).args[2] end if !isempty(defs) ex = Expr(:let, Expr(:block, defs...), ex) @@ -287,23 +301,10 @@ function PrettyPrinting.quoteof(q::SQLQuery; limit::Bool = false, head_only::Boo ex end -PrettyPrinting.quoteof(n::AbstractSQLNode; limit::Bool = false) = - quoteof(convert(SQLQuery, n), limit = limit, head_only = true) - -function PrettyPrinting.quoteof(q::SQLQuery, ctx::QuoteContext, top::Bool = false, full::Bool = false) +function PrettyPrinting.quoteof(q::SQLQuery, ctx::QuoteContext) if !ctx.limit - if q.head isa HighlightNode && q.tail !== nothing - color = q.head.color - push!(ctx.colors, color) - ex = quoteof(q.tail, ctx) - pop!(ctx.colors) - EscWrapper(ex, color, copy(ctx.colors)) - elseif !full && (local var = get(ctx.vars, q, nothing); var !== nothing) - var - elseif !full && !top && q.tail === nothing && q.head isa LiteralNode && q.head.val isa SQLLiteralType - quoteof(q.head.val) - elseif q.head isa GetNode && (local q′ = _tosqlgetquery(q); q′) !== nothing - quoteof(q′) + if (local ex = get(ctx.repl, q, nothing); ex !== nothing) + ex else ex = quoteof(q.head, ctx) if q.tail !== nothing @@ -325,6 +326,19 @@ PrettyPrinting.quoteof(qs::Vector{SQLQuery}, ctx::QuoteContext) = Any[:…] end +Base.show(io::IO, n::AbstractSQLNode) = + print(io, quoteof(n)) + +function Base.show(io::IO, ::MIME"text/plain", n::AbstractSQLNode) + if n isa FunSQLMacroNode + println(io, n.line) + end + pprint(io, n) +end + +PrettyPrinting.quoteof(@nospecialize n::AbstractSQLNode) = + Expr(:., quoteof(convert(SQLQuery, n), limit = true), QuoteNode(:head)) + # Errors. @@ -487,20 +501,52 @@ function Base.showerror(io::IO, err::ReferenceError) end function showpath(io, path::Vector{SQLQuery}) - if !isempty(path) - q = highlight(path) - println(io, " in:") - pprint(io, q) + stack = SQLQuery[] + while !isempty(path) + top = path[1] + if top.head isa FunSQLMacroNode + k = 1 + for (i, q) in enumerate(path) + if q.head isa FunSQLMacroNode && q.head.base === top.head.base + if q.tail === nothing || !(q.tail in path) + top = q + k = i + end + end + end + push!(stack, SQLQuery(top.head)) + path = path[k+1:end] + else + push!(stack, top |> HighlightTarget(path[end], :red)) + k = lastindex(path)+1 + for (i, q) in enumerate(path) + if q.head isa FunSQLMacroNode + if q.tail === nothing || !(q.tail in path) + k = i + break + end + end + end + path = path[k:end] + end end -end - -function highlight(path::Vector{SQLQuery}, color = Base.error_color()) - @assert !isempty(path) - q = Highlight(tail = path[end], color = color) - for k = lastindex(path):-1:2 - q = substitute(path[k - 1], path[k], q) + while !isempty(stack) + q = pop!(stack) + if q.head isa FunSQLMacroNode + print(io, " at $(relpath(string(q.head.line.file))):$(q.head.line.line)") + if q.head.def !== nothing + print(io, " in $(q.head.def)") + end + println(io, ":") + else + println(io, " in:") + end + pprint(io, q) + if !isempty(stack) + println(io) + print(io, "#") + end end - q end """ @@ -512,7 +558,7 @@ struct TransliterationError <: FunSQLError end function Base.showerror(io::IO, err::TransliterationError) - println(io, "FunSQL.TransliterationError: ill-formed @funsql notation:") + println(io, "FunSQL.TransliterationError: ill-formed @funsql notation at $(relpath(string(err.src.file))):$(err.src.line):") pprint(io, err.expr) end @@ -569,21 +615,43 @@ end struct TransliterateContext mod::Module - src::LineNumberNode + def::Union{Symbol, Nothing} + base::LineNumberNode + line::LineNumberNode decl::Bool - TransliterateContext(mod::Module, src::LineNumberNode, decl::Bool = false) = - new(mod, src, decl) + TransliterateContext(mod::Module, line::LineNumberNode, decl::Bool = false) = + new(mod, nothing, line, line, decl) - TransliterateContext(ctx::TransliterateContext; src = ctx.src, decl = ctx.decl) = - new(ctx.mod, src, decl) + TransliterateContext(ctx::TransliterateContext; def = ctx.def, base = ctx.base, line = ctx.line, decl = ctx.decl) = + new(ctx.mod, def, base, line, decl) end """ Convenient notation for assembling FunSQL queries. """ macro funsql(ex) - transliterate(ex, TransliterateContext(__module__, __source__)) + ctx = TransliterateContext(__module__, __source__) + transliterate_toplevel(ex, ctx) +end + +function transliterate_toplevel(@nospecialize(ex), ctx) + ex′ = transliterate(ex, ctx) + quote + let q = $ex′ + if q isa Union{SQLQuery, SQLGetQuery} + FunSQLMacro( + q, + $(QuoteNode(ex)), + $(ctx.mod), + $(ctx.def !== nothing ? QuoteNode(ctx.def) : nothing), + $(QuoteNode(ctx.base)), + $(QuoteNode(ctx.line))) + else + q + end + end + end end function transliterate(@nospecialize(ex), ctx::TransliterateContext) @@ -609,7 +677,7 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) if @dissect(arg, (local name)::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String)) arg = Symbol("funsql_$name") else - ctx = TransliterateContext(ctx, src = ln) + ctx = TransliterateContext(ctx, line = ln) arg = transliterate(arg, ctx) end return Expr(:macrocall, ref, ln, doc, arg) @@ -617,10 +685,10 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) # name(args...) = body ctx = TransliterateContext(ctx, decl = true) trs = Any[transliterate(arg, ctx) for arg in args] - ctx = TransliterateContext(ctx, decl = false) + ctx = TransliterateContext(ctx, def = Symbol(name), decl = false) return Expr(:(=), :($(esc(Symbol("funsql_$name")))($(trs...))), - transliterate(body, ctx)) + transliterate_toplevel(body, ctx)) elseif @dissect(ex, Expr(:(=), (local name)::Symbol, (local arg))) # name = arg return Expr(:(=), esc(name), transliterate(arg, ctx)) @@ -728,7 +796,7 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) trs = Any[] for arg in args if arg isa LineNumberNode - ctx = TransliterateContext(ctx, src = arg) + ctx = TransliterateContext(ctx, base = arg, line = arg) push!(trs, arg) else push!(trs, transliterate(arg, ctx)) @@ -739,9 +807,9 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) tr = nothing for arg in args if arg isa LineNumberNode - ctx = TransliterateContext(ctx, src = arg) + ctx = TransliterateContext(ctx, line = arg) else - tr′ = Expr(:block, ctx.src, transliterate(arg, ctx)) + tr′ = Expr(:block, ctx.line, transliterate_toplevel(arg, ctx)) tr = tr !== nothing ? :(Chain($tr, $tr′)) : tr′ end end @@ -770,7 +838,7 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) return :(Fun(:case, $(trs...))) end end - throw(TransliterationError(ex, ctx.src)) + throw(TransliterationError(ex, ctx.line)) end diff --git a/src/nodes/from.jl b/src/nodes/from.jl index 1b8159c6..14639d69 100644 --- a/src/nodes/from.jl +++ b/src/nodes/from.jl @@ -219,10 +219,16 @@ Base.convert(::Type{SQLQuery}, source::SQLTable) = terminal(::Type{FromNode}) = true +function children!(n::FromNode, children::Vector{SQLQuery}) + if n.source isa FunctionSource + push!(children, n.source.query) + end +end + function PrettyPrinting.quoteof(n::FromNode, ctx::QuoteContext) source = n.source if source isa SQLTable - tex = get(ctx.vars, source, nothing) + tex = get(ctx.repl, source, nothing) if tex === nothing tex = quoteof(source, limit = true) end diff --git a/src/nodes/highlight.jl b/src/nodes/highlight.jl index be1aa259..02b155c1 100644 --- a/src/nodes/highlight.jl +++ b/src/nodes/highlight.jl @@ -11,6 +11,16 @@ Base.show(io::IO, esc::Esc) = print(io, esc.val) end +struct NormalWrapper + content::Any +end + +PrettyPrinting.tile(w::NormalWrapper) = + tile_expr(w.content) * literal(Esc(:normal), 0) + +PrettyPrinting.tile_expr_or_repr(w::NormalWrapper, pr = -1) = + PrettyPrinting.tile(w) + struct EscWrapper content::Any color::Symbol diff --git a/src/nodes/internal.jl b/src/nodes/internal.jl index d929460e..b423519a 100644 --- a/src/nodes/internal.jl +++ b/src/nodes/internal.jl @@ -38,7 +38,7 @@ function PrettyPrinting.quoteof(n::ResolvedNode, ctx::QuoteContext) end # Annotations added by "link" pass. -struct LinkedNode <: TabularNode +struct LinkedNode <: AbstractSQLNode refs::Vector{SQLQuery} n_ext_refs::Int @@ -129,7 +129,7 @@ terminal(::Type{FromTableNode}) = true function PrettyPrinting.quoteof(n::FromTableNode, ctx::QuoteContext) - tex = get(ctx.vars, n.table, nothing) + tex = get(ctx.repl, n.table, nothing) if tex === nothing tex = quoteof(n.table, limit = true) end @@ -283,3 +283,71 @@ terminal(::Type{IsolatedNode}) = PrettyPrinting.quoteof(n::IsolatedNode, ctx::QuoteContext) = Expr(:call, :Isolated, n.idx, quoteof(n.type)) + + +# Wraps a query generated with @funsql macro. +struct FunSQLMacroNode <: AbstractSQLNode + query::SQLQuery + ex + mod::Module + def::Union{Symbol, Nothing} + base::LineNumberNode + line::LineNumberNode + + FunSQLMacroNode(; query, ex, mod, def, base, line) = + new(query, ex, mod, def, base, line) +end + +FunSQLMacroNode(query, ex, mod, def, base, line) = + FunSQLMacroNode(; query, ex, mod, def, base, line) + +const FunSQLMacro = SQLQueryCtor{FunSQLMacroNode}(:FunSQLMacro) + +terminal(n::FunSQLMacroNode) = + terminal(n.query) + +PrettyPrinting.quoteof(n::FunSQLMacroNode, ctx::QuoteContext) = + Expr(:macrocall, Symbol("@funsql"), n.line, !ctx.limit ? n.ex : :…) + + +# Unwrap @funsql macro when displaying the query. +struct UnwrapFunSQLMacroNode <: AbstractSQLNode + depth::Union{Int} + + UnwrapFunSQLMacroNode(; depth = -1) = + new(depth) +end + +UnwrapFunSQLMacroNode(depth) = + UnwrapFunSQLMacroNode(; depth) + +const UnwrapFunSQLMacro = SQLQueryCtor{UnwrapFunSQLMacroNode}(:UnwrapFunSQLMacro) + +function PrettyPrinting.quoteof(n::UnwrapFunSQLMacroNode, ctx::QuoteContext) + ex = Expr(:call, :UnwrapFunSQLMacro) + if n.depth != -1 + push!(ex.args, n.depth) + end + ex +end + +unwrap_funsql_macro(q; depth = -1) = + q |> UnwrapFunSQLMacro(depth) + + +# Highlight a target node for error reporting. +struct HighlightTargetNode <: AbstractSQLNode + target::Any + color::Symbol + + HighlightTargetNode(; target, color) = + new(target, color) +end + +HighlightTargetNode(target, color) = + HighlightTargetNode(; target, color) + +const HighlightTarget = SQLQueryCtor{HighlightTargetNode}(:HighlightTarget) + +PrettyPrinting.quoteof(n::HighlightTargetNode, ctx::QuoteContext) = + Expr(:call, :HighlightTarget, :…, QuoteNode(n.color)) diff --git a/src/quote.jl b/src/quote.jl index 93198f7c..31052cf2 100644 --- a/src/quote.jl +++ b/src/quote.jl @@ -2,14 +2,12 @@ struct QuoteContext limit::Bool - vars::IdDict{Any, Symbol} - colors::Vector{Symbol} + repl::IdDict{Any, Any} QuoteContext(; limit = false, - vars = IdDict{Any, Symbol}(), - colors = [:normal]) = - new(limit, vars, colors) + repl = IdDict{Any, Any}()) = + new(limit, repl) end # Compact representation of a column table. diff --git a/src/resolve.jl b/src/resolve.jl index 1eb7085a..1f19f948 100644 --- a/src/resolve.jl +++ b/src/resolve.jl @@ -4,6 +4,7 @@ struct ResolveContext catalog::SQLCatalog tail::Union{SQLQuery, Nothing} path::Vector{SQLQuery} + path_subs::Dict{SQLQuery, SQLQuery} row_type::RowType cte_types::Base.ImmutableDict{Symbol, Tuple{Int, RowType}} var_types::Base.ImmutableDict{Symbol, Tuple{Int, ScalarType}} @@ -14,6 +15,7 @@ struct ResolveContext new(catalog, nothing, SQLQuery[], + Dict{SQLQuery, SQLQuery}(), EMPTY_ROW, Base.ImmutableDict{Symbol, Tuple{Int, RowType}}(), Base.ImmutableDict{Symbol, Tuple{Int, ScalarType}}(), @@ -31,6 +33,7 @@ struct ResolveContext new(ctx.catalog, tail, ctx.path, + ctx.path_subs, row_type, cte_types, var_types, @@ -39,7 +42,7 @@ struct ResolveContext end get_path(ctx::ResolveContext) = - copy(ctx.path) + SQLQuery[get(ctx.path_subs, q, q) for q in ctx.path] function row_type(q::SQLQuery) @dissect(q, Resolved(type = (local type)::RowType)) || throw(IllFormedError()) @@ -333,6 +336,24 @@ function resolve_scalar(n::FunctionNode, ctx) Resolved(ScalarType(), tail = q′) end +function rebase(q::SQLQuery, ctx::ResolveContext) + ctx.tail !== nothing || return q + q′ = + if q.tail !== nothing + SQLQuery(rebase(q.tail, ctx), q.head) + else + SQLQuery(ctx.tail, q.head) + end + ctx.path_subs[q′] = q + q′ +end + +resolve(n::FunSQLMacroNode, ctx) = + resolve(ResolveContext(ctx, tail = rebase(n.query, ctx))) + +resolve_scalar(n::FunSQLMacroNode, ctx) = + resolve_scalar(ResolveContext(ctx, tail = rebase(n.query, ctx))) + function resolve_scalar(n::GetNode, ctx) if ctx.tail !== nothing q′ = unnest(ctx.tail, Get(n.name), ctx) From 697fca0327839d1991c46db5054c5508114c3faa Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sun, 18 May 2025 18:09:36 -0500 Subject: [PATCH 08/17] Fix transliterate() to allow its use in other modules --- src/nodes.jl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/nodes.jl b/src/nodes.jl index 886472f6..06bbb176 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -639,8 +639,8 @@ function transliterate_toplevel(@nospecialize(ex), ctx) ex′ = transliterate(ex, ctx) quote let q = $ex′ - if q isa Union{SQLQuery, SQLGetQuery} - FunSQLMacro( + if q isa $(Union{SQLQuery, SQLGetQuery}) + $FunSQLMacro( q, $(QuoteNode(ex)), $(ctx.mod), @@ -667,7 +667,7 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) end elseif @dissect(ex, QuoteNode((local name)::Symbol)) # :name - return :(Var($ex)) + return :($Var($ex)) elseif ex isa Expr if @dissect(ex, Expr(:($), (local arg))) # $(...) @@ -713,32 +713,32 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) # over.name(args...) tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:call, name, args...), ctx) - return :(Chain($tr1, $tr2)) + return :($Chain($tr1, $tr2)) elseif @dissect(ex, Expr(:macrocall, Expr(:., (local over), Expr(:quote, (local ex′))), (local args)...)) # over.`name` tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:macrocall, ex′, args...), ctx) - return :(Chain($tr1, $tr2)) + return :($Chain($tr1, $tr2)) elseif @dissect(ex, Expr(:., (local over), Expr(:quote, (local arg)))) # over.`name` (Julia ≥ 1.10) tr1 = transliterate(over, ctx) tr2 = transliterate(arg, ctx) - return :(Chain($tr1, $tr2)) + return :($Chain($tr1, $tr2)) elseif @dissect(ex, Expr(:call, Expr(:macrocall, Expr(:., (local over), Expr(:quote, (local ex′))), (local args)...), (local args′)...)) # over.`name`(args...) tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:call, Expr(:macrocall, ex′, args...), args′...), ctx) - return :(Chain($tr1, $tr2)) + return :($Chain($tr1, $tr2)) elseif @dissect(ex, Expr(:call, Expr(:., (local over), Expr(:quote, (local arg))), (local args)...)) # over.`name`(args...) (Julia ≥ 1.10) tr1 = transliterate(over, ctx) tr2 = transliterate(Expr(:call, arg, args...), ctx) - return :(Chain($tr1, $tr2)) + return :($Chain($tr1, $tr2)) elseif @dissect(ex, Expr(:., (local over), QuoteNode((local name)))) # over.name tr1 = transliterate(over, ctx) tr2 = transliterate(name, ctx) - return :(Chain($tr1, $tr2)) + return :($Chain($tr1, $tr2)) elseif @dissect(ex, Expr(:call, :(=>), (local name = QuoteNode(_::Symbol)), (local arg))) # :name => arg tr = transliterate(arg, ctx) @@ -768,15 +768,15 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) tr1 = transliterate(arg1, ctx) tr2 = transliterate(arg3, ctx) tr3 = transliterate(Expr(:comparison, arg3, args...), ctx) - return :(Fun(:and, $(esc(Symbol("funsql_$arg2")))($tr1, $tr2), $tr3)) + return :($Fun(:and, $(esc(Symbol("funsql_$arg2")))($tr1, $tr2), $tr3)) elseif @dissect(ex, Expr(:(&&), (local args)...)) # &&(args...) trs = Any[transliterate(arg, ctx) for arg in args] - return :(Fun(:and, args = [$(trs...)])) + return :($Fun(:and, args = [$(trs...)])) elseif @dissect(ex, Expr(:(||), (local args)...)) # ||(args...) trs = Any[transliterate(arg, ctx) for arg in args] - return :(Fun(:or, args = [$(trs...)])) + return :($Fun(:or, args = [$(trs...)])) elseif @dissect(ex, Expr(:call, (local op = :+ || :-), (local arg = :Inf))) # ±Inf tr = transliterate(arg, ctx) @@ -810,7 +810,7 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) ctx = TransliterateContext(ctx, line = arg) else tr′ = Expr(:block, ctx.line, transliterate_toplevel(arg, ctx)) - tr = tr !== nothing ? :(Chain($tr, $tr′)) : tr′ + tr = tr !== nothing ? :($Chain($tr, $tr′)) : tr′ end end return tr @@ -818,7 +818,7 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) elseif @dissect(ex, Expr(:if, (local arg1), (local arg2))) tr1 = transliterate(arg1, ctx) tr2 = transliterate(arg2, ctx) - return :(Fun(:case, $tr1, $tr2)) + return :($Fun(:case, $tr1, $tr2)) elseif @dissect(ex, Expr(:if, (local arg1), (local arg2), (local arg3))) trs = Any[transliterate(arg1, ctx), transliterate(arg2, ctx)] @@ -835,7 +835,7 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) else push!(trs, transliterate(arg3, ctx)) end - return :(Fun(:case, $(trs...))) + return :($Fun(:case, $(trs...))) end end throw(TransliterationError(ex, ctx.line)) From 53a52db113979b21c25a93d08273b77849edffc8 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Fri, 30 May 2025 18:42:10 -0500 Subject: [PATCH 09/17] Typo --- src/link.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/link.jl b/src/link.jl index 496d2418..00015f41 100644 --- a/src/link.jl +++ b/src/link.jl @@ -26,7 +26,7 @@ struct LinkContext end function link(q::SQLQuery) - @dissect(q, (local tail) |> WithContext(catalog = (local catalog))) || throw(ILLFormedError()) + @dissect(q, (local tail) |> WithContext(catalog = (local catalog))) || throw(IllFormedError()) ctx = LinkContext(catalog) t = row_type(tail) refs = SQLQuery[] From fe41692f0304acbddb197679c4cbd2d68de4b760 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Mon, 26 May 2025 20:35:09 -0500 Subject: [PATCH 10/17] Wrap the output of execute() in FunSQL.SQLCursor() --- docs/src/test/other.md | 46 +++++++++++++++++++++++++++++--- src/connections.jl | 59 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/docs/src/test/other.md b/docs/src/test/other.md index c1bf6611..0054ba31 100644 --- a/docs/src/test/other.md +++ b/docs/src/test/other.md @@ -9,6 +9,7 @@ with the database catalog. using FunSQL: SQLConnection, SQLCatalog, SQLTable using Pkg.Artifacts, LazyArtifacts using SQLite + using Tables const DATABASE = joinpath(artifact"synpuf-10p", "synpuf-10p.sqlite") @@ -42,11 +43,50 @@ a FunSQL-specific `SQLStatement` object. DBInterface.getconnection(stmt) #-> SQLConnection( … ) - DBInterface.execute(stmt) - #-> SQLite.Query{false}( … ) +The output of the statement is wrapped in a FunSQL-specific `SQLCursor` +object. + + cr = DBInterface.execute(stmt) + #-> SQLCursor(SQLite.Query{false}( … )) + +`SQLCursor` implements standard interfaces by delegating supported methods +to the wrapped cursor object. + + eltype(cr) + #-> SQLite.Row + + for row in cr + println(row) + end + #=> + SQLite.Row{false}: + :person_id 1780 + :year_of_birth 1940 + ⋮ + =# + + DBInterface.lastrowid(cr) + #-> 0 + + Tables.schema(cr) + #=> + Tables.Schema: + :person_id Union{Missing, Int64} + :year_of_birth Union{Missing, Int64} + =# + + cr = DBInterface.execute(stmt) + Tables.rowtable(cr) + #-> @NamedTuple{ … }[(person_id = 1780, year_of_birth = 1940), … ] + + cr = DBInterface.execute(stmt) + Tables.columntable(cr) + #-> (person_id = [1780, … ], year_of_birth = [1940, … ]) DBInterface.close!(stmt) + DBInterface.close!(cr) + For a query with parameters, this allows us to specify the parameter values by name. @@ -59,7 +99,7 @@ by name. #-> SQLStatement(SQLConnection( … ), SQLite.Stmt( … ), vars = [:YEAR]) DBInterface.execute(stmt, YEAR = 1950) - #-> SQLite.Query{false}( … ) + #-> SQLCursor(SQLite.Query{false}( … )) DBInterface.close!(stmt) diff --git a/src/connections.jl b/src/connections.jl index d4bf0431..2b7f908b 100644 --- a/src/connections.jl +++ b/src/connections.jl @@ -59,6 +59,57 @@ Shorthand for [`SQLConnection`](@ref). """ const DB = SQLConnection +""" + SQLCursor(raw) + +Wraps the query result. +""" +struct SQLCursor{RawCrType} <: DBInterface.Cursor + raw::RawCrType + + SQLCursor{RawCrType}(raw::RawCrType) where {RawCrType} = + new(raw) +end + +SQLCursor(raw::RawCrType) where {RawCrType} = + SQLCursor{RawCrType}(raw) + +function Base.show(io::IO, cr::SQLCursor) + print(io, "SQLCursor(") + show(io, cr.raw) + print(io, ")") +end + +Base.eltype(cr::SQLCursor) = + eltype(cr.raw) + +Base.IteratorSize(::Type{SQLCursor{RawCrType}}) where {RawCrType} = + Base.IteratorSize(RawCrType) + +Base.length(cr::SQLCursor) = + length(cr.raw) + +Base.iterate(cr::SQLCursor, state...) = + iterate(cr.raw, state...) + +Tables.istable(::Type{SQLCursor{RawCrType}}) where {RawCrType} = + Tables.istable(RawCrType) + +Tables.rowaccess(::Type{SQLCursor{RawCrType}}) where {RawCrType} = + Tables.rowaccess(RawCrType) + +Tables.rows(cr::SQLCursor) = + Tables.rows(cr.raw) + +Tables.columnaccess(::Type{SQLCursor{RawCrType}}) where {RawCrType} = + Tables.columnaccess(RawCrType) + +Tables.columns(cr::SQLCursor) = + Tables.columns(cr.raw) + +Tables.schema(cr::SQLCursor) = + Tables.schema(cr.raw) + """ DBInterface.connect(DB{RawConnType}, args...; @@ -132,10 +183,16 @@ DBInterface.close!(conn::SQLConnection) = Execute the prepared SQL statement. """ DBInterface.execute(stmt::SQLStatement, params) = - DBInterface.execute(stmt.raw, pack(stmt.vars, params)) + SQLCursor(DBInterface.execute(stmt.raw, pack(stmt.vars, params))) DBInterface.getconnection(stmt::SQLStatement) = stmt.conn DBInterface.close!(stmt::SQLStatement) = DBInterface.close!(stmt.raw) + +DBInterface.lastrowid(cr::SQLCursor) = + DBInterface.lastrowid(cr.raw) + +DBInterface.close!(cr::SQLCursor) = + DBInterface.close!(cr.raw) From fc03cfa6193ba3fb5201fd52f0b4c454a950d996 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sat, 31 May 2025 07:50:07 -0500 Subject: [PATCH 11/17] Two-argument funsql macro for executing queries --- src/nodes.jl | 140 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 99 insertions(+), 41 deletions(-) diff --git a/src/nodes.jl b/src/nodes.jl index 06bbb176..ab470726 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -628,11 +628,51 @@ struct TransliterateContext end """ -Convenient notation for assembling FunSQL queries. + @funsql ex + +Assemble a FunSQL query using convenient macro notation. """ macro funsql(ex) ctx = TransliterateContext(__module__, __source__) - transliterate_toplevel(ex, ctx) + if transliterate_is_definition(ex) + transliterate_definition(ex, ctx) + else + transliterate_toplevel(ex, ctx) + end +end + +""" + @funsql db ex args... + +Assemble and execute a FunSQL query using convenient macro notation. +""" +macro funsql(db, ex, args...) + ctx = TransliterateContext(__module__, __source__) + q = transliterate_toplevel(ex, ctx) + args = Any[transliterate_parameter(arg, ctx) for arg in args] + Expr(:call, DBInterface.execute, esc(db), q, args...) +end + +function transliterate_is_definition(@nospecialize(ex)) + ex isa Expr || return false + if @dissect(ex, Expr(:(=), Expr(:call, _...), _)) + return true + end + if @dissect(ex, Expr(:macrocall, GlobalRef($Core, $(Symbol("@doc"))), _, _, (local arg))) + if @dissect(arg, ::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), _, _)) + return true + end + if @dissect(arg, Expr(:(=), Expr(:call, _...), _)) + return true + end + end + if @dissect(ex, Expr(:block, (local args)...)) + for arg in args + !(arg isa LineNumberNode) || continue + return transliterate_is_definition(arg) + end + end + return false end function transliterate_toplevel(@nospecialize(ex), ctx) @@ -672,23 +712,6 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) if @dissect(ex, Expr(:($), (local arg))) # $(...) return esc(arg) - elseif @dissect(ex, Expr(:macrocall, (local ref = GlobalRef($Core, $(Symbol("@doc")))), (local ln)::LineNumberNode, (local doc), (local arg))) - # "..." ... - if @dissect(arg, (local name)::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String)) - arg = Symbol("funsql_$name") - else - ctx = TransliterateContext(ctx, line = ln) - arg = transliterate(arg, ctx) - end - return Expr(:macrocall, ref, ln, doc, arg) - elseif @dissect(ex, Expr(:(=), Expr(:call, (local name)::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String), (local args)...), (local body))) - # name(args...) = body - ctx = TransliterateContext(ctx, decl = true) - trs = Any[transliterate(arg, ctx) for arg in args] - ctx = TransliterateContext(ctx, def = Symbol(name), decl = false) - return Expr(:(=), - :($(esc(Symbol("funsql_$name")))($(trs...))), - transliterate_toplevel(body, ctx)) elseif @dissect(ex, Expr(:(=), (local name)::Symbol, (local arg))) # name = arg return Expr(:(=), esc(name), transliterate(arg, ctx)) @@ -777,6 +800,11 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) # ||(args...) trs = Any[transliterate(arg, ctx) for arg in args] return :($Fun(:or, args = [$(trs...)])) + elseif @dissect(ex, Expr(:(:=), (local arg1), (local arg2))) + # arg1 := arg2 + tr1 = transliterate(arg1, ctx) + tr2 = transliterate(arg2, ctx) + return :($(esc(Symbol("funsql_:=")))($tr1, $tr2)) elseif @dissect(ex, Expr(:call, (local op = :+ || :-), (local arg = :Inf))) # ±Inf tr = transliterate(arg, ctx) @@ -791,30 +819,16 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) return :($(esc(Symbol("funsql_$name")))($(trs...))) elseif @dissect(ex, Expr(:block, (local args)...)) # begin; args...; end - if all(@dissect(arg, ::LineNumberNode || Expr(:(=), _...) || Expr(:macrocall, GlobalRef($Core, $(Symbol("@doc"))), _...)) - for arg in args) - trs = Any[] - for arg in args - if arg isa LineNumberNode - ctx = TransliterateContext(ctx, base = arg, line = arg) - push!(trs, arg) - else - push!(trs, transliterate(arg, ctx)) - end - end - return Expr(:block, trs...) - else - tr = nothing - for arg in args - if arg isa LineNumberNode - ctx = TransliterateContext(ctx, line = arg) - else - tr′ = Expr(:block, ctx.line, transliterate_toplevel(arg, ctx)) - tr = tr !== nothing ? :($Chain($tr, $tr′)) : tr′ - end + tr = nothing + for arg in args + if arg isa LineNumberNode + ctx = TransliterateContext(ctx, line = arg) + else + tr′ = Expr(:block, ctx.line, transliterate_toplevel(arg, ctx)) + tr = tr !== nothing ? :($Chain($tr, $tr′)) : tr′ end - return tr end + return tr elseif @dissect(ex, Expr(:if, (local arg1), (local arg2))) tr1 = transliterate(arg1, ctx) tr2 = transliterate(arg2, ctx) @@ -841,6 +855,50 @@ function transliterate(@nospecialize(ex), ctx::TransliterateContext) throw(TransliterationError(ex, ctx.line)) end +function transliterate_definition(@nospecialize(ex), ctx) + if ex isa Expr + if @dissect(ex, Expr(:macrocall, (local ref = GlobalRef($Core, $(Symbol("@doc")))), (local ln)::LineNumberNode, (local doc), (local arg))) + # "..." ... + if @dissect(arg, (local name)::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String)) + arg = Symbol("funsql_$name") + else + ctx = TransliterateContext(ctx, line = ln) + arg = transliterate_definition(arg, ctx) + end + return Expr(:macrocall, ref, ln, doc, arg) + elseif @dissect(ex, Expr(:(=), Expr(:call, (local name)::Symbol || Expr(:macrocall, GlobalRef($Core, $(Symbol("@cmd"))), ::LineNumberNode, (local name)::String), (local args)...), (local body))) + # name(args...) = body + ctx = TransliterateContext(ctx, decl = true) + trs = Any[transliterate(arg, ctx) for arg in args] + ctx = TransliterateContext(ctx, def = Symbol(name), decl = false) + return Expr(:(=), + :($(esc(Symbol("funsql_$name")))($(trs...))), + transliterate_toplevel(body, ctx)) + elseif @dissect(ex, Expr(:block, (local args)...)) + # begin; args...; end + trs = Any[] + for arg in args + if arg isa LineNumberNode + ctx = TransliterateContext(ctx, base = arg, line = arg) + push!(trs, arg) + else + push!(trs, transliterate_definition(arg, ctx)) + end + end + return Expr(:block, trs...) + end + end + throw(TransliterationError(ex, ctx.line)) +end + +function transliterate_parameter(@nospecialize(ex), ctx) + if @dissect(ex, Expr(:kw || :(=), (local key), (local arg))) + Expr(:kw, esc(key), esc(arg)) + else + esc(ex) + end +end + # Concrete node types. From 055ce337f3fd1fe7f8090674c1df8b346a23729a Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sat, 31 May 2025 08:29:39 -0500 Subject: [PATCH 12/17] Fix broken tests caused by output variations --- docs/src/test/other.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/src/test/other.md b/docs/src/test/other.md index 0054ba31..bdb27c43 100644 --- a/docs/src/test/other.md +++ b/docs/src/test/other.md @@ -76,12 +76,16 @@ to the wrapped cursor object. =# cr = DBInterface.execute(stmt) - Tables.rowtable(cr) - #-> @NamedTuple{ … }[(person_id = 1780, year_of_birth = 1940), … ] + display(Tables.rowtable(cr)) + #=> + 10-element Vector{@NamedTuple{ … }}: + (person_id = 1780, year_of_birth = 1940) + ⋮ + =# cr = DBInterface.execute(stmt) - Tables.columntable(cr) - #-> (person_id = [1780, … ], year_of_birth = [1940, … ]) + display(Tables.columntable(cr)) + #-> (person_id = …[1780, … ], year_of_birth = …[1940, … ]) DBInterface.close!(stmt) From fc54844ff7eec0c90a54801defbb0ed0fdec5f32 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Tue, 10 Jun 2025 10:06:42 -0500 Subject: [PATCH 13/17] Implement label() for FunSQLMacroNode --- src/nodes/internal.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/nodes/internal.jl b/src/nodes/internal.jl index b423519a..91f866fd 100644 --- a/src/nodes/internal.jl +++ b/src/nodes/internal.jl @@ -309,6 +309,9 @@ terminal(n::FunSQLMacroNode) = PrettyPrinting.quoteof(n::FunSQLMacroNode, ctx::QuoteContext) = Expr(:macrocall, Symbol("@funsql"), n.line, !ctx.limit ? n.ex : :…) +label(n::FunSQLMacroNode) = + label(n.query) + # Unwrap @funsql macro when displaying the query. struct UnwrapFunSQLMacroNode <: AbstractSQLNode From dc3613ad6048f5682c6b19717897cf34df06fb2e Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sat, 14 Jun 2025 11:44:22 -0500 Subject: [PATCH 14/17] Add and() and or() as aliases for && and || in funsql macro --- src/FunSQL.jl | 2 ++ src/nodes/function.jl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/FunSQL.jl b/src/FunSQL.jl index d3f00af6..fb6a5ca8 100644 --- a/src/FunSQL.jl +++ b/src/FunSQL.jl @@ -27,6 +27,7 @@ export var"funsql_∈", var"funsql_∉", funsql_agg, + funsql_and, funsql_append, funsql_as, funsql_asc, @@ -73,6 +74,7 @@ export funsql_not_like, funsql_nth_value, funsql_ntile, + funsql_or, funsql_order, funsql_over, funsql_partition, diff --git a/src/nodes/function.jl b/src/nodes/function.jl index f450bcc8..aae32314 100644 --- a/src/nodes/function.jl +++ b/src/nodes/function.jl @@ -222,6 +222,7 @@ const var"funsql_*" = FunClosure("*") const var"funsql_/" = FunClosure("/") const var"funsql_∈" = FunClosure(:in) const var"funsql_∉" = FunClosure(:not_in) +const funsql_and = FunClosure(:and) const funsql_between = FunClosure(:between) const funsql_case = FunClosure(:case) const funsql_cast = FunClosure(:cast) @@ -235,6 +236,7 @@ const funsql_in = FunClosure(:in) const funsql_is_not_null = FunClosure(:is_not_null) const funsql_is_null = FunClosure(:is_null) const funsql_like = FunClosure(:like) +const funsql_or = FunClosure(:or) const funsql_not_between = FunClosure(:not_between) const funsql_not_exists = FunClosure(:not_exists) const funsql_not_in = FunClosure(:not_in) From d6f3c306b34b8f4e8c7093fc5de2566740550455 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sat, 28 Jun 2025 09:22:37 -0500 Subject: [PATCH 15/17] Interpret Get() in tabular context as From() --- src/resolve.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/resolve.jl b/src/resolve.jl index 1f19f948..73bf67ac 100644 --- a/src/resolve.jl +++ b/src/resolve.jl @@ -354,6 +354,14 @@ resolve(n::FunSQLMacroNode, ctx) = resolve_scalar(n::FunSQLMacroNode, ctx) = resolve_scalar(ResolveContext(ctx, tail = rebase(n.query, ctx))) +function resolve(n::GetNode, ctx) + if ctx.tail !== nothing + q′ = unnest(ctx.tail, Get(n.name), ctx) + return resolve(q′, ctx) + end + resolve(FromNode(n.name), ctx) +end + function resolve_scalar(n::GetNode, ctx) if ctx.tail !== nothing q′ = unnest(ctx.tail, Get(n.name), ctx) From 702412bb2ab95fade9c334e69018c2725874a5a8 Mon Sep 17 00:00:00 2001 From: Kyrylo Simonov Date: Sat, 19 Jul 2025 20:01:55 -0500 Subject: [PATCH 16/17] Fix label() for FunSQLMacroNode --- src/nodes.jl | 14 ++++++++------ src/nodes/internal.jl | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/nodes.jl b/src/nodes.jl index ab470726..908385c9 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -54,17 +54,19 @@ terminal(q::SQLQuery) = Chain(q′, q) = convert(SQLQuery, q)(q′) -label(q::SQLQuery) = - @something label(q.head) label(q.tail) +function label(q::SQLQuery; default = :_) + l = label(q.head) + l !== nothing ? l : label(q.tail; default) +end label(n::AbstractSQLNode) = nothing -label(::Nothing) = - :_ +label(::Nothing; default = :_) = + default -label(q) = - label(convert(SQLQuery, q)) +label(q; default = :_) = + label(convert(SQLQuery, q); default) # A variant of SQLQuery for assembling a chain of identifiers. diff --git a/src/nodes/internal.jl b/src/nodes/internal.jl index 91f866fd..1b5ec514 100644 --- a/src/nodes/internal.jl +++ b/src/nodes/internal.jl @@ -310,7 +310,7 @@ PrettyPrinting.quoteof(n::FunSQLMacroNode, ctx::QuoteContext) = Expr(:macrocall, Symbol("@funsql"), n.line, !ctx.limit ? n.ex : :…) label(n::FunSQLMacroNode) = - label(n.query) + label(n.query; default = nothing) # Unwrap @funsql macro when displaying the query. From 3fb3de11dcec001024572afe24938b2dc8aa96a6 Mon Sep 17 00:00:00 2001 From: piever Date: Thu, 30 Jul 2026 10:30:04 +0200 Subject: [PATCH 17/17] support OrderedCollections v2 --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 2d76c69e..1b7fd3e4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "FunSQL" uuid = "cf6cc811-59f4-4a10-b258-a8547a8f6407" authors = ["Kirill Simonov ", "Clark C. Evans "] -version = "0.15.0" +version = "0.15.1" [deps] DBInterface = "a10d1c49-ce27-4219-8d33-6db1a4562965" @@ -16,7 +16,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" DBInterface = "2.5" DataAPI = "1.13" LRUCache = "1.3" -OrderedCollections = "1.4" +OrderedCollections = "1.4, 2" PrettyPrinting = "0.3.2, 0.4" Tables = "1.6" julia = "1.10"