elasticgraph-graphql 0.18.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/elasticgraph-graphql.gemspec +23 -0
- data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +79 -0
- data/lib/elastic_graph/graphql/aggregation/computation.rb +39 -0
- data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +83 -0
- data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +47 -0
- data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +26 -0
- data/lib/elastic_graph/graphql/aggregation/key.rb +87 -0
- data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +37 -0
- data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +129 -0
- data/lib/elastic_graph/graphql/aggregation/path_segment.rb +31 -0
- data/lib/elastic_graph/graphql/aggregation/query.rb +172 -0
- data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +345 -0
- data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +187 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +41 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +44 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +30 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +64 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +83 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +82 -0
- data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +32 -0
- data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +118 -0
- data/lib/elastic_graph/graphql/client.rb +43 -0
- data/lib/elastic_graph/graphql/config.rb +81 -0
- data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +100 -0
- data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +142 -0
- data/lib/elastic_graph/graphql/datastore_query/paginator.rb +199 -0
- data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +239 -0
- data/lib/elastic_graph/graphql/datastore_query.rb +372 -0
- data/lib/elastic_graph/graphql/datastore_response/document.rb +78 -0
- data/lib/elastic_graph/graphql/datastore_response/search_response.rb +79 -0
- data/lib/elastic_graph/graphql/datastore_search_router.rb +151 -0
- data/lib/elastic_graph/graphql/decoded_cursor.rb +120 -0
- data/lib/elastic_graph/graphql/filtering/boolean_query.rb +45 -0
- data/lib/elastic_graph/graphql/filtering/field_path.rb +81 -0
- data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +58 -0
- data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +526 -0
- data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +148 -0
- data/lib/elastic_graph/graphql/filtering/range_query.rb +56 -0
- data/lib/elastic_graph/graphql/http_endpoint.rb +229 -0
- data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +56 -0
- data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +48 -0
- data/lib/elastic_graph/graphql/query_adapter/filters.rb +161 -0
- data/lib/elastic_graph/graphql/query_adapter/pagination.rb +27 -0
- data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +124 -0
- data/lib/elastic_graph/graphql/query_adapter/sort.rb +32 -0
- data/lib/elastic_graph/graphql/query_details_tracker.rb +60 -0
- data/lib/elastic_graph/graphql/query_executor.rb +200 -0
- data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +49 -0
- data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +114 -0
- data/lib/elastic_graph/graphql/resolvers/list_records.rb +29 -0
- data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +74 -0
- data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +85 -0
- data/lib/elastic_graph/graphql/resolvers/query_source.rb +46 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +71 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +65 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +82 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +40 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +42 -0
- data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +56 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +35 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +64 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +60 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +30 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +47 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +24 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +44 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +32 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +634 -0
- data/lib/elastic_graph/graphql/schema/arguments.rb +78 -0
- data/lib/elastic_graph/graphql/schema/enum_value.rb +30 -0
- data/lib/elastic_graph/graphql/schema/field.rb +147 -0
- data/lib/elastic_graph/graphql/schema/relation_join.rb +103 -0
- data/lib/elastic_graph/graphql/schema/type.rb +263 -0
- data/lib/elastic_graph/graphql/schema.rb +164 -0
- data/lib/elastic_graph/graphql.rb +253 -0
- data/script/dump_time_zones +81 -0
- data/script/dump_time_zones.java +17 -0
- 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
|