elasticgraph-graphql 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
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