Current section
Files
Jump to
Current section
Files
lib/query.ex
defmodule Exddb.Query do
@moduledoc ~S"""
Simple language integrated query for DynamoDB tables
Queries are parsed to function calls to this module which return tuple like `{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}`
which in turn erlclouds' ddb2 will understand.
# Examples
iex> from r in TestModel, where: r.data_id == "post"
%Exddb.Query.QueryObject{model: TestModel, options: [], query: {"data_id", {:s, "post"}, :eq}}
iex> from r in ModelWithHashAndRange, where: r.data_id == "post" and r.timestamp == 10
%Exddb.Query.QueryObject{model: ModelWithHashAndRange, options: [], query: [{"data_id", {:s, "post"}, :eq}, {"timestamp", {:n, 10}, :eq}]}
In the examples above the `query` and `options` is what will be sent to `erlcloud_ddb2`.
Model is used to determine which DynamoDB table should be queried and how the results should be deserialized.
"""
defmacro __using__(_opts) do
quote do
import Exddb.Query
end
end
@doc ~S"""
This structure is used to store a query in the form its passed to erlcloud and metadata about the query.
"""
defmodule QueryObject do
defstruct model: nil, query: {}, options: []
end
@doc ~S"""
Convert Elixir expression into a function call to on of the query_by_ functions.
"""
defmacro from({:in, _, [var, module]}, opts) do
where_expr = Keyword.get(opts, :where)
opts = Keyword.delete(opts, :where)
evaluate_where where_expr, var, module, opts
end
#
# Compile time macro helpers
#
@doc ~S"""
Query by hash and range key
"""
def evaluate_where({:and, _, ops}, _var, module, opts) when is_list(ops) do
# first convert the query to AST
quoted = Enum.map(ops, fn(op) -> evaluate_query_part(op, module) end)
# then return AST for building QueryObject struct
quote do
%QueryObject{query: unquote(quoted), model: unquote(module), options: unquote(opts)}
end
end
@doc ~S"""
Query by hash key, only one comparison in `:where` expression that will be converted to function call
"""
def evaluate_where({compare_op, _, [{{:., _, [_expr_var, expr_var_field]}, _, _}, expect_field_value]}, _var, module, opts) do
quote do
query_by_hashkey(unquote(module), unquote(expr_var_field), unquote(expect_field_value), unquote(compare_op), unquote(opts))
end
end
def evaluate_query_part({op, _, [{{:., _, [_expr_var, expr_var_field]}, _, []}, [{:.., _, [range_start, range_end]}]]}, module) do
quote do
build_query_part(unquote(module), unquote(expr_var_field), unquote(range_start), unquote(range_end), unquote(op))
end
end
def evaluate_query_part({op, _, [{{:., _, [_expr_var, expr_var_field]}, _, []}, expect_field_value]}, module) do
quote do
build_query_part(unquote(module), unquote(expr_var_field), unquote(expect_field_value), unquote(op))
end
end
#
# Runtime functions
#
@doc ~S"""
Query by hash key, will try to build a tuple like this `{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}`
"""
def query_by_hashkey(module, field, value, op, opts) do
key_field = module.schema(:key)
case key_field do
{hash, _range} -> %QueryObject{query: build_query_part(module, hash, value, op), model: module, options: opts}
_ -> %QueryObject{query: build_query_part(module, field, value, op), model: module, options: opts}
end
end
def build_query_part(module, field, value, op) do
field_type = module.schema(:field, field)
encoded_value = field_type |> Exddb.Type.dump(value)
{Atom.to_string(field), {Exddb.Type.dynamo_type(field_type), encoded_value}, dynamize_op(op)}
end
def build_query_part(module, field, range_start, range_end, op) do
field_type = module.schema(:field, field)
encoded_value1 = field_type |> Exddb.Type.dump(range_start)
encoded_value2 = field_type |> Exddb.Type.dump(range_end)
{Atom.to_string(field), {{Exddb.Type.dynamo_type(field_type), encoded_value1}, {Exddb.Type.dynamo_type(field_type), encoded_value2}}, dynamize_op(op)}
end
def dynamize_op(:==), do: :eq
def dynamize_op(:<=), do: :le
def dynamize_op(:<), do: :lt
def dynamize_op(:>), do: :gt
def dynamize_op(:>=), do: :ge
def dynamize_op(:in), do: :between
end