From 911b003c91100dc97ba867847168e113d4310267 Mon Sep 17 00:00:00 2001 From: Martyn Loughran Date: Mon, 21 Sep 2026 16:31:54 +0200 Subject: [PATCH] Add missing `QueryBuilder#insert(records)` API This is the non-bang version of `QueryBuilder#insert(records)!` which returns the inserted rows. I noticed that `CreateManyOperation#call` was previously dead code (and broken), so this has been updated exactly matching the convention from `CreateOperation#call`, and specs added. The alternative would be to delete `CreateManyOperation#call`. --- spec/interro_spec.cr | 37 ++++++++++++++++++++++++++++++++++++ src/create_many_operation.cr | 14 ++++++++++---- src/query_builder.cr | 5 +++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/spec/interro_spec.cr b/spec/interro_spec.cr index b36e82f..9b3e492 100644 --- a/spec/interro_spec.cr +++ b/spec/interro_spec.cr @@ -106,6 +106,10 @@ struct UserQuery < Interro::QueryBuilder(User) insert! email: email, name: name end + def create(user_templates : Array(Template)) + insert user_templates.map(&.to_named_tuple) + end + def create!(user_templates : Array(Template)) insert! user_templates.map(&.to_named_tuple) end @@ -117,6 +121,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) : Array(User) + insert user_templates.map(&.to_named_tuple), + on_conflict: Interro::ConflictHandler.new("email", + do: Interro::Update.new(set: {name: name_on_conflict})) + 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", @@ -445,6 +455,17 @@ describe Interro do user.email.should eq email user.name.should eq "Foo" end + + it "can insert many rows" do + templates = Array.new(10) { |i| + UserQuery::Template.new(email: "one-of-many.#{UUID.v7}", name: "User #{i}") + } + + users = UserQuery.new.create(templates) + + users.map(&.email).should eq templates.map(&.email) + users.map(&.name).should eq templates.map(&.name) + end it "can insert many rows without returning them" do result = UserQuery.new.create!(Array.new(10) { |i| @@ -464,6 +485,22 @@ describe Interro do end it "can upsert multiple rows" do + templates = [ + UserQuery::Template.new(email: "pip-#{UUID.random}@example.com", name: "Pip"), + UserQuery::Template.new(email: "pat-#{UUID.random}@example.com", name: "Pat"), + ] + + # No rows exist yet, so this only inserts; the conflict update does not run. + inserted = query.upsert(templates, name_on_conflict: "ignored") + inserted.map(&.name).should eq %w[Pip Pat] + + # Every row now conflicts on email, so the handler updates each name. + upserted = query.upsert(templates, name_on_conflict: "Updated") + upserted.map(&.id).should eq inserted.map(&.id) + upserted.map(&.name).should eq %w[Updated Updated] + end + + it "can upsert multiple rows without returning them" do templates = [ UserQuery::Template.new(email: "ivy-#{UUID.random}@example.com", name: "Ivy"), UserQuery::Template.new(email: "jack-#{UUID.random}@example.com", name: "Jack"), diff --git a/src/create_many_operation.cr b/src/create_many_operation.cr index aa1ae08..b853fad 100644 --- a/src/create_many_operation.cr +++ b/src/create_many_operation.cr @@ -8,17 +8,17 @@ module Interro def initialize(@queryable : DB::Database | DB::Connection) end - def call(query : QueryBuilder(T), params, on_conflict conflict_handler : ConflictHandler? = nil) : T + def call(query : QueryBuilder(T), params : Array(NamedTuple), on_conflict conflict_handler : ConflictHandler? = nil) : Array(T) table_name = query.sql_table_name args = params - .values + .flat_map(&.values.to_a) .map { |value| Interro::Any.new(value) } .to_a sql = generate_query query.sql_table_name, params, args, on_conflict: conflict_handler, returning: ->(io : IO) { query.select_columns io } - @queryable.query_one sql, args: args, as: T + @queryable.query_all sql, args: args, as: T end def call!(query : QueryBuilder(T), params : Array(NamedTuple), on_conflict conflict_handler : ConflictHandler? = nil) : Int32 @@ -28,7 +28,8 @@ module Interro .map { |value| Interro::Any.new(value) } .to_a sql = generate_query query.sql_table_name, params, args, - on_conflict: conflict_handler + on_conflict: conflict_handler, + returning: nil @queryable.exec(sql, args: args) .rows_affected @@ -42,6 +43,7 @@ module Interro params : Array(NamedTuple), args, on_conflict conflict_handler : ConflictHandler?, + returning returning_clause, ) String.build do |str| str << "INSERT INTO " << table_name << " (" @@ -75,6 +77,10 @@ module Interro end conflict_handler.to_sql str, start_at: start || 1 end + if returning_clause + str << " RETURNING " + returning_clause.call str + end end end end diff --git a/src/query_builder.cr b/src/query_builder.cr index 8dd7d3a..7612b09 100644 --- a/src/query_builder.cr +++ b/src/query_builder.cr @@ -687,6 +687,11 @@ module Interro create_operation.call!(self, values, on_conflict: on_conflict) end + # :doc: + protected def insert(records : Array(NamedTuple), on_conflict : ConflictHandler? = nil) : Array(T) + create_many_operation.call(self, records, on_conflict: on_conflict) + end + # :doc: protected def insert!(records : Array(NamedTuple), on_conflict : ConflictHandler? = nil) : Int32 create_many_operation.call!(self, records, on_conflict: on_conflict)