Repository: pennylane-hq/activerecord_batch_update
Pull request #11: Add Rails 8 support
Author: login: frodsan
Created: 2025-03-25T09:26:39Z | Merged: 2025-03-25T09:44:06Z

I've also renamed the private methods to identify they come from this gem and to avoid clashes with other AR methods in the future. I can revert that if you think it's not necessary 😄 


Diff:
diff --git a/.tool-versions b/.tool-versions
index 05668b7..ae5ecdb 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1 +1 @@
-ruby 3.3.4
+ruby 3.4.2
diff --git a/Gemfile b/Gemfile
index f604bf6..004c82a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,4 +8,4 @@ gem 'rake-release'
 gem 'rspec'
 gem 'rubocop'
 gem 'rubocop-rspec'
-gem 'sqlite3', '~> 1.4'
+gem 'sqlite3', '~> 2.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 32dd451..db8a0e4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,86 +1,97 @@
 PATH
   remote: .
   specs:
-    activerecord_batch_update (0.0.3)
-      activerecord (~> 7.0)
-      activesupport (~> 7.0)
+    activerecord_batch_update (1.0.0)
+      activerecord (>= 7.0, < 8.1)
+      activesupport (>= 7.0, < 8.1)
 
 GEM
   remote: https://rubygems.org/
   specs:
-    activemodel (7.1.3.4)
-      activesupport (= 7.1.3.4)
-    activerecord (7.1.3.4)
-      activemodel (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    activemodel (8.0.2)
+      activesupport (= 8.0.2)
+    activerecord (8.0.2)
+      activemodel (= 8.0.2)
+      activesupport (= 8.0.2)
       timeout (>= 0.4.0)
-    activesupport (7.1.3.4)
+    activesupport (8.0.2)
       base64
+      benchmark (>= 0.3)
       bigdecimal
-      concurrent-ruby (~> 1.0, >= 1.0.2)
+      concurrent-ruby (~> 1.0, >= 1.3.1)
       connection_pool (>= 2.2.5)
       drb
       i18n (>= 1.6, < 2)
+      logger (>= 1.4.2)
       minitest (>= 5.1)
-      mutex_m
-      tzinfo (~> 2.0)
-    ast (2.4.2)
+      securerandom (>= 0.3)
+      tzinfo (~> 2.0, >= 2.0.5)
+      uri (>= 0.13.1)
+    ast (2.4.3)
     base64 (0.2.0)
-    bigdecimal (3.1.8)
+    benchmark (0.4.0)
+    bigdecimal (3.1.9)
     byebug (11.1.3)
-    concurrent-ruby (1.3.4)
-    connection_pool (2.4.1)
-    diff-lcs (1.5.1)
+    concurrent-ruby (1.3.5)
+    connection_pool (2.5.0)
+    diff-lcs (1.6.1)
     drb (2.2.1)
-    i18n (1.14.6)
+    i18n (1.14.7)
       concurrent-ruby (~> 1.0)
-    json (2.7.2)
-    language_server-protocol (3.17.0.3)
-    minitest (5.25.1)
-    mutex_m (0.2.0)
+    json (2.10.2)
+    language_server-protocol (3.17.0.4)
+    lint_roller (1.1.0)
+    logger (1.6.6)
+    minitest (5.25.5)
     parallel (1.26.3)
-    parser (3.3.5.0)
+    parser (3.3.7.2)
       ast (~> 2.4.1)
       racc
     racc (1.8.1)
     rainbow (3.1.1)
     rake-release (1.3.0)
       bundler (>= 1.11, < 3)
-    regexp_parser (2.9.2)
+    regexp_parser (2.10.0)
     rspec (3.13.0)
       rspec-core (~> 3.13.0)
       rspec-expectations (~> 3.13.0)
       rspec-mocks (~> 3.13.0)
-    rspec-core (3.13.0)
+    rspec-core (3.13.3)
       rspec-support (~> 3.13.0)
-    rspec-expectations (3.13.1)
+    rspec-expectations (3.13.3)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.13.0)
-    rspec-mocks (3.13.1)
+    rspec-mocks (3.13.2)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.13.0)
-    rspec-support (3.13.1)
-    rubocop (1.67.0)
+    rspec-support (3.13.2)
+    rubocop (1.74.0)
       json (~> 2.3)
-      language_server-protocol (>= 3.17.0)
+      language_server-protocol (~> 3.17.0.2)
+      lint_roller (~> 1.1.0)
       parallel (~> 1.10)
       parser (>= 3.3.0.2)
       rainbow (>= 2.2.2, < 4.0)
-      regexp_parser (>= 2.4, < 3.0)
-      rubocop-ast (>= 1.32.2, < 2.0)
+      regexp_parser (>= 2.9.3, < 3.0)
+      rubocop-ast (>= 1.38.0, < 2.0)
       ruby-progressbar (~> 1.7)
-      unicode-display_width (>= 2.4.0, < 3.0)
-    rubocop-ast (1.32.3)
-      parser (>= 3.3.1.0)
-    rubocop-rspec (3.1.0)
-      rubocop (~> 1.61)
+      unicode-display_width (>= 2.4.0, < 4.0)
+    rubocop-ast (1.41.0)
+      parser (>= 3.3.7.2)
+    rubocop-rspec (3.5.0)
+      lint_roller (~> 1.1)
+      rubocop (~> 1.72, >= 1.72.1)
     ruby-progressbar (1.13.0)
-    sqlite3 (1.7.3-arm64-darwin)
-    sqlite3 (1.7.3-x86_64-linux)
-    timeout (0.4.1)
+    securerandom (0.4.1)
+    sqlite3 (2.6.0-arm64-darwin)
+    sqlite3 (2.6.0-x86_64-linux-gnu)
+    timeout (0.4.3)
     tzinfo (2.0.6)
       concurrent-ruby (~> 1.0)
-    unicode-display_width (2.6.0)
+    unicode-display_width (3.1.4)
+      unicode-emoji (~> 4.0, >= 4.0.4)
+    unicode-emoji (4.0.4)
+    uri (1.0.3)
 
 PLATFORMS
   arm64-darwin-23
@@ -93,7 +104,7 @@ DEPENDENCIES
   rspec
   rubocop
   rubocop-rspec
-  sqlite3 (~> 1.4)
+  sqlite3 (~> 2.1)
 
 BUNDLED WITH
    2.5.11
diff --git a/activerecord_batch_update.gemspec b/activerecord_batch_update.gemspec
index 384db16..2efb532 100644
--- a/activerecord_batch_update.gemspec
+++ b/activerecord_batch_update.gemspec
@@ -2,7 +2,7 @@
 
 Gem::Specification.new do |s|
   s.name        = 'activerecord_batch_update'
-  s.version     = '0.0.3'
+  s.version     = '1.0.0'
   s.summary     = 'Update multiple records with different values in a small number of queries'
   s.description = ''
   s.authors     = ['Quentin de Metz']
@@ -14,8 +14,8 @@ Gem::Specification.new do |s|
 
   s.required_ruby_version = '>= 3.3.4'
 
-  s.add_dependency 'activerecord', '~> 7.0'
-  s.add_dependency 'activesupport', '~> 7.0'
+  s.add_dependency 'activerecord', '>= 7.0', '< 8.1'
+  s.add_dependency 'activesupport', '>= 7.0', '< 8.1'
 
   s.metadata['rubygems_mfa_required'] = 'true'
 end
diff --git a/lib/activerecord_batch_update.rb b/lib/activerecord_batch_update.rb
index 9f03a31..e7db923 100644
--- a/lib/activerecord_batch_update.rb
+++ b/lib/activerecord_batch_update.rb
@@ -45,15 +45,15 @@ def batch_update_statements(entries, update_on: :id, batch_size: 100)
       entries.map(&:stringify_keys).group_by { _1.keys.sort! }.sort.flat_map do |(keys, items)|
         next [] if keys.empty?
 
-        where_clause = where_statement(update_on)
-        update_clause = update_statement(keys - update_on)
+        where_clause = batch_update_where_statement(update_on)
+        update_clause = batch_update_statement(keys - update_on)
 
         items.each_slice(batch_size).map do |slice|
           [
-            "WITH \"#{cte_table.name}\" (#{keys.join(', ')})",
-            "AS ( #{values_statement(slice, keys)} )",
+            "WITH \"#{batch_update_table.name}\" (#{keys.join(', ')})",
+            "AS ( #{batch_update_values_statement(slice, keys)} )",
             update_clause,
-            "FROM \"#{cte_table.name}\"",
+            "FROM \"#{batch_update_table.name}\"",
             "WHERE #{where_clause}"
           ].join(' ')
         end
@@ -62,22 +62,22 @@ def batch_update_statements(entries, update_on: :id, batch_size: 100)
 
     private
 
-    def cte_table
-      @cte_table ||= Arel::Table.new('batch_updates')
+    def batch_update_table
+      @batch_update_table ||= Arel::Table.new('batch_updates')
     end
 
-    def values_statement(items, cols)
+    def batch_update_values_statement(items, cols)
       first, *rest = items
 
       rows = [
-        values_list_casted_item(first, cols),
-        *rest.map { values_list_other_item(_1, cols) }
+        batch_update_casted_item(first, cols),
+        *rest.map { batch_update_quoted_item(_1, cols) }
       ]
 
       "VALUES #{rows.map { "(#{_1.map(&:to_sql).join(', ')})" }.join(', ')}"
     end
 
-    def values_list_casted_item(item, cols)
+    def batch_update_casted_item(item, cols)
       cols.map do |col|
         Arel::Nodes::NamedFunction.new(
           'CAST',
@@ -88,27 +88,27 @@ def values_list_casted_item(item, cols)
       end
     end
 
-    def values_list_other_item(item, cols)
+    def batch_update_quoted_item(item, cols)
       cols.map do |col|
         Arel::Nodes.build_quoted(item[col], arel_table[col])
       end
     end
 
-    def update_statement(cols)
+    def batch_update_statement(cols)
       Arel::UpdateManager.new(arel_table).tap do |um|
         um.set(
           cols.map do |col|
             [
               arel_table[col],
-              cte_table[col]
+              batch_update_table[col]
             ]
           end
         )
       end.to_sql
     end
 
-    def where_statement(primary_keys)
-      primary_keys.map { arel_table[_1].eq(cte_table[_1]) }.reduce(:and).to_sql
+    def batch_update_where_statement(primary_keys)
+      primary_keys.map { arel_table[_1].eq(batch_update_table[_1]) }.reduce(:and).to_sql
     end
   end
 end


Reviews:
user: login: quentindemetz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T09:27:52Z
user: login: quentindemetz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T09:28:41Z
user: login: quentindemetz
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T09:43:55Z

Review comments:
diff_hunk: @@ -2,7 +2,7 @@
 
 Gem::Specification.new do |s|
   s.name        = 'activerecord_batch_update'
-  s.version     = '0.0.3'
+  s.version     = '1.0.0'
path: activerecord_batch_update.gemspec
user: login: quentindemetz
body: `1.0.0` release 🍻 
created_at: 2025-03-25T09:27:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,15 +45,15 @@ def batch_update_statements(entries, update_on: :id, batch_size: 100)
       entries.map(&:stringify_keys).group_by { _1.keys.sort! }.sort.flat_map do |(keys, items)|
         next [] if keys.empty?
 
-        where_clause = where_statement(update_on)
-        update_clause = update_statement(keys - update_on)
+        where_clause = batch_update_where_statement(update_on)
+        update_clause = batch_update_statement(keys - update_on)
 
         items.each_slice(batch_size).map do |slice|
           [
-            "WITH \"#{cte_table.name}\" (#{keys.join(', ')})",
-            "AS ( #{values_statement(slice, keys)} )",
+            "WITH \"#{batch_update_table.name}\" (#{keys.join(', ')})",
+            "AS ( #{batch_update_values_statement(slice, keys)} )",
             update_clause,
-            "FROM \"#{cte_table.name}\"",
+            "FROM \"#{batch_update_table.name}\"",
             "WHERE #{where_clause}"
           ].join(' ')
         end
path: lib/activerecord_batch_update.rb
user: login: quentindemetz
body: we're waiting for some of [these PRs](https://github.com/rails/rails/pulls/benedikt) to be merged to get rid of string interpolation here ⌛ 
created_at: 2025-03-25T09:28:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: quentindemetz
created_at: 2025-03-25T09:29:39Z
body: Is the rubygems release automated or do you need me to do it?
author_association: CONTRIBUTOR
user: login: frodsan
created_at: 2025-03-25T09:30:00Z
body: @quentindemetz I need you to do it 😄 
author_association: CONTRIBUTOR