/* * Licensed to Crate under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. Crate licenses this file * to you under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * However, if you have executed another commercial license agreement * with Crate these terms will supersede the license and you may use the * software solely pursuant to the terms of the relevant commercial * agreement. */ package io.crate.planner; import com.google.common.collect.Iterables; import io.crate.action.sql.SessionContext; import io.crate.analyze.EvaluatingNormalizer; import io.crate.analyze.QuerySpec; import io.crate.analyze.TableDefinitions; import io.crate.analyze.WhereClause; import io.crate.analyze.symbol.*; import io.crate.exceptions.UnsupportedFeatureException; import io.crate.exceptions.VersionInvalidException; import io.crate.metadata.*; import io.crate.operation.aggregation.impl.CountAggregation; import io.crate.operation.projectors.TopN; import io.crate.planner.node.dql.*; import io.crate.planner.node.dql.join.JoinType; import io.crate.planner.node.dql.join.NestedLoop; import io.crate.planner.projection.*; import io.crate.test.integration.CrateDummyClusterServiceUnitTest; import io.crate.testing.SQLExecutor; import io.crate.testing.T3; import io.crate.types.DataTypes; import org.apache.lucene.util.BytesRef; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; import java.util.*; import static io.crate.testing.SymbolMatchers.*; import static io.crate.testing.TestingHelpers.isDocKey; import static io.crate.testing.TestingHelpers.isSQL; import static org.hamcrest.Matchers.*; public class SelectPlannerTest extends CrateDummyClusterServiceUnitTest { private SQLExecutor e; @Before public void prepare() { e = SQLExecutor.builder(clusterService) .addDocTable(TableDefinitions.USER_TABLE_INFO) .addDocTable(TableDefinitions.TEST_CLUSTER_BY_STRING_TABLE_INFO) .addDocTable(TableDefinitions.PARTED_PKS_TI) .addDocTable(TableDefinitions.TEST_PARTITIONED_TABLE_INFO) .addDocTable(TableDefinitions.IGNORED_NESTED_TABLE_INFO) .addDocTable(TableDefinitions.TEST_MULTIPLE_PARTITIONED_TABLE_INFO) .addDocTable(T3.T1_INFO) .build(); } @Test public void testHandlerSideRouting() throws Exception { // just testing the dispatching here.. making sure it is not a ESSearchNode e.plan("select * from sys.cluster"); } @Test public void testWherePKAndMatchDoesNotResultInESGet() throws Exception { Plan plan = e.plan("select * from users where id in (1, 2, 3) and match(text, 'Hello')"); assertThat(plan, instanceOf(QueryThenFetch.class)); } @Test public void testGetPlan() throws Exception { ESGet esGet = e.plan("select name from users where id = 1"); assertThat(esGet.tableInfo().ident().name(), is("users")); assertThat(esGet.docKeys().getOnlyKey(), isDocKey(1L)); assertThat(esGet.outputs().size(), is(1)); } @Test public void testGetWithVersion() throws Exception { expectedException.expect(VersionInvalidException.class); expectedException.expectMessage("\"_version\" column is not valid in the WHERE clause of a SELECT statement"); e.plan("select name from users where id = 1 and _version = 1"); } @Test public void testGetPlanStringLiteral() throws Exception { ESGet esGet = e.plan("select name from bystring where name = 'one'"); assertThat(esGet.tableInfo().ident().name(), is("bystring")); assertThat(esGet.docKeys().getOnlyKey(), isDocKey("one")); assertThat(esGet.outputs().size(), is(1)); } @Test public void testGetPlanPartitioned() throws Exception { ESGet esGet = e.plan("select name, date from parted_pks where id = 1 and date = 0"); assertThat(esGet.tableInfo().ident().name(), is("parted_pks")); assertThat(esGet.docKeys().getOnlyKey(), isDocKey(1, 0L)); //is(new PartitionName("parted", Arrays.asList(new BytesRef("0"))).asIndexName())); assertEquals(DataTypes.STRING, esGet.outputTypes().get(0)); assertEquals(DataTypes.TIMESTAMP, esGet.outputTypes().get(1)); } @Test public void testMultiGetPlan() throws Exception { ESGet esGet = e.plan("select name from users where id in (1, 2)"); assertThat(esGet.docKeys().size(), is(2)); assertThat(esGet.docKeys(), containsInAnyOrder(isDocKey(1L), isDocKey(2L))); } @Test public void testGlobalAggregationPlan() throws Exception { Merge globalAggregate = e.plan("select count(name) from users"); Collect collect = (Collect) globalAggregate.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) collect.collectPhase()); assertEquals(CountAggregation.LongStateType.INSTANCE, collectPhase.outputTypes().get(0)); assertThat(collectPhase.maxRowGranularity(), is(RowGranularity.DOC)); assertThat(collectPhase.projections().size(), is(1)); assertThat(collectPhase.projections().get(0), instanceOf(AggregationProjection.class)); assertThat(collectPhase.projections().get(0).requiredGranularity(), is(RowGranularity.SHARD)); MergePhase mergePhase = globalAggregate.mergePhase(); assertEquals(CountAggregation.LongStateType.INSTANCE, Iterables.get(mergePhase.inputTypes(), 0)); assertEquals(DataTypes.LONG, mergePhase.outputTypes().get(0)); } @Test public void testShardSelectWithOrderBy() throws Exception { Collect collect = e.plan("select id from sys.shards order by id limit 10"); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) collect.collectPhase()); assertEquals(DataTypes.INTEGER, collectPhase.outputTypes().get(0)); assertThat(collectPhase.maxRowGranularity(), is(RowGranularity.SHARD)); assertThat(collectPhase.orderBy(), notNullValue()); List<Projection> projections = collectPhase.projections(); assertThat(projections, contains( instanceOf(TopNProjection.class), instanceOf(TopNProjection.class) )); } @Test public void testCollectAndMergePlan() throws Exception { QueryThenFetch qtf = e.plan("select name from users where name = 'x' order by id limit 10"); Merge merge = (Merge) qtf.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) ((Collect) merge.subPlan()).collectPhase()); assertTrue(collectPhase.whereClause().hasQuery()); TopNProjection topNProjection = (TopNProjection) collectPhase.projections().get(0); assertThat(topNProjection.limit(), is(10)); MergePhase mergePhase = merge.mergePhase(); assertThat(mergePhase.outputTypes().size(), is(1)); assertEquals(DataTypes.STRING, mergePhase.outputTypes().get(0)); assertTrue(mergePhase.finalProjection().isPresent()); Projection lastProjection = mergePhase.finalProjection().get(); assertThat(lastProjection, instanceOf(FetchProjection.class)); FetchProjection fetchProjection = (FetchProjection) lastProjection; assertThat(fetchProjection.outputs(), isSQL("FETCH(INPUT(0), doc.users._doc['name'])")); } @Test public void testCollectAndMergePlanNoFetch() throws Exception { // testing that a fetch projection is not added if all output symbols are included // at the orderBy symbols Merge merge = e.plan("select name from users where name = 'x' order by name limit 10"); Collect collect = (Collect) merge.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) collect.collectPhase()); assertTrue(collectPhase.whereClause().hasQuery()); MergePhase mergePhase = merge.mergePhase(); assertThat(mergePhase.outputTypes().size(), is(1)); assertEquals(DataTypes.STRING, mergePhase.outputTypes().get(0)); assertTrue(mergePhase.finalProjection().isPresent()); Projection lastProjection = mergePhase.finalProjection().get(); assertThat(lastProjection, instanceOf(TopNProjection.class)); TopNProjection topNProjection = (TopNProjection) lastProjection; assertThat(topNProjection.outputs().size(), is(1)); } @Test public void testCollectAndMergePlanHighLimit() throws Exception { QueryThenFetch qtf = e.plan("select name from users limit 100000"); Merge merge = (Merge) qtf.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) ((Collect) merge.subPlan()).collectPhase()); assertThat(collectPhase.nodePageSizeHint(), is(100_000)); MergePhase mergePhase = merge.mergePhase(); assertThat(mergePhase.projections().size(), is(2)); assertThat(mergePhase.finalProjection().get(), instanceOf(FetchProjection.class)); TopNProjection topN = (TopNProjection) mergePhase.projections().get(0); assertThat(topN.limit(), is(100_000)); assertThat(topN.offset(), is(0)); FetchProjection fetchProjection = (FetchProjection) mergePhase.projections().get(1); // with offset qtf = e.plan("select name from users limit 100000 offset 20"); merge = ((Merge) qtf.subPlan()); collectPhase = ((RoutedCollectPhase) ((Collect) merge.subPlan()).collectPhase()); assertThat(collectPhase.nodePageSizeHint(), is(100_000 + 20)); mergePhase = merge.mergePhase(); assertThat(mergePhase.projections().size(), is(2)); assertThat(mergePhase.finalProjection().get(), instanceOf(FetchProjection.class)); topN = (TopNProjection) mergePhase.projections().get(0); assertThat(topN.limit(), is(100_000)); assertThat(topN.offset(), is(20)); fetchProjection = (FetchProjection) mergePhase.projections().get(1); } @Test public void testCollectAndMergePlanPartitioned() throws Exception { QueryThenFetch qtf = e.plan("select id, name, date from parted_pks where date > 0 and name = 'x' order by id limit 10"); Merge merge = (Merge) qtf.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) ((Collect) merge.subPlan()).collectPhase()); List<String> indices = new ArrayList<>(); Map<String, Map<String, List<Integer>>> locations = collectPhase.routing().locations(); for (Map.Entry<String, Map<String, List<Integer>>> entry : locations.entrySet()) { indices.addAll(entry.getValue().keySet()); } assertThat(indices, Matchers.contains( new PartitionName("parted", Arrays.asList(new BytesRef("123"))).asIndexName())); assertTrue(collectPhase.whereClause().hasQuery()); MergePhase mergePhase = merge.mergePhase(); assertThat(mergePhase.outputTypes().size(), is(3)); } @Test public void testCollectAndMergePlanFunction() throws Exception { QueryThenFetch qtf = e.plan("select format('Hi, my name is %s', name), name from users where name = 'x' order by id limit 10"); Merge merge = (Merge) qtf.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) ((Collect) merge.subPlan()).collectPhase()); assertTrue(collectPhase.whereClause().hasQuery()); MergePhase mergePhase = merge.mergePhase(); assertThat(mergePhase.outputTypes().size(), is(2)); assertEquals(DataTypes.STRING, mergePhase.outputTypes().get(0)); assertEquals(DataTypes.STRING, mergePhase.outputTypes().get(1)); assertTrue(mergePhase.finalProjection().isPresent()); Projection lastProjection = mergePhase.finalProjection().get(); assertThat(lastProjection, instanceOf(FetchProjection.class)); FetchProjection fetchProjection = (FetchProjection) lastProjection; assertThat(fetchProjection.outputs().size(), is(2)); assertThat(fetchProjection.outputs().get(0), isFunction("format")); assertThat(fetchProjection.outputs().get(1), isFetchRef(0, "_doc['name']")); } @Test public void testCountDistinctPlan() throws Exception { Merge globalAggregate = e.plan("select count(distinct name) from users"); Collect collect = (Collect) globalAggregate.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) collect.collectPhase()); Projection projection = collectPhase.projections().get(0); assertThat(projection, instanceOf(AggregationProjection.class)); AggregationProjection aggregationProjection = (AggregationProjection) projection; assertThat(aggregationProjection.aggregations().size(), is(1)); assertThat(aggregationProjection.mode(), is(AggregateMode.ITER_PARTIAL)); Aggregation aggregation = aggregationProjection.aggregations().get(0); Symbol aggregationInput = aggregation.inputs().get(0); assertThat(aggregationInput.symbolType(), is(SymbolType.INPUT_COLUMN)); assertThat(collectPhase.toCollect().get(0), instanceOf(Reference.class)); assertThat(((Reference) collectPhase.toCollect().get(0)).ident().columnIdent().name(), is("name")); MergePhase mergePhase = globalAggregate.mergePhase(); assertThat(mergePhase.projections().size(), is(2)); Projection projection1 = mergePhase.projections().get(1); assertThat(projection1, instanceOf(EvalProjection.class)); Symbol collection_count = projection1.outputs().get(0); assertThat(collection_count, instanceOf(Function.class)); } @Test public void testGlobalAggregationHaving() throws Exception { Merge globalAggregate = e.plan( "select avg(date) from users having min(date) > '1970-01-01'"); Collect collect = (Collect) globalAggregate.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) collect.collectPhase()); assertThat(collectPhase.projections().size(), is(1)); assertThat(collectPhase.projections().get(0), instanceOf(AggregationProjection.class)); MergePhase localMergeNode = globalAggregate.mergePhase(); assertThat(localMergeNode.projections(), contains( instanceOf(AggregationProjection.class), instanceOf(FilterProjection.class), instanceOf(EvalProjection.class))); AggregationProjection aggregationProjection = (AggregationProjection) localMergeNode.projections().get(0); assertThat(aggregationProjection.aggregations().size(), is(2)); FilterProjection filterProjection = (FilterProjection) localMergeNode.projections().get(1); assertThat(filterProjection.outputs().size(), is(2)); assertThat(filterProjection.outputs().get(0), instanceOf(InputColumn.class)); InputColumn inputColumn = (InputColumn) filterProjection.outputs().get(0); assertThat(inputColumn.index(), is(0)); EvalProjection evalProjection = (EvalProjection) localMergeNode.projections().get(2); assertThat(evalProjection.outputs().size(), is(1)); } @Test public void testCountOnPartitionedTable() throws Exception { CountPlan plan = e.plan("select count(*) from parted where date = 1395874800000"); assertThat(plan.countPhase().whereClause().partitions(), containsInAnyOrder(".partitioned.parted.04732cpp6ks3ed1o60o30c1g")); } @Test(expected = UnsupportedOperationException.class) public void testSelectPartitionedTableOrderByPartitionedColumn() throws Exception { e.plan("select name from parted order by date"); } @Test(expected = UnsupportedOperationException.class) public void testSelectPartitionedTableOrderByPartitionedColumnInFunction() throws Exception { e.plan("select name from parted order by year(date)"); } @Test(expected = UnsupportedOperationException.class) public void testSelectOrderByPartitionedNestedColumn() throws Exception { e.plan("select id from multi_parted order by obj['name']"); } @Test(expected = UnsupportedOperationException.class) public void testSelectOrderByPartitionedNestedColumnInFunction() throws Exception { e.plan("select id from multi_parted order by format('abc %s', obj['name'])"); } @Test(expected = UnsupportedFeatureException.class) public void testQueryRequiresScalar() throws Exception { // only scalar functions are allowed on system tables because we have no lucene queries e.plan("select * from sys.shards where match(table_name, 'characters')"); } @Test public void testOrderByOnAnalyzed() throws Exception { expectedException.expect(UnsupportedOperationException.class); expectedException.expectMessage("Cannot ORDER BY 'text': sorting on analyzed/fulltext columns is not possible"); e.plan("select text from users u order by 1"); } @Test public void testSortOnUnknownColumn() throws Exception { expectedException.expect(UnsupportedOperationException.class); expectedException.expectMessage("Cannot ORDER BY 'details['unknown_column']': invalid data type 'null'."); e.plan("select details from ignored_nested order by details['unknown_column']"); } @Test public void testOrderByOnIndexOff() throws Exception { expectedException.expect(UnsupportedOperationException.class); expectedException.expectMessage("Cannot ORDER BY 'no_index': sorting on non-indexed columns is not possible"); e.plan("select no_index from users u order by 1"); } @Test public void testSelectAnalyzedReferenceInFunctionAggregation() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Cannot select analyzed column 'text' within grouping or aggregations"); e.plan("select min(substr(text, 0, 2)) from users"); } @Test public void testSelectNonIndexedReferenceInFunctionAggregation() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Cannot select non-indexed column 'no_index' within grouping or aggregations"); e.plan("select min(substr(no_index, 0, 2)) from users"); } @Test public void testGlobalAggregateWithWhereOnPartitionColumn() throws Exception { Merge globalAggregate = e.plan( "select min(name) from parted where date > 1395961100000"); Collect collect = (Collect) globalAggregate.subPlan(); WhereClause whereClause = ((RoutedCollectPhase) collect.collectPhase()).whereClause(); assertThat(whereClause.partitions().size(), is(1)); assertThat(whereClause.noMatch(), is(false)); } @Test public void testHasNoResultFromHaving() throws Exception { e.plan("select min(name) from users having 1 = 2"); // TODO: } @Test public void testHasNoResultFromLimit() { // TODO: e.plan("select count(*) from users limit 1 offset 1"); e.plan("select count(*) from users limit 5 offset 1"); e.plan("select count(*) from users limit 0"); e.plan("select * from users order by name limit 0"); e.plan("select * from users order by name limit 0 offset 0"); } @Test public void testHasNoResultFromQuery() { // TODO: e.plan("select name from users where false"); } @Test public void testShardQueueSizeCalculation() throws Exception { Merge merge = e.plan("select name from users order by name limit 100"); Collect collect = (Collect) merge.subPlan(); int shardQueueSize = ((RoutedCollectPhase) collect.collectPhase()).shardQueueSize( collect.collectPhase().nodeIds().iterator().next()); assertThat(shardQueueSize, is(75)); } @Test public void testQAFPagingIsEnabledOnHighLimit() throws Exception { Merge plan = e.plan("select name from users order by name limit 1000000"); assertThat(plan.mergePhase().nodeIds().size(), is(1)); // mergePhase with executionNode = paging enabled Collect collect = (Collect) plan.subPlan(); assertThat(((RoutedCollectPhase) collect.collectPhase()).nodePageSizeHint(), is(750000)); } @Test public void testQAFPagingIsEnabledOnHighOffset() throws Exception { Merge merge = e.plan("select name from users order by name limit 10 offset 1000000"); Collect collect = (Collect) merge.subPlan(); assertThat(merge.mergePhase().nodeIds().size(), is(1)); // mergePhase with executionNode = paging enabled assertThat(((RoutedCollectPhase) collect.collectPhase()).nodePageSizeHint(), is(750007)); } @Test public void testQTFPagingIsEnabledOnHighLimit() throws Exception { QueryThenFetch qtf = e.plan("select name, date from users order by name limit 1000000"); Merge merge = (Merge) qtf.subPlan(); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) ((Collect) merge.subPlan()).collectPhase()); assertThat(merge.mergePhase().nodeIds().size(), is(1)); // mergePhase with executionNode = paging enabled assertThat(collectPhase.nodePageSizeHint(), is(750000)); } @Test public void testSelectFromUnnestResultsInTableFunctionPlan() throws Exception { Collect collect = e.plan("select * from unnest([1, 2], ['Arthur', 'Trillian'])"); assertNotNull(collect); assertThat(collect.collectPhase().toCollect(), contains(isReference("col1"), isReference("col2"))); } @Test public void testSoftLimitIsApplied() throws Exception { QueryThenFetch qtf = e.plan("select * from users", UUID.randomUUID(), 10, 0); Merge merge = (Merge) qtf.subPlan(); assertThat(merge.mergePhase().projections(), contains(instanceOf(TopNProjection.class), instanceOf(FetchProjection.class))); TopNProjection topNProjection = (TopNProjection) merge.mergePhase().projections().get(0); assertThat(topNProjection.limit(), is(10)); qtf = e.plan("select * from users limit 5", UUID.randomUUID(), 10, 0); merge = (Merge) qtf.subPlan(); assertThat(merge.mergePhase().projections(), contains(instanceOf(TopNProjection.class), instanceOf(FetchProjection.class))); topNProjection = (TopNProjection) merge.mergePhase().projections().get(0); assertThat(topNProjection.limit(), is(5)); } @Test public void testReferenceToNestedAggregatedField() throws Exception { Merge merge = e.plan("select ii, xx from ( " + " select i + i as ii, xx from (" + " select i, sum(x) as xx from t1 group by i) as t) as tt " + "where (ii * 2) > 4 and (xx * 2) > 120"); Plan subQuery = merge.subPlan(); assertThat(subQuery, instanceOf(Collect.class)); assertThat(((Collect) subQuery).collectPhase().projections(), contains( instanceOf(GroupProjection.class) )); assertThat(merge.mergePhase().projections(), contains( instanceOf(GroupProjection.class), instanceOf(EvalProjection.class), instanceOf(FilterProjection.class), instanceOf(EvalProjection.class) )); } @Test public void test3TableJoinQuerySplitting() throws Exception { QueryThenFetch qtf = e.plan("select" + " u1.id as u1, " + " u2.id as u2, " + " u3.id as u3 " + "from " + " users u1," + " users u2," + " users u3 " + "where " + " u1.name = 'Arthur'" + " and u2.id = u1.id" + " and u2.name = u1.name"); NestedLoop outerNl = (NestedLoop) qtf.subPlan(); NestedLoop innerNl = (NestedLoop) outerNl.left(); assertThat(((FilterProjection) innerNl.nestedLoopPhase().projections().get(0)).query(), isSQL("((INPUT(2) = INPUT(0)) AND (INPUT(3) = INPUT(1)))")); } @Test public void testOuterJoinToInnerJoinRewrite() throws Exception { QueryThenFetch qtf = e.plan("select u1.text, u2.text " + "from users u1 left join users u2 on u1.id = u2.id " + "where u2.name = 'Arthur'" + "and u2.id > 1 "); NestedLoop nl = (NestedLoop) qtf.subPlan(); assertThat(nl.nestedLoopPhase().joinType(), is(JoinType.INNER)); Collect rightCM = (Collect) nl.right(); assertThat(((RoutedCollectPhase) rightCM.collectPhase()).whereClause().query(), isSQL("((doc.users.name = 'Arthur') AND (doc.users.id > 1))")); // doesn't contain "name" because whereClause is pushed down, // but still contains "id" because it is in the joinCondition assertThat(rightCM.collectPhase().toCollect(), contains(isReference("_fetchid"), isReference("id"))); } @Test public void testNoSoftLimitOnUnlimitedChildRelation() throws Exception { int softLimit = 10_000; EvaluatingNormalizer normalizer = EvaluatingNormalizer.functionOnlyNormalizer(e.functions(), ReplaceMode.COPY); Planner.Context plannerContext = new Planner.Context( e.planner, clusterService, UUID.randomUUID(), null, normalizer, new TransactionContext(SessionContext.SYSTEM_SESSION), softLimit, 0); Limits limits = plannerContext.getLimits(new QuerySpec()); assertThat(limits.finalLimit(), is(TopN.NO_LIMIT)); } @Test public void testShardSelect() throws Exception { Collect collect = e.plan("select id from sys.shards"); RoutedCollectPhase collectPhase = ((RoutedCollectPhase) collect.collectPhase()); assertThat(collectPhase.maxRowGranularity(), is(RowGranularity.SHARD)); } @Test public void testGlobalCountPlan() throws Exception { CountPlan plan = e.plan("select count(*) from users"); assertThat(plan.countPhase().whereClause(), equalTo(WhereClause.MATCH_ALL)); assertThat(plan.mergePhase().projections().size(), is(1)); assertThat(plan.mergePhase().projections().get(0), instanceOf(MergeCountProjection.class)); } @SuppressWarnings("ConstantConditions") @Test public void testLimitThatIsBiggerThanPageSizeCausesQTFPUshPlan() throws Exception { QueryThenFetch qtf = e.plan("select * from users limit 2147483647 "); Merge merge = (Merge) qtf.subPlan(); assertThat(merge.mergePhase().nodeIds().size(), is(1)); qtf = e.plan("select * from users limit 2"); merge = (Merge) qtf.subPlan(); assertThat(merge.mergePhase().nodeIds().size(), is(0)); } }