Bylaw.Ecto.Query.Checks.DeterministicOrder (bylaw_ecto_query v0.3.0)

Copy Markdown View Source

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 - explicit false disables this check. It can be used in the repo-wide check list or in call-site overrides passed to Bylaw.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"]
        ]
      }
    end

    Database schemas may be nil for unqualified visible table entries. Resolver failures are not suppressed. Invalid return values raise ArgumentError.

Run globally with defaults:

Bylaw.Ecto.Query.Checks.DeterministicOrder

Run 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.

Types

unique_key()

@type unique_key() :: [String.t(), ...]

Verified unique database columns for one table.

unique_key_catalogue()

@type unique_key_catalogue() :: %{
  optional({String.t() | nil, String.t()}) => [unique_key()]
}

Verified unique database columns keyed by database schema and table.

unique_keys_resolver()

@type unique_keys_resolver() :: (-> unique_key_catalogue())

Function used to resolve verified database unique keys.

Functions

validate(operation, query, opts)

Implements the Bylaw.Ecto.Query.Check validation callback.