junk_drawer 2.0.0 → 2.1.1

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: 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: []