dynamoid-advanced-where 1.0.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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +97 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +19 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +5 -0
  8. data/Appraisals +8 -0
  9. data/Gemfile +9 -0
  10. data/Gemfile.lock +121 -0
  11. data/README.md +375 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/dynamoid_advanced_where.gemspec +41 -0
  16. data/gemfiles/.bundle/config +2 -0
  17. data/gemfiles/dynamoid_3.4.gemfile +8 -0
  18. data/gemfiles/dynamoid_3.4.gemfile.lock +118 -0
  19. data/gemfiles/dynamoid_latest.gemfile +8 -0
  20. data/gemfiles/dynamoid_latest.gemfile.lock +118 -0
  21. data/lib/dynamoid_advanced_where.rb +8 -0
  22. data/lib/dynamoid_advanced_where/batched_updater.rb +229 -0
  23. data/lib/dynamoid_advanced_where/filter_builder.rb +136 -0
  24. data/lib/dynamoid_advanced_where/integrations/model.rb +34 -0
  25. data/lib/dynamoid_advanced_where/nodes.rb +15 -0
  26. data/lib/dynamoid_advanced_where/nodes/and_node.rb +43 -0
  27. data/lib/dynamoid_advanced_where/nodes/base_node.rb +18 -0
  28. data/lib/dynamoid_advanced_where/nodes/equality_node.rb +37 -0
  29. data/lib/dynamoid_advanced_where/nodes/exists_node.rb +44 -0
  30. data/lib/dynamoid_advanced_where/nodes/field_node.rb +186 -0
  31. data/lib/dynamoid_advanced_where/nodes/greater_than_node.rb +25 -0
  32. data/lib/dynamoid_advanced_where/nodes/includes.rb +29 -0
  33. data/lib/dynamoid_advanced_where/nodes/less_than_node.rb +27 -0
  34. data/lib/dynamoid_advanced_where/nodes/literal_node.rb +28 -0
  35. data/lib/dynamoid_advanced_where/nodes/not.rb +35 -0
  36. data/lib/dynamoid_advanced_where/nodes/null_node.rb +25 -0
  37. data/lib/dynamoid_advanced_where/nodes/operation_node.rb +44 -0
  38. data/lib/dynamoid_advanced_where/nodes/or_node.rb +41 -0
  39. data/lib/dynamoid_advanced_where/nodes/root_node.rb +47 -0
  40. data/lib/dynamoid_advanced_where/nodes/subfield.rb +17 -0
  41. data/lib/dynamoid_advanced_where/query_builder.rb +47 -0
  42. data/lib/dynamoid_advanced_where/query_materializer.rb +73 -0
  43. data/lib/dynamoid_advanced_where/version.rb +3 -0
  44. metadata +216 -0
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dynamoid/advanced/where"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,41 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'dynamoid_advanced_where/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'dynamoid-advanced-where'
7
+ spec.version = DynamoidAdvancedWhere::VERSION
8
+ spec.authors = ['Brian Malinconico']
9
+ spec.email = ['bmalinconico@terminus.com']
10
+
11
+ spec.summary = 'things'
12
+ spec.description = 'things'
13
+ # spec.homepage = "things"
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ # if spec.respond_to?(:metadata)
18
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
19
+ # else
20
+ # raise "RubyGems 2.0 or newer is required to protect against " \
21
+ # "public gem pushes."
22
+ # end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{^(test|spec|features)/})
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_dependency 'dynamoid', '>= 3.2', '< 4'
32
+
33
+ spec.add_development_dependency 'bundle-audit'
34
+ spec.add_development_dependency 'bundler', '>= 1.16'
35
+ spec.add_development_dependency 'fasterer'
36
+ spec.add_development_dependency 'overcommit'
37
+ spec.add_development_dependency 'rake', '~> 10.0'
38
+ spec.add_development_dependency 'rspec', '~> 3.0'
39
+ spec.add_development_dependency 'rubocop'
40
+ spec.add_development_dependency 'appraisal'
41
+ end
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry"
6
+ gem "dynamoid", "~> 3.4.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,118 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ dynamoid-advanced-where (0.1.0)
5
+ dynamoid (>= 3.2, < 4)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.0.2.1)
11
+ activesupport (= 6.0.2.1)
12
+ activesupport (6.0.2.1)
13
+ concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ i18n (>= 0.7, < 2)
15
+ minitest (~> 5.1)
16
+ tzinfo (~> 1.1)
17
+ zeitwerk (~> 2.2)
18
+ appraisal (2.2.0)
19
+ bundler
20
+ rake
21
+ thor (>= 0.14.0)
22
+ ast (2.4.0)
23
+ aws-eventstream (1.0.3)
24
+ aws-partitions (1.270.0)
25
+ aws-sdk-core (3.89.1)
26
+ aws-eventstream (~> 1.0, >= 1.0.2)
27
+ aws-partitions (~> 1, >= 1.239.0)
28
+ aws-sigv4 (~> 1.1)
29
+ jmespath (~> 1.0)
30
+ aws-sdk-dynamodb (1.41.0)
31
+ aws-sdk-core (~> 3, >= 3.71.0)
32
+ aws-sigv4 (~> 1.1)
33
+ aws-sigv4 (1.1.0)
34
+ aws-eventstream (~> 1.0, >= 1.0.2)
35
+ bundle-audit (0.1.0)
36
+ bundler-audit
37
+ bundler-audit (0.6.1)
38
+ bundler (>= 1.2.0, < 3)
39
+ thor (~> 0.18)
40
+ childprocess (3.0.0)
41
+ coderay (1.1.2)
42
+ colorize (0.8.1)
43
+ concurrent-ruby (1.1.5)
44
+ diff-lcs (1.3)
45
+ dynamoid (3.4.1)
46
+ activemodel (>= 4)
47
+ aws-sdk-dynamodb (~> 1)
48
+ concurrent-ruby (>= 1.0)
49
+ null-logger
50
+ fasterer (0.8.2)
51
+ colorize (~> 0.7)
52
+ ruby_parser (>= 3.14.1)
53
+ i18n (1.8.2)
54
+ concurrent-ruby (~> 1.0)
55
+ iniparse (1.4.4)
56
+ jaro_winkler (1.5.4)
57
+ jmespath (1.4.0)
58
+ method_source (0.9.2)
59
+ minitest (5.14.0)
60
+ null-logger (0.1.5)
61
+ overcommit (0.52.1)
62
+ childprocess (>= 0.6.3, < 4)
63
+ iniparse (~> 1.4)
64
+ parallel (1.19.1)
65
+ parser (2.7.0.2)
66
+ ast (~> 2.4.0)
67
+ pry (0.12.2)
68
+ coderay (~> 1.1.0)
69
+ method_source (~> 0.9.0)
70
+ rainbow (3.0.0)
71
+ rake (10.5.0)
72
+ rspec (3.9.0)
73
+ rspec-core (~> 3.9.0)
74
+ rspec-expectations (~> 3.9.0)
75
+ rspec-mocks (~> 3.9.0)
76
+ rspec-core (3.9.1)
77
+ rspec-support (~> 3.9.1)
78
+ rspec-expectations (3.9.0)
79
+ diff-lcs (>= 1.2.0, < 2.0)
80
+ rspec-support (~> 3.9.0)
81
+ rspec-mocks (3.9.1)
82
+ diff-lcs (>= 1.2.0, < 2.0)
83
+ rspec-support (~> 3.9.0)
84
+ rspec-support (3.9.2)
85
+ rubocop (0.79.0)
86
+ jaro_winkler (~> 1.5.1)
87
+ parallel (~> 1.10)
88
+ parser (>= 2.7.0.1)
89
+ rainbow (>= 2.2.2, < 4.0)
90
+ ruby-progressbar (~> 1.7)
91
+ unicode-display_width (>= 1.4.0, < 1.7)
92
+ ruby-progressbar (1.10.1)
93
+ ruby_parser (3.14.1)
94
+ sexp_processor (~> 4.9)
95
+ sexp_processor (4.13.0)
96
+ thor (0.20.3)
97
+ thread_safe (0.3.6)
98
+ tzinfo (1.2.6)
99
+ thread_safe (~> 0.1)
100
+ unicode-display_width (1.6.1)
101
+ zeitwerk (2.2.2)
102
+
103
+ PLATFORMS
104
+ ruby
105
+ x86_64-darwin-18
106
+
107
+ DEPENDENCIES
108
+ appraisal
109
+ bundle-audit
110
+ bundler (>= 1.16)
111
+ dynamoid (~> 3.4.0)
112
+ dynamoid-advanced-where!
113
+ fasterer
114
+ overcommit
115
+ pry
116
+ rake (~> 10.0)
117
+ rspec (~> 3.0)
118
+ rubocop
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "pry"
6
+ gem "dynamoid", "~> 3.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,118 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ dynamoid-advanced-where (0.1.0)
5
+ dynamoid (>= 3.2, < 4)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (6.0.2.1)
11
+ activesupport (= 6.0.2.1)
12
+ activesupport (6.0.2.1)
13
+ concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ i18n (>= 0.7, < 2)
15
+ minitest (~> 5.1)
16
+ tzinfo (~> 1.1)
17
+ zeitwerk (~> 2.2)
18
+ appraisal (2.2.0)
19
+ bundler
20
+ rake
21
+ thor (>= 0.14.0)
22
+ ast (2.4.0)
23
+ aws-eventstream (1.0.3)
24
+ aws-partitions (1.270.0)
25
+ aws-sdk-core (3.89.1)
26
+ aws-eventstream (~> 1.0, >= 1.0.2)
27
+ aws-partitions (~> 1, >= 1.239.0)
28
+ aws-sigv4 (~> 1.1)
29
+ jmespath (~> 1.0)
30
+ aws-sdk-dynamodb (1.41.0)
31
+ aws-sdk-core (~> 3, >= 3.71.0)
32
+ aws-sigv4 (~> 1.1)
33
+ aws-sigv4 (1.1.0)
34
+ aws-eventstream (~> 1.0, >= 1.0.2)
35
+ bundle-audit (0.1.0)
36
+ bundler-audit
37
+ bundler-audit (0.6.1)
38
+ bundler (>= 1.2.0, < 3)
39
+ thor (~> 0.18)
40
+ childprocess (3.0.0)
41
+ coderay (1.1.2)
42
+ colorize (0.8.1)
43
+ concurrent-ruby (1.1.5)
44
+ diff-lcs (1.3)
45
+ dynamoid (3.4.1)
46
+ activemodel (>= 4)
47
+ aws-sdk-dynamodb (~> 1)
48
+ concurrent-ruby (>= 1.0)
49
+ null-logger
50
+ fasterer (0.8.2)
51
+ colorize (~> 0.7)
52
+ ruby_parser (>= 3.14.1)
53
+ i18n (1.8.2)
54
+ concurrent-ruby (~> 1.0)
55
+ iniparse (1.4.4)
56
+ jaro_winkler (1.5.4)
57
+ jmespath (1.4.0)
58
+ method_source (0.9.2)
59
+ minitest (5.14.0)
60
+ null-logger (0.1.5)
61
+ overcommit (0.52.1)
62
+ childprocess (>= 0.6.3, < 4)
63
+ iniparse (~> 1.4)
64
+ parallel (1.19.1)
65
+ parser (2.7.0.2)
66
+ ast (~> 2.4.0)
67
+ pry (0.12.2)
68
+ coderay (~> 1.1.0)
69
+ method_source (~> 0.9.0)
70
+ rainbow (3.0.0)
71
+ rake (10.5.0)
72
+ rspec (3.9.0)
73
+ rspec-core (~> 3.9.0)
74
+ rspec-expectations (~> 3.9.0)
75
+ rspec-mocks (~> 3.9.0)
76
+ rspec-core (3.9.1)
77
+ rspec-support (~> 3.9.1)
78
+ rspec-expectations (3.9.0)
79
+ diff-lcs (>= 1.2.0, < 2.0)
80
+ rspec-support (~> 3.9.0)
81
+ rspec-mocks (3.9.1)
82
+ diff-lcs (>= 1.2.0, < 2.0)
83
+ rspec-support (~> 3.9.0)
84
+ rspec-support (3.9.2)
85
+ rubocop (0.79.0)
86
+ jaro_winkler (~> 1.5.1)
87
+ parallel (~> 1.10)
88
+ parser (>= 2.7.0.1)
89
+ rainbow (>= 2.2.2, < 4.0)
90
+ ruby-progressbar (~> 1.7)
91
+ unicode-display_width (>= 1.4.0, < 1.7)
92
+ ruby-progressbar (1.10.1)
93
+ ruby_parser (3.14.1)
94
+ sexp_processor (~> 4.9)
95
+ sexp_processor (4.13.0)
96
+ thor (0.20.3)
97
+ thread_safe (0.3.6)
98
+ tzinfo (1.2.6)
99
+ thread_safe (~> 0.1)
100
+ unicode-display_width (1.6.1)
101
+ zeitwerk (2.2.2)
102
+
103
+ PLATFORMS
104
+ ruby
105
+ x86_64-darwin-18
106
+
107
+ DEPENDENCIES
108
+ appraisal
109
+ bundle-audit
110
+ bundler (>= 1.16)
111
+ dynamoid (~> 3.0)
112
+ dynamoid-advanced-where!
113
+ fasterer
114
+ overcommit
115
+ pry
116
+ rake (~> 10.0)
117
+ rspec (~> 3.0)
118
+ rubocop
@@ -0,0 +1,8 @@
1
+ require 'dynamoid'
2
+
3
+ require "dynamoid_advanced_where/version"
4
+ require "dynamoid_advanced_where/integrations/model"
5
+
6
+ module DynamoidAdvancedWhere
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamoidAdvancedWhere
4
+ class BatchedUpdater
5
+ DEEP_MERGE_ATTRIBUTES = %i[expression_attribute_names expression_attribute_values].freeze
6
+
7
+ attr_accessor :query_builder, :_set_values, :_array_appends, :_set_appends, :_increments
8
+ delegate :klass, to: :query_builder
9
+
10
+ def initialize(query_builder:)
11
+ self.query_builder = query_builder
12
+ self._set_values = {}
13
+ self._set_appends = []
14
+ self._array_appends = []
15
+ self._increments = Hash.new(0)
16
+ end
17
+
18
+ def apply(hash_key, range_key = nil)
19
+ key_args = {
20
+ table_name: klass.table_name,
21
+ return_values: 'ALL_NEW',
22
+ key: {
23
+ klass.hash_key => hash_key,
24
+ klass.range_key => range_key
25
+ }.delete_if { |k, _v| k.nil? }
26
+ }
27
+ resp = client.update_item(update_item_arguments.merge(key_args))
28
+
29
+ klass.from_database(resp.attributes)
30
+ rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
31
+ end
32
+
33
+ def set_values(vals)
34
+ _set_values.merge!(vals)
35
+ self
36
+ end
37
+
38
+ def append_to(appends)
39
+ appends.each do |k, v|
40
+ case klass.attributes[k.to_sym][:type]
41
+ when :set
42
+ _set_appends << { k => v.to_set }
43
+ when :array
44
+ _array_appends << { k => v }
45
+ else
46
+ raise 'can only append to sets or arrays'
47
+ end
48
+ end
49
+
50
+ self
51
+ end
52
+
53
+ def increment(*fields, by: 1)
54
+ fields.each { |field| _increments[field] += by }
55
+ self
56
+ end
57
+
58
+ def decrement(*fields, by: 1)
59
+ increment(*fields, by: -1 * by)
60
+ self
61
+ end
62
+
63
+ private
64
+
65
+ def merge_multiple_sets(items_to_merge, result_base: {})
66
+ default = { collected_update_expression: [] }
67
+ result = result_base.merge(default)
68
+ items_to_merge.each do |update_data|
69
+ result[:collected_update_expression] << update_data.delete(:collected_update_expression)
70
+ result.merge!(update_data, &method(:hash_extendeer))
71
+ end
72
+
73
+ result[:collected_update_expression].flatten!
74
+ result[:collected_update_expression].reject!(&:blank?)
75
+
76
+ return default if result[:collected_update_expression].empty?
77
+
78
+ result
79
+ end
80
+
81
+ def field_update_arguments
82
+ merge_multiple_sets([set_values_update_args])
83
+ end
84
+
85
+ def update_item_arguments
86
+ filter = merge_multiple_sets(
87
+ [
88
+ field_update_arguments,
89
+ add_update_args,
90
+ ],
91
+ result_base: filter_builder.to_scan_filter,
92
+ )
93
+
94
+ filter[:update_expression] = filter.delete(:collected_update_expression).join(' ')
95
+ filter[:condition_expression] = filter.delete(:filter_expression)
96
+
97
+ filter
98
+ end
99
+
100
+ def args_to_update_command(update_args, command:)
101
+ return {} if update_args[:collected_update_expression].empty?
102
+
103
+ update_args.merge!(
104
+ collected_update_expression: [
105
+ "#{command} #{update_args[:collected_update_expression].join(', ')}"
106
+ ]
107
+ )
108
+ end
109
+
110
+ def set_values_update_args
111
+ args_to_update_command(
112
+ merge_multiple_sets(
113
+ [
114
+ explicit_set_args,
115
+ list_append_for_arrays,
116
+ increment_field_updates
117
+ ]
118
+ ),
119
+ command: 'SET'
120
+ )
121
+ end
122
+
123
+ def add_update_args
124
+ args_to_update_command(list_append_for_sets, command: 'ADD')
125
+ end
126
+
127
+ def explicit_set_args
128
+ builder_hash = { collected_update_expression: [] }
129
+
130
+ _set_values.each_with_object(builder_hash) do |(k, v), h|
131
+ prefix = merge_in_attr_placeholders(h, k, v)
132
+ h[:collected_update_expression] << "##{prefix} = :#{prefix}"
133
+ end
134
+ end
135
+
136
+
137
+ def increment_field_updates
138
+ return {} if _increments.empty?
139
+
140
+ zero_prefix = SecureRandom.hex
141
+
142
+ builder_hash = {
143
+ collected_update_expression: [],
144
+ expression_attribute_values: {
145
+ ":#{zero_prefix}": 0
146
+ }
147
+ }
148
+
149
+ _increments.each_with_object(builder_hash) do |(field, change), h|
150
+ prefix = merge_in_attr_placeholders(h, field, change)
151
+ builder_hash[:collected_update_expression] << "##{prefix} = if_not_exists(##{prefix}, :#{zero_prefix}) + :#{prefix}"
152
+ end
153
+ end
154
+
155
+ def list_append_for_sets
156
+ builder_hash = { collected_update_expression: [] }
157
+
158
+ _set_appends.each_with_object(builder_hash) do |to_append, h|
159
+ to_append.each do |k, v|
160
+ prefix = merge_in_attr_placeholders(h, k, v)
161
+ builder_hash[:collected_update_expression] << "##{prefix} :#{prefix}"
162
+ end
163
+ end
164
+ end
165
+
166
+ def list_append_for_arrays
167
+ empty_list_prefix = SecureRandom.hex
168
+
169
+ builder_hash = {
170
+ collected_update_expression: [],
171
+ expression_attribute_values: {
172
+ ":#{empty_list_prefix}": []
173
+ }
174
+ }
175
+
176
+ update_args = _array_appends.each_with_object(builder_hash) do |to_append, h|
177
+ to_append.each do |k, v|
178
+ prefix = merge_in_attr_placeholders(h, k, v)
179
+ builder_hash[:collected_update_expression] << "##{prefix} = list_append(if_not_exists(##{prefix}, :#{empty_list_prefix}), :#{prefix})"
180
+ end
181
+ end
182
+
183
+ builder_hash[:collected_update_expression].empty? ? {} : update_args
184
+ end
185
+
186
+ def merge_in_attr_placeholders(hsh, field_name, value)
187
+ prefix, new_data = prefixerize(field_name, value)
188
+
189
+ hsh.merge!(new_data, &method(:hash_extendeer))
190
+
191
+ prefix
192
+ end
193
+
194
+ def prefixerize(field_name, value)
195
+ prefix = SecureRandom.hex
196
+
197
+ [
198
+ prefix,
199
+ {
200
+ expression_attribute_names: { "##{prefix}" => field_name },
201
+ expression_attribute_values: {
202
+ ":#{prefix}" => dump(value, klass.attributes[field_name])
203
+ }
204
+ }
205
+ ]
206
+ end
207
+
208
+ def hash_extendeer(key, old_value, new_value)
209
+ return new_value unless key.in?(DEEP_MERGE_ATTRIBUTES)
210
+
211
+ old_value.merge(new_value)
212
+ end
213
+
214
+ def client
215
+ Dynamoid.adapter.client
216
+ end
217
+
218
+ def filter_builder
219
+ @filter_builder ||= FilterBuilder.new(
220
+ root_node: query_builder.root_node,
221
+ klass: klass,
222
+ )
223
+ end
224
+
225
+ def dump(value, config)
226
+ Dynamoid::Dumping.dump_field(value, config)
227
+ end
228
+ end
229
+ end