Skip to content

Commit 7674392

Browse files
committed
Add support for custom selection fields
1 parent 55926a0 commit 7674392

5 files changed

Lines changed: 138 additions & 31 deletions

File tree

lib/feeb/db.ex

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ defmodule Feeb.DB do
9696

9797
def one(partial_or_full_query_id, bindings \\ [], opts \\ [])
9898

99-
# TODO: See Feeb.DB, I also support `custom_fields`
10099
def one({domain, :fetch}, bindings, opts) when is_list(bindings) do
100+
target_fields = opts[:select] || [:*]
101+
101102
{get_context!(), domain, :__fetch}
102-
|> Query.get_templated_query_id([], %{})
103+
|> Query.get_templated_query_id(target_fields, %{})
103104
|> one(bindings, opts)
104105
end
105106

@@ -127,8 +128,10 @@ defmodule Feeb.DB do
127128
def all(partial_or_full_query_id, bindings \\ [], opts \\ [])
128129

129130
def all(schema, _bindings, opts) when is_atom(schema) do
131+
target_fields = opts[:select] || [:*]
132+
130133
{get_context!(), schema.__table__(), :__all}
131-
|> Query.get_templated_query_id(:all, %{})
134+
|> Query.get_templated_query_id(target_fields, %{})
132135
|> all([], opts)
133136
end
134137

@@ -147,7 +150,7 @@ defmodule Feeb.DB do
147150

148151
def insert(%schema{} = struct, opts \\ []) do
149152
{get_context!(), schema.__table__(), :__insert}
150-
|> Query.get_templated_query_id(:all, %{schema: schema})
153+
|> Query.get_templated_query_id([:*], %{schema: schema})
151154
|> insert_sql(struct, opts)
152155
end
153156

lib/feeb/db/query.ex

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,26 @@ defmodule Feeb.DB.Query do
5858

5959
def get_templated_query_id(query_id, target_fields, meta \\ %{})
6060

61+
def get_templated_query_id({context, domain, query_name} = query_id, target_fields, _meta)
62+
when query_name in [:__all, :__fetch] do
63+
model = Schema.get_model_from_query_id(query_id)
64+
65+
real_query_id = {context, domain, :"#{query_name}$#{get_query_name_suffix(target_fields)}"}
66+
67+
case get(real_query_id) do
68+
{_, _, _} = _compiled_query ->
69+
real_query_id
70+
71+
nil ->
72+
compile_templated_query(query_name, query_id, target_fields, model)
73+
end
74+
end
75+
6176
def get_templated_query_id({context, domain, :__insert} = query_id, target_fields, _meta) do
6277
model = Schema.get_model_from_query_id(query_id)
6378

6479
target_fields =
65-
if target_fields == :all do
80+
if target_fields == [:*] do
6681
model.__cols__()
6782
else
6883
raise "Not supported for now, add & test it once needed"
@@ -106,16 +121,15 @@ defmodule Feeb.DB.Query do
106121
end
107122
end
108123

109-
def get_templated_query_id({_context, _domain, query_name} = query_id, target_fields, _meta)
110-
when query_name in [:__all, :__fetch, :__delete] do
124+
def get_templated_query_id({_context, _domain, :__delete} = query_id, target_fields, _meta) do
111125
model = Schema.get_model_from_query_id(query_id)
112126

113127
case get(query_id) do
114128
{_, _, _} = _compiled_query ->
115129
query_id
116130

117131
nil ->
118-
compile_templated_query(query_name, query_id, target_fields, model)
132+
compile_templated_query(:__delete, query_id, target_fields, model)
119133
end
120134
end
121135

@@ -139,34 +153,32 @@ defmodule Feeb.DB.Query do
139153
primary_keys = model.__primary_keys__()
140154
assert_adhoc_query!(primary_keys, query_id, model)
141155

142-
set_conditions =
143-
target_fields
144-
|> Enum.reduce([], fn field, acc ->
145-
["#{field} = ?" | acc]
146-
end)
147-
|> Enum.reverse()
148-
|> Enum.join(", ")
156+
set_clause = generate_update_set_clause(target_fields)
157+
where_clause = generate_where_clause(primary_keys)
158+
sql = "UPDATE #{domain} #{set_clause} #{where_clause};"
149159

150-
sql = "UPDATE #{domain} SET #{set_conditions} #{generate_where_clause(primary_keys)};"
151160
adhoc_query = {sql, {[], target_fields ++ primary_keys}, :update}
152161
append_runtime_query(query_id, adhoc_query)
153162

154163
query_id
155164
end
156165

157-
defp compile_templated_query(:__all, {_, domain, _} = query_id, _target_fields, _model) do
158-
sql = "SELECT * FROM #{domain};"
159-
adhoc_query = {sql, {[:*], []}, :select}
166+
defp compile_templated_query(:__all, {_, domain, _} = query_id, target_fields, _model) do
167+
sql = "#{generate_select_clause(target_fields)} FROM #{domain};"
168+
adhoc_query = {sql, {target_fields, []}, :select}
160169
append_runtime_query(query_id, adhoc_query)
161170
query_id
162171
end
163172

164-
defp compile_templated_query(:__fetch, {_, domain, _} = query_id, _target_fields, model) do
173+
defp compile_templated_query(:__fetch, {_, domain, _} = query_id, target_fields, model) do
165174
primary_keys = model.__primary_keys__()
166175
assert_adhoc_query!(primary_keys, query_id, model)
167-
sql = "SELECT * FROM #{domain} #{generate_where_clause(primary_keys)};"
168176

169-
adhoc_query = {sql, {[:*], primary_keys}, :select}
177+
select_clause = generate_select_clause(target_fields)
178+
where_clause = generate_where_clause(primary_keys)
179+
sql = "#{select_clause} FROM #{domain} #{where_clause};"
180+
181+
adhoc_query = {sql, {target_fields, primary_keys}, :select}
170182
append_runtime_query(query_id, adhoc_query)
171183
query_id
172184
end
@@ -232,6 +244,16 @@ defmodule Feeb.DB.Query do
232244
end
233245
end
234246

247+
defp get_query_name_suffix(target_fields) when is_list(target_fields) do
248+
target_fields
249+
|> Enum.sort()
250+
|> Enum.reduce([], fn field, acc ->
251+
["#{field}" | acc]
252+
end)
253+
|> Enum.reverse()
254+
|> Enum.join("$")
255+
end
256+
235257
defp maybe_inject_returning_clause(nil, _), do: nil
236258
defp maybe_inject_returning_clause(query, []), do: query
237259

@@ -253,6 +275,32 @@ defmodule Feeb.DB.Query do
253275
defp get_returning_fields({_, _, operation}) when operation in [:update, :delete],
254276
do: "*"
255277

278+
defp generate_select_clause([]), do: "SELECT *"
279+
280+
defp generate_select_clause(fields) when is_list(fields) do
281+
select_conditions =
282+
fields
283+
|> Enum.reduce([], fn field, acc ->
284+
["#{field}" | acc]
285+
end)
286+
|> Enum.reverse()
287+
|> Enum.join(", ")
288+
289+
"SELECT #{select_conditions}"
290+
end
291+
292+
defp generate_update_set_clause(fields) when is_list(fields) do
293+
set_conditions =
294+
fields
295+
|> Enum.reduce([], fn field, acc ->
296+
["#{field} = ?" | acc]
297+
end)
298+
|> Enum.reverse()
299+
|> Enum.join(", ")
300+
301+
"SET #{set_conditions}"
302+
end
303+
256304
defp generate_where_clause(primary_keys) when is_list(primary_keys) do
257305
where_conditions =
258306
primary_keys

lib/feeb/db/schema.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ defmodule Feeb.DB.Schema do
126126
assert_env.(:schema)
127127
end
128128

129-
defmacro cast(args, target_fields \\ unquote(:all)) do
129+
# TODO: Why is this a macro?
130+
defmacro cast(args, target_fields \\ unquote([:*])) do
130131
quote do
131132
meta = %{
132133
valid?: true,

test/db/db_test.exs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Feeb.DBTest do
22
use Test.Feeb.DBCase, async: true
33
alias Feeb.DB, as: DB
44
alias Feeb.DB.LocalState
5+
alias Feeb.DB.Value.NotLoaded
56
alias Sample.{AllTypes, CustomTypes, Friend, OrderItems, Post}
67
alias Sample.Types.TypedID
78

@@ -398,22 +399,29 @@ defmodule Feeb.DBTest do
398399
end
399400
end
400401

401-
describe "one/1" do
402+
describe "one/3" do
402403
test "returns the expected result", %{shard_id: shard_id} do
403404
DB.begin(@context, shard_id, :read)
404405
assert %{id: 1, name: "Phoebe"} = DB.one({:friends, :get_by_id}, [1])
405406
assert [_] = DB.one({:pragma, :user_version})
406407
assert nil == DB.one({:friends, :get_by_id}, [0])
407408
end
408409

409-
test ":fetch templated query works", %{shard_id: shard_id} do
410+
test ":__fetch templated query works", %{shard_id: shard_id} do
410411
DB.begin(@context, shard_id, :write)
411412

412413
assert %{id: 1, name: "Phoebe"} = DB.one({:friends, :fetch}, [1])
413414
assert nil == DB.one({:friends, :fetch}, [500])
414415
end
415416

416-
test ":fetch templated query works on schema with composite PKs", %{shard_id: shard_id} do
417+
test ":__fetch templated query with custom selection", %{shard_id: shard_id} do
418+
DB.begin(@context, shard_id, :write)
419+
420+
assert %{id: %NotLoaded{}, name: "Phoebe"} = DB.one({:friends, :fetch}, [1], select: [:name])
421+
assert nil == DB.one({:friends, :fetch}, [500], select: [:name])
422+
end
423+
424+
test ":__fetch templated query works on schema with composite PKs", %{shard_id: shard_id} do
417425
DB.begin(@context, shard_id, :write)
418426

419427
%{order_id: 1, product_id: 2, quantity: 10, price: 50}
@@ -505,6 +513,29 @@ defmodule Feeb.DBTest do
505513
end
506514

507515
describe "all/3" do
516+
test ":__all templated query works", %{shard_id: shard_id} do
517+
DB.begin(@context, shard_id, :write)
518+
519+
# There are 6 friends
520+
assert [_, _, _, _, _, _] = DB.all(Friend)
521+
522+
%{id: 1, title: "My Post", body: "My Body"}
523+
|> Post.new()
524+
|> DB.insert!()
525+
526+
assert [%{id: 1, title: "My Post", body: "My Body"}] = DB.all(Post)
527+
end
528+
529+
test ":__all templated query works with custom selection", %{shard_id: shard_id} do
530+
DB.begin(@context, shard_id, :write)
531+
532+
%{id: 1, title: "Post", body: "My Body"}
533+
|> Post.new()
534+
|> DB.insert!()
535+
536+
assert [%{id: 1, title: "Post", body: %NotLoaded{}}] = DB.all(Post, [], select: [:id, :title])
537+
end
538+
508539
test "supports the :format flag", %{shard_id: shard_id} do
509540
DB.begin(@context, shard_id, :write)
510541

test/db/query_test.exs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ defmodule Feeb.DB.QueryTest do
5454

5555
# Ensures the :__fetch is compiled (due to it being an "ad-hoc" query)
5656
query_id = {:test, :friends, :__all}
57-
Query.get_templated_query_id(query_id, [])
57+
Query.get_templated_query_id(query_id, [:*])
5858

5959
assert {sql, {target_fields, bindings}, query_type} = Query.fetch!(query_id)
6060
assert query_type == :select
@@ -63,11 +63,23 @@ defmodule Feeb.DB.QueryTest do
6363
assert sql == "SELECT * FROM friends;"
6464
end
6565

66+
test ":__all - with custom target fields" do
67+
Query.compile(@friends_path, {:test, :friends})
68+
69+
query_id = Query.get_templated_query_id({:test, :friends, :__all}, [:name])
70+
71+
assert {sql, {target_fields, bindings}, query_type} = Query.fetch!(query_id)
72+
assert query_type == :select
73+
assert target_fields == [:name]
74+
assert bindings == []
75+
assert sql == "SELECT name FROM friends;"
76+
end
77+
6678
test ":__fetch" do
6779
Query.compile(@friends_path, {:test, :friends})
6880

6981
query_id = {:test, :friends, :__fetch}
70-
Query.get_templated_query_id(query_id, [])
82+
Query.get_templated_query_id(query_id, [:*])
7183

7284
assert {sql, {target_fields, bindings}, query_type} = Query.fetch!(query_id)
7385
assert query_type == :select
@@ -80,7 +92,7 @@ defmodule Feeb.DB.QueryTest do
8092
Query.compile(@order_items_path, {:test, :order_items})
8193

8294
query_id = {:test, :order_items, :__fetch}
83-
Query.get_templated_query_id(query_id, [])
95+
Query.get_templated_query_id(query_id, [:*])
8496

8597
assert {sql, {target_fields, bindings}, query_type} = Query.fetch!(query_id)
8698
assert query_type == :select
@@ -89,14 +101,26 @@ defmodule Feeb.DB.QueryTest do
89101
assert sql == "SELECT * FROM order_items WHERE order_id = ? AND product_id = ?;"
90102
end
91103

104+
test ":__fetch - with custom target fields" do
105+
Query.compile(@order_items_path, {:test, :order_items})
106+
107+
query_id = Query.get_templated_query_id({:test, :order_items, :__fetch}, [:quantity, :price])
108+
109+
assert {sql, {target_fields, bindings}, query_type} = Query.fetch!(query_id)
110+
assert sql == "SELECT quantity, price FROM order_items WHERE order_id = ? AND product_id = ?;"
111+
assert target_fields == [:quantity, :price]
112+
assert bindings == [:order_id, :product_id]
113+
assert query_type == :select
114+
end
115+
92116
test ":__fetch - raises when schema has no PK" do
93117
Query.compile(@all_types_path, {:test, :all_types})
94118

95119
query_id = {:test, :all_types, :__fetch}
96120

97121
%{message: error} =
98122
assert_raise RuntimeError, fn ->
99-
Query.get_templated_query_id(query_id, [])
123+
Query.get_templated_query_id(query_id, [:*])
100124
end
101125

102126
assert error =~ "Can't generate adhoc query"
@@ -108,7 +132,7 @@ defmodule Feeb.DB.QueryTest do
108132
Query.compile(@friends_path, {:test, :friends})
109133

110134
query_id = {:test, :friends, :__insert}
111-
Query.get_templated_query_id(query_id, :all)
135+
Query.get_templated_query_id(query_id, [:*])
112136

113137
assert {sql, {target_fields, bindings}, query_type} = Query.fetch!(query_id)
114138
assert query_type == :insert

0 commit comments

Comments
 (0)