Bylaw.Db.Adapters.Postgres.Checks.EctoChangesetUniqueConstraints (bylaw_postgres v0.2.0)

Copy Markdown View Source

Validates Ecto.Changeset.unique_constraint/3 annotations for Postgres indexes.

Examples

With a unique index on users.email, before:

def changeset(user, attrs) do
  user
  |> Ecto.Changeset.cast(attrs, [:email])
  |> Ecto.Changeset.validate_required([:email])
end

The database protects uniqueness, but an insert conflict can bubble up as a database error instead of a changeset error attached to :email.

After, annotate the changeset with the matching constraint:

def changeset(user, attrs) do
  user
  |> Ecto.Changeset.cast(attrs, [:email])
  |> Ecto.Changeset.validate_required([:email])
  |> Ecto.Changeset.unique_constraint(:email)
end

Ecto can translate the database constraint violation into a normal changeset error for callers.

Notes

The check skips dynamic cast or change field lists, expression indexes, partial indexes, primary keys, and unique indexes whose columns cannot be mapped to Ecto schema fields.

Options

  • :validate - explicit false disables this check.
  • :paths - required non-empty list of source paths to parse for changeset functions.
  • :otp_app - OTP app used for compiled schema discovery. When the target repo can report config()[:otp_app], this is inferred.
  • :schema_modules - explicit non-empty list of schema modules to inspect instead of discovering schemas from :otp_app.
  • :rules - optional rule keyword list or non-empty list of rule keyword lists. Rules use only shared scope keys.

This check requires :paths and schema discovery from the target repo's inferred :otp_app, explicit :otp_app, or explicit :schema_modules, so bare-module configuration is not valid.

Run globally for discovered schemas:

{Bylaw.Db.Adapters.Postgres.Checks.EctoChangesetUniqueConstraints,
 paths: ["lib/my_app"]}

Run globally for explicit schema modules:

{Bylaw.Db.Adapters.Postgres.Checks.EctoChangesetUniqueConstraints,
 paths: ["lib/my_app/accounts"],
 schema_modules: [MyApp.Accounts.User, MyApp.Accounts.Organization]}

Run only for matching rule scopes:

{Bylaw.Db.Adapters.Postgres.Checks.EctoChangesetUniqueConstraints,
 paths: ["lib/my_app"],
 rules: [
   where: [schemas: ["public"]],
   except: [[tables: ["legacy_users"], constraints: ["legacy_users_email_index"]]]
 ]}

The check discovers compiled Ecto schemas through reflection, parses source files for conservative changeset candidates, and only requires unique_constraint/3 when a candidate casts all fields covered by a unique Postgres index.

Usage

Add this module to the checks passed to Bylaw.Db.Adapters.Postgres.validate/2. See the README usage section for the full ExUnit setup.

Summary

Functions

Implements the Bylaw.Db.Check validation callback.

Types

check_opt()

@type check_opt() ::
  {:validate, boolean()}
  | {:otp_app, atom()}
  | {:paths, [Path.t()]}
  | {:schema_modules, [module()]}
  | {:rules, keyword() | [keyword()]}

check_opts()

@type check_opts() :: [check_opt()]

Functions

validate(target, opts)

@spec validate(target :: Bylaw.Db.Target.t(), opts :: check_opts()) ::
  Bylaw.Db.Check.result()

Implements the Bylaw.Db.Check validation callback.