diff --git a/config/schema/artifacts/runtime_metadata.yaml b/config/schema/artifacts/runtime_metadata.yaml index 0a5647e6a..f615e096a 100644 --- a/config/schema/artifacts/runtime_metadata.yaml +++ b/config/schema/artifacts/runtime_metadata.yaml @@ -4026,24 +4026,19 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4065,24 +4060,19 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4416,30 +4406,23 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4683,36 +4666,27 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead exact_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4845,36 +4819,27 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead exact_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4886,24 +4851,19 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4911,46 +4871,35 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead approximate_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead exact_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead graphql_only_return_type: true @@ -6085,9 +6034,7 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead graphql_only_return_type: true diff --git a/config/schema/artifacts_with_apollo/runtime_metadata.yaml b/config/schema/artifacts_with_apollo/runtime_metadata.yaml index f61c15ec7..3bdc412ac 100644 --- a/config/schema/artifacts_with_apollo/runtime_metadata.yaml +++ b/config/schema/artifacts_with_apollo/runtime_metadata.yaml @@ -4128,24 +4128,19 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4167,24 +4162,19 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4518,30 +4508,23 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4785,36 +4768,27 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead exact_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4947,36 +4921,27 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead exact_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead graphql_only_return_type: true @@ -4988,24 +4953,19 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead graphql_only_return_type: true @@ -5013,46 +4973,35 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_avg: - computation_detail: - function: avg + computation_function: avg resolver: name: object_with_lookahead approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead approximate_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead approximate_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead approximate_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead exact_max: - computation_detail: - function: max + computation_function: max resolver: name: object_with_lookahead exact_min: - computation_detail: - function: min + computation_function: min resolver: name: object_with_lookahead exact_sum: - computation_detail: - empty_bucket_value: 0 - function: sum + computation_function: sum resolver: name: object_with_lookahead graphql_only_return_type: true @@ -6208,9 +6157,7 @@ object_types_by_name: elasticgraph_category: scalar_aggregated_values graphql_fields_by_name: approximate_distinct_value_count: - computation_detail: - empty_bucket_value: 0 - function: cardinality + computation_function: cardinality resolver: name: object_with_lookahead graphql_only_return_type: true diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/computation.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/computation.rb index d577a390c..49bd7a1e8 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/computation.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/computation.rb @@ -19,7 +19,16 @@ module Aggregation # https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations-metrics-max-aggregation.html # https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations-metrics-min-aggregation.html # https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations-metrics-sum-aggregation.html - Computation = ::Data.define(:source_field_path, :leaf, :detail) do + Computation = ::Data.define( + :source_field_path, + # The path segment for the computation function itself (e.g. `exactMin`), potentially aliased. + :leaf, + # The adapter that owns this function's datastore-specific behavior. + :function_adapter, + # The canonically-keyed GraphQL arguments passed to the function, as produced by + # `function_adapter.extract_args` at query building time. + :function_args + ) do # @implements Computation def key(aggregation_name:) @@ -32,7 +41,8 @@ def key(aggregation_name:) def clause encoded_path = FieldPathEncoder.join(source_field_path.filter_map(&:name_in_index)) - {detail.function.to_s => {"field" => encoded_path}} + clause_body = {"field" => encoded_path}.merge(function_adapter.clause_options(function_args)) + {function_adapter.datastore_function_name => clause_body} end end end diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/function_adapter.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/function_adapter.rb new file mode 100644 index 000000000..b91d469fc --- /dev/null +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/function_adapter.rb @@ -0,0 +1,53 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +module ElasticGraph + class GraphQL + module Aggregation + # Namespace for the adapters that own the datastore-specific behavior of each aggregated + # value function (`sum`, `avg`, etc). A function's adapter knows the datastore aggregation + # it maps to, how to translate its GraphQL arguments into the aggregation clause, how to + # locate its value in the datastore response, and what response to fabricate for a bucket + # the datastore omitted. + # + # @private + module FunctionAdapter + # Adapter for metric aggregations that take no arguments and return a flat + # `{"value" => ...}` response, which describes all currently supported functions. + # + # @private + class SimpleMetric < ::Data.define(:datastore_function_name, :empty_bucket_value) + def extract_args(args, element_names) + {} + end + + def clause_options(function_args) + {} + end + + def extract_result(raw) + raw + end + + def empty_bucket_result + {"value" => empty_bucket_value} + end + end + + # The registered adapters, keyed by the function name used in runtime metadata. + BY_NAME = { + avg: SimpleMetric.new(datastore_function_name: "avg", empty_bucket_value: nil), + cardinality: SimpleMetric.new(datastore_function_name: "cardinality", empty_bucket_value: 0), + max: SimpleMetric.new(datastore_function_name: "max", empty_bucket_value: nil), + min: SimpleMetric.new(datastore_function_name: "min", empty_bucket_value: nil), + sum: SimpleMetric.new(datastore_function_name: "sum", empty_bucket_value: 0) + }.freeze + end + end + end +end diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/query_adapter.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/query_adapter.rb index fe62a4bf2..924d425bd 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/query_adapter.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/query_adapter.rb @@ -188,12 +188,13 @@ def build_computations_from(node_node, from_field_path: []) get_children_nodes(node).map do |fn_node| computed_field = field_from_node(fn_node) - computation_detail = computed_field.computation_detail # : SchemaArtifacts::RuntimeMetadata::ComputationDetail + function_adapter = computed_field.function_adapter # : FunctionAdapter::adapter Aggregation::Computation.new( source_field_path: field_path, leaf: PathSegment.for(field: computed_field, lookahead: fn_node), - detail: computation_detail + function_adapter: function_adapter, + function_args: function_adapter.extract_args(computed_field.args_to_schema_form(fn_node.arguments), element_names) ) end end diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb index 2df1cdb53..51d583c9e 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb @@ -24,7 +24,8 @@ def resolve(field:, object:, args:, context:, lookahead:) function_name: PathSegment.for(field: field, lookahead: lookahead).name_in_graphql_query ) - result = Support::HashUtil.verbose_fetch(bucket, key.encode) + function_adapter = field.function_adapter # : FunctionAdapter::adapter + result = function_adapter.extract_result(Support::HashUtil.verbose_fetch(bucket, key.encode)) # Aggregated value results always have a `value` key; in addition, for `date` field, they also have a `value_as_string`. # In that case, `value` is a number (e.g. ms since epoch) whereas `value_as_string` is a formatted value. ElasticGraph diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb index a89aa35c3..3d35ceece 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb @@ -69,7 +69,7 @@ def self.build_from_buckets(query:, parent_queries:, schema:, field_path: [], &b } empty_bucket_computations = query.computations.to_h do |computation| - [computation.key(aggregation_name: query.name), {"value" => computation.detail.empty_bucket_value}] + [computation.key(aggregation_name: query.name), computation.function_adapter.empty_bucket_result] end defaults diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/schema/field.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/schema/field.rb index 8bcba35eb..2069b6268 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/schema/field.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/schema/field.rb @@ -7,6 +7,7 @@ # frozen_string_literal: true require "elastic_graph/errors" +require "elastic_graph/graphql/aggregation/function_adapter" require "elastic_graph/graphql/schema/relation_join" require "elastic_graph/graphql/schema/arguments" @@ -18,7 +19,12 @@ class Field # The type in which the field resides. attr_reader :parent_type - attr_reader :schema, :schema_element_names, :graphql_field, :name_in_index, :relation, :computation_detail, :resolver + # The adapter for the aggregation function this field computes, or nil for a field that + # computes none. Resolved from the registry once here (fields are built once and cached) + # so that an unregistered function name fails at boot rather than mid-query. + attr_reader :function_adapter + + attr_reader :schema, :schema_element_names, :graphql_field, :name_in_index, :relation, :resolver def initialize(schema, parent_type, graphql_field, runtime_metadata, resolvers_needing_lookahead) @schema = schema @@ -26,7 +32,9 @@ def initialize(schema, parent_type, graphql_field, runtime_metadata, resolvers_n @parent_type = parent_type @graphql_field = graphql_field @relation = runtime_metadata&.relation - @computation_detail = runtime_metadata&.computation_detail + @function_adapter = runtime_metadata&.computation_function&.then do |function| + Aggregation::FunctionAdapter::BY_NAME.fetch(function) + end @resolver = runtime_metadata&.resolver @name_in_index = runtime_metadata&.name_in_index || name @graphql_field.extras([:lookahead]) if resolvers_needing_lookahead.include?(@resolver&.name) diff --git a/elasticgraph-graphql/sig/elastic_graph/graphql/aggregation/computation.rbs b/elasticgraph-graphql/sig/elastic_graph/graphql/aggregation/computation.rbs index 50825a14e..d103ea41a 100644 --- a/elasticgraph-graphql/sig/elastic_graph/graphql/aggregation/computation.rbs +++ b/elasticgraph-graphql/sig/elastic_graph/graphql/aggregation/computation.rbs @@ -4,7 +4,8 @@ module ElasticGraph class Computation attr_reader source_field_path: fieldPath attr_reader leaf: PathSegment - attr_reader detail: SchemaArtifacts::RuntimeMetadata::ComputationDetail + attr_reader function_adapter: FunctionAdapter::adapter + attr_reader function_args: FunctionAdapter::functionArgs def key: (aggregation_name: ::String) -> ::String def clause: () -> ::Hash[::String, untyped] @@ -12,13 +13,15 @@ module ElasticGraph def initialize : ( source_field_path: fieldPath, leaf: PathSegment, - detail: SchemaArtifacts::RuntimeMetadata::ComputationDetail + function_adapter: FunctionAdapter::adapter, + function_args: FunctionAdapter::functionArgs ) -> void def with: ( ?source_field_path: fieldPath, ?leaf: PathSegment, - ?detail: SchemaArtifacts::RuntimeMetadata::ComputationDetail + ?function_adapter: FunctionAdapter::adapter, + ?function_args: FunctionAdapter::functionArgs ) -> instance end end diff --git a/elasticgraph-graphql/sig/elastic_graph/graphql/aggregation/function_adapter.rbs b/elasticgraph-graphql/sig/elastic_graph/graphql/aggregation/function_adapter.rbs new file mode 100644 index 000000000..0c3af1a2a --- /dev/null +++ b/elasticgraph-graphql/sig/elastic_graph/graphql/aggregation/function_adapter.rbs @@ -0,0 +1,38 @@ +module ElasticGraph + class GraphQL + module Aggregation + module FunctionAdapter + # Adapters need not all be data class instances. A function whose behavior is not a + # parameterization of anything is registered as a singleton module instead, and joins + # this union as a `singleton(...)` member. + type adapter = SimpleMetric + + type functionArgs = ::Hash[::Symbol, untyped] + + class SimpleMetricSupertype < Data + attr_reader datastore_function_name: ::String + attr_reader empty_bucket_value: ::Numeric? + + def initialize: ( + datastore_function_name: ::String, + empty_bucket_value: ::Numeric? + ) -> void + + def self.new: ( + datastore_function_name: ::String, + empty_bucket_value: ::Numeric? + ) -> SimpleMetric + end + + class SimpleMetric < SimpleMetricSupertype + def extract_args: (::Hash[::String, untyped], SchemaArtifacts::RuntimeMetadata::SchemaElementNames) -> functionArgs + def clause_options: (functionArgs) -> ::Hash[::String, untyped] + def extract_result: (::Hash[::String, untyped]) -> ::Hash[::String, untyped] + def empty_bucket_result: () -> ::Hash[::String, untyped] + end + + BY_NAME: ::Hash[::Symbol, adapter] + end + end + end +end diff --git a/elasticgraph-graphql/sig/elastic_graph/graphql/schema/field.rbs b/elasticgraph-graphql/sig/elastic_graph/graphql/schema/field.rbs index da85b12d1..c8fa27fd3 100644 --- a/elasticgraph-graphql/sig/elastic_graph/graphql/schema/field.rbs +++ b/elasticgraph-graphql/sig/elastic_graph/graphql/schema/field.rbs @@ -10,7 +10,7 @@ module ElasticGraph attr_reader parent_type: Type attr_reader type: Type attr_reader graphql_field: ::GraphQL::Schema::Field - attr_reader computation_detail: SchemaArtifacts::RuntimeMetadata::ComputationDetail? + attr_reader function_adapter: Aggregation::FunctionAdapter::adapter? attr_reader relation: SchemaArtifacts::RuntimeMetadata::Relation def aggregated?: () -> bool diff --git a/elasticgraph-graphql/spec/support/aggregations_helpers.rb b/elasticgraph-graphql/spec/support/aggregations_helpers.rb index 90156c2cc..882b59bbb 100644 --- a/elasticgraph-graphql/spec/support/aggregations_helpers.rb +++ b/elasticgraph-graphql/spec/support/aggregations_helpers.rb @@ -10,27 +10,25 @@ require "elastic_graph/graphql/aggregation/computation" require "elastic_graph/graphql/aggregation/date_histogram_grouping" require "elastic_graph/graphql/aggregation/field_term_grouping" +require "elastic_graph/graphql/aggregation/function_adapter" require "elastic_graph/graphql/aggregation/key" require "elastic_graph/graphql/aggregation/nested_sub_aggregation" require "elastic_graph/graphql/aggregation/non_composite_grouping_adapter" require "elastic_graph/graphql/aggregation/path_segment" require "elastic_graph/graphql/aggregation/query" require "elastic_graph/graphql/aggregation/script_term_grouping" -require "elastic_graph/schema_artifacts/runtime_metadata/computation_detail" require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names" module ElasticGraph module AggregationsHelpers - def computation_of(*field_names_in_index, function, computed_field_name: function.to_s, leaf_alias: computed_field_name, field_names_in_graphql_query: field_names_in_index) + def computation_of(*field_names_in_index, function, computed_field_name: function.to_s, leaf_alias: computed_field_name, field_names_in_graphql_query: field_names_in_index, function_args: {}) source_field_path = build_field_path(names_in_index: field_names_in_index, names_in_graphql_query: field_names_in_graphql_query) GraphQL::Aggregation::Computation.new( source_field_path: source_field_path, leaf: GraphQL::Aggregation::PathSegment.new(name_in_graphql_query: leaf_alias, name_in_index: computed_field_name), - detail: SchemaArtifacts::RuntimeMetadata::ComputationDetail.new( - function: function, - empty_bucket_value: (function == :sum || function == :cardinality) ? 0 : nil - ) + function_adapter: GraphQL::Aggregation::FunctionAdapter::BY_NAME.fetch(function), + function_args: function_args ) end diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/aggregation/computation_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/aggregation/computation_spec.rb index 2304f891a..ce5eb8f24 100644 --- a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/aggregation/computation_spec.rb +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/aggregation/computation_spec.rb @@ -67,6 +67,31 @@ module Aggregation expect(computation.clause).to eq({"avg" => {"field" => "foo.bar"}}) end + + it "keys the clause off the adapter's datastore function name and merges the adapter's clause options alongside `field`" do + # A fake adapter for a function whose datastore name differs from its ElasticGraph function name + # and which turns its args into clause options. None of the registered functions do either, so + # there's no real adapter to exercise this part of the interface with. + adapter = Object.new + class << adapter + def datastore_function_name + "some_datastore_fn" + end + + def clause_options(function_args) + {"percents" => function_args.fetch(:percents)} + end + end + + computation = Computation.new( + source_field_path: [PathSegment.new(name_in_graphql_query: "foo", name_in_index: "foo")], + leaf: PathSegment.new(name_in_graphql_query: "myFn", name_in_index: "some_fn"), + function_adapter: adapter, + function_args: {percents: [25, 75]} + ) + + expect(computation.clause).to eq({"some_datastore_fn" => {"field" => "foo", "percents" => [25, 75]}}) + end end end end diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/aggregation/function_adapter_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/aggregation/function_adapter_spec.rb new file mode 100644 index 000000000..854057bee --- /dev/null +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/aggregation/function_adapter_spec.rb @@ -0,0 +1,70 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/graphql/aggregation/function_adapter" +require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names" + +module ElasticGraph + class GraphQL + module Aggregation + RSpec.describe FunctionAdapter do + describe "BY_NAME" do + it "registers an adapter for each supported aggregated value function" do + expect(FunctionAdapter::BY_NAME.keys).to contain_exactly(:avg, :cardinality, :max, :min, :sum) + end + + it "maps each function to its datastore aggregation name" do + expect(FunctionAdapter::BY_NAME.transform_values(&:datastore_function_name)).to eq( + avg: "avg", + cardinality: "cardinality", + max: "max", + min: "min", + sum: "sum" + ) + end + + it "fabricates the empty bucket response each function returns when the datastore has no documents to aggregate" do + expect(FunctionAdapter::BY_NAME.transform_values(&:empty_bucket_result)).to eq( + avg: {"value" => nil}, + cardinality: {"value" => 0}, + max: {"value" => nil}, + min: {"value" => nil}, + sum: {"value" => 0} + ) + end + end + + describe FunctionAdapter::SimpleMetric do + let(:element_names) { SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(form: :snake_case, overrides: {}) } + let(:adapter) { FunctionAdapter::SimpleMetric.new(datastore_function_name: "avg", empty_bucket_value: nil) } + + it "exposes the datastore function name it was configured with" do + expect(adapter.datastore_function_name).to eq "avg" + end + + it "extracts no args, since these functions take none" do + expect(adapter.extract_args({"some_arg" => 3}, element_names)).to eq({}) + end + + it "contributes no extra clause options beyond the `field` the clause builder provides" do + expect(adapter.clause_options({})).to eq({}) + end + + it "returns the raw datastore response as the value hash, since these functions return a flat response" do + expect(adapter.extract_result({"value" => 3.7})).to eq({"value" => 3.7}) + end + + it "fabricates an empty bucket response using the configured empty bucket value" do + expect(adapter.empty_bucket_result).to eq({"value" => nil}) + expect(FunctionAdapter::SimpleMetric.new(datastore_function_name: "sum", empty_bucket_value: 0).empty_bucket_result).to eq({"value" => 0}) + end + end + end + end + end +end diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/field_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/field_spec.rb index 583f57021..bdbf8238d 100644 --- a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/field_spec.rb +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/field_spec.rb @@ -193,22 +193,18 @@ class Schema end end - describe "#computation_detail" do - it "returns the aggregation function from an aggregated values field" do - field = define_schema do |s| + describe "#function_adapter" do + it "resolves an aggregated values field's function name to its registered adapter, and is nil for a field that computes nothing" do + schema = define_schema do |s| s.object_type "Photo" do |t| t.field "id", "ID!" t.field "some_field", "Int" t.index "photos" end - end.field_named("IntAggregatedValues", "exact_sum") - - expect(field.computation_detail).to eq( - SchemaArtifacts::RuntimeMetadata::ComputationDetail.new( - function: :sum, - empty_bucket_value: 0 - ) - ) + end + + expect(schema.field_named("IntAggregatedValues", "exact_sum").function_adapter).to be Aggregation::FunctionAdapter::BY_NAME.fetch(:sum) + expect(schema.field_named("Photo", "some_field").function_adapter).to be nil end end diff --git a/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rb b/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rb deleted file mode 100644 index 0e013abe5..000000000 --- a/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rb +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2024 - 2026 Block, Inc. -# -# Use of this source code is governed by an MIT-style -# license that can be found in the LICENSE file or at -# https://opensource.org/licenses/MIT. -# -# frozen_string_literal: true - -module ElasticGraph - module SchemaArtifacts - module RuntimeMetadata - # Details about our aggregation functions. - # - # @private - class ComputationDetail < ::Data.define(:empty_bucket_value, :function) - FUNCTION = "function" - EMPTY_BUCKET_VALUE = "empty_bucket_value" - - def self.from_hash(hash) - new( - empty_bucket_value: hash[EMPTY_BUCKET_VALUE], - function: hash.fetch(FUNCTION).to_sym - ) - end - - def to_dumpable_hash - { - # Keys here are ordered alphabetically; please keep them that way. - EMPTY_BUCKET_VALUE => empty_bucket_value, - FUNCTION => function.to_s - } - end - end - end - end -end diff --git a/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rb b/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rb index ed7f43ee5..3afea7903 100644 --- a/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rb +++ b/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rb @@ -6,7 +6,6 @@ # # frozen_string_literal: true -require "elastic_graph/schema_artifacts/runtime_metadata/computation_detail" require "elastic_graph/schema_artifacts/runtime_metadata/configured_graphql_resolver" require "elastic_graph/schema_artifacts/runtime_metadata/relation" @@ -14,18 +13,18 @@ module ElasticGraph module SchemaArtifacts module RuntimeMetadata # @private - class GraphQLField < ::Data.define(:name_in_index, :relation, :computation_detail, :resolver) + class GraphQLField < ::Data.define(:name_in_index, :relation, :computation_function, :resolver) EMPTY = new(nil, nil, nil, nil) NAME_IN_INDEX = "name_in_index" RELATION = "relation" - AGGREGATION_DETAIL = "computation_detail" + COMPUTATION_FUNCTION = "computation_function" RESOLVER = "resolver" def self.from_hash(hash) new( name_in_index: hash[NAME_IN_INDEX], relation: hash[RELATION]&.then { |rel_hash| Relation.from_hash(rel_hash) }, - computation_detail: hash[AGGREGATION_DETAIL]&.then { |agg_hash| ComputationDetail.from_hash(agg_hash) }, + computation_function: hash[COMPUTATION_FUNCTION]&.to_sym, resolver: hash[RESOLVER]&.then { |res_hash| ConfiguredGraphQLResolver.from_hash(res_hash) } ) end @@ -33,7 +32,7 @@ def self.from_hash(hash) def to_dumpable_hash { # Keys here are ordered alphabetically; please keep them that way. - AGGREGATION_DETAIL => computation_detail&.to_dumpable_hash, + COMPUTATION_FUNCTION => computation_function&.to_s, NAME_IN_INDEX => name_in_index, RELATION => relation&.to_dumpable_hash, RESOLVER => resolver&.to_dumpable_hash @@ -45,18 +44,11 @@ def to_dumpable_hash # included in the dumped runtime metadata. def needed?(name_in_graphql) !!relation || - !!computation_detail || + !!computation_function || name_in_index&.!=(name_in_graphql) || !resolver.nil? || false end - - def with_computation_detail(empty_bucket_value:, function:) - with(computation_detail: ComputationDetail.new( - empty_bucket_value: empty_bucket_value, - function: function - )) - end end end end diff --git a/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rbs b/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rbs deleted file mode 100644 index ef5970796..000000000 --- a/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rbs +++ /dev/null @@ -1,32 +0,0 @@ -module ElasticGraph - module SchemaArtifacts - module RuntimeMetadata - class ComputationDetailSupertype - attr_reader function: ::Symbol - attr_reader empty_bucket_value: ::Numeric? - - def initialize: ( - function: ::Symbol, - empty_bucket_value: ::Numeric? - ) -> void - - def with: ( - ?function: ::Symbol, - ?empty_bucket_value: ::Numeric? - ) -> instance - - def self.new: - (function: ::Symbol, empty_bucket_value: ::Numeric?) -> instance - | (::Symbol, ::Numeric?) -> instance - end - - class ComputationDetail < ComputationDetailSupertype - FUNCTION: "function" - EMPTY_BUCKET_VALUE: "empty_bucket_value" - - def self.from_hash: (::Hash[::String, untyped]) -> ComputationDetail - def to_dumpable_hash: () -> ::Hash[::String, untyped] - end - end - end -end diff --git a/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rbs b/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rbs index 06c954c27..aa261c6ed 100644 --- a/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rbs +++ b/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rbs @@ -4,43 +4,38 @@ module ElasticGraph class GraphQLFieldSupertype attr_reader name_in_index: ::String? attr_reader relation: Relation? - attr_reader computation_detail: ComputationDetail? + attr_reader computation_function: ::Symbol? attr_reader resolver: ConfiguredGraphQLResolver? def initialize: ( name_in_index: ::String?, relation: Relation?, - computation_detail: ComputationDetail?, + computation_function: ::Symbol?, resolver: ConfiguredGraphQLResolver? ) -> void def with: ( ?name_in_index: ::String?, ?relation: Relation?, - ?computation_detail: ComputationDetail?, + ?computation_function: ::Symbol?, ?resolver: ConfiguredGraphQLResolver? ) -> instance def self.new: - (name_in_index: ::String?, relation: Relation?, computation_detail: ComputationDetail?, resolver: ConfiguredGraphQLResolver?) -> instance - | (::String?, Relation?, ComputationDetail?, ConfiguredGraphQLResolver?) -> instance + (name_in_index: ::String?, relation: Relation?, computation_function: ::Symbol?, resolver: ConfiguredGraphQLResolver?) -> instance + | (::String?, Relation?, ::Symbol?, ConfiguredGraphQLResolver?) -> instance end class GraphQLField < GraphQLFieldSupertype EMPTY: GraphQLField NAME_IN_INDEX: "name_in_index" RELATION: "relation" - AGGREGATION_DETAIL: "computation_detail" + COMPUTATION_FUNCTION: "computation_function" RESOLVER: "resolver" def self.from_hash: (::Hash[::String, untyped]) -> GraphQLField def to_dumpable_hash: () -> ::Hash[::String, untyped] def needed?: (::String) -> bool - - def with_computation_detail: ( - empty_bucket_value: ::Numeric?, - function: ::Symbol - ) -> GraphQLField end end end diff --git a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/graphql_field_spec.rb b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/graphql_field_spec.rb index 1d1403d5d..ab5ccff7c 100644 --- a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/graphql_field_spec.rb +++ b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/graphql_field_spec.rb @@ -19,30 +19,23 @@ module RuntimeMetadata field = GraphQLField.from_hash({}) expect(field).to eq GraphQLField.new( - computation_detail: nil, + computation_function: nil, name_in_index: nil, relation: nil, resolver: nil ) end - it "offers `with_computation_detail` updating aggregation detail" do + it "round-trips `computation_function` through the dumped form, symbolizing it on load" do field = GraphQLField.new( - computation_detail: nil, + computation_function: :sum, name_in_index: nil, relation: nil, resolver: configured_graphql_resolver(:self) ) - updated = field.with_computation_detail( - empty_bucket_value: 0, - function: :sum - ) - - expect(updated.computation_detail).to eq(ComputationDetail.new( - empty_bucket_value: 0, - function: :sum - )) + expect(field.to_dumpable_hash).to include("computation_function" => "sum") + expect(GraphQLField.from_hash(field.to_dumpable_hash).computation_function).to eq :sum end it "exposes `resolver` as nil when it is unset" do diff --git a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/object_type_spec.rb b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/object_type_spec.rb index d2c845018..44b191a5a 100644 --- a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/object_type_spec.rb +++ b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/object_type_spec.rb @@ -72,7 +72,7 @@ module RuntimeMetadata "foo1" => graphql_field_with(name_in_index: "foo1"), "foo2" => graphql_field_with(name_in_index: "foo2_in_index"), "foo3" => graphql_field_with(name_in_index: "foo3", relation: relation_with), - "foo4" => graphql_field_with(name_in_index: "foo4", computation_detail: computation_detail_with), + "foo4" => graphql_field_with(name_in_index: "foo4", computation_function: :sum), "foo5" => graphql_field_with(resolver: :other, name_in_index: "foo5"), "foo6" => graphql_field_with(name_in_index: nil) } diff --git a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_spec.rb b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_spec.rb index 058254673..cd1709d4a 100644 --- a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_spec.rb +++ b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_spec.rb @@ -63,13 +63,13 @@ module RuntimeMetadata ], graphql_fields_by_name: { "name_graphql" => GraphQLField.new( - computation_detail: nil, + computation_function: nil, name_in_index: "name_index", relation: nil, resolver: ConfiguredGraphQLResolver.new(:self, {arg1: 17}) ), "parent" => GraphQLField.new( - computation_detail: nil, + computation_function: nil, name_in_index: "parent", relation: Relation.new( foreign_key: "grandparents.parents.some_id", @@ -80,10 +80,7 @@ module RuntimeMetadata resolver: ConfiguredGraphQLResolver.new(:self, {}) ), "sum" => GraphQLField.new( - computation_detail: ComputationDetail.new( - empty_bucket_value: 0, - function: :sum - ), + computation_function: :sum, name_in_index: "sum", relation: nil, resolver: ConfiguredGraphQLResolver.new(:self, {}) @@ -212,10 +209,7 @@ module RuntimeMetadata "resolver" => {"name" => "self"} }, "sum" => { - "computation_detail" => { - "empty_bucket_value" => 0, - "function" => "sum" - }, + "computation_function" => "sum", "resolver" => {"name" => "self"} } }, @@ -348,7 +342,7 @@ module RuntimeMetadata "FieldsByGraphQLOnly" => object_type_with(graphql_fields_by_name: { "name_graphql" => GraphQLField.new( name_in_index: "name_index", - computation_detail: nil, + computation_function: nil, relation: nil, resolver: ConfiguredGraphQLResolver.new(:self, {}) ) diff --git a/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec b/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec index 2b48234c4..251ec6405 100644 --- a/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec +++ b/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec @@ -41,7 +41,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = [">= 3.4", "< 4.1"] - spec.add_dependency "elasticgraph-graphql", ElasticGraph::VERSION # needed since we validate that scalar `coerce_with` options are valid (which loads scalar coercion adapters) + spec.add_dependency "elasticgraph-graphql", ElasticGraph::VERSION # needed since we validate that scalar `coerce_with` options and aggregation function names are valid (which loads scalar coercion adapters and the aggregation function adapter registry) spec.add_dependency "elasticgraph-indexer", ElasticGraph::VERSION # needed since we validate that scalar `prepare_for_indexing_with` options are valid (which loads indexing preparer adapters) spec.add_dependency "elasticgraph-schema_artifacts", ElasticGraph::VERSION spec.add_dependency "elasticgraph-support", ElasticGraph::VERSION diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/factory.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/factory.rb index b32611fac..8c4c7735f 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/factory.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/factory.rb @@ -370,7 +370,7 @@ def new_aggregated_values_type_for_index_leaf_type(index_leaf_type) it usually differs from the true distinct value count by less than 7%. EOS - f.runtime_metadata_computation_detail empty_bucket_value: 0, function: :cardinality + f.computes :cardinality end yield type diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb index 783f05be7..adfe0106f 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb @@ -681,7 +681,7 @@ def register_standard_graphql_scalars t.customize_aggregated_values_type do |avt| # not nullable, since sum(empty_set) == 0 avt.field names.approximate_sum, "Float!", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: 0, function: :sum + f.computes :sum f.documentation <<~EOS The sum of the field values within this grouping. @@ -701,7 +701,7 @@ def register_standard_graphql_scalars end avt.field names.approximate_avg, "Float", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: nil, function: :avg + f.computes :avg f.documentation <<~EOS The average (mean) of the field values within this grouping. @@ -1004,7 +1004,7 @@ def register_custom_elastic_graph_scalars t.customize_aggregated_values_type do |avt| # not nullable, since sum(empty_set) == 0 avt.field names.approximate_sum, "Float!", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: 0, function: :sum + f.computes :sum f.documentation <<~EOS The (approximate) sum of the field values within this grouping. @@ -1016,7 +1016,7 @@ def register_custom_elastic_graph_scalars end avt.field names.exact_sum, "JsonSafeLong", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: 0, function: :sum + f.computes :sum f.documentation <<~EOS The exact sum of the field values within this grouping, if it fits in a `JsonSafeLong`. @@ -1045,7 +1045,7 @@ def register_custom_elastic_graph_scalars names.exact_max => [:max, "maximum", names.approximate_max, "largest"] }.each do |exact_name, (func, full_name, approx_name, adjective)| avt.field approx_name, "LongString", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: nil, function: func + f.computes func f.documentation <<~EOS The #{full_name} of the field values within this grouping. @@ -1060,7 +1060,7 @@ def register_custom_elastic_graph_scalars end avt.field names.approximate_avg, "Float", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: nil, function: :avg + f.computes :avg f.documentation <<~EOS The average (mean) of the field values within this grouping. @@ -1532,7 +1532,7 @@ def define_integral_aggregated_values_for(scalar_type, long_type: "JsonSafeLong" scalar_type.customize_aggregated_values_type do |t| # not nullable, since sum(empty_set) == 0 t.field names.approximate_sum, "Float!", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: 0, function: :sum + f.computes :sum f.documentation <<~EOS The (approximate) sum of the field values within this grouping. @@ -1544,7 +1544,7 @@ def define_integral_aggregated_values_for(scalar_type, long_type: "JsonSafeLong" end t.field names.exact_sum, long_type, graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: 0, function: :sum + f.computes :sum f.documentation <<~EOS The exact sum of the field values within this grouping, if it fits in a `#{long_type}`. @@ -1563,7 +1563,7 @@ def define_integral_aggregated_values_for(scalar_type, long_type: "JsonSafeLong" end t.field names.approximate_avg, "Float", graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: nil, function: :avg + f.computes :avg f.documentation <<~EOS The average (mean) of the field values within this grouping. @@ -1580,7 +1580,7 @@ def define_exact_min_max_and_approx_avg_on_aggregated_values(aggregated_values_t define_exact_min_and_max_on_aggregated_values(aggregated_values_type, scalar_type, &block) aggregated_values_type.field names.approximate_avg, scalar_type, graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: nil, function: :avg + f.computes :avg f.documentation <<~EOS The average (mean) of the field values within this grouping. @@ -1597,7 +1597,7 @@ def define_exact_min_and_max_on_aggregated_values(aggregated_values_type, scalar discussion = yield(adjective: adjective, full_name: full_name) aggregated_values_type.field name, scalar_type, graphql_only: true do |f| - f.runtime_metadata_computation_detail empty_bucket_value: nil, function: func + f.computes func f.documentation ["The #{full_name} of the field values within this grouping.", discussion].compact.join("\n\n") end diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/field.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/field.rb index 856c2eea4..fda7ebc63 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/field.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/field.rb @@ -8,6 +8,7 @@ require "delegate" require "elastic_graph/constants" +require "elastic_graph/graphql/aggregation/function_adapter" require "elastic_graph/schema_artifacts/runtime_metadata/configured_graphql_resolver" require "elastic_graph/schema_definition/indexing/field" require "elastic_graph/schema_definition/indexing/field_reference" @@ -83,7 +84,7 @@ module SchemaElements # @private # @!attribute [rw] singular_name # @private - # @!attribute [rw] computation_detail + # @!attribute [rw] computation_function # @private # @!attribute [rw] as_input # @private @@ -93,7 +94,7 @@ class Field < Struct.new( :aggregated_values_customizations, :sort_order_enum_value_customizations, :args, :sortable, :filterable, :aggregatable, :groupable, :highlightable, :returnable, :graphql_only, :source, :runtime_field_script, :relationship, :singular_name, - :computation_detail, :as_input, + :computation_function, :as_input, :name_in_index, :resolver ) include Mixins::HasDocumentation @@ -1091,24 +1092,27 @@ def nested? mapping_type == "nested" end - # Records the `ComputationDetail` that should be on the `runtime_metadata_graphql_field`. + # Records the aggregation function this field computes, which the query engine resolves to a + # {ElasticGraph::GraphQL::Aggregation::FunctionAdapter} when it builds the field. # # @private - def runtime_metadata_computation_detail(empty_bucket_value:, function:) - self.computation_detail = SchemaArtifacts::RuntimeMetadata::ComputationDetail.new( - empty_bucket_value: empty_bucket_value, - function: function - ) + def computes(function) + unless GraphQL::Aggregation::FunctionAdapter::BY_NAME.key?(function) + raise Errors::SchemaError, "`#{parent_type.name}.#{name}` has an unregistered aggregation function: `#{function}`. " \ + "Valid functions are: #{GraphQL::Aggregation::FunctionAdapter::BY_NAME.keys.map(&:inspect).join(", ")}." + end + + self.computation_function = function end - # Lazily creates and returns a GraphQLField using the field's {#name_in_index}, {#computation_detail}, + # Lazily creates and returns a GraphQLField using the field's {#name_in_index}, {#computation_function}, # and {#relationship}. # # @private def runtime_metadata_graphql_field SchemaArtifacts::RuntimeMetadata::GraphQLField.new( name_in_index: name_in_index, - computation_detail: computation_detail, + computation_function: computation_function, relation: relationship&.runtime_metadata, resolver: resolver ) diff --git a/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/schema_elements/field.rbs b/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/schema_elements/field.rbs index e1b34cdc6..bd7b0bab5 100644 --- a/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/schema_elements/field.rbs +++ b/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/schema_elements/field.rbs @@ -18,7 +18,7 @@ module ElasticGraph attr_reader parent_type: indexableType attr_reader schema_def_state: State attr_reader accuracy_confidence: accuracyConfidence - attr_accessor computation_detail: SchemaArtifacts::RuntimeMetadata::ComputationDetail + attr_accessor computation_function: ::Symbol? attr_reader filter_customizations: ::Array[^(Field) -> void] attr_reader sort_order_enum_value_customizations: ::Array[^(SortOrderEnumValue) -> void] attr_reader source: FieldSource? @@ -93,7 +93,7 @@ module ElasticGraph def nested?: () -> bool - def runtime_metadata_computation_detail: (empty_bucket_value: ::Numeric?, function: ::Symbol) -> void + def computes: (::Symbol) -> void def runtime_metadata_graphql_field: () -> SchemaArtifacts::RuntimeMetadata::GraphQLField def backing_indexing_field: () -> Field? diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/for_built_in_types_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/for_built_in_types_spec.rb index b37136130..3dcf40f26 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/for_built_in_types_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/for_built_in_types_spec.rb @@ -24,13 +24,13 @@ module SchemaDefinition end expect(metadata.elasticgraph_category).to eq :scalar_aggregated_values - expect(metadata.graphql_fields_by_name.transform_values(&:computation_detail)).to eq( - "approximate_avg" => agg_detail_of(:avg, nil), - "approximate_sum" => agg_detail_of(:sum, 0), - "exact_sum" => agg_detail_of(:sum, 0), - "exact_max" => agg_detail_of(:max, nil), - "exact_min" => agg_detail_of(:min, nil), - "approximate_distinct_value_count" => agg_detail_of(:cardinality, 0) + expect(metadata.graphql_fields_by_name.transform_values(&:computation_function)).to eq( + "approximate_avg" => :avg, + "approximate_sum" => :sum, + "exact_sum" => :sum, + "exact_max" => :max, + "exact_min" => :min, + "approximate_distinct_value_count" => :cardinality ) end @@ -44,12 +44,12 @@ module SchemaDefinition end expect(metadata.elasticgraph_category).to eq :scalar_aggregated_values - expect(metadata.graphql_fields_by_name.transform_values(&:computation_detail)).to eq( - "approximate_avg" => agg_detail_of(:avg, nil), - "approximate_sum" => agg_detail_of(:sum, 0), - "exact_max" => agg_detail_of(:max, nil), - "exact_min" => agg_detail_of(:min, nil), - "approximate_distinct_value_count" => agg_detail_of(:cardinality, 0) + expect(metadata.graphql_fields_by_name.transform_values(&:computation_function)).to eq( + "approximate_avg" => :avg, + "approximate_sum" => :sum, + "exact_max" => :max, + "exact_min" => :min, + "approximate_distinct_value_count" => :cardinality ) end @@ -63,13 +63,13 @@ module SchemaDefinition end expect(metadata.elasticgraph_category).to eq :scalar_aggregated_values - expect(metadata.graphql_fields_by_name.transform_values(&:computation_detail)).to eq( - "approximate_avg" => agg_detail_of(:avg, nil), - "approximate_sum" => agg_detail_of(:sum, 0), - "exact_sum" => agg_detail_of(:sum, 0), - "exact_max" => agg_detail_of(:max, nil), - "exact_min" => agg_detail_of(:min, nil), - "approximate_distinct_value_count" => agg_detail_of(:cardinality, 0) + expect(metadata.graphql_fields_by_name.transform_values(&:computation_function)).to eq( + "approximate_avg" => :avg, + "approximate_sum" => :sum, + "exact_sum" => :sum, + "exact_max" => :max, + "exact_min" => :min, + "approximate_distinct_value_count" => :cardinality ) end @@ -83,15 +83,15 @@ module SchemaDefinition end expect(metadata.elasticgraph_category).to eq :scalar_aggregated_values - expect(metadata.graphql_fields_by_name.transform_values(&:computation_detail)).to eq( - "approximate_avg" => agg_detail_of(:avg, nil), - "approximate_sum" => agg_detail_of(:sum, 0), - "exact_sum" => agg_detail_of(:sum, 0), - "approximate_max" => agg_detail_of(:max, nil), - "exact_max" => agg_detail_of(:max, nil), - "approximate_min" => agg_detail_of(:min, nil), - "exact_min" => agg_detail_of(:min, nil), - "approximate_distinct_value_count" => agg_detail_of(:cardinality, 0) + expect(metadata.graphql_fields_by_name.transform_values(&:computation_function)).to eq( + "approximate_avg" => :avg, + "approximate_sum" => :sum, + "exact_sum" => :sum, + "approximate_max" => :max, + "exact_max" => :max, + "approximate_min" => :min, + "exact_min" => :min, + "approximate_distinct_value_count" => :cardinality ) end end @@ -122,13 +122,6 @@ module SchemaDefinition end end - def agg_detail_of(function, empty_bucket_value) - SchemaArtifacts::RuntimeMetadata::ComputationDetail.new( - function: function, - empty_bucket_value: empty_bucket_value - ) - end - prepend Module.new { def object_type_metadata_for(...) super(...).tap do |metadata| diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/graphql_fields_by_name_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/graphql_fields_by_name_spec.rb index 4750cfabc..f2d2dc0cf 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/graphql_fields_by_name_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/graphql_fields_by_name_spec.rb @@ -87,6 +87,34 @@ module SchemaDefinition }) end + it "dumps the aggregation function recorded by `computes`" do + metadata = object_type_metadata_for "Widget" do |s| + s.object_type "Widget" do |t| + t.field "id", "ID" + t.field "total", "Int", graphql_only: true do |f| + f.computes :sum + end + end + end + + expect(metadata.graphql_fields_by_name.fetch("total").computation_function).to eq :sum + end + + it "raises an error when `computes` is given an unregistered function name, since the query engine would have no adapter for it" do + expect { + object_type_metadata_for "Widget" do |s| + s.object_type "Widget" do |t| + t.field "id", "ID" + t.field "total", "Int", graphql_only: true do |f| + f.computes :not_a_real_function + end + end + end + }.to raise_error Errors::SchemaError, a_string_including( + "`Widget.total` has an unregistered aggregation function: `not_a_real_function`." + ) + end + it "raises an error when a custom `Query` field lacks a resolver, as it won't be resolvable by elasticgraph-graphql" do expect { object_type_metadata_for "Query" do |s| diff --git a/spec_support/lib/elastic_graph/spec_support/runtime_metadata_support.rb b/spec_support/lib/elastic_graph/spec_support/runtime_metadata_support.rb index 33aee9181..401642705 100644 --- a/spec_support/lib/elastic_graph/spec_support/runtime_metadata_support.rb +++ b/spec_support/lib/elastic_graph/spec_support/runtime_metadata_support.rb @@ -102,13 +102,6 @@ def normal_indexing_update_target_with( ) end - def computation_detail_with(empty_bucket_value: 0, function: :sum) - ComputationDetail.new( - empty_bucket_value: empty_bucket_value, - function: function - ) - end - def dynamic_param_with(source_path: "some_field", cardinality: :one) DynamicParam.new(source_path: source_path, cardinality: cardinality) end @@ -162,9 +155,9 @@ def relation_with(foreign_key: "some_id", direction: :asc, additional_filter: {} Relation.new(foreign_key: foreign_key, direction: direction, additional_filter: additional_filter, foreign_key_nested_paths: foreign_key_nested_paths) end - def graphql_field_with(name_in_index: "name_index", relation: nil, computation_detail: nil, resolver: nil) + def graphql_field_with(name_in_index: "name_index", relation: nil, computation_function: nil, resolver: nil) GraphQLField.new( - computation_detail: computation_detail, + computation_function: computation_function, name_in_index: name_in_index, relation: relation, resolver: resolver