From 63c50ad28ced523eb492116a69538d47fa50387d Mon Sep 17 00:00:00 2001 From: jeredmasters Date: Mon, 23 Apr 2018 13:40:52 +0800 Subject: [PATCH 1/5] Added support for lateral joins --- .gitignore | 1 + lib/sql_dust.ex | 4 +++- lib/sql_dust/utils/compose_utils.ex | 17 ++++++++++++++++- lib/sql_dust/utils/join_utils.ex | 11 +++++++++++ test/sql_dust/lateral_join_test.exs | 16 ++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/sql_dust/lateral_join_test.exs diff --git a/.gitignore b/.gitignore index 85bcb1f..75c3b7f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /deps erl_crash.dump *.ez +.elixir_ls \ No newline at end of file diff --git a/lib/sql_dust.ex b/lib/sql_dust.ex index f4eca6e..7d95ca6 100644 --- a/lib/sql_dust.ex +++ b/lib/sql_dust.ex @@ -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. @@ -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 diff --git a/lib/sql_dust/utils/compose_utils.ex b/lib/sql_dust/utils/compose_utils.ex index 92d5967..0b8b71a 100644 --- a/lib/sql_dust/utils/compose_utils.ex +++ b/lib/sql_dust/utils/compose_utils.ex @@ -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 @@ -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) diff --git a/lib/sql_dust/utils/join_utils.ex b/lib/sql_dust/utils/join_utils.ex index e9d93a1..238c619 100644 --- a/lib/sql_dust/utils/join_utils.ex +++ b/lib/sql_dust/utils/join_utils.ex @@ -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 diff --git a/test/sql_dust/lateral_join_test.exs b/test/sql_dust/lateral_join_test.exs new file mode 100644 index 0000000..2133205 --- /dev/null +++ b/test/sql_dust/lateral_join_test.exs @@ -0,0 +1,16 @@ +defmodule SqlDust.LateralJoinTest do + use ExUnit.Case + doctest SqlDust.Query + 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) + + %{join_lateral: joins} = query_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 + +end From ba6086d9e2c6a14062e19b46a638a6a228992b7f Mon Sep 17 00:00:00 2001 From: jeredmasters Date: Tue, 24 Apr 2018 15:55:33 +0800 Subject: [PATCH 2/5] implemented lateral aliases --- lib/sql_dust.ex | 2 +- lib/sql_dust/utils/path_utils.ex | 23 +++++++++++++++++++++-- test/sql_dust/lateral_join_test.exs | 5 +---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/sql_dust.ex b/lib/sql_dust.ex index 7d95ca6..c446147 100644 --- a/lib/sql_dust.ex +++ b/lib/sql_dust.ex @@ -100,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]|$)/) diff --git a/lib/sql_dust/utils/path_utils.ex b/lib/sql_dust/utils/path_utils.ex index 64f34cf..9f8af2c 100644 --- a/lib/sql_dust/utils/path_utils.ex +++ b/lib/sql_dust/utils/path_utils.ex @@ -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 @@ -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) diff --git a/test/sql_dust/lateral_join_test.exs b/test/sql_dust/lateral_join_test.exs index 2133205..42d2b64 100644 --- a/test/sql_dust/lateral_join_test.exs +++ b/test/sql_dust/lateral_join_test.exs @@ -1,16 +1,13 @@ defmodule SqlDust.LateralJoinTest do use ExUnit.Case - doctest SqlDust.Query + 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) - %{join_lateral: joins} = query_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 end From 46ce64f8928eaca81900bd30db033878b524ff4d Mon Sep 17 00:00:00 2001 From: jeredmasters Date: Tue, 24 Apr 2018 16:08:01 +0800 Subject: [PATCH 3/5] Added another test, added documentation --- README.md | 31 +++++++++++++++++++++++++++++ test/sql_dust/lateral_join_test.exs | 7 +++++++ 2 files changed, 38 insertions(+) diff --git a/README.md b/README.md index 9d7643d..8ccec03 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,37 @@ 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. +[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 > a0.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: diff --git a/test/sql_dust/lateral_join_test.exs b/test/sql_dust/lateral_join_test.exs index 42d2b64..ed2b842 100644 --- a/test/sql_dust/lateral_join_test.exs +++ b/test/sql_dust/lateral_join_test.exs @@ -10,4 +10,11 @@ defmodule SqlDust.LateralJoinTest do 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 From 7a4b5bf55153cca1987e851ff1cd5af149f0ad4d Mon Sep 17 00:00:00 2001 From: jeredmasters Date: Tue, 24 Apr 2018 16:15:43 +0800 Subject: [PATCH 4/5] fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ccec03..8ca9feb 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ City City |> select("id, name") |> join_lateral("first_city", sub_query) - |> where("date_started > a0.date_started") + |> where("date_started > first_city.date_started") |> to_sql |> elem(0) |> IO.puts From 2438d9082f6caa08d42b192d9a97e1863e2f1b19 Mon Sep 17 00:00:00 2001 From: jeredmasters Date: Tue, 24 Apr 2018 16:16:27 +0800 Subject: [PATCH 5/5] added more infor to read me --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ca9feb..2704f74 100644 --- a/README.md +++ b/README.md @@ -249,8 +249,10 @@ To have it clear: ### 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. -[Read more here](https://www.postgresql.org/docs/9.4/static/queries-table-expressions.html) +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 =