Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/sql-test-and-build-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
if [ "${{ matrix.test-type }}" = "unit" ]; then
./gradlew --continue build -x spotlessCheck -x integTest -x tracingIntegTest -x yamlRestTest -x doctest
elif [ "${{ matrix.test-type }}" = "integration" ]; then
./gradlew --continue integTest tracingIntegTest yamlRestTest
./gradlew --continue integTest tracingIntegTest yamlRestTest mappingConflictIntegTest

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason mappingConflictIntegTest not part of integTest?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because A stats ... by over a field that is keyword in one backing index and a number or date in the other, the #5811 case, trips the InternalComposite$InternalBucket.compareKey assert in OpenSearch core. The integTest node runs with assertions on, so the first such query kills the node and fails every IT after it. This task runs them on their own node with only that assert off, as production runs. It follows tracingIntegTest, which also runs on its own cluster beside integTest.

else
./gradlew --continue doctest
fi
Expand Down Expand Up @@ -213,7 +213,7 @@ jobs:
if [ "${{ matrix.test-type }}" = "unit" ]; then
./gradlew --continue build -x spotlessCheck -x integTest -x tracingIntegTest -x yamlRestTest -x doctest ${{ matrix.entry.os_build_args }}
elif [ "${{ matrix.test-type }}" = "integration" ]; then
./gradlew --continue integTest tracingIntegTest yamlRestTest ${{ matrix.entry.os_build_args }}
./gradlew --continue integTest tracingIntegTest yamlRestTest mappingConflictIntegTest ${{ matrix.entry.os_build_args }}
else
./gradlew --continue doctest ${{ matrix.entry.os_build_args }}
fi
Expand Down
53 changes: 53 additions & 0 deletions integ-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,17 @@ testClusters {
'io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter'
setting 'telemetry.otel.tracer.exporter.delay', '50ms'
}
mappingConflictIntegTest {
testDistribution = 'archive'
plugin(getJobSchedulerPlugin())
plugin ":opensearch-sql-plugin"
setting "plugins.query.datasources.encryption.masterkey", "1234567812345678"
// Mixed-type bucket keys trip the key-class assert in InternalBucket.compareKey, and the
// AssertionError exits the node. Off here only, so the node returns the 500 a production
// node returns. Named by the top-level class, because a nested class takes its assertion
// status from there. The shared integTest cluster keeps every assert.
jvmArgs '-da:org.opensearch.search.aggregations.bucket.composite.InternalComposite'
}
yamlRestTest {
testDistribution = 'archive'
plugin(getJobSchedulerPlugin())
Expand Down Expand Up @@ -917,6 +928,9 @@ integTest {

exclude 'org/opensearch/sql/calcite/tracing/**'

// Runs in :mappingConflictIntegTest, whose cluster disables the InternalComposite asserts.
exclude 'org/opensearch/sql/calcite/remote/CalciteOtel*DataStreamIT.class'

// Explain IT is dependent on internal implementation of old engine so it's not necessary
// to run these with new engine and not necessary to make this consistent with old engine.
exclude 'org/opensearch/sql/legacy/ExplainIT.class'
Expand Down Expand Up @@ -1026,6 +1040,40 @@ task tracingIntegTest(type: RestIntegTestTask) {
}
}

task mappingConflictIntegTest(type: RestIntegTestTask) {
useCluster testClusters.mappingConflictIntegTest
dependsOn ':opensearch-sql-plugin:bundlePlugin'

systemProperty 'tests.security.manager', 'false'
systemProperty 'project.root', project.projectDir.absolutePath
systemProperty 'defaultQuerySizeLimit', '10000'

doFirst {
getClusters().forEach { cluster ->
String allHttpSocketURI = cluster.nodes.stream().flatMap { node ->
node.getAllHttpSocketURI().stream()
}.collect(Collectors.joining(","))
String allTransportSocketURI = cluster.nodes.stream().flatMap { node ->
node.getAllTransportPortURI().stream()
}.collect(Collectors.joining(","))
systemProperty "tests.rest.${cluster.name}.http_hosts", "${-> allHttpSocketURI}"
systemProperty "tests.rest.${cluster.name}.transport_hosts", "${-> allTransportSocketURI}"
}
systemProperty "https", "false"
systemProperty "user", "admin"
systemProperty "password", "admin"
systemProperty 'cluster.debug', getDebug()
}

testLogging {
events "passed", "skipped", "failed"
}

filter {
includeTestsMatching 'org.opensearch.sql.calcite.remote.CalciteOtel*DataStreamIT'
}
}

task comparisonTest(type: RestIntegTestTask) {
testLogging {
events "passed", "skipped", "failed"
Expand Down Expand Up @@ -1596,4 +1644,9 @@ task integTestRemote(type: RestIntegTestTask) {
// externally provided cluster, so these suites cannot pass here. integTest still covers them.
exclude 'org/opensearch/sql/datasource/DataSourceAPIsIT.class'
exclude 'org/opensearch/sql/datasource/DataSourceEnabledIT.class'

// Suites that list the known 500s of one build on a single node with the InternalComposite
// asserts off. An external cluster can add nodes, which redraws the merge winner per request,
// and cannot drop the assert, so they run only in :mappingConflictIntegTest.
exclude 'org/opensearch/sql/calcite/remote/CalciteOtel*DataStreamIT.class'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite.remote;

import java.io.IOException;
import org.junit.Test;
import org.opensearch.sql.util.OtelDataStream;

/** Schema evolution across the backing indices of an OTel logs data stream. */
public class CalciteOtelLogsDataStreamIT extends OtelDataStreamConflictTestCase {

@Override
protected OtelDataStream create(String name) throws IOException {
return OtelDataStream.logs(client(), name);
}

@Test
public void typeDrifts() throws IOException {
runDrifts("otel-logs-drift");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite.remote;

import java.io.IOException;
import org.junit.Test;
import org.opensearch.sql.util.OtelDataStream;

/** Schema evolution across the backing indices of an OTel traces data stream. */
public class CalciteOtelTracesDataStreamIT extends OtelDataStreamConflictTestCase {

@Override
protected OtelDataStream create(String name) throws IOException {
return OtelDataStream.traces(client(), name);
}

@Test
public void typeDrifts() throws IOException {
runDrifts("otel-traces-drift");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite.remote;

import java.util.List;

/**
* The data the OTel data stream ITs run over, meaning the field types the drifts cross, the PPL
* commands, and the known 500s with their issues. {@link OtelDataStreamConflictTestCase} holds the
* flow, so a fix that clears a 500 edits only this file.
*/
final class MappingConflictFixtures {

/** The field types the drifts cross, each against the others and against the field absent. */
static final List<String> TYPES =
List.of("text", "keyword", "byte", "integer", "long", "date", "object");
Comment on lines +18 to +19

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to support future supported TYPES, e.g. flat_object, #5830

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A type the fixture already holds is one entry in TYPES, and the run then pairs it with every other type. flat_object is not among the 22 types the fixture holds under attributes.types, so it also needs a field and base values in both data files and a mapping in both templates, since dynamic mapping never produces flat_object.

I tried it on main with #5830. Against keyword, the numbers and date it returns only 200 or 400, and it adds 24 keys returning 500 when it stands alone or wins against text or object, all from does not support doc_value in root field on a sort or aggregation pushed to the flat_object root. I can add it in this or a follow-up PR, together with an issue for that 500.


/**
* A PPL command template, holding {@code {I}} for the source and {@code {F}} for the field under
* test.
*/
record Command(String name, String template) {
String query(String source, String field) {
return template.replace("{I}", source).replace("{F}", field);
}
}

/**
* Representative commands, one per way PPL reads, filters, aggregates, orders or windows the
* field, covering each command family the mapping conflict issues in #5829 report as a 500.
*/
static final List<Command> REPRESENTATIVE =
List.of(
new Command("where_isnotnull", "source={I} | where isnotnull({F})"),
new Command("fields_keep", "source={I} | fields {F}"),
new Command("eval_cast_string", "source={I} | eval x = cast({F} as string) | fields x"),
new Command("stats_count_by", "source={I} | stats count() by {F}"),
new Command("stats_max", "source={I} | stats max({F})"),
new Command("stats_dc", "source={I} | stats dc({F})"),
new Command("stats", "source={I} | stats count()"),
new Command("eventstats_by", "source={I} | eventstats count() by {F}"),
new Command("streamstats_count_by", "source={I} | streamstats count() by {F}"),
new Command("timechart_by", "source={I} | timechart span=1d count() by {F}"),
new Command("chart_by", "source={I} | chart count() by {F}"),
new Command("sort", "source={I} | sort {F}"),
new Command("reverse_after_sort", "source={I} | sort {F} | reverse"),
new Command("head_after_sort", "source={I} | sort {F} | head 1"),
new Command("dedup", "source={I} | dedup {F}"),
new Command("top", "source={I} | top {F}"),
new Command("rare", "source={I} | rare {F}"),
new Command("rex_named_group", "source={I} | rex field={F} '(?<rr>.+)' | fields rr"),
new Command(
"join_self",
"source={I} | head 100 | inner join left = l right = r on l.{F} = r.{F} [ source={I}"
+ " | head 100 ] | head 10"));

/** The source a known 500 covers, the data stream name, the wildcard over it, or both. */
enum Source {
DATA_STREAM,
WILDCARD,
BOTH;

boolean matches(String source) {
return this == BOTH || (this == WILDCARD) == source.endsWith("*");
}
}

/**
* A group of queries that return a 500 today, tied to the issue that tracks them. Every listed
* command over every listed pair returns a 500 under {@code winner}, the PPL type the merged
* schema reports, or under every winner the pair shows when {@code winner} is null, through
* {@code source}. A fix narrows its entry to the queries that still fail, splitting the entry if
* they no longer form one set of commands over one set of pairs, and the run fails until it does.
*/
record Known500(
String issue, List<String> commands, List<String> pairs, String winner, Source source) {
Known500 {
if (commands.isEmpty() || pairs.isEmpty()) {
throw new IllegalArgumentException("The " + issue + " known 500 needs commands and pairs");
}
}

/** A known 500 on both sources. */
Known500(String issue, List<String> commands, List<String> pairs, String winner) {
this(issue, commands, pairs, winner, Source.BOTH);
}

boolean covers(String pair, String observedWinner, String command, String observedSource) {
return pairs.contains(pair)
&& commands.contains(command)
&& (winner == null || winner.equals(observedWinner))
&& source.matches(observedSource);
}
}

/**
* The queries that return a 500 today, grouped by the issue that tracks them. Logs and traces
* return the same ones, since the drift lands on the same fields. Each entry lists its own
* commands and pairs, so a fix edits only its own entry.
*/
static final List<Known500> KNOWN_500S =
List.of(
// #5811 Composite aggregation pushdown does not support a field whose type differs across
// indices
new Known500(
"#5811",
List.of("stats_count_by", "dedup", "top", "rare", "chart_by", "timechart_by"),
List.of("keyword/byte", "keyword/integer", "keyword/long"),
null),
// #5811 Composite aggregation pushdown does not support a field whose type differs across
// indices
new Known500(
"#5811",
List.of("stats_count_by", "dedup", "top", "rare", "chart_by", "timechart_by"),
List.of("keyword/date"),
"string"),
// #5825 sort returns 500 when a field is keyword or ip in one index and a number, date or
// boolean in another
new Known500(
"#5825",
List.of("sort", "reverse_after_sort", "head_after_sort", "join_self"),
List.of("keyword/byte", "keyword/integer", "keyword/long", "keyword/date"),
null),
// #5835 stats max and stats min return 500 when a field is keyword or ip and boolean or a
// number in another
new Known500(
"#5835",
List.of("stats_max"),
List.of("keyword/byte", "keyword/integer", "keyword/long", "keyword/date"),
"string"),
// #5836 sort, stats max, and stats min on an object or nested field return 500
new Known500(
"#5836",
List.of("sort", "reverse_after_sort", "head_after_sort", "join_self", "stats_max"),
List.of("text/object", "object/absent", "absent/object"),
"struct"));

private MappingConflictFixtures() {}
}
Loading
Loading