Skip to content

Commit 8d55f90

Browse files
Prepare datastore_core and schema_artifacts for nested sourced_from fields. (#1255)
This is the parts of #1252 which are ready to merge. Co-authored-by: ellisandrews-toast <ellis.andrews@toasttab.com>
2 parents ff0d1e9 + 679265f commit 8d55f90

10 files changed

Lines changed: 109 additions & 9 deletions

File tree

elasticgraph-datastore_core/lib/elastic_graph/datastore_core/index_definition.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def self.with(name:, runtime_metadata:, config:, datastore_clients_by_name:, sch
3333
env_index_config: env_index_config,
3434
defined_clusters: config.clusters.keys.to_set,
3535
datastore_clients_by_name: datastore_clients_by_name,
36-
has_had_multiple_sources: runtime_metadata.has_had_multiple_sources
36+
has_had_multiple_sources: runtime_metadata.has_had_multiple_sources,
37+
sourced_from_nested_paths_by_qualified_relationship: runtime_metadata.sourced_from_nested_paths_by_qualified_relationship
3738
}
3839

3940
if (rollover = runtime_metadata.rollover)

elasticgraph-datastore_core/lib/elastic_graph/datastore_core/index_definition/base.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ def list_counts_field_paths_for_source(source)
130130
@list_counts_field_paths_for_source[source] ||= identify_list_counts_field_paths_for_source(source)
131131
end
132132

133+
# The value of the painless `index_data` script's `sourcedFromNestedPaths` param: nested `sourced_from`
134+
# paths keyed by qualified relationship. Empty when the index has no nested sourced fields.
135+
def sourced_from_nested_paths_as_painless_param
136+
@sourced_from_nested_paths_as_painless_param ||= sourced_from_nested_paths_by_qualified_relationship.transform_values do |segments|
137+
segments.map(&:to_painless_hash)
138+
end
139+
end
140+
133141
def to_s
134142
"#<#{self.class.name} #{name}>"
135143
end

elasticgraph-datastore_core/lib/elastic_graph/datastore_core/index_definition/index.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@ class DatastoreCore
1515
module IndexDefinition
1616
class Index < Support::MemoizableData.define(
1717
:name, :route_with, :default_sort_clauses, :current_sources, :fields_by_path,
18-
:env_index_config, :defined_clusters, :datastore_clients_by_name, :env_agnostic_settings, :has_had_multiple_sources
18+
:env_index_config, :defined_clusters, :datastore_clients_by_name, :env_agnostic_settings, :has_had_multiple_sources,
19+
:sourced_from_nested_paths_by_qualified_relationship
1920
)
2021
# `Data.define` provides all these methods:
2122
# @dynamic name, route_with, default_sort_clauses, current_sources, fields_by_path, env_index_config, env_agnostic_settings
2223
# @dynamic defined_clusters, datastore_clients_by_name, initialize, has_had_multiple_sources
24+
# @dynamic sourced_from_nested_paths_by_qualified_relationship
2325

2426
# `include IndexDefinition::Base` provides all these methods. Steep should be able to detect it
2527
# but can't for some reason so we have to declare them with `@dynamic`.
2628
# @dynamic flattened_env_setting_overrides, routing_value_for_prepared_record, has_custom_routing?, cluster_to_query
2729
# @dynamic clusters_to_index_into, all_accessible_cluster_names, ignored_values_for_routing, searches_could_hit_incomplete_docs?, max_result_window
2830
# @dynamic accessible_cluster_names_to_index_into, accessible_from_queries?, known_related_query_rollover_indices, list_counts_field_paths_for_source
31+
# @dynamic sourced_from_nested_paths_as_painless_param
2932
include IndexDefinition::Base
3033

3134
def mappings_in_datastore(datastore_client)

elasticgraph-datastore_core/lib/elastic_graph/datastore_core/index_definition/rollover_index_template.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@ module IndexDefinition
2323
class RolloverIndexTemplate < Support::MemoizableData.define(
2424
:name, :route_with, :default_sort_clauses, :current_sources, :fields_by_path, :env_index_config,
2525
:index_args, :defined_clusters, :datastore_clients_by_name, :timestamp_field_path, :frequency,
26-
:env_agnostic_settings, :has_had_multiple_sources
26+
:env_agnostic_settings, :has_had_multiple_sources, :sourced_from_nested_paths_by_qualified_relationship
2727
)
2828
# `Data.define` provides all these methods:
2929
# @dynamic name, route_with, default_sort_clauses, current_sources, fields_by_path, env_index_config, env_agnostic_settings
3030
# @dynamic index_args, defined_clusters, datastore_clients_by_name, timestamp_field_path, frequency, initialize, has_had_multiple_sources
31+
# @dynamic sourced_from_nested_paths_by_qualified_relationship
3132

3233
# `include IndexDefinition::Base` provides all these methods. Steep should be able to detect it
3334
# but can't for some reason so we have to declare them with `@dynamic`.
3435
# @dynamic flattened_env_setting_overrides, routing_value_for_prepared_record, has_custom_routing?, cluster_to_query
3536
# @dynamic clusters_to_index_into, all_accessible_cluster_names, ignored_values_for_routing, searches_could_hit_incomplete_docs?, max_result_window
3637
# @dynamic accessible_cluster_names_to_index_into, accessible_from_queries?, known_related_query_rollover_indices, list_counts_field_paths_for_source
38+
# @dynamic sourced_from_nested_paths_as_painless_param
3739
include IndexDefinition::Base
3840

3941
def mappings_in_datastore(datastore_client)

elasticgraph-datastore_core/sig/elastic_graph/datastore_core/index_definition.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module ElasticGraph
1616
def accessible_from_queries?: () -> bool
1717
def known_related_query_rollover_indices: () -> ::Array[IndexDefinition::RolloverIndex]
1818
def list_counts_field_paths_for_source: (::String) -> ::Set[::String]
19+
def sourced_from_nested_paths_as_painless_param: () -> ::Hash[::String, ::Array[::Hash[::String, ::String]]]
1920
end
2021

2122
# Defines methods of the _IndexDefinition interface that each specific implementation must provide.
@@ -26,6 +27,7 @@ module ElasticGraph
2627
def current_sources: () -> ::Set[::String]
2728
def fields_by_path: () -> ::Hash[::String, SchemaArtifacts::RuntimeMetadata::IndexField]
2829
def has_had_multiple_sources: () -> bool
30+
def sourced_from_nested_paths_by_qualified_relationship: () -> ::Hash[::String, ::Array[SchemaArtifacts::RuntimeMetadata::sourcedFromNestedPathSegment]]
2931
def env_index_config: () -> Configuration::IndexDefinition
3032
def env_agnostic_settings: () -> ::Hash[::String, untyped]
3133
def defined_clusters: () -> ::Set[::String]

elasticgraph-datastore_core/sig/elastic_graph/datastore_core/index_definition/base.rbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module ElasticGraph
1010
@known_related_query_rollover_indices: ::Array[RolloverIndex]?
1111
@searches_could_hit_incomplete_docs: bool
1212
@list_counts_field_paths_for_source: ::Hash[::String, ::Set[::String]]
13+
@sourced_from_nested_paths_as_painless_param: ::Hash[::String, ::Array[::Hash[::String, ::String]]]?
1314
@max_result_window: ::Integer?
1415

1516
def to_s: () -> ::String

elasticgraph-datastore_core/spec/unit/elastic_graph/datastore_core/index_definition/implementation_shared_examples.rb

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,16 +544,62 @@ def accessible_cluster_names_to_index_into_for_config(clusters:, index_definitio
544544
index.list_counts_field_paths_for_source(SELF_RELATIONSHIP_NAME)
545545
)
546546
end
547+
end
548+
549+
describe "#sourced_from_nested_paths_as_painless_param" do
550+
it "returns an empty hash for an index with no nested `sourced_from` fields" do
551+
index = define_index
547552

548-
def update_type_for_index(schema)
549-
yield schema.state.object_types_by_name.fetch("MyType")
553+
expect(index.sourced_from_nested_paths_as_painless_param).to eq({})
554+
end
555+
556+
it "converts the registered nested paths into the painless hash form, keyed by qualified relationship" do
557+
index = define_index(schema_def: lambda do |schema|
558+
schema.object_type "NestedThing" do |t|
559+
t.field "id", "ID!"
560+
t.field "name", "String" do |f|
561+
f.sourced_from "related_thing", "name"
562+
end
563+
t.relates_to_one "related_thing", "RelatedThing", via: "nested_thing_id", dir: :in, indexing_only: true do |r|
564+
r.parent_relationship "MyType", "related_things"
565+
end
566+
end
567+
568+
schema.object_type "RelatedThing" do |t|
569+
t.field "id", "ID!"
570+
t.field "my_type_id", "ID"
571+
t.field "nested_thing_id", "ID"
572+
t.field "name", "String"
573+
t.field "created_at", "DateTime!"
574+
t.index "related_things"
575+
end
576+
577+
update_type_for_index(schema) do |t|
578+
t.field "nested_things", "[NestedThing!]!" do |f|
579+
f.mapping type: "object"
580+
end
581+
# `equivalent_field` is required because `MyType` may be a rollover index, so the indexer
582+
# needs to know which `RelatedThing` timestamp selects the `MyType` index to update.
583+
t.relates_to_many "related_things", "RelatedThing", via: "my_type_id", dir: :in, indexing_only: true, singular: "related_thing" do |r|
584+
r.equivalent_field "created_at"
585+
end
586+
end
587+
end) { |i| i.has_had_multiple_sources! }
588+
589+
expect(index.sourced_from_nested_paths_as_painless_param).to eq(
590+
"nested_things.related_thing" => [{"field" => "nested_things", "sourceField" => "nested_thing_id"}]
591+
)
550592
end
551593
end
552594

553595
def define_index(name = "my_type", **options, &block)
554596
define_datastore_core_with_index(name, **options, &block)
555597
.index_definitions_by_name.fetch(name)
556598
end
599+
600+
def update_type_for_index(schema)
601+
yield schema.state.object_types_by_name.fetch("MyType")
602+
end
557603
end
558604
end
559605
end

elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/sourced_from_nested_path_segment.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ def self.from_hash(hash)
2525
# always on `id` (relationships join on `id` via foreign keys), so it's implicit rather
2626
# than stored here.
2727
#
28-
# A future PR will add `to_painless_param` to convert these segments into the
29-
# camelCase hash format expected by the painless script (with a "type" discriminator).
30-
#
3128
# @private
3229
class ListPathSegment < ::Data.define(:field, :source_field)
3330
FIELD = "field"
@@ -41,10 +38,14 @@ def to_dumpable_hash
4138
def self.from_hash(hash)
4239
new(field: hash[FIELD], source_field: hash[SOURCE_FIELD])
4340
end
41+
42+
# The painless script expects camelCase and discriminates list segments by the presence of `sourceField`.
43+
def to_painless_hash
44+
{"field" => field, "sourceField" => source_field}
45+
end
4446
end
4547

4648
# Represents a segment in a nested sourced path that navigates into an object field.
47-
# See `ListPathSegment` for notes on `to_painless_param`.
4849
#
4950
# @private
5051
class ObjectPathSegment < ::Data.define(:field)
@@ -58,6 +59,11 @@ def to_dumpable_hash
5859
def self.from_hash(hash)
5960
new(field: hash[FIELD])
6061
end
62+
63+
# No `sourceField`, which is how the painless script tells object segments from list segments.
64+
def to_painless_hash
65+
{"field" => field}
66+
end
6167
end
6268
end
6369
end

elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/sourced_from_nested_path_segment.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module ElasticGraph
1414

1515
def self.from_hash: (::Hash[::String, untyped]) -> ListPathSegment
1616
def to_dumpable_hash: () -> ::Hash[::String, ::String]
17+
def to_painless_hash: () -> ::Hash[::String, ::String]
1718
end
1819

1920
class ObjectPathSegmentSuperType
@@ -27,6 +28,7 @@ module ElasticGraph
2728

2829
def self.from_hash: (::Hash[::String, untyped]) -> ObjectPathSegment
2930
def to_dumpable_hash: () -> ::Hash[::String, ::String]
31+
def to_painless_hash: () -> ::Hash[::String, ::String]
3032
end
3133

3234
type sourcedFromNestedPathSegment = ListPathSegment | ObjectPathSegment
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2024 - 2026 Block, Inc.
2+
#
3+
# Use of this source code is governed by an MIT-style
4+
# license that can be found in the LICENSE file or at
5+
# https://opensource.org/licenses/MIT.
6+
#
7+
# frozen_string_literal: true
8+
9+
require "elastic_graph/schema_artifacts/runtime_metadata/sourced_from_nested_path_segment"
10+
11+
module ElasticGraph
12+
module SchemaArtifacts
13+
module RuntimeMetadata
14+
RSpec.describe SourcedFromNestedPathSegment do
15+
it "converts a list segment to a painless hash with a camelCased `sourceField`" do
16+
segment = ListPathSegment.new(field: "players", source_field: "playerId")
17+
18+
expect(segment.to_painless_hash).to eq("field" => "players", "sourceField" => "playerId")
19+
end
20+
21+
it "converts an object segment to a painless hash without a `sourceField` (which marks it an object segment)" do
22+
segment = ObjectPathSegment.new(field: "roster")
23+
24+
expect(segment.to_painless_hash).to eq("field" => "roster")
25+
end
26+
end
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)