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