elasticgraph-graphql 1.0.1 → 1.0.3.rc1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +1 -1
  4. data/lib/elastic_graph/graphql/aggregation/computation.rb +1 -1
  5. data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +5 -1
  6. data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +1 -1
  7. data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +22 -2
  8. data/lib/elastic_graph/graphql/aggregation/key.rb +1 -1
  9. data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +1 -1
  10. data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +17 -8
  11. data/lib/elastic_graph/graphql/aggregation/path_segment.rb +1 -1
  12. data/lib/elastic_graph/graphql/aggregation/query.rb +25 -22
  13. data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +3 -2
  14. data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +1 -1
  15. data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +1 -1
  16. data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +1 -1
  17. data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +1 -1
  18. data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +1 -1
  19. data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +1 -1
  20. data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +1 -1
  21. data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +5 -1
  22. data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +1 -1
  23. data/lib/elastic_graph/graphql/client.rb +1 -1
  24. data/lib/elastic_graph/graphql/config.rb +1 -1
  25. data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +1 -1
  26. data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +1 -1
  27. data/lib/elastic_graph/graphql/datastore_query/paginator.rb +1 -1
  28. data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +1 -1
  29. data/lib/elastic_graph/graphql/datastore_query.rb +1 -1
  30. data/lib/elastic_graph/graphql/datastore_response/document.rb +2 -1
  31. data/lib/elastic_graph/graphql/datastore_response/search_response.rb +1 -1
  32. data/lib/elastic_graph/graphql/datastore_search_router.rb +1 -1
  33. data/lib/elastic_graph/graphql/decoded_cursor.rb +2 -2
  34. data/lib/elastic_graph/graphql/filtering/boolean_query.rb +1 -1
  35. data/lib/elastic_graph/graphql/filtering/field_path.rb +1 -1
  36. data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +1 -1
  37. data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +1 -1
  38. data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +11 -1
  39. data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +1 -1
  40. data/lib/elastic_graph/graphql/filtering/range_query.rb +39 -5
  41. data/lib/elastic_graph/graphql/http_endpoint.rb +1 -1
  42. data/lib/elastic_graph/graphql/query_adapter/filters.rb +1 -1
  43. data/lib/elastic_graph/graphql/query_adapter/pagination.rb +1 -1
  44. data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +1 -1
  45. data/lib/elastic_graph/graphql/query_adapter/sort.rb +1 -1
  46. data/lib/elastic_graph/graphql/query_details_tracker.rb +10 -1
  47. data/lib/elastic_graph/graphql/query_executor.rb +6 -6
  48. data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +1 -1
  49. data/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb +1 -1
  50. data/lib/elastic_graph/graphql/resolvers/list_records.rb +1 -1
  51. data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +1 -1
  52. data/lib/elastic_graph/graphql/resolvers/nested_relationships_source.rb +1 -1
  53. data/lib/elastic_graph/graphql/resolvers/object.rb +1 -1
  54. data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +1 -1
  55. data/lib/elastic_graph/graphql/resolvers/query_source.rb +1 -1
  56. data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +2 -1
  57. data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +1 -1
  58. data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +1 -1
  59. data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +1 -1
  60. data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +1 -1
  61. data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +2 -1
  62. data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +1 -1
  63. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +1 -1
  64. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +5 -6
  65. data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +1 -1
  66. data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +1 -1
  67. data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +1 -1
  68. data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +1 -1
  69. data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +1 -1
  70. data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +1 -1
  71. data/lib/elastic_graph/graphql/schema/arguments.rb +1 -1
  72. data/lib/elastic_graph/graphql/schema/enum_value.rb +1 -1
  73. data/lib/elastic_graph/graphql/schema/field.rb +1 -1
  74. data/lib/elastic_graph/graphql/schema/relation_join.rb +1 -1
  75. data/lib/elastic_graph/graphql/schema/type.rb +25 -1
  76. data/lib/elastic_graph/graphql/schema.rb +2 -1
  77. data/lib/elastic_graph/graphql.rb +1 -1
  78. data/script/dump_time_zones +1 -1
  79. metadata +22 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc665f641870a1f2cc3c76587de1ab30de92150f4e4f0992dbb62677becdf7cb
4
- data.tar.gz: ce6326b9bf846cdafa2f86b61149f820da3be2f19e84e5d2366db93f0d334570
3
+ metadata.gz: 491f97b51a739a57f7fd24238518fc02c0642ba2536da338d287694025f1cc9a
4
+ data.tar.gz: ed33c16391844f8b3321ec5849dba8a7acbe502ace6286aaabc401122d085aee
5
5
  SHA512:
6
- metadata.gz: 72fb2fd0bacc529b2befe78f503da293c9279a33bc7605905c1bf6ba12e332bd7f94fccafedd07a55092ed3069f0e690965f3a0471896aeeb7e464d993a92a7c
7
- data.tar.gz: 8cf3ca4740cfca36b66ba59a8fcd7c4b254cd4895b3765b9204c25a94bd45f70fa9f8ee38b5fbe8475a366c9077800876b5c03d45571017d070999f402269ef2
6
+ metadata.gz: 4608f68a96a8f3f55af27ca0102552888f391243c2d71961a0e395f3b9c66ea0d4fde3b68933e04d974243c4ef10c8282ff377f5888e5c1de799f4660a6b15d8
7
+ data.tar.gz: 9c3e3ea42f82d421c5319c373f60f23666bee9237ed7df0e84ea2a51f0d7228af49bbc8e3cce9856eaac532b5ea0b0110e7afb9204abd9e737e055743425bb9e
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2024 - 2025 Block, Inc.
3
+ Copyright (c) 2024 - 2026 Block, Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -53,6 +53,10 @@ module ElasticGraph
53
53
  INNER_META
54
54
  end
55
55
 
56
+ def handles_missing_values?
57
+ false
58
+ end
59
+
56
60
  INNER_META = {
57
61
  # On a date histogram aggregation, the `key` is formatted as a number (milliseconds since epoch). We
58
62
  # need it formatted as a string, which `key_as_string` provides.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -11,10 +11,30 @@ require "elastic_graph/graphql/aggregation/term_grouping"
11
11
  module ElasticGraph
12
12
  class GraphQL
13
13
  module Aggregation
14
- class FieldTermGrouping < Support::MemoizableData.define(:field_path)
14
+ class FieldTermGrouping < Support::MemoizableData.define(:field_path, :missing_value_placeholder)
15
15
  # @dynamic field_path
16
16
  include TermGrouping
17
17
 
18
+ # Returns true if this grouping handles missing values using a placeholder value
19
+ # instead of a separate missing aggregation.
20
+ #
21
+ # @return [Boolean] true if missing values are handled via placeholder
22
+ def handles_missing_values?
23
+ !missing_value_placeholder.nil?
24
+ end
25
+
26
+ def non_composite_clause_for(query)
27
+ return super unless handles_missing_values?
28
+
29
+ Support::HashUtil.deep_merge(super, {"terms" => {"missing" => missing_value_placeholder}})
30
+ end
31
+
32
+ def inner_meta
33
+ return super unless handles_missing_values?
34
+
35
+ super.merge({"missing_values" => [missing_value_placeholder]})
36
+ end
37
+
18
38
  private
19
39
 
20
40
  def terms_subclause
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -65,29 +65,38 @@ module ElasticGraph
65
65
  def format_buckets(sub_agg, buckets_path, parent_key_fields: {}, parent_key_values: [])
66
66
  agg_with_buckets = sub_agg.dig(*buckets_path)
67
67
 
68
- missing_bucket = {
69
- # Doc counts in missing value buckets are always perfectly accurate.
70
- "doc_count_error_upper_bound" => 0
71
- }.merge(sub_agg.dig(*missing_bucket_path_from(buckets_path))) # : ::Hash[::String, untyped]
72
-
73
68
  meta = agg_with_buckets.fetch("meta")
74
69
 
75
70
  grouping_field_names = meta.fetch("grouping_fields") # provides the names of the fields being grouped on
76
71
  key_path = meta.fetch("key_path") # indicates whether we want to get the key values from `key` or `key_as_string`.
77
72
  sub_buckets_path = meta["buckets_path"] # buckets_path is optional, so we don't use fetch.
73
+ missing_values = meta["missing_values"] # missing_values is optional, so we don't use fetch.
78
74
  merge_into_bucket = meta.fetch("merge_into_bucket")
79
75
 
80
76
  raw_buckets = agg_with_buckets.fetch("buckets") # : ::Array[::Hash[::String, untyped]]
81
77
 
82
78
  # If the missing bucket is non-empty, include it. This matches the behavior of composite aggregations
83
79
  # when the `missing_bucket` option is used.
84
- raw_buckets += [missing_bucket] if missing_bucket.fetch("doc_count") > 0
80
+ missing_bucket = sub_agg.dig(*missing_bucket_path_from(buckets_path)) || {}
81
+ if missing_bucket.fetch("doc_count", 0) > 0
82
+ # Doc counts in missing value buckets are always perfectly accurate.
83
+ raw_buckets += [{"doc_count_error_upper_bound" => 0}.merge(missing_bucket)]
84
+ end
85
85
 
86
86
  raw_buckets.flat_map do |raw_bucket|
87
+ key_values = Array(raw_bucket.dig(*key_path))
88
+ if missing_values
89
+ # if missing_values are present, then for each element in key_values, replace with nil if it matches the placeholder value from missing_values
90
+ key_values = key_values.each_with_index.map do |value, index|
91
+ # @type var value: untyped
92
+ # @type var index: Integer
93
+ (value == missing_values[index]) ? nil : value
94
+ end
95
+ end
96
+
87
97
  # The key will either be a single value (e.g. `47`) if we used a `terms`/`date_histogram` aggregation,
88
98
  # or a tuple of values (e.g. `[47, "abc"]`) if we used a `multi_terms` aggregation. Here we convert it
89
99
  # to the form needed for resolving `grouped_by` fields: a hash like `{"size" => 47, "tag" => "abc"}`.
90
- key_values = Array(raw_bucket.dig(*key_path))
91
100
  key_fields_hash = grouping_field_names.zip(key_values).to_h
92
101
 
93
102
  # If we have multiple levels of aggregations, we need to merge the key fields hash with the key fields from the parent levels.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -154,27 +154,30 @@ module ElasticGraph
154
154
  "meta" => meta.merge(extra_inner_meta)
155
155
  }.compact
156
156
 
157
- missing_bucket_inner_agg_hash = inner_agg_hash.key?("aggs") ? inner_agg_hash : {} # : ::Hash[::String, untyped]
158
-
159
- AggregationDetail.new(
160
- {
161
- agg_key => grouping.non_composite_clause_for(query).merge(inner_agg_hash),
162
-
163
- # Here we include a `missing` aggregation as a sibling to the main grouping aggregation. We do this
164
- # so that we get a bucket of documents that have `null` values for the field we are grouping on, in
165
- # order to provide the same behavior as the `CompositeGroupingAdapter` (which uses the built-in
166
- # `missing_bucket` option).
167
- #
168
- # To work correctly, we need to include this `missing` aggregation as a sibling at _every_ level of
169
- # the aggregation structure, and the `missing` aggregation needs the same child aggregations as the
170
- # main grouping aggregation has. Given the recursive nature of how this is applied, this results in
171
- # a fairly complex structure, even though conceptually the idea behind this isn't _too_ bad.
172
- Key.missing_value_bucket_key(agg_key) => {
173
- "missing" => {"field" => grouping.encoded_index_field_path}
174
- }.merge(missing_bucket_inner_agg_hash)
175
- },
176
- {"buckets_path" => [agg_key]}
177
- )
157
+ wrapped_clauses = {
158
+ agg_key => grouping.non_composite_clause_for(query).merge(inner_agg_hash)
159
+ }
160
+
161
+ unless grouping.handles_missing_values?
162
+ # Here we include a `missing` aggregation as a sibling to the main grouping aggregation. We do this
163
+ # so that we get a bucket of documents that have `null` values for the field we are grouping on, in
164
+ # order to provide the same behavior as the `CompositeGroupingAdapter` (which uses the built-in
165
+ # `missing_bucket` option).
166
+ #
167
+ # To work correctly, we need to include this `missing` aggregation as a sibling at _every_ level of
168
+ # the aggregation structure, and the `missing` aggregation needs the same child aggregations as the
169
+ # main grouping aggregation has. Given the recursive nature of how this is applied, this results in
170
+ # a fairly complex structure, even though conceptually the idea behind this isn't _too_ bad.
171
+ missing_bucket_inner_agg_hash = inner_agg_hash.key?("aggs") ? inner_agg_hash : {} # : ::Hash[::String, untyped]
172
+ missing_bucket_inner_agg_hash = missing_bucket_inner_agg_hash.merge({
173
+ "missing" => {"field" => grouping.encoded_index_field_path}
174
+ })
175
+ wrapped_clauses = wrapped_clauses.merge({
176
+ Key.missing_value_bucket_key(agg_key) => missing_bucket_inner_agg_hash
177
+ })
178
+ end
179
+
180
+ AggregationDetail.new(wrapped_clauses, {"buckets_path" => [agg_key]})
178
181
  end
179
182
  end
180
183
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -211,7 +211,8 @@ module ElasticGraph
211
211
  date_time_groupings_from(field_path: field_path, node: node)
212
212
  elsif !field.type.object?
213
213
  # Non-date/time grouping
214
- [FieldTermGrouping.new(field_path: field_path)]
214
+ missing_value_placeholder = field.type.unwrap_fully.grouping_missing_value_placeholder
215
+ [FieldTermGrouping.new(field_path: field_path, missing_value_placeholder: missing_value_placeholder)]
215
216
  end
216
217
  end
217
218
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -16,6 +16,10 @@ module ElasticGraph
16
16
  # @dynamic field_path
17
17
  include TermGrouping
18
18
 
19
+ def handles_missing_values?
20
+ false
21
+ end
22
+
19
23
  private
20
24
 
21
25
  def terms_subclause
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -19,6 +19,7 @@ module ElasticGraph
19
19
  Document = Support::MemoizableData.define(:raw_data, :payload, :decoded_cursor_factory) do
20
20
  # @implements Document
21
21
  extend Forwardable
22
+
22
23
  def_delegators :payload, :[], :fetch
23
24
 
24
25
  def self.build(raw_data, decoded_cursor_factory: DecodedCursor::Factory::Null)
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -55,7 +55,7 @@ module ElasticGraph
55
55
  # Encodes the cursor to a string using JSON and Base64 encoding.
56
56
  def encode
57
57
  @encode ||= begin
58
- json = ::JSON.fast_generate(sort_values)
58
+ json = ::JSON.generate(sort_values)
59
59
  ::Base64.urlsafe_encode64(json, padding: false)
60
60
  end
61
61
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -107,6 +107,16 @@ module ElasticGraph
107
107
  query: value.fetch(schema_names.phrase)
108
108
  }}})
109
109
  },
110
+ schema_names.matches_query_with_prefix => ->(field_name, value) do
111
+ allowed_edits_per_term = value.fetch(schema_names.allowed_edits_per_term).runtime_metadata.datastore_abbreviation
112
+
113
+ BooleanQuery.filter({match_bool_prefix: {field_name => {
114
+ query: value.fetch(schema_names.query_with_prefix),
115
+ # This is always a string field, even though the value is often an integer
116
+ fuzziness: allowed_edits_per_term.to_s,
117
+ operator: value[schema_names.require_all_terms] ? "AND" : "OR"
118
+ }}})
119
+ end,
110
120
 
111
121
  schema_names.contains => ->(field_name, value) {
112
122
  case_insensitive = value[schema_names.ignore_case] || false
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -39,15 +39,49 @@ module ElasticGraph
39
39
  # a value > 10 and a value < 100 (even though no single value satisfies both parts!). When we combine
40
40
  # the clauses into a single `range` query then the filtering works like we expect.
41
41
  class RangeQuery < ::Data.define(:field_name, :operator, :value)
42
+ SAME_TYPE_OPS = {
43
+ gt: [:gt, :gte],
44
+ gte: [:gt, :gte],
45
+ lt: [:lt, :lte],
46
+ lte: [:lt, :lte]
47
+ }.freeze
48
+
42
49
  def merge_into(bool_node)
43
50
  existing_range_index = bool_node[:filter].find_index { |clause| clause.dig(:range, field_name) }
44
- new_range_clause = {range: {field_name => {operator => value}}}
45
51
 
46
52
  if existing_range_index
47
- existing_range_clause = bool_node[:filter][existing_range_index]
48
- bool_node[:filter][existing_range_index] = Support::HashUtil.deep_merge(existing_range_clause, new_range_clause)
53
+ existing_range_hash = bool_node[:filter][existing_range_index].dig(:range, field_name)
54
+ merged_range_hash = merge_operators(existing_range_hash)
55
+ bool_node[:filter][existing_range_index] = {range: {field_name => merged_range_hash}}
56
+ else
57
+ bool_node[:filter] << {range: {field_name => {operator => value}}}
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Merges the new operator with existing operators.
64
+ # Keeps the stricter one (gt vs gte or lt vs lte) when there are conflicts.
65
+ def merge_operators(existing_range_hash)
66
+ conflicting_op = SAME_TYPE_OPS.fetch(operator).find { |op| existing_range_hash.key?(op) }
67
+
68
+ if conflicting_op
69
+ existing_val = existing_range_hash[conflicting_op]
70
+ stricter_op, stricter_val = stricter_operator(operator, value, conflicting_op, existing_val)
71
+ existing_range_hash.except(conflicting_op).merge(stricter_op => stricter_val)
72
+ else
73
+ existing_range_hash.merge(operator => value)
74
+ end
75
+ end
76
+
77
+ # Returns the stricter of two operators/values in the same direction.
78
+ def stricter_operator(op1, val1, op2, val2)
79
+ if [:gt, :gte].include?(op1)
80
+ # lower bound: higher is stricter, gt wins ties
81
+ (val1 > val2 || (val1 == val2 && op1 == :gt)) ? [op1, val1] : [op2, val2]
49
82
  else
50
- bool_node[:filter] << new_range_clause
83
+ # upper bound: lower is stricter; lt wins ties
84
+ (val1 < val2 || (val1 == val2 && op1 == :lt)) ? [op1, val1] : [op2, val2]
51
85
  end
52
86
  end
53
87
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,4 +1,4 @@
1
- # Copyright 2024 - 2025 Block, Inc.
1
+ # Copyright 2024 - 2026 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -17,6 +17,7 @@ module ElasticGraph
17
17
  :datastore_query_server_duration_ms,
18
18
  :datastore_query_client_duration_ms,
19
19
  :queried_shard_count,
20
+ :extension_data,
20
21
  :mutex
21
22
  )
22
23
  def self.empty
@@ -27,6 +28,7 @@ module ElasticGraph
27
28
  datastore_query_server_duration_ms: 0,
28
29
  datastore_query_client_duration_ms: 0,
29
30
  queried_shard_count: 0,
31
+ extension_data: {},
30
32
  mutex: ::Thread::Mutex.new
31
33
  )
32
34
  end
@@ -52,6 +54,13 @@ module ElasticGraph
52
54
  def datastore_request_transport_duration_ms
53
55
  datastore_query_client_duration_ms - datastore_query_server_duration_ms
54
56
  end
57
+
58
+ # Allows extensions to set custom data that will be included in the query duration log.
59
+ def []=(key, value)
60
+ mutex.synchronize do
61
+ extension_data[key] = value
62
+ end
63
+ end
55
64
  end
56
65
  end
57
66
  end