Skip to content

Commit b22d94b

Browse files
committed
Add support for custom/composite PKs on adhoc queries
1 parent f6f244b commit b22d94b

5 files changed

Lines changed: 315 additions & 31 deletions

File tree

lib/feeb/db/query.ex

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule Feeb.DB.Query do
22
require Logger
3+
alias Feeb.DB.Schema
34
alias __MODULE__.Binding
45

56
@initial_q {"", {[], []}, nil}
@@ -53,10 +54,14 @@ defmodule Feeb.DB.Query do
5354
adhoc_query_id
5455
end
5556

56-
def get_templated_query_id({context, domain, :__insert}, target_fields, meta) do
57+
def get_templated_query_id(query_id, target_fields, meta \\ %{})
58+
59+
def get_templated_query_id({context, domain, :__insert} = query_id, target_fields, _meta) do
60+
model = Schema.get_model_from_query_id(query_id)
61+
5762
target_fields =
5863
if target_fields == :all do
59-
meta.schema.__cols__()
64+
model.__cols__()
6065
else
6166
raise "Not supported for now, add & test it once needed"
6267
target_fields
@@ -70,11 +75,14 @@ defmodule Feeb.DB.Query do
7075
real_query_id
7176

7277
nil ->
73-
compile_templated_query(:__insert, real_query_id, target_fields)
78+
compile_templated_query(:__insert, real_query_id, target_fields, model)
7479
end
7580
end
7681

77-
def get_templated_query_id({context, domain, :__update}, target_fields, _meta) do
82+
def get_templated_query_id({context, domain, :__update} = query_id, target_fields, _meta) do
83+
model = Schema.get_model_from_query_id(query_id)
84+
85+
# TODO: Transparently include `updated_at` (if present in the model)
7886
target_fields = Enum.sort(target_fields)
7987

8088
query_name_suffix =
@@ -92,22 +100,24 @@ defmodule Feeb.DB.Query do
92100
real_query_id
93101

94102
nil ->
95-
compile_templated_query(:__update, real_query_id, target_fields)
103+
compile_templated_query(:__update, real_query_id, target_fields, model)
96104
end
97105
end
98106

99107
def get_templated_query_id({_context, _domain, query_name} = query_id, target_fields, _meta)
100108
when query_name in [:__all, :__fetch, :__delete] do
109+
model = Schema.get_model_from_query_id(query_id)
110+
101111
case get(query_id) do
102112
{_, _, _} = _compiled_query ->
103113
query_id
104114

105115
nil ->
106-
compile_templated_query(query_name, query_id, target_fields)
116+
compile_templated_query(query_name, query_id, target_fields, model)
107117
end
108118
end
109119

110-
defp compile_templated_query(:__insert, {_, domain, _} = query_id, target_fields) do
120+
defp compile_templated_query(:__insert, {_, domain, _} = query_id, target_fields, _model) do
111121
columns_clause = target_fields |> Enum.join(", ")
112122

113123
values_clause =
@@ -123,42 +133,48 @@ defmodule Feeb.DB.Query do
123133
query_id
124134
end
125135

126-
defp compile_templated_query(:__update, {_, domain, _} = query_id, target_fields) do
127-
# Hard-coded for now, but we may need this to be dynamc in the future
128-
primary_key_col = :id
136+
defp compile_templated_query(:__update, {_, domain, _} = query_id, target_fields, model) do
137+
primary_keys = model.__primary_keys__()
138+
assert_adhoc_query!(primary_keys, query_id, model)
129139

130-
set_clause =
140+
set_conditions =
131141
target_fields
132142
|> Enum.reduce([], fn field, acc ->
133143
["#{field} = ?" | acc]
134144
end)
135145
|> Enum.reverse()
136146
|> Enum.join(", ")
137147

138-
sql = "UPDATE #{domain} SET #{set_clause} WHERE id = ?;"
139-
adhoc_query = {sql, {[], target_fields ++ [primary_key_col]}, :update}
148+
sql = "UPDATE #{domain} SET #{set_conditions} #{generate_where_clause(primary_keys)};"
149+
adhoc_query = {sql, {[], target_fields ++ primary_keys}, :update}
140150
append_runtime_query(query_id, adhoc_query)
141151

142152
query_id
143153
end
144154

145-
defp compile_templated_query(:__all, {_, domain, _} = query_id, _target_fields) do
155+
defp compile_templated_query(:__all, {_, domain, _} = query_id, _target_fields, _model) do
146156
sql = "SELECT * FROM #{domain};"
147157
adhoc_query = {sql, {[:*], []}, :select}
148158
append_runtime_query(query_id, adhoc_query)
149159
query_id
150160
end
151161

152-
defp compile_templated_query(:__fetch, {_, domain, _} = query_id, _target_fields) do
153-
sql = "SELECT * FROM #{domain} WHERE id = ?;"
154-
adhoc_query = {sql, {[:*], [:id]}, :select}
162+
defp compile_templated_query(:__fetch, {_, domain, _} = query_id, _target_fields, model) do
163+
primary_keys = model.__primary_keys__()
164+
assert_adhoc_query!(primary_keys, query_id, model)
165+
sql = "SELECT * FROM #{domain} #{generate_where_clause(primary_keys)};"
166+
167+
adhoc_query = {sql, {[:*], primary_keys}, :select}
155168
append_runtime_query(query_id, adhoc_query)
156169
query_id
157170
end
158171

159-
defp compile_templated_query(:__delete, {_, domain, _} = query_id, _target_fields) do
160-
sql = "DELETE FROM #{domain} WHERE id = ?;"
161-
adhoc_query = {sql, {[], [:id]}, :delete}
172+
defp compile_templated_query(:__delete, {_, domain, _} = query_id, _target_fields, model) do
173+
primary_keys = model.__primary_keys__()
174+
assert_adhoc_query!(primary_keys, query_id, model)
175+
sql = "DELETE FROM #{domain} #{generate_where_clause(primary_keys)};"
176+
177+
adhoc_query = {sql, {[], primary_keys}, :delete}
162178
append_runtime_query(query_id, adhoc_query)
163179
query_id
164180
end
@@ -235,6 +251,24 @@ defmodule Feeb.DB.Query do
235251
defp get_returning_fields({_, _, operation}) when operation in [:update, :delete],
236252
do: "*"
237253

254+
defp generate_where_clause(primary_keys) when is_list(primary_keys) do
255+
where_conditions =
256+
primary_keys
257+
|> Enum.reduce([], fn field, acc ->
258+
["#{field} = ?" | acc]
259+
end)
260+
|> Enum.reverse()
261+
|> Enum.join(" AND ")
262+
263+
"WHERE #{where_conditions}"
264+
end
265+
266+
defp assert_adhoc_query!(nil, query_id, model) do
267+
raise("Can't generate adhoc query #{inspect(query_id)} because #{inspect(model)} has no PKs")
268+
end
269+
270+
defp assert_adhoc_query!(_, _, _), do: :ok
271+
238272
# Line-break
239273
defp handle_line(<<>>, qs, id, q) when not is_nil(id) do
240274
{sql, {fields_bindings, params_bindings}, query_type} = q

lib/feeb/db/repo.ex

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -345,22 +345,22 @@ defmodule Feeb.DB.Repo do
345345
defp create_schema_from_rows({_, :pragma, _}, _, rows), do: rows
346346

347347
defp create_schema_from_rows(query_id, {_, {fields_bindings, _}, :select}, rows) do
348-
model = get_model_from_query_id(query_id)
348+
model = Schema.get_model_from_query_id(query_id)
349349
Enum.map(rows, fn row -> Schema.from_row(model, fields_bindings, row) end)
350350
end
351351

352352
defp create_schema_from_rows(query_id, {_, {_, params_bindings}, :insert}, rows) do
353-
model = get_model_from_query_id(query_id)
353+
model = Schema.get_model_from_query_id(query_id)
354354
Enum.map(rows, fn row -> Schema.from_row(model, params_bindings, row) end)
355355
end
356356

357357
defp create_schema_from_rows(query_id, {_, _, :update}, rows) do
358-
model = get_model_from_query_id(query_id)
358+
model = Schema.get_model_from_query_id(query_id)
359359
Enum.map(rows, fn row -> Schema.from_row(model, model.__cols__(), row) end)
360360
end
361361

362362
defp create_schema_from_rows(query_id, {_, _, :delete}, rows) do
363-
model = get_model_from_query_id(query_id)
363+
model = Schema.get_model_from_query_id(query_id)
364364
Enum.map(rows, fn row -> Schema.from_row(model, model.__cols__(), row) end)
365365
end
366366

@@ -399,10 +399,6 @@ defmodule Feeb.DB.Repo do
399399
|> trunc()
400400
end
401401

402-
defp get_model_from_query_id({context, domain, _}) do
403-
:persistent_term.get({:db_table_models, {context, domain}})
404-
end
405-
406402
# TODO: These pragma functions will be removed once that gets turned into a hook
407403
def conditional_pragma_based_on_env(conn, :test, _), do: custom_pragma_for_test(conn)
408404
def conditional_pragma_based_on_env(conn, _, true), do: custom_pragma_for_test(conn)

lib/feeb/db/schema.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,9 @@ defmodule Feeb.DB.Schema do
366366
def get_private(%{__private__: private}, k), do: private[k]
367367
def get_private!(%{__private__: private}, k), do: Map.fetch!(private, k)
368368

369+
def get_model_from_query_id({context, table, _}),
370+
do: :persistent_term.get({:db_table_models, {context, table}})
371+
369372
defp add_missing_values(struct, f, f), do: struct
370373

371374
defp add_missing_values(struct, all_fields, added_fields) do

test/db/db_test.exs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +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 Sample.{AllTypes, CustomTypes, Friend, Post}
5+
alias Sample.{AllTypes, CustomTypes, Friend, OrderItems, Post}
66
alias Sample.Types.TypedID
77

88
@context :test
@@ -406,6 +406,29 @@ defmodule Feeb.DBTest do
406406
assert nil == DB.one({:friends, :get_by_id}, [0])
407407
end
408408

409+
test ":fetch templated query works", %{shard_id: shard_id} do
410+
DB.begin(@context, shard_id, :write)
411+
412+
assert %{id: 1, name: "Phoebe"} = DB.one({:friends, :fetch}, [1])
413+
assert nil == DB.one({:friends, :fetch}, [500])
414+
end
415+
416+
test ":fetch templated query works on schema with composite PKs", %{shard_id: shard_id} do
417+
DB.begin(@context, shard_id, :write)
418+
419+
%{order_id: 1, product_id: 2, quantity: 10, price: 50}
420+
|> OrderItems.new()
421+
|> DB.insert!()
422+
423+
order_item = DB.one({:order_items, :fetch}, [1, 2])
424+
assert order_item.order_id == 1
425+
assert order_item.product_id == 2
426+
assert order_item.quantity == 10
427+
assert order_item.price == 50
428+
429+
assert nil == DB.one({:order_items, :fetch}, [2, 1])
430+
end
431+
409432
test "supports the :format flag", %{shard_id: shard_id} do
410433
DB.begin(@context, shard_id, :write)
411434

@@ -560,6 +583,20 @@ defmodule Feeb.DBTest do
560583
refute post.updated_at == new_post.updated_at
561584
end
562585

586+
test "updates the struct on schema with composite PKs", %{shard_id: shard_id} do
587+
DB.begin(@context, shard_id, :write)
588+
589+
order_item =
590+
%{order_id: 1, product_id: 2, quantity: 10, price: 50}
591+
|> OrderItems.new()
592+
|> DB.insert!()
593+
594+
assert {:ok, %{order_id: 1, product_id: 2, quantity: 20}} =
595+
order_item
596+
|> OrderItems.update(%{quantity: 20})
597+
|> DB.update()
598+
end
599+
563600
test "update attempt that failed to find a matching entry", %{shard_id: shard_id} do
564601
DB.begin(@context, shard_id, :write)
565602

@@ -655,6 +692,20 @@ defmodule Feeb.DBTest do
655692
refute DB.one({:friends, :get_by_id}, [1])
656693
end
657694

695+
test "deletes the struct (schema with composite PKs)", %{shard_id: shard_id} do
696+
DB.begin(@context, shard_id, :write)
697+
698+
order_item =
699+
%{order_id: 1, product_id: 2, quantity: 10, price: 50}
700+
|> OrderItems.new()
701+
|> DB.insert!()
702+
703+
assert {:ok, _} = DB.delete(order_item)
704+
705+
# OrderItem has been deleted
706+
assert [] == DB.all(OrderItems)
707+
end
708+
658709
test "delete attempt that failed to find a matching entry", %{shard_id: shard_id} do
659710
DB.begin(@context, shard_id, :write)
660711
friend = DB.one({:friends, :get_by_id}, [1])

0 commit comments

Comments
 (0)