elasticgraph-graphql 0.19.0.0.rc2 → 0.19.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.
- checksums.yaml +4 -4
- data/elasticgraph-graphql.gemspec +1 -1
- data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +1 -1
- data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +9 -2
- data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +10 -76
- data/lib/elastic_graph/graphql/datastore_query.rb +11 -5
- data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +1 -1
- data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +0 -10
- data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +12 -15
- data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +66 -15
- data/lib/elastic_graph/graphql/http_endpoint.rb +1 -1
- data/lib/elastic_graph/graphql/query_adapter/filters.rb +8 -2
- data/lib/elastic_graph/graphql/resolvers/query_source.rb +1 -1
- data/lib/elastic_graph/graphql.rb +4 -3
- metadata +27 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87e3b12d1297421b2631906b7c682d38f87d4111fc9929d56d2be55f6cc481fe
|
4
|
+
data.tar.gz: bb454a309d0869c7c0d446d5ca2882cda4d005e412966db327e3dab56c0916f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 803c34dfff0d787e61987f5d1877ea93485180161d3f1fc730bf6989d52f17e929c7fe476b434d4d59d59acf86622c5f68e6b2ed2f050df4abcdc13257c33bb8
|
7
|
+
data.tar.gz: 505707f3dd8bab37cc388cc542c343d4a541f5354c8e192d131a3cff36096e2d1ad9578872a68b169806f476acf0bdfd46d4db5389bb8e1dce09a984745265df
|
@@ -13,7 +13,7 @@ ElasticGraphGemspecHelper.define_elasticgraph_gem(gemspec_file: __FILE__, catego
|
|
13
13
|
|
14
14
|
spec.add_dependency "elasticgraph-datastore_core", eg_version
|
15
15
|
spec.add_dependency "elasticgraph-schema_artifacts", eg_version
|
16
|
-
spec.add_dependency "graphql", "~> 2.4.
|
16
|
+
spec.add_dependency "graphql", "~> 2.4.8"
|
17
17
|
|
18
18
|
spec.add_development_dependency "elasticgraph-admin", eg_version
|
19
19
|
spec.add_development_dependency "elasticgraph-elasticsearch", eg_version
|
@@ -154,7 +154,7 @@ module ElasticGraph
|
|
154
154
|
# If there are no aggregations, there's nothing to unmerge--just return it as is.
|
155
155
|
return response_from_merged_query unless (aggs = response_from_merged_query["aggregations"])
|
156
156
|
|
157
|
-
prefix = @unique_prefix_by_query[original_query]
|
157
|
+
prefix = @unique_prefix_by_query[original_query] # : ::String
|
158
158
|
agg_names = original_query.aggregations.keys.map { |name| "#{prefix}#{name}" }.to_set
|
159
159
|
|
160
160
|
filtered_aggs = aggs
|
@@ -14,8 +14,13 @@ module ElasticGraph
|
|
14
14
|
class DatastoreQuery
|
15
15
|
# Responsible for building a search index expression for a specific query based on the filters.
|
16
16
|
class IndexExpressionBuilder
|
17
|
-
def initialize(schema_names:)
|
18
|
-
@filter_value_set_extractor = Filtering::FilterValueSetExtractor.new(
|
17
|
+
def initialize(filter_node_interpreter:, schema_names:)
|
18
|
+
@filter_value_set_extractor = Filtering::FilterValueSetExtractor.new(
|
19
|
+
filter_node_interpreter,
|
20
|
+
schema_names,
|
21
|
+
Support::TimeSet::ALL,
|
22
|
+
Support::TimeSet::EMPTY
|
23
|
+
) do |operator, filter_value|
|
19
24
|
case operator
|
20
25
|
when :gt, :gte, :lt, :lte
|
21
26
|
if date_string?(filter_value)
|
@@ -73,6 +78,8 @@ module ElasticGraph
|
|
73
78
|
|
74
79
|
time_set = @filter_value_set_extractor.extract_filter_value_set(filter_hashes, [index_def.timestamp_field_path])
|
75
80
|
|
81
|
+
return IndexExpression.only(index_def.index_expression_for_search) if time_set.nil?
|
82
|
+
|
76
83
|
if time_set.empty?
|
77
84
|
return require_indices ?
|
78
85
|
# Indices are required. Given the time set is empty, it's impossible for any documents to match our search.
|
@@ -13,21 +13,19 @@ module ElasticGraph
|
|
13
13
|
class DatastoreQuery
|
14
14
|
# Responsible for picking routing values for a specific query based on the filters.
|
15
15
|
class RoutingPicker
|
16
|
-
def initialize(schema_names:)
|
17
|
-
# @type var all_values_set: _RoutingValueSet
|
16
|
+
def initialize(filter_node_interpreter:, schema_names:)
|
18
17
|
all_values_set = RoutingValueSet::ALL
|
19
18
|
empty_set = RoutingValueSet::EMPTY
|
20
19
|
|
21
|
-
@filter_value_set_extractor = Filtering::FilterValueSetExtractor.new(
|
20
|
+
@filter_value_set_extractor = Filtering::FilterValueSetExtractor.new(
|
21
|
+
filter_node_interpreter,
|
22
|
+
schema_names,
|
23
|
+
all_values_set,
|
24
|
+
empty_set
|
25
|
+
) do |operator, filter_value|
|
22
26
|
if operator == :equal_to_any_of
|
23
27
|
# This calls `.compact` to remove `nil` filter_value values
|
24
28
|
RoutingValueSet.of(filter_value.compact)
|
25
|
-
else # gt, lt, gte, lte, matches
|
26
|
-
# With one of these inexact/inequality operators, we don't have a way to precisely represent
|
27
|
-
# the set of values. Instead, we represent it with the special UnboundedWithExclusions
|
28
|
-
# implementation since when these operators are used the set is unbounded (there's an infinite
|
29
|
-
# number of values in the set) but it doesn't contain all values (it has some exclusions).
|
30
|
-
RoutingValueSet::UnboundedWithExclusions
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
@@ -55,13 +53,11 @@ module ElasticGraph
|
|
55
53
|
# end
|
56
54
|
# ```
|
57
55
|
def extract_eligible_routing_values(filter_hashes, routing_field_paths)
|
58
|
-
@filter_value_set_extractor.extract_filter_value_set(filter_hashes, routing_field_paths)
|
56
|
+
@filter_value_set_extractor.extract_filter_value_set(filter_hashes, routing_field_paths)&.to_return_value
|
59
57
|
end
|
60
58
|
end
|
61
59
|
|
62
60
|
class RoutingValueSet < Data.define(:type, :routing_values)
|
63
|
-
# @dynamic ==
|
64
|
-
|
65
61
|
def self.of(routing_values)
|
66
62
|
new(:inclusive, routing_values.to_set)
|
67
63
|
end
|
@@ -73,15 +69,7 @@ module ElasticGraph
|
|
73
69
|
ALL = of_all_except([])
|
74
70
|
EMPTY = of([])
|
75
71
|
|
76
|
-
def intersection(
|
77
|
-
# Here we return `self` to preserve the commutative property of `intersection`. Returning `self`
|
78
|
-
# here matches the behavior of `UnboundedWithExclusions.intersection`. See the comment there for
|
79
|
-
# rationale.
|
80
|
-
return self if other_set == UnboundedWithExclusions
|
81
|
-
|
82
|
-
# @type var other: RoutingValueSet
|
83
|
-
other = _ = other_set
|
84
|
-
|
72
|
+
def intersection(other)
|
85
73
|
if inclusive? && other.inclusive?
|
86
74
|
# Since both sets are inclusive, we can just delegate to `Set#intersection` here.
|
87
75
|
RoutingValueSet.of(routing_values.intersection(other.routing_values))
|
@@ -111,15 +99,7 @@ module ElasticGraph
|
|
111
99
|
end
|
112
100
|
end
|
113
101
|
|
114
|
-
def union(
|
115
|
-
# Here we return `other` to preserve the commutative property of `union`. Returning `other`
|
116
|
-
# here matches the behavior of `UnboundedWithExclusions.union`. See the comment there for
|
117
|
-
# rationale.
|
118
|
-
return other_set if other_set == UnboundedWithExclusions
|
119
|
-
|
120
|
-
# @type var other: RoutingValueSet
|
121
|
-
other = _ = other_set
|
122
|
-
|
102
|
+
def union(other)
|
123
103
|
if inclusive? && other.inclusive?
|
124
104
|
# Since both sets are inclusive, we can just delegate to `Set#union` here.
|
125
105
|
RoutingValueSet.of(routing_values.union(other.routing_values))
|
@@ -180,52 +160,6 @@ module ElasticGraph
|
|
180
160
|
def get_included_and_excluded_values(other)
|
181
161
|
inclusive? ? [routing_values, other.routing_values] : [other.routing_values, routing_values]
|
182
162
|
end
|
183
|
-
|
184
|
-
# This `RoutingValueSet` implementation is used for otherwise unrepresentable cases. We use it when
|
185
|
-
# a filter on one of the `routing_field_paths` uses an inequality like:
|
186
|
-
#
|
187
|
-
# {routing_field: {gt: "abc"}}
|
188
|
-
#
|
189
|
-
# In a case like that, the set is unbounded (there's an infinite number of values that are greater
|
190
|
-
# than `"abc"`...), but it's not `RoutingValueSet::ALL`--since it's based on an inequality, there are
|
191
|
-
# _some_ values that are excluded from the set. But we can't use `RoutingValueSet.of_all_except(...)`
|
192
|
-
# because the set of exclusions is also unbounded!
|
193
|
-
#
|
194
|
-
# When our filter value extraction results in this set, we must search all shards of the index and
|
195
|
-
# cannot pass any `routing` value to the datastore at all.
|
196
|
-
module UnboundedWithExclusions
|
197
|
-
# @dynamic self.==
|
198
|
-
|
199
|
-
def self.intersection(other)
|
200
|
-
# Technically, the "true" intersection would be `other - values_of(self)` but as we don't have
|
201
|
-
# any known values from this unbounded set, we just return `other`. It's OK to include extra values
|
202
|
-
# in the set (we'll search additional shards) but not OK to fail to include necessary values in
|
203
|
-
# the set (we'd avoid searching a shard that may have matching documents) so we err on the side of
|
204
|
-
# including more values.
|
205
|
-
other
|
206
|
-
end
|
207
|
-
|
208
|
-
def self.union(other)
|
209
|
-
# Since our set here is unbounded, the resulting union is also unbounded. This errs on the side
|
210
|
-
# of safety since this set's `to_return_value` returns `nil` to cause the datastore to search
|
211
|
-
# all shards.
|
212
|
-
self
|
213
|
-
end
|
214
|
-
|
215
|
-
def self.negate
|
216
|
-
# This here is the only difference in behavior of this set implementation vs `RoutingValueSet::ALL`.
|
217
|
-
# Where as `ALL.negate` returns an empty set, we treat `negate` as a no-op. We do that because the
|
218
|
-
# negation of an inexact unbounded set is still an inexact unbounded set. While it flips which values
|
219
|
-
# are in or out of the set, this object is still the representation in our datamodel for that case.
|
220
|
-
self
|
221
|
-
end
|
222
|
-
|
223
|
-
def self.to_return_value
|
224
|
-
# Here we return `nil` to make sure that the datastore searches all shards, since we don't have
|
225
|
-
# any information we can use to safely limit what shards it searches.
|
226
|
-
nil
|
227
|
-
end
|
228
|
-
end
|
229
163
|
end
|
230
164
|
|
231
165
|
# `Query::RoutingPicker` exists only for use by `Query` and is effectively private.
|
@@ -344,17 +344,23 @@ module ElasticGraph
|
|
344
344
|
|
345
345
|
# Encapsulates dependencies of `Query`, giving us something we can expose off of `application`
|
346
346
|
# to build queries when desired.
|
347
|
-
class Builder < Support::MemoizableData.define(:runtime_metadata, :logger, :query_defaults)
|
348
|
-
def self.with(runtime_metadata:, logger:, **query_defaults)
|
349
|
-
new(runtime_metadata
|
347
|
+
class Builder < Support::MemoizableData.define(:runtime_metadata, :logger, :filter_node_interpreter, :query_defaults)
|
348
|
+
def self.with(runtime_metadata:, logger:, filter_node_interpreter:, **query_defaults)
|
349
|
+
new(runtime_metadata:, logger:, filter_node_interpreter:, query_defaults:)
|
350
350
|
end
|
351
351
|
|
352
352
|
def routing_picker
|
353
|
-
@routing_picker ||= RoutingPicker.new(
|
353
|
+
@routing_picker ||= RoutingPicker.new(
|
354
|
+
filter_node_interpreter: filter_node_interpreter,
|
355
|
+
schema_names: runtime_metadata.schema_element_names
|
356
|
+
)
|
354
357
|
end
|
355
358
|
|
356
359
|
def index_expression_builder
|
357
|
-
@index_expression_builder ||= IndexExpressionBuilder.new(
|
360
|
+
@index_expression_builder ||= IndexExpressionBuilder.new(
|
361
|
+
filter_node_interpreter: filter_node_interpreter,
|
362
|
+
schema_names: runtime_metadata.schema_element_names
|
363
|
+
)
|
358
364
|
end
|
359
365
|
|
360
366
|
def new_query(**options)
|
@@ -26,7 +26,7 @@ module ElasticGraph
|
|
26
26
|
# vs GraphQL.
|
27
27
|
def translate_filter_args(field:, args:)
|
28
28
|
return nil unless (filter_hash = args[filter_arg_name])
|
29
|
-
filter_type = field.schema.type_from(field.graphql_field.arguments
|
29
|
+
filter_type = field.schema.type_from(field.graphql_field.arguments.fetch(filter_arg_name).type)
|
30
30
|
convert(filter_type, filter_hash)
|
31
31
|
end
|
32
32
|
|
@@ -137,8 +137,6 @@ module ElasticGraph
|
|
137
137
|
# this because we do not generate `any_satisfy` filters on `object` list fields (instead,
|
138
138
|
# they get generated on their leaf fields).
|
139
139
|
def process_list_any_filter_expression(bool_node, filter, field_path)
|
140
|
-
return if filter.nil? || filter == {}
|
141
|
-
|
142
140
|
if filters_on_sub_fields?(filter)
|
143
141
|
process_any_satisfy_filter_expression_on_nested_object_list(bool_node, filter, field_path)
|
144
142
|
else
|
@@ -195,8 +193,6 @@ module ElasticGraph
|
|
195
193
|
# * `filter: {anyOf: [{field: null}]}` -> return all results
|
196
194
|
# * `filter: {anyOf: [{field: null}, {field: ...}]}` -> return all results
|
197
195
|
def process_any_of_expression(bool_node, expressions, field_path)
|
198
|
-
return if expressions.nil? || expressions == {}
|
199
|
-
|
200
196
|
if expressions.empty?
|
201
197
|
# When our `expressions` array is empty, we want to match no documents. However, that's
|
202
198
|
# not the behavior the datastore will give us if we have an empty array in the query under
|
@@ -218,8 +214,6 @@ module ElasticGraph
|
|
218
214
|
end
|
219
215
|
|
220
216
|
def process_all_of_expression(bool_node, expressions, field_path)
|
221
|
-
return if expressions.nil? || expressions == {}
|
222
|
-
|
223
217
|
# `all_of` represents an AND. AND is the default way that `process_filter_hash` combines
|
224
218
|
# filters so we just have to call it for each sub-expression.
|
225
219
|
expressions.each do |sub_expression|
|
@@ -228,8 +222,6 @@ module ElasticGraph
|
|
228
222
|
end
|
229
223
|
|
230
224
|
def process_operator_expression(bool_node, operator, expression, field_path)
|
231
|
-
return if expression.nil? || expression == {}
|
232
|
-
|
233
225
|
# `operator` is a filtering operator, and `expression` is the value the filtering
|
234
226
|
# operator should be applied to. The `op_applicator` lambda, when called, will
|
235
227
|
# return a Clause instance (defined in this module).
|
@@ -317,8 +309,6 @@ module ElasticGraph
|
|
317
309
|
end
|
318
310
|
|
319
311
|
def process_list_count_expression(bool_node, expression, field_path)
|
320
|
-
return if expression.nil? || expression == {}
|
321
|
-
|
322
312
|
# Normally, we don't have to do anything special for list count expressions.
|
323
313
|
# That's the case, for example, for an expression like:
|
324
314
|
#
|
@@ -25,33 +25,30 @@ module ElasticGraph
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def identify_node_type(field_or_op, sub_expression)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def filter_operators
|
32
|
-
@filter_operators ||= build_filter_operators(runtime_metadata)
|
33
|
-
end
|
28
|
+
# `:not` must go before `:empty`, because `not: empty_filter` must be inverted from just `empty_filter`.
|
29
|
+
return :not if field_or_op == schema_names.not
|
34
30
|
|
35
|
-
|
31
|
+
# The `:empty` check can go before all other checks; besides `not`, none of the other operators require special
|
32
|
+
# handling when the filter is empty, and we want to detect this as early as possible.
|
33
|
+
# Note: `any_of: [empty_filter]` does have special handling, but `any_of: empty_filter` does not.
|
34
|
+
return :empty if sub_expression.nil? || sub_expression == {}
|
36
35
|
|
37
|
-
def identify_by_field_or_op(field_or_op)
|
38
|
-
return :not if field_or_op == schema_names.not
|
39
36
|
return :list_any_filter if field_or_op == schema_names.any_satisfy
|
40
37
|
return :all_of if field_or_op == schema_names.all_of
|
41
38
|
return :any_of if field_or_op == schema_names.any_of
|
42
39
|
return :operator if filter_operators.key?(field_or_op)
|
43
40
|
return :list_count if field_or_op == LIST_COUNTS_FIELD
|
41
|
+
return :sub_field if sub_expression.is_a?(::Hash)
|
44
42
|
|
45
|
-
|
43
|
+
:unknown
|
46
44
|
end
|
47
45
|
|
48
|
-
def
|
49
|
-
|
50
|
-
return :sub_field if sub_expression.is_a?(::Hash)
|
51
|
-
|
52
|
-
nil
|
46
|
+
def filter_operators
|
47
|
+
@filter_operators ||= build_filter_operators(runtime_metadata)
|
53
48
|
end
|
54
49
|
|
50
|
+
private
|
51
|
+
|
55
52
|
def build_filter_operators(runtime_metadata)
|
56
53
|
filter_by_time_of_day_script_id = runtime_metadata
|
57
54
|
.static_script_ids_by_scoped_name
|
@@ -12,7 +12,8 @@ module ElasticGraph
|
|
12
12
|
# Responsible for extracting a set of values from query filters, based on a using a custom
|
13
13
|
# set type that is able to efficiently model the "all values" case.
|
14
14
|
class FilterValueSetExtractor
|
15
|
-
def initialize(schema_names, all_values_set, empty_set, &build_set_for_filter)
|
15
|
+
def initialize(filter_node_interpreter, schema_names, all_values_set, empty_set, &build_set_for_filter)
|
16
|
+
@filter_node_interpreter = filter_node_interpreter
|
16
17
|
@schema_names = schema_names
|
17
18
|
@all_values_set = all_values_set
|
18
19
|
@empty_set = empty_set
|
@@ -41,9 +42,12 @@ module ElasticGraph
|
|
41
42
|
# routing values causes no adverse behavior (although it may introduce an inefficiency)
|
42
43
|
# but if we fail to route to a shard that contains a matching document, the search results
|
43
44
|
# will be incorrect.
|
44
|
-
map_reduce_sets(target_field_paths, :union, negate: false) do |target_field_path|
|
45
|
+
value_set = map_reduce_sets(target_field_paths, :union, negate: false) do |target_field_path|
|
45
46
|
filter_value_set_for_target_field_path(target_field_path, filter_hashes)
|
46
47
|
end
|
48
|
+
|
49
|
+
return nil if (_ = value_set) == UnboundedSetWithExclusions
|
50
|
+
_ = value_set
|
47
51
|
end
|
48
52
|
|
49
53
|
private
|
@@ -83,24 +87,32 @@ module ElasticGraph
|
|
83
87
|
# the key could identify either a field we are filtering on or a filtering operator to apply
|
84
88
|
# to a particular field.
|
85
89
|
def filter_value_set_for_filter_hash_entry(field_or_op, filter_value, target_field_path_parts, traversed_field_path_parts, negate:)
|
86
|
-
|
87
|
-
|
90
|
+
node_type = @filter_node_interpreter.identify_node_type(field_or_op, filter_value)
|
91
|
+
case node_type
|
92
|
+
when :empty
|
93
|
+
# Any empty filter is effectively treated as `true` by our filtering logic, so we need
|
88
94
|
# to return our `@all_values_set` to indicate this filter matches all documents.
|
89
95
|
@all_values_set
|
90
|
-
|
91
|
-
filter_value_set_for_filter_hash(filter_value, target_field_path_parts, traversed_field_path_parts, negate: !negate)
|
92
|
-
|
93
|
-
# the only time `value` is a hash is when `field_or_op` is a field name.
|
94
|
-
# In that case, `value` is a hash of filters that apply to that field.
|
96
|
+
when :not
|
97
|
+
filter_value_set_for_filter_hash(filter_value || {}, target_field_path_parts, traversed_field_path_parts, negate: !negate)
|
98
|
+
when :sub_field
|
95
99
|
filter_value_set_for_filter_hash(filter_value, target_field_path_parts, traversed_field_path_parts + [field_or_op], negate: negate)
|
96
|
-
|
100
|
+
when :any_of
|
97
101
|
filter_value_set_for_any_of(filter_value, target_field_path_parts, traversed_field_path_parts, negate: negate)
|
98
|
-
|
102
|
+
when :operator
|
103
|
+
# Check to make sure the operator applies to the target field. If not, we have no information
|
104
|
+
# in this clause. The set is unbounded, and may have exclusions.
|
105
|
+
return UnboundedSetWithExclusions unless target_field_path_parts == traversed_field_path_parts
|
106
|
+
|
99
107
|
set = filter_value_set_for_field_filter(field_or_op, filter_value)
|
100
108
|
negate ? set.negate : set
|
109
|
+
when :all_of, :list_any_filter, :list_count, :unknown
|
110
|
+
# We have no information in this clause. The set is unbounded, and may have exclusions.
|
111
|
+
UnboundedSetWithExclusions
|
101
112
|
else
|
102
|
-
#
|
103
|
-
|
113
|
+
# :nocov: -- not possible to cover without mocking `@filter_node_interpreter` to return a node type outside the allowed values.
|
114
|
+
raise "`FilterValueSetExtractor` must be updated to handle `:#{node_type}` nodes."
|
115
|
+
# :nocov:
|
104
116
|
end
|
105
117
|
end
|
106
118
|
|
@@ -125,7 +137,7 @@ module ElasticGraph
|
|
125
137
|
# Determines the set of filter values for a single filter on a single field.
|
126
138
|
def filter_value_set_for_field_filter(filter_op, filter_value)
|
127
139
|
operator_name = @schema_names.canonical_name_for(filter_op)
|
128
|
-
@build_set_for_filter.call(operator_name, filter_value) ||
|
140
|
+
@build_set_for_filter.call(operator_name, filter_value) || UnboundedSetWithExclusions
|
129
141
|
end
|
130
142
|
|
131
143
|
# Maps over the provided `collection` by applying the given `map_transform`
|
@@ -144,10 +156,49 @@ module ElasticGraph
|
|
144
156
|
# of each set is the difference between @all_values_set and the given set)--and vice versa.
|
145
157
|
reduction = REDUCTION_INVERSIONS.fetch(reduction) if negate
|
146
158
|
|
147
|
-
collection.map(&map_transform).reduce
|
159
|
+
collection.map(&map_transform).reduce do |s1, s2|
|
160
|
+
receiver, argument = ((_ = s2) == UnboundedSetWithExclusions) ? [s2, s1] : [s1, s2]
|
161
|
+
(reduction == :union) ? receiver.union(argument) : receiver.intersection(argument)
|
162
|
+
end
|
148
163
|
end
|
149
164
|
|
150
165
|
REDUCTION_INVERSIONS = {union: :intersection, intersection: :union}
|
166
|
+
|
167
|
+
# This minimal set implementation is used for otherwise unrepresentable cases. We use it when
|
168
|
+
# a filter on a `target_field_path` uses an inequality like:
|
169
|
+
#
|
170
|
+
# {field: {gt: "abc"}}
|
171
|
+
#
|
172
|
+
# In a case like that, the set is unbounded (there's an infinite number of values that are greater
|
173
|
+
# than `"abc"`...), but it's not `@all_values_set`--since it's based on an inequality, there are
|
174
|
+
# _some_ values that are excluded from the set. We also can't represent this case with an
|
175
|
+
# `all_values_except(...)` set implementation because the set of exclusions is also unbounded!
|
176
|
+
#
|
177
|
+
# When our filter value extraction results in this set, we cannot limit what shards or indices we must hit based
|
178
|
+
# on the filters.
|
179
|
+
module UnboundedSetWithExclusions
|
180
|
+
def self.intersection(other)
|
181
|
+
# Technically, the accurate intersection would be `other - values_of(self)` but as we don't have
|
182
|
+
# any known values from this unbounded set, we just return `other`. It's OK to include extra values
|
183
|
+
# in the set (we'll search additional shards or indices) but not OK to fail to include necessary values
|
184
|
+
# in the set (we'd avoid searching a shard that may have matching documents) so we err on the side of
|
185
|
+
# including more values.
|
186
|
+
other
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.union(other)
|
190
|
+
# Since our set here is unbounded, the resulting union is also unbounded.
|
191
|
+
self
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.negate
|
195
|
+
# The negation of an `UnboundedSetWithExclusions` is still an `UnboundedSetWithExclusions`. While it would flip
|
196
|
+
# which values are in or out of the set, this object is still the representation in our data model for that case.
|
197
|
+
self
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
private_constant :UnboundedSetWithExclusions
|
151
202
|
end
|
152
203
|
end
|
153
204
|
end
|
@@ -96,12 +96,18 @@ module ElasticGraph
|
|
96
96
|
# on `filter_value_set_extractor` to determine it, since it understands the semantics
|
97
97
|
# of `any_of`, `not`, etc.
|
98
98
|
def can_match_nil_values_at?(path, filter)
|
99
|
-
filter_value_set_extractor.extract_filter_value_set([filter], [path])
|
99
|
+
value_set = filter_value_set_extractor.extract_filter_value_set([filter], [path])
|
100
|
+
(_ = value_set).nil? || value_set.includes_nil?
|
100
101
|
end
|
101
102
|
|
102
103
|
def filter_value_set_extractor
|
103
104
|
@filter_value_set_extractor ||=
|
104
|
-
Filtering::FilterValueSetExtractor.new(
|
105
|
+
Filtering::FilterValueSetExtractor.new(
|
106
|
+
filter_node_interpreter,
|
107
|
+
schema_element_names,
|
108
|
+
IncludesNilSet,
|
109
|
+
ExcludesNilSet
|
110
|
+
) do |operator, filter_value|
|
105
111
|
if operator == :equal_to_any_of && filter_value.include?(nil)
|
106
112
|
IncludesNilSet
|
107
113
|
else
|
@@ -25,7 +25,7 @@ module ElasticGraph
|
|
25
25
|
def fetch(queries)
|
26
26
|
responses_by_query = @datastore_router.msearch(queries, query_tracker: @query_tracker)
|
27
27
|
@query_tracker.record_datastore_queries_for_single_request(queries)
|
28
|
-
queries.map { |q| responses_by_query
|
28
|
+
queries.map { |q| responses_by_query.fetch(q) }
|
29
29
|
end
|
30
30
|
|
31
31
|
def self.execute_many(queries, for_context:)
|
@@ -129,9 +129,10 @@ module ElasticGraph
|
|
129
129
|
@datastore_query_builder ||= begin
|
130
130
|
require "elastic_graph/graphql/datastore_query"
|
131
131
|
DatastoreQuery::Builder.with(
|
132
|
-
filter_interpreter
|
133
|
-
|
134
|
-
|
132
|
+
filter_interpreter:,
|
133
|
+
filter_node_interpreter:,
|
134
|
+
runtime_metadata:,
|
135
|
+
logger:,
|
135
136
|
default_page_size: @config.default_page_size,
|
136
137
|
max_page_size: @config.max_page_size
|
137
138
|
)
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elasticgraph-graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.19.0.0
|
4
|
+
version: 0.19.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Myron Marston
|
8
8
|
- Ben VandenBos
|
9
9
|
- Block Engineering
|
10
|
-
autorequire:
|
10
|
+
autorequire:
|
11
11
|
bindir: exe
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-12-
|
13
|
+
date: 2024-12-10 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rubocop-factory_bot
|
@@ -74,14 +74,14 @@ dependencies:
|
|
74
74
|
requirements:
|
75
75
|
- - "~>"
|
76
76
|
- !ruby/object:Gem::Version
|
77
|
-
version:
|
77
|
+
version: 1.9.0
|
78
78
|
type: :development
|
79
79
|
prerelease: false
|
80
80
|
version_requirements: !ruby/object:Gem::Requirement
|
81
81
|
requirements:
|
82
82
|
- - "~>"
|
83
83
|
- !ruby/object:Gem::Version
|
84
|
-
version:
|
84
|
+
version: 1.9.0
|
85
85
|
- !ruby/object:Gem::Dependency
|
86
86
|
name: coderay
|
87
87
|
requirement: !ruby/object:Gem::Requirement
|
@@ -268,113 +268,113 @@ dependencies:
|
|
268
268
|
requirements:
|
269
269
|
- - '='
|
270
270
|
- !ruby/object:Gem::Version
|
271
|
-
version: 0.19.0.0
|
271
|
+
version: 0.19.0.0
|
272
272
|
type: :runtime
|
273
273
|
prerelease: false
|
274
274
|
version_requirements: !ruby/object:Gem::Requirement
|
275
275
|
requirements:
|
276
276
|
- - '='
|
277
277
|
- !ruby/object:Gem::Version
|
278
|
-
version: 0.19.0.0
|
278
|
+
version: 0.19.0.0
|
279
279
|
- !ruby/object:Gem::Dependency
|
280
280
|
name: elasticgraph-schema_artifacts
|
281
281
|
requirement: !ruby/object:Gem::Requirement
|
282
282
|
requirements:
|
283
283
|
- - '='
|
284
284
|
- !ruby/object:Gem::Version
|
285
|
-
version: 0.19.0.0
|
285
|
+
version: 0.19.0.0
|
286
286
|
type: :runtime
|
287
287
|
prerelease: false
|
288
288
|
version_requirements: !ruby/object:Gem::Requirement
|
289
289
|
requirements:
|
290
290
|
- - '='
|
291
291
|
- !ruby/object:Gem::Version
|
292
|
-
version: 0.19.0.0
|
292
|
+
version: 0.19.0.0
|
293
293
|
- !ruby/object:Gem::Dependency
|
294
294
|
name: graphql
|
295
295
|
requirement: !ruby/object:Gem::Requirement
|
296
296
|
requirements:
|
297
297
|
- - "~>"
|
298
298
|
- !ruby/object:Gem::Version
|
299
|
-
version: 2.4.
|
299
|
+
version: 2.4.8
|
300
300
|
type: :runtime
|
301
301
|
prerelease: false
|
302
302
|
version_requirements: !ruby/object:Gem::Requirement
|
303
303
|
requirements:
|
304
304
|
- - "~>"
|
305
305
|
- !ruby/object:Gem::Version
|
306
|
-
version: 2.4.
|
306
|
+
version: 2.4.8
|
307
307
|
- !ruby/object:Gem::Dependency
|
308
308
|
name: elasticgraph-admin
|
309
309
|
requirement: !ruby/object:Gem::Requirement
|
310
310
|
requirements:
|
311
311
|
- - '='
|
312
312
|
- !ruby/object:Gem::Version
|
313
|
-
version: 0.19.0.0
|
313
|
+
version: 0.19.0.0
|
314
314
|
type: :development
|
315
315
|
prerelease: false
|
316
316
|
version_requirements: !ruby/object:Gem::Requirement
|
317
317
|
requirements:
|
318
318
|
- - '='
|
319
319
|
- !ruby/object:Gem::Version
|
320
|
-
version: 0.19.0.0
|
320
|
+
version: 0.19.0.0
|
321
321
|
- !ruby/object:Gem::Dependency
|
322
322
|
name: elasticgraph-elasticsearch
|
323
323
|
requirement: !ruby/object:Gem::Requirement
|
324
324
|
requirements:
|
325
325
|
- - '='
|
326
326
|
- !ruby/object:Gem::Version
|
327
|
-
version: 0.19.0.0
|
327
|
+
version: 0.19.0.0
|
328
328
|
type: :development
|
329
329
|
prerelease: false
|
330
330
|
version_requirements: !ruby/object:Gem::Requirement
|
331
331
|
requirements:
|
332
332
|
- - '='
|
333
333
|
- !ruby/object:Gem::Version
|
334
|
-
version: 0.19.0.0
|
334
|
+
version: 0.19.0.0
|
335
335
|
- !ruby/object:Gem::Dependency
|
336
336
|
name: elasticgraph-opensearch
|
337
337
|
requirement: !ruby/object:Gem::Requirement
|
338
338
|
requirements:
|
339
339
|
- - '='
|
340
340
|
- !ruby/object:Gem::Version
|
341
|
-
version: 0.19.0.0
|
341
|
+
version: 0.19.0.0
|
342
342
|
type: :development
|
343
343
|
prerelease: false
|
344
344
|
version_requirements: !ruby/object:Gem::Requirement
|
345
345
|
requirements:
|
346
346
|
- - '='
|
347
347
|
- !ruby/object:Gem::Version
|
348
|
-
version: 0.19.0.0
|
348
|
+
version: 0.19.0.0
|
349
349
|
- !ruby/object:Gem::Dependency
|
350
350
|
name: elasticgraph-indexer
|
351
351
|
requirement: !ruby/object:Gem::Requirement
|
352
352
|
requirements:
|
353
353
|
- - '='
|
354
354
|
- !ruby/object:Gem::Version
|
355
|
-
version: 0.19.0.0
|
355
|
+
version: 0.19.0.0
|
356
356
|
type: :development
|
357
357
|
prerelease: false
|
358
358
|
version_requirements: !ruby/object:Gem::Requirement
|
359
359
|
requirements:
|
360
360
|
- - '='
|
361
361
|
- !ruby/object:Gem::Version
|
362
|
-
version: 0.19.0.0
|
362
|
+
version: 0.19.0.0
|
363
363
|
- !ruby/object:Gem::Dependency
|
364
364
|
name: elasticgraph-schema_definition
|
365
365
|
requirement: !ruby/object:Gem::Requirement
|
366
366
|
requirements:
|
367
367
|
- - '='
|
368
368
|
- !ruby/object:Gem::Version
|
369
|
-
version: 0.19.0.0
|
369
|
+
version: 0.19.0.0
|
370
370
|
type: :development
|
371
371
|
prerelease: false
|
372
372
|
version_requirements: !ruby/object:Gem::Requirement
|
373
373
|
requirements:
|
374
374
|
- - '='
|
375
375
|
- !ruby/object:Gem::Version
|
376
|
-
version: 0.19.0.0
|
377
|
-
description:
|
376
|
+
version: 0.19.0.0
|
377
|
+
description:
|
378
378
|
email:
|
379
379
|
- myron@squareup.com
|
380
380
|
executables: []
|
@@ -466,12 +466,12 @@ licenses:
|
|
466
466
|
- MIT
|
467
467
|
metadata:
|
468
468
|
bug_tracker_uri: https://github.com/block/elasticgraph/issues
|
469
|
-
changelog_uri: https://github.com/block/elasticgraph/releases/tag/v0.19.0.0
|
469
|
+
changelog_uri: https://github.com/block/elasticgraph/releases/tag/v0.19.0.0
|
470
470
|
documentation_uri: https://block.github.io/elasticgraph/docs/main/
|
471
471
|
homepage_uri: https://block.github.io/elasticgraph/
|
472
|
-
source_code_uri: https://github.com/block/elasticgraph/tree/v0.19.0.0
|
472
|
+
source_code_uri: https://github.com/block/elasticgraph/tree/v0.19.0.0/elasticgraph-graphql
|
473
473
|
gem_category: core
|
474
|
-
post_install_message:
|
474
|
+
post_install_message:
|
475
475
|
rdoc_options: []
|
476
476
|
require_paths:
|
477
477
|
- lib
|
@@ -486,8 +486,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
486
486
|
- !ruby/object:Gem::Version
|
487
487
|
version: '0'
|
488
488
|
requirements: []
|
489
|
-
rubygems_version: 3.5.
|
490
|
-
signing_key:
|
489
|
+
rubygems_version: 3.5.22
|
490
|
+
signing_key:
|
491
491
|
specification_version: 4
|
492
492
|
summary: The ElasticGraph GraphQL query engine.
|
493
493
|
test_files: []
|