activerecord-updateinbulk 0.2.0 → 0.3.0
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 -9
- data/lib/activerecord-updateinbulk/adapters/postgresql_adapter.rb +2 -1
- data/lib/activerecord-updateinbulk/base.rb +12 -0
- data/lib/activerecord-updateinbulk/builder.rb +75 -47
- data/lib/activerecord-updateinbulk/railtie.rb +14 -10
- data/lib/activerecord-updateinbulk/version.rb +1 -1
- metadata +1 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b333b357426d210ed69dfa18de2f7f77ae8088e9fc47819198118f22f7647fc
|
|
4
|
+
data.tar.gz: 96eb8a466eb413735de1e1ed4e80dbbc18402b356b5870d9b24b92826828b425
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a9ff7817a67baf305cd391c855e7607b7adc4c756abadf8d11bba119ad137bd378e83841e80dacaa0abe8265cd3c6d432f58faa4029608fe783c3ab410b7b14
|
|
7
|
+
data.tar.gz: b71a70301fa255f1c52ae9472b6cce357b2d8f10313226398e74294af0a76179b04383d3d2c5fa52b82fa05dc5dd3581fe4da0330414c1394d8ab900dd4863e9
|
data/README.md
CHANGED
|
@@ -61,17 +61,10 @@ Order.joins(:items).where(items: { status: :shipped }).update_in_bulk({
|
|
|
61
61
|
|
|
62
62
|
### Rails configuration
|
|
63
63
|
|
|
64
|
-
Railtie options are available at `config.active_record_update_in_bulk`:
|
|
64
|
+
Railtie initializer options are available at `config.active_record_update_in_bulk`:
|
|
65
65
|
|
|
66
66
|
- `values_table_alias` (`String`, optional): alias used for generated VALUES tables (default `"t"`).
|
|
67
|
-
- `ignore_scope_order` (`Boolean`, default `true`): when true, ORDER BY scopes are ignored by `update_in_bulk
|
|
68
|
-
|
|
69
|
-
Example initializer:
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
Rails.application.config.active_record_update_in_bulk.values_table_alias = "vals"
|
|
73
|
-
Rails.application.config.active_record_update_in_bulk.ignore_scope_order = true
|
|
74
|
-
```
|
|
67
|
+
- `ignore_scope_order` (`Boolean`, default `true`): when true, ORDER BY scopes are ignored by `update_in_bulk`.
|
|
75
68
|
|
|
76
69
|
### Record timestamps
|
|
77
70
|
|
|
@@ -12,8 +12,9 @@ module ActiveRecord::UpdateInBulk
|
|
|
12
12
|
case column
|
|
13
13
|
when ActiveRecord::ConnectionAdapters::PostgreSQL::Column
|
|
14
14
|
if SAFE_TYPES_FOR_VALUES_TABLE.exclude?(column.type) ||
|
|
15
|
+
column.array ||
|
|
15
16
|
values_table.rows.all? { |row| row[index].nil? }
|
|
16
|
-
column.sql_type
|
|
17
|
+
column.sql_type_metadata.sql_type
|
|
17
18
|
end
|
|
18
19
|
when Arel::Nodes::SqlLiteral, nil
|
|
19
20
|
column
|
|
@@ -18,6 +18,18 @@ module ActiveRecord
|
|
|
18
18
|
def self.load_from_connection_pool(connection_pool) # :nodoc:
|
|
19
19
|
require_adapter connection_pool.db_config.adapter
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def self.register_formula(name, &formula)
|
|
23
|
+
Builder.register_formula(name, &formula)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.unregister_formula(name)
|
|
27
|
+
Builder.unregister_formula(name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.registered_formula?(name)
|
|
31
|
+
Builder.registered_formula?(name)
|
|
32
|
+
end
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
|
|
@@ -4,13 +4,31 @@ require "active_support/core_ext/enumerable"
|
|
|
4
4
|
|
|
5
5
|
module ActiveRecord::UpdateInBulk
|
|
6
6
|
class Builder # :nodoc:
|
|
7
|
-
FORMULAS = [:add, :subtract, :concat_append, :concat_prepend].freeze
|
|
8
7
|
SAFE_COMPARISON_TYPES = [:boolean, :string, :text, :integer, :float, :decimal].freeze
|
|
9
8
|
|
|
10
9
|
class << self
|
|
11
10
|
attr_accessor :values_table_name
|
|
12
11
|
attr_accessor :ignore_scope_order
|
|
13
12
|
|
|
13
|
+
def register_formula(name, &formula)
|
|
14
|
+
raise ArgumentError, "Missing block" unless formula
|
|
15
|
+
|
|
16
|
+
name = name.to_sym
|
|
17
|
+
if registered_formulas.key?(name)
|
|
18
|
+
raise ArgumentError, "Formula already registered: #{name.inspect}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
registered_formulas[name] = formula
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def unregister_formula(name)
|
|
25
|
+
registered_formulas.delete(name.to_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def registered_formula?(name)
|
|
29
|
+
registered_formulas.key?(name.to_sym)
|
|
30
|
+
end
|
|
31
|
+
|
|
14
32
|
# Normalize all input formats into separated format [conditions, assigns].
|
|
15
33
|
def normalize_updates(model, updates, values = nil)
|
|
16
34
|
conditions = []
|
|
@@ -48,39 +66,37 @@ module ActiveRecord::UpdateInBulk
|
|
|
48
66
|
end
|
|
49
67
|
|
|
50
68
|
def apply_formula(formula, lhs, rhs, model)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
when :add
|
|
54
|
-
lhs + rhs
|
|
55
|
-
when :subtract
|
|
56
|
-
lhs - rhs
|
|
57
|
-
when :concat_append
|
|
58
|
-
lhs.concat(rhs)
|
|
59
|
-
when :concat_prepend
|
|
60
|
-
rhs.concat(lhs)
|
|
61
|
-
when Proc
|
|
62
|
-
node = apply_proc_formula(formula, lhs, rhs, model)
|
|
63
|
-
unless Arel.arel_node?(node)
|
|
64
|
-
raise ArgumentError, "Custom formula must return an Arel node"
|
|
65
|
-
end
|
|
66
|
-
node
|
|
69
|
+
formula_proc = case formula
|
|
70
|
+
when Proc then formula
|
|
67
71
|
else
|
|
68
|
-
|
|
72
|
+
registered_formulas.fetch(formula.to_sym) do
|
|
73
|
+
raise ArgumentError, "Unknown formula: #{formula.inspect}"
|
|
74
|
+
end
|
|
69
75
|
end
|
|
76
|
+
|
|
77
|
+
node = apply_proc_formula(formula_proc, lhs, rhs, model)
|
|
78
|
+
raise ArgumentError, "Custom formula must return an Arel node" unless Arel.arel_node?(node)
|
|
79
|
+
|
|
80
|
+
node
|
|
70
81
|
end
|
|
71
82
|
|
|
72
83
|
def apply_proc_formula(formula, lhs, rhs, model)
|
|
73
84
|
case formula.arity
|
|
74
|
-
when 2
|
|
75
|
-
|
|
76
|
-
when 3
|
|
77
|
-
formula.call(lhs, rhs, model)
|
|
78
|
-
else
|
|
79
|
-
raise ArgumentError, "Custom formula must accept 2 or 3 arguments"
|
|
85
|
+
when 2 then formula.call(lhs, rhs)
|
|
86
|
+
else formula.call(lhs, rhs, model)
|
|
80
87
|
end
|
|
81
88
|
end
|
|
82
89
|
|
|
83
90
|
private
|
|
91
|
+
def registered_formulas
|
|
92
|
+
@registered_formulas ||= {
|
|
93
|
+
add: lambda { |lhs, rhs| lhs + rhs },
|
|
94
|
+
subtract: lambda { |lhs, rhs| lhs - rhs },
|
|
95
|
+
concat_append: lambda { |lhs, rhs| lhs.concat(rhs) },
|
|
96
|
+
concat_prepend: lambda { |lhs, rhs| rhs.concat(lhs) }
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
84
100
|
def normalize_conditions(model, conditions)
|
|
85
101
|
if conditions.is_a?(Hash)
|
|
86
102
|
conditions
|
|
@@ -122,14 +138,14 @@ module ActiveRecord::UpdateInBulk
|
|
|
122
138
|
@conditions = conditions
|
|
123
139
|
@assigns = assigns
|
|
124
140
|
@formulas = normalize_formulas(formulas)
|
|
141
|
+
@auto_locking_column = nil
|
|
125
142
|
|
|
126
143
|
resolve_attribute_aliases!
|
|
127
144
|
resolve_read_and_write_keys!
|
|
145
|
+
apply_optimistic_locking!
|
|
128
146
|
verify_read_and_write_keys!
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
serialize_values!
|
|
132
|
-
end
|
|
147
|
+
serialize_values!
|
|
148
|
+
detect_constant_columns! unless simple_update?
|
|
133
149
|
end
|
|
134
150
|
|
|
135
151
|
def build_arel
|
|
@@ -160,26 +176,24 @@ module ActiveRecord::UpdateInBulk
|
|
|
160
176
|
def build_simple_conditions(table)
|
|
161
177
|
row_conditions = @conditions.first
|
|
162
178
|
read_keys.map do |key|
|
|
163
|
-
table[key].eq(
|
|
179
|
+
table[key].eq(quoted_value(row_conditions.fetch(key)))
|
|
164
180
|
end
|
|
165
181
|
end
|
|
166
182
|
|
|
167
183
|
def build_simple_assignments(table)
|
|
168
184
|
row_assigns = @assigns.first
|
|
169
185
|
write_keys.map do |key|
|
|
170
|
-
[table[key],
|
|
186
|
+
[table[key], quoted_value(row_assigns.fetch(key))]
|
|
171
187
|
end
|
|
172
188
|
end
|
|
173
189
|
|
|
174
190
|
def detect_constant_columns!
|
|
175
191
|
@constant_assigns = {}
|
|
176
|
-
columns_hash = model.columns_hash
|
|
177
192
|
|
|
178
193
|
(write_keys - optional_keys).each do |key|
|
|
179
194
|
next if @formulas.key?(key) # need to pass Arel::Attribute as argument to formula
|
|
180
|
-
next unless SAFE_COMPARISON_TYPES.include?(columns_hash.fetch(key).type)
|
|
181
195
|
first = @assigns.first[key]
|
|
182
|
-
@constant_assigns[key] = first if @assigns.all? { |a| !
|
|
196
|
+
@constant_assigns[key] = first if @assigns.all? { |a| !Arel.arel_node?(v = a[key]) && v == first }
|
|
183
197
|
end
|
|
184
198
|
end
|
|
185
199
|
|
|
@@ -187,14 +201,14 @@ module ActiveRecord::UpdateInBulk
|
|
|
187
201
|
types = read_keys.index_with { |key| model.type_for_attribute(key) }
|
|
188
202
|
@conditions.each do |row|
|
|
189
203
|
row.each do |key, value|
|
|
190
|
-
next if
|
|
204
|
+
next if Arel.arel_node?(value)
|
|
191
205
|
row[key] = ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
|
|
192
206
|
end
|
|
193
207
|
end
|
|
194
208
|
types = write_keys.index_with { |key| model.type_for_attribute(key) }
|
|
195
209
|
@assigns.each do |row|
|
|
196
210
|
row.each do |key, value|
|
|
197
|
-
next if
|
|
211
|
+
next if Arel.arel_node?(value)
|
|
198
212
|
row[key] = ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
|
|
199
213
|
end
|
|
200
214
|
end
|
|
@@ -210,7 +224,7 @@ module ActiveRecord::UpdateInBulk
|
|
|
210
224
|
|
|
211
225
|
def build_values_table_rows
|
|
212
226
|
bitmask_keys = Set.new
|
|
213
|
-
non_constant_write_keys = write_keys
|
|
227
|
+
non_constant_write_keys = write_keys.reject { |key| constant_assigns.key?(key) }
|
|
214
228
|
|
|
215
229
|
rows = @conditions.map.with_index do |row_conditions, row_index|
|
|
216
230
|
row_assigns = @assigns[row_index]
|
|
@@ -252,6 +266,10 @@ module ActiveRecord::UpdateInBulk
|
|
|
252
266
|
else
|
|
253
267
|
build_simple_assignments(table)
|
|
254
268
|
end
|
|
269
|
+
if @auto_locking_column
|
|
270
|
+
lock = table[@auto_locking_column]
|
|
271
|
+
set_assignments << [lock, table.coalesce(lock, 0) + 1]
|
|
272
|
+
end
|
|
255
273
|
|
|
256
274
|
if timestamp_keys.any?
|
|
257
275
|
# Timestamp assignments precede data assignments to increase the
|
|
@@ -274,9 +292,10 @@ module ActiveRecord::UpdateInBulk
|
|
|
274
292
|
lhs = table[key]
|
|
275
293
|
|
|
276
294
|
if constant_assigns.key?(key)
|
|
277
|
-
rhs = Arel::Nodes::
|
|
295
|
+
rhs = Arel::Nodes::Quoted.new(constant_assigns[key])
|
|
278
296
|
else
|
|
279
|
-
|
|
297
|
+
val = values_table[column]
|
|
298
|
+
rhs = val
|
|
280
299
|
column += 1
|
|
281
300
|
rhs = self.class.apply_formula(formula, lhs, rhs, model) if formula
|
|
282
301
|
end
|
|
@@ -284,7 +303,11 @@ module ActiveRecord::UpdateInBulk
|
|
|
284
303
|
if function = bitmask_functions[key]
|
|
285
304
|
rhs = Arel::Nodes::Case.new(function).when("1").then(rhs).else(lhs)
|
|
286
305
|
elsif optional_keys.include?(key)
|
|
287
|
-
|
|
306
|
+
if formula
|
|
307
|
+
rhs = Arel::Nodes::Case.new.when(val.eq(nil)).then(lhs).else(rhs)
|
|
308
|
+
else
|
|
309
|
+
rhs = table.coalesce(rhs, lhs)
|
|
310
|
+
end
|
|
288
311
|
end
|
|
289
312
|
[lhs, rhs]
|
|
290
313
|
end
|
|
@@ -327,15 +350,11 @@ module ActiveRecord::UpdateInBulk
|
|
|
327
350
|
# When you assign a value to NULL, we need to use a bitmask to distinguish that
|
|
328
351
|
# row in the values table from rows where the column is not to be assigned at all.
|
|
329
352
|
def might_be_nil_value?(value)
|
|
330
|
-
value.nil? ||
|
|
353
|
+
value.nil? || Arel.arel_node?(value)
|
|
331
354
|
end
|
|
332
355
|
|
|
333
|
-
def
|
|
334
|
-
Arel.arel_node?(value)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def cast_for_column(value, column)
|
|
338
|
-
opaque_value?(value) ? value : Arel::Nodes::Casted.new(value, column)
|
|
356
|
+
def quoted_value(value)
|
|
357
|
+
Arel.arel_node?(value) ? value : Arel::Nodes::Quoted.new(value)
|
|
339
358
|
end
|
|
340
359
|
|
|
341
360
|
def normalize_formulas(formulas)
|
|
@@ -344,13 +363,22 @@ module ActiveRecord::UpdateInBulk
|
|
|
344
363
|
normalized = formulas.to_h do |key, value|
|
|
345
364
|
[key.to_s, value.is_a?(Proc) ? value : value.to_sym]
|
|
346
365
|
end
|
|
347
|
-
invalid = normalized.values.reject { |v| v.is_a?(Proc)
|
|
366
|
+
invalid = normalized.values.reject { |v| v.is_a?(Proc) || self.class.registered_formula?(v) }
|
|
348
367
|
if invalid.any?
|
|
349
368
|
raise ArgumentError, "Unknown formula: #{invalid.first.inspect}"
|
|
350
369
|
end
|
|
351
370
|
normalized
|
|
352
371
|
end
|
|
353
372
|
|
|
373
|
+
def apply_optimistic_locking!
|
|
374
|
+
return unless model.locking_enabled?
|
|
375
|
+
|
|
376
|
+
locking_column = model.locking_column
|
|
377
|
+
return if write_keys.include?(locking_column)
|
|
378
|
+
|
|
379
|
+
@auto_locking_column = locking_column
|
|
380
|
+
end
|
|
381
|
+
|
|
354
382
|
def resolve_attribute_aliases!
|
|
355
383
|
return if model.attribute_aliases.empty?
|
|
356
384
|
|
|
@@ -29,21 +29,25 @@ module ActiveRecord
|
|
|
29
29
|
config.active_record_update_in_bulk = ActiveSupport::OrderedOptions.new
|
|
30
30
|
config.active_record_update_in_bulk.ignore_scope_order = true
|
|
31
31
|
|
|
32
|
-
initializer "active_record_update_in_bulk.values_table_alias"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
initializer "active_record_update_in_bulk.values_table_alias" do |app|
|
|
33
|
+
ActiveSupport.on_load(:active_record) do
|
|
34
|
+
if (bulk_alias = app.config.active_record_update_in_bulk.values_table_alias)
|
|
35
|
+
unless bulk_alias.instance_of?(String) && !bulk_alias.empty?
|
|
36
|
+
raise ArgumentError, "values_table_alias must be a non-empty String"
|
|
37
|
+
end
|
|
38
|
+
ActiveRecord::UpdateInBulk::Builder.values_table_name = bulk_alias
|
|
36
39
|
end
|
|
37
|
-
ActiveRecord::UpdateInBulk::Builder.values_table_name = bulk_alias
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
initializer "active_record_update_in_bulk.ignore_scope_order"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
initializer "active_record_update_in_bulk.ignore_scope_order" do |app|
|
|
44
|
+
ActiveSupport.on_load(:active_record) do
|
|
45
|
+
ignore_scope_order = app.config.active_record_update_in_bulk.ignore_scope_order
|
|
46
|
+
unless ignore_scope_order == true || ignore_scope_order == false
|
|
47
|
+
raise ArgumentError, "ignore_scope_order must be true or false"
|
|
48
|
+
end
|
|
49
|
+
ActiveRecord::UpdateInBulk::Builder.ignore_scope_order = ignore_scope_order
|
|
45
50
|
end
|
|
46
|
-
ActiveRecord::UpdateInBulk::Builder.ignore_scope_order = ignore_scope_order
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Carvalho
|
|
@@ -23,20 +23,6 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '8.0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: rake
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
26
|
description: Introduces update_in_bulk(), a method to update many records in a table
|
|
41
27
|
with different values in a single SQL statement.
|
|
42
28
|
email:
|