Skip to content

Add test case for lookup join#15244

Merged
yashmayya merged 10 commits intoapache:masterfrom
krishan1390:lookup_join_integration_test
Mar 18, 2025
Merged

Add test case for lookup join#15244
yashmayya merged 10 commits intoapache:masterfrom
krishan1390:lookup_join_integration_test

Conversation

@krishan1390
Copy link
Contributor

@krishan1390 krishan1390 commented Mar 11, 2025

Addresses #15224

Verified that the test case breaks without #15223 with exception trace

java.util.concurrent.ExecutionException: java.lang.RuntimeException: Caught exception while submitting request: 2087751583000000009, stage: 1: java.lang.UnsupportedOperationException: Plan node of type TableScanNode is not supported!
	at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396) ~[?:?]
	at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) ~[?:?]
	at org.apache.pinot.query.service.server.QueryServer.forEachStage(QueryServer.java:311) ~[classes/:?]
	at org.apache.pinot.query.service.server.QueryServer.submit(QueryServer.java:147) ~[classes/:?]
	at org.apache.pinot.common.proto.PinotQueryWorkerGrpc$MethodHandlers.invoke(PinotQueryWorkerGrpc.java:461) ~[classes/:?]
	at io.grpc.stub.ServerCalls$UnaryServerCallHandler$UnaryServerCallListener.onHalfClose(ServerCalls.java:182) ~[grpc-stub-1.71.0.jar:1.71.0]
	at io.grpc.internal.ServerCallImpl$ServerStreamListenerImpl.halfClosed(ServerCallImpl.java:356) ~[grpc-core-1.71.0.jar:1.71.0]
	at io.grpc.internal.ServerImpl$JumpToApplicationThreadServerStreamListener$1HalfClosed.runInContext(ServerImpl.java:861) ~[grpc-core-1.71.0.jar:1.71.0]
	at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37) ~[grpc-core-1.71.0.jar:1.71.0]
	at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:133) ~[grpc-core-1.71.0.jar:1.71.0]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
	at java.base/java.lang.Thread.run(Thread.java:1583) [?:?]
Caused by: java.lang.RuntimeException: Caught exception while submitting request: 2087751583000000009, stage: 1: java.lang.UnsupportedOperationException: Plan node of type TableScanNode is not supported!
	at org.apache.pinot.query.service.server.QueryServer.submitStage(QueryServer.java:274) ~[classes/:?]
	at org.apache.pinot.query.service.server.QueryServer.lambda$forEachStage$5(QueryServer.java:304) ~[classes/:?]
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1768) ~[?:?]
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java) ~[?:?]
	... 3 more
Caused by: java.util.concurrent.ExecutionException: java.lang.UnsupportedOperationException: Plan node of type TableScanNode is not supported!
	at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396) ~[?:?]
	at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) ~[?:?]
	at org.apache.pinot.query.service.server.QueryServer.submitStage(QueryServer.java:265) ~[classes/:?]
	at org.apache.pinot.query.service.server.QueryServer.lambda$forEachStage$5(QueryServer.java:304) ~[classes/:?]
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1768) ~[?:?]
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java) ~[?:?]
	... 3 more
Caused by: java.lang.UnsupportedOperationException: Plan node of type TableScanNode is not supported!
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitTableScan(PlanNodeToOpChain.java:209) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitTableScan(PlanNodeToOpChain.java:97) ~[classes/:?]
	at org.apache.pinot.query.planner.plannode.TableScanNode.visit(TableScanNode.java:52) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visit(PlanNodeToOpChain.java:112) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitProject(PlanNodeToOpChain.java:199) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitProject(PlanNodeToOpChain.java:97) ~[classes/:?]
	at org.apache.pinot.query.planner.plannode.ProjectNode.visit(ProjectNode.java:47) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visit(PlanNodeToOpChain.java:112) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitJoin(PlanNodeToOpChain.java:181) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitJoin(PlanNodeToOpChain.java:97) ~[classes/:?]
	at org.apache.pinot.query.planner.plannode.JoinNode.visit(JoinNode.java:73) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visit(PlanNodeToOpChain.java:112) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitProject(PlanNodeToOpChain.java:199) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitProject(PlanNodeToOpChain.java:97) ~[classes/:?]
	at org.apache.pinot.query.planner.plannode.ProjectNode.visit(ProjectNode.java:47) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visit(PlanNodeToOpChain.java:112) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitMailboxSend(PlanNodeToOpChain.java:129) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain$MyVisitor.visitMailboxSend(PlanNodeToOpChain.java:97) ~[classes/:?]
	at org.apache.pinot.query.planner.plannode.MailboxSendNode.visit(MailboxSendNode.java:187) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain.convert(PlanNodeToOpChain.java:92) ~[classes/:?]
	at org.apache.pinot.query.runtime.plan.PlanNodeToOpChain.convert(PlanNodeToOpChain.java:78) ~[classes/:?]
	at org.apache.pinot.query.runtime.QueryRunner.processQuery(QueryRunner.java:286) ~[classes/:?]
	at org.apache.pinot.query.service.server.QueryServer.lambda$submit$0(QueryServer.java:149) ~[classes/:?]
	at org.apache.pinot.query.service.server.QueryServer.lambda$submitStage$4(QueryServer.java:259) ~[classes/:?]
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run$$$capture(CompletableFuture.java:1768) ~[?:?]
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java) ~[?:?]
	... 3 more

@yashmayya yashmayya linked an issue Mar 12, 2025 that may be closed by this pull request
Copy link
Contributor

@yashmayya yashmayya left a comment

Choose a reason for hiding this comment

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

Thanks for adding this test @krishan1390, I've left a suggestion on how we can improve the robustness of the assertions being made and verify that the expected join strategy was used.

Comment on lines +1625 to +1629
String query = "select /*+ joinOptions(join_strategy='lookup') */ yearID, teamName from baseballStats "
+ "join dimBaseballTeams ON baseballStats.teamID = dimBaseballTeams.teamID where playerId = 'aardsda01'";
JsonNode jsonNode = postQuery(query);
long result = jsonNode.get("resultTable").get("rows").size();
assertEquals(result, 3);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we also make assertions to verify that the lookup join strategy is actually being used? Few ideas off the top of my head:

  • EXPLAIN PLAN FOR... output for the query should have an exchange attached only to the non-dimensional table side of the join and not the dimensional side. And I believe the distribution type should be singleton as opposed to a hash distribution.
  • Similarly, EXPLAIN IMPLEMENTATION PLAN FOR... output for the query should have MAIL_SEND / MAIL_RECEIVE operators only on the non dimensional table side of the join.
  • The actual query response should have the LOOKUP_JOIN operator in the query stats.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks.

The LOOKUP_JOIN operator is a robust verification. Verified that this check fails if I remove /* joinOptions(join_strategy='lookup') */ so its helpful. Added this.

The explain plan / implementation plan verifications are string pattern matching verifications. I think better to not verify explain plan / implementation plan in this integration test because its part of details of how lookup join is implemented. And any changes there would affect this test case unncessarily.

Let me know if thats fine or if you have other opinions.

Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds good to me.

@yashmayya yashmayya added testing multi-stage Related to the multi-stage query engine labels Mar 12, 2025
@codecov-commenter
Copy link

codecov-commenter commented Mar 12, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 63.60%. Comparing base (59551e4) to head (7b87b38).
Report is 1859 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #15244      +/-   ##
============================================
+ Coverage     61.75%   63.60%   +1.85%     
- Complexity      207     1459    +1252     
============================================
  Files          2436     2780     +344     
  Lines        133233   156665   +23432     
  Branches      20636    24035    +3399     
============================================
+ Hits          82274    99647   +17373     
- Misses        44911    49519    +4608     
- Partials       6048     7499    +1451     
Flag Coverage Δ
custom-integration1 100.00% <ø> (+99.99%) ⬆️
integration 100.00% <ø> (+99.99%) ⬆️
integration1 100.00% <ø> (+99.99%) ⬆️
integration2 0.00% <ø> (ø)
java-11 63.57% <ø> (+1.86%) ⬆️
java-21 63.48% <ø> (+1.85%) ⬆️
skip-bytebuffers-false 63.59% <ø> (+1.84%) ⬆️
skip-bytebuffers-true 63.45% <ø> (+35.73%) ⬆️
temurin 63.60% <ø> (+1.85%) ⬆️
unittests 63.60% <ø> (+1.85%) ⬆️
unittests1 56.13% <ø> (+9.24%) ⬆️
unittests2 34.16% <ø> (+6.43%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +25 to +26
"segmentIngestionType": "APPEND",
"segmentIngestionFrequency": "DAILY",
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed now

Comment on lines +1625 to +1629
String query = "select /*+ joinOptions(join_strategy='lookup') */ yearID, teamName from baseballStats "
+ "join dimBaseballTeams ON baseballStats.teamID = dimBaseballTeams.teamID where playerId = 'aardsda01'";
JsonNode jsonNode = postQuery(query);
long result = jsonNode.get("resultTable").get("rows").size();
assertEquals(result, 3);
Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds good to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is an 8 MB file, I'm wondering if it's really necessary to introduce this? Can't we add some dummy data for a dimension table that can be joined with the existing On_Time_On_Time_Performance_2014_100k_subset_nonulls data? Maybe something like DayOfWeek integer primary key to DayOfWeekName strings (just one idea, can be anything really since we don't care about the actual results, just the lookup join functionality).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes was previously comparing the actual result of the join and seemed easier to verify on a smaller file like baseball stats than On_Time_On_Time_Performance_2014_100k_subset_nonulls.

but now that we're verifying the join through query operator, removed the assertion of actual count and used On_Time_On_Time_Performance_2014_100k_subset_nonulls table.

thanks for the suggestion.

@krishan1390 krishan1390 mentioned this pull request Mar 18, 2025
Comment on lines +1661 to +1662
assertTrue(stages.contains("LOOKUP_JOIN"), "Could not find LOOKUP_JOIN stage in the query plan");
assertFalse(stages.contains("HASH_JOIN"), "HASH_JOIN stage should not be present in the query plan");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: LOOKUP_JOIN and HASH_JOIN are operators, not stages.

@yashmayya yashmayya merged commit 7ab5a13 into apache:master Mar 18, 2025
22 checks passed
tsekityam pushed a commit to tsekityam/pinot that referenced this pull request Mar 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

multi-stage Related to the multi-stage query engine testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Test] Add query test for LOOKUP join

3 participants