Skip to content

Commit

Permalink
Define: add 'before' and 'after' parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
xitology committed Jun 15, 2024
1 parent 4011c7a commit 2459dd0
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 19 deletions.
123 changes: 123 additions & 0 deletions docs/src/test/nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,129 @@ a simple reference.
FROM "person" AS "person_1"
=#

`Define` allows you to insert columns at the beginning or at the end of
the column list.

q = From(person) |>
Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
before = true)

display(q)
#=>
let person = SQLTable(:person, …),
q1 = From(person),
q2 = q1 |>
Define(Fun."-"(Fun.now(), Get.birth_datetime) |> As(:age),
Get.birth_datetime,
before = true)
q2
end
=#

print(render(q))
#=>
SELECT
(now() - "person_1"."birth_datetime") AS "age",
"person_1"."birth_datetime",
"person_1"."person_id",
"person_1"."gender_concept_id",
"person_1"."year_of_birth",
"person_1"."month_of_birth",
"person_1"."day_of_birth",
"person_1"."location_id"
FROM "person" AS "person_1"
=#

q = From(person) |>
Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
after = true)

display(q)
#=>
let person = SQLTable(:person, …),
q1 = From(person),
q2 = q1 |>
Define(Fun."-"(Fun.now(), Get.birth_datetime) |> As(:age),
Get.birth_datetime,
after = true)
q2
end
=#

print(render(q))
#=>
SELECT
"person_1"."person_id",
"person_1"."gender_concept_id",
"person_1"."year_of_birth",
"person_1"."month_of_birth",
"person_1"."day_of_birth",
"person_1"."location_id",
(now() - "person_1"."birth_datetime") AS "age",
"person_1"."birth_datetime"
FROM "person" AS "person_1"
=#

It can also insert columns in front of or right after a specified column.

q = From(person) |>
Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
before = :year_of_birth)

print(render(q))
#=>
SELECT
"person_1"."person_id",
"person_1"."gender_concept_id",
(now() - "person_1"."birth_datetime") AS "age",
"person_1"."birth_datetime",
"person_1"."year_of_birth",
"person_1"."month_of_birth",
"person_1"."day_of_birth",
"person_1"."location_id"
FROM "person" AS "person_1"
=#

q = From(person) |>
Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
after = :birth_datetime)

print(render(q))
#=>
SELECT
"person_1"."person_id",
"person_1"."gender_concept_id",
"person_1"."year_of_birth",
"person_1"."month_of_birth",
"person_1"."day_of_birth",
(now() - "person_1"."birth_datetime") AS "age",
"person_1"."birth_datetime",
"person_1"."location_id"
FROM "person" AS "person_1"
=#

It is an error to set both `before` and `after` or to refer to a non-existent
column.

q = From(person) |>
Define(before = true, after = true)

print(render(q))
#=>
ERROR: DomainError with (before = true, after = true):
only one of `before` and `after` could be set
=#

q = Define(before = :person_id)

print(render(q))
#=>
ERROR: FunSQL.ReferenceError: cannot find `person_id` in:
let q1 = Define(before = :person_id)
q1
end
=#

`Define` has no effect if none of the defined fields are used in the query.

q = From(person) |>
Expand Down
43 changes: 30 additions & 13 deletions src/nodes/define.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,39 @@
mutable struct DefineNode <: TabularNode
over::Union{SQLNode, Nothing}
args::Vector{SQLNode}
before::Union{Symbol, Bool}
after::Union{Symbol, Bool}
label_map::OrderedDict{Symbol, Int}

function DefineNode(; over = nothing, args = [], label_map = nothing)
function DefineNode(; over = nothing, args = [], before = nothing, after = nothing, label_map = nothing)
if label_map !== nothing
new(over, args, label_map)
n = new(over, args, something(before, false), something(after, false), label_map)
else
n = new(over, args, OrderedDict{Symbol, Int}())
n = new(over, args, something(before, false), something(after, false), OrderedDict{Symbol, Int}())
populate_label_map!(n)
n
end
if (n.before isa Symbol || n.before) && (n.after isa Symbol || n.after)
throw(DomainError((before = n.before, after = n.after), "only one of `before` and `after` could be set"))
end
n
end
end

DefineNode(args...; over = nothing) =
DefineNode(over = over, args = SQLNode[args...])
DefineNode(args...; over = nothing, before = nothing, after = nothing) =
DefineNode(over = over, args = SQLNode[args...], before = before, after = after)

"""
Define(; over; args = [])
Define(args...; over)
Define(; over; args = [], before = nothing, after = nothing)
Define(args...; over, before = nothing, after = nothing)
The `Define` node adds or replaces output columns.
By default, new columns are added at the end of the column list while replaced
columns retain their position. Set `after = true` (`after = <column>`) to add
both new and replaced columns at the end (after a specified column).
Alternatively, set `before = true` (`before = <column>`) to add both new and
replaced columns at the front (before the specified column).
# Examples
*Show patients who are at least 16 years old.*
Expand All @@ -33,19 +44,19 @@ The `Define` node adds or replaces output columns.
julia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);
julia> q = From(:person) |>
Define(:age => Fun.now() .- Get.birth_datetime) |>
Define(:age => Fun.now() .- Get.birth_datetime, before = :birth_datetime) |>
Where(Get.age .>= "16 years");
julia> print(render(q, tables = [person]))
SELECT
"person_2"."person_id",
"person_2"."birth_datetime",
"person_2"."age"
"person_2"."age",
"person_2"."birth_datetime"
FROM (
SELECT
"person_1"."person_id",
"person_1"."birth_datetime",
(now() - "person_1"."birth_datetime") AS "age"
(now() - "person_1"."birth_datetime") AS "age",
"person_1"."birth_datetime"
FROM "person" AS "person_1"
) AS "person_2"
WHERE ("person_2"."age" >= '16 years')
Expand Down Expand Up @@ -78,6 +89,12 @@ dissect(scr::Symbol, ::typeof(Define), pats::Vector{Any}) =

function PrettyPrinting.quoteof(n::DefineNode, ctx::QuoteContext)
ex = Expr(:call, nameof(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
Expand Down
38 changes: 32 additions & 6 deletions src/resolve.jl
Original file line number Diff line number Diff line change
Expand Up @@ -206,18 +206,44 @@ end
function resolve(n::DefineNode, ctx)
over′ = resolve(n.over, ctx)
t = row_type(over′)
anchor =
n.before isa Symbol ? n.before :
n.before && !isempty(t.fields) ? first(first(t.fields)) :
n.after isa Symbol ? n.after :
n.after && !isempty(t.fields) ? first(last(t.fields)) :
nothing
if anchor !== nothing && !haskey(t.fields, anchor)
throw(ReferenceError(REFERENCE_ERROR_TYPE.UNDEFINED_NAME, name = anchor, path = get_path(ctx)))
end
before = n.before isa Symbol || n.before
after = n.after isa Symbol || n.after
args′ = resolve_scalar(n.args, ctx, t)
fields = FieldTypeMap()
for (f, ft) in t.fields
i = get(n.label_map, f, nothing)
if i !== nothing
ft = type(args′[i])
if f === anchor
if after && i === nothing
fields[f] = ft
end
for (l, j) in n.label_map
fields[l] = type(args′[j])
end
if before && i === nothing
fields[f] = ft
end
elseif i !== nothing
if anchor === nothing
fields[f] = type(args′[i])
end
else
fields[f] = ft
end
fields[f] = ft
end
for (f, i) in n.label_map
if !haskey(fields, f)
fields[f] = type(args′[i])
if anchor === nothing
for (l, j) in n.label_map
if !haskey(fields, l)
fields[l] = type(args′[j])
end
end
end
n′ = Define(over = over′, args = args′, label_map = n.label_map)
Expand Down

0 comments on commit 2459dd0

Please sign in to comment.