if Code.ensure_loaded?(Ecto.Query.API) do defmodule QuackDB.Ecto.Regex do @moduledoc """ DuckDB regular-expression helpers for Ecto queries. DuckDB uses RE2 regular expressions. Elixir's `Regex` uses Erlang/OTP `:re`, so only the shared regex subset is portable. Literal `~r/.../` patterns are accepted for convenience and their compatible modifiers are translated to DuckDB options. import Ecto.Query import QuackDB.Ecto.Regex from event in "events", where: regexp_matches(event.name, ~r/^duck/i), select: %{ year: regexp_extract(event.name, ~r/(\\d{4})/, 1), slug: regexp_replace(event.name, ~r/\\s+/, "-", "g"), parts: regexp_split_to_array(event.name, ~r/\\s*,\\s*/) } Compatible `~r` modifiers are translated to DuckDB option strings: `i`, `m`, and `s`. Elixir's Unicode modifier is ignored; unsupported modifiers raise at macro expansion time. """ @regex_helpers [ %{name: :regexp_escape, arities: [1]}, %{name: :regexp_extract, arities: [2, 3, 4]}, %{name: :regexp_extract_all, arities: [2, 3, 4]}, %{name: :regexp_full_match, arities: [2, 3]}, %{name: :regexp_matches, arities: [2, 3]}, %{name: :regexp_replace, arities: [3, 4]}, %{name: :regexp_split_to_array, arities: [2, 3]} ] @doc false def __regex_helpers__, do: @regex_helpers defmacro regexp_escape(string) do regex_fragment("regexp_escape", [string]) end defmacro regexp_extract(string, pattern) do regex_fragment("regexp_extract", [string], pattern, []) end defmacro regexp_extract(string, pattern, group_or_names) do regex_fragment("regexp_extract", [string], pattern, [group_or_names]) end defmacro regexp_extract(string, pattern, group_or_names, options) do regex_fragment("regexp_extract", [string], pattern, [group_or_names, options], true) end defmacro regexp_extract_all(string, pattern) do regex_fragment("regexp_extract_all", [string], pattern, []) end defmacro regexp_extract_all(string, pattern, group_or_names) do regex_fragment("regexp_extract_all", [string], pattern, [group_or_names]) end defmacro regexp_extract_all(string, pattern, group_or_names, options) do regex_fragment("regexp_extract_all", [string], pattern, [group_or_names, options], true) end defmacro regexp_full_match(string, pattern) do regex_fragment("regexp_full_match", [string], pattern, []) end defmacro regexp_full_match(string, pattern, options) do regex_fragment("regexp_full_match", [string], pattern, [options], true) end defmacro regexp_matches(string, pattern) do regex_fragment("regexp_matches", [string], pattern, []) end defmacro regexp_matches(string, pattern, options) do regex_fragment("regexp_matches", [string], pattern, [options], true) end defmacro regexp_replace(string, pattern, replacement) do regex_fragment("regexp_replace", [string], pattern, [replacement]) end defmacro regexp_replace(string, pattern, replacement, options) do regex_fragment("regexp_replace", [string], pattern, [replacement, options], true) end defmacro regexp_split_to_array(string, pattern) do regex_fragment("regexp_split_to_array", [string], pattern, []) end defmacro regexp_split_to_array(string, pattern, options) do regex_fragment("regexp_split_to_array", [string], pattern, [options], true) end defp regex_fragment(function, arguments) do quote do fragment(unquote(call_sql(function, length(arguments))), unquote_splicing(arguments)) end end defp regex_fragment( function, leading_arguments, pattern, trailing_arguments, explicit_options? \\ false ) do {pattern, pattern_options} = regex_pattern(pattern) arguments = leading_arguments ++ [pattern] ++ trailing_arguments arguments = if pattern_options == nil or explicit_options? do arguments else [pattern_options | Enum.reverse(arguments)] |> Enum.reverse() end regex_fragment(function, arguments) end defp regex_pattern({:sigil_r, _meta, [{:<<>>, _string_meta, [source]}, modifiers]}) when is_binary(source) and is_list(modifiers) do options = duckdb_regex_options!(modifiers) {source, options} end defp regex_pattern( {:%, _meta, [ {:__aliases__, _alias_meta, [:Regex]}, {:%{}, _map_meta, fields} ]} ) when is_list(fields) do source = Keyword.fetch!(fields, :source) options = fields |> Keyword.get(:opts, "") |> to_charlist() |> duckdb_regex_options!() {source, options} end defp regex_pattern(pattern), do: {pattern, nil} defp duckdb_regex_options!(modifiers) do modifiers |> Enum.reduce([], fn ?i, options -> [?i | options] ?m, options -> [?m | options] ?s, options -> [?s | options] ?u, options -> options modifier, _options -> raise ArgumentError, "unsupported DuckDB regex modifier: #{<>}" end) |> Enum.reverse() |> case do [] -> nil options -> to_string(options) end end defp call_sql(function, arity) do [function, "(", Enum.map_join(1..arity, ", ", fn _ -> "?" end), ")"] |> IO.iodata_to_binary() end end end