Carry CalciteEvalCommandIT through the helper-managed index path#5407
Conversation
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>
PR Reviewer Guide 🔍(Review updated until commit 435827e)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 435827e
Previous suggestionsSuggestions up to commit 28a5853
|
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 opensearch-project#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>
|
Persistent review updated to latest commit 435827e |
6ffba26
into
opensearch-project:feature/mustang-ppl-integration
Description
Two things this PR carries together (commit-separated for clean review):
Carry
CalciteEvalCommandITthrough the helper-managed index path (28a58536d) — provisions the inlinetest_evalindex viaTestUtils.createIndexByRestClientinstead of directPUT /test_eval/_doc/N, so the analytics-engine compatibility run (tests.analytics.parquet_indices=true) gets a parquet-backed index. Mirrors the helper-managed pattern thatCalciteFillNullCommandITandCalcitePPLAppendCommandITalready use vialoadIndex(Index.X).Workaround for Gradle 9.4.1
TestEventReporterAsListenercast bug (435827e99) — excludes@Ignore-annotated test classes fromintegTestso the task no longer aborts mid-run on this branch's CI.1. Helper-managed
test_evalprovisioningWhy this matters
When the analytics-engine compatibility path is active (
tests.analytics.force_routing=trueandtests.analytics.parquet_indices=true), every test-created index needs parquet-backed storage so the DataFusion backend can scan it —TestUtils.makeParquetBackedhandles the injection insidecreateIndexByRestClient. DirectPUT /test_eval/_doc/Ncalls bypass that helper, so the index lands Lucene-backed and the analytics planner rejects every query withNo backend can scan all requested fields on index [test_eval].The three tests that use
test_evalfail on the analytics route today; onlytestEvalStringConcatenationWithExistingData(which usesloadIndex(Index.BANK)) survives. With this change,test_evalis created via the same helper as every other fixture and inherits the parquet flag automatically.The companion analytics-engine wiring (CONCAT/CAST/SAFE_CAST capabilities + null-propagating concat adapter) lands separately in opensearch-project/OpenSearch#21498. On its own, this PR is a no-op for the v2 / Calcite path — once both this and the core PR are merged,
CalciteEvalCommandITgoes from 1/4 to 4/4 on the analytics route.Changes
Helper-managed
test_evalprovisioning.init()now callsTestUtils.createIndexByRestClient(client(), "test_eval", testEvalMapping)with an explicit mapping (name/title=keyword,age=long). The helper is a thin wrapper around the index PUT on the v2 path (no-op whentests.analytics.parquet_indicesis unset), so v2 behaviour is unchanged.Idempotency guard. The whole init body is wrapped in
TestUtils.isIndexExist(client(), "test_eval")— same patternloadIndexuses for predefined fixtures.init()runs before every@Testmethod; first run provisions, subsequent runs skip. Without the guard,createIndexByRestClient(unlike the original doc-level PUT) would throwresource_already_exists_exceptionon the second invocation.Pinned projection order on
testEvalStringConcatenation. The original query had no| fieldsclause and relied on the implicit projection's column order, which differs between paths (v2 returns_source-iteration order, analytics returns parquet-storage order). Added| fields name, title, age, greetingto make the assertion deterministic. Expected rows already match this order, so v2 behaviour is preserved.V2-path impact
namePPL typestring(text → string)string(keyword → string)agePPL typebigintbiginttitlePPL typestringstring_source_source"Hello Alice""Hello Alice"[name, title, age, greeting](matched by coincidence)[name, title, age, greeting](now pinned)verifySchema(schema("name", "string"))matches bothtext+keyword(dynamic) andkeyword(explicit) — PPL maps both to the same logical type. No assertion changes were needed.2. Gradle 9.4.1
@Ignorecast workaroundWhy this is in this PR
CI on this branch (post-#5406 wrapper bump to 9.4.1) aborts the
:integ-test:integTesttask with:…even though no test assertion fails. Without a workaround, this PR's tests would be invisible — the task crashes before final result aggregation. Folding the workaround into this PR unblocks our CI; the workaround is a base-branch concern but the symptom blocks every PR until landed.
Root cause (verified via
javapagainstgradle-testing-base-9.4.1.jar)org/gradle/api/tasks/testing/AbstractTestTask(line 263) instantiatesTestEventReporterAsListener— Gradle's internal bridge fromTestListenerInternalto the newTestEventReporterAPI.TestEventReporterAsListener.startedline 58 stores a per-descriptor reporter in a map keyed by id and, on each child event, casts the parent's stored reporter toGroupTestEventReporterInternal.@Ignoreproduces a non-composite parent descriptor with a leafTestEventReporterrather than a group reporter — the cast fails. First failure in CI:CalciteInformationSchemaCommandIT.testTablesFromPrometheusCatalog(that class is@Ignore'd for [FEATURE] Support Prometheus DataSource with Calcite #3455).AbstractTestTask, not byOpenSearchTestBasePlugin.addTestListener(ErrorReportingTestListener). MigratingErrorReportingTestListenerto the new event-reporter API would not stop the bridge from firing.Workaround
Exclude all
@Ignore-annotated test classes fromintegTest. The classes were already skipped at the JUnit layer; this just moves the skip earlier (build layer) so the buggy bridge never sees them.@Ignore'd test runtime status@Ignore'dintegTesttask aborts mid-runtestLogging.events "failed"per-test message24 classes excluded (one —
OrderIT— was already excluded by the existing block). Source:grep -rln '^@Ignore' integ-test/src/test/javaminus already-excluded paths.This block should be removed once Gradle ships a fix for the bridge cast. Given the bug is in Gradle's own internal class, the proper long-term fix is upstream — file an issue on
gradle/gradlereferencing theTestEventReporterAsListener.started:58site if not already filed.Test results
testEvalStringConcatenationtestEvalStringConcatenationWithNullFieldtestEvalStringConcatenationWithLiteralstestEvalStringConcatenationWithExistingData:integ-test:integTesttaskForward note
The same direct-
PUTpattern that this PR fixes forCalciteEvalCommandITexists in roughly a dozen other Calcite ITs (CalcitePPLAggregationIT,CalcitePPLBasicIT,CalciteFieldFormatCommandIT,CalcitePPLCaseFunctionIT, etc.). Each will need the same one-line carryover when its corresponding command lands on the analytics route. This PR addresses only the eval surface.By submitting this pull request