An Ecto SQLite3 adapter.
0

Configure Feed

Select the types of activity you want to include in your feed.

Implement values (#174)

authored by

spicychickensauce and committed by
GitHub
(Sep 15, 2025, 10:37 AM -0500) 3724cc50 79d94c8b

+143 -16
+8 -2
integration_test/test_helper.exs
··· 127 127 # SQLite does not support anything except a single column in DISTINCT 128 128 :multicolumn_distinct, 129 129 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 132 138 ] 133 139 134 140 ExUnit.configure(exclude: excludes)
+91
integration_test/values_test.exs
··· 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
+32 -8
lib/ecto/adapters/sqlite3/connection.ex
··· 1037 1037 message: "join hints are not supported by SQLite3" 1038 1038 end 1039 1039 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 - 1046 1040 defp assert_valid_join(_join_expr, _query), do: :ok 1047 1041 1048 1042 defp join_on(:cross, true, _sources, _query), do: [] ··· 1368 1362 |> parens_for_select 1369 1363 end 1370 1364 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), ?)] 1373 1367 end 1374 1368 1375 1369 defp expr({:identifier, _, [literal]}, _sources, _query) do ··· 1560 1554 message: "unsupported expression #{inspect(expr)}" 1561 1555 end 1562 1556 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 + 1563 1584 def interval(_, "microsecond", _sources) do 1564 1585 raise ArgumentError, 1565 1586 "SQLite does not support microsecond precision in datetime intervals" ··· 1617 1638 case elem(sources, pos) do 1618 1639 {:fragment, _, _} -> 1619 1640 {nil, as_prefix ++ [?f | Integer.to_string(pos)], nil} 1641 + 1642 + {:values, _, _} -> 1643 + {nil, as_prefix ++ [?v | Integer.to_string(pos)], nil} 1620 1644 1621 1645 {table, schema, prefix} -> 1622 1646 name = as_prefix ++ [create_alias(table) | Integer.to_string(pos)]
+12 -6
test/ecto/adapters/sqlite3/connection/join_test.exs
··· 135 135 end 136 136 end 137 137 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} 142 141 142 + query = 143 143 Schema 144 144 |> join( 145 145 :inner, ··· 149 149 ) 150 150 |> select([p, q], {p.id, q.x}) 151 151 |> 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) 154 160 end 155 161 156 162 test "join with fragment" do