defmodule Selecto.Subfilter.Registry do
@moduledoc """
Registry system for managing multiple subfilters with strategy selection and optimization.
The Registry handles:
- Multiple subfilter registration and management
- Strategy selection (EXISTS, IN, ANY, ALL) based on query patterns
- Performance optimization through join analysis
- Conflict detection and resolution
- SQL generation coordination
## Examples
iex> registry = Selecto.Subfilter.Registry.new(:film_domain)
iex> registry = Registry.add_subfilter(registry, "film.rating", "R")
iex> registry = Registry.add_subfilter(registry, "film.category.name", "Action")
iex> Registry.generate_sql(registry, base_query)
{:ok, optimized_query_with_subfilters}
"""
# alias Selecto.Subfilter
alias Selecto.Subfilter.{Spec, Parser, JoinPathResolver, SQL, Error}
# Registry structure to manage multiple subfilters
defstruct [
# Domain configuration to use
:domain_name,
# Base table for the query
:base_table,
# Map of subfilter_id => Spec
:subfilters,
# Map of subfilter_id => JoinResolution
:join_resolutions,
# Manual strategy overrides
:strategy_overrides,
# Performance optimization hints
:optimization_hints,
# Compound operations (AND/OR between subfilters)
:compound_ops
]
@type t :: %__MODULE__{
domain_name: atom() | map(),
base_table: atom() | nil,
subfilters: %{String.t() => Spec.t()},
join_resolutions: %{String.t() => JoinPathResolver.JoinResolution.t()},
strategy_overrides: %{String.t() => atom()},
optimization_hints: keyword(),
compound_ops: [compound_operation()]
}
@type compound_operation :: %{
type: :and | :or,
subfilter_ids: [String.t()]
}
@doc """
Create a new subfilter registry for the specified domain.
## Parameters
- `domain_name` - Domain configuration to use (e.g., :film_domain)
- `opts` - Options including :base_table, :optimization_hints
"""
@spec new(atom() | map(), keyword()) :: t()
def new(domain_name, opts \\ []) do
%__MODULE__{
domain_name: domain_name,
base_table: Keyword.get(opts, :base_table),
subfilters: %{},
join_resolutions: %{},
strategy_overrides: %{},
optimization_hints: Keyword.get(opts, :optimization_hints, []),
compound_ops: []
}
end
@doc """
Add a subfilter to the registry.
## Examples
add_subfilter(registry, "film.rating", "R")
add_subfilter(registry, "film.category.name", ["Action", "Drama"], strategy: :in)
add_subfilter(registry, "film", {:count, ">", 5}, id: "film_count_filter")
"""
@spec add_subfilter(t(), String.t(), any(), keyword()) ::
{:ok, t()} | {:error, Error.t()}
def add_subfilter(%__MODULE__{} = registry, relationship_path, filter_spec, opts \\ []) do
with {:ok, parsed_spec} <- Parser.parse(relationship_path, filter_spec, opts),
{:ok, resolved_joins} <-
JoinPathResolver.resolve(
parsed_spec.relationship_path,
registry.domain_name,
registry.base_table
) do
subfilter_id = Keyword.get(opts, :id, parsed_spec.id)
# Check for conflicts
case check_for_conflicts(registry, subfilter_id, parsed_spec) do
:ok ->
# Update the spec with the final ID
final_spec = %{parsed_spec | id: subfilter_id}
updated_registry = %{
registry
| subfilters: Map.put(registry.subfilters, subfilter_id, final_spec),
join_resolutions: Map.put(registry.join_resolutions, subfilter_id, resolved_joins)
}
# Apply strategy optimization
optimized_registry = optimize_strategies(updated_registry)
{:ok, optimized_registry}
{:error, reason} ->
{:error, reason}
end
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Add compound subfilter operations (AND/OR).
## Examples
add_compound(registry, :and, [
{"film.rating", "R"},
{"film.release_year", {">", 2000}}
])
"""
@spec add_compound(
t(),
:and | :or,
[{String.t(), any()}] | [{String.t(), any(), keyword()}],
keyword()
) ::
{:ok, t()} | {:error, Error.t()}
def add_compound(%__MODULE__{} = registry, compound_type, subfilter_specs, opts \\ []) do
with {:ok, compound_spec} <- Parser.parse_compound(compound_type, subfilter_specs, opts) do
# Add each individual subfilter first
case add_compound_subfilters(registry, compound_spec.subfilters) do
{:ok, updated_registry, subfilter_ids} ->
compound_op = %{
type: compound_type,
subfilter_ids: subfilter_ids
}
final_registry = %{
updated_registry
| compound_ops: [compound_op | updated_registry.compound_ops]
}
{:ok, final_registry}
{:error, reason} ->
{:error, reason}
end
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Remove a subfilter from the registry.
"""
@spec remove_subfilter(t(), String.t()) :: t()
def remove_subfilter(%__MODULE__{} = registry, subfilter_id) do
%{
registry
| subfilters: Map.delete(registry.subfilters, subfilter_id),
join_resolutions: Map.delete(registry.join_resolutions, subfilter_id),
strategy_overrides: Map.delete(registry.strategy_overrides, subfilter_id),
compound_ops: remove_from_compound_ops(registry.compound_ops, subfilter_id)
}
end
@doc """
Override the strategy for a specific subfilter.
Useful for performance tuning when the automatic strategy selection
doesn't produce optimal results.
"""
@spec override_strategy(t(), String.t(), atom()) :: {:ok, t()} | {:error, Error.t()}
def override_strategy(%__MODULE__{} = registry, subfilter_id, strategy)
when strategy in [:exists, :in, :any, :all] do
case Map.has_key?(registry.subfilters, subfilter_id) do
true ->
updated_registry = %{
registry
| strategy_overrides: Map.put(registry.strategy_overrides, subfilter_id, strategy)
}
{:ok, updated_registry}
false ->
{:error,
%Error{
type: :subfilter_not_found,
message: "Subfilter not found in registry",
details: %{subfilter_id: subfilter_id, available_ids: Map.keys(registry.subfilters)}
}}
end
end
def override_strategy(_registry, _subfilter_id, invalid_strategy) do
{:error,
%Error{
type: :invalid_strategy,
message: "Invalid strategy for override",
details: %{strategy: invalid_strategy, valid_strategies: [:exists, :in, :any, :all]}
}}
end
@doc """
Get comprehensive analysis of all subfilters in the registry.
Returns information about join patterns, strategy selections,
performance implications, and optimization opportunities.
"""
@spec analyze(t()) :: %{
subfilter_count: non_neg_integer(),
join_complexity: atom(),
strategy_distribution: %{atom() => non_neg_integer()},
performance_score: float(),
optimization_suggestions: [String.t()]
}
def analyze(%__MODULE__{} = registry) do
subfilter_count = map_size(registry.subfilters)
%{
subfilter_count: subfilter_count,
join_complexity: assess_join_complexity(registry),
strategy_distribution: calculate_strategy_distribution(registry),
performance_score: calculate_performance_score(registry),
optimization_suggestions: generate_optimization_suggestions(registry)
}
end
@doc """
Generate SQL for all subfilters in the registry.
This coordinates with the SQL generation system to produce optimized
subquery SQL that integrates with the main query.
"""
@spec generate_sql(t(), String.t()) :: {:ok, String.t(), [any()]} | {:error, Error.t()}
def generate_sql(%__MODULE__{} = registry, base_query) when is_binary(base_query) do
if map_size(registry.subfilters) == 0 do
{:ok, base_query, []}
else
case SQL.generate(registry) do
{:ok, where_clause, params} ->
{:ok, merge_where_clause(base_query, where_clause), params}
{:error, reason} ->
{:error, reason}
end
end
end
# Private implementation functions
defp check_for_conflicts(%__MODULE__{} = registry, subfilter_id, _spec) do
case Map.has_key?(registry.subfilters, subfilter_id) do
true ->
{:error,
%Error{
type: :duplicate_subfilter_id,
message: "Subfilter ID already exists in registry",
details: %{subfilter_id: subfilter_id}
}}
false ->
:ok
end
end
defp optimize_strategies(%__MODULE__{} = registry) do
# Apply automatic strategy optimization based on subfilter patterns
# This would analyze join complexity, filter selectivity, etc.
# For now, return as-is
registry
end
defp add_compound_subfilters(registry, subfilters) do
prepared_subfilters =
Enum.map(subfilters, fn spec ->
subfilter_id = generate_compound_subfilter_id(spec)
final_spec = %{spec | id: subfilter_id}
{subfilter_id, final_spec}
end)
subfilter_ids = Enum.map(prepared_subfilters, fn {subfilter_id, _spec} -> subfilter_id end)
with :ok <- ensure_unique_subfilter_ids(subfilter_ids),
:ok <- ensure_registry_conflicts(registry, prepared_subfilters),
{:ok, resolutions} <- resolve_compound_paths(prepared_subfilters, registry) do
updated_registry =
Enum.zip(prepared_subfilters, resolutions)
|> Enum.reduce(registry, fn {{subfilter_id, spec}, resolved_joins}, acc ->
%{
acc
| subfilters: Map.put(acc.subfilters, subfilter_id, spec),
join_resolutions: Map.put(acc.join_resolutions, subfilter_id, resolved_joins)
}
end)
{:ok, optimize_strategies(updated_registry), subfilter_ids}
else
{:error, reason} ->
{:error, reason}
end
end
defp ensure_unique_subfilter_ids(subfilter_ids) do
duplicate_id =
subfilter_ids
|> Enum.frequencies()
|> Enum.find_value(fn
{subfilter_id, count} when count > 1 -> subfilter_id
_ -> nil
end)
case duplicate_id do
nil ->
:ok
subfilter_id ->
{:error,
%Error{
type: :duplicate_subfilter_id,
message: "Subfilter ID already exists in registry",
details: %{subfilter_id: subfilter_id}
}}
end
end
defp ensure_registry_conflicts(registry, prepared_subfilters) do
Enum.reduce_while(prepared_subfilters, :ok, fn {subfilter_id, spec}, _acc ->
case check_for_conflicts(registry, subfilter_id, spec) do
:ok -> {:cont, :ok}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp resolve_compound_paths(prepared_subfilters, registry) do
relationship_paths =
Enum.map(prepared_subfilters, fn {_subfilter_id, spec} -> spec.relationship_path end)
JoinPathResolver.resolve_multiple(
relationship_paths,
registry.domain_name,
registry.base_table
)
end
defp merge_where_clause(base_query, where_clause) do
trimmed_where = String.replace_prefix(where_clause, "WHERE ", "")
if Regex.match?(~r/\bwhere\b/i, base_query) do
base_query <> " AND (" <> trimmed_where <> ")"
else
base_query <> " " <> where_clause
end
end
defp generate_compound_subfilter_id(spec) do
path_segments = spec.relationship_path.path_segments
path_str = Enum.join(path_segments, "_")
field = spec.relationship_path.target_field || "agg"
"compound_#{path_str}_#{field}"
end
defp remove_from_compound_ops(compound_ops, subfilter_id) do
Enum.map(compound_ops, fn op ->
%{op | subfilter_ids: List.delete(op.subfilter_ids, subfilter_id)}
end)
|> Enum.reject(fn op -> Enum.empty?(op.subfilter_ids) end)
end
defp assess_join_complexity(%__MODULE__{join_resolutions: join_resolutions}) do
total_joins =
join_resolutions
|> Map.values()
|> Enum.map(fn resolution -> length(resolution.joins) end)
|> Enum.sum()
cond do
total_joins == 0 -> :none
total_joins <= 3 -> :low
total_joins <= 8 -> :medium
total_joins <= 15 -> :high
true -> :very_high
end
end
defp calculate_strategy_distribution(%__MODULE__{
subfilters: subfilters,
strategy_overrides: overrides
}) do
subfilters
|> Map.keys()
|> Enum.reduce(%{}, fn subfilter_id, acc ->
strategy = Map.get(overrides, subfilter_id, Map.get(subfilters, subfilter_id).strategy)
Map.update(acc, strategy, 1, &(&1 + 1))
end)
end
defp calculate_performance_score(%__MODULE__{} = registry) do
# Simple scoring based on join complexity and subfilter count
complexity_penalty =
case assess_join_complexity(registry) do
:none -> 0.0
:low -> 0.1
:medium -> 0.3
:high -> 0.5
:very_high -> 0.7
end
subfilter_count_penalty = map_size(registry.subfilters) * 0.05
max(0.0, 1.0 - complexity_penalty - subfilter_count_penalty)
end
defp generate_optimization_suggestions(%__MODULE__{} = registry) do
suggestions = []
# Check for high join complexity
suggestions =
case assess_join_complexity(registry) do
complexity when complexity in [:high, :very_high] ->
[
"Consider reducing join complexity by using IN strategy for some subfilters"
| suggestions
]
_ ->
suggestions
end
# Check for too many EXISTS subfilters
strategy_dist = calculate_strategy_distribution(registry)
exists_count = Map.get(strategy_dist, :exists, 0)
suggestions =
if exists_count > 3 do
[
"Consider using IN strategy for some EXISTS subfilters to improve performance"
| suggestions
]
else
suggestions
end
# Check for compound operations optimization
suggestions =
if length(registry.compound_ops) > 2 do
["Complex compound operations may benefit from query restructuring" | suggestions]
else
suggestions
end
suggestions
end
end