Skip to content

Commit 6ffba26

Browse files
authored
Carry CalciteEvalCommandIT through the helper-managed index path (#5407)
* Carry CalciteEvalCommandIT through the helper-managed index path The IT's init() previously created the in-test test_eval index via direct `PUT /test_eval/_doc/N` requests, relying on dynamic mapping. Two problems with that: 1. The doc PUTs auto-create the index with whatever settings the cluster defaults to. The analytics-engine compatibility path (force-routing on; tests.analytics.parquet_indices=true) needs parquet-backed indices, which TestUtils.createIndexByRestClient applies via TestUtils.makeParquetBacked when the system property is set. Direct PUTs sidestep that helper, so test_eval lands as Lucene-backed and the analytics planner rejects it with "No backend can scan all requested fields on index [test_eval]". Three of the four tests fail at execution; the fourth uses BANK (loadIndex'd) and passes. 2. init() runs before every @test method. The doc PUTs are doc-level idempotent, so re-running was wasteful but not failing. Once we switch to createIndexByRestClient, the index-level PUT is no longer idempotent and re-running throws "resource_already_exists_exception". Both addressed in one change: - test_eval is created via TestUtils.createIndexByRestClient with an explicit mapping (name/title=keyword, age=long). The helper honours tests.analytics.parquet_indices=true and produces a parquet-backed index for the analytics-engine sweep; on the v2 path the helper is a no-op around the index PUT, so behaviour is unchanged. - The whole init body is guarded by TestUtils.isIndexExist — same idempotency idiom that loadIndex uses for predefined fixtures. First @test method provisions; subsequent methods skip. Also pins the projection order on testEvalStringConcatenation. The original query (`source=test_eval | eval greeting = 'Hello ' + name`) had no `| fields` clause and relied on the implicit projection's column order — v2 returns Lucene-source insertion order, analytics returns parquet-storage order (alphabetical), so the assertion only matched on v2 by coincidence. Adding `| fields name, title, age, greeting` makes the assertion deterministic across paths; the existing expected rows (`rows("Alice", "Engineer", 25, "Hello Alice")`) already match this order, so v2 behaviour is preserved. No semantic change for the v2 path: the explicit mapping types (keyword, long, keyword) resolve to the same PPL types ("string", "bigint", "string") that dynamic mapping inferred, and eval reads from _source either way. Analytics-route compatibility goes from 1/4 to 4/4 once the corresponding analytics-engine wiring lands in core. Signed-off-by: Kai Huang <ahkcs@amazon.com> * Exclude @ignore'd test classes from integTest as Gradle 9.4.1 workaround Gradle 9.4.1 has a ClassCastException in TestEventReporterAsListener.started (line 58 of org/gradle/api/internal/tasks/testing/junit/result/TestEventReporterAsListener.class inside plugins/gradle-testing-base-9.4.1.jar): ((GroupTestEventReporterInternal) reportersById.get(parent.getId())) .reportTestDirectly(...) The bridge stores test descriptor reporters keyed by id and casts the parent's stored reporter to GroupTestEventReporterInternal when a child event arrives. A class-level @ignore produces a non-composite parent descriptor with a leaf TestEventReporter (not a group reporter), so the cast fails. The first failure in CI was on CalciteInformationSchemaCommandIT.testTablesFromPrometheusCatalog — that IT is @ignore'd at the class level pointing at issue #3455. Once any @ignore'd class is loaded by the test runner, the cast aborts the whole integTest task with "Test process encountered an unexpected problem", even though no actual test assertion failed and the tests themselves would have been skipped at the JUnit layer. The bridge is instantiated by Gradle's own AbstractTestTask (verified via javap of `gradle-testing-base-9.4.1.jar` — `new TestEventReporterAsListener` at AbstractTestTask:263), so we cannot prevent its registration from user code. The only available remedy until Gradle ships a fix is to keep @ignore'd classes off the test runner's input set entirely. Net behaviour for CI: - @ignore'd tests were already skipped at the JUnit layer; skipping them at the Gradle layer instead changes "skipped" to "not run". For metrics/dashboards that count skipped tests, this is a one-time minor delta. For pass/fail status, identical. - Pre-existing exclude block (`OrderIT`, `ExplainIT`, etc.) already excludes some of the same classes; this commit covers the remaining @ignore'd ones. OrderIT not duplicated. - integTest task no longer aborts mid-run; per-test FAILED messages (testLogging.events "failed") are preserved. Remove this block once Gradle 9.4.x bug is fixed upstream. The list mirrors `grep -rln '^@ignore' integ-test/src/test/java` minus already-excluded paths. Signed-off-by: Kai Huang <ahkcs@amazon.com> --------- Signed-off-by: Kai Huang <ahkcs@amazon.com>
1 parent b3ef50d commit 6ffba26

2 files changed

Lines changed: 69 additions & 11 deletions

File tree

integ-test/build.gradle

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,40 @@ integTest {
557557
// Exclude this IT, because they executed in another task (:integTestWithSecurity)
558558
exclude 'org/opensearch/sql/security/**'
559559

560+
// Workaround for Gradle 9.4.1 ClassCastException in TestEventReporterAsListener.started
561+
// (line 58) — the bridge casts a parent test descriptor's reporter to
562+
// GroupTestEventReporterInternal but a class-level @Ignore produces a non-composite parent
563+
// descriptor with a leaf reporter, so the cast fails and aborts the entire integTest task
564+
// even though the tests would have been skipped anyway. The bridge is registered by Gradle's
565+
// own AbstractTestTask (we can't bypass it from user code), so the only available remedy is
566+
// to keep these classes off the test runner's input set. Net behaviour for CI: still
567+
// skipped, just at the build layer instead of inside JUnit. Remove once Gradle ships a fix.
568+
// OrderIT is already excluded above.
569+
exclude 'org/opensearch/sql/calcite/remote/CalciteInformationSchemaCommandIT.class'
570+
exclude 'org/opensearch/sql/calcite/remote/CalciteJsonFunctionsIT.class'
571+
exclude 'org/opensearch/sql/calcite/remote/CalcitePrometheusDataSourceCommandsIT.class'
572+
exclude 'org/opensearch/sql/calcite/remote/CalciteShowDataSourcesCommandIT.class'
573+
exclude 'org/opensearch/sql/legacy/AggregationIT.class'
574+
exclude 'org/opensearch/sql/legacy/DateFormatIT.class'
575+
exclude 'org/opensearch/sql/legacy/DateFunctionsIT.class'
576+
exclude 'org/opensearch/sql/legacy/HashJoinIT.class'
577+
exclude 'org/opensearch/sql/legacy/HavingIT.class'
578+
exclude 'org/opensearch/sql/legacy/JSONRequestIT.class'
579+
exclude 'org/opensearch/sql/legacy/JoinIT.class'
580+
exclude 'org/opensearch/sql/legacy/MathFunctionsIT.class'
581+
exclude 'org/opensearch/sql/legacy/MetricsIT.class'
582+
exclude 'org/opensearch/sql/legacy/MultiQueryIT.class'
583+
exclude 'org/opensearch/sql/legacy/NestedFieldQueryIT.class'
584+
exclude 'org/opensearch/sql/legacy/PreparedStatementIT.class'
585+
exclude 'org/opensearch/sql/legacy/QueryFunctionsIT.class'
586+
exclude 'org/opensearch/sql/legacy/QueryIT.class'
587+
exclude 'org/opensearch/sql/legacy/SQLFunctionsIT.class'
588+
exclude 'org/opensearch/sql/legacy/ShowIT.class'
589+
exclude 'org/opensearch/sql/legacy/SourceFieldIT.class'
590+
exclude 'org/opensearch/sql/legacy/SubqueryIT.class'
591+
exclude 'org/opensearch/sql/ppl/JsonFunctionsIT.class'
592+
exclude 'org/opensearch/sql/sql/ExpressionIT.class'
593+
560594
finalizedBy 'printIntegTestPaths'
561595
}
562596

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteEvalCommandIT.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.json.JSONObject;
1616
import org.junit.jupiter.api.Test;
1717
import org.opensearch.client.Request;
18+
import org.opensearch.sql.legacy.TestUtils;
1819
import org.opensearch.sql.ppl.PPLIntegTestCase;
1920

2021
public class CalciteEvalCommandIT extends PPLIntegTestCase {
@@ -26,23 +27,46 @@ public void init() throws Exception {
2627

2728
loadIndex(Index.BANK);
2829

29-
// Create test data for string concatenation
30-
Request request1 = new Request("PUT", "/test_eval/_doc/1?refresh=true");
31-
request1.setJsonEntity("{\"name\": \"Alice\", \"age\": 25, \"title\": \"Engineer\"}");
32-
client().performRequest(request1);
30+
// Pre-create test_eval through the helper so the analytics-engine compatibility run
31+
// (tests.analytics.parquet_indices=true) provisions it as a parquet-backed composite
32+
// index. Plain auto-mapping via the doc PUTs would create a Lucene-backed index, which
33+
// the analytics-engine planner cannot scan ("No backend can scan all requested fields").
34+
// Explicit mapping pins types so both v2 (verifySchema "string"/"bigint") and analytics
35+
// paths see the same shape regardless of dynamic-mapping behavior on the parquet engine.
36+
// Guarded by isIndexExist for idempotency — init() runs before each @Test method.
37+
if (!TestUtils.isIndexExist(client(), "test_eval")) {
38+
String testEvalMapping =
39+
"{\"mappings\":{\"properties\":{"
40+
+ "\"name\":{\"type\":\"keyword\"},"
41+
+ "\"age\":{\"type\":\"long\"},"
42+
+ "\"title\":{\"type\":\"keyword\"}}}}";
43+
TestUtils.createIndexByRestClient(client(), "test_eval", testEvalMapping);
3344

34-
Request request2 = new Request("PUT", "/test_eval/_doc/2?refresh=true");
35-
request2.setJsonEntity("{\"name\": \"Bob\", \"age\": 30, \"title\": \"Manager\"}");
36-
client().performRequest(request2);
45+
// Create test data for string concatenation
46+
Request request1 = new Request("PUT", "/test_eval/_doc/1?refresh=true");
47+
request1.setJsonEntity("{\"name\": \"Alice\", \"age\": 25, \"title\": \"Engineer\"}");
48+
client().performRequest(request1);
3749

38-
Request request3 = new Request("PUT", "/test_eval/_doc/3?refresh=true");
39-
request3.setJsonEntity("{\"name\": \"Charlie\", \"age\": null, \"title\": \"Analyst\"}");
40-
client().performRequest(request3);
50+
Request request2 = new Request("PUT", "/test_eval/_doc/2?refresh=true");
51+
request2.setJsonEntity("{\"name\": \"Bob\", \"age\": 30, \"title\": \"Manager\"}");
52+
client().performRequest(request2);
53+
54+
Request request3 = new Request("PUT", "/test_eval/_doc/3?refresh=true");
55+
request3.setJsonEntity("{\"name\": \"Charlie\", \"age\": null, \"title\": \"Analyst\"}");
56+
client().performRequest(request3);
57+
}
4158
}
4259

4360
@Test
4461
public void testEvalStringConcatenation() throws IOException {
45-
JSONObject result = executeQuery("source=test_eval | eval greeting = 'Hello ' + name");
62+
// Pin the projection so column order is deterministic across execution paths — the
63+
// analytics-engine route reads parquet schema in storage order, which can differ from the
64+
// v2 / Lucene path's _source-iteration order. Adding an explicit | fields makes the test
65+
// a strict assertion on the eval expression rather than a coincidence of projection order.
66+
JSONObject result =
67+
executeQuery(
68+
"source=test_eval | eval greeting = 'Hello ' + name | fields name, title, age,"
69+
+ " greeting");
4670
verifySchema(
4771
result,
4872
schema("name", "string"),

0 commit comments

Comments
 (0)