Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/deps
erl_crash.dump
*.ez
.elixir_ls
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,39 @@ To have it clear:
* SqlDust will return a tuple of **two** elements when NOT having passed variables
* SqlDust will return a tuple of **three** elements when having passed variables

### Lateral Joins

Postgres 9.3 introduced lateral joins; a very convenient way to compare and filter your query based on other information in the database.
A lateral subquery gets executed once and it's result is available for the outer query to compare to.

[Read more here](https://www.postgresql.org/docs/9.4/static/queries-table-expressions.html)

```elixir
sub_query =
City
|> select("date_started")
|> where("id = 5")

City
|> select("id, name")
|> join_lateral("first_city", sub_query)
|> where("date_started > first_city.date_started")
|> to_sql
|> elem(0)
|> IO.puts

"""
SELECT `c`.`id`, `c`.`name`, `c`.`date_started`,
FROM `cities` `c`
LEFT JOIN LATERAL (
SELECT `c`.`date_started`
FROM `cities` `c`
WHERE (`c`.`id` = 1)
) AS first_city ON TRUE
WHERE (`c`.`date_started` > first_city."date_started")
"""
```

## Testing

Run the following command for testing:
Expand Down
6 changes: 4 additions & 2 deletions lib/sql_dust.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule SqlDust do
import SqlDust.JoinUtils
alias SqlDust.MapUtils

defstruct [:select, :from, :join_on, :where, :group_by, :order_by, :limit, :offset, :unique, :schema, :variables, :adapter]
defstruct [:select, :from, :join_on, :join_lateral, :where, :group_by, :order_by, :limit, :offset, :unique, :schema, :variables, :adapter]

@moduledoc """
SqlDust is a module that generates SQL queries as intuitively as possible.
Expand Down Expand Up @@ -75,6 +75,8 @@ defmodule SqlDust do
|> Enum.map(fn(path) -> derive_joins(path, options) end)
|> List.flatten

joins = joins ++ derive_lateral_joins(options)

Map.put options, :joins, joins
end

Expand All @@ -98,7 +100,7 @@ defmodule SqlDust do

{having, where} =
where
|> Enum.partition(fn([sql | _]) ->
|> Enum.split_with(fn([sql | _]) ->
sql = sanitize_sql(sql)
Enum.any?(options.aliases, fn(sql_alias) ->
String.match?(sql, ~r/(^|[^\.\w])#{sql_alias}([^\.\w]|$)/)
Expand Down
17 changes: 16 additions & 1 deletion lib/sql_dust/utils/compose_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ defmodule SqlDust.ComposeUtils do
def join_on(statements) do join_on(options(), statements) end
def join_on(options, statements) do append(options, :join_on, statements, false) end

def join_lateral(name, sub_dust) do join_lateral(options(), name, sub_dust) end
def join_lateral(options, name, sub_dust) do append(options, :join_lateral, {name, sub_dust}, false) end

def where(statements) do where(options(), statements) end
def where(options, statements) do append(options, :where, statements, false) end

Expand Down Expand Up @@ -65,9 +68,21 @@ defmodule SqlDust.ComposeUtils do

from = options.from
schema = options.schema
options = Map.take(options, [:select, :join_on, :where, :group_by, :order_by, :limit, :offset, :unique, :variables, :adapter])
options = Map.take(options, [:select, :join_on, :join_lateral, :where, :group_by, :order_by, :limit, :offset, :unique, :variables, :adapter])
|> Enum.reduce(%{from: options.from}, fn
({_key, nil}, map) -> map
({:join_lateral, joins}, map) ->
Map.put(map, :join_lateral,
Enum.map(joins, fn

{name, %SqlDust{} = sub_dust} ->
{name, to_sql(sub_dust) |> elem(0)}

{name, sub_query} ->
{name, sub_query}

end
))
({key, value}, map) -> Map.put(map, key, value)
end)

Expand Down
11 changes: 11 additions & 0 deletions lib/sql_dust/utils/join_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ defmodule SqlDust.JoinUtils do
end)
end

def derive_lateral_joins(%{join_lateral: joins}) when not is_nil(joins) do
joins
|> Enum.map(fn({name, sub_query}) ->
{:has_one, ["LEFT JOIN LATERAL (", sub_query, ") AS", name, "ON TRUE"] |> Enum.join(" ")}
end)
end

def derive_lateral_joins(_) do
[]
end

defp additional_join_conditions(path, %{join_on: join_on} = options) when is_bitstring(join_on) do
additional_join_conditions(path, %{options | join_on: [join_on]})
end
Expand Down
23 changes: 21 additions & 2 deletions lib/sql_dust/utils/path_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,27 @@ defmodule SqlDust.PathUtils do

excluded = format_excluded_aliases(excluded, aliases, options)

{sql, aliases, excluded}
{sql, aliases, excluded ++ scan_and_format_lateral_fields(sql, options)}
end

defp scan_and_format_lateral_fields(sql, %{join_lateral: laterals}) when is_binary(sql) do
laterals
|> Enum.reduce([], fn {lateral_alias, _subquery}, acc ->
acc ++
(
"\\b#{lateral_alias}\\.(\\w+)"
|> Regex.compile()
|> elem(1)
|> Regex.scan(sql)
|> Enum.map(fn [full_path, field] ->
[Regex.compile(full_path) |> elem(1), "#{lateral_alias}.\"#{field}\""]
end)
)
end)
end

defp scan_and_format_lateral_fields(_, _) do
[]
end

defp format_excluded_aliases(excluded, aliases, options) do
Expand Down Expand Up @@ -149,7 +169,6 @@ defmodule SqlDust.PathUtils do
end

defp do_dissect_path(path, options) do

quotation_symbol = quotation_mark(options)
split_on_dot_outside_quotation_mark = ~r/\.(?=(?:[^#{quotation_symbol}]*#{quotation_symbol}[^#{quotation_symbol}]*#{quotation_symbol})*[^#{quotation_symbol}]*$)/
segments = String.split(path, split_on_dot_outside_quotation_mark)
Expand Down
20 changes: 20 additions & 0 deletions test/sql_dust/lateral_join_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule SqlDust.LateralJoinTest do
use ExUnit.Case

import SqlDust.Query

test "laterl join returns join" do
sub_dust = select("id") |> from("account") |> where("id = 1")
query_dust = select("id") |> from("users") |> join_lateral("sub_table", sub_dust)

assert query_dust |> to_sql |> elem(0) == "SELECT `u`.`id`\nFROM `users` `u`\nLEFT JOIN LATERAL ( SELECT `a`.`id`\nFROM `account` `a`\nWHERE (`a`.`id` = 1)\n ) AS sub_table ON TRUE\n"
end


test "laterl join alias doesn't get ignored" do
sub_dust = select("id", "user_id") |> from("account") |> where("id = 1")
query_dust = select("id") |> from("users") |> join_lateral("sub_table", sub_dust) |> where("sub_table.user_id = id")

assert query_dust |> to_sql |> elem(0) == "SELECT `u`.`id`\nFROM `users` `u`\nLEFT JOIN LATERAL ( SELECT `a`.`user_id`\nFROM `account` `a`\nWHERE (`a`.`id` = 1)\n ) AS sub_table ON TRUE\nWHERE (sub_table.\"user_id\" = `u`.`id`)\n"
end
end