blob: dbf62277789ddee1661f929100177f0ecde01724 [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.ignite.internal.processors.query.h2.sql;
import java.util.HashSet;
import java.util.Set;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.processors.query.h2.opt.GridH2Table;
/**
* Traverse over query AST to find info about partitioned table usage.
*/
class SqlAstTraverser {
/** Query AST root to check. */
private final GridSqlAst root;
/** Whether user specified distributed joins flag. */
private final boolean distributedJoins;
/** Ignite logger. */
private final IgniteLogger log;
/** Whether query has partitioned tables. */
private boolean hasPartitionedTables;
/** Whether query has sub queries. */
private boolean hasSubQueries;
/** Whether query has joins between replicated and partitioned tables. */
private boolean hasOuterJoinReplicatedPartitioned;
/** */
SqlAstTraverser(GridSqlAst root, boolean distributedJoins, IgniteLogger log) {
this.root = root;
this.distributedJoins = distributedJoins;
this.log = log;
}
/** */
public void traverse() {
lookForPartitionedJoin(root, null);
}
/** */
public boolean hasPartitionedTables() {
return hasPartitionedTables;
}
/** */
public boolean hasSubQueries() {
return hasSubQueries;
}
/** */
public boolean hasOuterJoinReplicatedPartitioned() {
return hasOuterJoinReplicatedPartitioned;
}
/**
* Traverse AST while join operation isn't found. Check it if found.
*
* @param ast AST item to check recursively.
* @param upWhere Where condition that applies to this ast.
*/
private void lookForPartitionedJoin(GridSqlAst ast, GridSqlAst upWhere) {
if (ast == null)
return;
GridSqlJoin join = null;
GridSqlAst where = null;
if (ast instanceof GridSqlJoin) {
join = (GridSqlJoin) ast;
where = upWhere;
}
else if (ast instanceof GridSqlSelect) {
GridSqlSelect select = (GridSqlSelect) ast;
if (select.from() instanceof GridSqlJoin) {
join = (GridSqlJoin) select.from();
where = select.where();
}
}
else if (ast instanceof GridSqlSubquery)
hasSubQueries = true;
else if (ast instanceof GridSqlTable)
hasPartitionedTables |= ((GridSqlTable) ast).dataTable().isPartitioned();
// No joins on this level. Traverse AST deeper.
if (join == null) {
for (int i = 0; i < ast.size(); i++)
lookForPartitionedJoin(ast.child(i), null);
return;
}
// Check WHERE clause first.
lookForPartitionedJoin(where, null);
// Check left side of join.
GridSqlTable leftTable = getTable(join.leftTable());
GridH2Table left = null;
// Left side of join is a subquery.
if (leftTable == null) {
hasSubQueries = true;
// Check subquery on left side.
lookForPartitionedJoin(join.leftTable(), where);
}
else {
left = leftTable.dataTable();
// Data table is NULL for views.
if (left != null && left.isPartitioned())
hasPartitionedTables = true;
}
// Check right side of join.
GridSqlTable rightTable = getTable(join.rightTable());
// Right side of join is a subquery.
if (rightTable == null) {
hasSubQueries = true;
// Check subquery and return (can't exctract more info there).
lookForPartitionedJoin(join.rightTable(), where);
return;
}
GridH2Table right = rightTable.dataTable();
if (right != null && right.isPartitioned())
hasPartitionedTables = true;
// Skip check of views.
if (left == null || right == null)
return;
if (join.isLeftOuter() && !left.isPartitioned() && right.isPartitioned())
hasOuterJoinReplicatedPartitioned = true;
// Skip check if at least one of tables isn't partitioned.
if (!(left.isPartitioned() && right.isPartitioned()))
return;
if (!distributedJoins)
checkPartitionedJoin(join, where, left, right, log);
}
/**
* Checks whether an AST contains valid join operation between partitioned tables.
* Join condition should be an equality operation of affinity keys of tables. Conditions can be splitted between
* join and where clauses. If join is invalid then warning a user about that.
*
* @param join The join to check.
* @param where The where statement from previous AST, for nested joins.
* @param left Left side of join.
* @param right Right side of join.
* @param log Ignite logger.
*/
private void checkPartitionedJoin(GridSqlJoin join, GridSqlAst where, GridH2Table left, GridH2Table right, IgniteLogger log) {
String leftTblAls = getAlias(join.leftTable());
String rightTblAls = getAlias(join.rightTable());
// User explicitly specify an affinity key. Otherwise use primary key.
boolean pkLeft = left.getExplicitAffinityKeyColumn() == null;
boolean pkRight = right.getExplicitAffinityKeyColumn() == null;
Set<String> leftAffKeys = affKeys(pkLeft, left);
Set<String> rightAffKeys = affKeys(pkRight, right);
boolean joinIsValid = checkPartitionedCondition(join.on(),
leftTblAls, leftAffKeys, pkLeft,
rightTblAls, rightAffKeys, pkRight);
if (!joinIsValid && where instanceof GridSqlElement)
joinIsValid = checkPartitionedCondition((GridSqlElement) where,
leftTblAls, leftAffKeys, pkLeft,
rightTblAls, rightAffKeys, pkRight);
if (!joinIsValid) {
log.warning(
String.format(
"For join two partitioned tables join condition should be the equality operation of affinity keys." +
" Left side: %s; right side: %s", left.getName(), right.getName())
);
}
}
/** Extract table instance from an AST element. */
private GridSqlTable getTable(GridSqlElement el) {
if (el instanceof GridSqlTable)
return (GridSqlTable) el;
if (el instanceof GridSqlAlias && el.child() instanceof GridSqlTable)
return el.child();
return null;
}
/** Extract alias value. */
private String getAlias(GridSqlElement el) {
if (el instanceof GridSqlAlias)
return ((GridSqlAlias)el).alias();
return null;
}
/** @return Set of possible affinity keys for this table, incl. default _KEY. */
private Set<String> affKeys(boolean pk, GridH2Table tbl) {
Set<String> affKeys = new HashSet<>();
// User explicitly specify an affinity key. Otherwise use primary key.
if (!pk)
affKeys.add(tbl.getAffinityKeyColumn().columnName);
else {
affKeys.add("_KEY");
String keyFieldName = tbl.rowDescriptor().type().keyFieldName();
if (keyFieldName == null)
affKeys.addAll(tbl.rowDescriptor().type().primaryKeyFields());
else
affKeys.add(keyFieldName);
}
return affKeys;
}
/**
* Valid join condition contains:
* 1. Equality of Primary (incl. cases of complex PK) or Affinity keys;
* 2. Additional conditions must be joint with AND operation to the affinity join condition.
*
* @return {@code true} if join condition contains affinity join condition, otherwise {@code false}.
*/
private boolean checkPartitionedCondition(GridSqlElement condition,
String leftTbl, Set<String> leftAffKeys, boolean pkLeft,
String rightTbl, Set<String> rightAffKeys, boolean pkRight) {
if (!(condition instanceof GridSqlOperation))
return false;
GridSqlOperation op = (GridSqlOperation) condition;
// It is may be a part of affinity condition.
if (GridSqlOperationType.EQUAL == op.operationType())
checkEqualityOperation(op, leftTbl, leftAffKeys, pkLeft, rightTbl, rightAffKeys, pkRight);
// Check affinity condition is covered fully. If true then return. Otherwise go deeper.
if (affinityCondIsCovered(leftAffKeys, rightAffKeys))
return true;
// If we don't cover affinity condition prior to first AND then this is not an affinity condition.
if (GridSqlOperationType.AND != op.operationType())
return false;
// Go recursively to childs.
for (int i = 0; i < op.size(); i++) {
boolean ret = checkPartitionedCondition(op.child(i),
leftTbl, leftAffKeys, pkLeft,
rightTbl, rightAffKeys, pkRight);
if (ret)
return true;
}
// Join condition doesn't contain affinity condition.
return false;
}
/** */
private void checkEqualityOperation(GridSqlOperation equalOp,
String leftTbl, Set<String> leftCols, boolean pkLeft,
String rightTbl, Set<String> rightCols, boolean pkRight) {
if (!(equalOp.child(0) instanceof GridSqlColumn))
return;
if (!(equalOp.child(1) instanceof GridSqlColumn))
return;
String leftCol = ((GridSqlColumn) equalOp.child(0)).columnName();
String rightCol = ((GridSqlColumn) equalOp.child(1)).columnName();
String leftTblAls = ((GridSqlColumn) equalOp.child(0)).tableAlias();
String rightTblAls = ((GridSqlColumn) equalOp.child(1)).tableAlias();
Set<String> actLeftCols;
Set<String> actRightCols;
if (leftTbl.equals(leftTblAls))
actLeftCols = leftCols;
else if (leftTbl.equals(rightTblAls))
actLeftCols = rightCols;
else
return;
if (rightTbl.equals(rightTblAls))
actRightCols = rightCols;
else if (rightTbl.equals(leftTblAls))
actRightCols = leftCols;
else
return;
// This is part of the affinity join condition.
if (actLeftCols.contains(leftCol) && actRightCols.contains(rightCol)) {
if (pkLeft && "_KEY".equals(leftCol))
actLeftCols.clear();
else if (pkLeft) {
actLeftCols.remove(leftCol);
// Only _KEY is there.
if (actLeftCols.size() == 1)
actLeftCols.clear();
}
else
actLeftCols.remove(leftCol);
if (pkRight && "_KEY".equals(rightCol))
actRightCols.clear();
else if (pkRight) {
actRightCols.remove(rightCol);
// Only _KEY is there.
if (actRightCols.size() == 1)
actRightCols.clear();
}
else
actRightCols.remove(rightCol);
}
}
/** */
private boolean affinityCondIsCovered(Set<String> leftAffKeys, Set<String> rightAffKeys) {
return leftAffKeys.isEmpty() && rightAffKeys.isEmpty();
}
}