Current section
Files
Jump to
Current section
Files
lib/selecto/subfilter/registry.ex
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_name, # Domain configuration to use
:base_table, # Base table for the query
:subfilters, # Map of subfilter_id => Spec
:join_resolutions, # Map of subfilter_id => JoinResolution
:strategy_overrides, # Manual strategy overrides
:optimization_hints, # Performance optimization hints
:compound_ops # Compound operations (AND/OR between subfilters)
]
@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
add_compound_subfilters(registry, subfilters, [])
end
defp add_compound_subfilters(registry, [], subfilter_ids) do
{:ok, registry, Enum.reverse(subfilter_ids)}
end
defp add_compound_subfilters(registry, [spec | rest], subfilter_ids) do
# Generate a unique ID for this subfilter based on its spec
subfilter_id = generate_compound_subfilter_id(spec)
case add_parsed_subfilter(registry, subfilter_id, spec) do
{:ok, updated_registry} ->
add_compound_subfilters(updated_registry, rest, [subfilter_id | subfilter_ids])
{:error, reason} ->
{:error, reason}
end
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 add_parsed_subfilter(registry, subfilter_id, spec) do
with {:ok, resolved_joins} <- JoinPathResolver.resolve(spec.relationship_path, registry.domain_name, registry.base_table) do
case check_for_conflicts(registry, subfilter_id, spec) do
:ok ->
updated_registry = %{registry |
subfilters: Map.put(registry.subfilters, subfilter_id, spec),
join_resolutions: Map.put(registry.join_resolutions, subfilter_id, resolved_joins)
}
{:ok, updated_registry}
{:error, reason} ->
{:error, reason}
end
else
{:error, reason} -> {:error, reason}
end
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