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.