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 +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