/*
* 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.opt;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.cache.CacheException;
import org.apache.ignite.internal.processors.query.h2.sql.GridSqlQueryParser;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.h2.command.dml.Query;
import org.h2.command.dml.Select;
import org.h2.command.dml.SelectUnion;
import org.h2.expression.Comparison;
import org.h2.expression.Expression;
import org.h2.expression.ExpressionColumn;
import org.h2.index.IndexCondition;
import org.h2.index.ViewIndex;
import org.h2.table.Column;
import org.h2.table.IndexColumn;
import org.h2.table.SubQueryInfo;
import org.h2.table.Table;
import org.h2.table.TableFilter;
import org.h2.table.TableView;
/**
* Collocation model for a query.
*/
public final class GridH2CollocationModel {
/** */
public static final int MULTIPLIER_COLLOCATED = 1;
/** */
private static final int MULTIPLIER_UNICAST = 50;
/** */
private static final int MULTIPLIER_BROADCAST = 200;
/** */
private static final int MULTIPLIER_REPLICATED_NOT_LAST = 10_000;
/** */
private final GridH2CollocationModel upper;
/** */
private final int filter;
/** */
private final boolean view;
/** */
private int multiplier;
/** */
private Type type;
/** */
private GridH2CollocationModel[] children;
/** */
private TableFilter[] childFilters;
/** */
private List<GridH2CollocationModel> unions;
/** */
private Select select;
/** */
private final boolean validate;
/**
* @param upper Upper.
* @param filter Filter.
* @param view This model will be a subquery (or top level query) and must contain child filters.
* @param validate Query validation flag.
*/
private GridH2CollocationModel(GridH2CollocationModel upper, int filter, boolean view, boolean validate) {
this.upper = upper;
this.filter = filter;
this.view = view;
this.validate = validate;
}
/**
* @return Table filter for this collocation model.
*/
private TableFilter filter() {
return upper == null ? null : upper.childFilters[filter];
}
/** {@inheritDoc} */
@Override public String toString() {
calculate();
SB b = new SB();
for (int lvl = 0; lvl < 20; lvl++) {
if (!toString(b, lvl))
break;
b.a('\n');
}
return b.toString();
}
/**
* @param b String builder.
* @param lvl Depth level.
*/
private boolean toString(SB b, int lvl) {
boolean res = false;
if (lvl == 0) {
TableFilter f = filter();
String tblAlias = f == null ? "^" : f.getTableAlias();
b.a("[tbl=").a(tblAlias).a(", type=").a(type).a(", mul=").a(multiplier).a("]");
res = true;
}
else if (childFilters != null) {
assert lvl > 0;
lvl--;
for (int i = 0; i < childFilters.length; i++) {
if (lvl == 0)
b.a(" | ");
res |= child(i, true).toString(b, lvl);
}
if (lvl == 0)
b.a(" | ");
}
return res;
}
/**
* @param upper Upper.
* @param filter Filter.
* @param unions Unions.
* @param view This model will be a subquery (or top level query) and must contain child filters.
* @param validate Query validation flag.
* @return Created child collocation model.
*/
private static GridH2CollocationModel createChildModel(GridH2CollocationModel upper,
int filter,
List<GridH2CollocationModel> unions,
boolean view,
boolean validate) {
GridH2CollocationModel child = new GridH2CollocationModel(upper, filter, view, validate);
if (unions != null) {
// Bind created child to unions.
assert upper == null || upper.child(filter, false) != null || unions.isEmpty();
if (upper != null && unions.isEmpty()) {
assert upper.child(filter, false) == null;
upper.children[filter] = child;
}
unions.add(child);
child.unions = unions;
}
else if (upper != null) {
// Bind created child to upper model.
assert upper.child(filter, false) == null;
upper.children[filter] = child;
}
return child;
}
/**
* @param childFilters New child filters.
* @return {@code true} If child filters were updated.
*/
private boolean childFilters(TableFilter[] childFilters) {
assert childFilters != null;
assert view;
Select select = childFilters[0].getSelect();
assert this.select == null || this.select == select;
if (this.select == null) {
this.select = select;
assert this.childFilters == null;
}
else if (Arrays.equals(this.childFilters, childFilters))
return false;
if (this.childFilters == null) {
// We have to clone because H2 reuses array and reorders elements.
this.childFilters = childFilters.clone();
children = new GridH2CollocationModel[childFilters.length];
}
else {
assert this.childFilters.length == childFilters.length;
// We have to copy because H2 reuses array and reorders elements.
System.arraycopy(childFilters, 0, this.childFilters, 0, childFilters.length);
Arrays.fill(children, null);
}
// Reset results.
type = null;
multiplier = 0;
return true;
}
/**
* @param i Index.
* @param f Table filter.
* @return {@code true} If the child is not a table or view.
*/
private boolean isChildTableOrView(int i, TableFilter f) {
if (f == null)
f = childFilters[i];
Table t = f.getTable();
return t.isView() || t instanceof GridH2Table;
}
/**
* Do the needed calculations.
*/
private void calculate() {
if (type != null)
return;
if (view) { // We are at (sub-)query model.
assert childFilters != null;
boolean collocated = true;
boolean partitioned = false;
int maxMultiplier = MULTIPLIER_COLLOCATED;
for (int i = 0; i < childFilters.length; i++) {
GridH2CollocationModel child = child(i, true);
Type t = child.type(true);
if (child.multiplier == MULTIPLIER_REPLICATED_NOT_LAST)
maxMultiplier = child.multiplier;
if (t.isPartitioned()) {
partitioned = true;
if (!t.isCollocated()) {
collocated = false;
int m = child.multiplier(true);
if (m > maxMultiplier) {
maxMultiplier = m;
if (maxMultiplier == MULTIPLIER_REPLICATED_NOT_LAST)
break;
}
}
}
}
type = Type.of(partitioned, collocated);
multiplier = maxMultiplier;
}
else {
assert upper != null;
assert childFilters == null;
// We are at table instance.
Table tbl = filter().getTable();
// Only partitioned tables will do distributed joins.
if (!(tbl instanceof GridH2Table) || !((GridH2Table)tbl).isPartitioned()) {
type = Type.REPLICATED;
multiplier = MULTIPLIER_COLLOCATED;
return;
}
// If we are the first partitioned table in a join, then we are "base" for all the rest partitioned tables
// which will need to get remote result (if there is no affinity condition). Since this query is broadcasted
// to all the affinity nodes the "base" does not need to get remote results.
if (!upper.findPartitionedTableBefore(filter)) {
type = Type.PARTITIONED_COLLOCATED;
multiplier = MULTIPLIER_COLLOCATED;
}
else {
// It is enough to make sure that our previous join by affinity key is collocated, then we are
// collocated. If we at least have affinity key condition, then we do unicast which is cheaper.
switch (upper.joinedWithCollocated(filter)) {
case COLLOCATED_JOIN:
type = Type.PARTITIONED_COLLOCATED;
multiplier = MULTIPLIER_COLLOCATED;
break;
case HAS_AFFINITY_CONDITION:
type = Type.PARTITIONED_NOT_COLLOCATED;
multiplier = MULTIPLIER_UNICAST;
break;
case NONE:
type = Type.PARTITIONED_NOT_COLLOCATED;
multiplier = MULTIPLIER_BROADCAST;
break;
default:
throw new IllegalStateException();
}
}
if (upper.previousReplicated(filter))
multiplier = MULTIPLIER_REPLICATED_NOT_LAST;
}
}
/**
* @param f Current filter.
* @return {@code true} If partitioned table was found.
*/
private boolean findPartitionedTableBefore(int f) {
for (int i = 0; i < f; i++) {
GridH2CollocationModel child = child(i, true);
// The c can be null if it is not a GridH2Table and not a sub-query,
// it is a some kind of function table or anything else that considered replicated.
if (child != null && child.type(true).isPartitioned())
return true;
}
// We have to search globally in upper queries as well.
return upper != null && upper.findPartitionedTableBefore(filter);
}
/**
* @param f Current filter.
* @return {@code true} If previous table is REPLICATED.
*/
private boolean previousReplicated(int f) {
if (f > 0 && child(f - 1, true).type(true) == Type.REPLICATED)
return true;
return upper != null && upper.previousReplicated(filter);
}
/**
* @param f Filter.
* @return Affinity join type.
*/
private Affinity joinedWithCollocated(int f) {
TableFilter tf = childFilters[f];
GridH2Table tbl = (GridH2Table)tf.getTable();
if (validate) {
if (tbl.rowDescriptor().context().customAffinityMapper())
throw customAffinityError(tbl.spaceName());
if (F.isEmpty(tf.getIndexConditions())) {
throw new CacheException("Failed to prepare distributed join query: " +
"join condition does not use index [joinedCache=" + tbl.spaceName() +
", plan=" + tf.getSelect().getPlanSQL() + ']');
}
}
IndexColumn affCol = tbl.getAffinityKeyColumn();
boolean affKeyCondFound = false;
if (affCol != null) {
ArrayList<IndexCondition> idxConditions = tf.getIndexConditions();
int affColId = affCol.column.getColumnId();
for (int i = 0; i < idxConditions.size(); i++) {
IndexCondition c = idxConditions.get(i);
int colId = c.getColumn().getColumnId();
int cmpType = c.getCompareType();
if ((cmpType == Comparison.EQUAL || cmpType == Comparison.EQUAL_NULL_SAFE) &&
(colId == affColId || tbl.rowDescriptor().isKeyColumn(colId)) && c.isEvaluatable()) {
affKeyCondFound = true;
Expression exp = c.getExpression();
exp = exp.getNonAliasExpression();
if (exp instanceof ExpressionColumn) {
ExpressionColumn expCol = (ExpressionColumn)exp;
// This is one of our previous joins.
TableFilter prevJoin = expCol.getTableFilter();
if (prevJoin != null) {
GridH2CollocationModel cm = child(indexOf(prevJoin), true);
// If the previous joined model is a subquery (view), we can not be sure that
// the found affinity column is the needed one, since we can select multiple
// different affinity columns from different tables.
if (cm != null && !cm.view) {
Type t = cm.type(true);
if (t.isPartitioned() && t.isCollocated() && isAffinityColumn(prevJoin, expCol, validate))
return Affinity.COLLOCATED_JOIN;
}
}
}
}
}
}
return affKeyCondFound ? Affinity.HAS_AFFINITY_CONDITION : Affinity.NONE;
}
/**
* @param f Table filter.
* @return Index.
*/
private int indexOf(TableFilter f) {
for (int i = 0; i < childFilters.length; i++) {
if (childFilters[i] == f)
return i;
}
throw new IllegalStateException();
}
/**
* @param f Table filter.
* @param expCol Expression column.
* @param validate Query validation flag.
* @return {@code true} It it is an affinity column.
*/
private static boolean isAffinityColumn(TableFilter f, ExpressionColumn expCol, boolean validate) {
Column col = expCol.getColumn();
if (col == null)
return false;
Table t = col.getTable();
if (t.isView()) {
Query qry;
if (f.getIndex() != null)
qry = getSubQuery(f);
else
qry = GridSqlQueryParser.VIEW_QUERY.get((TableView)t);
return isAffinityColumn(qry, expCol, validate);
}
if (t instanceof GridH2Table) {
if (validate && ((GridH2Table)t).rowDescriptor().context().customAffinityMapper())
throw customAffinityError(((GridH2Table)t).spaceName());
IndexColumn affCol = ((GridH2Table)t).getAffinityKeyColumn();
return affCol != null && col.getColumnId() == affCol.column.getColumnId();
}
return false;
}
/**
* @param qry Query.
* @param expCol Expression column.
* @param validate Query validation flag.
* @return {@code true} It it is an affinity column.
*/
private static boolean isAffinityColumn(Query qry, ExpressionColumn expCol, boolean validate) {
if (qry.isUnion()) {
SelectUnion union = (SelectUnion)qry;
return isAffinityColumn(union.getLeft(), expCol, validate) && isAffinityColumn(union.getRight(), expCol, validate);
}
Expression exp = qry.getExpressions().get(expCol.getColumn().getColumnId()).getNonAliasExpression();
if (exp instanceof ExpressionColumn) {
expCol = (ExpressionColumn)exp;
return isAffinityColumn(expCol.getTableFilter(), expCol, validate);
}
return false;
}
/**
* @return Multiplier.
*/
public int calculateMultiplier() {
// We don't need multiplier for union here because it will be summarized in H2.
return multiplier(false);
}
/**
* @param withUnion With respect to union.
* @return Multiplier.
*/
private int multiplier(boolean withUnion) {
calculate();
assert multiplier != 0;
if (withUnion && unions != null) {
int maxMultiplier = 0;
for (int i = 0; i < unions.size(); i++) {
int m = unions.get(i).multiplier(false);
if (m > maxMultiplier)
maxMultiplier = m;
}
return maxMultiplier;
}
return multiplier;
}
/**
* @param withUnion With respect to union.
* @return Type.
*/
private Type type(boolean withUnion) {
calculate();
assert type != null;
if (withUnion && unions != null) {
Type left = unions.get(0).type(false);
for (int i = 1; i < unions.size(); i++) {
Type right = unions.get(i).type(false);
if (!left.isCollocated() || !right.isCollocated()) {
left = Type.PARTITIONED_NOT_COLLOCATED;
break;
}
else if (!left.isPartitioned() && !right.isPartitioned())
left = Type.REPLICATED;
else
left = Type.PARTITIONED_COLLOCATED;
}
return left;
}
return type;
}
/**
* @param i Index.
* @param create Create child if needed.
* @return Child collocation.
*/
private GridH2CollocationModel child(int i, boolean create) {
GridH2CollocationModel child = children[i];
if (child == null && create) {
TableFilter f = childFilters[i];
if (f.getTable().isView()) {
if (f.getIndex() == null) {
// If we don't have view index yet, then we just creating empty model and it must be filled later.
child = createChildModel(this, i, null, true, validate);
}
else
child = buildCollocationModel(this, i, getSubQuery(f), null, validate);
}
else
child = createChildModel(this, i, null, false, validate);
assert child != null;
assert children[i] == child;
}
return child;
}
/**
* @param f Table filter.
* @return Sub-query.
*/
private static Query getSubQuery(TableFilter f) {
return ((ViewIndex)f.getIndex()).getQuery();
}
/**
* @return Unions list.
*/
private List<GridH2CollocationModel> getOrCreateUnions() {
if (unions == null) {
unions = new ArrayList<>(4);
unions.add(this);
}
return unions;
}
/**
* @param qctx Query context.
* @param info Sub-query info.
* @param filters Filters.
* @param filter Filter.
* @param validate Query validation flag.
* @return Collocation.
*/
public static GridH2CollocationModel buildCollocationModel(GridH2QueryContext qctx, SubQueryInfo info,
TableFilter[] filters, int filter, boolean validate) {
GridH2CollocationModel cm;
if (info != null) {
// Go up until we reach the root query.
cm = buildCollocationModel(qctx, info.getUpper(), info.getFilters(), info.getFilter(), validate);
}
else {
// We are at the root query.
cm = qctx.queryCollocationModel();
if (cm == null) {
cm = createChildModel(null, -1, null, true, validate);
qctx.queryCollocationModel(cm);
}
}
assert cm.view;
Select select = filters[0].getSelect();
// Handle union. We have to rely on fact that select will be the same on uppermost select.
// For sub-queries we will drop collocation models, so that they will be recalculated anyways.
if (cm.select != null && cm.select != select) {
List<GridH2CollocationModel> unions = cm.getOrCreateUnions();
// Try to find this select in existing unions.
// Start with 1 because at 0 it always will be c.
for (int i = 1; i < unions.size(); i++) {
GridH2CollocationModel u = unions.get(i);
if (u.select == select) {
cm = u;
break;
}
}
// Nothing was found, need to create new child in union.
if (cm.select != select)
cm = createChildModel(cm.upper, cm.filter, unions, true, validate);
}
cm.childFilters(filters);
return cm.child(filter, true);
}
/**
* @param qry Query.
* @return {@code true} If the query is collocated.
*/
public static boolean isCollocated(Query qry) {
GridH2CollocationModel mdl = buildCollocationModel(null, -1, qry, null, true);
Type type = mdl.type(true);
if (!type.isCollocated() && mdl.multiplier == MULTIPLIER_REPLICATED_NOT_LAST)
throw new CacheException("Failed to execute query: for distributed join " +
"all REPLICATED caches must be at the end of the joined tables list.");
return type.isCollocated();
}
/**
* @param upper Upper.
* @param filter Filter.
* @param qry Query.
* @param unions Unions.
* @param validate Query validation flag.
* @return Built model.
*/
private static GridH2CollocationModel buildCollocationModel(GridH2CollocationModel upper,
int filter,
Query qry,
List<GridH2CollocationModel> unions,
boolean validate) {
if (qry.isUnion()) {
if (unions == null)
unions = new ArrayList<>();
SelectUnion union = (SelectUnion)qry;
GridH2CollocationModel left = buildCollocationModel(upper, filter, union.getLeft(), unions, validate);
GridH2CollocationModel right = buildCollocationModel(upper, filter, union.getRight(), unions, validate);
assert left != null;
assert right != null;
return upper != null ? upper : left;
}
Select select = (Select)qry;
List<TableFilter> list = new ArrayList<>();
for (TableFilter f = select.getTopTableFilter(); f != null; f = f.getJoin())
list.add(f);
TableFilter[] filters = list.toArray(new TableFilter[list.size()]);
GridH2CollocationModel cm = createChildModel(upper, filter, unions, true, validate);
cm.childFilters(filters);
for (int i = 0; i < filters.length; i++) {
TableFilter f = filters[i];
if (f.getTable().isView())
buildCollocationModel(cm, i, getSubQuery(f), null, validate);
else if (f.getTable() instanceof GridH2Table)
createChildModel(cm, i, null, false, validate);
}
return upper != null ? upper : cm;
}
/**
* @param cacheName Cache name.
* @return Error.
*/
private static CacheException customAffinityError(String cacheName) {
return new CacheException("Failed to prepare distributed join query: can not use distributed joins for cache " +
"with custom AffinityKeyMapper configured. " +
"Please use AffinityKeyMapped annotation instead [cache=" + cacheName + ']');
}
/**
* Collocation type.
*/
private enum Type {
/** */
PARTITIONED_COLLOCATED(true, true),
/** */
PARTITIONED_NOT_COLLOCATED(true, false),
/** */
REPLICATED(false, true);
/** */
private final boolean partitioned;
/** */
private final boolean collocated;
/**
* @param partitioned Partitioned.
* @param collocated Collocated.
*/
Type(boolean partitioned, boolean collocated) {
this.partitioned = partitioned;
this.collocated = collocated;
}
/**
* @return {@code true} If partitioned.
*/
public boolean isPartitioned() {
return partitioned;
}
/**
* @return {@code true} If collocated.
*/
public boolean isCollocated() {
return collocated;
}
/**
* @param partitioned Partitioned.
* @param collocated Collocated.
* @return Type.
*/
static Type of(boolean partitioned, boolean collocated) {
if (collocated)
return partitioned ? Type.PARTITIONED_COLLOCATED : Type.REPLICATED;
assert partitioned;
return Type.PARTITIONED_NOT_COLLOCATED;
}
}
/**
* Affinity of a table relative to previous joined tables.
*/
private enum Affinity {
/** */
NONE,
/** */
HAS_AFFINITY_CONDITION,
/** */
COLLOCATED_JOIN
}
}