Validates that ordered queries include a known unique key for the root source.
This is useful when callers page through ordered rows or use helpers such as
Repo.one/2 with Ecto.Query.first/2 or Ecto.Query.last/2. Ordering by a
non-unique field such as :inserted_at or :name leaves rows with the same
value free to move between executions unless the query also orders by a
deterministic tie-breaker.
By default, this check trusts the root Ecto schema primary key. Callers may
also provide a zero-arity :unique_keys resolver that returns verified
database unique keys by {database_schema, table}.
Examples
Bad:
from(Post, as: :post)
|> order_by([post: p], desc: p.inserted_at)
|> limit(10)Why this is bad:
inserted_at is not guaranteed to be unique. Rows with the same timestamp can
move between executions, which can make paginated queries skip or duplicate
rows.
Better:
from(Post, as: :post)
|> order_by([post: p], desc: p.inserted_at)
|> order_by([post: p], asc: p.id)
|> limit(10)Why this is better:
The root primary key resolves ties in the visible sort key, so every row has a stable relative position.
Better for a composite primary key:
from(Membership, as: :membership)
|> order_by([membership: mem], asc: mem.inserted_at)
|> order_by([membership: mem], asc: mem.organization_id)
|> order_by([membership: mem], asc: mem.sequence)Notes
Without a :unique_keys resolver, this check only trusts the root Ecto schema
primary key. It cannot independently verify arbitrary database unique indexes
or schema-less query sources.
The check infers root schema primary keys with Ecto schema reflection. A resolver can additionally prove database-backed keys for schema and schema-less table sources. Unsupported query sources return an issue unless validation is explicitly disabled.
Options
:validate- explicitfalsedisables this check. It can be used in the repo-wide check list or in call-site overrides passed toBylaw.Ecto.Query.validate/4.:unique_keys- optional zero-arity function returning a map from{database_schema, table}to lists of unique database column sets:fn -> %{ {"public", "posts"} => [ ["id"], ["slug"], ["organisation_id", "sequence"] ] } endDatabase schemas may be
nilfor unqualified visible table entries. Resolver failures are not suppressed. Invalid return values raiseArgumentError.
Run globally with defaults:
Bylaw.Ecto.Query.Checks.DeterministicOrderRun only for matching rule scopes:
{Bylaw.Ecto.Query.Checks.DeterministicOrder,
rules: [
[where: [ecto_schemas: [Post]]],
[where: [tables: ["posts"]]]
]}This check has no check-specific rule options. :unique_keys configures the
whole check and cannot be set inside individual rules.
Usage
Add this module to the explicit check list passed through Bylaw.Ecto.Query.
See Bylaw.Ecto.Query for the full Ecto.Repo.prepare_query/3 setup.
Summary
Types
Verified unique database columns for one table.
Verified unique database columns keyed by database schema and table.
Function used to resolve verified database unique keys.
Functions
Implements the Bylaw.Ecto.Query.Check validation callback.
Types
@type unique_key() :: [String.t(), ...]
Verified unique database columns for one table.
@type unique_key_catalogue() :: %{ optional({String.t() | nil, String.t()}) => [unique_key()] }
Verified unique database columns keyed by database schema and table.
@type unique_keys_resolver() :: (-> unique_key_catalogue())
Function used to resolve verified database unique keys.
Functions
@spec validate( Bylaw.Ecto.Query.Check.operation(), Bylaw.Ecto.Query.Check.query(), opts() ) :: Bylaw.Ecto.Query.Check.result()
Implements the Bylaw.Ecto.Query.Check validation callback.