Skip to main content

lib/quack_db/ecto/list.ex

if Code.ensure_loaded?(Ecto.Query.API) do
  defmodule QuackDB.Ecto.List do
    @moduledoc """
    DuckDB LIST/ARRAY expression helpers for Ecto queries.

    The macros map directly to DuckDB list functions and are useful for querying
    `LIST` columns or Ecto `{:array, type}` fields backed by DuckDB lists.
    """

    @doc "Builds `len(list)`. Named to avoid `Kernel.length/1` import ambiguity."
    defmacro list_length(list) do
      quote do
        fragment("len(?)", unquote(list))
      end
    end

    @doc "Builds `list_extract(list, index)`. DuckDB list indexes are 1-based."
    defmacro extract(list, index) do
      quote do
        fragment("list_extract(?, ?)", unquote(list), unquote(index))
      end
    end

    @doc "Builds `list_slice(list, begin, end)`."
    defmacro slice(list, begin_index, end_index) do
      quote do
        fragment("list_slice(?, ?, ?)", unquote(list), unquote(begin_index), unquote(end_index))
      end
    end

    @doc "Builds `list_slice(list, begin, end, step)`."
    defmacro slice(list, begin_index, end_index, step) do
      quote do
        fragment(
          "list_slice(?, ?, ?, ?)",
          unquote(list),
          unquote(begin_index),
          unquote(end_index),
          unquote(step)
        )
      end
    end

    @doc "Builds `list_sort(list)`."
    defmacro sort(list) do
      quote do
        fragment("list_sort(?)", unquote(list))
      end
    end

    @doc "Builds `list_reverse_sort(list)`."
    defmacro reverse_sort(list) do
      quote do
        fragment("list_reverse_sort(?)", unquote(list))
      end
    end

    @doc "Builds `list_distinct(list)`."
    defmacro distinct(list) do
      quote do
        fragment("list_distinct(?)", unquote(list))
      end
    end

    @doc "Builds `list_unique(list)`."
    defmacro unique(list) do
      quote do
        fragment("list_unique(?)", unquote(list))
      end
    end

    @doc "Builds `list_position(list, value)`."
    defmacro position(list, value) do
      quote do
        fragment("list_position(?, ?)", unquote(list), unquote(value))
      end
    end

    @doc "Builds `list_filter(list, lambda x : ...)`."
    defmacro list_filter(list, lambda) do
      lambda_fragment("list_filter", list, lambda, "list_filter/2", [1, 2])
    end

    @doc "Builds `list_transform(list, lambda x : ...)`."
    defmacro list_transform(list, lambda) do
      lambda_fragment("list_transform", list, lambda, "list_transform/2", [1, 2])
    end

    @doc "Builds `list_reduce(list, lambda acc, x : ...)`."
    defmacro list_reduce(list, lambda) do
      lambda_fragment("list_reduce", list, lambda, "list_reduce/2", [2, 3])
    end

    @doc "Builds `list_reduce(list, lambda acc, x : ..., initial_value)`."
    defmacro list_reduce(list, lambda, initial_value) do
      {lambda_sql, params} =
        QuackDB.Ecto.Lambda.to_sql!(lambda, function: "list_reduce/3", arities: [2, 3])

      fragment_sql = "list_reduce(?, #{lambda_sql}, ?)"

      quote do
        fragment(
          unquote(fragment_sql),
          unquote(list),
          unquote_splicing(params),
          unquote(initial_value)
        )
      end
    end

    @doc "Builds `list_contains(list, value)`."
    defmacro contains(list, value) do
      quote do
        fragment("list_contains(?, ?)", unquote(list), unquote(value))
      end
    end

    @doc "Alias for `contains/2` that avoids shared `contains/2` import ambiguity."
    defmacro contains_list(list, value) do
      quote do
        fragment("list_contains(?, ?)", unquote(list), unquote(value))
      end
    end

    @doc "Builds `list_has_any(left, right)`."
    defmacro has_any(left, right) do
      quote do
        fragment("list_has_any(?, ?)", unquote(left), unquote(right))
      end
    end

    @doc "Builds `list_has_all(left, right)`."
    defmacro has_all(left, right) do
      quote do
        fragment("list_has_all(?, ?)", unquote(left), unquote(right))
      end
    end

    @doc "Builds `list_intersect(left, right)`. Named to avoid Ecto `intersect/2` import ambiguity."
    defmacro intersect_list(left, right) do
      quote do
        fragment("list_intersect(?, ?)", unquote(left), unquote(right))
      end
    end

    @doc "Builds `list_concat(left, right)`."
    defmacro concat(left, right) do
      quote do
        fragment("list_concat(?, ?)", unquote(left), unquote(right))
      end
    end

    @doc "Builds `unnest(list)`."
    defmacro unnest(list) do
      quote do
        fragment("unnest(?)", unquote(list))
      end
    end

    defp lambda_fragment(function_name, list, lambda, label, arities) do
      {lambda_sql, params} =
        QuackDB.Ecto.Lambda.to_sql!(lambda, function: label, arities: arities)

      fragment_sql = "#{function_name}(?, #{lambda_sql})"

      quote do
        fragment(unquote(fragment_sql), unquote(list), unquote_splicing(params))
      end
    end
  end
end