Skip to content

Commit 36c6804

Browse files
authored
Add query_cache option to selectively bypass query cache (#4690)
1 parent a49e499 commit 36c6804

File tree

6 files changed

+84
-6
lines changed

6 files changed

+84
-6
lines changed

lib/ecto/adapter/queryable.ex

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,19 @@ defmodule Ecto.Adapter.Queryable do
9898
@doc """
9999
Plans and prepares a query for the given repo, leveraging its query cache.
100100
101-
This operation uses the query cache if one is available.
101+
This operation uses the query cache if one is available, unless
102+
`query_cache: false` is passed as option, which bypasses the query cache.
102103
"""
103-
def prepare_query(operation, repo_name_or_pid, queryable) do
104+
def prepare_query(operation, repo_name_or_pid, queryable, opts \\ []) do
104105
%{adapter: adapter, cache: cache} = Ecto.Repo.Registry.lookup(repo_name_or_pid)
105106

107+
query_cache? = Keyword.get(opts, :query_cache, true)
108+
106109
{_meta, prepared, _cast_params, dump_params} =
107110
queryable
108111
|> Ecto.Queryable.to_query()
109112
|> Ecto.Query.Planner.ensure_select(operation == :all)
110-
|> Ecto.Query.Planner.query(operation, cache, adapter, 0)
113+
|> Ecto.Query.Planner.query(operation, cache, adapter, 0, query_cache?)
111114

112115
{prepared, dump_params}
113116
end

lib/ecto/query/planner.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ defmodule Ecto.Query.Planner do
133133
The cache value is the compiled query by the adapter
134134
along-side the select expression.
135135
"""
136-
def query(query, operation, cache, adapter, counter) do
136+
def query(query, operation, cache, adapter, counter, query_cache?) do
137137
{query, params, key} = plan(query, operation, adapter)
138138
{cast_params, dump_params} = Enum.unzip(params)
139+
key = if query_cache?, do: key, else: :nocache
139140
query_with_cache(key, query, operation, cache, adapter, counter, cast_params, dump_params)
140141
end
141142

lib/ecto/repo.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ defmodule Ecto.Repo do
136136
See the next section for more information
137137
* `:telemetry_options` - Extra options to attach to telemetry event name.
138138
See the next section for more information
139+
* `:query_cache` - When set to `false`, bypasses the Ecto query cache for the current
140+
operation. This means the query will not be looked up in the cache, it will not be stored
141+
in the cache and no cache update function will not be passed to the adapter. Note that
142+
this doesn't necessarily disable the database cache, it only affects Ecto's internal
143+
cache of normalized queries and adapter prepared statements. Defaults to `true`.
139144
140145
## Adapter-Specific Errors
141146

lib/ecto/repo/queryable.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ defmodule Ecto.Repo.Queryable do
3939
{query, opts} = repo.prepare_query(:stream, query, opts)
4040
query = attach_prefix(query, opts)
4141

42+
query_cache? = Keyword.get(opts, :query_cache, true)
43+
4244
{query_meta, prepared, cast_params, dump_params} =
43-
Planner.query(query, :all, cache, adapter, 0)
45+
Planner.query(query, :all, cache, adapter, 0, query_cache?)
4446

4547
opts = [cast_params: cast_params] ++ opts
4648

@@ -223,8 +225,10 @@ defmodule Ecto.Repo.Queryable do
223225
{query, opts} = repo.prepare_query(operation, query, opts)
224226
query = attach_prefix(query, opts)
225227

228+
query_cache? = Keyword.get(opts, :query_cache, true)
229+
226230
{query_meta, prepared, cast_params, dump_params} =
227-
Planner.query(query, operation, cache, adapter, 0)
231+
Planner.query(query, operation, cache, adapter, 0, query_cache?)
228232

229233
opts = [cast_params: cast_params] ++ opts
230234

test/ecto/query/planner_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2829,4 +2829,29 @@ defmodule Ecto.Query.PlannerTest do
28292829
end
28302830
end
28312831
end
2832+
2833+
describe "query: query_cache option" do
2834+
setup do
2835+
cache = Planner.new_query_cache(__MODULE__)
2836+
{:ok, cache: cache}
2837+
end
2838+
2839+
test "uses cache if true", %{cache: cache} do
2840+
query = from(p in Post, where: p.title == ^"hello")
2841+
2842+
{_meta, {:cache, _update, _prepared}, _cast, _dump} =
2843+
Planner.query(query, :all, cache, Ecto.CachingTestAdapter, 0, true)
2844+
2845+
assert :ets.info(cache, :size) == 1
2846+
end
2847+
2848+
test "bypasses cache if false", %{cache: cache} do
2849+
query = from(p in Post, where: p.title == ^"hello")
2850+
2851+
{_meta1, {:nocache, _prepared}, _cast1, _dump1} =
2852+
Planner.query(query, :all, cache, Ecto.CachingTestAdapter, 0, false)
2853+
2854+
assert :ets.info(cache, :size) == 0
2855+
end
2856+
end
28322857
end

test/support/test_repo.exs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,46 @@ defmodule Ecto.TestAdapter do
161161
end
162162
end
163163

164+
defmodule Ecto.CachingTestAdapter do
165+
@moduledoc """
166+
Test adapter that supports query caching, used for testing the query_cache option.
167+
"""
168+
@behaviour Ecto.Adapter
169+
@behaviour Ecto.Adapter.Queryable
170+
171+
defmacro __before_compile__(_opts), do: :ok
172+
173+
def ensure_all_started(_, _), do: {:ok, []}
174+
175+
def init(_opts) do
176+
{:ok, Supervisor.child_spec({Task, fn -> :timer.sleep(:infinity) end}, []), %{}}
177+
end
178+
179+
def checkout(_mod, _opts, fun), do: fun.()
180+
def checked_out?(_mod), do: false
181+
182+
def loaders(_primitive, type), do: [type]
183+
def dumpers(_primitive, type), do: [type]
184+
def autogenerate(:id), do: nil
185+
def autogenerate(:embed_id), do: Ecto.UUID.autogenerate()
186+
def autogenerate(:binary_id), do: Ecto.UUID.bingenerate()
187+
188+
# Return :cache to trigger default caching in the planner
189+
def prepare(operation, query), do: {:cache, {operation, query}}
190+
191+
def execute(_adapter_meta, _query_meta, {_cache_status, {:all, _query}}, _dump_params, _opts) do
192+
[]
193+
end
194+
195+
def execute(_adapter_meta, _query_meta, {_cache_status, {_operation, _query}}, _dump_params, _opts) do
196+
{1, nil}
197+
end
198+
199+
def stream(_adapter_meta, _query_meta, _prepared, _dump_params, _opts) do
200+
[]
201+
end
202+
end
203+
164204
Application.put_env(:ecto, Ecto.TestRepo, user: "invalid")
165205

166206
defmodule Ecto.TestRepo do

0 commit comments

Comments
 (0)