if Code.ensure_loaded?(Ecto.Query) do
defmodule QuackDB.Ecto do
@moduledoc """
Convenience imports for Ecto-based QuackDB query modules.
Use this in modules that build DuckDB analytical, spatial, series, or
full-text search Ecto queries and want the standard Ecto query DSL together with
QuackDB's Ecto helper macros:
defmodule MyApp.Analytics do
use QuackDB.Ecto
def category_scores do
from event in "events",
group_by: event.category,
select: %{
category: event.category,
median_score: median(event.score),
fts_score: search_score("fts_main_events", event.id, ^"duckdb")
}
end
end
The macro imports:
- `Ecto.Query`.
- `QuackDB.Ecto.Analytics`.
- `QuackDB.Ecto.Spatial`.
- `QuackDB.Ecto.FTS`.
- `QuackDB.Ecto.Regex`.
- `QuackDB.Ecto.Text`.
- `QuackDB.Ecto.List`.
- `QuackDB.Ecto.Map`.
- `QuackDB.Ecto.Struct`.
- `QuackDB.Ecto.Series`.
- `QuackDB.Ecto.Star`.
- `QuackDB.Ecto.WindowFrames`.
`QuackDB.Ecto.Conditionals.case_when/1` is also imported for multi-branch
DuckDB `CASE WHEN` expressions using Elixir clause syntax.
Imports can be disabled individually. When spatial and text helpers are both
enabled, shared `contains/2` dispatches obvious text calls to DuckDB
`contains` and spatial helper calls to `ST_Contains`. Ambiguous calls raise;
use `contains_text/2` or `st_contains/2` when intent is not obvious.
use QuackDB.Ecto, spatial: false
use QuackDB.Ecto, full_text_search: false
use QuackDB.Ecto, analytics: false
use QuackDB.Ecto, regex: false
use QuackDB.Ecto, text: false
use QuackDB.Ecto, list: false
use QuackDB.Ecto, map: false
use QuackDB.Ecto, struct: false
use QuackDB.Ecto, series: false
use QuackDB.Ecto, star: false
use QuackDB.Ecto, window_frames: false
use QuackDB.Ecto, query: false
"""
@doc """
Returns the sequence name QuackDB migrations use for a serial table column.
QuackDB.Ecto.column_sequence_name("fragments", :id)
#=> "fragments_id_seq"
QuackDB.Ecto.column_sequence_name({"main", "fragments"}, :id)
#=> "main_fragments_id_seq"
Schema modules and `{source, schema}` tuples use Ecto field-source metadata:
QuackDB.Ecto.column_sequence_name(FragmentRecord, :id)
QuackDB.Ecto.column_sequence_name({"tenant_fragments", FragmentRecord}, :id)
"""
@spec column_sequence_name(
module()
| atom()
| String.t()
| {atom() | String.t(), atom() | String.t()}
| {atom() | String.t(), module()},
atom() | String.t()
) :: String.t()
def column_sequence_name({source, schema}, field) when is_atom(schema) do
if ecto_schema?(schema) do
source = to_string(source)
field = schema_field_source(schema, field)
case schema_prefix(schema) do
nil -> sequence_name([source, field])
prefix -> sequence_name([prefix, source, field])
end
else
sequence_name([source, schema, field])
end
end
def column_sequence_name(schema, field) when is_atom(schema) do
if ecto_schema?(schema) do
source = schema.__schema__(:source)
field = schema_field_source(schema, field)
case schema_prefix(schema) do
nil -> sequence_name([source, field])
prefix -> sequence_name([prefix, source, field])
end
else
sequence_name([schema, field])
end
end
def column_sequence_name({prefix, table}, field) do
sequence_name([prefix, table, field])
end
def column_sequence_name(table, field) do
sequence_name([table, field])
end
defp sequence_name(parts),
do: parts |> Enum.concat(["seq"]) |> Enum.map_join("_", &to_string/1)
defp ecto_schema?(schema) do
Code.ensure_loaded?(schema) and function_exported?(schema, :__schema__, 1)
end
defp schema_field_source(schema, field) when is_atom(field) do
schema.__schema__(:field_source, field)
rescue
FunctionClauseError -> field
end
defp schema_field_source(_schema, field), do: field
defp schema_prefix(schema) do
schema.__schema__(:prefix)
rescue
FunctionClauseError -> nil
end
@doc false
defmacro __using__(options) do
enabled? = fn key -> Keyword.get(options, key, true) end
shared_predicates? = enabled?.(:predicates) and enabled?.(:spatial) and enabled?.(:text)
imports =
[
maybe_import(enabled?.(:query), Ecto.Query),
maybe_import(enabled?.(:analytics), QuackDB.Ecto.Analytics),
spatial_import(enabled?.(:spatial), shared_predicates?),
maybe_import(enabled?.(:full_text_search), QuackDB.Ecto.FTS),
maybe_import(enabled?.(:series), QuackDB.Ecto.Series),
maybe_import(enabled?.(:star), QuackDB.Ecto.Star),
maybe_import(enabled?.(:list), QuackDB.Ecto.List, except: [contains: 2]),
maybe_import(enabled?.(:map), QuackDB.Ecto.Map,
except: [contains: 2, extract: 2, values: 1, concat: 2]
),
maybe_import(enabled?.(:struct), QuackDB.Ecto.Struct,
except: [contains: 2, extract: 2, values: 1, position: 2, concat: 2]
),
maybe_import(enabled?.(:window_frames), QuackDB.Ecto.WindowFrames),
maybe_import(enabled?.(:regex), QuackDB.Ecto.Regex),
text_import(enabled?.(:text), shared_predicates?),
maybe_import(shared_predicates?, QuackDB.Ecto.Predicates),
maybe_import(enabled?.(:conditionals), QuackDB.Ecto.Conditionals)
]
|> Enum.reject(&is_nil/1)
quote do
(unquote_splicing(imports))
end
end
defp spatial_import(false, _shared_predicates?), do: nil
defp spatial_import(true, true),
do: import_quoted(QuackDB.Ecto.Spatial, except: [contains: 2])
defp spatial_import(true, false), do: import_quoted(QuackDB.Ecto.Spatial)
defp text_import(false, _shared_predicates?), do: nil
defp text_import(true, true), do: import_quoted(QuackDB.Ecto.Text, except: [contains: 2])
defp text_import(true, false), do: import_quoted(QuackDB.Ecto.Text)
defp maybe_import(enabled?, module, options \\ [])
defp maybe_import(false, _module, _options), do: nil
defp maybe_import(true, module, options), do: import_quoted(module, options)
defp import_quoted(module, options \\ [])
defp import_quoted(module, []), do: quote(do: import(unquote(module)))
defp import_quoted(module, options),
do: quote(do: import(unquote(module), unquote(options)))
end
end