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,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './nodes/null_node'
4
+
5
+ module DynamoidAdvancedWhere
6
+ class FilterBuilder
7
+ VALID_COMPARETORS_FOR_RANGE_FILTER = [
8
+ Nodes::GreaterThanNode
9
+ ].freeze
10
+
11
+ attr_accessor :expression_node, :klass
12
+
13
+ def initialize(root_node:, klass:)
14
+ self.expression_node = root_node.child_node
15
+ self.klass = klass
16
+ end
17
+
18
+ def index_nodes
19
+ [
20
+ extract_query_filter_node,
21
+ extract_range_key_node
22
+ ].compact
23
+ end
24
+
25
+ def to_query_filter
26
+ {
27
+ key_condition_expression: key_condition_expression
28
+ }.merge!(expression_filters)
29
+ end
30
+
31
+ def to_scan_filter
32
+ expression_filters
33
+ end
34
+
35
+ def must_scan?
36
+ !extract_query_filter_node.is_a?(Nodes::BaseNode)
37
+ end
38
+
39
+ private
40
+
41
+ def key_condition_expression
42
+ @key_condition_expression ||= [
43
+ extract_query_filter_node,
44
+ extract_range_key_node
45
+ ].compact.map(&:to_expression).join(' AND ')
46
+ end
47
+
48
+ def expression_attribute_names
49
+ [
50
+ expression_node,
51
+ *index_nodes
52
+ ].map(&:expression_attribute_names).inject({}, &:merge!)
53
+ end
54
+
55
+ def expression_attribute_values
56
+ [
57
+ expression_node,
58
+ *index_nodes
59
+ ].map(&:expression_attribute_values).inject({}, &:merge!)
60
+ end
61
+
62
+ def expression_filters
63
+ {
64
+ filter_expression: expression_node.to_expression,
65
+ expression_attribute_names: expression_attribute_names,
66
+ expression_attribute_values: expression_attribute_values
67
+ }.delete_if { |_, v| v.nil? || v.empty? }
68
+ end
69
+
70
+ def extract_query_filter_node
71
+ @extract_query_filter_node ||=
72
+ case expression_node
73
+ when Nodes::EqualityNode
74
+ node = expression_node
75
+ if field_node_valid_for_key_filter(expression_node)
76
+ self.expression_node = Nodes::NullNode.new
77
+ node
78
+ end
79
+ when Nodes::AndNode
80
+ id_filters = expression_node.child_nodes.select do |i|
81
+ field_node_valid_for_key_filter(i)
82
+ end
83
+
84
+ if id_filters.length == 1
85
+ self.expression_node = Nodes::AndNode.new(
86
+ *(expression_node.child_nodes - id_filters)
87
+ )
88
+
89
+ id_filters.first
90
+ end
91
+ end
92
+ end
93
+
94
+ def field_node_valid_for_key_filter(node)
95
+ node.is_a?(Nodes::EqualityNode) &&
96
+ node.lh_operation.is_a?(Nodes::FieldNode) &&
97
+ node.lh_operation.field_path.length == 1 &&
98
+ node.lh_operation.field_path[0].to_s == hash_key
99
+ end
100
+
101
+ def extract_range_key_node
102
+ return unless extract_query_filter_node
103
+
104
+ @extract_range_key_node ||=
105
+ case expression_node
106
+ when Nodes::AndNode
107
+ id_filters = expression_node.child_nodes.select do |i|
108
+ field_node_valid_for_range_filter(i)
109
+ end
110
+
111
+ if id_filters.length == 1
112
+ self.expression_node = Nodes::AndNode.new(
113
+ *(expression_node.child_nodes - id_filters)
114
+ )
115
+
116
+ id_filters.first
117
+ end
118
+ end
119
+ end
120
+
121
+ def field_node_valid_for_range_filter(node)
122
+ node.lh_operation.is_a?(Nodes::FieldNode) &&
123
+ node.lh_operation.field_path.length == 1 &&
124
+ node.lh_operation.field_path[0].to_s == range_key &&
125
+ VALID_COMPARETORS_FOR_RANGE_FILTER.any? { |type| node.is_a?(type) }
126
+ end
127
+
128
+ def hash_key
129
+ @hash_key ||= klass.hash_key.to_s
130
+ end
131
+
132
+ def range_key
133
+ @range_key ||= klass.range_key.to_s
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dynamoid_advanced_where/query_builder'
4
+
5
+ module DynamoidAdvancedWhere
6
+ # Allows classes to be queried by where, all, first, and each and return criteria chains.
7
+ module Integrations
8
+ module Model
9
+ extend ActiveSupport::Concern
10
+
11
+ class_methods do
12
+ def advanced_where(&blk)
13
+ DynamoidAdvancedWhere::QueryBuilder.new(klass: self, &blk)
14
+ end
15
+
16
+ def batch_update
17
+ advanced_where {}.batch_update
18
+ end
19
+
20
+ def where(*args, &blk)
21
+ if !args.empty?
22
+ raise ArgumentError, 'You may not specify where arguments and block' if blk
23
+
24
+ super(*args)
25
+ else
26
+ DynamoidAdvancedWhere::QueryBuilder.new(klass: self, &blk)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ Dynamoid::Document.send(:include, DynamoidAdvancedWhere::Integrations::Model)
@@ -0,0 +1,15 @@
1
+ require_relative './nodes/base_node'
2
+ require_relative './nodes/field_node'
3
+ require_relative './nodes/literal_node'
4
+ require_relative './nodes/operation_node'
5
+ require_relative './nodes/root_node'
6
+ require_relative './nodes/and_node'
7
+ require_relative './nodes/or_node'
8
+
9
+ require_relative './nodes/equality_node'
10
+ require_relative './nodes/exists_node'
11
+ require_relative './nodes/includes'
12
+
13
+ require_relative './nodes/greater_than_node'
14
+
15
+ require_relative './nodes/less_than_node'
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamoidAdvancedWhere
4
+ module Nodes
5
+ class AndNode < BaseNode
6
+ include Concerns::Negatable
7
+ attr_accessor :child_nodes
8
+
9
+ def initialize(*child_nodes)
10
+ self.child_nodes = child_nodes.freeze
11
+ freeze
12
+ end
13
+
14
+ def to_expression
15
+ return if child_nodes.empty?
16
+
17
+ "(#{child_nodes.map(&:to_expression).join(') and (')})"
18
+ end
19
+
20
+ def expression_attribute_names
21
+ child_nodes.map(&:expression_attribute_names).inject({}, &:merge!)
22
+ end
23
+
24
+ def expression_attribute_values
25
+ child_nodes.map(&:expression_attribute_values).inject({}, &:merge!)
26
+ end
27
+
28
+ def and(other_value)
29
+ AndNode.new(other_value, *child_nodes)
30
+ end
31
+ alias & and
32
+ end
33
+
34
+ module Concerns
35
+ module SupportsLogicalAnd
36
+ def and(other_value)
37
+ AndNode.new(self, other_value)
38
+ end
39
+ alias & and
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ require 'securerandom'
2
+
3
+ module DynamoidAdvancedWhere
4
+ module Nodes
5
+ class BaseNode
6
+ attr_accessor :expression_prefix
7
+
8
+ def expression_attribute_names
9
+ {}
10
+ end
11
+
12
+ def expression_attribute_values
13
+ {}
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './operation_node'
4
+ require_relative './not'
5
+
6
+ module DynamoidAdvancedWhere
7
+ module Nodes
8
+ class EqualityNode < OperationNode
9
+ include Concerns::Negatable
10
+
11
+ self.operator = '='
12
+ end
13
+
14
+ module Concerns
15
+ module SupportsEquality
16
+ def eq(other_value)
17
+ val = if respond_to?(:parse_right_hand_side)
18
+ parse_right_hand_side(other_value)
19
+ else
20
+ other_value
21
+ end
22
+
23
+ EqualityNode.new(
24
+ lh_operation: self,
25
+ rh_operation: LiteralNode.new(val)
26
+ )
27
+ end
28
+ alias == eq
29
+
30
+ def not_eq(other_value)
31
+ eq(other_value).negate
32
+ end
33
+ alias != not_eq
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module DynamoidAdvancedWhere
6
+ module Nodes
7
+ class ExistsNode < BaseNode
8
+ include Concerns::Negatable
9
+
10
+ attr_accessor :field_node, :prefix
11
+ def initialize(field_node:)
12
+ self.field_node = field_node
13
+ self.prefix = SecureRandom.hex
14
+ freeze
15
+ end
16
+
17
+ def to_expression
18
+ "NOT(
19
+ attribute_not_exists(#{field_node.to_expression})
20
+ or #{field_node.to_expression} = :#{prefix}
21
+ )"
22
+ end
23
+
24
+ def expression_attribute_names
25
+ field_node.expression_attribute_names
26
+ end
27
+
28
+ def expression_attribute_values
29
+ {
30
+ ":#{prefix}" => nil
31
+ }
32
+ end
33
+ end
34
+
35
+ module Concerns
36
+ module SupportsExistance
37
+ def exists?
38
+ ExistsNode.new(field_node: self)
39
+ end
40
+ alias present? exists?
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './equality_node'
4
+ require_relative './greater_than_node'
5
+ require_relative './exists_node'
6
+ require_relative './includes'
7
+ require_relative './subfield'
8
+
9
+ module DynamoidAdvancedWhere
10
+ module Nodes
11
+ class FieldNode < BaseNode
12
+ include Concerns::SupportsEquality
13
+ include Concerns::SupportsExistance
14
+
15
+ attr_accessor :field_path, :attr_prefix
16
+
17
+ class << self
18
+ def create_node(field_path:, attr_config:)
19
+ specific_klass = FIELD_MAPPING.detect do |config, _type|
20
+ config.respond_to?(:call) ? config.call(attr_config) : config <= attr_config
21
+ end&.last
22
+
23
+ unless specific_klass
24
+ raise ArgumentError, "unable to find field type for `#{attr_config}`"
25
+ end
26
+
27
+ specific_klass.new(field_path: field_path)
28
+ end
29
+ end
30
+
31
+ def initialize(field_path:)
32
+ self.field_path = field_path.is_a?(Array) ? field_path : [field_path]
33
+ self.attr_prefix = SecureRandom.hex
34
+ freeze
35
+ end
36
+
37
+ def to_expression
38
+ String.new.tap do |s|
39
+ field_path.collect.with_index do |segment, i|
40
+ if segment.is_a?(Integer)
41
+ s << "[#{segment}]"
42
+ else
43
+ s << '.' unless s.blank?
44
+ s << "##{attr_prefix}#{i}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def expression_attribute_names
51
+ field_path.each_with_object({}).with_index do |(segment, hsh), i|
52
+ next if segment.is_a?(Integer)
53
+
54
+ hsh["##{attr_prefix}#{i}"] = segment
55
+ end
56
+ end
57
+
58
+ def expression_attribute_values
59
+ {}
60
+ end
61
+ end
62
+
63
+ class StringAttributeNode < FieldNode
64
+ include Concerns::SupportsIncludes
65
+ end
66
+ class NativeBooleanAttributeNode < FieldNode; end
67
+
68
+ class StringBooleanAttributeNode < FieldNode
69
+ def parse_right_hand_side(val)
70
+ val ? 't' : 'f'
71
+ end
72
+ end
73
+
74
+ class NumberAttributeNode < FieldNode
75
+ include Concerns::SupportsGreaterThan
76
+
77
+ ALLOWED_COMPARISON_TYPES = [
78
+ Numeric
79
+ ].freeze
80
+
81
+ def parse_right_hand_side(val)
82
+ unless ALLOWED_COMPARISON_TYPES.detect { |k| val.is_a?(k) }
83
+ raise ArgumentError, "unable to compare number to `#{val.class}`"
84
+ end
85
+
86
+ val
87
+ end
88
+ end
89
+
90
+ class NumericDatetimeAttributeNode < FieldNode
91
+ include Concerns::SupportsGreaterThan
92
+
93
+ def parse_right_hand_side(val)
94
+ if val.is_a?(Date)
95
+ val.to_time.to_i
96
+ elsif val.is_a?(Time)
97
+ val.to_f
98
+ else
99
+ raise ArgumentError, "unable to compare datetime to type #{val.class}"
100
+ end
101
+ end
102
+ end
103
+
104
+ class NumericDateAttributeNode < FieldNode
105
+ include Concerns::SupportsGreaterThan
106
+
107
+ def parse_right_hand_side(val)
108
+ if !val.is_a?(Date) || val.is_a?(DateTime)
109
+ raise ArgumentError, "unable to compare date to type #{val.class}"
110
+ end
111
+
112
+ (val - Dynamoid::Persistence::UNIX_EPOCH_DATE).to_i
113
+ end
114
+ end
115
+
116
+ class StringSetAttributeNode < FieldNode
117
+ include Concerns::SupportsIncludes
118
+
119
+ def parse_right_hand_side(val)
120
+ unless val.is_a?(String)
121
+ raise ArgumentError, "unable to compare date to type #{val.class}"
122
+ end
123
+
124
+ val
125
+ end
126
+ end
127
+
128
+ class IntegerSetAttributeNode < FieldNode
129
+ include Concerns::SupportsIncludes
130
+
131
+ def parse_right_hand_side(val)
132
+ unless val.is_a?(Integer)
133
+ raise ArgumentError, "unable to compare date to type #{val.class}"
134
+ end
135
+
136
+ val
137
+ end
138
+ end
139
+
140
+ class MapAttributeNode < FieldNode
141
+ include Concerns::SupportsSubFields
142
+ end
143
+
144
+ class RawAttributeNode < FieldNode
145
+ include Concerns::SupportsSubFields
146
+ end
147
+
148
+ class CustomClassAttributeNode < FieldNode
149
+ include Concerns::SupportsSubFields
150
+ end
151
+
152
+ FIELD_MAPPING = {
153
+ { type: :string } => StringAttributeNode,
154
+ { type: :number } => NumberAttributeNode,
155
+
156
+ # Boolean Fields
157
+ { type: :boolean, store_as_native_boolean: true } =>
158
+ NativeBooleanAttributeNode,
159
+ { type: :boolean, store_as_native_boolean: false } =>
160
+ StringBooleanAttributeNode,
161
+
162
+ # Datetime fields
163
+ { type: :datetime, store_as_string: true } => nil,
164
+ { type: :datetime, store_as_string: false } => NumericDatetimeAttributeNode,
165
+ { type: :datetime } => NumericDatetimeAttributeNode,
166
+
167
+ # Date fields
168
+ { type: :date, store_as_string: true } => nil,
169
+ { type: :date, store_as_string: false } => NumericDateAttributeNode,
170
+ { type: :date } => NumericDateAttributeNode,
171
+
172
+ # Set Types
173
+ { type: :set, of: :string } => StringSetAttributeNode,
174
+ { type: :set, of: :integer } => IntegerSetAttributeNode,
175
+
176
+ # Map Types
177
+ { type: :map } => MapAttributeNode,
178
+
179
+ # Raw Types
180
+ { type: :raw } => RawAttributeNode,
181
+
182
+ # Custom Object
183
+ ->(c) { c[:type].is_a?(Class) } => CustomClassAttributeNode
184
+ }.freeze
185
+ end
186
+ end