/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.planner.parseinfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.voltcore.utils.Pair;
import org.voltdb.catalog.Index;
import org.voltdb.expressions.AbstractExpression;
import org.voltdb.expressions.TupleValueExpression;
import org.voltdb.planner.AbstractParsedStmt;
import org.voltdb.planner.CompiledPlan;
import org.voltdb.planner.ParsedColInfo;
import org.voltdb.planner.ParsedSelectStmt;
import org.voltdb.planner.ParsedUnionStmt;
import org.voltdb.planner.PlanningErrorException;
import org.voltdb.planner.StatementPartitioning;
import org.voltdb.plannodes.SchemaColumn;
/**
* StmtTableScan caches data related to a given instance of a sub-query within the statement scope
*/
public class StmtSubqueryScan extends StmtTableScan {
// Sub-Query
private final AbstractParsedStmt m_subqueryStmt;
private final ArrayList<SchemaColumn> m_outputColumnList = new ArrayList<>();
private final Map<Pair<String, Integer>, Integer> m_outputColumnIndexMap = new HashMap<>();
private CompiledPlan m_bestCostPlan = null;
private StatementPartitioning m_subqueriesPartitioning = null;
private boolean m_failedSingleFragmentTest = false;
private boolean m_tableAggregateSubquery = false;
/*
* This 'subquery' actually is the parent query on the derived table with alias 'tableAlias'
*/
public StmtSubqueryScan(AbstractParsedStmt subqueryStmt, String tableAlias, int stmtId) {
super(tableAlias, stmtId);
m_subqueryStmt = subqueryStmt;
// A union or other set operator uses the output columns of its left-most leaf child statement.
while (subqueryStmt instanceof ParsedUnionStmt) {
assert( ! ((ParsedUnionStmt)subqueryStmt).m_children.isEmpty());
subqueryStmt = ((ParsedUnionStmt)subqueryStmt).m_children.get(0);
}
assert (subqueryStmt instanceof ParsedSelectStmt);
int i = 0;
for (ParsedColInfo col: ((ParsedSelectStmt)subqueryStmt).displayColumns()) {
String colAlias = col.alias == null? col.columnName : col.alias;
SchemaColumn scol = col.asSchemaColumn();
m_outputColumnList.add(scol);
m_outputColumnIndexMap.put(Pair.of(colAlias, col.differentiator), i);
i++;
}
}
public StmtSubqueryScan(AbstractParsedStmt subqueryStmt, String tableAlias) {
this(subqueryStmt, tableAlias, 0);
}
public void setSubqueriesPartitioning(StatementPartitioning subqueriesPartitioning) {
assert(subqueriesPartitioning != null);
m_subqueriesPartitioning = subqueriesPartitioning;
findPartitioningColumns();
}
/**
* upgrade single partitioning expression to parent level
* add the info to equality sets and input value equivalence
* @param valueEquivalence
* @param eqSets
*/
public void promoteSinglePartitionInfo(
HashMap<AbstractExpression, Set<AbstractExpression>> valueEquivalence,
Set< Set<AbstractExpression> > eqSets) {
assert(m_subqueriesPartitioning != null);
if (m_subqueriesPartitioning.getCountOfPartitionedTables() == 0 ||
m_subqueriesPartitioning.requiresTwoFragments()) {
return;
}
// This subquery is a single partitioned query on partitioned tables
// promoting the single partition expression up to its parent level.
AbstractExpression spExpr = m_subqueriesPartitioning.singlePartitioningExpression();
for (SchemaColumn col: m_partitioningColumns) {
AbstractExpression tveKey = col.getExpression();
assert(tveKey instanceof TupleValueExpression);
Set<AbstractExpression> values = null;
if (valueEquivalence.containsKey(tveKey)) {
values = valueEquivalence.get(tveKey);
}
else if (valueEquivalence.containsKey(spExpr)) {
values = valueEquivalence.get(spExpr);
}
else {
for (SchemaColumn otherCol: m_partitioningColumns) {
if (col != otherCol &&
valueEquivalence.containsKey(otherCol.getExpression())) {
values = valueEquivalence.get(otherCol.getExpression());
break;
}
}
if (values == null) {
values = new HashSet<>();
}
}
updateEqualSets(values, valueEquivalence, eqSets, tveKey, spExpr);
}
}
// update the new equal sets for partitioning columns and values
// (Xin): If it changes valueEquivalence, we have to update eqSets
// Because HashSet stored a legacy hashcode for the non-final object.
private void updateEqualSets(Set<AbstractExpression> values,
HashMap<AbstractExpression, Set<AbstractExpression>> valueEquivalence,
Set< Set<AbstractExpression> > eqSets,
AbstractExpression tveKey, AbstractExpression spExpr) {
boolean hasLegacyValues = false;
if (eqSets.contains(values)) {
eqSets.remove(values);
hasLegacyValues = true;
}
values.add(spExpr);
values.add(tveKey);
if (hasLegacyValues) {
eqSets.add(values);
}
valueEquivalence.put(spExpr, values);
valueEquivalence.put(tveKey, values);
}
// exported subquery partitioning column(s)
private List<SchemaColumn> findPartitioningColumns() {
if (m_partitioningColumns != null) {
return m_partitioningColumns;
}
m_partitioningColumns = new ArrayList<>();
assert(m_subqueriesPartitioning != null);
if (m_subqueriesPartitioning.getCountOfPartitionedTables() > 0) {
for (StmtTableScan tableScan : m_subqueryStmt.allScans()) {
List<SchemaColumn> scols = tableScan.getPartitioningColumns();
if (scols != null) {
addPartitioningColumns(scols);
}
}
}
return m_partitioningColumns;
}
private void addPartitioningColumns(List<SchemaColumn> scols) {
// The partitioning columns have to be in its output column list
// in order to be referenced on parent level.
for (SchemaColumn partitionCol: scols) {
SchemaColumn matchedCol = null;
// Find whether the partition column is in output column list
for (SchemaColumn outputCol: m_outputColumnList) {
AbstractExpression outputExpr = outputCol.getExpression();
if ( ! (outputExpr instanceof TupleValueExpression)) {
continue;
}
TupleValueExpression tve = (TupleValueExpression)outputExpr;
if (tve.getTableName().equals(partitionCol.getTableName()) &&
tve.getColumnName().equals(partitionCol.getColumnName())) {
matchedCol = outputCol;
break;
}
}
String colNameForParentQuery;
if (matchedCol != null) {
colNameForParentQuery = matchedCol.getColumnAlias();
}
// single partition sub-query case can be single partition without
// including partition column in its display column list
else if ( ! m_subqueriesPartitioning.requiresTwoFragments()) {
colNameForParentQuery = partitionCol.getColumnName();
}
else {
continue;
}
partitionCol.reset(m_tableAlias, m_tableAlias,
colNameForParentQuery, colNameForParentQuery);
m_partitioningColumns.add(partitionCol);
}
}
@Override
public String getTableName() {
// Because a derived table must have an alias, use its alias instead.
return m_tableAlias;
}
/**
* The subquery is replicated if all tables from the FROM clause defining this subquery
* are replicated
* @return True if the subquery is replicated
*/
@Override
public boolean getIsReplicated() {
for (StmtTableScan tableScan : m_subqueryStmt.allScans()) {
if ( ! tableScan.getIsReplicated()) {
return false;
}
}
return true;
}
public List<StmtTargetTableScan> getAllTargetTables() {
List <StmtTargetTableScan> stmtTables = new ArrayList<>();
for (StmtTableScan tableScan : m_subqueryStmt.allScans()) {
if (tableScan instanceof StmtTargetTableScan) {
stmtTables.add((StmtTargetTableScan)tableScan);
}
else {
assert(tableScan instanceof StmtSubqueryScan);
StmtSubqueryScan subScan = (StmtSubqueryScan)tableScan;
stmtTables.addAll(subScan.getAllTargetTables());
}
}
return stmtTables;
}
static final List<Index> noIndexesSupportedOnSubqueryScans = new ArrayList<>();
@Override
public List<Index> getIndexes() {
return noIndexesSupportedOnSubqueryScans;
}
public AbstractParsedStmt getSubqueryStmt() {
return m_subqueryStmt;
}
public CompiledPlan getBestCostPlan() {
return m_bestCostPlan;
}
public void setBestCostPlan(CompiledPlan costPlan) {
m_bestCostPlan = costPlan;
}
@Override
public String getColumnName(int columnIndex) {
return getSchemaColumn(columnIndex).getColumnName();
}
public SchemaColumn getSchemaColumn(int columnIndex) {
return m_outputColumnList.get(columnIndex);
}
public Integer getColumnIndex(String columnAlias, int differentiator) {
return m_outputColumnIndexMap.get(Pair.of(columnAlias, differentiator));
}
@Override
public AbstractExpression processTVE(TupleValueExpression expr, String columnName) {
Integer idx = m_outputColumnIndexMap.get(Pair.of(columnName, expr.getDifferentiator()));
if (idx == null) {
throw new PlanningErrorException("Mismatched columns " + columnName + " in subquery");
}
SchemaColumn schemaCol = m_outputColumnList.get(idx.intValue());
expr.setColumnIndex(idx.intValue());
expr.setTypeSizeAndInBytes(schemaCol);
return expr;
}
/**
* Some subquery results can only be joined with a partitioned table after
* it finishes some work on the coordinator. With the 2 fragment plan limit,
* those queries can not be supported.
* Other than that case, the planner will typically have added a
* send/receive pair to the subquery plan that is actually only suitable to
* a stand-alone plan. This function distinguishes subqueries that should NOT
* have a send/receive pair.
* @param root
* @return true if there is no aspect to the plan that requires execution on the coordinator.
*/
public boolean canRunInOneFragment() {
assert(m_subqueriesPartitioning != null);
assert(m_subqueryStmt != null);
if (m_subqueriesPartitioning.getCountOfPartitionedTables() == 0) {
return true;
}
// recursive check for its nested subqueries that require coordination
// of their results.
if (failsSingleFragmentTest()) {
return false;
}
// Tentative assignment in case of early return.
// This gets immediately reset if it passes all the tests.
m_failedSingleFragmentTest = true;
if (m_subqueryStmt instanceof ParsedUnionStmt) {
// Union are just returned
return false;
}
if ( ! (m_subqueryStmt instanceof ParsedSelectStmt)) {
throw new PlanningErrorException("Unsupported subquery found in FROM clause:" +
m_subqueryStmt);
}
ParsedSelectStmt selectStmt = (ParsedSelectStmt)m_subqueryStmt;
// Now If query has LIMIT/OFFSET/DISTINCT on a replicated table column,
// we should get rid of the receive node. I (--paul) don't know what this means.
if (selectStmt.hasLimitOrOffset() || selectStmt.hasDistinctWithGroupBy()) {
return false;
}
// If the query uses the partitioned materialized view table with the
// need to Re-aggregate, then we can not get rid of the receive node.
// This is also caught in StatementPartitioning when analyzing the join criteria,
// because it contains a partitioned view that does not have a partition column.
if (selectStmt.m_mvFixInfo.needed()) {
return false;
}
// Table aggregate cases should not get rid of the receive node
if (selectStmt.hasAggregateOrGroupby()) {
if (!selectStmt.isGrouped()) {
m_tableAggregateSubquery = true;
return false;
}
// For group by queries, there are two cases on group by columns.
// (1) Does not contain a partition column:
// If joined with a partitioned table in the parent query, it will
// violate the partitioned table join criteria.
// Detect case (1) to mark receive node.
if ( ! selectStmt.hasPartitionColumnInGroupby()) {
return false;
}
}
if ( ! selectStmt.hasPartitionColumnInWindowFunctionExpression()) {
return false;
}
// Now. If this sub-query joins with a partitioned table in the parent statement,
// push the join down by removing the send/receive plan node pair.
m_failedSingleFragmentTest = false;
return true;
}
public boolean failsSingleFragmentTest() {
if (m_failedSingleFragmentTest) {
return true;
}
for (StmtTableScan tableScan : m_subqueryStmt.allScans()) {
if (tableScan instanceof StmtSubqueryScan) {
StmtSubqueryScan subScan = (StmtSubqueryScan)tableScan;
if (subScan.failsSingleFragmentTest()) {
// Cache known test failures on parent subqueries.
m_failedSingleFragmentTest = true;
return true;
}
}
}
return false;
}
public boolean isTableAggregate() {
return m_tableAggregateSubquery;
}
/** Produce a tuple value expression for a column produced by this subquery */
public TupleValueExpression getOutputExpression(int index) {
SchemaColumn schemaCol = m_outputColumnList.get(index);
TupleValueExpression tve = new TupleValueExpression(getTableAlias(), getTableAlias(),
schemaCol.getColumnAlias(), schemaCol.getColumnAlias(), index);
return tve;
}
public List<SchemaColumn> getOutputSchema() {
return m_outputColumnList;
}
public String calculateContentDeterminismMessage() {
return m_subqueryStmt.calculateContentDeterminismMessage();
}
}