Current section

Files

Jump to
ravix lib rql query_parser.ex
Raw

lib/rql/query_parser.ex

defmodule Ravix.RQL.QueryParser do
@moduledoc false
require OK
alias Ravix.RQL.Query
@non_aliasable_fields ["id()", "count()", "sum()", :"id()", :"count()", :"sum()"]
@doc """
Receives a `Ravix.RQL.Query` object and parses it to a RQL query string
"""
@spec parse(Query.t()) :: {:error, any} | {:ok, Query.t()}
def parse(%Query{} = query) do
OK.for do
parsed_query =
query
|> parse_stmt(query.from_token)
|> parse_stmt(query.group_token)
|> parse_stmt(query.where_token)
|> parse_stmts(query.and_tokens)
|> parse_stmts(query.or_tokens)
|> parse_stmt(query.order_token)
|> parse_stmt(query.update_token)
|> parse_stmt(query.select_token)
|> parse_stmt(query.limit_token)
after
parsed_query
end
end
defp parse_stmts({:ok, query}, stmts), do: parse_stmts(query, stmts)
defp parse_stmts({:error, err}, _),
do: {:error, err}
defp parse_stmts(%Query{} = query, []), do: query
defp parse_stmts(%Query{} = query, stmts) do
stmts
|> Enum.reduce(query, fn stmt, acc ->
case parse_stmt(acc, stmt) do
{:ok, stmts} -> Map.merge(acc, stmts)
{:error, err} -> {:error, err}
end
end)
end
defp parse_stmt({:ok, query}, stmt), do: parse_stmt(query, stmt)
defp parse_stmt(%Query{} = query, nil), do: query
defp parse_stmt({:error, err}, _),
do: {:error, err}
# credo:disable-for-next-line
defp parse_stmt(%Query{} = query, stmt) do
case stmt.token do
:from -> parse_from(query, stmt)
:from_index -> parse_from_index(query, stmt)
:select -> parse_select(query, stmt)
:select_function -> parse_select_function(query, stmt)
:group_by -> parse_group_by(query, stmt)
:update -> parse_update(query, stmt)
:where -> parse_where(query, stmt)
:and -> parse_and(query, stmt)
:or -> parse_or(query, stmt)
:not -> parse_not(query, stmt)
:order_by -> parse_ordering(query, stmt)
:limit -> parse_limit(query, stmt)
_ -> {:error, :invalid_statement}
end
end
defp parse_condition_stmt(%Query{} = query, nil), do: query
# credo:disable-for-next-line
defp parse_condition_stmt(%Query{} = query, condition) do
query_part =
case condition.token do
:greater_than ->
"#{parse_field(query, condition.field)} > $p#{query.params_count}"
:eq ->
"#{parse_field(query, condition.field)} = $p#{query.params_count}"
:greater_than_or_eq ->
"#{parse_field(query, condition.field)} >= $p#{query.params_count}"
:lower_than ->
"#{parse_field(query, condition.field)} < $p#{query.params_count}"
:lower_than_or_eq ->
"#{parse_field(query, condition.field)} <= $p#{query.params_count}"
:between ->
"#{parse_field(query, condition.field)} between $p#{query.params_count} and $p#{query.params_count + 1}"
:in ->
"#{parse_field(query, condition.field)} in " <>
"(" <> parse_params_to_positional_string(query, condition.params) <> ")"
# This one is weird yeah, RavenDB only accepts the NOT in binary operations (OR | AND), so we need
# to be a little hackish
:nin ->
"#{parse_field(query, condition.field)} != null and not #{parse_field(query, condition.field)} in " <>
"(" <> parse_params_to_positional_string(query, condition.params) <> ")"
:ne ->
"#{parse_field(query, condition.field)} != $p#{query.params_count}"
_ ->
{:error, :invalid_condition_param}
end
case query_part do
{:error, :invalid_condition_param} -> {:error, :invalid_condition_param}
_ -> {:ok, query_part}
end
end
defp parse_from(%Query{} = query, from_token) do
query_fragment =
"from #{from_token.document_or_index}" <> parse_alias(query, from_token.document_or_index)
{:ok, append_query_fragment(query, query_fragment)}
end
defp parse_from_index(%Query{} = query, from_token) do
query_fragment =
"from #{from_token.document_or_index}" <> parse_alias(query, from_token.document_or_index)
{:ok, append_query_fragment(query, query_fragment)}
end
defp parse_select(%Query{} = query, select_token) do
query_fragment =
" select " <> Enum.map_join(select_token.fields, ", ", &parse_field(query, &1))
{:ok, append_query_fragment(query, query_fragment)}
end
defp parse_select_function(%Query{} = query, projected_select_token) do
query_fragment =
" select { " <>
Enum.map_join(projected_select_token.fields, "\n", fn {field, projected} ->
Atom.to_string(field) <> " : " <> projected
end) <> " }"
{:ok, append_query_fragment(query, query_fragment)}
end
defp parse_update(%Query{} = query, update_token) do
fields_to_update =
update_token.fields
|> Enum.reduce(%{updates: [], current_position: query.params_count}, fn field, acc ->
%{
acc
| updates:
acc.updates ++
[
"#{parse_field(query, field.name)} #{parse_assignment_operation(field.operation)} $p#{acc.current_position}"
],
current_position: acc.current_position + 1
}
end)
field_values = Enum.map(update_token.fields, fn field -> field.value end)
positional_params = parse_params_to_positional(query, field_values)
query_params =
Map.merge(
query.query_params,
positional_params
)
{:ok,
%Query{
query
| query_params: query_params,
params_count: fields_to_update.current_position
}
|> append_query_fragment(" update{ " <> Enum.join(fields_to_update.updates, ", ") <> " }")}
end
defp parse_assignment_operation(operation) do
case operation do
:set -> "="
:inc -> "+="
:dec -> "-="
end
end
defp parse_where(%Query{} = query, where_token) do
parse_locator_stmt(query, where_token, "where", false)
end
defp parse_and(%Query{} = query, and_token, negated \\ false) do
parse_locator_stmt(query, and_token, "and", negated)
end
defp parse_or(%Query{} = query, or_token, negated \\ false) do
parse_locator_stmt(query, or_token, "or", negated)
end
defp parse_not(%Query{} = query, not_token) do
case not_token.condition do
%Ravix.RQL.Tokens.And{} = and_token -> parse_and(query, and_token, true)
%Ravix.RQL.Tokens.Or{} = or_token -> parse_or(query, or_token, true)
end
end
defp parse_ordering(%Query{} = query, order_by_token) do
query_fragment =
" order by " <>
Enum.map_join(order_by_token.fields, ",", fn field ->
"#{parse_field(query, field.name)}#{parse_ordering_type(field.type)} #{Atom.to_string(field.order)}"
end)
{:ok, append_query_fragment(query, query_fragment)}
end
defp parse_ordering_type(ordering_type) do
case ordering_type do
:float -> " as float"
:number -> " as long"
_ -> ""
end
end
defp parse_group_by(%Query{} = query, group_by_token) do
query_fragment =
" group by " <> Enum.map_join(group_by_token.fields, ", ", &parse_field(query, &1))
{:ok, append_query_fragment(query, query_fragment)}
end
defp parse_limit(%Query{} = query, limit_token) do
query_fragment = " limit #{limit_token.skip}, #{limit_token.next}"
{:ok, append_query_fragment(query, query_fragment)}
end
defp parse_locator_stmt(%Query{} = query, stmt, locator, negated) do
OK.for do
condition <- parse_condition_stmt(query, stmt.condition)
positional_params = parse_params_to_positional(query, stmt.condition.params)
params_count = query.params_count + length(stmt.condition.params)
negated =
case negated do
true -> " not "
false -> " "
end
query_params =
Map.merge(
query.query_params,
positional_params
)
after
%Query{
query
| query_string: query.query_string <> " #{locator}#{negated}#{condition}",
query_params: query_params,
params_count: params_count
}
end
end
defp parse_params_to_positional(query, params) do
query.params_count..(query.params_count + length(params) - 1)
|> Enum.reduce(%{}, fn position, acc ->
Map.put(acc, "p#{position}", Enum.at(params, position - query.params_count))
end)
end
defp parse_params_to_positional_string(query, params) do
parse_params_to_positional(query, params)
|> Map.keys()
|> Enum.map_join(",", fn key -> "$#{key}" end)
end
defp parse_alias(%Query{aliases: aliases}, document) do
case Map.has_key?(aliases, document) do
true -> " as " <> Map.get(aliases, document)
false -> ""
end
end
defp parse_field(%Query{}, {field_name, field_alias})
when field_name in @non_aliasable_fields do
field_name <> " as #{field_alias}"
end
defp parse_field(%Query{aliases: aliases, from_token: from_token}, {field_name, field_alias}) do
case Map.has_key?(aliases, from_token.document_or_index) do
true -> Map.get(aliases, from_token.document_or_index) <> ".#{field_name} as #{field_alias}"
false -> field_name <> " as #{field_alias}"
end
end
defp parse_field(%Query{}, field) when field in @non_aliasable_fields, do: field
defp parse_field(%Query{aliases: aliases, from_token: from_token}, field) do
case Map.has_key?(aliases, from_token.document_or_index) do
true -> Map.get(aliases, from_token.document_or_index) <> ".#{field}"
false -> field
end
end
defp append_query_fragment(%Query{} = query, append) do
%Query{
query
| query_string: query.query_string <> "#{append}"
}
end
end