Skip to content

Commit b121291

Browse files
Support values lists (#552)
Co-authored-by: narrowtux <narrow.m@gmail.com>
1 parent 70937d3 commit b121291

10 files changed

Lines changed: 287 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
- uses: earthly/actions-setup@v1
4848
- uses: actions/checkout@v3
4949
- name: test ecto_sql
50-
run: earthly -P --ci --build-arg ELIXIR_BASE=${{matrix.elixirbase}} --build-arg POSTGRES=${{matrix.postgres}} +integration-test-mysql
50+
run: earthly -P --ci --build-arg ELIXIR_BASE=${{matrix.elixirbase}} --build-arg MYSQL=${{matrix.mysql}} +integration-test-mysql
5151
test-mssql:
5252
name: mssql integration test
5353
runs-on: ubuntu-latest

Earthfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ integration-test-postgres:
7171
ELSE IF [ "$POSTGRES" = "15.0" ]
7272
# for 15.0 we need an upgraded version of pg_dump;
7373
# alpine 3.17 does not come with the postgres 15 client by default;
74-
# we must first update the public keys for the packages because they
74+
# we must first update the public keys for the packages because they
7575
# might have been rotated since our image was built
7676
RUN apk add -X https://dl-cdn.alpinelinux.org/alpine/v3.17/main -u alpine-keys
7777
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.17/main' >> /etc/apk/repositories

integration_test/myxql/test_helper.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ excludes = [
108108
if Version.match?(version, ">= 8.0.0") do
109109
ExUnit.configure(exclude: excludes)
110110
else
111-
ExUnit.configure(exclude: [:rename_column | excludes])
111+
ExUnit.configure(exclude: [:values_list, :rename_column | excludes])
112112
end
113113

114114
:ok = Ecto.Migrator.up(TestRepo, 0, Ecto.Integration.Migration, log: false)

lib/ecto/adapters/myxql/connection.ex

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,10 @@ if Code.ensure_loaded?(MyXQL) do
704704
|> parens_for_select
705705
end
706706

707+
defp expr({:values, _, [types, _idx, num_rows]}, _, query) do
708+
[?(, values_list(types, num_rows, query), ?)]
709+
end
710+
707711
defp expr({:literal, _, [literal]}, _sources, _query) do
708712
quote_name(literal)
709713
end
@@ -832,6 +836,23 @@ if Code.ensure_loaded?(MyXQL) do
832836
error!(query, "unsupported expression: #{inspect(expr)}")
833837
end
834838

839+
defp values_list(types, num_rows, query) do
840+
rows = Enum.to_list(1..num_rows)
841+
842+
[
843+
"VALUES ",
844+
intersperse_map(rows, ?,, fn _ ->
845+
["ROW(", values_expr(types, query), ?)]
846+
end)
847+
]
848+
end
849+
850+
defp values_expr(types, query) do
851+
intersperse_map(types, ?,, fn {_field, type} ->
852+
["CAST(", ??, " AS ", ecto_cast_to_db(type, query), ?)]
853+
end)
854+
end
855+
835856
defp interval(count, "millisecond", sources, query) do
836857
["INTERVAL (", expr(count, sources, query) | " * 1000) microsecond"]
837858
end
@@ -870,6 +891,9 @@ if Code.ensure_loaded?(MyXQL) do
870891
{:fragment, _, _} ->
871892
{nil, as_prefix ++ [?f | Integer.to_string(pos)], nil}
872893

894+
{:values, _, _} ->
895+
{nil, as_prefix ++ [?v | Integer.to_string(pos)], nil}
896+
873897
{table, schema, prefix} ->
874898
name = as_prefix ++ [create_alias(table) | Integer.to_string(pos)]
875899
{quote_table(prefix, table), name, schema}
@@ -1330,6 +1354,7 @@ if Code.ensure_loaded?(MyXQL) do
13301354

13311355
defp get_source(query, sources, ix, source) do
13321356
{expr, name, _schema} = elem(sources, ix)
1357+
name = maybe_add_column_names(source, name)
13331358
{expr || expr(source, sources, query), name}
13341359
end
13351360

@@ -1340,6 +1365,13 @@ if Code.ensure_loaded?(MyXQL) do
13401365
end
13411366
end
13421367

1368+
defp maybe_add_column_names({:values, _, [types, _, _]}, name) do
1369+
fields = Keyword.keys(types)
1370+
[name, ?\s, ?(, quote_names(fields), ?)]
1371+
end
1372+
1373+
defp maybe_add_column_names(_, name), do: name
1374+
13431375
defp quote_name(name) when is_atom(name) do
13441376
quote_name(Atom.to_string(name))
13451377
end

lib/ecto/adapters/postgres/connection.ex

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,10 @@ if Code.ensure_loaded?(Postgrex) do
893893
|> parens_for_select
894894
end
895895

896+
defp expr({:values, _, [types, idx, num_rows]}, _, _query) do
897+
[?(, values_list(types, idx + 1, num_rows), ?)]
898+
end
899+
896900
defp expr({:literal, _, [literal]}, _sources, _query) do
897901
quote_name(literal)
898902
end
@@ -1038,6 +1042,25 @@ if Code.ensure_loaded?(Postgrex) do
10381042
[?(, expr(expr, sources, query), "#>'{", path, "}')"]
10391043
end
10401044

1045+
defp values_list(types, idx, num_rows) do
1046+
rows = Enum.to_list(1..num_rows)
1047+
1048+
[
1049+
"VALUES ",
1050+
intersperse_reduce(rows, ?,, idx, fn _, idx ->
1051+
{value, idx} = values_expr(types, idx)
1052+
{[?(, value, ?)], idx}
1053+
end)
1054+
|> elem(0)
1055+
]
1056+
end
1057+
1058+
defp values_expr(types, idx) do
1059+
intersperse_reduce(types, ?,, idx, fn {_field, type}, idx ->
1060+
{[?$ , Integer.to_string(idx), ?:, ?: | tagged_to_db(type)], idx + 1}
1061+
end)
1062+
end
1063+
10411064
defp type_unless_typed(%Ecto.Query.Tagged{}, _type), do: []
10421065
defp type_unless_typed(_, type), do: [?:, ?: | type]
10431066

@@ -1102,6 +1125,9 @@ if Code.ensure_loaded?(Postgrex) do
11021125
{:fragment, _, _} ->
11031126
{nil, as_prefix ++ [?f | Integer.to_string(pos)], nil}
11041127

1128+
{:values, _, _} ->
1129+
{nil, as_prefix ++ [?v | Integer.to_string(pos)], nil}
1130+
11051131
{table, schema, prefix} ->
11061132
name = as_prefix ++ [create_alias(table) | Integer.to_string(pos)]
11071133
{quote_table(prefix, table), name, schema}
@@ -1702,6 +1728,7 @@ if Code.ensure_loaded?(Postgrex) do
17021728

17031729
defp get_source(query, sources, ix, source) do
17041730
{expr, name, _schema} = elem(sources, ix)
1731+
name = maybe_add_column_names(source, name)
17051732
{expr || expr(source, sources, query), name}
17061733
end
17071734

@@ -1712,6 +1739,13 @@ if Code.ensure_loaded?(Postgrex) do
17121739
end
17131740
end
17141741

1742+
defp maybe_add_column_names({:values, _, [types, _, _]}, name) do
1743+
fields = Keyword.keys(types)
1744+
[name, ?\s, ?(, quote_names(fields), ?)]
1745+
end
1746+
1747+
defp maybe_add_column_names(_, name), do: name
1748+
17151749
defp quote_qualified_name(name, sources, ix) do
17161750
{_, source, _} = elem(sources, ix)
17171751
[source, ?. | quote_name(name)]

lib/ecto/adapters/tds/connection.ex

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,10 @@ if Code.ensure_loaded?(Tds) do
791791
|> parens_for_select
792792
end
793793

794+
defp expr({:values, _, [types, idx, num_rows]}, _, _query) do
795+
[?(, values_list(types, idx + 1, num_rows), ?)]
796+
end
797+
794798
defp expr({:literal, _, [literal]}, _sources, _query) do
795799
quote_name(literal)
796800
end
@@ -939,6 +943,25 @@ if Code.ensure_loaded?(Tds) do
939943
error!(query, "unsupported MSSQL expressions: `#{inspect(field)}`")
940944
end
941945

946+
defp values_list(types, idx, num_rows) do
947+
rows = Enum.to_list(1..num_rows)
948+
949+
[
950+
"VALUES ",
951+
intersperse_reduce(rows, ?,, idx, fn _, idx ->
952+
{value, idx} = values_expr(types, idx)
953+
{[?(, value, ?)], idx}
954+
end)
955+
|> elem(0)
956+
]
957+
end
958+
959+
defp values_expr(types, idx) do
960+
intersperse_reduce(types, ?,, idx, fn {_field, type}, idx ->
961+
{["CAST(", ?@ , Integer.to_string(idx), " AS ", column_type(type, []), ?)], idx + 1}
962+
end)
963+
end
964+
942965
defp op_to_binary({op, _, [_, _]} = expr, sources, query) when op in @binary_ops do
943966
paren_expr(expr, sources, query)
944967
end
@@ -1002,6 +1025,9 @@ if Code.ensure_loaded?(Tds) do
10021025
{:fragment, _, _} ->
10031026
{nil, as_prefix ++ [?f | Integer.to_string(pos)], nil}
10041027

1028+
{:values, _, _} ->
1029+
{nil, as_prefix ++ [?v | Integer.to_string(pos)], nil}
1030+
10051031
{table, model, prefix} ->
10061032
name = as_prefix ++ [create_alias(table) | Integer.to_string(pos)]
10071033
{quote_table(prefix, table), name, model}
@@ -1563,6 +1589,7 @@ if Code.ensure_loaded?(Tds) do
15631589

15641590
defp get_source(query, sources, ix, source) do
15651591
{expr, name, _schema} = elem(sources, ix)
1592+
name = maybe_add_column_names(source, name)
15661593
{expr || expr(source, sources, query), name}
15671594
end
15681595

@@ -1573,6 +1600,13 @@ if Code.ensure_loaded?(Tds) do
15731600
end
15741601
end
15751602

1603+
defp maybe_add_column_names({:values, _, [types, _, _]}, name) do
1604+
fields = Keyword.keys(types)
1605+
[name, ?\s, ?(, quote_names(fields), ?)]
1606+
end
1607+
1608+
defp maybe_add_column_names(_, name), do: name
1609+
15761610
defp quote_name(name) when is_atom(name) do
15771611
quote_name(Atom.to_string(name))
15781612
end

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
66
"deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm", "e3bf435a54ed27b0ba3a01eb117ae017988804e136edcbe8a6a14c310daa966e"},
77
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
8-
"ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "eb03f45b999e2bf67ffae92811233eb6e56dba55", []},
8+
"ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "743ce048cfee927e20dba72f37d58eb52c3535a9", []},
99
"ex_doc": {:hex, :ex_doc, "0.30.5", "aa6da96a5c23389d7dc7c381eba862710e108cee9cfdc629b7ec021313900e9e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "88a1e115dcb91cefeef7e22df4a6ebbe4634fbf98b38adcbc25c9607d6d9d8e6"},
1010
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
1111
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},

test/ecto/adapters/myxql_test.exs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,67 @@ defmodule Ecto.Adapters.MyXQLTest do
11841184
assert query == ~s{DELETE FROM `schema` WHERE `x` IS NULL AND `y` = ?}
11851185
end
11861186

1187+
# Values List
1188+
1189+
test "values list: all" do
1190+
uuid = Ecto.UUID.generate()
1191+
values = [%{bid: uuid, num: 1}, %{num: 2, bid: uuid}]
1192+
types = %{bid: Ecto.UUID, num: :integer}
1193+
1194+
query =
1195+
from(v1 in values(values, types),
1196+
join: v2 in values(values, types),
1197+
on: v1.bid == v2.bid,
1198+
select: v2,
1199+
where: v1.num == ^2
1200+
)
1201+
|> plan()
1202+
|> all()
1203+
1204+
assert query ==
1205+
~s{SELECT v1.`bid`, v1.`num` } <>
1206+
~s{FROM (VALUES ROW(CAST(? AS binary(16)),CAST(? AS unsigned)),ROW(CAST(? AS binary(16)),CAST(? AS unsigned))) AS v0 (`bid`,`num`) } <>
1207+
~s{INNER JOIN (VALUES ROW(CAST(? AS binary(16)),CAST(? AS unsigned)),ROW(CAST(? AS binary(16)),CAST(? AS unsigned))) AS v1 (`bid`,`num`) ON v0.`bid` = v1.`bid` } <>
1208+
~s{WHERE (v0.`num` = ?)}
1209+
end
1210+
1211+
test "values list: delete_all" do
1212+
uuid = Ecto.UUID.generate()
1213+
values = [%{bid: uuid, num: 1}, %{num: 2, bid: uuid}]
1214+
types = %{bid: Ecto.UUID, num: :integer}
1215+
1216+
query =
1217+
from(s in "schema", join: v in values(values, types), on: s.x == v.num, where: v.num == ^2)
1218+
|> plan(:delete_all)
1219+
|> delete_all()
1220+
1221+
assert query ==
1222+
~s{DELETE s0.* FROM `schema` AS s0 } <>
1223+
~s{INNER JOIN (VALUES ROW(CAST(? AS binary(16)),CAST(? AS unsigned)),ROW(CAST(? AS binary(16)),CAST(? AS unsigned))) AS v1 (`bid`,`num`) } <>
1224+
~s{ON s0.`x` = v1.`num` WHERE (v1.`num` = ?)}
1225+
end
1226+
1227+
test "values list: update_all" do
1228+
uuid = Ecto.UUID.generate()
1229+
values = [%{bid: uuid, num: 1}, %{num: 2, bid: uuid}]
1230+
types = %{bid: Ecto.UUID, num: :integer}
1231+
1232+
query =
1233+
from(s in "schema",
1234+
join: v in values(values, types),
1235+
on: s.x == v.num,
1236+
where: v.num == ^2,
1237+
update: [set: [y: v.num]]
1238+
)
1239+
|> plan(:update_all)
1240+
|> update_all()
1241+
1242+
assert query ==
1243+
~s{UPDATE `schema` AS s0, } <>
1244+
~s{(VALUES ROW(CAST(? AS binary(16)),CAST(? AS unsigned)),ROW(CAST(? AS binary(16)),CAST(? AS unsigned))) AS v1 (`bid`,`num`) } <>
1245+
~s{SET s0.`y` = v1.`num` WHERE (s0.`x` = v1.`num`) AND (v1.`num` = ?)}
1246+
end
1247+
11871248
# DDL
11881249

11891250
import Ecto.Migration, only: [table: 1, table: 2, index: 2, index: 3,

test/ecto/adapters/postgres_test.exs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,67 @@ defmodule Ecto.Adapters.PostgresTest do
14851485
assert query == ~s{DELETE FROM "prefix"."schema" WHERE "x" IS NULL AND "y" = $1}
14861486
end
14871487

1488+
# Values List
1489+
1490+
test "values list: all" do
1491+
uuid = Ecto.UUID.generate()
1492+
values = [%{bid: uuid, num: 1}, %{num: 2, bid: uuid}]
1493+
types = %{bid: Ecto.UUID, num: :integer}
1494+
1495+
query =
1496+
from(v1 in values(values, types),
1497+
join: v2 in values(values, types),
1498+
on: v1.bid == v2.bid,
1499+
select: v2,
1500+
where: v1.num == ^2
1501+
)
1502+
|> plan()
1503+
|> all()
1504+
1505+
assert query ==
1506+
~s{SELECT v1."bid", v1."num" } <>
1507+
~s{FROM (VALUES ($1::uuid,$2::bigint),($3::uuid,$4::bigint)) AS v0 ("bid","num") } <>
1508+
~s{INNER JOIN (VALUES ($5::uuid,$6::bigint),($7::uuid,$8::bigint)) AS v1 ("bid","num") ON v0."bid" = v1."bid" } <>
1509+
~s{WHERE (v0."num" = $9)}
1510+
end
1511+
1512+
test "values list: delete_all" do
1513+
uuid = Ecto.UUID.generate()
1514+
values = [%{bid: uuid, num: 1}, %{num: 2, bid: uuid}]
1515+
types = %{bid: Ecto.UUID, num: :integer}
1516+
1517+
query =
1518+
from(s in "schema", join: v in values(values, types), on: s.x == v.num, where: v.num == ^2)
1519+
|> plan(:delete_all)
1520+
|> delete_all()
1521+
1522+
assert query ==
1523+
~s{DELETE FROM "schema" AS s0 } <>
1524+
~s{USING (VALUES ($1::uuid,$2::bigint),($3::uuid,$4::bigint)) AS v1 ("bid","num") } <>
1525+
~s{WHERE (s0."x" = v1."num") AND (v1."num" = $5)}
1526+
end
1527+
1528+
test "values list: update_all" do
1529+
uuid = Ecto.UUID.generate()
1530+
values = [%{bid: uuid, num: 1}, %{num: 2, bid: uuid}]
1531+
types = %{bid: Ecto.UUID, num: :integer}
1532+
1533+
query =
1534+
from(s in "schema",
1535+
join: v in values(values, types),
1536+
on: s.x == v.num,
1537+
where: v.num == ^2,
1538+
update: [set: [y: v.num]]
1539+
)
1540+
|> plan(:update_all)
1541+
|> update_all()
1542+
1543+
assert query ==
1544+
~s{UPDATE "schema" AS s0 SET "y" = v1."num" } <>
1545+
~s{FROM (VALUES ($1::uuid,$2::bigint),($3::uuid,$4::bigint)) AS v1 ("bid","num") } <>
1546+
~s{WHERE (s0."x" = v1."num") AND (v1."num" = $5)}
1547+
end
1548+
14881549
# DDL
14891550

14901551
alias Ecto.Migration.Reference

0 commit comments

Comments
 (0)