Current section
Files
Jump to
Current section
Files
lib/connection/query/expression.ex
defmodule MssqlEcto.Connection.Query.Expression do
@moduledoc false
alias MssqlEcto.Connection.Query
import MssqlEcto.Connection.Helper
binary_ops = [
==: " = ",
!=: " != ",
<=: " <= ",
>=: " >= ",
<: " < ",
>: " > ",
+: " + ",
-: " - ",
*: " * ",
/: " / ",
and: " AND ",
or: " OR ",
ilike: " ILIKE ",
like: " LIKE "
]
@binary_ops Keyword.keys(binary_ops)
Enum.map(binary_ops, fn {op, str} ->
defp handle_call(unquote(op), 2), do: {:binary_op, unquote(str)}
end)
defp handle_call(fun, _arity), do: {:fun, Atom.to_string(fun)}
defp op_to_binary({op, _, [_, _]} = expr, sources, query) when op in @binary_ops do
paren_expr(expr, sources, query)
end
defp op_to_binary(expr, sources, query) do
expr(expr, sources, query)
end
def paren_expr(false, _sources, _query), do: "(0=1)"
def paren_expr(true, _sources, _query), do: "(1=1)"
def paren_expr(expr, sources, query) do
[?(, expr(expr, sources, query), ?)]
end
def expr(%Ecto.SubQuery{query: query}, _sources, _query) do
[?(, Query.all(query), ?)]
end
def expr({:^, [], [_]}, _sources, _query) do
[??]
end
def expr({{:., _, [{:&, _, [idx]}, field]}, _, []}, sources, _query) when is_atom(field) do
quote_qualified_name(field, sources, idx)
end
def expr({:&, _, [idx]}, sources, query) do
{_source, name, _schema} = elem(sources, idx)
error!(
query,
"Microsoft SQL Server requires a schema module when using selector " <>
"#{inspect(name)} but none was given. " <>
"Please specify a schema or specify exactly which fields from " <>
"#{inspect(name)} you desire"
)
end
def expr({:&, _, [idx, fields, _counter]}, sources, query) do
{_, name, schema} = elem(sources, idx)
if is_nil(schema) and is_nil(fields) do
error!(
query,
"Microsoft SQL Server requires a schema module when using selector " <>
"#{inspect(name)} but none was given. " <>
"Please specify a schema or specify exactly which fields from " <>
"#{inspect(name)} you desire"
)
end
intersperse_map(fields, ", ", &[name, ?. | quote_name(&1)])
end
def expr({:in, _, [_left, []]}, _sources, _query) do
"0=1"
end
def expr({:in, _, [left, right]}, sources, query) when is_list(right) do
args = intersperse_map(right, ?,, &expr(&1, sources, query))
[expr(left, sources, query), " IN (", args, ?)]
end
def expr({:in, _, [_, {:^, _, [_, 0]}]}, _sources, _query) do
"0=1"
end
def expr({:in, _, [left, {:^, _, [_ix, length]}]}, sources, query) do
args =
Enum.map(1..length, fn _ -> [??] end)
|> Enum.intersperse(?,)
[expr(left, sources, query), " IN (", args, ?)]
end
def expr({:in, _, [left, right]}, sources, query) do
[expr(left, sources, query), " = ANY(", expr(right, sources, query), ?)]
end
def expr({:is_nil, _, [arg]}, sources, query) do
[expr(arg, sources, query) | " IS NULL"]
end
def expr({:not, _, [expr]}, sources, query) do
case expr do
{fun, _, _} when fun in @binary_ops ->
["NOT (", expr(expr, sources, query), ?)]
_ ->
["~(", expr(expr, sources, query), ?)]
end
end
def expr({:fragment, _, [kw]}, _sources, query)
when is_list(kw) or tuple_size(kw) == 3 do
error!(
query,
"Microsoft SQL Server adapter does not support keyword or interpolated fragments"
)
end
def expr({:fragment, _, parts}, sources, query) do
Enum.map(parts, fn
{:raw, part} -> part
{:expr, expr} -> expr(expr, sources, query)
end)
|> parens_for_select()
end
# TODO timestamp and date types? is this correct
def expr({:datetime_add, _, [datetime, count, interval]}, sources, query) do
[
expr(datetime, sources, query),
"::timestamp + ",
interval(count, interval, sources, query)
]
end
def expr({:date_add, _, [date, count, interval]}, sources, query) do
[
?(,
expr(date, sources, query),
"::date + ",
interval(count, interval, sources, query) | ")::date"
]
end
def expr({:filter, _, [agg, filter]}, sources, query) do
aggregate = expr(agg, sources, query)
[aggregate, " FILTER (WHERE ", expr(filter, sources, query), ?)]
end
def expr({:over, _, [agg, name]}, sources, query) when is_atom(name) do
aggregate = expr(agg, sources, query)
[aggregate, " OVER " | quote_name(name)]
end
def expr({:over, _, [agg, kw]}, sources, query) do
aggregate = expr(agg, sources, query)
[aggregate, " OVER ", window_exprs(kw, sources, query)]
end
def expr({:{}, _, elems}, sources, query) do
[?(, intersperse_map(elems, ?,, &expr(&1, sources, query)), ?)]
end
def expr({:count, _, []}, _sources, _query), do: "count(*)"
def expr({fun, _, args}, sources, query) when is_atom(fun) and is_list(args) do
{modifier, args} =
case args do
[rest, :distinct] -> {"DISTINCT ", [rest]}
_ -> {[], args}
end
case handle_call(fun, length(args)) do
{:binary_op, op} ->
[left, right] = args
[op_to_binary(left, sources, query), op | op_to_binary(right, sources, query)]
{:fun, fun} ->
[fun, ?(, modifier, intersperse_map(args, ", ", &expr(&1, sources, query)), ?)]
end
end
def expr(list, sources, query) when is_list(list) do
["ARRAY[", intersperse_map(list, ?,, &expr(&1, sources, query)), ?]]
end
def expr(%Decimal{} = decimal, _sources, _query) do
Decimal.to_string(decimal, :normal)
end
def expr(%Ecto.Query.Tagged{value: binary, type: :binary}, _sources, _query)
when is_binary(binary) do
["0x", Base.encode16(binary, case: :lower)]
end
def expr(%Ecto.Query.Tagged{value: other, type: type}, sources, query) do
["CAST(", expr(other, sources, query), " AS ", tagged_to_db(type) | ")"]
end
def expr(nil, _sources, _query), do: "NULL"
def expr(true, _sources, _query), do: "1"
def expr(false, _sources, _query), do: "0"
def expr(literal, _sources, _query) when is_binary(literal) do
[?\', escape_string(literal), ?\']
end
def expr(literal, _sources, _query) when is_integer(literal) do
Integer.to_string(literal)
end
def expr(literal, _sources, _query) when is_float(literal) do
[Float.to_string(literal)]
end
defp parens_for_select([first_expr | _] = expr) do
if is_binary(first_expr) and String.starts_with?(first_expr, ["SELECT", "select"]) do
[?(, expr, ?)]
else
expr
end
end
defp interval(count, interval, _sources, _query) when is_integer(count) do
["interval '", String.Chars.Integer.to_string(count), ?\s, interval, ?\']
end
defp interval(count, interval, _sources, _query) when is_float(count) do
count = :erlang.float_to_binary(count, [:compact, decimals: 16])
["interval '", count, ?\s, interval, ?\']
end
# TODO numeric data type? Is this correct?
defp interval(count, interval, sources, query) do
[
?(,
expr(count, sources, query),
"::numeric * ",
interval(1, interval, sources, query),
?)
]
end
defp tagged_to_db({:array, type}), do: [tagged_to_db(type), ?[, ?]]
# Always use the largest possible type for integers
defp tagged_to_db(:id), do: "int"
defp tagged_to_db(:integer), do: "int"
defp tagged_to_db(type), do: ecto_to_db(type)
def window_exprs(kw, sources, query) do
[?(, intersperse_map(kw, ?\s, &window_expr(&1, sources, query)), ?)]
end
defp window_expr({:partition_by, fields}, sources, query) do
["PARTITION BY " | intersperse_map(fields, ", ", &expr(&1, sources, query))]
end
defp window_expr({:order_by, fields}, sources, query) do
["ORDER BY " | intersperse_map(fields, ", ", &order_by_expr(&1, sources, query))]
end
defp window_expr({:frame, {:fragment, _, _} = fragment}, sources, query) do
expr(fragment, sources, query)
end
def order_by_expr({dir, expr}, sources, query) do
str = expr(expr, sources, query)
case dir do
:asc -> str
:asc_nulls_last -> [str | " ASC NULLS LAST"]
:asc_nulls_first -> [str | " ASC NULLS FIRST"]
:desc -> [str | " DESC"]
:desc_nulls_last -> [str | " DESC NULLS LAST"]
:desc_nulls_first -> [str | " DESC NULLS FIRST"]
end
end
end