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 +4 -4
- data/README.md +2 -2
- data/lib/activerecord-updateinbulk/builder.rb +81 -23
- data/lib/activerecord-updateinbulk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3bac8ddb8a7317ff277d0afbf77432ed979aa3246ec4030fd62f64ef64c98127
|
|
4
|
+
data.tar.gz: f09a62eda8673ad252d49038a8f1c3605e435b04121069b531f8d56fef48a919
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
|
214
|
+
set_assignments = write_keys.map do |key|
|
|
174
215
|
formula = @formulas[key]
|
|
175
216
|
lhs = table[key]
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
205
|
-
|
|
255
|
+
row = []
|
|
256
|
+
read_keys.each do |key|
|
|
257
|
+
next if constant_conditions.key?(key)
|
|
258
|
+
row << row_conditions[key]
|
|
206
259
|
end
|
|
207
|
-
|
|
208
|
-
next
|
|
209
|
-
|
|
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
|
-
|
|
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
|
|
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? ||
|
|
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)
|