Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions lib/feeb/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,8 @@ defmodule Feeb.DB do
end

def reload(%schema{} = struct) do
# Support for custom or composite PKs is TODO
primary_key_cols = [:id]

bindings =
Enum.map(primary_key_cols, fn col_name ->
Enum.map(schema.__primary_keys__() || [], fn col_name ->
Map.fetch!(struct, col_name)
end)

Expand Down
1 change: 0 additions & 1 deletion lib/feeb/db/boot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ defmodule Feeb.DB.Boot do
end

def get_all_models do
# TODO: Schema.List should be generated from a JSON that lives in the application's priv/ folder
Schema.List.all()
|> Enum.map(fn {context, modules} ->
modules_details =
Expand Down
76 changes: 56 additions & 20 deletions lib/feeb/db/query.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule Feeb.DB.Query do
require Logger
alias Feeb.DB.Schema
alias __MODULE__.Binding

@initial_q {"", {[], []}, nil}
Expand Down Expand Up @@ -35,7 +36,9 @@ defmodule Feeb.DB.Query do
Compiling an adhoc query is useful when you want to select custom fields
off of a "select *" query. It's like a subset of the original query
"""
@spec compile_adhoc_query(term, term) :: no_return
def compile_adhoc_query({context, domain, query_name} = query_id, custom_fields) do
raise "Deprecated; consider implementing this feature as part of `get_templated_query_id/3`"
query_name = :"#{query_name}$#{Enum.join(custom_fields, "$")}"
adhoc_query_id = {context, domain, query_name}

Expand All @@ -53,10 +56,14 @@ defmodule Feeb.DB.Query do
adhoc_query_id
end

def get_templated_query_id({context, domain, :__insert}, target_fields, meta) do
def get_templated_query_id(query_id, target_fields, meta \\ %{})

def get_templated_query_id({context, domain, :__insert} = query_id, target_fields, _meta) do
model = Schema.get_model_from_query_id(query_id)

target_fields =
if target_fields == :all do
meta.schema.__cols__()
model.__cols__()
else
raise "Not supported for now, add & test it once needed"
target_fields
Expand All @@ -70,11 +77,14 @@ defmodule Feeb.DB.Query do
real_query_id

nil ->
compile_templated_query(:__insert, real_query_id, target_fields)
compile_templated_query(:__insert, real_query_id, target_fields, model)
end
end

def get_templated_query_id({context, domain, :__update}, target_fields, _meta) do
def get_templated_query_id({context, domain, :__update} = query_id, target_fields, _meta) do
model = Schema.get_model_from_query_id(query_id)

# TODO: Transparently include `updated_at` (if present in the model)
target_fields = Enum.sort(target_fields)

query_name_suffix =
Expand All @@ -92,22 +102,24 @@ defmodule Feeb.DB.Query do
real_query_id

nil ->
compile_templated_query(:__update, real_query_id, target_fields)
compile_templated_query(:__update, real_query_id, target_fields, model)
end
end

def get_templated_query_id({_context, _domain, query_name} = query_id, target_fields, _meta)
when query_name in [:__all, :__fetch, :__delete] do
model = Schema.get_model_from_query_id(query_id)

case get(query_id) do
{_, _, _} = _compiled_query ->
query_id

nil ->
compile_templated_query(query_name, query_id, target_fields)
compile_templated_query(query_name, query_id, target_fields, model)
end
end

defp compile_templated_query(:__insert, {_, domain, _} = query_id, target_fields) do
defp compile_templated_query(:__insert, {_, domain, _} = query_id, target_fields, _model) do
columns_clause = target_fields |> Enum.join(", ")

values_clause =
Expand All @@ -123,42 +135,48 @@ defmodule Feeb.DB.Query do
query_id
end

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

set_clause =
set_conditions =
target_fields
|> Enum.reduce([], fn field, acc ->
["#{field} = ?" | acc]
end)
|> Enum.reverse()
|> Enum.join(", ")

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

query_id
end

defp compile_templated_query(:__all, {_, domain, _} = query_id, _target_fields) do
defp compile_templated_query(:__all, {_, domain, _} = query_id, _target_fields, _model) do
sql = "SELECT * FROM #{domain};"
adhoc_query = {sql, {[:*], []}, :select}
append_runtime_query(query_id, adhoc_query)
query_id
end

defp compile_templated_query(:__fetch, {_, domain, _} = query_id, _target_fields) do
sql = "SELECT * FROM #{domain} WHERE id = ?;"
adhoc_query = {sql, {[:*], [:id]}, :select}
defp compile_templated_query(:__fetch, {_, domain, _} = query_id, _target_fields, model) do
primary_keys = model.__primary_keys__()
assert_adhoc_query!(primary_keys, query_id, model)
sql = "SELECT * FROM #{domain} #{generate_where_clause(primary_keys)};"

adhoc_query = {sql, {[:*], primary_keys}, :select}
append_runtime_query(query_id, adhoc_query)
query_id
end

defp compile_templated_query(:__delete, {_, domain, _} = query_id, _target_fields) do
sql = "DELETE FROM #{domain} WHERE id = ?;"
adhoc_query = {sql, {[], [:id]}, :delete}
defp compile_templated_query(:__delete, {_, domain, _} = query_id, _target_fields, model) do
primary_keys = model.__primary_keys__()
assert_adhoc_query!(primary_keys, query_id, model)
sql = "DELETE FROM #{domain} #{generate_where_clause(primary_keys)};"

adhoc_query = {sql, {[], primary_keys}, :delete}
append_runtime_query(query_id, adhoc_query)
query_id
end
Expand Down Expand Up @@ -235,6 +253,24 @@ defmodule Feeb.DB.Query do
defp get_returning_fields({_, _, operation}) when operation in [:update, :delete],
do: "*"

defp generate_where_clause(primary_keys) when is_list(primary_keys) do
where_conditions =
primary_keys
|> Enum.reduce([], fn field, acc ->
["#{field} = ?" | acc]
end)
|> Enum.reverse()
|> Enum.join(" AND ")

"WHERE #{where_conditions}"
end

defp assert_adhoc_query!(nil, query_id, model) do
raise("Can't generate adhoc query #{inspect(query_id)} because #{inspect(model)} has no PKs")
end

defp assert_adhoc_query!(_, _, _), do: :ok

# Line-break
defp handle_line(<<>>, qs, id, q) when not is_nil(id) do
{sql, {fields_bindings, params_bindings}, query_type} = q
Expand Down
12 changes: 4 additions & 8 deletions lib/feeb/db/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -345,22 +345,22 @@ defmodule Feeb.DB.Repo do
defp create_schema_from_rows({_, :pragma, _}, _, rows), do: rows

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

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

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

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

Expand Down Expand Up @@ -399,10 +399,6 @@ defmodule Feeb.DB.Repo do
|> trunc()
end

defp get_model_from_query_id({context, domain, _}) do
:persistent_term.get({:db_table_models, {context, domain}})
end

# TODO: These pragma functions will be removed once that gets turned into a hook
def conditional_pragma_based_on_env(conn, :test, _), do: custom_pragma_for_test(conn)
def conditional_pragma_based_on_env(conn, _, true), do: custom_pragma_for_test(conn)
Expand Down
15 changes: 10 additions & 5 deletions lib/feeb/db/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ defmodule Feeb.DB.Schema do
@derived_fields []
end

if :primary_keys not in Module.attributes_in(__MODULE__) do
# We default to `[:id]` as primary keys iff the schema does not define custom PKs. We can't
# use an `is_nil/1` check because @primary_keys nil should not be overriden.
@primary_keys [:id]
end

defstruct Map.keys(@schema) ++ unquote(meta_keys)

# TODO: Inline?
Expand All @@ -105,15 +111,11 @@ defmodule Feeb.DB.Schema do
def __context__, do: @context
def __modded_fields__, do: @modded_fields
def __derived_fields__, do: @derived_fields
def __primary_keys__, do: @primary_keys
end
end

defmacro __after_compile__(env, _module) do
# santiy_checks()
# TODO:
# - Quais checks?
# - Que as env vars estao setadas

assert_env = fn var ->
if is_nil(Module.get_attribute(env.module, var)),
do: raise("Missing @#{var} attribute in #{env.module}")
Expand Down Expand Up @@ -364,6 +366,9 @@ defmodule Feeb.DB.Schema do
def get_private(%{__private__: private}, k), do: private[k]
def get_private!(%{__private__: private}, k), do: Map.fetch!(private, k)

def get_model_from_query_id({context, table, _}),
do: :persistent_term.get({:db_table_models, {context, table}})

defp add_missing_values(struct, f, f), do: struct

defp add_missing_values(struct, all_fields, added_fields) do
Expand Down
2 changes: 1 addition & 1 deletion priv/test/feebdb_schemas.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"test":["Elixir.Sample.AllTypes","Elixir.Sample.CustomTypes","Elixir.Sample.Friend","Elixir.Sample.Post"]}
{"test":["Elixir.Sample.AllTypes","Elixir.Sample.CustomTypes","Elixir.Sample.Friend","Elixir.Sample.OrderItems","Elixir.Sample.Post"]}
9 changes: 9 additions & 0 deletions priv/test/migrations/test/250426163848_order_items.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
price INTEGER,
inserted_at TEXT,
updated_at TEXT,
PRIMARY KEY (order_id, product_id)
) STRICT;
Empty file.
2 changes: 1 addition & 1 deletion test/db/boot_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule Feeb.DB.BootTest do

# Test migrations rarely change and thus can be hard-coded here (string because it gets
# formatted to 123_456_789_012 otherwise, which is hard to read).
expected_test_latest = "241108191351" |> String.to_integer()
expected_test_latest = "250426163848" |> String.to_integer()
test_latest = Migrator.get_latest_version(:test)

assert test_latest == expected_test_latest
Expand Down
Loading