elasticgraph-graphql 0.18.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 (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +3 -0
  4. data/elasticgraph-graphql.gemspec +23 -0
  5. data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +79 -0
  6. data/lib/elastic_graph/graphql/aggregation/computation.rb +39 -0
  7. data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +83 -0
  8. data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +47 -0
  9. data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +26 -0
  10. data/lib/elastic_graph/graphql/aggregation/key.rb +87 -0
  11. data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +37 -0
  12. data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +129 -0
  13. data/lib/elastic_graph/graphql/aggregation/path_segment.rb +31 -0
  14. data/lib/elastic_graph/graphql/aggregation/query.rb +172 -0
  15. data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +345 -0
  16. data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +187 -0
  17. data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +41 -0
  18. data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +44 -0
  19. data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +30 -0
  20. data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +64 -0
  21. data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +83 -0
  22. data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +82 -0
  23. data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +32 -0
  24. data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +118 -0
  25. data/lib/elastic_graph/graphql/client.rb +43 -0
  26. data/lib/elastic_graph/graphql/config.rb +81 -0
  27. data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +100 -0
  28. data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +142 -0
  29. data/lib/elastic_graph/graphql/datastore_query/paginator.rb +199 -0
  30. data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +239 -0
  31. data/lib/elastic_graph/graphql/datastore_query.rb +372 -0
  32. data/lib/elastic_graph/graphql/datastore_response/document.rb +78 -0
  33. data/lib/elastic_graph/graphql/datastore_response/search_response.rb +79 -0
  34. data/lib/elastic_graph/graphql/datastore_search_router.rb +151 -0
  35. data/lib/elastic_graph/graphql/decoded_cursor.rb +120 -0
  36. data/lib/elastic_graph/graphql/filtering/boolean_query.rb +45 -0
  37. data/lib/elastic_graph/graphql/filtering/field_path.rb +81 -0
  38. data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +58 -0
  39. data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +526 -0
  40. data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +148 -0
  41. data/lib/elastic_graph/graphql/filtering/range_query.rb +56 -0
  42. data/lib/elastic_graph/graphql/http_endpoint.rb +229 -0
  43. data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +56 -0
  44. data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +48 -0
  45. data/lib/elastic_graph/graphql/query_adapter/filters.rb +161 -0
  46. data/lib/elastic_graph/graphql/query_adapter/pagination.rb +27 -0
  47. data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +124 -0
  48. data/lib/elastic_graph/graphql/query_adapter/sort.rb +32 -0
  49. data/lib/elastic_graph/graphql/query_details_tracker.rb +60 -0
  50. data/lib/elastic_graph/graphql/query_executor.rb +200 -0
  51. data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +49 -0
  52. data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +114 -0
  53. data/lib/elastic_graph/graphql/resolvers/list_records.rb +29 -0
  54. data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +74 -0
  55. data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +85 -0
  56. data/lib/elastic_graph/graphql/resolvers/query_source.rb +46 -0
  57. data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +71 -0
  58. data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +65 -0
  59. data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +82 -0
  60. data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +40 -0
  61. data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +42 -0
  62. data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +56 -0
  63. data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +35 -0
  64. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +64 -0
  65. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +60 -0
  66. data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +30 -0
  67. data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +47 -0
  68. data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +24 -0
  69. data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +44 -0
  70. data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +32 -0
  71. data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +634 -0
  72. data/lib/elastic_graph/graphql/schema/arguments.rb +78 -0
  73. data/lib/elastic_graph/graphql/schema/enum_value.rb +30 -0
  74. data/lib/elastic_graph/graphql/schema/field.rb +147 -0
  75. data/lib/elastic_graph/graphql/schema/relation_join.rb +103 -0
  76. data/lib/elastic_graph/graphql/schema/type.rb +263 -0
  77. data/lib/elastic_graph/graphql/schema.rb +164 -0
  78. data/lib/elastic_graph/graphql.rb +253 -0
  79. data/script/dump_time_zones +81 -0
  80. data/script/dump_time_zones.java +17 -0
  81. metadata +503 -0
@@ -0,0 +1,199 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/error"
10
+ require "elastic_graph/support/memoizable_data"
11
+
12
+ module ElasticGraph
13
+ class GraphQL
14
+ class DatastoreQuery
15
+ # A generic pagination implementation, designed to handle both document pagination and
16
+ # aggregation pagination. Not tested directly; tests drive the `Query` interface instead.
17
+ #
18
+ # Our pagination support is designed to support Facebook's Relay Cursor Connections Spec.
19
+ # The description of the pagination algorithm is directly implemented by this class:
20
+ #
21
+ # https://facebook.github.io/relay/graphql/connections.htm#sec-Pagination-algorithm
22
+ #
23
+ # As described by the spec, we support 4 pagination arguments, and apply them in this order:
24
+ #
25
+ # - `after`: items with a cursor value on or before this value are excluded
26
+ # - `before`: items with a cursor value on or after this value are excluded
27
+ # - `first`: after applying before/after, all but the first `N` items are excluded
28
+ # - `last`: after applying before/after/first, all but the last `N` items are excluded
29
+ #
30
+ # Note that `first` is applied before `last`, meaning that when both are provided (as in
31
+ # `first: 10, last: 4`) it is interpreted as "the last 4 of the first 10". However, the Relay
32
+ # spec itself discourages clients from passing both, but servers must still support it:
33
+ #
34
+ # > Including a value for both first and last is strongly discouraged, as it is likely to lead
35
+ # > to confusing queries and results.
36
+ #
37
+ # For document pagination, the relay semantics are implemented on top of Elasticsearch/OpenSearch's `search_after` feature:
38
+ #
39
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-search-after.html
40
+ #
41
+ # For aggregation pagination, the relay semantics are implemented on top of the composite aggregation
42
+ # pagination feature:
43
+ #
44
+ # https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations-bucket-composite-aggregation.html#_pagination
45
+ #
46
+ # In either case, the `search_after` (or `after`) argument is directly analogous to Relay's `after`.
47
+ # To support the full Relay spec, we have to do some additional clever things:
48
+ #
49
+ # - When necessary (such as for `last: 50, before: some_cursor`), we have to _reverse_ the
50
+ # sort, perform the query with a size of `last`, and then reverse the returned items
51
+ # to the originally requested order.
52
+ # - In some cases, we have to apply `after`, `before` or `last` as a post-processing step
53
+ # to the items returned by the datastore.
54
+ #
55
+ # Note, however, that the sort key data type used for these two cases is a bit different:
56
+ #
57
+ # - For document pagination, `search_after` is a list of scalar values, corresponding to the order
58
+ # of `sort` clauses. That is, if we are sorting on `amount` ascending and `createdAt` descending,
59
+ # then the `search_after` value (and the `sort` value of each document) will be an
60
+ # `[amount, createdAt]` tuple.
61
+ # - For aggregation pagination, `after` (and the `key` of each aggregation bucket is an unordered
62
+ # hash of sort values. The sort field order is instead implied by the composite aggregation
63
+ # `sources`.
64
+ class Paginator < Support::MemoizableData.define(:default_page_size, :max_page_size, :first, :after, :last, :before, :schema_element_names)
65
+ # These methods are provided by `Data.define`:
66
+ # @dynamic default_page_size, max_page_size, first, after, last, before, schema_element_names, initialize
67
+
68
+ def requested_page_size
69
+ # `+ 1` so we can tell if there are more docs for `has_next_page`/`has_previous_page`
70
+ # ...but only if we need to get anything at all.
71
+ (desired_page_size == 0) ? 0 : desired_page_size + 1
72
+ end
73
+
74
+ # Indicates if we need to search in reverse or not in order to satisfy the Relay pagination args.
75
+ # If searching in reverse is necessary, `process_items_and_build_page_info` will take care of
76
+ # reversing the reversed results back to their original order.
77
+ def search_in_reverse?
78
+ # If `first` has been provided then we _must not_ search in reverse.
79
+ # The relay spec requires us to apply `first` before `last`, and searching
80
+ # in reverse would prevent us from being able to return the first `N`.
81
+ return false if first_n
82
+
83
+ # If we do not have to return the first N results, we are free to search in
84
+ # reverse if needed. Either `last` or `before` requires it.
85
+ last_n || before
86
+ end
87
+
88
+ # The cursor values to search after (if we need to search after one at all).
89
+ def search_after
90
+ search_in_reverse? ? before : after
91
+ end
92
+
93
+ # In some cases, we're forced to search in reverse; in those caes, this is used to restore
94
+ # the ordering of the items to the intended order.
95
+ def restore_intended_item_order(items)
96
+ search_in_reverse? ? items.reverse : items
97
+ end
98
+
99
+ # Used for post-processing a list of items from a search result, truncating the list as needed. Truncation
100
+ # may be necessary because we may request an extra item as part of our pagination implementation.
101
+ def truncate_items(items)
102
+ # Remove the extra doc we requested by doing `size: size + 1`, if an extra was returned.
103
+ # Removing the first or last doc (as this will do) will signal to `bulid_page_info`
104
+ # that there definitely is a previous or next page.
105
+ # Note: we use `to_a` to satisfy steep, since `Array#[]` can return `nil`--but with the arg
106
+ # we pass, never does when items is non-empty, which our conditional enforces here.
107
+ items = items[search_in_reverse? ? 1..-1 : 0...-1].to_a if items.size > desired_page_size
108
+
109
+ # We can't always use `before` and `after` in the datastore query (such as when both are provided!),
110
+ # so here we drop items from the start that come on or before `after`, and items from the
111
+ # end that come on or after `before`.
112
+ if (after_cursor = after)
113
+ items = items.drop_while do |doc|
114
+ item_sort_values_satisfy?(yield(doc, after_cursor), :<=)
115
+ end
116
+ end
117
+
118
+ if (before_cursor = before)
119
+ items = items.take_while do |doc|
120
+ item_sort_values_satisfy?(yield(doc, before_cursor), :<)
121
+ end
122
+ end
123
+
124
+ # We are not always able to use `last` as the query `size` (such as when `first` is also provided)
125
+ # so here we apply `last`. If it has already been used this line will be a no-op.
126
+ items = (_ = items).last(last_n.to_i) if last_n
127
+ items
128
+ end
129
+
130
+ def paginated_from_singleton_cursor?
131
+ before == DecodedCursor::SINGLETON || after == DecodedCursor::SINGLETON
132
+ end
133
+
134
+ def desired_page_size
135
+ # The relay spec requires us to apply `first` before `last`, but if neither
136
+ # is provided we fall back to `default_page_size`.
137
+ @desired_page_size ||= [first_n || last_n || default_page_size, max_page_size].min.to_i
138
+ end
139
+
140
+ private
141
+
142
+ def first_n
143
+ @first_n ||= size_arg_value(:first, first)
144
+ end
145
+
146
+ def last_n
147
+ @last_n ||= size_arg_value(:last, last)
148
+ end
149
+
150
+ def size_arg_value(arg_name, value)
151
+ if value && value < 0
152
+ raise ::GraphQL::ExecutionError, "`#{schema_element_names.public_send(arg_name)}` cannot be negative, but is #{value}."
153
+ else
154
+ value
155
+ end
156
+ end
157
+
158
+ # A bit like `Array#<=>`, but understands ascending vs descending sorts.
159
+ # We can't simply use doc_sort_values <=> cursor_sort_values` because our
160
+ # sort might mix ascending and descending sorts. So, we have to go value-by-value
161
+ # and compare each.
162
+ def item_sort_values_satisfy?(sort_values, comparison_operator)
163
+ if (first_unequal_sort_value = sort_values.find(&:unequal?))
164
+ # Since each subsequent sort field is a tie breaker that only gets used if two documents
165
+ # have the same values for all the prior sort fields, as soon as we find a sort value that
166
+ # is unequal we can just do the comparison based on it.
167
+ first_unequal_sort_value.item_satisfies_compared_to_cursor?(comparison_operator)
168
+ else
169
+ # The doc values and cursor values are all exactly equal. Return true or false on
170
+ # the basis of whether or not the comparison operator allows exact equality.
171
+ comparison_operator == :<= || comparison_operator == :>=
172
+ end
173
+ end
174
+
175
+ SortValue = ::Data.define(:from_item, :from_cursor, :sort_direction) do
176
+ # @implements SortValue
177
+ def unequal?
178
+ from_item != from_cursor
179
+ end
180
+
181
+ def item_satisfies_compared_to_cursor?(comparison_operator)
182
+ if from_item.nil?
183
+ # nil values sort first when sorting ascending, and last when sorting descending.
184
+ # (see `DocumentPaginator#sort` for a more thorough explanation).
185
+ sort_direction == :asc
186
+ elsif from_cursor.nil?
187
+ # nil values sort first when sorting ascending, and last when sorting descending.
188
+ # (see `DocumentPaginator#sort` for a more thorough explanation).
189
+ sort_direction == :desc
190
+ else # both `from_item` and `from_cursor` are non-nil, and can be compared.
191
+ result = from_item.public_send(comparison_operator, from_cursor)
192
+ (sort_direction == :asc) ? result : !result
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,239 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/graphql/filtering/filter_value_set_extractor"
10
+
11
+ module ElasticGraph
12
+ class GraphQL
13
+ class DatastoreQuery
14
+ # Responsible for picking routing values for a specific query based on the filters.
15
+ class RoutingPicker
16
+ def initialize(schema_names:)
17
+ # @type var all_values_set: _RoutingValueSet
18
+ all_values_set = RoutingValueSet::ALL
19
+
20
+ @filter_value_set_extractor = Filtering::FilterValueSetExtractor.new(schema_names, all_values_set) do |operator, filter_value|
21
+ if operator == :equal_to_any_of
22
+ # This calls `.compact` to remove `nil` filter_value values
23
+ RoutingValueSet.of(filter_value.compact)
24
+ else # gt, lt, gte, lte, matches
25
+ # With one of these inexact/inequality operators, we don't have a way to precisely represent
26
+ # the set of values. Instead, we represent it with the special UnboundedWithExclusions
27
+ # implementation since when these operators are used the set is unbounded (there's an infinite
28
+ # number of values in the set) but it doesn't contain all values (it has some exclusions).
29
+ RoutingValueSet::UnboundedWithExclusions
30
+ end
31
+ end
32
+ end
33
+
34
+ # Given a list of `filter_hashes` and a list of `routing_field_paths`, returns a list of
35
+ # routing values that can safely be used to limit what index shards we search
36
+ # without risking missing any matching documents that could exist on other shards.
37
+ #
38
+ # If an eligible list of routing values cannot be determined, returns `nil`.
39
+ #
40
+ # Importantly, we have to be careful to not return routing values unless we are 100% sure
41
+ # that the set of values will route to the full set of shards on which documents matching
42
+ # the filters could live. If a document matching the filters lived on a shard that our
43
+ # search does not route to, it will not be included in the search response.
44
+ #
45
+ # Essentially, this method guarantees that the following pseudo code is always satisfied:
46
+ #
47
+ # ``` ruby
48
+ # if (routing_values = extract_eligible_routing_values(filter_hashes, routing_field_paths))
49
+ # Datastore.all_documents_matching(filter_hashes).each do |document|
50
+ # routing_field_paths.each do |field_path|
51
+ # expect(routing_values).to include(document.value_at(field_path))
52
+ # end
53
+ # end
54
+ # end
55
+ # ```
56
+ def extract_eligible_routing_values(filter_hashes, routing_field_paths)
57
+ @filter_value_set_extractor.extract_filter_value_set(filter_hashes, routing_field_paths).to_return_value
58
+ end
59
+ end
60
+
61
+ class RoutingValueSet < Data.define(:type, :routing_values)
62
+ # @dynamic ==
63
+
64
+ def self.of(routing_values)
65
+ new(:inclusive, routing_values.to_set)
66
+ end
67
+
68
+ def self.of_all_except(routing_values)
69
+ new(:exclusive, routing_values.to_set)
70
+ end
71
+
72
+ ALL = of_all_except([])
73
+
74
+ def intersection(other_set)
75
+ # Here we return `self` to preserve the commutative property of `intersection`. Returning `self`
76
+ # here matches the behavior of `UnboundedWithExclusions.intersection`. See the comment there for
77
+ # rationale.
78
+ return self if other_set == UnboundedWithExclusions
79
+
80
+ # @type var other: RoutingValueSet
81
+ other = _ = other_set
82
+
83
+ if inclusive? && other.inclusive?
84
+ # Since both sets are inclusive, we can just delegate to `Set#intersection` here.
85
+ RoutingValueSet.of(routing_values.intersection(other.routing_values))
86
+ elsif exclusive? && other.exclusive?
87
+ # Since both sets are exclusive, we need to return an exclusive set of the union of the
88
+ # excluded values. For example, when dealing with positive integers:
89
+ #
90
+ # s1 = RoutingValueSet.of_all_except([1, 2, 3]) # > 3
91
+ # s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
92
+ #
93
+ # s3 = s1.intersection(s2)
94
+ #
95
+ # Here s3 would be all values > 5 (the same as `RoutingValueSet.of_all_except([1, 2, 3, 4, 5])`)
96
+ RoutingValueSet.of_all_except(routing_values.union(other.routing_values))
97
+ else
98
+ # Since one set is inclusive and one set is exclusive, we need to return an inclusive set of
99
+ # `included_values - excluded_values`. For example, when dealing with positive integers:
100
+ #
101
+ # s1 = RoutingValueSet.of([1, 2, 3]) # 1, 2, 3
102
+ # s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
103
+ #
104
+ # s3 = s1.intersection(s2)
105
+ #
106
+ # Here s3 would be just `1, 2`.
107
+ included_values, excluded_values = get_included_and_excluded_values(other)
108
+ RoutingValueSet.of(included_values - excluded_values)
109
+ end
110
+ end
111
+
112
+ def union(other_set)
113
+ # Here we return `other` to preserve the commutative property of `union`. Returning `other`
114
+ # here matches the behavior of `UnboundedWithExclusions.union`. See the comment there for
115
+ # rationale.
116
+ return other_set if other_set == UnboundedWithExclusions
117
+
118
+ # @type var other: RoutingValueSet
119
+ other = _ = other_set
120
+
121
+ if inclusive? && other.inclusive?
122
+ # Since both sets are inclusive, we can just delegate to `Set#union` here.
123
+ RoutingValueSet.of(routing_values.union(other.routing_values))
124
+ elsif exclusive? && other.exclusive?
125
+ # Since both sets are exclusive, we need to return an exclusive set of the intersection of the
126
+ # excluded values. For example, when dealing with positive integers:
127
+ #
128
+ # s1 = RoutingValueSet.of_all_except([1, 2, 3]) # > 3
129
+ # s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
130
+ #
131
+ # s3 = s1.union(s2)
132
+ #
133
+ # Here s3 would be all 1, 2, > 3 (the same as `RoutingValueSet.of_all_except([3])`)
134
+ RoutingValueSet.of_all_except(routing_values.intersection(other.routing_values))
135
+ else
136
+ # Since one set is inclusive and one set is exclusive, we need to return an exclusive set of
137
+ # `excluded_values - included_values`. For example, when dealing with positive integers:
138
+ #
139
+ # s1 = RoutingValueSet.of([1, 2, 3]) # 1, 2, 3
140
+ # s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
141
+ #
142
+ # s3 = s1.union(s2)
143
+ #
144
+ # Here s3 would be 1, 2, 3, > 5 (the same as `RoutingValueSet.of_all_except([4, 5])`)
145
+ included_values, excluded_values = get_included_and_excluded_values(other)
146
+ RoutingValueSet.of_all_except(excluded_values - included_values)
147
+ end
148
+ end
149
+
150
+ def negate
151
+ with(type: INVERTED_TYPES.fetch(type))
152
+ end
153
+
154
+ INVERTED_TYPES = {inclusive: :exclusive, exclusive: :inclusive}
155
+
156
+ def to_return_value
157
+ # Elasticsearch/OpenSearch have no routing value syntax to tell it to avoid searching a specific shard
158
+ # (and the fact that we are excluding a routing value doesn't mean that other documents that
159
+ # live on the same shard with different routing values can't match!) so we return `nil` to
160
+ # force the datastore to search all shards.
161
+ return nil if exclusive?
162
+
163
+ routing_values.to_a
164
+ end
165
+
166
+ protected
167
+
168
+ def inclusive?
169
+ type == :inclusive
170
+ end
171
+
172
+ def exclusive?
173
+ type == :exclusive
174
+ end
175
+
176
+ private
177
+
178
+ def get_included_and_excluded_values(other)
179
+ inclusive? ? [routing_values, other.routing_values] : [other.routing_values, routing_values]
180
+ end
181
+
182
+ # This `RoutingValueSet` implementation is used for otherwise unrepresentable cases. We use it when
183
+ # a filter on one of the `routing_field_paths` uses an inequality like:
184
+ #
185
+ # {routing_field: {gt: "abc"}}
186
+ #
187
+ # In a case like that, the set is unbounded (there's an infinite number of values that are greater
188
+ # than `"abc"`...), but it's not `RoutingValueSet::ALL`--since it's based on an inequality, there are
189
+ # _some_ values that are excluded from the set. But we can't use `RoutingValueSet.of_all_except(...)`
190
+ # because the set of exclusions is also unbounded!
191
+ #
192
+ # When our filter value extraction results in this set, we must search all shards of the index and
193
+ # cannot pass any `routing` value to the datastore at all.
194
+ module UnboundedWithExclusions
195
+ # @dynamic self.==
196
+
197
+ def self.intersection(other)
198
+ # Technically, the "true" intersection would be `other - values_of(self)` but as we don't have
199
+ # any known values from this unbounded set, we just return `other`. It's OK to include extra values
200
+ # in the set (we'll search additional shards) but not OK to fail to include necessary values in
201
+ # the set (we'd avoid searching a shard that may have matching documents) so we err on the side of
202
+ # including more values.
203
+ other
204
+ end
205
+
206
+ def self.union(other)
207
+ # Since our set here is unbounded, the resulting union is also unbounded. This errs on the side
208
+ # of safety since this set's `to_return_value` returns `nil` to cause the datastore to search
209
+ # all shards.
210
+ self
211
+ end
212
+
213
+ def self.negate
214
+ # This here is the only difference in behavior of this set implementation vs `RoutingValueSet::ALL`.
215
+ # Where as `ALL.negate` returns an empty set, we treat `negate` as a no-op. We do that because the
216
+ # negation of an inexact unbounded set is still an inexact unbounded set. While it flips which values
217
+ # are in or out of the set, this object is still the representation in our datamodel for that case.
218
+ self
219
+ end
220
+
221
+ def self.to_return_value
222
+ # Here we return `nil` to make sure that the datastore searches all shards, since we don't have
223
+ # any information we can use to safely limit what shards it searches.
224
+ nil
225
+ end
226
+ end
227
+ end
228
+
229
+ # `Query::RoutingPicker` exists only for use by `Query` and is effectively private.
230
+ private_constant :RoutingPicker
231
+ # `RoutingValueSet` exists only for use here and is effectively private.
232
+ private_constant :RoutingValueSet
233
+
234
+ # Steep is complaining that it can't find some `Query` but they are not in this file...
235
+ # @dynamic aggregations, shard_routing_values, search_index_definitions, merge_with, search_index_expression
236
+ # @dynamic with, to_datastore_msearch_header_and_body, document_paginator
237
+ end
238
+ end
239
+ end