Runs Ecto query checks from an explicit list of check specs.
Bylaw.Ecto.Query.validate/3 and Bylaw.Ecto.Query.validate/4 are the
public entry points for end-user query validation. Use them from
Ecto.Repo.prepare_query/3 when you want repo-wide enforcement while
keeping check selection explicit:
@query_checks [
Bylaw.Ecto.Query.Checks.RequiredOrder,
{Bylaw.Ecto.Query.Checks.MandatoryWhereKeys,
rules: [fields: [:organization_id]]},
{Bylaw.Ecto.Query.Checks.ExplicitVisibilityPredicates,
rules: [
[where: [ecto_schemas: [Post]], fields: [:deleted_at, :archived_at]],
[where: [tables: ["comments"]], fields: [:deleted_at]]
]}
]
def prepare_query(operation, query, opts) do
case Bylaw.Ecto.Query.validate(
operation,
query,
@query_checks,
Keyword.get(opts, :bylaw, [])
) do
:ok -> {query, opts}
{:error, issues} -> raise Bylaw.Ecto.Query.Issue.format_many(issues)
end
endA check spec is either a check module or {check_module, opts}. Each check
module may appear at most once.
Warning
bylaw_ecto_query inspects prepared %Ecto.Query{} structs. Ecto exposes
Ecto.Query.t(), but the internal shape of query expressions is not a
stable extension API. Review and run your enabled checks when upgrading
Ecto.
Rules DSL
Every check can be scoped with :rules. Rule scope is shared across checks;
check-specific rule options stay specific to each check.
A bare module applies that check globally with its defaults:
@query_checks [
Bylaw.Ecto.Query.Checks.RequiredOrder
]{Check, rules: [...]} runs the check only when at least one rule scope
matches. A single global rule can use the shorthand keyword form:
@query_checks [
{Bylaw.Ecto.Query.Checks.MandatoryWhereKeys,
rules: [fields: [:organization_id]]}
]Scoped rules use the list-of-rules form. :where and :except are shared
scope keys:
@query_checks [
{Bylaw.Ecto.Query.Checks.ExplicitVisibilityPredicates,
rules: [
[where: [ecto_schemas: [Post]], fields: [:deleted_at, :archived_at]],
[where: [tables: ["comments"]], fields: [:deleted_at]]
]}
]Matchers use plural keys with list values:
rules: [
where: [
ecto_schemas: [Post],
tables: ["posts"],
db_schemas: ["public"],
operations: [:all, :stream]
]
]Top-level validate: false is a check spec option that disables the whole
check, especially when passed through call-site overrides. Rule-level
validate: false disables only that rule. Checks with no check-specific rule
options accept only :where, :except, and validate: false inside rules.
Checks with required rule options validate those options only for matching
rules.
Each check module documents its own rule options and copyable rule examples.
Call-Site Overrides
Ecto passes repo call options to Ecto.Repo.prepare_query/3, so callers
can pass per-call Bylaw options with Repo.all(query, bylaw: ...).
Bylaw does not read those options automatically. Apps explicitly opt in by
passing Keyword.get(opts, :bylaw, []) to Bylaw.Ecto.Query.validate/4
inside prepare_query/3:
@query_checks [
Bylaw.Ecto.Query.Checks.RequiredOrder,
{Bylaw.Ecto.Query.Checks.MandatoryWhereKeys,
rules: [fields: [:organization_id]]}
]
def prepare_query(operation, query, opts) do
case Bylaw.Ecto.Query.validate(
operation,
query,
@query_checks,
Keyword.get(opts, :bylaw, [])
) do
:ok -> {query, opts}
{:error, issues} -> raise Bylaw.Ecto.Query.Issue.format_many(issues)
end
endRepo-wide check specs define defaults. Call-site bylaw: specs replace
matching repo-wide specs and append new checks after the unchanged repo-wide
checks. Passing bylaw: false disables all checks for that call.
Repo.all(query, bylaw: false)
Repo.all(query,
bylaw: [
{Bylaw.Ecto.Query.Checks.RequiredOrder, validate: false}
])
Repo.all(query,
bylaw: [
{Bylaw.Ecto.Query.Checks.MandatoryWhereKeys,
rules: [fields: [:account_id]]}
])
Repo.all(query,
bylaw: [
Bylaw.Ecto.Query.Checks.EmptyInPredicates
])
iex> import Ecto.Query
iex> query = from("posts", as: :post, limit: 1)
iex> {:error, [issue]} =
...> Bylaw.Ecto.Query.validate(:all, query, [
...> Bylaw.Ecto.Query.Checks.RequiredOrder
...> ])
iex> issue.check
Bylaw.Ecto.Query.Checks.RequiredOrder
iex> Bylaw.Ecto.Query.validate(:all, :query, [])
:ok
Summary
Functions
Runs the given query checks against a prepared Ecto query.
Runs base query checks with explicit call-site Bylaw options.
Types
@type check_spec() :: module() | {module(), Bylaw.Ecto.Query.Check.opts()}
@type checks() :: [check_spec()]
Functions
@spec validate( Bylaw.Ecto.Query.Check.operation(), Bylaw.Ecto.Query.Check.query(), checks() ) :: :ok | {:error, [Bylaw.Ecto.Query.Issue.t(), ...]}
Runs the given query checks against a prepared Ecto query.
Returns :ok when every enabled check passes. Returns {:error, issues}
when one or more checks fail.
checks accepts modules and {module, opts} tuples. Duplicate check modules
raise ArgumentError. Bylaw does not read check lists from application
config; callers pass checks explicitly.
@spec validate( Bylaw.Ecto.Query.Check.operation(), Bylaw.Ecto.Query.Check.query(), checks(), false | checks() ) :: :ok | {:error, [Bylaw.Ecto.Query.Issue.t(), ...]}
Runs base query checks with explicit call-site Bylaw options.
Pass the value from Keyword.get(repo_opts, :bylaw, []) as bylaw_opts.
Bylaw does not read Ecto repo options automatically.
An empty option list preserves the base checks. false disables all checks
for the call site. A call-site check spec with the same module as a base spec
replaces it entirely; a new check module is appended after the base checks
that were not replaced.