Patterns.Queryable.Filters (patterns v0.0.1)

Copy Markdown View Source

Default filter implementations for Patterns.Queryable modules.

apply_filter/2 turns common filter tuples into Ecto query expressions so a Patterns.Queryable module can focus on custom filters and delegate the rest.

Query Modifiers

Query modifiers affect only the top-level query. They are ignored inside association filters.

Supported modifiers are:

  • {:distinct, value}
  • {:limit, value}
  • {:offset, value}
  • {:preload, value}
  • {:select, value}

Modifier values are forwarded to Ecto's query DSL, so Ecto syntax like {:preload, :comments}, {:preload, [comments: :post]}, and {:distinct, true} is supported. Atom selects are treated as fields on the current binding, so {:select, :id} selects the current binding's id field. Non-atom selects are passed through to Ecto as values. Distinct values are forwarded to Ecto; use {:distinct, true} as the portable parent-deduplication form for association filters.

Modifier behavior is delegated to Ecto and the configured adapter. SQL shape and support can differ between adapters such as ecto_sqlite3 and PostgreSQL, especially for non-boolean distinct values and complex preloads.

Field Comparators

Field comparators apply predicates to the current binding.

Supported comparators are:

  • {field, value} and {field, {:eq, value}}
  • {field, nil} and {field, {:eq, nil}}
  • {field, [value]} and {field, {:in, [value]}}
  • {field, %Regex{}} and {field, {:like, %Regex{}}}
  • {field, {:not, value}}
  • {field, {:not, nil}}
  • {field, {:not, [value]}} and {field, {:not_in, [value]}}
  • {field, {:not, %Regex{}}} and {field, {:not_like, %Regex{}}}
  • {field, {:gt, value}}
  • {field, {:gte, value}}
  • {field, {:lt, value}}
  • {field, {:lte, value}}
  • {field, {:like, pattern}}
  • {field, {:not_like, pattern}}

Unsupported comparator tuples raise ArgumentError instead of silently becoming equality comparisons against the tuple value.

Regex Values

Regex shorthand is LIKE shorthand

Regex values are shorthand for SQL LIKE patterns, not full database regex predicates. They are converted by translating .* to % and . to _. Unanchored regexes are padded with %; ^ and $ suppress leading and trailing padding respectively.

The i regex option lower-cases the field and generated LIKE pattern. Other regex options raise ArgumentError.

Matching semantics are still delegated to the database adapter and collation. For example, plain SQL LIKE case-sensitivity can differ between SQLite and PostgreSQL.

Literal % and _ are rejected in regex shorthand because they are SQL LIKE wildcards. Escaped regex syntax is not supported.

Regex syntax that cannot be represented by this simple conversion raises ArgumentError.

Association Comparators

Association comparators use {field, filters} where filters is a map or keyword-shaped list and field is an Ecto association.

Empty filters like {:comments, []} and {:comments, %{}} still join the association. When Patterns.Queryable adds a new inner join, they match parents with at least one associated row. When an existing named binding is reused, that binding's join semantics are preserved.

Treating map and keyword values as association filters is an implementation detail. Future versions may support map equality for field types that can encode and compare maps directly, such as PostgreSQL jsonb fields.

Association filters require a schema-backed query source because they inspect association metadata from the current scoped binding. Raw string sources and subquery sources cannot use association filters.

Join generation and association metadata are delegated to Ecto. Adapter SQL output may differ, but the filter semantics are based on Ecto's assoc/2 join behavior.

Nested association filters are resolved from the current scoped binding, so {:comments, [post: [title: "Hello"]]} filters :post as an association of the joined :comments binding.

Association filters use Ecto's assoc/2 join syntax and keep normal join semantics. For has_many and many_to_many associations, one parent row is returned for each matching associated row. Pass {:distinct, true} when parent row uniqueness matters.

Named binding reuse

If the query already has a named binding for the association, that binding is reused instead of adding another join. Reusing an existing binding preserves that binding's join semantics, including left-join behavior. Named bindings are query-global, so nested association filters can reuse an existing binding with the same association name. Implement custom query/2 clauses when a nested association path needs stricter binding control.

When the associated schema exports query/2, nested filters are delegated to that function against the joined association binding. Otherwise, each nested filter is applied directly to the joined association binding.

Binding resolution uses Patterns.Queryable.DSL.binding_schema/1, so nested association filters are resolved from the current scoped binding rather than always from the root query source.

Summary

Functions

Applies a single query filter.

Functions

apply_filter(query, filter)

@spec apply_filter(
  Ecto.Queryable.t(),
  {field :: atom(), value :: term()}
) :: Ecto.Queryable.t()

Applies a single query filter.

See the module documentation for supported filter shapes.