/*
* Licensed 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 com.facebook.presto.sql.planner.assertions;
import com.facebook.presto.sql.tree.ArithmeticBinaryExpression;
import com.facebook.presto.sql.tree.AstVisitor;
import com.facebook.presto.sql.tree.BetweenPredicate;
import com.facebook.presto.sql.tree.BooleanLiteral;
import com.facebook.presto.sql.tree.Cast;
import com.facebook.presto.sql.tree.CoalesceExpression;
import com.facebook.presto.sql.tree.ComparisonExpression;
import com.facebook.presto.sql.tree.DoubleLiteral;
import com.facebook.presto.sql.tree.Expression;
import com.facebook.presto.sql.tree.GenericLiteral;
import com.facebook.presto.sql.tree.InListExpression;
import com.facebook.presto.sql.tree.InPredicate;
import com.facebook.presto.sql.tree.LogicalBinaryExpression;
import com.facebook.presto.sql.tree.LongLiteral;
import com.facebook.presto.sql.tree.Node;
import com.facebook.presto.sql.tree.NotExpression;
import com.facebook.presto.sql.tree.StringLiteral;
import com.facebook.presto.sql.tree.SymbolReference;
import com.facebook.presto.sql.tree.TryExpression;
import java.util.List;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
/**
* Expression visitor which verifies if given expression (actual) is matching other expression given as context (expected).
* Visitor returns true if plans match to each other.
* <p/>
* Note that actual expression is using real name references (table columns etc) while expected expression is using symbol aliases.
* Given symbol alias can point only to one real name reference.
* <p/>
* Example:
* <pre>
* NOT (orderkey = 3 AND custkey = 3 AND orderkey < 10)
* </pre>
* will match to:
* <pre>
* NOT (X = 3 AND Y = 3 AND X < 10)
* </pre>
* , but will not match to:
* <pre>
* NOT (X = 3 AND Y = 3 AND Z < 10)
* </pre>
* nor to
* <pre>
* NOT (X = 3 AND X = 3 AND X < 10)
* </pre>
*/
final class ExpressionVerifier
extends AstVisitor<Boolean, Expression>
{
private final SymbolAliases symbolAliases;
ExpressionVerifier(SymbolAliases symbolAliases)
{
this.symbolAliases = requireNonNull(symbolAliases, "symbolAliases is null");
}
@Override
protected Boolean visitNode(Node node, Expression context)
{
throw new IllegalStateException(format("Node %s is not supported", node));
}
@Override
protected Boolean visitTryExpression(TryExpression actual, Expression expected)
{
if (!(expected instanceof TryExpression)) {
return false;
}
return process(actual.getInnerExpression(), ((TryExpression) expected).getInnerExpression());
}
@Override
protected Boolean visitCast(Cast actual, Expression expectedExpression)
{
if (!(expectedExpression instanceof Cast)) {
return false;
}
Cast expected = (Cast) expectedExpression;
if (!actual.getType().equals(expected.getType())) {
return false;
}
return process(actual.getExpression(), expected.getExpression());
}
@Override
protected Boolean visitInPredicate(InPredicate actual, Expression expectedExpression)
{
if (expectedExpression instanceof InPredicate) {
InPredicate expected = (InPredicate) expectedExpression;
if (actual.getValueList() instanceof InListExpression) {
return process(actual.getValue(), expected.getValue()) && process(actual.getValueList(), expected.getValueList());
}
else {
checkState(
expected.getValueList() instanceof InListExpression,
"ExpressionVerifier doesn't support unpacked expected values. Feel free to add support if needed");
/*
* If the expected value is a value list, but the actual is e.g. a SymbolReference,
* we need to unpack the value from the list so that when we hit visitSymbolReference, the
* expected.toString() call returns something that the symbolAliases actually contains.
* For example, InListExpression.toString returns "(onlyitem)" rather than "onlyitem".
*
* This is required because actual passes through the analyzer, planner, and possibly optimizers,
* one of which sometimes takes the liberty of unpacking the InListExpression.
*
* Since the expected value doesn't go through all of that, we have to deal with the case
* of the actual value being unpacked, but the expected value being an InListExpression.
*/
List<Expression> values = ((InListExpression) expected.getValueList()).getValues();
checkState(values.size() == 1, "Multiple expressions in expected value list %s, but actual value is not a list", values, actual.getValue());
Expression onlyExpectedExpression = values.get(0);
return process(actual.getValue(), expected.getValue()) && process(actual.getValueList(), onlyExpectedExpression);
}
}
return false;
}
@Override
protected Boolean visitComparisonExpression(ComparisonExpression actual, Expression expectedExpression)
{
if (expectedExpression instanceof ComparisonExpression) {
ComparisonExpression expected = (ComparisonExpression) expectedExpression;
if (actual.getType() == expected.getType()) {
return process(actual.getLeft(), expected.getLeft()) && process(actual.getRight(), expected.getRight());
}
}
return false;
}
@Override
protected Boolean visitArithmeticBinary(ArithmeticBinaryExpression actual, Expression expectedExpression)
{
if (expectedExpression instanceof ArithmeticBinaryExpression) {
ArithmeticBinaryExpression expected = (ArithmeticBinaryExpression) expectedExpression;
if (actual.getType() == expected.getType()) {
return process(actual.getLeft(), expected.getLeft()) && process(actual.getRight(), expected.getRight());
}
}
return false;
}
protected Boolean visitGenericLiteral(GenericLiteral actual, Expression expected)
{
if (expected instanceof GenericLiteral) {
return getValueFromLiteral(actual).equals(getValueFromLiteral(expected));
}
return false;
}
@Override
protected Boolean visitLongLiteral(LongLiteral actual, Expression expected)
{
if (expected instanceof LongLiteral) {
return getValueFromLiteral(actual).equals(getValueFromLiteral(expected));
}
return false;
}
@Override
protected Boolean visitDoubleLiteral(DoubleLiteral actual, Expression expected)
{
if (expected instanceof DoubleLiteral) {
return getValueFromLiteral(actual).equals(getValueFromLiteral(expected));
}
return false;
}
@Override
protected Boolean visitBooleanLiteral(BooleanLiteral actual, Expression expected)
{
if (expected instanceof BooleanLiteral) {
return getValueFromLiteral(actual).equals(getValueFromLiteral(expected));
}
return false;
}
private static String getValueFromLiteral(Expression expression)
{
if (expression instanceof LongLiteral) {
return String.valueOf(((LongLiteral) expression).getValue());
}
else if (expression instanceof BooleanLiteral) {
return String.valueOf(((BooleanLiteral) expression).getValue());
}
else if (expression instanceof DoubleLiteral) {
return String.valueOf(((DoubleLiteral) expression).getValue());
}
else if (expression instanceof GenericLiteral) {
return ((GenericLiteral) expression).getValue();
}
else {
throw new IllegalArgumentException("Unsupported literal expression type: " + expression.getClass().getName());
}
}
@Override
protected Boolean visitStringLiteral(StringLiteral actual, Expression expectedExpression)
{
if (expectedExpression instanceof StringLiteral) {
StringLiteral expected = (StringLiteral) expectedExpression;
return actual.getValue().equals(expected.getValue());
}
return false;
}
@Override
protected Boolean visitLogicalBinaryExpression(LogicalBinaryExpression actual, Expression expectedExpression)
{
if (expectedExpression instanceof LogicalBinaryExpression) {
LogicalBinaryExpression expected = (LogicalBinaryExpression) expectedExpression;
if (actual.getType() == expected.getType()) {
return process(actual.getLeft(), expected.getLeft()) && process(actual.getRight(), expected.getRight());
}
}
return false;
}
@Override
protected Boolean visitBetweenPredicate(BetweenPredicate actual, Expression expectedExpression)
{
if (expectedExpression instanceof BetweenPredicate) {
BetweenPredicate expected = (BetweenPredicate) expectedExpression;
return process(actual.getValue(), expected.getValue()) && process(actual.getMin(), expected.getMin()) && process(actual.getMax(), expected.getMax());
}
return false;
}
@Override
protected Boolean visitNotExpression(NotExpression actual, Expression expected)
{
if (expected instanceof NotExpression) {
return process(actual.getValue(), ((NotExpression) expected).getValue());
}
return false;
}
@Override
protected Boolean visitSymbolReference(SymbolReference actual, Expression expected)
{
if (!(expected instanceof SymbolReference)) {
return false;
}
return symbolAliases.get(((SymbolReference) expected).getName()).equals(actual);
}
@Override
protected Boolean visitCoalesceExpression(CoalesceExpression actual, Expression expected)
{
if (!(expected instanceof CoalesceExpression)) {
return false;
}
CoalesceExpression expectedCoalesce = (CoalesceExpression) expected;
if (actual.getOperands().size() == expectedCoalesce.getOperands().size()) {
boolean verified = true;
for (int i = 0; i < actual.getOperands().size(); i++) {
verified &= process(actual.getOperands().get(i), expectedCoalesce.getOperands().get(i));
}
return verified;
}
return false;
}
}