blob: 5b747df1e07f242c760aabd14f10c3d8aa7fd7af [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF 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.
package org.apache.impala.analysis;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.impala.analysis.Path.PathType;
import org.apache.impala.authorization.Privilege;
import org.apache.impala.catalog.Column;
import org.apache.impala.catalog.FeTable;
import org.apache.impala.catalog.FeView;
import org.apache.impala.catalog.StructField;
import org.apache.impala.catalog.StructType;
import org.apache.impala.catalog.TableLoadingException;
import org.apache.impala.common.AnalysisException;
import org.apache.impala.common.ColumnAliasGenerator;
import org.apache.impala.common.TableAliasGenerator;
import org.apache.impala.common.TreeNode;
import org.apache.impala.rewrite.ExprRewriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
/**
* Representation of a single select block, including GROUP BY, ORDER BY and HAVING
* clauses.
*/
public class SelectStmt extends QueryStmt {
private final static Logger LOG = LoggerFactory.getLogger(SelectStmt.class);
/**
* Impala 3.x and earlier provide non-standard support for a single ordinal
* in the HAVING clause:
*
* SELECT region, count(*), count(*) > 10 FROM sales
* HAVING 3
*
* Most other SQL dialects do not allow ordinals in expressions (such as
* HAVING), only in lists (GROUP BY, ORDER BY). The standard way to express
* the above is:
*
* SELECT region, count(*) FROM sales
* HAVING count(*) > 10
*
* Or, better (not currently supported by IMPALA):
*
* SELECT region, count(*) c FROM SALES
* HAVING c > 10
*
* We intend to disable this non-standard feature in the next major
* release. For now, we leave the feature/bug enabled, controlled by
* the following constant.
*
* In that same release, we will enable the third example above.
* At present, all that is supported is the odd:
*
* SELECT region, count(*), count(*) > 10 p FROM sales
* HAVING p
*
* See IMPALA-7844.
*/
public static final boolean ALLOW_ORDINALS_IN_HAVING = true;
/////////////////////////////////////////
// BEGIN: Members that need to be reset()
protected SelectList selectList_;
protected final List<String> colLabels_; // lower case column labels
protected final FromClause fromClause_;
protected Expr whereClause_;
protected List<Expr> groupingExprs_;
protected Expr havingClause_; // original having clause
// havingClause with aliases and agg output resolved
private Expr havingPred_;
// set if we have any kind of aggregation operation, include SELECT DISTINCT
private MultiAggregateInfo multiAggInfo_;
// set if we have AnalyticExprs in the select list/order by clause
private AnalyticInfo analyticInfo_;
// substitutes all exprs in this select block to reference base tables
// directly
private ExprSubstitutionMap baseTblSmap_ = new ExprSubstitutionMap();
// END: Members that need to be reset()
/////////////////////////////////////////
// SQL string of this SelectStmt before inline-view expression substitution.
// Set in analyze().
protected String sqlString_;
SelectStmt(SelectList selectList,
FromClause fromClause,
Expr wherePredicate, List<Expr> groupingExprs,
Expr havingPredicate, List<OrderByElement> orderByElements,
LimitElement limitElement) {
super(orderByElements, limitElement);
selectList_ = selectList;
if (fromClause == null) {
fromClause_ = new FromClause();
} else {
fromClause_ = fromClause;
}
whereClause_ = wherePredicate;
groupingExprs_ = groupingExprs;
havingClause_ = havingPredicate;
colLabels_ = new ArrayList<>();
havingPred_ = null;
multiAggInfo_ = null;
sortInfo_ = null;
}
/**
* @return the original select list items from the query
*/
public SelectList getSelectList() { return selectList_; }
/**
* @return the HAVING clause post-analysis and with aliases resolved
*/
public Expr getHavingPred() { return havingPred_; }
@Override
public List<String> getColLabels() { return colLabels_; }
public List<TableRef> getTableRefs() { return fromClause_.getTableRefs(); }
public boolean hasWhereClause() { return whereClause_ != null; }
public boolean hasGroupByClause() { return groupingExprs_ != null; }
public Expr getWhereClause() { return whereClause_; }
public void setWhereClause(Expr whereClause) { whereClause_ = whereClause; }
public MultiAggregateInfo getMultiAggInfo() { return multiAggInfo_; }
public boolean hasMultiAggInfo() { return multiAggInfo_ != null; }
public AnalyticInfo getAnalyticInfo() { return analyticInfo_; }
public boolean hasAnalyticInfo() { return analyticInfo_ != null; }
public boolean hasHavingClause() { return havingClause_ != null; }
public ExprSubstitutionMap getBaseTblSmap() { return baseTblSmap_; }
// Column alias generator used during query rewriting.
private ColumnAliasGenerator columnAliasGenerator_ = null;
public ColumnAliasGenerator getColumnAliasGenerator() {
if (columnAliasGenerator_ == null) {
columnAliasGenerator_ = new ColumnAliasGenerator(colLabels_, null);
}
return columnAliasGenerator_;
}
// Table alias generator used during query rewriting.
private TableAliasGenerator tableAliasGenerator_ = null;
public TableAliasGenerator getTableAliasGenerator() {
if (tableAliasGenerator_ == null) {
tableAliasGenerator_ = new TableAliasGenerator(analyzer_, null);
}
return tableAliasGenerator_;
}
@Override
public void setDoTableMasking(boolean doTableMasking) {
fromClause_.setDoTableMasking(doTableMasking);
}
/**
* Creates resultExprs and baseTblResultExprs.
*/
@Override
public void analyze(Analyzer analyzer) throws AnalysisException {
if (isAnalyzed()) return;
super.analyze(analyzer);
new SelectAnalyzer(analyzer).analyze();
}
/**
* Algorithm class for the SELECT statement analyzer. Holds
* the analyzer and intermediate state.
*/
private class SelectAnalyzer {
private final Analyzer analyzer_;
private List<Expr> groupingExprsCopy_;
private List<FunctionCallExpr> aggExprs_;
private ExprSubstitutionMap ndvSmap_;
private ExprSubstitutionMap countAllMap_;
private SelectAnalyzer(Analyzer analyzer) {
this.analyzer_ = analyzer;
}
private void analyze() throws AnalysisException {
// Start out with table refs to establish aliases.
fromClause_.analyze(analyzer_);
analyzeSelectClause();
verifyResultExprs();
registerViewColumnPrivileges();
analyzeWhereClause();
createSortInfo(analyzer_);
// Analyze aggregation-relevant components of the select block (Group By
// clause, select list, Order By clause), substitute AVG with SUM/COUNT,
// create the AggregationInfo, including the agg output tuple, and transform
// all post-agg exprs given AggregationInfo's smap.
analyzeHavingClause();
if (checkForAggregates()) {
verifyAggSemantics();
analyzeGroupingExprs();
collectAggExprs();
rewriteCountDistinct();
buildAggregateExprs();
buildResultExprs();
verifyAggregation();
}
createAnalyticInfo();
if (evaluateOrderBy_) createSortTupleInfo(analyzer_);
// Remember the SQL string before inline-view expression substitution.
sqlString_ = toSql();
if (origSqlString_ == null) origSqlString_ = sqlString_;
resolveInlineViewRefs();
// If this block's select-project-join portion returns an empty result set and the
// block has no aggregation, then mark this block as returning an empty result set.
if (analyzer_.hasEmptySpjResultSet() && multiAggInfo_ == null) {
analyzer_.setHasEmptyResultSet();
}
buildColumnLineageGraph();
}
private void analyzeSelectClause() throws AnalysisException {
// Generate !empty() predicates to filter out empty collections.
// Skip this step when analyzing a WITH-clause because CollectionTableRefs
// do not register collection slots in their parent in that context
// (see CollectionTableRef.analyze()).
if (!analyzer_.hasWithClause()) registerIsNotEmptyPredicates();
// analyze plan hints from select list
selectList_.analyzePlanHints(analyzer_);
// populate resultExprs_, aliasSmap_, and colLabels_
for (int i = 0; i < selectList_.getItems().size(); ++i) {
SelectListItem item = selectList_.getItems().get(i);
if (item.isStar()) {
if (item.getRawPath() != null) {
Path resolvedPath = analyzeStarPath(item.getRawPath(), analyzer_);
expandStar(resolvedPath);
} else {
expandStar();
}
} else {
// Analyze the resultExpr before generating a label to ensure enforcement
// of expr child and depth limits (toColumn() label may call toSql()).
item.getExpr().analyze(analyzer_);
if (item.getExpr().contains(Predicates.instanceOf(Subquery.class))) {
throw new AnalysisException(
"Subqueries are not supported in the select list.");
}
resultExprs_.add(item.getExpr());
String label = item.toColumnLabel(i, analyzer_.useHiveColLabels());
SlotRef aliasRef = new SlotRef(label);
Expr existingAliasExpr = aliasSmap_.get(aliasRef);
if (existingAliasExpr != null && !existingAliasExpr.equals(item.getExpr())) {
// If we have already seen this alias, it refers to more than one column and
// therefore is ambiguous.
ambiguousAliasList_.add(aliasRef);
}
aliasSmap_.put(aliasRef, item.getExpr().clone());
colLabels_.add(label);
}
}
}
private void verifyResultExprs() throws AnalysisException {
// Star exprs only expand to the scalar-typed columns/fields, so
// the resultExprs_ could be empty.
if (resultExprs_.isEmpty()) {
throw new AnalysisException("The star exprs expanded to an empty select list " +
"because the referenced tables only have complex-typed columns.\n" +
"Star exprs only expand to scalar-typed columns because " +
"complex-typed exprs " +
"are currently not supported in the select list.\n" +
"Affected select statement:\n" + toSql());
}
for (Expr expr: resultExprs_) {
// Complex types are currently not supported in the select list because
// we'd need to serialize them in a meaningful way.
if (expr.getType().isComplexType()) {
throw new AnalysisException(String.format(
"Expr '%s' in select list returns a complex type '%s'.\n" +
"Only scalar types are allowed in the select list.",
expr.toSql(), expr.getType().toSql()));
}
if (!expr.getType().isSupported()) {
throw new AnalysisException("Unsupported type '"
+ expr.getType().toSql() + "' in '" + expr.toSql() + "'.");
}
}
if (TreeNode.contains(resultExprs_, AnalyticExpr.class)) {
if (fromClause_.isEmpty()) {
throw new AnalysisException("Analytic expressions require FROM clause.");
}
// do this here, not after analyzeAggregation(), otherwise the AnalyticExprs
// will get substituted away
if (selectList_.isDistinct()) {
throw new AnalysisException(
"cannot combine SELECT DISTINCT with analytic functions");
}
}
}
private void analyzeWhereClause() throws AnalysisException {
if (whereClause_ == null) return;
whereClause_.analyze(analyzer_);
if (whereClause_.contains(Expr.IS_AGGREGATE)) {
throw new AnalysisException(
"aggregate function not allowed in WHERE clause");
}
whereClause_.checkReturnsBool("WHERE clause", false);
Expr e = whereClause_.findFirstOf(AnalyticExpr.class);
if (e != null) {
throw new AnalysisException(
"WHERE clause must not contain analytic expressions: " + e.toSql());
}
analyzer_.registerConjuncts(whereClause_, false);
}
/**
* Generates and registers !empty() predicates to filter out empty
* collections directly in the parent scan of collection table refs. This is
* a performance optimization to avoid the expensive processing of empty
* collections inside a subplan that would yield an empty result set.
*
* For correctness purposes, the predicates are generated in cases where we
* can ensure that they will be assigned only to the parent scan, and no other
* plan node.
*
* The conditions are as follows:
* - collection table ref is relative and non-correlated
* - collection table ref represents the rhs of an inner/cross/semi join
* - collection table ref's parent tuple is not outer joined
*
* Example: table T has field A which is of type array<array<int>>.
* 1) ... T join T.A a join a.item a_nest ... :
* all nodes on the path T -> a -> a_nest
* are required so are checked for !empty.
* 2) ... T left outer join T.A a join a.item a_nest ... : no !empty.
* 3) ... T join T.A a left outer join a.item a_nest ... :
* a checked for !empty.
* 4) ... T left outer join T.A a left outer join a.item a_nest ... :
* no !empty.
*
*
* TODO: In some cases, it is possible to generate !empty() predicates for
* a correlated table ref, but in general, that is not correct for non-trivial
* query blocks. For example, if the block with the correlated ref has an
* aggregation then adding a !empty() predicate would incorrectly discard rows
* from the final result set.
*
* TODO: Evaluating !empty() predicates at non-scan nodes interacts poorly with
* our BE projection of collection slots. For example, rows could incorrectly
* be filtered if a !empty() predicate is assigned to a plan node that comes
* after the unnest of the collection that also performs the projection.
*/
private void registerIsNotEmptyPredicates() throws AnalysisException {
for (TableRef tblRef: fromClause_.getTableRefs()) {
Preconditions.checkState(tblRef.isResolved());
if (!(tblRef instanceof CollectionTableRef)) continue;
CollectionTableRef ref = (CollectionTableRef) tblRef;
// Skip non-relative and correlated refs.
if (!ref.isRelative() || ref.isCorrelated()) continue;
// Skip outer and anti joins.
if (ref.getJoinOp().isOuterJoin() || ref.getJoinOp().isAntiJoin()) continue;
// Do not generate a predicate if the parent tuple is outer joined.
if (analyzer_.isOuterJoined(ref.getResolvedPath().getRootDesc().getId()))
continue;
IsNotEmptyPredicate isNotEmptyPred =
new IsNotEmptyPredicate(ref.getCollectionExpr().clone());
isNotEmptyPred.analyze(analyzer_);
// Register the predicate as an On-clause conjunct because it should only
// affect the result of this join and not the whole FROM clause.
analyzer_.registerOnClauseConjuncts(
Lists.<Expr>newArrayList(isNotEmptyPred), ref);
}
}
/**
* Populates baseTblSmap_ with our combined inline view smap and creates
* baseTblResultExprs.
*/
private void resolveInlineViewRefs()
throws AnalysisException {
// Gather the inline view substitution maps from the enclosed inline views
for (TableRef tblRef: fromClause_) {
if (tblRef instanceof InlineViewRef) {
InlineViewRef inlineViewRef = (InlineViewRef) tblRef;
baseTblSmap_ =
ExprSubstitutionMap.combine(baseTblSmap_, inlineViewRef.getBaseTblSmap());
}
}
baseTblResultExprs_ =
Expr.trySubstituteList(resultExprs_, baseTblSmap_, analyzer_, false);
if (LOG.isTraceEnabled()) {
LOG.trace("baseTblSmap_: " + baseTblSmap_.debugString());
LOG.trace("resultExprs: " + Expr.debugString(resultExprs_));
LOG.trace("baseTblResultExprs: " + Expr.debugString(baseTblResultExprs_));
}
}
/**
* Resolves the given raw path as a STAR path and checks its legality.
* Returns the resolved legal path, or throws if the raw path could not
* be resolved or is an illegal star path.
*/
private Path analyzeStarPath(List<String> rawPath, Analyzer analyzer)
throws AnalysisException {
Path resolvedPath = null;
try {
resolvedPath = analyzer.resolvePath(rawPath, PathType.STAR);
} catch (TableLoadingException e) {
// Should never happen because we only check registered table aliases.
Preconditions.checkState(false);
}
Preconditions.checkNotNull(resolvedPath);
return resolvedPath;
}
/**
* Expand "*" select list item, ignoring semi-joined tables as well as
* complex-typed fields because those are currently illegal in any select
* list (even for inline views, etc.)
*/
private void expandStar() throws AnalysisException {
if (fromClause_.isEmpty()) {
throw new AnalysisException(
"'*' expression in select list requires FROM clause.");
}
// expand in From clause order
for (TableRef tableRef: fromClause_) {
if (analyzer_.isSemiJoined(tableRef.getId())) continue;
Path resolvedPath = new Path(tableRef.getDesc(),
Collections.<String>emptyList());
Preconditions.checkState(resolvedPath.resolve());
expandStar(resolvedPath);
}
}
/**
* Expand "path.*" from a resolved path, ignoring complex-typed fields
* because those are currently illegal in any select list (even for
* inline views, etc.)
*/
private void expandStar(Path resolvedPath)
throws AnalysisException {
Preconditions.checkState(resolvedPath.isResolved());
if (resolvedPath.destTupleDesc() != null &&
resolvedPath.destTupleDesc().getTable() != null &&
resolvedPath.destTupleDesc().getPath().getMatchedTypes().isEmpty()) {
// The resolved path targets a registered tuple descriptor of a catalog
// table. Expand the '*' based on the Hive-column order.
TupleDescriptor tupleDesc = resolvedPath.destTupleDesc();
FeTable table = tupleDesc.getTable();
for (Column c: table.getColumnsInHiveOrder()) {
addStarResultExpr(resolvedPath, c.getName());
}
} else {
// The resolved path does not target the descriptor of a catalog table.
// Expand '*' based on the destination type of the resolved path.
Preconditions.checkState(resolvedPath.destType().isStructType());
StructType structType = (StructType) resolvedPath.destType();
Preconditions.checkNotNull(structType);
// Star expansion for references to nested collections.
// Collection Type Star Expansion
// array<int> --> item
// array<struct<f1,f2,...,fn>> --> f1, f2, ..., fn
// map<int,int> --> key, value
// map<int,struct<f1,f2,...,fn>> --> key, f1, f2, ..., fn
if (structType instanceof CollectionStructType) {
CollectionStructType cst = (CollectionStructType) structType;
if (cst.isMapStruct()) {
addStarResultExpr(resolvedPath, Path.MAP_KEY_FIELD_NAME);
}
if (cst.getOptionalField().getType().isStructType()) {
structType = (StructType) cst.getOptionalField().getType();
for (StructField f: structType.getFields()) {
addStarResultExpr(
resolvedPath, cst.getOptionalField().getName(), f.getName());
}
} else if (cst.isMapStruct()) {
addStarResultExpr(resolvedPath, Path.MAP_VALUE_FIELD_NAME);
} else {
addStarResultExpr(resolvedPath, Path.ARRAY_ITEM_FIELD_NAME);
}
} else {
// Default star expansion.
for (StructField f: structType.getFields()) {
addStarResultExpr(resolvedPath, f.getName());
}
}
}
}
/**
* Helper function used during star expansion to add a single result expr
* based on a given raw path to be resolved relative to an existing path.
* Ignores paths with a complex-typed destination because they are currently
* illegal in any select list (even for inline views, etc.)
*/
private void addStarResultExpr(Path resolvedPath,
String... relRawPath) throws AnalysisException {
Path p = Path.createRelPath(resolvedPath, relRawPath);
Preconditions.checkState(p.resolve());
if (p.destType().isComplexType()) return;
SlotDescriptor slotDesc = analyzer_.registerSlotRef(p);
SlotRef slotRef = new SlotRef(slotDesc);
slotRef.analyze(analyzer_);
resultExprs_.add(slotRef);
colLabels_.add(relRawPath[relRawPath.length - 1]);
}
/**
* Registers view column privileges only when the view a catalog view.
*/
private void registerViewColumnPrivileges() {
for (TableRef tableRef: fromClause_.getTableRefs()) {
if (!(tableRef instanceof InlineViewRef)) continue;
InlineViewRef inlineViewRef = (InlineViewRef) tableRef;
FeView view = inlineViewRef.getView();
// For, local views (CTE), the view definition is explicitly defined in the query,
// this requires registering base column privileges instead of view column
// privileges. For example:
//
// with v as (select id as foo from functional.alltypes) select foo from v
//
// The query will register "functional.alltypes.id" column instead of
// "v.foo" column. This behavior is similar to inline views, e.g.
//
// select foo from (select id as foo from functional.alltypes) v
//
// The query will register "functional.alltypes.id" column instead of "v.foo"
// column.
//
// Contrasting this with catalog views.
//
// create view v(foo) as select id from functional.alltypes
// select v.foo from v
//
// The "select v.foo from v" query requires "v.foo" view column privilege because
// the "v" view definition is not exposed in the query. This behavior is also
// consistent with view access, such that the query requires a "select" privilege
// on "v" view and not "functional.alltypes" table.
boolean isCatalogView = view != null && !view.isLocalView();
if (!isCatalogView) continue;
for (Expr expr: getResultExprs()) {
List<Expr> slotRefs = new ArrayList<>();
expr.collectAll(Predicates.instanceOf(SlotRef.class), slotRefs);
for (Expr e: slotRefs) {
SlotRef slotRef = (SlotRef) e;
analyzer_.registerPrivReq(builder -> builder
.allOf(Privilege.SELECT)
.onColumn(view.getDb().getName(), view.getName(),
slotRef.getDesc().getLabel(), view.getOwnerUser())
.build());
}
}
}
}
/**
* Analyze the HAVING clause. The HAVING clause is a predicate, not a list
* (like GROUP BY or ORDER BY) and so cannot contain ordinals.
*
* At present, Impala's alias substitution logic only works for top-level
* expressions in a list. Since HAVING is a predicate, alias substitution
* does not work (the top-level predicate in HAVING is a Boolean expression.)
*
* TODO: Modify alias substitution to work like column resolution so that aliases
* can appear anywhere in the expression. Then, enable alias substitution in HAVING.
*/
private void analyzeHavingClause() throws AnalysisException {
// Analyze the HAVING clause first so we can check if it contains aggregates.
// We need to analyze/register it even if we are not computing aggregates.
if (havingClause_ == null) return;
// can't contain subqueries
if (havingClause_.contains(Predicates.instanceOf(Subquery.class))) {
throw new AnalysisException(
"Subqueries are not supported in the HAVING clause.");
}
// Resolve (top-level) aliases and analyzes
havingPred_ = resolveReferenceExpr(havingClause_, "HAVING", analyzer_,
ALLOW_ORDINALS_IN_HAVING);
// can't contain analytic exprs
Expr analyticExpr = havingPred_.findFirstOf(AnalyticExpr.class);
if (analyticExpr != null) {
throw new AnalysisException(
"HAVING clause must not contain analytic expressions: "
+ analyticExpr.toSql());
}
havingPred_.checkReturnsBool("HAVING clause", true);
}
private boolean checkForAggregates() throws AnalysisException {
if (groupingExprs_ == null && !selectList_.isDistinct()
&& !TreeNode.contains(resultExprs_, Expr.IS_AGGREGATE)
&& (havingPred_ == null
|| !havingPred_.contains(Expr.IS_AGGREGATE))
&& (sortInfo_ == null
|| !TreeNode.contains(sortInfo_.getSortExprs(),
Expr.IS_AGGREGATE))) {
// We're not computing aggregates but we still need to register the HAVING
// clause which could, e.g., contain a constant expression evaluating to false.
if (havingPred_ != null) analyzer_.registerConjuncts(havingPred_, true);
return false;
}
return true;
}
private void verifyAggSemantics() throws AnalysisException {
// If we're computing an aggregate, we must have a FROM clause.
if (fromClause_.isEmpty()) {
throw new AnalysisException(
"aggregation without a FROM clause is not allowed");
}
if (selectList_.isDistinct()
&& (groupingExprs_ != null
|| TreeNode.contains(resultExprs_, Expr.IS_AGGREGATE)
|| (havingPred_ != null
&& havingPred_.contains(Expr.IS_AGGREGATE)))) {
throw new AnalysisException(
"cannot combine SELECT DISTINCT with aggregate functions or GROUP BY");
}
// Disallow '*' with explicit GROUP BY or aggregation function (we can't group by
// '*', and if you need to name all star-expanded cols in the group by clause you
// might as well do it in the select list).
if (groupingExprs_ != null ||
TreeNode.contains(resultExprs_, Expr.IS_AGGREGATE)) {
for (SelectListItem item : selectList_.getItems()) {
if (item.isStar()) {
throw new AnalysisException(
"cannot combine '*' in select list with grouping or aggregation");
}
}
}
// disallow subqueries in the GROUP BY clause
if (groupingExprs_ != null) {
for (Expr expr: groupingExprs_) {
if (expr.contains(Predicates.instanceOf(Subquery.class))) {
throw new AnalysisException(
"Subqueries are not supported in the GROUP BY clause.");
}
}
}
}
private void analyzeGroupingExprs() throws AnalysisException {
if (groupingExprs_ == null) {
groupingExprsCopy_ = new ArrayList<>();
return;
}
// make a deep copy here, we don't want to modify the original
// exprs during analysis (in case we need to print them later)
groupingExprsCopy_ = Expr.cloneList(groupingExprs_);
substituteOrdinalsAndAliases(groupingExprsCopy_, "GROUP BY", analyzer_);
for (int i = 0; i < groupingExprsCopy_.size(); ++i) {
groupingExprsCopy_.get(i).analyze(analyzer_);
if (groupingExprsCopy_.get(i).contains(Expr.IS_AGGREGATE)) {
// reference the original expr in the error msg
throw new AnalysisException(
"GROUP BY expression must not contain aggregate functions: "
+ groupingExprs_.get(i).toSql());
}
if (groupingExprsCopy_.get(i).contains(AnalyticExpr.class)) {
// reference the original expr in the error msg
throw new AnalysisException(
"GROUP BY expression must not contain analytic expressions: "
+ groupingExprsCopy_.get(i).toSql());
}
}
}
private void collectAggExprs() {
// Collect the aggregate expressions from the SELECT, HAVING and ORDER BY clauses
// of this statement.
aggExprs_ = new ArrayList<>();
TreeNode.collect(resultExprs_, Expr.IS_AGGREGATE, aggExprs_);
if (havingPred_ != null) {
havingPred_.collect(Expr.IS_AGGREGATE, aggExprs_);
}
if (sortInfo_ != null) {
// TODO: Avoid evaluating aggs in ignored order-bys
TreeNode.collect(sortInfo_.getSortExprs(), Expr.IS_AGGREGATE,
aggExprs_);
}
}
private void rewriteCountDistinct() {
// Optionally rewrite all count(distinct <expr>) into equivalent NDV() calls.
if (!analyzer_.getQueryCtx().client_request.query_options.appx_count_distinct) {
return;
}
ndvSmap_ = new ExprSubstitutionMap();
for (FunctionCallExpr aggExpr: aggExprs_) {
if (!aggExpr.isDistinct()
|| !aggExpr.getFnName().getFunction().equals("count")
|| aggExpr.getParams().size() != 1) {
continue;
}
FunctionCallExpr ndvFnCall =
new FunctionCallExpr("ndv", aggExpr.getParams().exprs());
ndvFnCall.analyzeNoThrow(analyzer_);
Preconditions.checkState(ndvFnCall.getType().equals(aggExpr.getType()));
ndvSmap_.put(aggExpr, ndvFnCall);
}
// Replace all count(distinct <expr>) with NDV(<expr>).
List<Expr> substAggExprs = Expr.substituteList(aggExprs_,
ndvSmap_, analyzer_, false);
aggExprs_.clear();
for (Expr aggExpr: substAggExprs) {
Preconditions.checkState(aggExpr instanceof FunctionCallExpr);
aggExprs_.add((FunctionCallExpr) aggExpr);
}
}
private void buildAggregateExprs() throws AnalysisException {
// When DISTINCT aggregates are present, non-distinct (i.e. ALL) aggregates are
// evaluated in two phases (see AggregateInfo for more details). In particular,
// COUNT(c) in "SELECT COUNT(c), AGG(DISTINCT d) from R" is transformed to
// "SELECT SUM(cnt) FROM (SELECT COUNT(c) as cnt from R group by d ) S".
// Since a group-by expression is added to the inner query it returns no rows if
// R is empty, in which case the SUM of COUNTs will return NULL.
// However the original COUNT(c) should have returned 0 instead of NULL in this
// case.
// Therefore, COUNT([ALL]) is transformed into zeroifnull(COUNT([ALL]) if
// i) There is no GROUP-BY clause, and
// ii) Other DISTINCT aggregates are present.
countAllMap_ = createCountAllMap();
countAllMap_ = ExprSubstitutionMap.compose(ndvSmap_, countAllMap_, analyzer_);
List<Expr> substitutedAggs =
Expr.substituteList(aggExprs_, countAllMap_, analyzer_, false);
aggExprs_.clear();
TreeNode.collect(substitutedAggs, Expr.IS_AGGREGATE, aggExprs_);
List<Expr> groupingExprs = groupingExprsCopy_;
if (selectList_.isDistinct()) {
// Create multiAggInfo for SELECT DISTINCT:
// - all select list items turn into grouping exprs
// - there are no aggregate exprs
Preconditions.checkState(groupingExprsCopy_.isEmpty());
Preconditions.checkState(aggExprs_.isEmpty());
groupingExprs = Expr.cloneList(resultExprs_);
}
Expr.removeDuplicates(aggExprs_);
Expr.removeDuplicates(groupingExprs);
multiAggInfo_ = new MultiAggregateInfo(groupingExprs, aggExprs_);
multiAggInfo_.analyze(analyzer_);
}
private void buildResultExprs() throws AnalysisException {
ExprSubstitutionMap finalOutputSmap = multiAggInfo_.getOutputSmap();
ExprSubstitutionMap combinedSmap =
ExprSubstitutionMap.compose(countAllMap_, finalOutputSmap, analyzer_);
// change select list, having and ordering exprs to point to agg output. We need
// to reanalyze the exprs at this point.
if (LOG.isTraceEnabled()) {
LOG.trace("combined smap: " + combinedSmap.debugString());
LOG.trace("desctbl: " + analyzer_.getDescTbl().debugString());
LOG.trace("resultexprs: " + Expr.debugString(resultExprs_));
}
resultExprs_ = Expr.substituteList(resultExprs_, combinedSmap, analyzer_, false);
if (LOG.isTraceEnabled()) {
LOG.trace("post-agg selectListExprs: " + Expr.debugString(resultExprs_));
}
if (havingPred_ != null) {
// Make sure the predicate in the HAVING clause does not contain a
// subquery.
Preconditions.checkState(!havingPred_.contains(
Predicates.instanceOf(Subquery.class)));
havingPred_ = havingPred_.substitute(combinedSmap, analyzer_, false);
analyzer_.registerConjuncts(havingPred_, true);
if (LOG.isTraceEnabled()) {
LOG.trace("post-agg havingPred: " + havingPred_.debugString());
}
}
if (sortInfo_ != null) {
sortInfo_.substituteSortExprs(combinedSmap, analyzer_);
if (LOG.isTraceEnabled()) {
LOG.trace("post-agg orderingExprs: " +
Expr.debugString(sortInfo_.getSortExprs()));
}
}
}
private void verifyAggregation() throws AnalysisException {
// check that all post-agg exprs point to agg output
for (int i = 0; i < selectList_.getItems().size(); ++i) {
if (!resultExprs_.get(i).isBound(multiAggInfo_.getResultTupleId())) {
SelectListItem selectListItem = selectList_.getItems().get(i);
throw new AnalysisException(
"select list expression not produced by aggregation output "
+ "(missing from GROUP BY clause?): "
+ selectListItem.toSql());
}
}
if (orderByElements_ != null) {
for (int i = 0; i < orderByElements_.size(); ++i) {
if (!sortInfo_.getSortExprs().get(i).isBound(
multiAggInfo_.getResultTupleId())) {
throw new AnalysisException(
"ORDER BY expression not produced by aggregation output "
+ "(missing from GROUP BY clause?): "
+ orderByElements_.get(i).getExpr().toSql());
}
}
}
if (havingPred_ != null) {
if (!havingPred_.isBound(multiAggInfo_.getResultTupleId())) {
throw new AnalysisException(
"HAVING clause not produced by aggregation output "
+ "(missing from GROUP BY clause?): "
+ havingClause_.toSql());
}
}
}
/**
* Create a map from COUNT([ALL]) -> zeroifnull(COUNT([ALL])) if
* i) There is no GROUP-BY, and
* ii) There are other distinct aggregates to be evaluated.
* This transformation is necessary for COUNT to correctly return 0
* for empty input relations.
*/
private ExprSubstitutionMap createCountAllMap()
throws AnalysisException {
ExprSubstitutionMap scalarCountAllMap = new ExprSubstitutionMap();
if (groupingExprs_ != null && !groupingExprs_.isEmpty()) {
// There are grouping expressions, so no substitution needs to be done.
return scalarCountAllMap;
}
com.google.common.base.Predicate<FunctionCallExpr> isNotDistinctPred =
new com.google.common.base.Predicate<FunctionCallExpr>() {
@Override
public boolean apply(FunctionCallExpr expr) {
return !expr.isDistinct();
}
};
if (Iterables.all(aggExprs_, isNotDistinctPred)) {
// Only [ALL] aggs, so no substitution needs to be done.
return scalarCountAllMap;
}
com.google.common.base.Predicate<FunctionCallExpr> isCountPred =
new com.google.common.base.Predicate<FunctionCallExpr>() {
@Override
public boolean apply(FunctionCallExpr expr) {
return expr.getFnName().getFunction().equals("count");
}
};
Iterable<FunctionCallExpr> countAllAggs =
Iterables.filter(aggExprs_, Predicates.and(isCountPred, isNotDistinctPred));
for (FunctionCallExpr countAllAgg: countAllAggs) {
// Replace COUNT(ALL) with zeroifnull(COUNT(ALL))
List<Expr> zeroIfNullParam = Lists.newArrayList(countAllAgg.clone());
FunctionCallExpr zeroIfNull =
new FunctionCallExpr("zeroifnull", zeroIfNullParam);
zeroIfNull.analyze(analyzer_);
scalarCountAllMap.put(countAllAgg, zeroIfNull);
}
return scalarCountAllMap;
}
/**
* If the select list contains AnalyticExprs, create AnalyticInfo and substitute
* AnalyticExprs using the AnalyticInfo's smap.
*/
private void createAnalyticInfo()
throws AnalysisException {
// collect AnalyticExprs from the SELECT and ORDER BY clauses
List<AnalyticExpr> analyticExprs = new ArrayList<>();
TreeNode.collect(resultExprs_, AnalyticExpr.class, analyticExprs);
if (sortInfo_ != null) {
TreeNode.collect(sortInfo_.getSortExprs(), AnalyticExpr.class,
analyticExprs);
}
if (analyticExprs.isEmpty()) return;
ExprSubstitutionMap rewriteSmap = new ExprSubstitutionMap();
for (Expr expr: analyticExprs) {
AnalyticExpr toRewrite = (AnalyticExpr)expr;
Expr newExpr = AnalyticExpr.rewrite(toRewrite);
if (newExpr != null) {
newExpr.analyze(analyzer_);
if (!rewriteSmap.containsMappingFor(toRewrite)) {
rewriteSmap.put(toRewrite, newExpr);
}
}
}
if (rewriteSmap.size() > 0) {
// Substitute the exprs with their rewritten versions.
List<Expr> updatedAnalyticExprs =
Expr.substituteList(analyticExprs, rewriteSmap, analyzer_, false);
// This is to get rid the original exprs which have been rewritten.
analyticExprs.clear();
// Collect the new exprs introduced through the rewrite and the
// non-rewrite exprs.
TreeNode.collect(updatedAnalyticExprs, AnalyticExpr.class, analyticExprs);
}
analyticInfo_ = AnalyticInfo.create(analyticExprs, analyzer_);
ExprSubstitutionMap smap = analyticInfo_.getSmap();
// If 'exprRewritten' is true, we have to compose the new smap with
// the existing one.
if (rewriteSmap.size() > 0) {
smap = ExprSubstitutionMap.compose(
rewriteSmap, analyticInfo_.getSmap(), analyzer_);
}
// change select list and ordering exprs to point to analytic output. We need
// to reanalyze the exprs at this point.
resultExprs_ = Expr.substituteList(resultExprs_, smap, analyzer_, false);
if (LOG.isTraceEnabled()) {
LOG.trace("post-analytic selectListExprs: " + Expr.debugString(resultExprs_));
}
if (sortInfo_ != null) {
sortInfo_.substituteSortExprs(smap, analyzer_);
if (LOG.isTraceEnabled()) {
LOG.trace("post-analytic orderingExprs: " +
Expr.debugString(sortInfo_.getSortExprs()));
}
}
}
private void buildColumnLineageGraph() {
ColumnLineageGraph graph = analyzer_.getColumnLineageGraph();
if (multiAggInfo_ != null && multiAggInfo_.hasAggregateExprs()) {
graph.addDependencyPredicates(multiAggInfo_.getGroupingExprs());
}
if (sortInfo_ != null && hasLimit()) {
// When there is a LIMIT clause in conjunction with an ORDER BY, the
// ordering exprs must be added in the column lineage graph.
graph.addDependencyPredicates(sortInfo_.getSortExprs());
}
}
}
/**
* Marks all unassigned join predicates as well as exprs in aggInfo and sortInfo.
*/
@Override
public void materializeRequiredSlots(Analyzer analyzer) {
// Mark unassigned join predicates. Some predicates that must be evaluated by a join
// can also be safely evaluated below the join (picked up by getBoundPredicates()).
// Such predicates will be marked twice and that is ok.
List<Expr> unassigned =
analyzer.getUnassignedConjuncts(getTableRefIds(), true);
List<Expr> unassignedJoinConjuncts = new ArrayList<>();
for (Expr e: unassigned) {
if (analyzer.evalAfterJoin(e)) unassignedJoinConjuncts.add(e);
}
List<Expr> baseTblJoinConjuncts =
Expr.substituteList(unassignedJoinConjuncts, baseTblSmap_, analyzer, false);
materializeSlots(analyzer, baseTblJoinConjuncts);
if (evaluateOrderBy_) {
// mark ordering exprs before marking agg/analytic exprs because they could contain
// agg/analytic exprs that are not referenced anywhere but the ORDER BY clause
sortInfo_.materializeRequiredSlots(analyzer, baseTblSmap_);
}
if (hasAnalyticInfo()) {
// Mark analytic exprs before marking agg exprs because they could contain agg
// exprs that are not referenced anywhere but the analytic expr.
// Gather unassigned predicates and mark their slots. It is not desirable
// to account for propagated predicates because if an analytic expr is only
// referenced by a propagated predicate, then it's better to not materialize the
// analytic expr at all.
List<TupleId> tids = new ArrayList<>();
getMaterializedTupleIds(tids); // includes the analytic tuple
List<Expr> conjuncts = analyzer.getUnassignedConjuncts(tids, false);
materializeSlots(analyzer, conjuncts);
analyticInfo_.materializeRequiredSlots(analyzer, baseTblSmap_);
}
if (multiAggInfo_ != null) {
// Mark all agg slots required for conjunct evaluation as materialized before
// calling MultiAggregateInfo.materializeRequiredSlots().
List<Expr> conjuncts = multiAggInfo_.collectConjuncts(analyzer, false);
materializeSlots(analyzer, conjuncts);
multiAggInfo_.materializeRequiredSlots(analyzer, baseTblSmap_);
}
}
public List<TupleId> getTableRefIds() {
List<TupleId> result = new ArrayList<>();
for (TableRef ref: fromClause_) {
result.add(ref.getId());
}
return result;
}
/**
* If given expr is rewritten into an integer literal, then return the original expr,
* otherwise return the rewritten expr.
* Used for GROUP BY, ORDER BY, and HAVING where we don't want to create an ordinal
* from a constant arithmetic expr, e.g. 1 * 2 =/=> 2
*/
private Expr rewriteCheckOrdinalResult(ExprRewriter rewriter, Expr expr)
throws AnalysisException {
Expr rewrittenExpr = rewriter.rewrite(expr, analyzer_);
if (Expr.IS_LITERAL.apply(rewrittenExpr) && rewrittenExpr.getType().isIntegerType()) {
return expr;
} else {
return rewrittenExpr;
}
}
@Override
public void rewriteExprs(ExprRewriter rewriter) throws AnalysisException {
Preconditions.checkState(isAnalyzed());
selectList_.rewriteExprs(rewriter, analyzer_);
for (TableRef ref: fromClause_.getTableRefs()) ref.rewriteExprs(rewriter, analyzer_);
if (whereClause_ != null) {
whereClause_ = rewriter.rewrite(whereClause_, analyzer_);
// Also rewrite exprs in the statements of subqueries.
List<Subquery> subqueryExprs = new ArrayList<>();
whereClause_.collect(Subquery.class, subqueryExprs);
for (Subquery s: subqueryExprs) s.getStatement().rewriteExprs(rewriter);
}
if (havingClause_ != null) {
havingClause_ = rewriteCheckOrdinalResult(rewriter, havingClause_);
}
if (groupingExprs_ != null) {
for (int i = 0; i < groupingExprs_.size(); ++i) {
groupingExprs_.set(i, rewriteCheckOrdinalResult(
rewriter, groupingExprs_.get(i)));
}
}
if (orderByElements_ != null) {
for (OrderByElement orderByElem: orderByElements_) {
orderByElem.setExpr(rewriteCheckOrdinalResult(rewriter, orderByElem.getExpr()));
}
}
}
@Override
public String toSql(ToSqlOptions options) {
// Return the SQL string before inline-view expression substitution.
if (!options.showRewritten() && sqlString_ != null) return sqlString_;
StringBuilder strBuilder = new StringBuilder();
if (withClause_ != null) {
strBuilder.append(withClause_.toSql(options));
strBuilder.append(" ");
}
// Select list
strBuilder.append("SELECT ");
if (selectList_.isDistinct()) {
strBuilder.append("DISTINCT ");
}
if (selectList_.hasPlanHints()) {
strBuilder.append(ToSqlUtils.getPlanHintsSql(options, selectList_.getPlanHints()))
.append(" ");
}
for (int i = 0; i < selectList_.getItems().size(); ++i) {
strBuilder.append(selectList_.getItems().get(i).toSql(options));
strBuilder.append((i+1 != selectList_.getItems().size()) ? ", " : "");
}
// From clause
if (!fromClause_.isEmpty()) {
strBuilder.append(fromClause_.toSql(options));
}
// Where clause
if (whereClause_ != null) {
strBuilder.append(" WHERE ");
strBuilder.append(whereClause_.toSql(options));
}
// Group By clause
if (groupingExprs_ != null) {
strBuilder.append(" GROUP BY ");
// Handle both analyzed (multiAggInfo_ != null) and unanalyzed cases.
// Unanalyzed case us used to generate SQL such as for views.
// See ToSqlUtils.getCreateViewSql().
List<Expr> groupingExprs = multiAggInfo_ == null
? groupingExprs_ : multiAggInfo_.getGroupingExprs();
for (int i = 0; i < groupingExprs.size(); ++i) {
strBuilder.append(groupingExprs.get(i).toSql(options));
strBuilder.append((i+1 != groupingExprs.size()) ? ", " : "");
}
}
// Having clause
if (havingClause_ != null) {
strBuilder.append(" HAVING ");
strBuilder.append(havingClause_.toSql(options));
}
// Order By clause
if (orderByElements_ != null) {
strBuilder.append(" ORDER BY ");
for (int i = 0; i < orderByElements_.size(); ++i) {
strBuilder.append(orderByElements_.get(i).toSql(options));
strBuilder.append((i+1 != orderByElements_.size()) ? ", " : "");
}
}
// Limit clause.
strBuilder.append(limitElement_.toSql(options));
return strBuilder.toString();
}
/**
* If the select statement has a sort/top that is evaluated, then the sort tuple
* is materialized. Else, if there is aggregation then the aggregate tuple id is
* materialized. Otherwise, all referenced tables are materialized as long as they are
* not semi-joined. If there are analytics and no sort, then the returned tuple
* ids also include the logical analytic output tuple.
*/
@Override
public void getMaterializedTupleIds(List<TupleId> tupleIdList) {
if (evaluateOrderBy_) {
tupleIdList.add(sortInfo_.getSortTupleDescriptor().getId());
} else if (multiAggInfo_ != null) {
// Return the tuple id produced in the final aggregation step.
tupleIdList.add(multiAggInfo_.getResultTupleId());
} else {
for (TableRef tblRef: fromClause_) {
// Don't include materialized tuple ids from semi-joined table
// refs (see IMPALA-1526)
if (tblRef.getJoinOp().isLeftSemiJoin()) continue;
// Remove the materialized tuple ids of all the table refs that
// are semi-joined by the right semi/anti join.
if (tblRef.getJoinOp().isRightSemiJoin()) tupleIdList.clear();
tupleIdList.addAll(tblRef.getMaterializedTupleIds());
}
}
// We materialize the agg tuple or the table refs together with the analytic tuple.
if (hasAnalyticInfo() && !evaluateOrderBy_) {
tupleIdList.add(analyticInfo_.getOutputTupleId());
}
}
/**
* C'tor for cloning.
*/
private SelectStmt(SelectStmt other) {
super(other);
selectList_ = other.selectList_.clone();
fromClause_ = other.fromClause_.clone();
whereClause_ = (other.whereClause_ != null) ? other.whereClause_.clone() : null;
groupingExprs_ =
(other.groupingExprs_ != null) ? Expr.cloneList(other.groupingExprs_) : null;
havingClause_ = (other.havingClause_ != null) ? other.havingClause_.clone() : null;
colLabels_ = Lists.newArrayList(other.colLabels_);
multiAggInfo_ = (other.multiAggInfo_ != null) ? other.multiAggInfo_.clone() : null;
analyticInfo_ = (other.analyticInfo_ != null) ? other.analyticInfo_.clone() : null;
sqlString_ = (other.sqlString_ != null) ? new String(other.sqlString_) : null;
baseTblSmap_ = other.baseTblSmap_.clone();
}
@Override
protected void collectTableRefs(List<TableRef> tblRefs, boolean fromClauseOnly) {
super.collectTableRefs(tblRefs, fromClauseOnly);
if (fromClauseOnly) {
fromClause_.collectFromClauseTableRefs(tblRefs);
} else {
fromClause_.collectTableRefs(tblRefs);
}
if (!fromClauseOnly && whereClause_ != null) {
// Collect TableRefs in WHERE-clause subqueries.
List<Subquery> subqueries = new ArrayList<>();
whereClause_.collect(Subquery.class, subqueries);
for (Subquery sq: subqueries) {
sq.getStatement().collectTableRefs(tblRefs, fromClauseOnly);
}
}
}
@Override
public void collectInlineViews(Set<FeView> inlineViews) {
// Impala currently supports sub queries only in FROM, WHERE & WITH clauses. Hence,
// this function does not carry out any checks on HAVING clause.
super.collectInlineViews(inlineViews);
List<TableRef> fromTblRefs = getTableRefs();
Preconditions.checkNotNull(inlineViews);
for (TableRef fromTblRef : fromTblRefs) {
if (fromTblRef instanceof InlineViewRef) {
InlineViewRef inlineViewRef = (InlineViewRef) fromTblRef;
inlineViews.add(inlineViewRef.getView());
inlineViewRef.getViewStmt().collectInlineViews(inlineViews);
}
}
if (whereClause_ != null) {
for (Expr conjunct : whereClause_.getConjuncts()) {
List<Subquery> whereSubQueries = Lists.newArrayList();
conjunct.collect(Predicates.instanceOf(Subquery.class), whereSubQueries);
if (whereSubQueries.size() == 0) continue;
// Check that multiple subqueries do not exist in the same expression. This
// should have been already caught by the analysis passes.
Preconditions.checkState(whereSubQueries.size() == 1, "Invariant " +
"violated: Multiple subqueries found in a single expression: " +
conjunct.toSql());
whereSubQueries.get(0).getStatement().collectInlineViews(inlineViews);
}
}
}
@Override
public void reset() {
super.reset();
selectList_.reset();
colLabels_.clear();
fromClause_.reset();
if (whereClause_ != null) whereClause_.reset();
if (groupingExprs_ != null) Expr.resetList(groupingExprs_);
if (havingClause_ != null) havingClause_.reset();
havingPred_ = null;
multiAggInfo_ = null;
analyticInfo_ = null;
baseTblSmap_.clear();
}
@Override
public SelectStmt clone() { return new SelectStmt(this); }
/**
* Check if the stmt returns at most one row. This can happen
* in the following cases:
* 1. select stmt with a 'limit 1' clause
* 2. select stmt with an aggregate function and no group by.
* 3. select stmt with no from clause.
* 4. select from an inline view that returns at most one row.
*
* This function may produce false negatives because the cardinality of the
* result set also depends on the data a stmt is processing.
*/
public boolean returnsSingleRow() {
Preconditions.checkState(isAnalyzed());
// limit 1 clause
if (limitElement_ != null && hasLimit() && limitElement_.getLimit() == 1) return true;
// No from clause (base tables or inline views)
if (fromClause_.isEmpty()) return true;
// Aggregation with no group by and no DISTINCT
if (hasMultiAggInfo() && !hasGroupByClause() && !selectList_.isDistinct()) {
return true;
}
// Select from an inline view that returns at most one row.
List<TableRef> tableRefs = fromClause_.getTableRefs();
if (tableRefs.size() == 1 && tableRefs.get(0) instanceof InlineViewRef) {
InlineViewRef inlineView = (InlineViewRef)tableRefs.get(0);
if (inlineView.queryStmt_ instanceof SelectStmt) {
SelectStmt selectStmt = (SelectStmt)inlineView.queryStmt_;
return selectStmt.returnsSingleRow();
}
}
// In all other cases, return false.
return false;
}
}