junk_drawer 2.0.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10b296732de43157b6cdde42030553a98da2223a7c31d035e4a89b82db768df2
4
- data.tar.gz: febc4381da50ed66564deecd40adf47444a8d7e7ccee07f212b8ed649a14ebaf
3
+ metadata.gz: af0c9f7f4aa7cd06e4d7f6aafb47263111d60d8039f2da8abc3a479d572a7d2a
4
+ data.tar.gz: a3b3420460631eb69b70771f4057cf3ed9980a41fb17aeda4a1cd8ec59d6e708
5
5
  SHA512:
6
- metadata.gz: eedb847db634ecd05167f8861991941be34f7d2ddc5cfad041f0cd13c60a8894371b84556ae8d8770e3ebaf63055e5c0a19631ae146480d2baff6f0788aadcf6
7
- data.tar.gz: a217f282c5f5083d4ffeea568431b93680a319564d71d0b17e0861ab37a19fb30b6466c5cdbad3a7f4cbc7351713cc6ba92b1eac3d94fe363d9a3d417967d273
6
+ metadata.gz: a6fc9b47023c4e1ee539191ec8862cbf982b62fb4a4e3f13dc9966d42bb00e20aa560858855deb6735bec907bcf61eefbbe7de35865451d0cd8724ad569ee2e6
7
+ data.tar.gz: eca53a28e48bd935474769d0cf5f56123caa2853f4ec8fb83f08b444a7f2d975b1f6aec9a33df058bb81d33a1a31e087e885ccefff03da329d7b03f406eb2ee6
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400`
3
- # on 2023-04-08 01:57:20 UTC using RuboCop version 1.49.0.
3
+ # on 2023-05-16 20:46:21 UTC using RuboCop version 1.49.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -36,7 +36,7 @@ Layout/LineEndStringConcatenationIndentation:
36
36
  # SupportedStyles: aligned, indented, indented_relative_to_receiver
37
37
  Layout/MultilineMethodCallIndentation:
38
38
  Exclude:
39
- - 'spec/junk_drawer/rails/bulk_updatable_spec.rb'
39
+ - 'spec/support/shared_examples/bulk_updatable_model.rb'
40
40
 
41
41
  # Offense count: 14
42
42
  # Configuration parameters: AllowedMethods.
@@ -46,7 +46,7 @@ Lint/ConstantDefinitionInBlock:
46
46
  - 'spec/junk_drawer/callable_spec.rb'
47
47
  - 'spec/junk_drawer/notifier/honeybadger_strategy_spec.rb'
48
48
  - 'spec/junk_drawer/notifier/null_strategy_spec.rb'
49
- - 'spec/junk_drawer/rails/bulk_updatable_spec.rb'
49
+ - 'spec/support/shared_examples/bulk_updatable_model.rb'
50
50
 
51
51
  # Offense count: 1
52
52
  # Configuration parameters: MaximumRangeSize.
@@ -65,6 +65,12 @@ Metrics/CyclomaticComplexity:
65
65
  Exclude:
66
66
  - 'lib/junk_drawer/rails/bulk_updatable.rb'
67
67
 
68
+ # Offense count: 1
69
+ # Configuration parameters: CountComments, Max, CountAsOne.
70
+ Metrics/ModuleLength:
71
+ Exclude:
72
+ - 'lib/junk_drawer/rails/bulk_updatable.rb'
73
+
68
74
  # Offense count: 1
69
75
  # This cop supports safe autocorrection (--autocorrect).
70
76
  # Configuration parameters: EnforcedStyle.
@@ -88,7 +94,7 @@ RSpec/LeakyConstantDeclaration:
88
94
  - 'spec/junk_drawer/callable_spec.rb'
89
95
  - 'spec/junk_drawer/notifier/honeybadger_strategy_spec.rb'
90
96
  - 'spec/junk_drawer/notifier/null_strategy_spec.rb'
91
- - 'spec/junk_drawer/rails/bulk_updatable_spec.rb'
97
+ - 'spec/support/shared_examples/bulk_updatable_model.rb'
92
98
 
93
99
  # Offense count: 1
94
100
  RSpec/StubbedMock:
@@ -102,10 +108,4 @@ RSpec/StubbedMock:
102
108
  Style/HashAsLastArrayItem:
103
109
  Exclude:
104
110
  - 'spec/junk_drawer/notifier/honeybadger_strategy_spec.rb'
105
- - 'spec/junk_drawer/rails/bulk_updatable_spec.rb'
106
-
107
- # Offense count: 2
108
- # This cop supports safe autocorrection (--autocorrect).
109
- Style/RedundantConstantBase:
110
- Exclude:
111
- - 'spec/junk_drawer/rails/bulk_updatable_spec.rb'
111
+ - 'spec/support/shared_examples/bulk_updatable_model.rb'
data/README.md CHANGED
@@ -219,7 +219,8 @@ MyModel.bulk_update([my_model_1, my_model_2])
219
219
  ```
220
220
 
221
221
  This will generate a single SQL query to update both of the records in the
222
- database.
222
+ database. If `prepared_statements` is set to true, `BulkUpdatable` will generate
223
+ queries that use bind parameters.
223
224
 
224
225
  #### Caveats
225
226
 
@@ -255,7 +256,7 @@ In order to run tests against different Rails versions, you can use
255
256
  `BUNDLE_GEMFILE`:
256
257
 
257
258
  ```sh
258
- $ BUNDLE_GEMFILE=gemfiles/rails_5.0.gems rake spec
259
+ $ BUNDLE_GEMFILE=gemfiles/rails_7.0.gems rake spec
259
260
  ```
260
261
 
261
262
  ## Contributing
@@ -3,6 +3,7 @@
3
3
  require 'active_support/all'
4
4
  require 'active_record'
5
5
  require 'active_record/connection_adapters/postgresql_adapter'
6
+ require 'active_record/relation/query_attribute'
6
7
 
7
8
  module JunkDrawer
8
9
  # module to allow bulk updates for `ActiveRecord` models
@@ -11,15 +12,35 @@ module JunkDrawer
11
12
  objects = objects.select(&:changed?)
12
13
  return unless objects.any?
13
14
 
14
- unique_objects = uniquify_and_merge(objects)
15
- changed_attributes = extract_changed_attributes(unique_objects)
16
- query = build_query_for(unique_objects, changed_attributes)
17
- connection.execute(query)
15
+ if connection.prepared_statements
16
+ build_and_exec_prepared_query(objects)
17
+ else
18
+ build_and_exec_unprepared_query(objects)
19
+ end
18
20
  objects.each(&:changes_applied)
19
21
  end
20
22
 
21
23
  private
22
24
 
25
+ def build_and_exec_prepared_query(objects)
26
+ unique_objects = uniquify_and_merge(objects)
27
+ changed_attributes = extract_changed_attributes(unique_objects)
28
+ attributes = ['id'] + changed_attributes
29
+
30
+ unique_objects.each_slice(batch_size(changed_attributes)) do |batch|
31
+ query = build_prepared_query_for(batch, attributes, changed_attributes)
32
+ values = values_for_objects(batch, attributes)
33
+ connection.exec_query(query, "#{name} Bulk Update", values, prepare: true)
34
+ end
35
+ end
36
+
37
+ def build_and_exec_unprepared_query(objects)
38
+ unique_objects = uniquify_and_merge(objects)
39
+ changed_attributes = extract_changed_attributes(unique_objects)
40
+ query = build_unprepared_query_for(unique_objects, changed_attributes)
41
+ connection.execute(query)
42
+ end
43
+
23
44
  def uniquify_and_merge(objects)
24
45
  grouped_objects = objects.group_by(&:id).values
25
46
  grouped_objects.each do |group|
@@ -40,19 +61,20 @@ module JunkDrawer
40
61
  objects.each { |object| object.updated_at = now }
41
62
 
42
63
  changed_attributes = objects.flat_map(&:changed).uniq
43
- if ::ActiveRecord::VERSION::MAJOR >= 5
44
- column_names & changed_attributes
45
- else
46
- # to remove virtual columns from jsonb_accessor 0.3.3
47
- columns.select(&:sql_type).map(&:name) & changed_attributes
48
- end
64
+ column_names & changed_attributes
49
65
  end
50
66
 
51
- def build_query_for(objects, attributes)
52
- object_values = objects.map do |object|
53
- sanitized_values(object, attributes)
54
- end.join(', ')
67
+ def build_unprepared_query_for(objects, attributes)
68
+ object_values = objects.map { |object| sanitized_values(object, attributes) }
69
+ build_query_for(attributes, object_values.join(', '))
70
+ end
71
+
72
+ def build_prepared_query_for(objects, attributes, changed_attributes)
73
+ object_placeholders = build_placeholders(objects, attributes)
74
+ build_query_for(changed_attributes, object_placeholders)
75
+ end
55
76
 
77
+ def build_query_for(attributes, values)
56
78
  assignment_query = attributes.map do |attribute|
57
79
  quoted_column_name = connection.quote_column_name(attribute)
58
80
  "#{quoted_column_name} = tmp_table.#{quoted_column_name}"
@@ -60,11 +82,50 @@ module JunkDrawer
60
82
 
61
83
  "UPDATE #{table_name} " \
62
84
  "SET #{assignment_query} " \
63
- "FROM (VALUES #{object_values}) " \
85
+ "FROM (VALUES #{values}) " \
64
86
  "AS tmp_table(id, #{attributes.join(', ')}) " \
65
87
  "WHERE #{table_name}.id = tmp_table.id"
66
88
  end
67
89
 
90
+ def build_placeholders(objects, attributes)
91
+ index = 0
92
+ objects.map do
93
+ attribute_placeholders = attributes.map do |attribute|
94
+ index += 1
95
+ attribute_placeholder(attribute, index)
96
+ end.join(', ')
97
+
98
+ "(#{attribute_placeholders})"
99
+ end.join(', ')
100
+ end
101
+
102
+ def attribute_placeholder(attribute, index)
103
+ # AR internal `columns_hash`
104
+ column = columns_hash[attribute.to_s]
105
+
106
+ type_cast = "::#{column.sql_type}"
107
+ type_cast = "#{type_cast}[]" if column.array
108
+
109
+ "$#{index}#{type_cast}"
110
+ end
111
+
112
+ def values_for_objects(objects, attributes)
113
+ objects.flat_map { |object| values_for_object(object, attributes) }
114
+ end
115
+
116
+ def values_for_object(object, attributes)
117
+ attributes.map do |attribute|
118
+ value = object[attribute]
119
+
120
+ # AR internal `columns_hash`
121
+ column = columns_hash[attribute.to_s]
122
+
123
+ # AR internal `type_for_attribute`
124
+ type = type_for_attribute(column.name)
125
+ ActiveRecord::Relation::QueryAttribute.new(column.name, value, type)
126
+ end
127
+ end
128
+
68
129
  def sanitized_values(object, attributes)
69
130
  postgres_values = attributes.map do |attribute|
70
131
  value = object[attribute]
@@ -77,18 +138,15 @@ module JunkDrawer
77
138
  type_cast = "::#{column.sql_type}"
78
139
  type_cast = "#{type_cast}[]" if column.array
79
140
 
80
- "#{connection.quote(serialized_value(type, value))}#{type_cast}"
141
+ "#{connection.quote(type.serialize(value))}#{type_cast}"
81
142
  end
82
143
 
83
144
  "(#{[object.id, *postgres_values].join(', ')})"
84
145
  end
85
146
 
86
- def serialized_value(type, value)
87
- if ::ActiveRecord::VERSION::MAJOR >= 5
88
- type.serialize(value)
89
- else
90
- type.type_cast_for_database(value)
91
- end
147
+ def batch_size(attribute_names)
148
+ max_bind_params = connection.__send__(:bind_params_length)
149
+ max_bind_params / attribute_names.length
92
150
  end
93
151
  end
94
152
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JunkDrawer
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: junk_drawer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Fletcher
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-12 00:00:00.000000000 Z
11
+ date: 2023-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: guard
@@ -164,7 +164,7 @@ dependencies:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
166
  version: '2.0'
167
- description:
167
+ description:
168
168
  email:
169
169
  - lobatifricha@gmail.com
170
170
  executables: []
@@ -199,7 +199,7 @@ homepage: https://github.com/thread-pond/junk_drawer
199
199
  licenses:
200
200
  - MIT
201
201
  metadata: {}
202
- post_install_message:
202
+ post_install_message:
203
203
  rdoc_options: []
204
204
  require_paths:
205
205
  - lib
@@ -214,8 +214,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
214
  - !ruby/object:Gem::Version
215
215
  version: '0'
216
216
  requirements: []
217
- rubygems_version: 3.4.2
218
- signing_key:
217
+ rubygems_version: 3.4.10
218
+ signing_key:
219
219
  specification_version: 4
220
220
  summary: random useful Ruby utilities
221
221
  test_files: []