dynamoid_advanced_where 1.5.1 → 1.7.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.
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