Search code examples
elixirecto

Ecto constraint error on different schema


I'm pretty sure I'm missing something here but I'm pulling my hair out over this. I've got the following schema's setup:

schema "accounts_providers" do
  field :provider, :string, null: false
  field :uid, :string, null: false
  field :auth_token, Persistence.Encrypted.Binary, null: false

  belongs_to :user, MyApp.Accounts.User
  timestamps()
end
schema "accounts_users" do
  field :name, :string
  field :username, :string
  field :nickname, :string
  field :email, :string
  field :avatar_url, :string

  has_many :providers, Polydoc.Accounts.Provider, on_delete: :delete_all
  timestamps()
end
schema "repositories_repositories" do
  field :name, :string
  field :description, :string
  field :fork, :boolean
  field :private, :boolean
  field :write_permission, :boolean
  field :uid, :string
  field :owner_name, :string

  belongs_to :provider, Polydoc.Accounts.Provider
end

I'm attempting to insert a record into the accounts_providers table but running into a constraint error, but the error is referencing a constraint that exists in the repositories_repositories table which I don't think I am touching

[debug] QUERY ERROR db=32.6ms queue=9.5ms
INSERT INTO "accounts_providers" ("auth_token","provider","uid","user_id","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT ("uid","provider") DO UPDATE SET "id" = EXCLUDED."id","provider" = EXCLUDED."provider","uid" = EXCLUDED."uid","auth_token" = EXCLUDED."auth_token","user_id" = EXCLUDED."user_id","inserted_at" = EXCLUDED."inserted_at","updated_at" = EXCLUDED."updated_at" RETURNING "id" [<<1, 10, 65, 69, 83, 46, 71, 67, 77, 46, 86, 49, 217, 158, 158, 208, 9, 91, 16, 111, 110, 135, 12, 82, 201, 8, 126, 181, 141, 227, 56, 145, 148, 2, 217, 50, 202, 36, 4, 85, 228, 160, 42, 249, 38, 24, 135, 59, 235, ...>>, "github", "1657075", 1, ~N[2019-02-03 09:42:39], ~N[2019-02-03 09:42:39]]
[info] Sent 500 in 1980ms
[error] #PID<0.586.0> running PolydocWeb.Endpoint (cowboy_protocol) terminated
Server: app.lvh.me:4000 (http)
Request: GET /auth/github/callback?code=5bf8f2761b6d3892054f
** (exit) an exception was raised:
    ** (Ecto.ConstraintError) constraint error when attempting to insert struct:

    * repositories_repositories_provider_id_fkey (foreign_key_constraint)

If you would like to stop this constraint violation from raising an
exception and instead add it as an error to your changeset, please
call `foreign_key_constraint/3` on your changeset with the constraint
`:name` as an option.

The changeset defined the following constraints:

    * accounts_providers_user_id_fkey (foreign_key_constraint)
    * accounts_providers_uid_provider_index (unique_constraint)

This is triggered when I try and run an upsert query:

provider = Provider.oauth_changeset(%Provider{}, attrs)
Repo.insert(provider, on_conflict: :replace_all, conflict_target: [:uid, :provider])

Where provider is

#Ecto.Changeset<
  action: nil,
  changes: %{
    auth_token: "5ab9b61181eb3bc8221310eb4121a861ebb5b0a8",
    provider: "github",
    uid: "1657075",
    user_id: 1
  },
  errors: [],
  data: #Polydoc.Accounts.Provider<>,
  valid?: true
>

Any clues why I'm hitting this error even though the constraint is on another table? I know I don't have the foreign_key_constraint in my changeset but this to me doesn't explain the error coming from the wrong table

Edit

Here's the changeset function for the Provider schema

def oauth_changeset(%__MODULE__{} = provider, attrs) do
  provider
  |> cast(attrs, [:provider, :uid, :auth_token, :user_id])
  |> validate_required([:provider, :uid, :auth_token])
  |> unique_constraint(:provider_uid, name: :accounts_providers_uid_provider_index)
  |> foreign_key_constraint(:user_id)
end

Solution

  • Ok figured it out - it's related to the way Ecto handles the on_conflict option of the insert/2 function. If set to :replace_all as mine was, it literally replaces everything which would appear to include the primary key which in turn broke my foreign key of the other table by making the existing records' id invalid.

    To fix, I've changed the insert statement to include specify which fields to replace instead:

    Repo.insert(provider,
                on_conflict: { :replace, [:auth_token, :updated_at]},
                conflict_target: [:uid, :provider])