activerecord-updateinbulk 0.1.2 → 0.2.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 +24 -8
- data/lib/activerecord-updateinbulk/adapters/abstract_adapter.rb +8 -0
- data/lib/activerecord-updateinbulk/arel/select_manager.rb +2 -2
- data/lib/activerecord-updateinbulk/arel/visitors/to_sql.rb +1 -13
- data/lib/activerecord-updateinbulk/base.rb +4 -8
- data/lib/activerecord-updateinbulk/builder.rb +140 -106
- data/lib/activerecord-updateinbulk/querying.rb +2 -2
- data/lib/activerecord-updateinbulk/railtie.rb +30 -0
- data/lib/activerecord-updateinbulk/relation.rb +20 -8
- data/lib/activerecord-updateinbulk/version.rb +1 -1
- metadata +1 -5
- data/lib/activerecord-updateinbulk/arel/math.rb +0 -17
- data/lib/activerecord-updateinbulk/arel/nodes/greatest.rb +0 -6
- data/lib/activerecord-updateinbulk/arel/nodes/least.rb +0 -6
- data/lib/activerecord-updateinbulk/arel/visitors/sqlite.rb +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25ba0ab087a02a9c275429d50daa9b31bc2bc93fe4ae40c726402f4fd2f21ec2
|
|
4
|
+
data.tar.gz: d32540835f40aaaf11aa0180d9f49763c775ff7b55287dec5cd9db568b20f45f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2dee9dd00a22a4bebf1d692d926ce2dfafaf2730b3163ee384dda5038616fbef79812caba778280d7ed36523e5f0433dac5cbfc19667c8d038a79bf10fc6db0e
|
|
7
|
+
data.tar.gz: 8273e2806d3dfceca28214e461b0a6a7954d613e2fc9dfeebc4c754e1582121473a87817fdbc14498d534429163f3554f82ba46eb6b10c1a322aedbcffc558ae
|
data/README.md
CHANGED
|
@@ -59,11 +59,26 @@ Order.joins(:items).where(items: { status: :shipped }).update_in_bulk({
|
|
|
59
59
|
})
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
### Rails configuration
|
|
63
|
+
|
|
64
|
+
Railtie options are available at `config.active_record_update_in_bulk`:
|
|
65
|
+
|
|
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
|
+
```
|
|
75
|
+
|
|
62
76
|
### Record timestamps
|
|
63
77
|
|
|
64
78
|
By default `update_in_bulk` implicitly bumps update timestamps similar to `upsert_all`.
|
|
65
79
|
- If the model has `updated_at`/`updated_on`, these are bumped *iff the row actually changed*.
|
|
66
80
|
- Passing `record_timestamps: false` can disable bumping the update timestamps for the query.
|
|
81
|
+
- Passing `record_timestamps: :always` always sets update timestamps to `CURRENT_TIMESTAMP`.
|
|
67
82
|
- The `updated_at` columns can also be manually assigned, this disables the implicit bump behaviour.
|
|
68
83
|
|
|
69
84
|
```ruby
|
|
@@ -91,21 +106,20 @@ Inventory.update_in_bulk({
|
|
|
91
106
|
```
|
|
92
107
|
|
|
93
108
|
Built-in formulas:
|
|
94
|
-
- `:add :subtract :
|
|
109
|
+
- `:add :subtract :concat_append :concat_prepend`
|
|
95
110
|
|
|
96
111
|
Custom formulas are supported by providing a `Proc`. The proc takes `(lhs,rhs,model)` and must return an **Arel node**.
|
|
97
112
|
Here `lhs` and `rhs` are instances of `Arel::Attribute` corresponding to the target table and values table respectively.
|
|
98
113
|
|
|
99
114
|
```ruby
|
|
100
|
-
# Restock some products
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Arel::Nodes::Least.new([Arel::Attribute.new("metadata", "max_stock"), lhs + rhs])
|
|
115
|
+
# Restock some products with a custom expression.
|
|
116
|
+
add_tax = proc do |lhs, rhs|
|
|
117
|
+
lhs + rhs + 1
|
|
104
118
|
end
|
|
105
|
-
Inventory.
|
|
119
|
+
Inventory.update_in_bulk({
|
|
106
120
|
"Christmas balls" => { quantity: 300 },
|
|
107
121
|
"Christmas tree" => { quantity: 10 }
|
|
108
|
-
}, formulas: { quantity:
|
|
122
|
+
}, formulas: { quantity: add_tax })
|
|
109
123
|
```
|
|
110
124
|
|
|
111
125
|
## Notes
|
|
@@ -124,7 +138,9 @@ The `UPDATE` is single-shot in any compliant database:
|
|
|
124
138
|
|
|
125
139
|
## Limitations
|
|
126
140
|
|
|
127
|
-
|
|
141
|
+
By default `ORDER BY` scopes are ignored for `update_in_bulk` (configurable via railtie setting
|
|
142
|
+
`config.active_record_update_in_bulk.ignore_scope_order = true/false`).
|
|
143
|
+
There is no support for `LIMIT`, `OFFSET`, `GROUP` or `HAVING` clauses in the relation.
|
|
128
144
|
|
|
129
145
|
The implementation does not automatically batch (nor reject) impermissibly large queries. The size of the values table is `rows * columns` when all rows assign to the same columns, or `rows * (distinct_columns + 1)` when the assign columns are not uniform (an extra bitmask indicator column is used).
|
|
130
146
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActiveRecord::UpdateInBulk
|
|
4
|
+
# Extension points mixed into +ActiveRecord::ConnectionAdapters::AbstractAdapter+
|
|
5
|
+
# and consumed by the builder/visitor pipeline.
|
|
6
|
+
#
|
|
7
|
+
# Concrete adapters may override any of these methods to describe their SQL
|
|
8
|
+
# capabilities and VALUES table semantics.
|
|
4
9
|
module AbstractAdapter
|
|
5
10
|
# Whether the database supports the SQL VALUES table constructor.
|
|
11
|
+
# When false, +Relation#update_in_bulk+ raises ArgumentError.
|
|
6
12
|
def supports_values_tables?
|
|
7
13
|
true
|
|
8
14
|
end
|
|
@@ -36,6 +42,8 @@ module ActiveRecord::UpdateInBulk
|
|
|
36
42
|
#
|
|
37
43
|
# Returns the typecasted Arel node: a new node or +values_table+ itself,
|
|
38
44
|
# possibly modified in place.
|
|
45
|
+
#
|
|
46
|
+
# The default implementation does no explicit type casting.
|
|
39
47
|
def typecast_values_table(values_table, _columns)
|
|
40
48
|
values_table
|
|
41
49
|
end
|
|
@@ -2,18 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveRecord::UpdateInBulk
|
|
4
4
|
module ToSql
|
|
5
|
-
def visit_Arel_Nodes_Least(o, collector)
|
|
6
|
-
collector << "LEAST("
|
|
7
|
-
inject_join(o.expressions, collector, ", ")
|
|
8
|
-
collector << ")"
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def visit_Arel_Nodes_Greatest(o, collector)
|
|
12
|
-
collector << "GREATEST("
|
|
13
|
-
inject_join(o.expressions, collector, ", ")
|
|
14
|
-
collector << ")"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
5
|
def visit_Arel_Nodes_ValuesTable(o, collector)
|
|
18
6
|
row_prefix = @connection.values_table_row_prefix
|
|
19
7
|
|
|
@@ -40,7 +28,7 @@ module ActiveRecord::UpdateInBulk
|
|
|
40
28
|
private
|
|
41
29
|
def build_values_table_single_value(value, collector)
|
|
42
30
|
case value
|
|
43
|
-
when Arel::Nodes::SqlLiteral, Arel::Nodes::BindParam, ActiveModel::Attribute
|
|
31
|
+
when Arel::Nodes::SqlLiteral, Arel::Nodes::BoundSqlLiteral, Arel::Nodes::BindParam, ActiveModel::Attribute
|
|
44
32
|
visit(value, collector)
|
|
45
33
|
else
|
|
46
34
|
collector << quote(value).to_s
|
|
@@ -10,24 +10,20 @@ module ActiveRecord
|
|
|
10
10
|
"trilogy" => "abstract_mysql",
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
def self.require_adapter(adapter)
|
|
13
|
+
def self.require_adapter(adapter) # :nodoc:
|
|
14
14
|
adapter = ADAPTER_EXTENSION_MAP.fetch(adapter, adapter)
|
|
15
15
|
require File.join(ADAPTER_PATH, "#{adapter}_adapter")
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def self.load_from_connection_pool(connection_pool)
|
|
18
|
+
def self.load_from_connection_pool(connection_pool) # :nodoc:
|
|
19
19
|
require_adapter connection_pool.db_config.adapter
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
require "activerecord-updateinbulk/builder"
|
|
25
|
-
require "activerecord-updateinbulk/arel/math"
|
|
26
|
-
require "activerecord-updateinbulk/arel/nodes/least"
|
|
27
|
-
require "activerecord-updateinbulk/arel/nodes/greatest"
|
|
28
25
|
require "activerecord-updateinbulk/arel/nodes/values_table"
|
|
29
26
|
require "activerecord-updateinbulk/arel/visitors/to_sql"
|
|
30
|
-
require "activerecord-updateinbulk/arel/visitors/sqlite"
|
|
31
27
|
require "activerecord-updateinbulk/arel/select_manager"
|
|
32
28
|
require "activerecord-updateinbulk/relation"
|
|
33
29
|
require "activerecord-updateinbulk/querying"
|
|
@@ -35,8 +31,8 @@ require "activerecord-updateinbulk/querying"
|
|
|
35
31
|
require "activerecord-updateinbulk/adapters/abstract_adapter"
|
|
36
32
|
|
|
37
33
|
module ActiveRecord::UpdateInBulk
|
|
38
|
-
module ConnectionHandler
|
|
39
|
-
def establish_connection(*args, **kwargs, &block)
|
|
34
|
+
module ConnectionHandler # :nodoc:
|
|
35
|
+
def establish_connection(*args, **kwargs, &block) # :nodoc:
|
|
40
36
|
pool = super(*args, **kwargs, &block)
|
|
41
37
|
ActiveRecord::UpdateInBulk.load_from_connection_pool pool
|
|
42
38
|
pool
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
require "active_support/core_ext/enumerable"
|
|
4
4
|
|
|
5
5
|
module ActiveRecord::UpdateInBulk
|
|
6
|
-
class Builder
|
|
7
|
-
FORMULAS =
|
|
6
|
+
class Builder # :nodoc:
|
|
7
|
+
FORMULAS = [:add, :subtract, :concat_append, :concat_prepend].freeze
|
|
8
|
+
SAFE_COMPARISON_TYPES = [:boolean, :string, :text, :integer, :float, :decimal].freeze
|
|
8
9
|
|
|
9
10
|
class << self
|
|
10
11
|
attr_accessor :values_table_name
|
|
12
|
+
attr_accessor :ignore_scope_order
|
|
11
13
|
|
|
12
14
|
# Normalize all input formats into separated format [conditions, assigns].
|
|
13
15
|
def normalize_updates(model, updates, values = nil)
|
|
@@ -46,19 +48,16 @@ module ActiveRecord::UpdateInBulk
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def apply_formula(formula, lhs, rhs, model)
|
|
51
|
+
formula = formula.to_sym if formula.is_a?(String)
|
|
49
52
|
case formula
|
|
50
|
-
when
|
|
53
|
+
when :add
|
|
51
54
|
lhs + rhs
|
|
52
|
-
when
|
|
55
|
+
when :subtract
|
|
53
56
|
lhs - rhs
|
|
54
|
-
when
|
|
57
|
+
when :concat_append
|
|
55
58
|
lhs.concat(rhs)
|
|
56
|
-
when
|
|
59
|
+
when :concat_prepend
|
|
57
60
|
rhs.concat(lhs)
|
|
58
|
-
when "min"
|
|
59
|
-
lhs.least(rhs)
|
|
60
|
-
when "max"
|
|
61
|
-
lhs.greatest(rhs)
|
|
62
61
|
when Proc
|
|
63
62
|
node = apply_proc_formula(formula, lhs, rhs, model)
|
|
64
63
|
unless Arel.arel_node?(node)
|
|
@@ -66,7 +65,7 @@ module ActiveRecord::UpdateInBulk
|
|
|
66
65
|
end
|
|
67
66
|
node
|
|
68
67
|
else
|
|
69
|
-
|
|
68
|
+
raise ArgumentError, "Unknown formula: #{formula.inspect}"
|
|
70
69
|
end
|
|
71
70
|
end
|
|
72
71
|
|
|
@@ -113,6 +112,7 @@ module ActiveRecord::UpdateInBulk
|
|
|
113
112
|
end
|
|
114
113
|
end
|
|
115
114
|
self.values_table_name = "t"
|
|
115
|
+
self.ignore_scope_order = true
|
|
116
116
|
|
|
117
117
|
attr_reader :model, :connection
|
|
118
118
|
|
|
@@ -126,97 +126,155 @@ module ActiveRecord::UpdateInBulk
|
|
|
126
126
|
resolve_attribute_aliases!
|
|
127
127
|
resolve_read_and_write_keys!
|
|
128
128
|
verify_read_and_write_keys!
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
unless simple_update?
|
|
130
|
+
detect_constant_columns!
|
|
131
|
+
serialize_values!
|
|
132
|
+
end
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
def build_arel
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
table = model.arel_table
|
|
137
|
+
values_table, bitmask_keys = build_values_table unless simple_update?
|
|
138
|
+
conditions = build_conditions(table, values_table)
|
|
139
|
+
set_assignments = build_set_assignments(table, values_table, bitmask_keys || Set.new)
|
|
140
|
+
derived_table = typecast_values_table(values_table) if values_table
|
|
138
141
|
|
|
139
|
-
[derived_table,
|
|
142
|
+
[derived_table, conditions, set_assignments]
|
|
140
143
|
end
|
|
141
144
|
|
|
142
145
|
private
|
|
143
|
-
attr_reader :read_keys, :write_keys, :
|
|
146
|
+
attr_reader :read_keys, :write_keys, :constant_assigns
|
|
147
|
+
|
|
148
|
+
def optional_keys
|
|
149
|
+
@optional_keys ||= write_keys - @assigns.map(&:keys).reduce(write_keys, &:intersection)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def timestamp_keys
|
|
153
|
+
@timestamp_keys ||= record_timestamps_enabled? ? model.timestamp_attributes_for_update_in_model.to_set - write_keys : Set.new
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def simple_update?
|
|
157
|
+
@conditions.size == 1 && @formulas.empty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def build_simple_conditions(table)
|
|
161
|
+
row_conditions = @conditions.first
|
|
162
|
+
read_keys.map do |key|
|
|
163
|
+
table[key].eq(cast_for_column(row_conditions.fetch(key), table[key]))
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def build_simple_assignments(table)
|
|
168
|
+
row_assigns = @assigns.first
|
|
169
|
+
write_keys.map do |key|
|
|
170
|
+
[table[key], cast_for_column(row_assigns.fetch(key), table[key])]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def detect_constant_columns!
|
|
175
|
+
@constant_assigns = {}
|
|
176
|
+
columns_hash = model.columns_hash
|
|
177
|
+
|
|
178
|
+
(write_keys - optional_keys).each do |key|
|
|
179
|
+
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
|
+
first = @assigns.first[key]
|
|
182
|
+
@constant_assigns[key] = first if @assigns.all? { |a| !opaque_value?(v = a[key]) && v == first }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
144
185
|
|
|
145
186
|
def serialize_values!
|
|
146
|
-
types =
|
|
187
|
+
types = read_keys.index_with { |key| model.type_for_attribute(key) }
|
|
147
188
|
@conditions.each do |row|
|
|
148
189
|
row.each do |key, value|
|
|
149
190
|
next if opaque_value?(value)
|
|
150
191
|
row[key] = ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
|
|
151
192
|
end
|
|
152
193
|
end
|
|
194
|
+
types = write_keys.index_with { |key| model.type_for_attribute(key) }
|
|
153
195
|
@assigns.each do |row|
|
|
154
196
|
row.each do |key, value|
|
|
155
|
-
next if opaque_value?(value)
|
|
197
|
+
next if opaque_value?(value) || constant_assigns.key?(key)
|
|
156
198
|
row[key] = ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
|
|
157
199
|
end
|
|
158
200
|
end
|
|
159
201
|
end
|
|
160
202
|
|
|
161
|
-
def
|
|
162
|
-
|
|
163
|
-
|
|
203
|
+
def build_values_table
|
|
204
|
+
rows, bitmask_keys = build_values_table_rows
|
|
205
|
+
append_bitmask_column(rows, bitmask_keys) unless bitmask_keys.empty?
|
|
206
|
+
column_names = connection.values_table_default_column_names(rows.first.size)
|
|
207
|
+
values_table = Arel::Nodes::ValuesTable.new(self.class.values_table_name, rows, column_names)
|
|
208
|
+
[values_table, bitmask_keys]
|
|
209
|
+
end
|
|
164
210
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
211
|
+
def build_values_table_rows
|
|
212
|
+
bitmask_keys = Set.new
|
|
213
|
+
non_constant_write_keys = write_keys - constant_assigns.keys
|
|
171
214
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
215
|
+
rows = @conditions.map.with_index do |row_conditions, row_index|
|
|
216
|
+
row_assigns = @assigns[row_index]
|
|
217
|
+
row = row_conditions.fetch_values(*read_keys)
|
|
218
|
+
non_constant_write_keys.each do |key|
|
|
219
|
+
next row << nil unless row_assigns.key?(key)
|
|
220
|
+
value = row_assigns[key]
|
|
221
|
+
bitmask_keys.add(key) if optional_keys.include?(key) && might_be_nil_value?(value)
|
|
222
|
+
row << value
|
|
223
|
+
end
|
|
224
|
+
row
|
|
176
225
|
end
|
|
177
|
-
end
|
|
178
226
|
|
|
179
|
-
|
|
180
|
-
@optional_keys ||= write_keys - @assigns.map(&:keys).reduce(write_keys, &:intersection)
|
|
227
|
+
[rows, bitmask_keys]
|
|
181
228
|
end
|
|
182
229
|
|
|
183
|
-
def
|
|
184
|
-
|
|
230
|
+
def append_bitmask_column(rows, bitmask_keys)
|
|
231
|
+
rows.each_with_index do |row, row_index|
|
|
232
|
+
row_assigns = @assigns[row_index]
|
|
233
|
+
bitmask = "0" * bitmask_keys.size
|
|
234
|
+
bitmask_keys.each_with_index do |key, index|
|
|
235
|
+
bitmask[index] = "1" if row_assigns.key?(key)
|
|
236
|
+
end
|
|
237
|
+
row.push(bitmask)
|
|
238
|
+
end
|
|
185
239
|
end
|
|
186
240
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
append_bitmask_column(rows, bitmask_keys) unless bitmask_keys.empty?
|
|
190
|
-
values_table = Arel::Nodes::ValuesTable.new(self.class.values_table_name, rows, connection.values_table_default_column_names(rows.first.size))
|
|
191
|
-
[values_table, bitmask_keys]
|
|
192
|
-
end
|
|
241
|
+
def build_conditions(table, values_table)
|
|
242
|
+
return build_simple_conditions(table) unless values_table
|
|
193
243
|
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
244
|
+
read_keys.map.with_index do |key, index|
|
|
245
|
+
table[key].eq(values_table[index])
|
|
204
246
|
end
|
|
205
247
|
end
|
|
206
248
|
|
|
207
249
|
def build_set_assignments(table, values_table, bitmask_keys)
|
|
208
|
-
|
|
250
|
+
set_assignments = if values_table
|
|
251
|
+
build_join_assignments(table, values_table, bitmask_keys)
|
|
252
|
+
else
|
|
253
|
+
build_simple_assignments(table)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
if timestamp_keys.any?
|
|
257
|
+
# Timestamp assignments precede data assignments to increase the
|
|
258
|
+
# chance MySQL will actually run them against the original data.
|
|
259
|
+
set_assignments = timestamp_assignments(set_assignments) + set_assignments
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
set_assignments
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def build_join_assignments(table, values_table, bitmask_keys)
|
|
266
|
+
column = read_keys.size
|
|
209
267
|
|
|
210
268
|
bitmask_functions = bitmask_keys.index_with.with_index(1) do |key, index|
|
|
211
269
|
Arel::Nodes::NamedFunction.new("SUBSTRING", [values_table[-1], index, 1])
|
|
212
270
|
end
|
|
213
271
|
|
|
214
|
-
|
|
272
|
+
write_keys.map do |key|
|
|
215
273
|
formula = @formulas[key]
|
|
216
274
|
lhs = table[key]
|
|
217
275
|
|
|
218
276
|
if constant_assigns.key?(key)
|
|
219
|
-
rhs = Arel::Nodes::Casted.new(constant_assigns[key],
|
|
277
|
+
rhs = Arel::Nodes::Casted.new(constant_assigns[key], lhs)
|
|
220
278
|
else
|
|
221
279
|
rhs = values_table[column]
|
|
222
280
|
column += 1
|
|
@@ -224,64 +282,28 @@ module ActiveRecord::UpdateInBulk
|
|
|
224
282
|
end
|
|
225
283
|
|
|
226
284
|
if function = bitmask_functions[key]
|
|
227
|
-
rhs = Arel::Nodes::Case.new(function).when("1").then(rhs).else(
|
|
285
|
+
rhs = Arel::Nodes::Case.new(function).when("1").then(rhs).else(lhs)
|
|
228
286
|
elsif optional_keys.include?(key)
|
|
229
|
-
rhs = table.coalesce(rhs,
|
|
287
|
+
rhs = table.coalesce(rhs, lhs)
|
|
230
288
|
end
|
|
231
|
-
[
|
|
289
|
+
[lhs, rhs]
|
|
232
290
|
end
|
|
233
|
-
|
|
234
|
-
if timestamp_keys.any?
|
|
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
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
set_assignments
|
|
241
291
|
end
|
|
242
292
|
|
|
243
293
|
def typecast_values_table(values_table)
|
|
244
|
-
variable_keys = read_keys
|
|
294
|
+
variable_keys = read_keys + write_keys.reject { |k| constant_assigns.key?(k) }
|
|
245
295
|
columns_hash = model.columns_hash
|
|
246
296
|
model_types = variable_keys.map! { |key| columns_hash.fetch(key) }
|
|
247
297
|
connection.typecast_values_table(values_table, model_types).alias(self.class.values_table_name)
|
|
248
298
|
end
|
|
249
299
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
row_assigns = @assigns[row_index]
|
|
255
|
-
row = []
|
|
256
|
-
read_keys.each do |key|
|
|
257
|
-
next if constant_conditions.key?(key)
|
|
258
|
-
row << row_conditions[key]
|
|
259
|
-
end
|
|
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]
|
|
264
|
-
bitmask_keys.add(key) if optional_keys.include?(key) && might_be_nil_value?(value)
|
|
265
|
-
row << value
|
|
266
|
-
end
|
|
267
|
-
row
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
[rows, bitmask_keys]
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def append_bitmask_column(rows, bitmask_keys)
|
|
274
|
-
rows.each_with_index do |row, row_index|
|
|
275
|
-
row_assigns = @assigns[row_index]
|
|
276
|
-
bitmask = "0" * bitmask_keys.size
|
|
277
|
-
bitmask_keys.each_with_index do |key, index|
|
|
278
|
-
bitmask[index] = "1" if row_assigns.key?(key)
|
|
300
|
+
def timestamp_assignments(set_assignments)
|
|
301
|
+
if always_record_timestamps?
|
|
302
|
+
return timestamp_keys.map do |key|
|
|
303
|
+
[model.arel_table[key], connection.high_precision_current_timestamp]
|
|
279
304
|
end
|
|
280
|
-
row.push(bitmask)
|
|
281
305
|
end
|
|
282
|
-
end
|
|
283
306
|
|
|
284
|
-
def timestamp_assignments(set_assignments)
|
|
285
307
|
case_conditions = set_assignments.map do |left, right|
|
|
286
308
|
left.is_not_distinct_from(right)
|
|
287
309
|
end
|
|
@@ -294,21 +316,33 @@ module ActiveRecord::UpdateInBulk
|
|
|
294
316
|
end
|
|
295
317
|
end
|
|
296
318
|
|
|
297
|
-
|
|
319
|
+
def record_timestamps_enabled?
|
|
320
|
+
!@record_timestamps.nil? && @record_timestamps != false
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def always_record_timestamps?
|
|
324
|
+
@record_timestamps == :always
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# When you assign a value to NULL, we need to use a bitmask to distinguish that
|
|
298
328
|
# row in the values table from rows where the column is not to be assigned at all.
|
|
299
329
|
def might_be_nil_value?(value)
|
|
300
330
|
value.nil? || opaque_value?(value)
|
|
301
331
|
end
|
|
302
332
|
|
|
303
333
|
def opaque_value?(value)
|
|
304
|
-
|
|
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)
|
|
305
339
|
end
|
|
306
340
|
|
|
307
341
|
def normalize_formulas(formulas)
|
|
308
342
|
return {} if formulas.blank?
|
|
309
343
|
|
|
310
344
|
normalized = formulas.to_h do |key, value|
|
|
311
|
-
[key.to_s, value.is_a?(Proc) ? value : value.
|
|
345
|
+
[key.to_s, value.is_a?(Proc) ? value : value.to_sym]
|
|
312
346
|
end
|
|
313
347
|
invalid = normalized.values.reject { |v| v.is_a?(Proc) } - FORMULAS
|
|
314
348
|
if invalid.any?
|
|
@@ -329,7 +363,7 @@ module ActiveRecord::UpdateInBulk
|
|
|
329
363
|
|
|
330
364
|
def resolve_read_and_write_keys!
|
|
331
365
|
@read_keys = @conditions.first.keys.to_set
|
|
332
|
-
@write_keys = @assigns.
|
|
366
|
+
@write_keys = @assigns.reduce(Set.new) { |set, row| set.merge(row.keys) }
|
|
333
367
|
end
|
|
334
368
|
|
|
335
369
|
def verify_read_and_write_keys!
|
|
@@ -4,8 +4,30 @@ require "rails/railtie"
|
|
|
4
4
|
|
|
5
5
|
module ActiveRecord
|
|
6
6
|
module UpdateInBulk
|
|
7
|
+
# Railtie integration for configuration values consumed by this gem.
|
|
8
|
+
#
|
|
9
|
+
# ==== Configuration
|
|
10
|
+
#
|
|
11
|
+
# Configure these in a Rails initializer (for example
|
|
12
|
+
# <tt>config/initializers/update_in_bulk.rb</tt>):
|
|
13
|
+
#
|
|
14
|
+
# Rails.application.config.active_record_update_in_bulk.values_table_alias = "vals"
|
|
15
|
+
# Rails.application.config.active_record_update_in_bulk.ignore_scope_order = true
|
|
16
|
+
#
|
|
17
|
+
# [config.active_record_update_in_bulk.values_table_alias]
|
|
18
|
+
# Optional string alias to use for the generated VALUES table.
|
|
19
|
+
# Defaults to <tt>"t"</tt>.
|
|
20
|
+
#
|
|
21
|
+
# [config.active_record_update_in_bulk.ignore_scope_order]
|
|
22
|
+
# Whether <tt>Relation#update_in_bulk</tt> should ignore any ORDER BY scope
|
|
23
|
+
# on the input relation. Necessary for invoking the method on scope-ordered
|
|
24
|
+
# associations, or models with a default scope that includes an order.
|
|
25
|
+
#
|
|
26
|
+
# * <tt>true</tt> (default): ORDER BY scopes are stripped.
|
|
27
|
+
# * <tt>false</tt>: ordered relations raise NotImplementedError.
|
|
7
28
|
class Railtie < Rails::Railtie
|
|
8
29
|
config.active_record_update_in_bulk = ActiveSupport::OrderedOptions.new
|
|
30
|
+
config.active_record_update_in_bulk.ignore_scope_order = true
|
|
9
31
|
|
|
10
32
|
initializer "active_record_update_in_bulk.values_table_alias", after: :load_config_initializers do |app|
|
|
11
33
|
if (bulk_alias = app.config.active_record_update_in_bulk.values_table_alias)
|
|
@@ -15,6 +37,14 @@ module ActiveRecord
|
|
|
15
37
|
ActiveRecord::UpdateInBulk::Builder.values_table_name = bulk_alias
|
|
16
38
|
end
|
|
17
39
|
end
|
|
40
|
+
|
|
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"
|
|
45
|
+
end
|
|
46
|
+
ActiveRecord::UpdateInBulk::Builder.ignore_scope_order = ignore_scope_order
|
|
47
|
+
end
|
|
18
48
|
end
|
|
19
49
|
end
|
|
20
50
|
end
|
|
@@ -51,13 +51,17 @@ module ActiveRecord::UpdateInBulk
|
|
|
51
51
|
# To override this and force automatic setting of timestamp columns one
|
|
52
52
|
# way or the other, pass <tt>:record_timestamps</tt>.
|
|
53
53
|
#
|
|
54
|
+
# Pass <tt>record_timestamps: :always</tt> to always assign timestamp
|
|
55
|
+
# columns to the current database timestamp (without change-detection
|
|
56
|
+
# CASE logic).
|
|
57
|
+
#
|
|
54
58
|
# [:formulas]
|
|
55
59
|
# A hash of column names to formula identifiers or Procs. Instead of
|
|
56
60
|
# a simple assignment, the column is set to an expression that can
|
|
57
61
|
# reference both the current selected row value and the incoming value.
|
|
58
62
|
#
|
|
59
|
-
# Built-in formulas: <tt>:add</tt>, <tt>:subtract</tt>,
|
|
60
|
-
# <tt>:
|
|
63
|
+
# Built-in formulas: <tt>:add</tt>, <tt>:subtract</tt>,
|
|
64
|
+
# <tt>:concat_append</tt>, <tt>:concat_prepend</tt>.
|
|
61
65
|
#
|
|
62
66
|
# Inventory.update_in_bulk({
|
|
63
67
|
# "Christmas balls" => { quantity: 73 },
|
|
@@ -68,8 +72,8 @@ module ActiveRecord::UpdateInBulk
|
|
|
68
72
|
# <tt>(lhs, rhs)</tt> or <tt>(lhs, rhs, model)</tt> and returns an
|
|
69
73
|
# Arel node:
|
|
70
74
|
#
|
|
71
|
-
#
|
|
72
|
-
# Inventory.update_in_bulk(updates, formulas: { quantity:
|
|
75
|
+
# add_tax = ->(lhs, rhs) { lhs + rhs + 1 }
|
|
76
|
+
# Inventory.update_in_bulk(updates, formulas: { quantity: add_tax })
|
|
73
77
|
#
|
|
74
78
|
# ==== Examples
|
|
75
79
|
#
|
|
@@ -87,8 +91,11 @@ module ActiveRecord::UpdateInBulk
|
|
|
87
91
|
# })
|
|
88
92
|
#
|
|
89
93
|
def update_in_bulk(updates, values = nil, record_timestamps: nil, formulas: nil)
|
|
90
|
-
unless limit_value.nil? && offset_value.nil? &&
|
|
91
|
-
raise NotImplementedError, "No support to update
|
|
94
|
+
unless limit_value.nil? && offset_value.nil? && group_values.empty? && having_clause.empty?
|
|
95
|
+
raise NotImplementedError, "No support to update relations with offset, limit, group, or having clauses"
|
|
96
|
+
end
|
|
97
|
+
if order_values.any? && !Builder.ignore_scope_order
|
|
98
|
+
raise NotImplementedError, "No support to update ordered relations (order clause)"
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
conditions, assigns = Builder.normalize_updates(model, updates, values)
|
|
@@ -101,8 +108,9 @@ module ActiveRecord::UpdateInBulk
|
|
|
101
108
|
|
|
102
109
|
arel = eager_loading? ? apply_join_dependency.arel : arel()
|
|
103
110
|
arel.source.left = table
|
|
111
|
+
arel.ast.orders = [] if Builder.ignore_scope_order
|
|
104
112
|
|
|
105
|
-
values_table,
|
|
113
|
+
values_table, conditions, set_assignments = Builder.new(
|
|
106
114
|
self,
|
|
107
115
|
c,
|
|
108
116
|
conditions,
|
|
@@ -110,7 +118,11 @@ module ActiveRecord::UpdateInBulk
|
|
|
110
118
|
record_timestamps:,
|
|
111
119
|
formulas:
|
|
112
120
|
).build_arel
|
|
113
|
-
|
|
121
|
+
if values_table
|
|
122
|
+
arel = arel.join(values_table).on(*conditions)
|
|
123
|
+
else
|
|
124
|
+
conditions.each { |condition| arel.where(condition) }
|
|
125
|
+
end
|
|
114
126
|
|
|
115
127
|
key = if model.composite_primary_key?
|
|
116
128
|
primary_key.map { |pk| table[pk] }
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Carvalho
|
|
@@ -52,12 +52,8 @@ files:
|
|
|
52
52
|
- lib/activerecord-updateinbulk/adapters/abstract_mysql_adapter.rb
|
|
53
53
|
- lib/activerecord-updateinbulk/adapters/postgresql_adapter.rb
|
|
54
54
|
- lib/activerecord-updateinbulk/adapters/sqlite3_adapter.rb
|
|
55
|
-
- lib/activerecord-updateinbulk/arel/math.rb
|
|
56
|
-
- lib/activerecord-updateinbulk/arel/nodes/greatest.rb
|
|
57
|
-
- lib/activerecord-updateinbulk/arel/nodes/least.rb
|
|
58
55
|
- lib/activerecord-updateinbulk/arel/nodes/values_table.rb
|
|
59
56
|
- lib/activerecord-updateinbulk/arel/select_manager.rb
|
|
60
|
-
- lib/activerecord-updateinbulk/arel/visitors/sqlite.rb
|
|
61
57
|
- lib/activerecord-updateinbulk/arel/visitors/to_sql.rb
|
|
62
58
|
- lib/activerecord-updateinbulk/base.rb
|
|
63
59
|
- lib/activerecord-updateinbulk/builder.rb
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Arel
|
|
4
|
-
module Math
|
|
5
|
-
def least(other)
|
|
6
|
-
lhs = is_a?(Arel::Nodes::Least) ? self.expressions : [self]
|
|
7
|
-
rhs = other.is_a?(Arel::Nodes::Least) ? other.expressions : [other]
|
|
8
|
-
Arel::Nodes::Least.new(lhs + rhs)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def greatest(other)
|
|
12
|
-
lhs = is_a?(Arel::Nodes::Greatest) ? self.expressions : [self]
|
|
13
|
-
rhs = other.is_a?(Arel::Nodes::Greatest) ? other.expressions : [other]
|
|
14
|
-
Arel::Nodes::Greatest.new(lhs + rhs)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveRecord::UpdateInBulk
|
|
4
|
-
module SQLiteToSql
|
|
5
|
-
private
|
|
6
|
-
def visit_Arel_Nodes_Least(o, collector)
|
|
7
|
-
collector << "MIN("
|
|
8
|
-
inject_join(o.expressions, collector, ", ")
|
|
9
|
-
collector << ")"
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def visit_Arel_Nodes_Greatest(o, collector)
|
|
13
|
-
collector << "MAX("
|
|
14
|
-
inject_join(o.expressions, collector, ", ")
|
|
15
|
-
collector << ")"
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
Arel::Visitors::SQLite.prepend(ActiveRecord::UpdateInBulk::SQLiteToSql)
|