dynamoid_advanced_where 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +100 -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 +119 -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 'bundler-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