From 1e9fae85eb3d74bfc1da234c6b7f8ea2a699b719 Mon Sep 17 00:00:00 2001 From: Martyn Loughran Date: Mon, 17 Aug 2026 12:55:37 +0100 Subject: [PATCH] Fix ON CONFLICT placeholder numbering when inserting multiple rows CreateManyOperation numbered the placeholders after ON CONFLICT incorrectly. For example, the added spec generated: INSERT INTO users ("email", "name") VALUES ($1, $2), ($3, $4) ON CONFLICT (email) DO UPDATE SET name = $3 which is incorrect and fails with "bind message supplies 5 parameters, but prepared statement requires 4". Fixed version: INSERT INTO users ("email", "name") VALUES ($1, $2), ($3, $4) ON CONFLICT (email) DO UPDATE SET name = $5 --- spec/interro_spec.cr | 22 ++++++++++++++++++++++ src/create_many_operation.cr | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/spec/interro_spec.cr b/spec/interro_spec.cr index 7d7daca..5bfc832 100644 --- a/spec/interro_spec.cr +++ b/spec/interro_spec.cr @@ -117,6 +117,12 @@ struct UserQuery < Interro::QueryBuilder(User) do: Interro::Update.new(set: {name: name, updated_at: Time.utc}) end + def upsert!(user_templates : Array(Template), name_on_conflict : String) : Int32 + insert! user_templates.map(&.to_named_tuple), + on_conflict: Interro::ConflictHandler.new("email", + do: Interro::Update.new(set: {name: name_on_conflict})) + end + def destroy(user : User) self .where(id: user.id) @@ -457,6 +463,22 @@ describe Interro do UserQuery.new.find!(id: user.id).name.should eq "Bar" end + it "can upsert multiple rows" do + templates = [ + UserQuery::Template.new(email: "ivy-#{UUID.random}@example.com", name: "Ivy"), + UserQuery::Template.new(email: "jack-#{UUID.random}@example.com", name: "Jack"), + ] + + # No rows exist yet, so this only inserts; the conflict update does not run. + query.upsert!(templates, name_on_conflict: "ignored").should eq 2 + UserQuery.new.find!(email: templates[0].email).name.should eq "Ivy" + + # Every row now conflicts on email, so the handler updates each name. + query.upsert!(templates, name_on_conflict: "Updated").should eq 2 + UserQuery.new.find!(email: templates[0].email).name.should eq "Updated" + UserQuery.new.find!(email: templates[1].email).name.should eq "Updated" + end + it "can find a row" do email = "finduser-#{UUID.random}@example.com" created_user = UserQuery.new.create(email: email, name: "Find User") diff --git a/src/create_many_operation.cr b/src/create_many_operation.cr index 5e6707f..aa1ae08 100644 --- a/src/create_many_operation.cr +++ b/src/create_many_operation.cr @@ -66,7 +66,8 @@ module Interro if conflict_handler if (action = conflict_handler.action) && (handler_params = action.params) if handler_params.responds_to? :each_value - start = params.size + # The VALUES lists consume one placeholder per row per column. + start = params.size * params.first.size handler_params.each_value do |value| args << Interro::Any.new(value) end