Skip to main content

lib/quack_db/ecto/text.ex

if Code.ensure_loaded?(Ecto.Query.API) do
  defmodule QuackDB.Ecto.Text do
    @moduledoc """
    DuckDB text-expression helpers for Ecto queries.

        import Ecto.Query
        import QuackDB.Ecto.Text

        from event in "events",
          where: contains(event.name, "duck") and starts_with(event.kind, "bird"),
          select: %{
            second_part: split_part(event.name, "-", 2),
            tags: string_split(event.tags, ",")
          }

    `contains_text/2` emits the same DuckDB `contains(?, ?)` call as
    `contains/2`. It is useful with `use QuackDB.Ecto`, where shared
    `contains/2` may dispatch between text and spatial predicates.
    """

    @text_helpers [
      %{name: :contains, sql: "contains", arities: [2]},
      %{name: :contains_text, sql: "contains", arities: [2]},
      %{name: :starts_with, arities: [2]},
      %{name: :ends_with, arities: [2]},
      %{name: :prefix, arities: [2]},
      %{name: :suffix, arities: [2]},
      %{name: :split_part, arities: [3]},
      %{name: :string_split, arities: [2]},
      %{name: :string_split_regex, arities: [2, 3]}
    ]

    @doc false
    def __text_helpers__, do: @text_helpers

    for %{name: name, arities: arities} = helper <- @text_helpers, arity <- arities do
      sql = Map.get(helper, :sql, Atom.to_string(name))
      arguments = Macro.generate_arguments(arity, __MODULE__)

      fragment_sql =
        IO.iodata_to_binary([
          sql,
          "(",
          Enum.map_join(1..arity, ", ", fn _ -> "?" end),
          ")"
        ])

      defmacro unquote(name)(unquote_splicing(arguments)) do
        text_fragment(unquote(fragment_sql), [unquote_splicing(arguments)])
      end
    end

    defp text_fragment(sql, arguments) do
      quote do
        fragment(unquote(sql), unquote_splicing(arguments))
      end
    end
  end
end