activerecord_batch_update 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/activerecord_batch_update.rb +118 -0
  3. metadata +72 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 614292d3e09e070b8d7aff48134d8cee8104dc9f00e96e9deb33b0195bf04858
4
+ data.tar.gz: 2c4f608cea0f5ae569c76df93c3f3aafed9bc0911c13253d7a2cb02b9c24332d
5
+ SHA512:
6
+ metadata.gz: d5be0abbf08a712b5b5a138b15e37928b9b50c138851651877ac56837f18b884e82b226eaa1c96c1dd1198c8c017bd665c3ac57b6c7e78ecbbfb70f8d0f22443
7
+ data.tar.gz: ba349d81455f2908dbcddff5da11c4016b8660e13af081dcdfd4cfd55043c35b73b22bd6e221f95638eb6a23e45a6042ae310e8b38de85f7d73e20bf9a677ea8
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ module ActiveRecordBatchUpdate
6
+ extend ::ActiveSupport::Concern
7
+
8
+ # Given an array of records with changes,
9
+ # perform the minimal amount of queries to update
10
+ # all the records without including unchanged attributes
11
+ # in the UPDATE statement.
12
+ #
13
+ # Do this without INSERT ... ON DUPLICATE KEY UPDATE
14
+ # which will re-insert the objects if they were deleted in another thread
15
+
16
+ module ClassMethods
17
+ def batch_update(entries, columns:, batch_size: 100, validate: true)
18
+ columns = column_names if columns == :all
19
+ columns = (Array.wrap(columns).map(&:to_s) + %w[updated_at]).uniq
20
+
21
+ entries = entries.select { columns.intersect?(_1.changed) }
22
+ entries.each { _1.updated_at = Time.current } if has_attribute?('updated_at')
23
+ entries.each(&:validate!) if validate
24
+
25
+ primary_keys = Array.wrap(primary_key).map(&:to_s)
26
+
27
+ updated_count = batch_update_statements(
28
+ entries.map do |entry|
29
+ (primary_keys + (entry.changed & columns)).to_h { [_1, entry.read_attribute(_1)] }
30
+ end,
31
+ update_on: primary_keys,
32
+ batch_size: batch_size
33
+ ).sum do |sql|
34
+ connection.exec_update(sql)
35
+ end
36
+
37
+ connection.clear_query_cache if connection.query_cache_enabled
38
+
39
+ updated_count
40
+ end
41
+
42
+ def batch_update_statements(entries, update_on: :id, batch_size: 100)
43
+ update_on = Array.wrap(update_on).map(&:to_s)
44
+
45
+ entries.map(&:stringify_keys).group_by { _1.keys.sort! }.sort.flat_map do |(keys, items)|
46
+ next [] if keys.empty?
47
+
48
+ where_clause = where_statement(update_on)
49
+ update_clause = update_statement(keys - update_on)
50
+
51
+ items.each_slice(batch_size).map do |slice|
52
+ [
53
+ "WITH \"#{cte_table.name}\" (#{keys.join(', ')})",
54
+ "AS ( #{values_statement(slice, keys)} )",
55
+ update_clause,
56
+ "FROM \"#{cte_table.name}\"",
57
+ "WHERE #{where_clause}"
58
+ ].join(' ')
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def cte_table
66
+ @cte_table ||= Arel::Table.new('batch_updates')
67
+ end
68
+
69
+ def values_statement(items, cols)
70
+ first, *rest = items
71
+
72
+ rows = [
73
+ values_list_casted_item(first, cols),
74
+ *rest.map { values_list_other_item(_1, cols) }
75
+ ]
76
+
77
+ "VALUES #{rows.map { "(#{_1.map(&:to_sql).join(', ')})" }.join(', ')}"
78
+ end
79
+
80
+ def values_list_casted_item(item, cols)
81
+ cols.map do |col|
82
+ Arel::Nodes::NamedFunction.new(
83
+ 'CAST',
84
+ [
85
+ Arel::Nodes.build_quoted(item[col], arel_table[col]).as(columns_hash[col].sql_type_metadata.sql_type)
86
+ ]
87
+ )
88
+ end
89
+ end
90
+
91
+ def values_list_other_item(item, cols)
92
+ cols.map do |col|
93
+ Arel::Nodes.build_quoted(item[col], arel_table[col])
94
+ end
95
+ end
96
+
97
+ def update_statement(cols)
98
+ Arel::UpdateManager.new(arel_table).tap do |um|
99
+ um.set(
100
+ cols.map do |col|
101
+ [
102
+ arel_table[col],
103
+ cte_table[col]
104
+ ]
105
+ end
106
+ )
107
+ end.to_sql
108
+ end
109
+
110
+ def where_statement(primary_keys)
111
+ primary_keys.map { arel_table[_1].eq(cte_table[_1]) }.reduce(:and).to_sql
112
+ end
113
+ end
114
+ end
115
+
116
+ ActiveSupport.on_load(:active_record) do
117
+ include(ActiveRecordBatchUpdate)
118
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord_batch_update
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Quentin de Metz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-10-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ description: ''
42
+ email: quentin@pennylane.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/activerecord_batch_update.rb
48
+ homepage: https://rubygems.org/gems/activerecord_batch_update
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ rubygems_mfa_required: 'true'
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.3.4
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.5.11
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Update multiple records with different values in a small number of queries
72
+ test_files: []