order_by.ex (6710B)
1 import Kernel, except: [apply: 2] 2 3 defmodule Ecto.Query.Builder.OrderBy do 4 @moduledoc false 5 6 alias Ecto.Query.Builder 7 8 @directions [ 9 :asc, 10 :asc_nulls_last, 11 :asc_nulls_first, 12 :desc, 13 :desc_nulls_last, 14 :desc_nulls_first 15 ] 16 17 @doc """ 18 Returns `true` if term is a valid order_by direction; otherwise returns `false`. 19 20 ## Examples 21 22 iex> valid_direction?(:asc) 23 true 24 25 iex> valid_direction?(:desc) 26 true 27 28 iex> valid_direction?(:invalid) 29 false 30 31 """ 32 def valid_direction?(term), do: term in @directions 33 34 @doc """ 35 Escapes an order by query. 36 37 The query is escaped to a list of `{direction, expression}` 38 pairs at runtime. Escaping also validates direction is one of 39 `:asc`, `:asc_nulls_last`, `:asc_nulls_first`, `:desc`, 40 `:desc_nulls_last` or `:desc_nulls_first`. 41 42 ## Examples 43 44 iex> escape(:order_by, quote do [x.x, desc: 13] end, {[], %{}}, [x: 0], __ENV__) 45 {[asc: {:{}, [], [{:{}, [], [:., [], [{:{}, [], [:&, [], [0]]}, :x]]}, [], []]}, 46 desc: 13], 47 {[], %{}}} 48 49 """ 50 @spec escape(:order_by | :distinct, Macro.t, {list, term}, Keyword.t, Macro.Env.t) :: 51 {Macro.t, {list, term}} 52 def escape(kind, expr, params_acc, vars, env) do 53 expr 54 |> List.wrap 55 |> Enum.map_reduce(params_acc, &do_escape(&1, &2, kind, vars, env)) 56 end 57 58 defp do_escape({dir, {:^, _, [expr]}}, params_acc, kind, _vars, _env) do 59 {{quoted_dir!(kind, dir), quote(do: Ecto.Query.Builder.OrderBy.field!(unquote(kind), unquote(expr)))}, params_acc} 60 end 61 62 defp do_escape({:^, _, [expr]}, params_acc, kind, _vars, _env) do 63 {{:asc, quote(do: Ecto.Query.Builder.OrderBy.field!(unquote(kind), unquote(expr)))}, params_acc} 64 end 65 66 defp do_escape({dir, field}, params_acc, kind, _vars, _env) when is_atom(field) do 67 {{quoted_dir!(kind, dir), Macro.escape(to_field(field))}, params_acc} 68 end 69 70 defp do_escape(field, params_acc, _kind, _vars, _env) when is_atom(field) do 71 {{:asc, Macro.escape(to_field(field))}, params_acc} 72 end 73 74 defp do_escape({dir, expr}, params_acc, kind, vars, env) do 75 {ast, params_acc} = Builder.escape(expr, :any, params_acc, vars, env) 76 {{quoted_dir!(kind, dir), ast}, params_acc} 77 end 78 79 defp do_escape(expr, params_acc, _kind, vars, env) do 80 {ast, params_acc} = Builder.escape(expr, :any, params_acc, vars, env) 81 {{:asc, ast}, params_acc} 82 end 83 84 @doc """ 85 Checks the variable is a quoted direction at compilation time or 86 delegate the check to runtime for interpolation. 87 """ 88 def quoted_dir!(kind, {:^, _, [expr]}), 89 do: quote(do: Ecto.Query.Builder.OrderBy.dir!(unquote(kind), unquote(expr))) 90 def quoted_dir!(_kind, dir) when dir in @directions, 91 do: dir 92 def quoted_dir!(kind, other) do 93 Builder.error!( 94 "expected #{Enum.map_join(@directions, ", ", &inspect/1)} or interpolated value " <> 95 "in `#{kind}`, got: `#{inspect other}`" 96 ) 97 end 98 99 @doc """ 100 Called by at runtime to verify the direction. 101 """ 102 def dir!(_kind, dir) when dir in @directions, 103 do: dir 104 105 def dir!(kind, other) do 106 raise ArgumentError, 107 "expected one of #{Enum.map_join(@directions, ", ", &inspect/1)} " <> 108 "in `#{kind}`, got: `#{inspect other}`" 109 end 110 111 @doc """ 112 Called at runtime to verify a field. 113 """ 114 def field!(_kind, field) when is_atom(field) do 115 to_field(field) 116 end 117 118 def field!(kind, %Ecto.Query.DynamicExpr{} = dynamic_expression) do 119 raise ArgumentError, 120 "expected a field as an atom in `#{kind}`, got: `#{inspect dynamic_expression}`. " <> 121 "To use dynamic expressions, you need to interpolate at root level, as in: " <> 122 "`^[asc: dynamic, desc: :id]`" 123 end 124 125 def field!(kind, other) do 126 raise ArgumentError, "expected a field as an atom in `#{kind}`, got: `#{inspect other}`" 127 end 128 129 defp to_field(field), do: {{:., [], [{:&, [], [0]}, field]}, [], []} 130 131 @doc """ 132 Shared between order_by and distinct. 133 """ 134 def order_by_or_distinct!(kind, query, exprs, params) do 135 {expr, {params, _}} = 136 Enum.map_reduce(List.wrap(exprs), {params, length(params)}, fn 137 {dir, expr}, params_count when dir in @directions -> 138 {expr, params} = dynamic_or_field!(kind, expr, query, params_count) 139 {{dir, expr}, params} 140 expr, params_count -> 141 {expr, params} = dynamic_or_field!(kind, expr, query, params_count) 142 {{:asc, expr}, params} 143 end) 144 145 {expr, params} 146 end 147 148 @doc """ 149 Called at runtime to assemble order_by. 150 """ 151 def order_by!(query, exprs, file, line) do 152 {expr, params} = order_by_or_distinct!(:order_by, query, exprs, []) 153 expr = %Ecto.Query.QueryExpr{expr: expr, params: Enum.reverse(params), line: line, file: file} 154 apply(query, expr) 155 end 156 157 defp dynamic_or_field!(kind, %Ecto.Query.DynamicExpr{} = dynamic, query, {params, count}) do 158 {expr, params, count} = Builder.Dynamic.partially_expand(kind, query, dynamic, params, count) 159 {expr, {params, count}} 160 end 161 162 defp dynamic_or_field!(_kind, field, _query, params_count) when is_atom(field) do 163 {to_field(field), params_count} 164 end 165 166 defp dynamic_or_field!(kind, other, _query, _params_count) do 167 raise ArgumentError, 168 "`#{kind}` interpolated on root expects a field or a keyword list " <> 169 "with the direction as keys and fields or dynamics as values, got: `#{inspect other}`" 170 end 171 172 @doc """ 173 Builds a quoted expression. 174 175 The quoted expression should evaluate to a query at runtime. 176 If possible, it does all calculations at compile time to avoid 177 runtime work. 178 """ 179 @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t 180 def build(query, _binding, {:^, _, [var]}, env) do 181 quote do 182 Ecto.Query.Builder.OrderBy.order_by!(unquote(query), unquote(var), unquote(env.file), unquote(env.line)) 183 end 184 end 185 186 def build(query, binding, expr, env) do 187 {query, binding} = Builder.escape_binding(query, binding, env) 188 {expr, {params, _acc}} = escape(:order_by, expr, {[], %{}}, binding, env) 189 params = Builder.escape_params(params) 190 191 order_by = quote do: %Ecto.Query.QueryExpr{ 192 expr: unquote(expr), 193 params: unquote(params), 194 file: unquote(env.file), 195 line: unquote(env.line)} 196 Builder.apply_query(query, __MODULE__, [order_by], env) 197 end 198 199 @doc """ 200 The callback applied by `build/4` to build the query. 201 """ 202 @spec apply(Ecto.Queryable.t, term) :: Ecto.Query.t 203 def apply(%Ecto.Query{order_bys: order_bys} = query, expr) do 204 %{query | order_bys: order_bys ++ [expr]} 205 end 206 def apply(query, expr) do 207 apply(Ecto.Queryable.to_query(query), expr) 208 end 209 end