@@ -492,6 +492,28 @@ TEST_P(IndexLookupJoinTest, planNodeAndSerde) {
492492 testSerde (plan);
493493 }
494494
495+ // with splitOutput.
496+ for (const auto splitOutput :
497+ {std::optional<bool >(true ),
498+ std::optional<bool >(false ),
499+ std::optional<bool >(std::nullopt )}) {
500+ auto plan = PlanBuilder (planNodeIdGenerator)
501+ .values ({left})
502+ .startIndexLookupJoin ()
503+ .leftKeys ({" t0" })
504+ .rightKeys ({" u0" })
505+ .indexSource (indexTableScan)
506+ .outputLayout ({" t0" , " u1" , " t2" , " t1" })
507+ .joinType (core::JoinType::kInner )
508+ .splitOutput (splitOutput)
509+ .endIndexLookupJoin ()
510+ .planNode ();
511+ auto indexLookupJoinNode =
512+ std::dynamic_pointer_cast<const core::IndexLookupJoinNode>(plan);
513+ ASSERT_EQ (indexLookupJoinNode->splitOutput (), splitOutput);
514+ testSerde (plan);
515+ }
516+
495517 // bad join type.
496518 {
497519 VELOX_ASSERT_USER_THROW (
@@ -2664,6 +2686,130 @@ TEST_P(IndexLookupJoinTest, outputBatchSizeWithLeftJoin) {
26642686 }
26652687}
26662688
2689+ TEST_P (IndexLookupJoinTest, splitOutputNodeOverride) {
2690+ IndexTableData tableData;
2691+ generateIndexTableData ({3'000 , 1 , 1 }, tableData, pool_);
2692+
2693+ const int numProbeBatches = 10 ;
2694+ const int numRowsPerProbeBatch = 100 ;
2695+ const int maxBatchRows = 10 ;
2696+
2697+ const auto probeVectors = generateProbeInput (
2698+ numProbeBatches,
2699+ numRowsPerProbeBatch,
2700+ 1 ,
2701+ tableData,
2702+ pool_,
2703+ {" t0" , " t1" , " t2" },
2704+ GetParam ().hasNullKeys ,
2705+ {},
2706+ {},
2707+ /* equalMatchPct=*/ 100 );
2708+ std::vector<std::shared_ptr<TempFilePath>> probeFiles =
2709+ createProbeFiles (probeVectors);
2710+
2711+ createDuckDbTable (" t" , probeVectors);
2712+ createDuckDbTable (" u" , {tableData.tableVectors });
2713+
2714+ const auto indexTable = TestIndexTable::create (
2715+ /* numEqualJoinKeys=*/ 3 ,
2716+ tableData.keyVectors ,
2717+ tableData.valueVectors ,
2718+ *pool ());
2719+ const auto indexTableHandle = makeIndexTableHandle (
2720+ indexTable, GetParam ().asyncLookup , GetParam ().needsIndexSplit );
2721+
2722+ struct {
2723+ std::optional<bool > nodeSplitOutput;
2724+ bool configSplitOutput;
2725+ bool expectSplit;
2726+
2727+ std::string debugString () const {
2728+ return fmt::format (
2729+ " nodeSplitOutput: {}, configSplitOutput: {}, expectSplit: {}" ,
2730+ nodeSplitOutput.has_value ()
2731+ ? (nodeSplitOutput.value () ? " true" : " false" )
2732+ : " nullopt" ,
2733+ configSplitOutput,
2734+ expectSplit);
2735+ }
2736+ } testSettings[] = {
2737+ // Node splitOutput not set, use config.
2738+ {std::nullopt , true , true },
2739+ {std::nullopt , false , false },
2740+ // Node splitOutput=true overrides config.
2741+ {true , true , true },
2742+ {true , false , true },
2743+ // Node splitOutput=false overrides config.
2744+ {false , true , false },
2745+ {false , false , false },
2746+ };
2747+
2748+ for (const auto & testData : testSettings) {
2749+ SCOPED_TRACE (testData.debugString ());
2750+
2751+ auto planNodeIdGenerator = std::make_shared<core::PlanNodeIdGenerator>();
2752+
2753+ const auto indexScanNode = makeIndexScanNode (
2754+ planNodeIdGenerator,
2755+ indexTableHandle,
2756+ makeScanOutputType ({" u0" , " u1" , " u2" , " u5" }),
2757+ makeIndexColumnHandles ({" u0" , " u1" , " u2" , " u5" }));
2758+
2759+ core::PlanNodeId joinNodeId;
2760+ auto plan = PlanBuilder (planNodeIdGenerator, pool_.get ())
2761+ .startTableScan ()
2762+ .outputType (probeType_)
2763+ .endTableScan ()
2764+ .captureScanNodeId (probeScanNodeId_)
2765+ .startIndexLookupJoin ()
2766+ .leftKeys ({" t0" , " t1" , " t2" })
2767+ .rightKeys ({" u0" , " u1" , " u2" })
2768+ .indexSource (indexScanNode)
2769+ .outputLayout ({" t4" , " u5" })
2770+ .joinType (core::JoinType::kInner )
2771+ .splitOutput (testData.nodeSplitOutput )
2772+ .endIndexLookupJoin ()
2773+ .capturePlanNodeId (joinNodeId)
2774+ .planNode ();
2775+
2776+ const int expectedNumOutputVectors = testData.expectSplit
2777+ ? ((numRowsPerProbeBatch + maxBatchRows - 1 ) / maxBatchRows) *
2778+ numProbeBatches
2779+ : numProbeBatches;
2780+
2781+ AssertQueryBuilder queryBuilder (duckDbQueryRunner_);
2782+ queryBuilder.plan (plan)
2783+ .config (
2784+ core::QueryConfig::kIndexLookupJoinMaxPrefetchBatches ,
2785+ std::to_string (GetParam ().numPrefetches ))
2786+ .config (
2787+ core::QueryConfig::kPreferredOutputBatchRows ,
2788+ std::to_string (maxBatchRows))
2789+ .config (
2790+ core::QueryConfig::kPreferredOutputBatchBytes ,
2791+ std::to_string (1ULL << 30 ))
2792+ .config (
2793+ core::QueryConfig::kIndexLookupJoinSplitOutput ,
2794+ testData.configSplitOutput ? " true" : " false" )
2795+ .splits (probeScanNodeId_, makeHiveConnectorSplits (probeFiles))
2796+ .serialExecution (GetParam ().serialExecution )
2797+ .barrierExecution (GetParam ().serialExecution );
2798+ if (GetParam ().needsIndexSplit ) {
2799+ queryBuilder.split (
2800+ indexScanNodeId_,
2801+ Split (
2802+ std::make_shared<TestIndexConnectorSplit>(
2803+ kTestIndexConnectorName )));
2804+ }
2805+ const auto task = queryBuilder.assertResults (
2806+ " SELECT t.c4, u.c5 FROM t, u WHERE t.c0 = u.c0 AND t.c1 = u.c1 AND t.c2 = u.c2" );
2807+ ASSERT_EQ (
2808+ toPlanStats (task->taskStats ()).at (joinNodeId).outputVectors ,
2809+ expectedNumOutputVectors);
2810+ }
2811+ }
2812+
26672813DEBUG_ONLY_TEST_P (IndexLookupJoinTest, statsSplitter) {
26682814 IndexTableData tableData;
26692815 generateIndexTableData ({100 , 1 , 1 }, tableData, pool_);
0 commit comments