Skip to content

Commit 8ff4e9c

Browse files
committed
Support custom field selection on user-defined select queries
1 parent 3d16f4d commit 8ff4e9c

5 files changed

Lines changed: 99 additions & 16 deletions

File tree

lib/feeb/db.ex

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ defmodule Feeb.DB do
107107
def one({domain, :fetch}, value, opts), do: one({domain, :fetch}, [value], opts)
108108

109109
def one({domain, query_name}, bindings, opts) when is_list(bindings) do
110-
one({get_context!(), domain, query_name}, bindings, opts)
110+
{get_context!(), domain, query_name}
111+
|> get_query_id_for_select_query(opts)
112+
|> one(bindings, opts)
111113
end
112114

113115
def one({domain, query_name}, value, opts), do: one({domain, query_name}, [value], opts)
@@ -135,13 +137,15 @@ defmodule Feeb.DB do
135137
|> all([], opts)
136138
end
137139

138-
def all({domain, query_name}, bindings, opts) do
139-
all({get_context!(), domain, query_name}, bindings, opts)
140+
def all({domain, query_name}, bindings, opts) when is_list(bindings) do
141+
{get_context!(), domain, query_name}
142+
|> get_query_id_for_select_query(opts)
143+
|> all(bindings, opts)
140144
end
141145

142-
def all({_, domain, query_name}, bindings, opts) do
143-
bindings = if is_list(bindings), do: bindings, else: [bindings]
146+
def all({domain, query_name}, value, opts), do: all({domain, query_name}, [value], opts)
144147

148+
def all({_, domain, query_name}, bindings, opts) do
145149
case GenServer.call(get_pid!(), {:query, :all, {domain, query_name}, bindings, opts}) do
146150
{:ok, rows} -> rows
147151
{:error, reason} -> raise reason
@@ -291,6 +295,18 @@ defmodule Feeb.DB do
291295
LocalState.get_current_context!().context
292296
end
293297

298+
defp get_query_id_for_select_query(original_query_id, []), do: original_query_id
299+
300+
defp get_query_id_for_select_query(original_query_id, opts) do
301+
target_fields = opts[:select] || [:*]
302+
303+
if target_fields == [:*] do
304+
original_query_id
305+
else
306+
Query.compile_adhoc_query(original_query_id, target_fields)
307+
end
308+
end
309+
294310
defp get_bindings(query_id, struct) do
295311
{_, {_, params_bindings}, _} = Query.fetch!(query_id)
296312

lib/feeb/db/query.ex

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,32 @@ defmodule Feeb.DB.Query do
3333
end
3434

3535
@doc """
36-
Compiling an adhoc query is useful when you want to select custom fields
37-
off of a "select *" query. It's like a subset of the original query
36+
Compiling an adhoc query is useful when the user wants to select custom fields off of a `SELECT *`
37+
query. It's essentially a subset of the original query, with specific fields being selected.
3838
"""
3939
@spec compile_adhoc_query(term, term) :: no_return
40-
def compile_adhoc_query({context, domain, query_name} = query_id, custom_fields) do
41-
raise "Deprecated; consider implementing this feature as part of `get_templated_query_id/3`"
42-
query_name = :"#{query_name}$#{Enum.join(custom_fields, "$")}"
43-
adhoc_query_id = {context, domain, query_name}
40+
def compile_adhoc_query({context, domain, query_name} = original_query_id, target_fields) do
41+
model = Schema.get_model_from_query_id(original_query_id)
42+
valid_fields = model.__cols__()
43+
sorted_target_fields = Enum.sort(target_fields)
44+
45+
adhoc_query_name = :"#{query_name}$#{get_query_name_suffix(sorted_target_fields)}"
46+
adhoc_query_id = {context, domain, adhoc_query_name}
4447

45-
{sql, {fields_bindings, params_bindings}, qt} = fetch!(query_id)
48+
{sql, {fields_bindings, params_bindings}, qt} = fetch!(original_query_id)
4649

4750
if fields_bindings != [:*] do
48-
raise "#{inspect(query_id)}: Custom fields can only be used on 'SELECT *' queries"
51+
raise "#{inspect(original_query_id)}: Custom selection can only be used on 'SELECT *' queries"
4952
end
5053

51-
# "Compile" new query
52-
new_sql = String.replace(sql, "*", Enum.join(custom_fields, ", "))
53-
adhoc_q = {new_sql, {custom_fields, params_bindings}, qt}
54+
Enum.each(target_fields, fn field ->
55+
if field not in valid_fields,
56+
do: raise("Can't select #{inspect(field)}; not a valid field for #{model}")
57+
end)
58+
59+
# "Compile" new query (replaces `SELECT *` with `SELECT <sorted_target_fields>`)
60+
new_sql = String.replace(sql, "*", Enum.join(sorted_target_fields, ", "), global: false)
61+
adhoc_q = {new_sql, {sorted_target_fields, params_bindings}, qt}
5462

5563
append_runtime_query(adhoc_query_id, adhoc_q)
5664
adhoc_query_id

priv/test/queries/test/all_types.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ select map_keys_atom from all_types;
77
-- :get_map
88
select map from all_types;
99

10+
-- :get_by_integer
11+
select * from all_types where integer = ?;
12+
1013
-- :get_max_integer
1114
select max(integer) from all_types;
1215

test/db/db_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,13 @@ defmodule Feeb.DBTest do
437437
assert nil == DB.one({:order_items, :fetch}, [2, 1])
438438
end
439439

440+
test "supports custom selection on user-defined queries", %{shard_id: shard_id} do
441+
DB.begin(@context, shard_id, :read)
442+
443+
assert %{id: %NotLoaded{}, name: "Phoebe"} =
444+
DB.one({:friends, :get_by_name}, "Phoebe", select: [:name])
445+
end
446+
440447
test "supports the :format flag", %{shard_id: shard_id} do
441448
DB.begin(@context, shard_id, :write)
442449

@@ -536,6 +543,13 @@ defmodule Feeb.DBTest do
536543
assert [%{id: 1, title: "Post", body: %NotLoaded{}}] = DB.all(Post, [], select: [:id, :title])
537544
end
538545

546+
test "supports custom selection on user-defined queries", %{shard_id: shard_id} do
547+
DB.begin(@context, shard_id, :read)
548+
549+
assert [%{id: %NotLoaded{}, name: "Phoebe"}] =
550+
DB.all({:friends, :get_by_name}, "Phoebe", select: [:name])
551+
end
552+
539553
test "supports the :format flag", %{shard_id: shard_id} do
540554
DB.begin(@context, shard_id, :write)
541555

test/db/query_test.exs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,48 @@ defmodule Feeb.DB.QueryTest do
4848
end
4949
end
5050

51+
describe "compile_adhoc_query/2" do
52+
test "generates a subset of the original query" do
53+
Query.compile(@all_types_path, {:test, :all_types})
54+
55+
original_query_id = {:test, :all_types, :get_by_integer}
56+
query_id = Query.compile_adhoc_query(original_query_id, [:string, :atom, :uuid])
57+
58+
assert {sql, {target_fields, bindings}, query_type} = Query.fetch!(query_id, [])
59+
assert query_type == :select
60+
assert target_fields == [:atom, :string, :uuid]
61+
assert bindings == [:integer]
62+
assert sql == "select atom, string, uuid from all_types where integer = ?;"
63+
end
64+
65+
test "raises on custom selection of a non-wildcard select query" do
66+
Query.compile(@all_types_path, {:test, :all_types})
67+
68+
# This query is: `SELECT atom, integer FROM all_types;`
69+
original_query_id = {:test, :all_types, :get_atom_and_integer}
70+
71+
%{message: error} =
72+
assert_raise RuntimeError, fn ->
73+
Query.compile_adhoc_query(original_query_id, [:string, :atom, :uuid])
74+
end
75+
76+
assert error =~ "Custom selection can only be used on 'SELECT *' queries"
77+
end
78+
79+
test "raises when trying to select a field that does not exist in the schema" do
80+
Query.compile(@all_types_path, {:test, :all_types})
81+
82+
original_query_id = {:test, :all_types, :get_by_integer}
83+
84+
%{message: error} =
85+
assert_raise RuntimeError, fn ->
86+
Query.compile_adhoc_query(original_query_id, [:string, :i_dont_exist, :atom])
87+
end
88+
89+
assert error =~ "Can't select :i_dont_exist; not a valid field for Elixir.Sample.AllTypes"
90+
end
91+
end
92+
5193
describe "get_templated_query_id/3" do
5294
test ":__all" do
5395
Query.compile(@friends_path, {:test, :friends})

0 commit comments

Comments
 (0)