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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25ba0ab087a02a9c275429d50daa9b31bc2bc93fe4ae40c726402f4fd2f21ec2
4
- data.tar.gz: d32540835f40aaaf11aa0180d9f49763c775ff7b55287dec5cd9db568b20f45f
3
+ metadata.gz: 5b333b357426d210ed69dfa18de2f7f77ae8088e9fc47819198118f22f7647fc
4
+ data.tar.gz: 96eb8a466eb413735de1e1ed4e80dbbc18402b356b5870d9b24b92826828b425
5
5
  SHA512:
6
- metadata.gz: 2dee9dd00a22a4bebf1d692d926ce2dfafaf2730b3163ee384dda5038616fbef79812caba778280d7ed36523e5f0433dac5cbfc19667c8d038a79bf10fc6db0e
7
- data.tar.gz: 8273e2806d3dfceca28214e461b0a6a7954d613e2fc9dfeebc4c754e1582121473a87817fdbc14498d534429163f3554f82ba46eb6b10c1a322aedbcffc558ae
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`; when false, ordered relations raise `NotImplementedError`.
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
- formula = formula.to_sym if formula.is_a?(String)
52
- case formula
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
- raise ArgumentError, "Unknown formula: #{formula.inspect}"
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
- formula.call(lhs, rhs)
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
- unless simple_update?
130
- detect_constant_columns!
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(cast_for_column(row_conditions.fetch(key), table[key]))
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], cast_for_column(row_assigns.fetch(key), 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| !opaque_value?(v = a[key]) && v == first }
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 opaque_value?(value)
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 opaque_value?(value) || constant_assigns.key?(key)
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 - constant_assigns.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::Casted.new(constant_assigns[key], lhs)
295
+ rhs = Arel::Nodes::Quoted.new(constant_assigns[key])
278
296
  else
279
- rhs = values_table[column]
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
- rhs = table.coalesce(rhs, lhs)
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? || opaque_value?(value)
353
+ value.nil? || Arel.arel_node?(value)
331
354
  end
332
355
 
333
- def opaque_value?(value)
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) } - FORMULAS
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", after: :load_config_initializers do |app|
33
- if (bulk_alias = app.config.active_record_update_in_bulk.values_table_alias)
34
- unless bulk_alias.instance_of?(String) && !bulk_alias.empty?
35
- raise ArgumentError, "values_table_alias must be a non-empty String"
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", after: :load_config_initializers do |app|
42
- ignore_scope_order = app.config.active_record_update_in_bulk.ignore_scope_order
43
- unless ignore_scope_order == true || ignore_scope_order == false
44
- raise ArgumentError, "ignore_scope_order must be true or false"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module UpdateInBulk
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
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.2.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: