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
37 changes: 28 additions & 9 deletions lib/feeb/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,20 @@ defmodule Feeb.DB do

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

# TODO: See Feeb.DB, I also support `custom_fields`
def one({domain, :fetch}, bindings, opts) when is_list(bindings) do
target_fields = opts[:select] || [:*]

{get_context!(), domain, :__fetch}
|> Query.get_templated_query_id([], %{})
|> Query.get_templated_query_id(target_fields, %{})
|> one(bindings, opts)
end

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

def one({domain, query_name}, bindings, opts) when is_list(bindings) do
one({get_context!(), domain, query_name}, bindings, opts)
{get_context!(), domain, query_name}
|> get_query_id_for_select_query(opts)
|> one(bindings, opts)
end

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

def all(schema, _bindings, opts) when is_atom(schema) do
target_fields = opts[:select] || [:*]

{get_context!(), schema.__table__(), :__all}
|> Query.get_templated_query_id(:all, %{})
|> Query.get_templated_query_id(target_fields, %{})
|> all([], opts)
end

def all({domain, query_name}, bindings, opts) do
all({get_context!(), domain, query_name}, bindings, opts)
def all({domain, query_name}, bindings, opts) when is_list(bindings) do
{get_context!(), domain, query_name}
|> get_query_id_for_select_query(opts)
|> all(bindings, opts)
end

def all({_, domain, query_name}, bindings, opts) do
bindings = if is_list(bindings), do: bindings, else: [bindings]
def all({domain, query_name}, value, opts), do: all({domain, query_name}, [value], opts)

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

def insert(%schema{} = struct, opts \\ []) do
{get_context!(), schema.__table__(), :__insert}
|> Query.get_templated_query_id(:all, %{schema: schema})
|> Query.get_templated_query_id([:*], %{schema: schema})
|> insert_sql(struct, opts)
end

Expand Down Expand Up @@ -288,6 +295,18 @@ defmodule Feeb.DB do
LocalState.get_current_context!().context
end

defp get_query_id_for_select_query(original_query_id, []), do: original_query_id

defp get_query_id_for_select_query(original_query_id, opts) do
target_fields = opts[:select] || [:*]

if target_fields == [:*] do
original_query_id
else
Query.compile_adhoc_query(original_query_id, target_fields)
end
end

defp get_bindings(query_id, struct) do
{_, {_, params_bindings}, _} = Query.fetch!(query_id)

Expand Down
119 changes: 90 additions & 29 deletions lib/feeb/db/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,59 @@ defmodule Feeb.DB.Query do
end

@doc """
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
Compiling an adhoc query is useful when the user wants to select custom fields off of a `SELECT *`
query. It's essentially a subset of the original query, with specific fields being selected.
"""
@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}
def compile_adhoc_query({context, domain, query_name} = original_query_id, target_fields) do
model = Schema.get_model_from_query_id(original_query_id)
valid_fields = model.__cols__()
sorted_target_fields = Enum.sort(target_fields)

{sql, {fields_bindings, params_bindings}, qt} = fetch!(query_id)
adhoc_query_name = :"#{query_name}$#{get_query_name_suffix(sorted_target_fields)}"
adhoc_query_id = {context, domain, adhoc_query_name}

{sql, {fields_bindings, params_bindings}, qt} = fetch!(original_query_id)

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

# "Compile" new query
new_sql = String.replace(sql, "*", Enum.join(custom_fields, ", "))
adhoc_q = {new_sql, {custom_fields, params_bindings}, qt}
Enum.each(target_fields, fn field ->
if field not in valid_fields,
do: raise("Can't select #{inspect(field)}; not a valid field for #{model}")
end)

# "Compile" new query (replaces `SELECT *` with `SELECT <sorted_target_fields>`)
new_sql = String.replace(sql, "*", Enum.join(sorted_target_fields, ", "), global: false)
adhoc_q = {new_sql, {sorted_target_fields, params_bindings}, qt}

append_runtime_query(adhoc_query_id, adhoc_q)
adhoc_query_id
end

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

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

real_query_id = {context, domain, :"#{query_name}$#{get_query_name_suffix(target_fields)}"}

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

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

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
if target_fields == [:*] do
model.__cols__()
else
raise "Not supported for now, add & test it once needed"
Expand Down Expand Up @@ -106,16 +129,15 @@ defmodule Feeb.DB.Query do
end
end

def get_templated_query_id({_context, _domain, query_name} = query_id, target_fields, _meta)
when query_name in [:__all, :__fetch, :__delete] do
def get_templated_query_id({_context, _domain, :__delete} = query_id, target_fields, _meta) 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, model)
compile_templated_query(:__delete, query_id, target_fields, model)
end
end

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

set_conditions =
target_fields
|> Enum.reduce([], fn field, acc ->
["#{field} = ?" | acc]
end)
|> Enum.reverse()
|> Enum.join(", ")
set_clause = generate_update_set_clause(target_fields)
where_clause = generate_where_clause(primary_keys)
sql = "UPDATE #{domain} #{set_clause} #{where_clause};"

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, _model) do
sql = "SELECT * FROM #{domain};"
adhoc_query = {sql, {[:*], []}, :select}
defp compile_templated_query(:__all, {_, domain, _} = query_id, target_fields, model) do
sql = "#{generate_select_clause(target_fields, model)} FROM #{domain};"
adhoc_query = {sql, {target_fields, []}, :select}
append_runtime_query(query_id, adhoc_query)
query_id
end

defp compile_templated_query(:__fetch, {_, domain, _} = query_id, _target_fields, model) do
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}
select_clause = generate_select_clause(target_fields, model)
where_clause = generate_where_clause(primary_keys)
sql = "#{select_clause} FROM #{domain} #{where_clause};"

adhoc_query = {sql, {target_fields, primary_keys}, :select}
append_runtime_query(query_id, adhoc_query)
query_id
end
Expand Down Expand Up @@ -232,6 +252,16 @@ defmodule Feeb.DB.Query do
end
end

defp get_query_name_suffix(target_fields) when is_list(target_fields) do
target_fields
|> Enum.sort()
|> Enum.reduce([], fn field, acc ->
["#{field}" | acc]
end)
|> Enum.reverse()
|> Enum.join("$")
end

defp maybe_inject_returning_clause(nil, _), do: nil
defp maybe_inject_returning_clause(query, []), do: query

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

defp generate_select_clause([:*], _), do: "SELECT *"

defp generate_select_clause(fields, model) when is_list(fields) do
valid_fields = model.__cols__()

select_conditions =
fields
|> Enum.reduce([], fn field, acc ->
if field not in valid_fields,
do: raise("Can't select #{inspect(field)}; not a valid field for #{model}")

["#{field}" | acc]
end)
|> Enum.reverse()
|> Enum.join(", ")

"SELECT #{select_conditions}"
end

defp generate_update_set_clause(fields) when is_list(fields) do
set_conditions =
fields
|> Enum.reduce([], fn field, acc ->
["#{field} = ?" | acc]
end)
|> Enum.reverse()
|> Enum.join(", ")

"SET #{set_conditions}"
end

defp generate_where_clause(primary_keys) when is_list(primary_keys) do
where_conditions =
primary_keys
Expand Down
12 changes: 6 additions & 6 deletions lib/feeb/db/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ defmodule Feeb.DB.Repo do

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

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

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

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

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

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

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

defp create_types_from_rows(query_id, {_, {fields_bindings, _}, :select} = query, rows) do
defp create_maps_from_rows(query_id, {_, {fields_bindings, _}, :select} = query, rows) do
# Performance-wise, not the best solution, but I'd rather keep the code readable for a bit
# longer. Simply create the full schema and use only the fields the user selected
create_schema_from_rows(query_id, query, rows)
Expand Down
3 changes: 2 additions & 1 deletion lib/feeb/db/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ defmodule Feeb.DB.Schema do
assert_env.(:schema)
end

defmacro cast(args, target_fields \\ unquote(:all)) do
# TODO: Why is this a macro?
defmacro cast(args, target_fields \\ unquote([:*])) do
quote do
meta = %{
valid?: true,
Expand Down
3 changes: 3 additions & 0 deletions priv/test/queries/test/all_types.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ select map_keys_atom from all_types;
-- :get_map
select map from all_types;

-- :get_by_integer
select * from all_types where integer = ?;

-- :get_max_integer
select max(integer) from all_types;

Expand Down
Loading