Skip to content

Commit 3724cc5

Browse files
Implement values (#174)
1 parent 79d94c8 commit 3724cc5

File tree

4 files changed

+143
-16
lines changed

4 files changed

+143
-16
lines changed

integration_test/test_helper.exs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,14 @@ excludes = [
127127
# SQLite does not support anything except a single column in DISTINCT
128128
:multicolumn_distinct,
129129

130-
# Values list
131-
:values_list
130+
# :location is not supported in elixir 1.15 and earlier, so we exclude all
131+
if Version.match?(System.version(), "~> 1.16") do
132+
# Run all with tag values_list, except for the "delete_all" test,
133+
# as JOINS are not supported on DELETE statements by SQLite.
134+
{:location, {"ecto/integration_test/cases/repo.exs", 2281}}
135+
else
136+
:values_list
137+
end
132138
]
133139

134140
ExUnit.configure(exclude: excludes)

integration_test/values_test.exs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
defmodule Ecto.Integration.ValuesTest do
2+
use Ecto.Integration.Case, async: true
3+
4+
import Ecto.Query, only: [from: 2, with_cte: 3]
5+
6+
alias Ecto.Integration.Comment
7+
alias Ecto.Integration.Post
8+
alias Ecto.Integration.TestRepo
9+
10+
test "values works with datetime" do
11+
TestRepo.insert!(%Post{inserted_at: ~N[2000-01-01 00:01:00]})
12+
TestRepo.insert!(%Post{inserted_at: ~N[2000-01-01 00:02:00]})
13+
TestRepo.insert!(%Post{inserted_at: ~N[2000-01-01 00:03:00]})
14+
15+
params = [
16+
%{id: 1, date: ~N[2000-01-01 00:00:00]},
17+
%{id: 2, date: ~N[2000-01-01 00:01:00]},
18+
%{id: 3, date: ~N[2000-01-01 00:02:00]},
19+
%{id: 4, date: ~N[2000-01-01 00:03:00]}
20+
]
21+
22+
types = %{id: :integer, date: :naive_datetime}
23+
24+
results =
25+
from(params in values(params, types),
26+
left_join: p in Post,
27+
on: p.inserted_at <= params.date,
28+
group_by: params.id,
29+
select: %{id: params.id, count: count(p.id)},
30+
order_by: count(p.id)
31+
)
32+
|> TestRepo.all()
33+
34+
assert results == [
35+
%{count: 0, id: 1},
36+
%{count: 1, id: 2},
37+
%{count: 2, id: 3},
38+
%{count: 3, id: 4}
39+
]
40+
end
41+
42+
test "join to values works" do
43+
TestRepo.insert!(%Post{id: 1})
44+
TestRepo.insert!(%Comment{post_id: 1, text: "short"})
45+
TestRepo.insert!(%Comment{post_id: 1, text: "much longer text"})
46+
47+
params = [%{id: 1, post_id: 1, n: 0}, %{id: 2, post_id: 1, n: 10}]
48+
types = %{id: :integer, post_id: :integer, n: :integer}
49+
50+
results =
51+
from(p in Post,
52+
right_join: params in values(params, types),
53+
on: params.post_id == p.id,
54+
left_join: c in Comment,
55+
on: c.post_id == p.id and fragment("LENGTH(?)", c.text) > params.n,
56+
group_by: params.id,
57+
select: {params.id, count(c.id)}
58+
)
59+
|> TestRepo.all()
60+
61+
assert [{1, 2}, {2, 1}] = results
62+
end
63+
64+
test "values can be used together with CTE" do
65+
TestRepo.insert!(%Post{id: 1, visits: 42})
66+
TestRepo.insert!(%Comment{post_id: 1, text: "short"})
67+
TestRepo.insert!(%Comment{post_id: 1, text: "much longer text"})
68+
69+
params = [%{id: 1, post_id: 1, n: 0}, %{id: 2, post_id: 1, n: 10}]
70+
types = %{id: :integer, post_id: :integer, n: :integer}
71+
72+
cte_query = from(p in Post, select: %{id: p.id, visits: coalesce(p.visits, 0)})
73+
74+
q = Post |> with_cte("xxx", as: ^cte_query)
75+
76+
results =
77+
from(p in q,
78+
right_join: params in values(params, types),
79+
on: params.post_id == p.id,
80+
left_join: c in Comment,
81+
on: c.post_id == p.id and fragment("LENGTH(?)", c.text) > params.n,
82+
left_join: cte in "xxx",
83+
on: cte.id == p.id,
84+
group_by: params.id,
85+
select: {params.id, count(c.id), cte.visits}
86+
)
87+
|> TestRepo.all()
88+
89+
assert [{1, 2, 42}, {2, 1, 42}] = results
90+
end
91+
end

lib/ecto/adapters/sqlite3/connection.ex

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,12 +1037,6 @@ defmodule Ecto.Adapters.SQLite3.Connection do
10371037
message: "join hints are not supported by SQLite3"
10381038
end
10391039

1040-
defp assert_valid_join(%JoinExpr{source: {:values, _, _}}, query) do
1041-
raise Ecto.QueryError,
1042-
query: query,
1043-
message: "SQLite3 adapter does not support values lists"
1044-
end
1045-
10461040
defp assert_valid_join(_join_expr, _query), do: :ok
10471041

10481042
defp join_on(:cross, true, _sources, _query), do: []
@@ -1368,8 +1362,8 @@ defmodule Ecto.Adapters.SQLite3.Connection do
13681362
|> parens_for_select
13691363
end
13701364

1371-
defp expr({:values, _, _}, _, _query) do
1372-
raise ArgumentError, "SQLite3 adapter does not support values lists"
1365+
defp expr({:values, _, [types, idx, num_rows]}, _, _query) do
1366+
[?(, values_list(types, idx + 1, num_rows), ?)]
13731367
end
13741368

13751369
defp expr({:identifier, _, [literal]}, _sources, _query) do
@@ -1560,6 +1554,33 @@ defmodule Ecto.Adapters.SQLite3.Connection do
15601554
message: "unsupported expression #{inspect(expr)}"
15611555
end
15621556

1557+
defp values_list(types, idx, num_rows) do
1558+
rows = :lists.seq(1, num_rows, 1)
1559+
1560+
col_names =
1561+
Enum.map_join(Enum.with_index(types), ", ", fn {{k, _v}, i} ->
1562+
"column#{i + 1} AS #{k}"
1563+
end)
1564+
1565+
[
1566+
"SELECT ",
1567+
col_names,
1568+
" FROM (VALUES ",
1569+
intersperse_reduce(rows, ?,, idx, fn _, idx ->
1570+
{value, idx} = values_expr(types, idx)
1571+
{[?(, value, ?)], idx}
1572+
end)
1573+
|> elem(0),
1574+
")"
1575+
]
1576+
end
1577+
1578+
defp values_expr(types, idx) do
1579+
intersperse_reduce(types, ?,, idx, fn {_field, type}, idx ->
1580+
{[?$, Integer.to_string(idx), ?:, ?: | column_type(type, nil)], idx + 1}
1581+
end)
1582+
end
1583+
15631584
def interval(_, "microsecond", _sources) do
15641585
raise ArgumentError,
15651586
"SQLite does not support microsecond precision in datetime intervals"
@@ -1618,6 +1639,9 @@ defmodule Ecto.Adapters.SQLite3.Connection do
16181639
{:fragment, _, _} ->
16191640
{nil, as_prefix ++ [?f | Integer.to_string(pos)], nil}
16201641

1642+
{:values, _, _} ->
1643+
{nil, as_prefix ++ [?v | Integer.to_string(pos)], nil}
1644+
16211645
{table, schema, prefix} ->
16221646
name = as_prefix ++ [create_alias(table) | Integer.to_string(pos)]
16231647
{quote_table(prefix, table), name, schema}

test/ecto/adapters/sqlite3/connection/join_test.exs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,11 @@ defmodule Ecto.Adapters.SQLite3.Connection.JoinTest do
135135
end
136136
end
137137

138-
test "join with values is not supported" do
139-
assert_raise Ecto.QueryError, fn ->
140-
rows = [%{x: 1, y: 1}, %{x: 2, y: 2}]
141-
types = %{x: :integer, y: :integer}
138+
test "join with values" do
139+
rows = [%{x: 1, y: 1}, %{x: 2, y: 2}]
140+
types = %{x: :integer, y: :integer}
142141

142+
query =
143143
Schema
144144
|> join(
145145
:inner,
@@ -149,8 +149,14 @@ defmodule Ecto.Adapters.SQLite3.Connection.JoinTest do
149149
)
150150
|> select([p, q], {p.id, q.x})
151151
|> plan()
152-
|> all()
153-
end
152+
153+
# Key order differs between OTP 25 and 26, so we can't hardcode the column names below
154+
[col1, col2] = Map.keys(types)
155+
156+
assert ~s{SELECT s0."id", v1."x" FROM "schema" AS s0 } <>
157+
~s{INNER JOIN (SELECT column1 AS #{col1}, column2 AS #{col2} FROM (VALUES ($1::INTEGER,$2::INTEGER),($3::INTEGER,$4::INTEGER))) } <>
158+
~s{AS v1 ON (v1."x" = s0."x") AND (v1."y" = s0."y")} ==
159+
all(query)
154160
end
155161

156162
test "join with fragment" do

0 commit comments

Comments
 (0)