package org.rakam.report;
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.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.InPredicate;
import com.facebook.presto.sql.tree.IntervalLiteral;
import com.facebook.presto.sql.tree.IsNotNullPredicate;
import com.facebook.presto.sql.tree.IsNullPredicate;
import com.facebook.presto.sql.tree.LikePredicate;
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.NullLiteral;
import com.facebook.presto.sql.tree.QualifiedNameReference;
import com.facebook.presto.sql.tree.StringLiteral;
import com.facebook.presto.sql.tree.TimeLiteral;
import com.facebook.presto.sql.tree.TimestampLiteral;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.rakam.util.RakamException;
import org.rakam.util.ValidationUtil;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.facebook.presto.sql.tree.LogicalBinaryExpression.Type.AND;
import static com.facebook.presto.sql.tree.LogicalBinaryExpression.Type.OR;
public class PreComputedTableSubQueryVisitor extends AstVisitor<String, Boolean> {
private final Function<String, Optional<String>> columnNameMapper;
public PreComputedTableSubQueryVisitor(Function<String, Optional<String>> columnNameMapper) {
this.columnNameMapper = columnNameMapper;
}
@Override
protected String visitLogicalBinaryExpression(LogicalBinaryExpression node, Boolean negate) {
LogicalBinaryExpression.Type type = node.getType();
if (type == AND) {
// TODO find a better way
// Optimization for the case when one hand or binary expression is IS NOT NULL predicate
if (node.getRight() instanceof IsNotNullPredicate && !(node.getLeft() instanceof IsNotNullPredicate) ||
node.getLeft() instanceof IsNotNullPredicate && !(node.getRight() instanceof IsNotNullPredicate)) {
Expression isNotNull = node.getRight() instanceof IsNotNullPredicate ? node.getRight() : node.getLeft();
Expression setExpression = isNotNull == node.getRight() ? node.getLeft() : node.getRight();
String excludeQuery = process(new IsNullPredicate(((IsNotNullPredicate) isNotNull).getValue()), negate);
return "SELECT l.date, l.dimension, l._user_set FROM (" + process(setExpression, negate) + ") l LEFT JOIN (" + excludeQuery + ") r ON (r.date = l.date AND l.dimension = r.dimension) WHERE r.date IS NULL";
}
String right = process(node.getRight(), negate);
String left = process(node.getLeft(), negate);
// TODO: use INTERSECT when it's implemented in Presto.
return "SELECT l.date, l.dimension, l._user_set FROM (" + left + ") l JOIN (" + right + ") r ON (r.date = l.date)";
} else if (type == OR) {
return "SELECT date, dimension, _user_set FROM (" + process(node.getLeft(), negate) +
" UNION ALL " + process(node.getRight(), negate) + ")";
} else {
throw new IllegalStateException();
}
}
@Override
protected String visitNotExpression(NotExpression node, Boolean negate) {
return process(node.getValue(), !negate);
}
@Override
protected String visitComparisonExpression(ComparisonExpression node, Boolean negate) {
String left = process(node.getLeft(), negate);
String right = process(node.getRight(), negate);
String predicate = node.getType().getValue() + " " + right;
if (negate) {
predicate = String.format("not(%s)", predicate);
}
return "SELECT date, dimension, _user_set FROM " + left + " WHERE dimension " + predicate;
}
@Override
protected String visitLongLiteral(LongLiteral node, Boolean negate) {
return Long.toString(node.getValue());
}
@Override
protected String visitLikePredicate(LikePredicate node, Boolean negate) {
return "SELECT date, dimension, _user_set FROM " + process(node.getValue(), negate) +
" WHERE dimension LIKE " + process(node.getPattern(), negate);
}
@Override
protected String visitIsNotNullPredicate(IsNotNullPredicate node, Boolean negate) {
if (negate) {
return visitIsNullPredicate(new IsNullPredicate(node.getValue()), !negate);
}
String column = process(node.getValue(), negate);
return "SELECT date, dimension, _user_set FROM " + column + " WHERE dimension is not null";
}
@Override
protected String visitIsNullPredicate(IsNullPredicate node, Boolean negate) {
if (negate) {
return visitIsNullPredicate(new IsNullPredicate(node.getValue()), !negate);
}
return "SELECT date, dimension, _user_set FROM " + process(node.getValue(), negate) + " WHERE dimension is null";
}
@Override
protected String visitInPredicate(InPredicate node, Boolean negate) {
String predicate = "IN " + process(node.getValue(), null) + " " + process(node, negate) + " ) ";
if (negate) {
predicate = String.format("NOT ", predicate);
}
return "SELECT date, dimension, _user_set FROM " + process(node.getValue(), negate) + " WHERE dimension " + predicate;
}
@Override
protected String visitBetweenPredicate(BetweenPredicate node, Boolean negate) {
String predicate = "BETWEEN " + process(node.getMin(), null) + " " + process(node, negate) + " ) ";
if (negate) {
predicate = String.format("not(%s)", predicate);
}
return "SELECT date, dimension, _user_set FROM " + process(node.getValue(), negate) + " WHERE dimension " + predicate;
}
@Override
protected String visitDoubleLiteral(DoubleLiteral node, Boolean negate) {
return Double.toString(node.getValue());
}
@Override
protected String visitGenericLiteral(GenericLiteral node, Boolean negate) {
return node.getType() + " '" + node.getValue() + "'";
}
@Override
protected String visitTimeLiteral(TimeLiteral node, Boolean negate) {
return "TIME '" + node.getValue() + "'";
}
@Override
protected String visitTimestampLiteral(TimestampLiteral node, Boolean negate) {
return "TIMESTAMP '" + node.getValue() + "'";
}
@Override
protected String visitNullLiteral(NullLiteral node, Boolean negate) {
return "null";
}
@Override
protected String visitIntervalLiteral(IntervalLiteral node, Boolean negate) {
String sign = (node.getSign() == IntervalLiteral.Sign.NEGATIVE) ? "- " : "";
StringBuilder builder = new StringBuilder()
.append("INTERVAL ")
.append(sign)
.append(" '").append(node.getValue()).append("' ")
.append(node.getStartField());
if (node.getEndField().isPresent()) {
builder.append(" TO ").append(node.getEndField().get());
}
return builder.toString();
}
@Override
protected String visitBooleanLiteral(BooleanLiteral node, Boolean negate) {
return String.valueOf(node.getValue());
}
@Override
protected String visitStringLiteral(StringLiteral node, Boolean negate) {
return "'" + node.getValue().replace("'", "''") + "'";
}
@Override
protected String visitQualifiedNameReference(QualifiedNameReference node, Boolean negate) {
if (node.getName().getParts().size() != 1) {
throw new RakamException("Multiple references in filter expression are not supported: " +
node.getName().getParts().stream().collect(Collectors.joining(",")), HttpResponseStatus.BAD_REQUEST);
}
String tableColumn = ValidationUtil
.checkTableColumn(node.getName().getParts().get(0), "reference in filter", '"');
Optional<String> preComputedTable = columnNameMapper.apply(tableColumn);
if (preComputedTable.isPresent()) {
return preComputedTable.get();
}
throw new UnsupportedOperationException();
}
@Override
protected String visitNode(Node node, Boolean negate) {
throw new UnsupportedOperationException();
}
}