activerecord-updateinbulk 0.1.1 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d50d84d5dbf5f42cee6fc93e3e4718b2e1b7993baac8d4bde92b7634d9e5675
4
- data.tar.gz: bc16526ad3800298a96bdb5cc3c9365d8d600ac083a9273e0f45ef8655d96fad
3
+ metadata.gz: 3bac8ddb8a7317ff277d0afbf77432ed979aa3246ec4030fd62f64ef64c98127
4
+ data.tar.gz: f09a62eda8673ad252d49038a8f1c3605e435b04121069b531f8d56fef48a919
5
5
  SHA512:
6
- metadata.gz: 7f1cc791591f474b8f93c124a74f27083bc6d134292d3c3a3a076370aff29e067874ffbc684e55808d639b890657300c0c9cfd9a23f2e12d161134f34ada7926
7
- data.tar.gz: 23a234ef95ada21deab31cdcefbd075caa4d856546f69ad6622d5b70ddd24d72c4b2633763b5b2e721baba3e5e013a4a2118e58ec39d2eaf62f09e00e6e4aa0c
6
+ metadata.gz: 3f8e68e4bcc3704ffa56960fb9f12c4e49156c027f156eea4b356e710916e839a99e398e8aaddad9d7cb5e93b28a32bfd380e0f9372967c08bf76377fd7fcf9d
7
+ data.tar.gz: b6ec0dbcb9a18e1182aab529ca685a2edf441d030a28c8bc0951a79b8dcfe4f5b4c4822724d9e3fc01de7eeda387e5c0c25636ff9ca6c4d0504fe341165a1d26
data/README.md CHANGED
@@ -10,8 +10,6 @@ Similar to `update_all`, it returns the number of affected rows, and bumps updat
10
10
 
11
11
  Tested on Ruby 3.4 and Rails 8 for all builtin databases on latest versions.
12
12
 
13
- Important note: there is a flaky test on MySQL (that is not flaky on MariaDB) that I am still investigating.
14
-
15
13
  ## Usage
16
14
 
17
15
  ```ruby
@@ -75,6 +73,8 @@ Employee.update_in_bulk({
75
73
  }, record_timestamps: false)
76
74
  ```
77
75
 
76
+ Note: On MySQL [assignments are processed in series](https://dev.mysql.com/doc/refman/9.0/en/update.html), so there is no real guarantee that the timestamps are actually updated.
77
+
78
78
  ### Formulas (computed assignments)
79
79
 
80
80
  In all examples so far the queries simply assign predetermined values to rows matched, irrespective of their previous values.
@@ -126,6 +126,8 @@ module ActiveRecord::UpdateInBulk
126
126
  resolve_attribute_aliases!
127
127
  resolve_read_and_write_keys!
128
128
  verify_read_and_write_keys!
129
+ serialize_values!
130
+ detect_constant_columns!
129
131
  end
130
132
 
131
133
  def build_arel
@@ -138,7 +140,41 @@ module ActiveRecord::UpdateInBulk
138
140
  end
139
141
 
140
142
  private
141
- attr_reader :read_keys, :write_keys
143
+ attr_reader :read_keys, :write_keys, :constant_conditions, :constant_assigns
144
+
145
+ def serialize_values!
146
+ types = (read_keys | write_keys).index_with { |key| model.type_for_attribute(key) }
147
+ @conditions.each do |row|
148
+ row.each do |key, value|
149
+ next if opaque_value?(value)
150
+ row[key] = ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
151
+ end
152
+ end
153
+ @assigns.each do |row|
154
+ row.each do |key, value|
155
+ next if opaque_value?(value)
156
+ row[key] = ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
157
+ end
158
+ end
159
+ end
160
+
161
+ def detect_constant_columns!
162
+ @constant_conditions = {}
163
+ @constant_assigns = {}
164
+
165
+ read_keys.each do |key|
166
+ first = @conditions.first.fetch(key)
167
+ @constant_conditions[key] = first if @conditions.all? { |c| !opaque_value?(v = c.fetch(key)) && v == first }
168
+ end
169
+ # We can't drop all the conditions columns
170
+ @constant_conditions.delete(read_keys.first) if @constant_conditions.size == read_keys.size
171
+
172
+ (write_keys - optional_keys).each do |key|
173
+ next if @formulas.key?(key) # need to pass Arel::Attribute as argument to formula
174
+ first = @assigns.first[key]
175
+ @constant_assigns[key] = first if @assigns.all? { |a| !opaque_value?(v = a[key]) && v == first }
176
+ end
177
+ end
142
178
 
143
179
  def optional_keys
144
180
  @optional_keys ||= write_keys - @assigns.map(&:keys).reduce(write_keys, &:intersection)
@@ -149,32 +185,44 @@ module ActiveRecord::UpdateInBulk
149
185
  end
150
186
 
151
187
  def build_values_table
152
- types = (read_keys | write_keys).index_with { |key| model.type_for_attribute(key) }
153
- rows, bitmask_keys = serialize_values_rows do |key, value|
154
- next value if Arel::Nodes::SqlLiteral === value
155
- ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
156
- end
188
+ rows, bitmask_keys = serialize_values_rows
157
189
  append_bitmask_column(rows, bitmask_keys) unless bitmask_keys.empty?
158
190
  values_table = Arel::Nodes::ValuesTable.new(self.class.values_table_name, rows, connection.values_table_default_column_names(rows.first.size))
159
191
  [values_table, bitmask_keys]
160
192
  end
161
193
 
162
194
  def build_join_conditions(table, values_table)
163
- read_keys.map.with_index do |key, index|
164
- table[key].eq(values_table[index])
195
+ variable_index = 0
196
+ read_keys.map do |key|
197
+ if constant_conditions.key?(key)
198
+ table[key].eq(Arel::Nodes::Casted.new(constant_conditions[key], table[key]))
199
+ else
200
+ condition = table[key].eq(values_table[variable_index])
201
+ variable_index += 1
202
+ condition
203
+ end
165
204
  end
166
205
  end
167
206
 
168
207
  def build_set_assignments(table, values_table, bitmask_keys)
208
+ column = read_keys.count { |k| !constant_conditions.key?(k) }
209
+
169
210
  bitmask_functions = bitmask_keys.index_with.with_index(1) do |key, index|
170
211
  Arel::Nodes::NamedFunction.new("SUBSTRING", [values_table[-1], index, 1])
171
212
  end
172
213
 
173
- set_assignments = write_keys.map.with_index do |key, index|
214
+ set_assignments = write_keys.map do |key|
174
215
  formula = @formulas[key]
175
216
  lhs = table[key]
176
- rhs = values_table[index + read_keys.size]
177
- rhs = self.class.apply_formula(formula, lhs, rhs, model) if formula
217
+
218
+ if constant_assigns.key?(key)
219
+ rhs = Arel::Nodes::Casted.new(constant_assigns[key], table[key])
220
+ else
221
+ rhs = values_table[column]
222
+ column += 1
223
+ rhs = self.class.apply_formula(formula, lhs, rhs, model) if formula
224
+ end
225
+
178
226
  if function = bitmask_functions[key]
179
227
  rhs = Arel::Nodes::Case.new(function).when("1").then(rhs).else(table[key])
180
228
  elsif optional_keys.include?(key)
@@ -184,33 +232,39 @@ module ActiveRecord::UpdateInBulk
184
232
  end
185
233
 
186
234
  if timestamp_keys.any?
187
- set_assignments += timestamp_assignments(set_assignments)
235
+ # Timestamp assignments precede data assignments to increase the
236
+ # change MySQL will actually run them against the original data.
237
+ set_assignments = timestamp_assignments(set_assignments) + set_assignments
188
238
  end
189
239
 
190
240
  set_assignments
191
241
  end
192
242
 
193
243
  def typecast_values_table(values_table)
244
+ variable_keys = read_keys.reject { |k| constant_conditions.key?(k) } + write_keys.reject { |k| constant_assigns.key?(k) }
194
245
  columns_hash = model.columns_hash
195
- model_types = read_keys.to_a.concat(write_keys.to_a).map! { |key| columns_hash.fetch(key) }
246
+ model_types = variable_keys.map! { |key| columns_hash.fetch(key) }
196
247
  connection.typecast_values_table(values_table, model_types).alias(self.class.values_table_name)
197
248
  end
198
249
 
199
- def serialize_values_rows(&)
250
+ def serialize_values_rows
200
251
  bitmask_keys = Set.new
201
252
 
202
253
  rows = @conditions.each_with_index.map do |row_conditions, row_index|
203
254
  row_assigns = @assigns[row_index]
204
- condition_values = read_keys.map do |key|
205
- yield(key, row_conditions[key])
255
+ row = []
256
+ read_keys.each do |key|
257
+ next if constant_conditions.key?(key)
258
+ row << row_conditions[key]
206
259
  end
207
- write_values = write_keys.map do |key|
208
- next unless row_assigns.key?(key)
209
- value = yield(key, row_assigns[key])
260
+ write_keys.each do |key|
261
+ next if constant_assigns.key?(key)
262
+ next row << nil unless row_assigns.key?(key)
263
+ value = row_assigns[key]
210
264
  bitmask_keys.add(key) if optional_keys.include?(key) && might_be_nil_value?(value)
211
- value
265
+ row << value
212
266
  end
213
- condition_values.concat(write_values)
267
+ row
214
268
  end
215
269
 
216
270
  [rows, bitmask_keys]
@@ -240,10 +294,14 @@ module ActiveRecord::UpdateInBulk
240
294
  end
241
295
  end
242
296
 
243
- # When you assign a value to NULL, we need to use a bitmask to distinguish that
297
+ # When you assign a value to NULL, we need to use a bitmask to distinguish
244
298
  # row in the values table from rows where the column is not to be assigned at all.
245
299
  def might_be_nil_value?(value)
246
- value.nil? || value.is_a?(Arel::Nodes::SqlLiteral) || value.is_a?(Arel::Nodes::BindParam)
300
+ value.nil? || opaque_value?(value)
301
+ end
302
+
303
+ def opaque_value?(value)
304
+ value.is_a?(Arel::Nodes::SqlLiteral) || value.is_a?(Arel::Nodes::BindParam)
247
305
  end
248
306
 
249
307
  def normalize_formulas(formulas)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module UpdateInBulk
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-updateinbulk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Carvalho