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,25 @@
1
+ module DynamoidAdvancedWhere
2
+ module Nodes
3
+ class GreaterThanNode < OperationNode
4
+ self.operator = '>'
5
+ end
6
+
7
+ module Concerns
8
+ module SupportsGreaterThan
9
+ def gt(other_value)
10
+ val = if respond_to?(:parse_right_hand_side)
11
+ parse_right_hand_side(other_value)
12
+ else
13
+ other_value
14
+ end
15
+
16
+ GreaterThanNode.new(
17
+ lh_operation: self,
18
+ rh_operation: LiteralNode.new(val)
19
+ )
20
+ end
21
+ alias > gt
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ module DynamoidAdvancedWhere
2
+ module Nodes
3
+ class IncludesNode < OperationNode
4
+ def to_expression
5
+ "contains(
6
+ #{lh_operation.to_expression},
7
+ #{rh_operation.to_expression}
8
+ )"
9
+ end
10
+ end
11
+
12
+ module Concerns
13
+ module SupportsIncludes
14
+ def includes?(other_value)
15
+ val = if respond_to?(:parse_right_hand_side)
16
+ parse_right_hand_side(other_value)
17
+ else
18
+ other_value
19
+ end
20
+
21
+ IncludesNode.new(
22
+ lh_operation: self,
23
+ rh_operation: LiteralNode.new(val)
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamoidAdvancedWhere
4
+ module Nodes
5
+ class LessThanNode < OperationNode
6
+ self.operator = '<'
7
+ end
8
+
9
+ module Concerns
10
+ module SupportsGreaterThan
11
+ def lt(other_value)
12
+ val = if respond_to?(:parse_right_hand_side)
13
+ parse_right_hand_side(other_value)
14
+ else
15
+ other_value
16
+ end
17
+
18
+ LessThanNode.new(
19
+ lh_operation: self,
20
+ rh_operation: LiteralNode.new(val)
21
+ )
22
+ end
23
+ alias < lt
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module DynamoidAdvancedWhere
6
+ module Nodes
7
+ class LiteralNode
8
+ attr_accessor :value, :attr_prefix
9
+ def initialize(value)
10
+ self.value = value
11
+ self.attr_prefix = SecureRandom.hex
12
+ freeze
13
+ end
14
+
15
+ def to_expression
16
+ ":#{attr_prefix}"
17
+ end
18
+
19
+ def expression_attribute_names
20
+ {}
21
+ end
22
+
23
+ def expression_attribute_values
24
+ { ":#{attr_prefix}" => value }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module DynamoidAdvancedWhere
6
+ module Nodes
7
+ class NotNode
8
+ extend Forwardable
9
+
10
+ attr_accessor :sub_node
11
+
12
+ def_delegators :@sub_node,
13
+ :expression_attribute_names,
14
+ :expression_attribute_values
15
+
16
+ def initialize(sub_node:)
17
+ self.sub_node = sub_node
18
+ freeze
19
+ end
20
+
21
+ def to_expression
22
+ "NOT(#{sub_node.to_expression})"
23
+ end
24
+ end
25
+
26
+ module Concerns
27
+ module Negatable
28
+ def negate
29
+ NotNode.new(sub_node: self)
30
+ end
31
+ alias ! negate
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module DynamoidAdvancedWhere
6
+ module Nodes
7
+ class NullNode
8
+ def initialize
9
+ freeze
10
+ end
11
+
12
+ def to_expression
13
+ ''
14
+ end
15
+
16
+ def expression_attribute_names
17
+ {}
18
+ end
19
+
20
+ def expression_attribute_values
21
+ {}
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './not'
4
+ require_relative './and_node'
5
+ require_relative './or_node'
6
+
7
+ module DynamoidAdvancedWhere
8
+ module Nodes
9
+ class OperationNode < BaseNode
10
+ include Concerns::Negatable
11
+ include Concerns::SupportsLogicalAnd
12
+ include Concerns::SupportsLogicalOr
13
+
14
+ class << self
15
+ attr_accessor :operator
16
+ end
17
+
18
+ attr_accessor :lh_operation, :rh_operation
19
+
20
+ def initialize(lh_operation:, rh_operation:)
21
+ self.lh_operation = lh_operation
22
+ self.rh_operation = rh_operation
23
+ freeze
24
+ end
25
+
26
+ def to_expression
27
+ "#{lh_operation.to_expression} #{self.class.operator} " \
28
+ "#{rh_operation.to_expression} "
29
+ end
30
+
31
+ def expression_attribute_names
32
+ lh_operation.expression_attribute_names.merge(
33
+ rh_operation.expression_attribute_names
34
+ )
35
+ end
36
+
37
+ def expression_attribute_values
38
+ lh_operation.expression_attribute_values.merge(
39
+ rh_operation.expression_attribute_values
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ module DynamoidAdvancedWhere
2
+ module Nodes
3
+ class OrNode < BaseNode
4
+ include Concerns::Negatable
5
+ attr_accessor :child_nodes
6
+
7
+ def initialize(*child_nodes)
8
+ self.child_nodes = child_nodes.freeze
9
+ freeze
10
+ end
11
+
12
+ def to_expression
13
+ return if child_nodes.empty?
14
+
15
+ "(#{child_nodes.map(&:to_expression).join(') or (')})"
16
+ end
17
+
18
+ def expression_attribute_names
19
+ child_nodes.map(&:expression_attribute_names).inject({}, &:merge!)
20
+ end
21
+
22
+ def expression_attribute_values
23
+ child_nodes.map(&:expression_attribute_values).inject({}, &:merge!)
24
+ end
25
+
26
+ def or(other_value)
27
+ OrNode.new(other_value, *child_nodes)
28
+ end
29
+ alias | or
30
+ end
31
+
32
+ module Concerns
33
+ module SupportsLogicalOr
34
+ def or(other_value)
35
+ OrNode.new(self, other_value)
36
+ end
37
+ alias | or
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative './null_node'
5
+
6
+ module DynamoidAdvancedWhere
7
+ module Nodes
8
+ class RootNode < BaseNode
9
+ extend Forwardable
10
+ attr_accessor :klass, :child_node
11
+
12
+ def initialize(klass:, &blk)
13
+ self.klass = klass
14
+ evaluate_block(blk) if blk
15
+ self.child_node ||= NullNode.new
16
+ freeze
17
+ end
18
+
19
+ def method_missing(method, *args, &blk)
20
+ if allowed_field?(method)
21
+ FieldNode.create_node(attr_config: klass.attributes[method], field_path: method)
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ def respond_to_missing?(method, _i)
28
+ allowed_field?(method)
29
+ end
30
+
31
+ def allowed_field?(method)
32
+ klass.attributes.key?(method.to_sym)
33
+ end
34
+
35
+ private
36
+
37
+ def evaluate_block(blk)
38
+ self.child_node = if blk.arity.zero?
39
+ Dynamoid.logger.warn 'Using DynamoidAdvancedWhere builder without an argument is now deprecated'
40
+ instance_eval(&blk)
41
+ else
42
+ blk.call(self)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamoidAdvancedWhere
4
+ module Nodes
5
+ module Concerns
6
+ module SupportsSubFields
7
+ def sub_field(*path, options)
8
+ Nodes::FieldNode.create_node(
9
+ field_path: field_path + path,
10
+ attr_config: options
11
+ )
12
+ end
13
+ alias dig sub_field
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ require_relative './nodes'
2
+ require_relative './query_materializer'
3
+ require_relative './batched_updater'
4
+
5
+ module DynamoidAdvancedWhere
6
+ class QueryBuilder
7
+ attr_accessor :klass, :root_node
8
+
9
+ delegate :all, :each, to: :query_materializer
10
+
11
+ def initialize(klass:, &blk)
12
+ self.klass = klass
13
+ self.root_node = Nodes::RootNode.new(klass: klass, &blk)
14
+ end
15
+
16
+ def query_materializer
17
+ QueryMaterializer.new(query_builder: self)
18
+ end
19
+
20
+ def batch_update
21
+ BatchedUpdater.new(query_builder: self)
22
+ end
23
+
24
+ def upsert(*args)
25
+ update_fields = args.extract_options!
26
+ batch_update.set_values(update_fields).apply(*args)
27
+ end
28
+
29
+ def where(other_builder = nil, &blk)
30
+ raise 'cannot use a block and an argument' if other_builder && blk
31
+
32
+ other_builder = self.class.new(klass: klass, &blk) if blk
33
+
34
+ raise 'passed argument must be a query builder' unless other_builder.is_a?(self.class)
35
+
36
+ local_root_node = root_node
37
+ self.class.new(klass: klass) do
38
+ Nodes::AndNode.new(
39
+ other_builder.root_node.child_node,
40
+ local_root_node.child_node
41
+ )
42
+ end
43
+ end
44
+ alias and where
45
+
46
+ end
47
+ end
@@ -0,0 +1,73 @@
1
+ require_relative './filter_builder'
2
+
3
+ module DynamoidAdvancedWhere
4
+ class QueryMaterializer
5
+ include Enumerable
6
+ attr_accessor :query_builder
7
+
8
+
9
+ delegate :klass, to: :query_builder
10
+ delegate :table_name, to: :klass
11
+ delegate :to_a, :first, to: :each
12
+
13
+ delegate :must_scan?, to: :filter_builder
14
+
15
+ def initialize(query_builder:)
16
+ self.query_builder = query_builder
17
+ end
18
+
19
+ def all
20
+ each.to_a
21
+ end
22
+
23
+ def each(&blk)
24
+ return enum_for(:each) unless blk
25
+
26
+ if must_scan?
27
+ each_via_scan(&blk)
28
+ else
29
+ each_via_query(&blk)
30
+ end
31
+ end
32
+
33
+ def each_via_query
34
+ query = {
35
+ table_name: table_name,
36
+ }.merge(filter_builder.to_query_filter)
37
+
38
+ results = client.query(query)
39
+
40
+ if results.items
41
+ results.items.each do |item|
42
+ yield klass.from_database(item.symbolize_keys)
43
+ end
44
+ end
45
+ end
46
+
47
+ def each_via_scan
48
+ query = {
49
+ table_name: table_name
50
+ }.merge(filter_builder.to_scan_filter)
51
+
52
+ results = client.scan(query)
53
+
54
+ if results.items
55
+ results.items.each do |item|
56
+ yield klass.from_database(item.symbolize_keys)
57
+ end
58
+ end
59
+ end
60
+
61
+ def filter_builder
62
+ @filter_builder ||= FilterBuilder.new(
63
+ root_node: query_builder.root_node,
64
+ klass: klass,
65
+ )
66
+ end
67
+
68
+ private
69
+ def client
70
+ Dynamoid.adapter.client
71
+ end
72
+ end
73
+ end