Insert exactly one sequence-breaking record?

32 views Asked by At

This is at the intersection of Ecto & Postgres.

I can't figure out how to create a one-off record into a normal table with a sequential id column using Ecto. I have a standard (?) ecto schema defined like

defmodule Recipe do
  schema "recipes" do  # standard @primary_key, meaning field(:id, :integer) iiuc
    ...
    timestamps()
  end
  @required_fields [ ... ]  # from the fields above, but not :id...
  @optional_fields [ ... ]  # from the fields above, but not :id...
  def changeset(recipe = %Recipe{}, attrs) do
    recipe
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
    |> ...  # unique constraints, custom validations, etc.
  end
end
...
defmodule Migrations.CreateRecipes do
  use Ecto.Migration

  def change do
    create table(:recipes) do
      ...  # No explicit add(:id), so getting the default
      timestamps()
    end
  end
end

The production database already contains thousands of entries and is humming along happily. These recipes are referenced by ID in all sorts of places including increasingly in csvs of user input ("use recipe #314 500 times by tuesday, storing the output on shelf #17").

I would like to reserve a few vanity IDs for special tracking & input purposes ("Use recipe #0 -- which contains only the text "Ask your shift supervisor, they know what's up" -- 500 times by tuesday. Store the mysterious output on shelf #17."). Id #0 (and for that matter, #1) are both unused and do not risk data corruption by being used in this way.

It is standard practice on my team to use one-off scripts for database backfill like this (see "what did I try" for some attempts). In part, this lets us also run post-commit hooks to publish updates, which I'd like to do here.


Do I need to break out of elixir here?

Naive insert:

changeset = Recipe.changeset(%Recipe{}, %{id: 0, ...})
Ecto.Multi.new()
|> Ecto.Multi.insert(:onetime_backfill, changeset)
|> multi_publish_on_commit()  # 
|> MyRepo.transaction()

# (with the MyRepo being normal: )
defmodule MyRepo do
  use Ecto.Repo, otp_app: :myapp, adapter: Ecto.Adapters.Postgres
  ...
end

Inserts the row, but with the sequentially next :id value not my specified value "0".

Slightly fancier insert

Same code as the Naive insert, but with a modified implementation of Recipe.changeset:

  def changeset(recipe = %Recipe{}, attrs) do
    recipe
    |> cast(attrs, [:id | @required_fields ++ @optional_fields)
    |> unique_constraint(:id)  # Surprisingly necessary!
    |> validate_required(@required_fields)

STILL inserts the row with the sequentially next :id value, not my chosen "id: 0". (which makes sense based on postgres sequential).

Raw SQL edit in my testing environment:

INSERT INTO recipes (id, name, inserted_at, updated_at, ...) 
OVERRIDING SYSTEM VALUE 
  VALUES(0, 'Misc (see Notes)', now(), now(), ...);

This worked! But no publication side effects, etc.

0

There are 0 answers