Skip to content

Commit 4b0bae6

Browse files
Merge pull request #25 from renatomassaro/add-support-for-custom-selection-fields
Add support for custom selection fields
2 parents 55926a0 + 86dca78 commit 4b0bae6

7 files changed

Lines changed: 283 additions & 60 deletions

File tree

lib/feeb/db.ex

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,20 @@ 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

106107
def one({domain, :fetch}, value, opts), do: one({domain, :fetch}, [value], opts)
107108

108109
def one({domain, query_name}, bindings, opts) when is_list(bindings) do
109-
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)
110113
end
111114

112115
def one({domain, query_name}, value, opts), do: one({domain, query_name}, [value], opts)
@@ -127,18 +130,22 @@ defmodule Feeb.DB do
127130
def all(partial_or_full_query_id, bindings \\ [], opts \\ [])
128131

129132
def all(schema, _bindings, opts) when is_atom(schema) do
133+
target_fields = opts[:select] || [:*]
134+
130135
{get_context!(), schema.__table__(), :__all}
131-
|> Query.get_templated_query_id(:all, %{})
136+
|> Query.get_templated_query_id(target_fields, %{})
132137
|> all([], opts)
133138
end
134139

135-
def all({domain, query_name}, bindings, opts) do
136-
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)
137144
end
138145

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

148+
def all({_, domain, query_name}, bindings, opts) do
142149
case GenServer.call(get_pid!(), {:query, :all, {domain, query_name}, bindings, opts}) do
143150
{:ok, rows} -> rows
144151
{:error, reason} -> raise reason
@@ -147,7 +154,7 @@ defmodule Feeb.DB do
147154

148155
def insert(%schema{} = struct, opts \\ []) do
149156
{get_context!(), schema.__table__(), :__insert}
150-
|> Query.get_templated_query_id(:all, %{schema: schema})
157+
|> Query.get_templated_query_id([:*], %{schema: schema})
151158
|> insert_sql(struct, opts)
152159
end
153160

@@ -288,6 +295,18 @@ defmodule Feeb.DB do
288295
LocalState.get_current_context!().context
289296
end
290297

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+
291310
defp get_bindings(query_id, struct) do
292311
{_, {_, params_bindings}, _} = Query.fetch!(query_id)
293312

lib/feeb/db/query.ex

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,36 +33,59 @@ 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)
4444

45-
{sql, {fields_bindings, params_bindings}, qt} = fetch!(query_id)
45+
adhoc_query_name = :"#{query_name}$#{get_query_name_suffix(sorted_target_fields)}"
46+
adhoc_query_id = {context, domain, adhoc_query_name}
47+
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
5765
end
5866

5967
def get_templated_query_id(query_id, target_fields, meta \\ %{})
6068

69+
def get_templated_query_id({context, domain, query_name} = query_id, target_fields, _meta)
70+
when query_name in [:__all, :__fetch] do
71+
model = Schema.get_model_from_query_id(query_id)
72+
73+
real_query_id = {context, domain, :"#{query_name}$#{get_query_name_suffix(target_fields)}"}
74+
75+
case get(real_query_id) do
76+
{_, _, _} = _compiled_query ->
77+
real_query_id
78+
79+
nil ->
80+
compile_templated_query(query_name, query_id, target_fields, model)
81+
end
82+
end
83+
6184
def get_templated_query_id({context, domain, :__insert} = query_id, target_fields, _meta) do
6285
model = Schema.get_model_from_query_id(query_id)
6386

6487
target_fields =
65-
if target_fields == :all do
88+
if target_fields == [:*] do
6689
model.__cols__()
6790
else
6891
raise "Not supported for now, add & test it once needed"
@@ -106,16 +129,15 @@ defmodule Feeb.DB.Query do
106129
end
107130
end
108131

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

113135
case get(query_id) do
114136
{_, _, _} = _compiled_query ->
115137
query_id
116138

117139
nil ->
118-
compile_templated_query(query_name, query_id, target_fields, model)
140+
compile_templated_query(:__delete, query_id, target_fields, model)
119141
end
120142
end
121143

@@ -139,34 +161,32 @@ defmodule Feeb.DB.Query do
139161
primary_keys = model.__primary_keys__()
140162
assert_adhoc_query!(primary_keys, query_id, model)
141163

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

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

154171
query_id
155172
end
156173

157-
defp compile_templated_query(:__all, {_, domain, _} = query_id, _target_fields, _model) do
158-
sql = "SELECT * FROM #{domain};"
159-
adhoc_query = {sql, {[:*], []}, :select}
174+
defp compile_templated_query(:__all, {_, domain, _} = query_id, target_fields, model) do
175+
sql = "#{generate_select_clause(target_fields, model)} FROM #{domain};"
176+
adhoc_query = {sql, {target_fields, []}, :select}
160177
append_runtime_query(query_id, adhoc_query)
161178
query_id
162179
end
163180

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

169-
adhoc_query = {sql, {[:*], primary_keys}, :select}
185+
select_clause = generate_select_clause(target_fields, model)
186+
where_clause = generate_where_clause(primary_keys)
187+
sql = "#{select_clause} FROM #{domain} #{where_clause};"
188+
189+
adhoc_query = {sql, {target_fields, primary_keys}, :select}
170190
append_runtime_query(query_id, adhoc_query)
171191
query_id
172192
end
@@ -232,6 +252,16 @@ defmodule Feeb.DB.Query do
232252
end
233253
end
234254

255+
defp get_query_name_suffix(target_fields) when is_list(target_fields) do
256+
target_fields
257+
|> Enum.sort()
258+
|> Enum.reduce([], fn field, acc ->
259+
["#{field}" | acc]
260+
end)
261+
|> Enum.reverse()
262+
|> Enum.join("$")
263+
end
264+
235265
defp maybe_inject_returning_clause(nil, _), do: nil
236266
defp maybe_inject_returning_clause(query, []), do: query
237267

@@ -253,6 +283,37 @@ defmodule Feeb.DB.Query do
253283
defp get_returning_fields({_, _, operation}) when operation in [:update, :delete],
254284
do: "*"
255285

286+
defp generate_select_clause([:*], _), do: "SELECT *"
287+
288+
defp generate_select_clause(fields, model) when is_list(fields) do
289+
valid_fields = model.__cols__()
290+
291+
select_conditions =
292+
fields
293+
|> Enum.reduce([], fn field, acc ->
294+
if field not in valid_fields,
295+
do: raise("Can't select #{inspect(field)}; not a valid field for #{model}")
296+
297+
["#{field}" | acc]
298+
end)
299+
|> Enum.reverse()
300+
|> Enum.join(", ")
301+
302+
"SELECT #{select_conditions}"
303+
end
304+
305+
defp generate_update_set_clause(fields) when is_list(fields) do
306+
set_conditions =
307+
fields
308+
|> Enum.reduce([], fn field, acc ->
309+
["#{field} = ?" | acc]
310+
end)
311+
|> Enum.reverse()
312+
|> Enum.join(", ")
313+
314+
"SET #{set_conditions}"
315+
end
316+
256317
defp generate_where_clause(primary_keys) when is_list(primary_keys) do
257318
where_conditions =
258319
primary_keys

lib/feeb/db/repo.ex

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@ defmodule Feeb.DB.Repo do
283283

284284
defp format_result(:one, _, _, [row], _, %{format: :raw}), do: {:ok, row}
285285

286-
defp format_result(:one, query_id, query, [row], _, %{format: :type}),
287-
do: {:ok, create_types_from_rows(query_id, query, [row]) |> List.first()}
286+
defp format_result(:one, query_id, query, [row], _, %{format: :map}),
287+
do: {:ok, create_maps_from_rows(query_id, query, [row]) |> List.first()}
288288

289289
defp format_result(:one, query_id, query, [row], _, _),
290290
do: {:ok, create_schema_from_rows(query_id, query, [row]) |> List.first()}
@@ -295,13 +295,13 @@ defmodule Feeb.DB.Repo do
295295
defp format_result(:all, _, _, [], _, _), do: {:ok, []}
296296
defp format_result(:all, _, _, rows, _, %{format: :raw}), do: {:ok, rows}
297297

298-
defp format_result(:all, query_id, query, rows, _, %{format: :type}),
299-
do: {:ok, create_types_from_rows(query_id, query, rows)}
298+
defp format_result(:all, query_id, query, rows, _, %{format: :map}),
299+
do: {:ok, create_maps_from_rows(query_id, query, rows)}
300300

301301
defp format_result(:all, query_id, query, rows, _, %{format: :schema}),
302302
do: {:ok, create_schema_from_rows(query_id, query, rows)}
303303

304-
defp format_result(:insert, _, _, [], _, %{format: format}) when format in [:raw, :type],
304+
defp format_result(:insert, _, _, [], _, %{format: format}) when format in [:raw, :map],
305305
do: {:ok, nil}
306306

307307
defp format_result(:insert, query_id, query, [], bindings, %{format: :schema}) do
@@ -364,7 +364,7 @@ defmodule Feeb.DB.Repo do
364364
Enum.map(rows, fn row -> Schema.from_row(model, model.__cols__(), row) end)
365365
end
366366

367-
defp create_types_from_rows(query_id, {_, {fields_bindings, _}, :select} = query, rows) do
367+
defp create_maps_from_rows(query_id, {_, {fields_bindings, _}, :select} = query, rows) do
368368
# Performance-wise, not the best solution, but I'd rather keep the code readable for a bit
369369
# longer. Simply create the full schema and use only the fields the user selected
370370
create_schema_from_rows(query_id, query, rows)

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,

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

0 commit comments

Comments
 (0)