dynamoid_advanced_where 1.5.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca5af3b66602bc1a123044bd91797fefc1417d08e2ebf8c8103f0540aa68f0af
4
- data.tar.gz: 167687a1a39ec4b088445dda5bbb15ad3463b03ee1cf9bf6d4ddff7359812185
3
+ metadata.gz: 239094fa5509e994cefa186a296b6f00bbc7a13913ea67803f82f173e6aea82a
4
+ data.tar.gz: cb46ded36d28fc09b54ea3b1598c34e2172232824056c9b6462f846f491e9673
5
5
  SHA512:
6
- metadata.gz: 164d8e1236c19769ecdeb29370aa5d0c0305a0070eb00477e7871d573a364e1288d1be77a1cb1fd2fa2ed3eea2885e887cb20fab1a7846613dbd8eb64b426576
7
- data.tar.gz: 735bc10d7925ae510f8154e0a3bffa0e6e77c76c5de700fa15b39b96fa6b2aebceb75607da860676ad5478cc34148a141ca8cf686457aed23b95de45247d9c42
6
+ metadata.gz: bce42bfde6205d3a63ea557b0624391170f586220247fc3c7ab17a1203eb6876d4356b6758a41df0756ea9de07ee06b29ce84ba4c40b516143a803d2d361a9cc
7
+ data.tar.gz: 633267b36adc9112e67aa8c541946d4ce9a56821d68aa339af211c5ffa400c6b6491b188e33f1dcda01ebd4ea32842fffcde432a783687d14080fc589beecb52
@@ -3,7 +3,8 @@ AllCops:
3
3
  - Makefile
4
4
  - vendor/**/*
5
5
  - bin/**/*
6
-
6
+ - '**/*_pb.rb'
7
+
7
8
  Layout/EndOfLine:
8
9
  Enabled: false
9
10
 
@@ -40,8 +41,6 @@ Layout/LineLength:
40
41
  Max: 280
41
42
  IgnoreCopDirectives: true
42
43
  AllowedPatterns: ['\A#', '\A\s*sig { .* }\Z']
43
- Exclude:
44
- - '**/*_pb.rb'
45
44
 
46
45
  Metrics/AbcSize:
47
46
  Enabled: true
@@ -76,7 +75,6 @@ Metrics/BlockLength:
76
75
  Max: 30
77
76
  Exclude:
78
77
  - spec/**/*.rb
79
- - '**/*_pb.rb'
80
78
 
81
79
  Metrics/ParameterLists:
82
80
  Max: 6
data/Gemfile.lock CHANGED
@@ -1,15 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dynamoid_advanced_where (1.5.1)
4
+ dynamoid_advanced_where (1.6.0)
5
5
  dynamoid (>= 3.2, < 4)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.0.4.2)
11
- activesupport (= 7.0.4.2)
12
- activesupport (7.0.4.2)
10
+ activemodel (7.0.4.3)
11
+ activesupport (= 7.0.4.3)
12
+ activesupport (7.0.4.3)
13
13
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
14
  i18n (>= 1.6, < 2)
15
15
  minitest (>= 5.1)
@@ -22,8 +22,8 @@ GEM
22
22
  thor (>= 0.14.0)
23
23
  ast (2.4.2)
24
24
  aws-eventstream (1.2.0)
25
- aws-partitions (1.725.0)
26
- aws-sdk-core (3.170.0)
25
+ aws-partitions (1.743.0)
26
+ aws-sdk-core (3.171.0)
27
27
  aws-eventstream (~> 1, >= 1.0.2)
28
28
  aws-partitions (~> 1, >= 1.651.0)
29
29
  aws-sigv4 (~> 1.5)
@@ -8,17 +8,18 @@ module DynamoidAdvancedWhere
8
8
  Nodes::GreaterThanNode,
9
9
  ].freeze
10
10
 
11
- attr_accessor :expression_node, :klass
11
+ attr_accessor :expression_node, :query_filter_node, :range_key_node, :klass
12
12
 
13
13
  def initialize(root_node:, klass:)
14
- self.expression_node = root_node.child_node
14
+ node = root_node.child_node
15
+ self.expression_node = node.is_a?(Nodes::AndNode) ? node : Nodes::AndNode.new(node)
15
16
  self.klass = klass
16
17
  end
17
18
 
18
19
  def index_nodes
19
20
  [
20
- extract_query_filter_node,
21
- extract_range_key_node,
21
+ query_filter_node,
22
+ range_key_node,
22
23
  ].compact
23
24
  end
24
25
 
@@ -32,16 +33,43 @@ module DynamoidAdvancedWhere
32
33
  expression_filters
33
34
  end
34
35
 
35
- def must_scan?
36
- !extract_query_filter_node.is_a?(Nodes::BaseNode)
36
+ def select_node_for_range_key(node)
37
+ raise 'node not found in expression' unless expression_node.child_nodes.include?(node)
38
+
39
+ self.range_key_node = node
40
+
41
+ self.expression_node = Nodes::AndNode.new(
42
+ *(expression_node.child_nodes - [node])
43
+ )
44
+ end
45
+
46
+ def select_node_for_query_filter(node)
47
+ raise 'node not found in expression' unless expression_node.child_nodes.include?(node)
48
+
49
+ self.query_filter_node = node
50
+
51
+ self.expression_node = Nodes::AndNode.new(
52
+ *(expression_node.child_nodes - [node])
53
+ )
54
+ end
55
+
56
+ # Returns a hash of the field name and the node that filters on it
57
+ def extractable_fields_for_hash_and_range
58
+ expression_node.child_nodes.each_with_object({}) do |node, hash|
59
+ next unless node.respond_to?(:lh_operation) &&
60
+ node.lh_operation.is_a?(Nodes::FieldNode) &&
61
+ node.lh_operation.field_path.length == 1
62
+
63
+ hash[node.lh_operation.field_path[0].to_s] = node
64
+ end
37
65
  end
38
66
 
39
67
  private
40
68
 
41
69
  def key_condition_expression
42
70
  @key_condition_expression ||= [
43
- extract_query_filter_node,
44
- extract_range_key_node,
71
+ query_filter_node,
72
+ range_key_node,
45
73
  ].compact.map(&:to_expression).join(' AND ')
46
74
  end
47
75
 
@@ -66,73 +94,5 @@ module DynamoidAdvancedWhere
66
94
  expression_attribute_values: expression_attribute_values,
67
95
  }.delete_if { |_, v| v.nil? || v.empty? }
68
96
  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.respond_to?(:lh_operation) &&
97
- node.lh_operation.is_a?(Nodes::FieldNode) &&
98
- node.lh_operation.field_path.length == 1 &&
99
- node.lh_operation.field_path[0].to_s == hash_key
100
- end
101
-
102
- def extract_range_key_node
103
- return unless extract_query_filter_node
104
-
105
- @extract_range_key_node ||=
106
- case expression_node
107
- when Nodes::AndNode
108
- id_filters = expression_node.child_nodes.select do |i|
109
- field_node_valid_for_range_filter(i)
110
- end
111
-
112
- if id_filters.length == 1
113
- self.expression_node = Nodes::AndNode.new(
114
- *(expression_node.child_nodes - id_filters)
115
- )
116
-
117
- id_filters.first
118
- end
119
- end
120
- end
121
-
122
- def field_node_valid_for_range_filter(node)
123
- node.respond_to?(:lh_operation) &&
124
- node.lh_operation.is_a?(Nodes::FieldNode) &&
125
- node.lh_operation.field_path.length == 1 &&
126
- node.lh_operation.field_path[0].to_s == range_key &&
127
- VALID_COMPARETORS_FOR_RANGE_FILTER.any? { |type| node.is_a?(type) }
128
- end
129
-
130
- def hash_key
131
- @hash_key ||= klass.hash_key.to_s
132
- end
133
-
134
- def range_key
135
- @range_key ||= klass.range_key.to_s
136
- end
137
97
  end
138
98
  end
@@ -12,7 +12,7 @@ module DynamoidAdvancedWhere
12
12
  def initialize(klass:, &blk)
13
13
  self.klass = klass
14
14
  evaluate_block(blk) if blk
15
- self.child_node ||= NullNode.new
15
+ self.child_node ||= AndNode.new
16
16
  freeze
17
17
  end
18
18
 
@@ -6,15 +6,16 @@ require_relative './batched_updater'
6
6
 
7
7
  module DynamoidAdvancedWhere
8
8
  class QueryBuilder
9
- attr_accessor :klass, :root_node, :start_hash, :record_limit
9
+ attr_accessor :klass, :root_node, :start_hash, :record_limit, :projected_fields
10
10
 
11
11
  delegate :all, :each_page, :each, to: :query_materializer
12
12
 
13
- def initialize(klass:, record_limit: nil, start_hash: nil, root_node: nil, &blk)
13
+ def initialize(klass:, projected_fields: [], record_limit: nil, start_hash: nil, root_node: nil, &blk)
14
14
  self.klass = klass
15
15
  self.root_node = root_node || Nodes::RootNode.new(klass: klass, &blk)
16
16
  self.start_hash = start_hash
17
17
  self.record_limit = record_limit
18
+ self.projected_fields = projected_fields
18
19
 
19
20
  freeze
20
21
  end
@@ -49,6 +50,10 @@ module DynamoidAdvancedWhere
49
50
  end
50
51
  alias and where
51
52
 
53
+ def project(*fields)
54
+ dup_with_changes(projected_fields: projected_fields + fields)
55
+ end
56
+
52
57
  def limit(value)
53
58
  dup_with_changes(record_limit: value)
54
59
  end
@@ -68,6 +73,7 @@ module DynamoidAdvancedWhere
68
73
  klass: klass,
69
74
  start_hash: start_hash,
70
75
  root_node: root_node,
76
+ projected_fields: projected_fields,
71
77
  }.merge(changes))
72
78
  end
73
79
  end
@@ -11,8 +11,6 @@ module DynamoidAdvancedWhere
11
11
  delegate :table_name, to: :klass
12
12
  delegate :to_a, :first, to: :each
13
13
 
14
- delegate :must_scan?, to: :filter_builder
15
-
16
14
  def initialize(query_builder:)
17
15
  self.query_builder = query_builder
18
16
  end
@@ -46,10 +44,13 @@ module DynamoidAdvancedWhere
46
44
  end
47
45
  end
48
46
 
49
- def each_page_via_query
50
- query = {
51
- table_name: table_name,
52
- }.merge(filter_builder.to_query_filter)
47
+ def enumerate_results(starting_query)
48
+ query = starting_query.dup
49
+
50
+ unless query_builder.projected_fields.empty?
51
+ query[:select] = 'SPECIFIC_ATTRIBUTES'
52
+ query[:projection_expression] = query_builder.projected_fields.map(&:to_s).join(',')
53
+ end
53
54
 
54
55
  query[:limit] = query_builder.record_limit if query_builder.record_limit
55
56
 
@@ -57,9 +58,10 @@ module DynamoidAdvancedWhere
57
58
 
58
59
  Enumerator.new do |yielder|
59
60
  loop do
60
- results = client.query(query.merge(exclusive_start_key: page_start))
61
+ query[:exclusive_start_key] = page_start
62
+ results = yield(query)
61
63
 
62
- items = (results.items || []).each do |item|
64
+ items = (results.items || []).map do |item|
63
65
  klass.from_database(item.symbolize_keys)
64
66
  end
65
67
 
@@ -74,39 +76,82 @@ module DynamoidAdvancedWhere
74
76
  end.lazy
75
77
  end
76
78
 
79
+ def each_page_via_query
80
+ query = {
81
+ table_name: table_name,
82
+ index_name: selected_index_for_query,
83
+ }.merge(filter_builder.to_query_filter)
84
+
85
+ enumerate_results(query) do |q|
86
+ client.query(q)
87
+ end
88
+ end
89
+
77
90
  def each_page_via_scan
78
91
  query = {
79
92
  table_name: table_name,
80
93
  }.merge(filter_builder.to_scan_filter)
81
94
 
82
- query[:limit] = query_builder.record_limit if query_builder.record_limit
95
+ enumerate_results(query) do |q|
96
+ client.scan(q)
97
+ end
98
+ end
83
99
 
84
- page_start = start_hash
100
+ def filter_builder
101
+ @filter_builder ||= FilterBuilder.new(
102
+ root_node: query_builder.root_node,
103
+ klass: klass,
104
+ )
105
+ end
85
106
 
86
- Enumerator.new do |yielder|
87
- loop do
88
- results = client.scan(query.merge(exclusive_start_key: page_start))
107
+ # Pick the index to query.
108
+ # 1) The first index chosen should be one that has the range and hash key satisfied.
109
+ # 2) The second should be one that has the hash key
110
+ def selected_index_for_query
111
+ possible_fields = filter_builder.extractable_fields_for_hash_and_range
89
112
 
90
- items = (results.items || []).map do |item|
91
- klass.from_database(item.symbolize_keys)
92
- end
113
+ satisfiable_indexes.each do |name, definition|
114
+ next unless possible_fields.key?(definition[:hash_key]) &&
115
+ possible_fields.key?(definition[:range_key])
93
116
 
94
- yielder.yield(items, results)
117
+ filter_builder.select_node_for_range_key(possible_fields[definition[:range_key]])
118
+ filter_builder.select_node_for_query_filter(possible_fields[definition[:hash_key]])
95
119
 
96
- query[:limit] = query[:limit] - results.items.length if query[:limit]
120
+ return name
121
+ end
97
122
 
98
- break if results.last_evaluated_key.nil? || query[:limit]&.zero?
123
+ # Just take the first matching query then
124
+ name, definition = satisfiable_indexes.first
125
+ filter_builder.select_node_for_query_filter(possible_fields[definition[:hash_key]])
126
+ filter_builder.select_node_for_range_key(possible_fields[definition[:range_key]]) unless possible_fields[definition[:range_key]].blank?
99
127
 
100
- (page_start = results.last_evaluated_key)
101
- end
102
- end.lazy
128
+ name
103
129
  end
104
130
 
105
- def filter_builder
106
- @filter_builder ||= FilterBuilder.new(
107
- root_node: query_builder.root_node,
108
- klass: klass,
109
- )
131
+ def must_scan?
132
+ satisfiable_indexes.empty?
133
+ end
134
+
135
+ # find all indexes where we have a predicate on the hash key
136
+ def satisfiable_indexes
137
+ possible_fields = filter_builder.extractable_fields_for_hash_and_range
138
+
139
+ all_possible_indexes.select do |_, definition|
140
+ possible_fields.key?(definition[:hash_key])
141
+ end
142
+ end
143
+
144
+ def all_possible_indexes
145
+ # The nil index name is the table itself
146
+ idx = { nil => { hash_key: klass.hash_key.to_s, range_key: klass.range_key.to_s } }
147
+
148
+ klass.indexes.each do |_, definition|
149
+ next unless definition.projected_attributes == :all
150
+
151
+ idx[definition.name] = { hash_key: definition.hash_key.to_s, range_key: definition.range_key.to_s }
152
+ end
153
+
154
+ idx
110
155
  end
111
156
 
112
157
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamoidAdvancedWhere
4
- VERSION = '1.5.1'
4
+ VERSION = '1.7.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamoid_advanced_where
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Malinconico
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-13 00:00:00.000000000 Z
11
+ date: 2023-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dynamoid