Current section
Files
Jump to
Current section
Files
lib/ecto/adapters/quack_db/query.ex
if Code.ensure_loaded?(Ecto.Query) do
defmodule Ecto.Adapters.QuackDB.Query do
@moduledoc """
Read-oriented Ecto query SQL generation for QuackDB.
This module turns supported `Ecto.Query` ASTs into DuckDB SQL iodata.
Unsupported analytical shapes raise explicit `QuackDB.Error` values so the
adapter does not emit misleading SQL.
"""
@spec all(Ecto.Query.t()) :: iodata()
def all(%Ecto.Query{} = query) do
assert_read_only_query!(query)
[
with_ctes(query.with_ctes),
select(query.select, query.distinct),
" FROM ",
source(query.from, 0),
joins(query.joins),
wheres(query.wheres),
group_bys(query.group_bys),
havings(query.havings),
windows(query.windows),
order_bys(query.order_bys),
limit(query.limit),
offset(query.offset)
]
end
defp assert_read_only_query!(%Ecto.Query{} = query) do
cond do
query.combinations != [] ->
unsupported!(:combinations, "Ecto combinations are not supported yet; use Repo.query/3")
query.lock != nil ->
unsupported!(:locks, "Ecto locks are not supported yet; use Repo.query/3")
true ->
:ok
end
end
defp with_ctes(nil), do: []
defp with_ctes(%Ecto.Query.WithExpr{queries: queries, recursive: recursive}) do
ctes =
Enum.map(queries, fn
{name, %{operation: :all}, %Ecto.Query{} = query} ->
[quote_identifier(name), " AS (", all(query), ")"]
{name, operation, %Ecto.Query{}} ->
unsupported!(
:cte,
"unsupported Ecto CTE operation for #{name}: #{inspect(operation)}"
)
end)
["WITH ", recursive(recursive), Enum.intersperse(ctes, ", "), " "]
end
defp recursive(true), do: "RECURSIVE "
defp recursive(false), do: []
defp select(nil, distinct), do: ["SELECT ", distinct(distinct), "*"]
defp select(%Ecto.Query.SelectExpr{expr: expr}, distinct) do
["SELECT ", distinct(distinct), select_expr(expr)]
end
defp distinct(nil), do: []
defp distinct(%{expr: true}), do: "DISTINCT "
defp distinct(%{expr: expressions}) when is_list(expressions) do
expressions =
expressions
|> Enum.map(fn {_direction, expression} -> expr(expression) end)
|> Enum.intersperse(", ")
["DISTINCT ON (", expressions, ") "]
end
defp distinct(%{expr: expression}), do: ["DISTINCT ON (", expr(expression), ") "]
defp select_expr({:%{}, _meta, fields}) do
fields
|> Enum.map(fn {alias_name, expr} -> [expr(expr), " AS ", quote_identifier(alias_name)] end)
|> Enum.intersperse(", ")
end
defp select_expr({:{}, _meta, fields}) do
fields |> Enum.map(&expr/1) |> Enum.intersperse(", ")
end
defp select_expr(fields) when is_list(fields) do
fields |> Enum.map(&expr/1) |> Enum.intersperse(", ")
end
defp select_expr(expr), do: expr(expr)
defp source(%{source: {table, nil}}, index) when is_binary(table) do
[source_name(table), " AS q", to_string(index)]
end
defp source(%{source: {table, schema}}, index)
when is_binary(table) and is_atom(schema) do
[source_name(table), " AS q", to_string(index)]
end
defp source(%{source: %Ecto.SubQuery{query: query}}, index) do
["(", all(query), ") AS q", to_string(index)]
end
defp source(%{source: {:fragment, _meta, parts}}, index) do
[fragment(parts), " AS q", to_string(index)]
end
defp source(_from, _index) do
unsupported!(
:source,
"only table, source helper, fragment, and subquery sources are supported in Ecto queries"
)
end
defp source_name(table) do
if QuackDB.Source.source?(table) do
table
else
quote_identifier(table)
end
end
defp joins([]), do: []
defp joins(joins) do
joins
|> Enum.with_index(1)
|> Enum.map(fn {join, index} ->
[" ", join_qualifier(join.qual), " ", source(join, index), " ON ", expr(join.on.expr)]
end)
end
defp join_qualifier(:inner), do: "INNER JOIN"
defp join_qualifier(:left), do: "LEFT OUTER JOIN"
defp join_qualifier(:right), do: "RIGHT OUTER JOIN"
defp join_qualifier(:full), do: "FULL OUTER JOIN"
defp join_qualifier(:cross), do: "CROSS JOIN"
defp join_qualifier(qualifier) do
unsupported!(:join, "unsupported Ecto join qualifier: #{inspect(qualifier)}")
end
defp wheres([]), do: []
defp wheres(wheres) do
expressions = Enum.map(wheres, fn %{expr: expression} -> expr(expression) end)
[" WHERE ", Enum.intersperse(expressions, " AND ")]
end
defp group_bys([]), do: []
defp group_bys(group_bys) do
expressions =
group_bys
|> Enum.flat_map(& &1.expr)
|> Enum.map(&expr/1)
[" GROUP BY ", Enum.intersperse(expressions, ", ")]
end
defp havings([]), do: []
defp havings(havings) do
expressions = Enum.map(havings, fn %{expr: expression} -> expr(expression) end)
[" HAVING ", Enum.intersperse(expressions, " AND ")]
end
defp windows([]), do: []
defp windows(windows) do
definitions =
Enum.map(windows, fn {name, window} ->
[quote_identifier(name), " AS (", window_expr(window.expr), ")"]
end)
[" WINDOW ", Enum.intersperse(definitions, ", ")]
end
defp window_expr(parts) do
parts
|> Enum.map(fn
{:partition_by, expressions} ->
["PARTITION BY ", expressions |> Enum.map(&expr/1) |> Enum.intersperse(", ")]
{:order_by, expressions} ->
["ORDER BY ", order_by_exprs(expressions)]
end)
|> Enum.intersperse(" ")
end
defp order_bys([]), do: []
defp order_bys(order_bys) do
expressions = order_bys |> Enum.flat_map(& &1.expr) |> order_by_exprs()
[" ORDER BY ", expressions]
end
defp order_by_exprs(expressions) do
expressions
|> Enum.map(fn {direction, expression} ->
[expr(expression), " ", order_direction(direction)]
end)
|> Enum.intersperse(", ")
end
defp limit(nil), do: []
defp limit(%{expr: expression}), do: [" LIMIT ", expr(expression)]
defp offset(nil), do: []
defp offset(%{expr: expression}), do: [" OFFSET ", expr(expression)]
defp expr({{:., _meta, [{:&, _binding_meta, [binding]}, field]}, _call_meta, []})
when is_integer(binding) and is_atom(field) do
["q", to_string(binding), ".", quote_identifier(field)]
end
defp expr({aggregate, _meta, [expression]})
when aggregate in [:count, :avg, :sum, :min, :max] do
[aggregate |> Atom.to_string() |> String.upcase(), "(", expr(expression), ")"]
end
defp expr({:count, _meta, []}), do: "COUNT(*)"
defp expr({window_function, _meta, []})
when window_function in [:row_number, :rank, :dense_rank, :percent_rank, :cume_dist] do
[window_function |> Atom.to_string() |> String.upcase(), "()"]
end
defp expr({:over, _meta, [expression, window]}) do
[expr(expression), " OVER ", over_expr(window)]
end
defp expr({:filter, _meta, [aggregate, predicate]}) do
[expr(aggregate), " FILTER (WHERE ", expr(predicate), ")"]
end
defp expr({:fragment, _meta, parts}), do: fragment(parts)
defp expr({op, _meta, [left, right]}) when op in [:==, :!=, :>, :<, :>=, :<=] do
["(", expr(left), " ", operator(op), " ", expr(right), ")"]
end
defp expr({op, _meta, [left, right]}) when op in [:and, :or] do
["(", expr(left), " ", op |> Atom.to_string() |> String.upcase(), " ", expr(right), ")"]
end
defp expr({op, _meta, [left, right]}) when op in [:+, :-, :*, :/] do
["(", expr(left), " ", operator(op), " ", expr(right), ")"]
end
defp expr({:in, _meta, [left, right]}) do
["(", expr(left), " IN ", in_expr(right), ")"]
end
defp expr({:not, _meta, [{:is_nil, _is_nil_meta, [expression]}]}) do
["(", expr(expression), " IS NOT NULL)"]
end
defp expr({:not, _meta, [expression]}) do
["(NOT ", expr(expression), ")"]
end
defp expr({:like, _meta, [left, right]}) do
["(", expr(left), " LIKE ", expr(right), ")"]
end
defp expr({:is_nil, _meta, [expression]}) do
["(", expr(expression), " IS NULL)"]
end
defp expr({:^, _meta, [_index]}), do: "?"
defp expr(%Ecto.Query.Tagged{value: value}), do: literal(value)
defp expr(value) when is_binary(value), do: literal(value)
defp expr(value) when is_integer(value) or is_float(value), do: to_string(value)
defp expr(value) when is_boolean(value), do: if(value, do: "TRUE", else: "FALSE")
defp expr(nil), do: "NULL"
defp expr(other) do
unsupported!(:expression, "unsupported Ecto query expression: #{inspect(other)}")
end
defp over_expr(window) when is_atom(window), do: quote_identifier(window)
defp over_expr(window) when is_list(window), do: ["(", window_expr(window), ")"]
defp fragment(parts) do
Enum.map(parts, fn
{:raw, value} -> value
{:expr, expression} -> expr(expression)
end)
end
defp in_expr(%Ecto.Query.Tagged{value: values}) when is_list(values), do: in_expr(values)
defp in_expr(values) when is_list(values) do
["(", values |> Enum.map(&expr/1) |> Enum.intersperse(", "), ")"]
end
defp in_expr(expression), do: expr(expression)
defp literal(value) when is_binary(value), do: ["'", String.replace(value, "'", "''"), "'"]
defp literal(%Date{} = value), do: ["DATE '", Date.to_iso8601(value), "'"]
defp literal(%NaiveDateTime{} = value),
do: ["TIMESTAMP '", NaiveDateTime.to_iso8601(value), "'"]
defp literal(%DateTime{} = value), do: ["TIMESTAMPTZ '", DateTime.to_iso8601(value), "'"]
defp literal(%Decimal{} = value), do: Decimal.to_string(value)
defp literal(value), do: expr(value)
defp operator(:==), do: "="
defp operator(:!=), do: "<>"
defp operator(op), do: Atom.to_string(op)
defp order_direction(:asc), do: "ASC"
defp order_direction(:desc), do: "DESC"
defp order_direction(:asc_nulls_last), do: "ASC NULLS LAST"
defp order_direction(:asc_nulls_first), do: "ASC NULLS FIRST"
defp order_direction(:desc_nulls_last), do: "DESC NULLS LAST"
defp order_direction(:desc_nulls_first), do: "DESC NULLS FIRST"
defp quote_identifier(value) do
value = value |> to_string() |> String.replace("\"", "\"\"")
["\"", value, "\""]
end
defp unsupported!(feature, message) do
raise QuackDB.Error.new(:ecto_feature_not_supported, message,
source: :client,
metadata: %{feature: feature}
)
end
end
end