/* * Licensed to CRATE Technology GmbH ("Crate") under one or more contributor * license agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. Crate 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. * * However, if you have executed another commercial license agreement * with Crate these terms will supersede the license and you may use the * software solely pursuant to the terms of the relevant commercial agreement. */ package io.crate.sql; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; import com.google.common.collect.TreeMultimap; import io.crate.sql.tree.*; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collector; import java.util.stream.Collectors; import static io.crate.sql.SqlFormatter.formatSql; public final class ExpressionFormatter { private static final Collector<CharSequence, ?, String> COMMA_JOINER = Collectors.joining(", "); private static final Set<String> FUNCTION_CALLS_WITHOUT_PARENTHESIS = ImmutableSet.of( "current_catalog", "current_schema", "current_user", "session_user", "user"); private ExpressionFormatter() { } public static String formatExpression(Expression expression) { return new Formatter().process(expression, null); } public static class Formatter extends AstVisitor<String, Void> { @Override protected String visitNode(Node node, Void context) { throw new UnsupportedOperationException(String.format(Locale.ENGLISH, "cannot handle node '%s'", node.toString())); } @Override protected String visitExpression(Expression node, Void context) { throw new UnsupportedOperationException(String.format(Locale.ENGLISH, "not yet implemented: %s.visit%s", getClass().getName(), node.getClass().getSimpleName())); } @Override protected String visitCurrentTime(CurrentTime node, Void context) { StringBuilder builder = new StringBuilder(); switch (node.getType()) { case TIME: builder.append("current_time"); break; case DATE: builder.append("current_date"); break; case TIMESTAMP: builder.append("current_timestamp"); break; default: throw new UnsupportedOperationException("not yet implemented: " + node.getType()); } if (node.getPrecision().isPresent()) { builder.append('(') .append(node.getPrecision().get()) .append(')'); } return builder.toString(); } @Override protected String visitExtract(Extract node, Void context) { return "EXTRACT(" + node.getField() + " FROM " + process(node.getExpression(), context) + ")"; } @Override protected String visitBooleanLiteral(BooleanLiteral node, Void context) { return String.valueOf(node.getValue()); } @Override protected String visitSubscriptExpression(SubscriptExpression node, Void context) { return node.name() + "[" + node.index() + "]"; } @Override public String visitParameterExpression(ParameterExpression node, Void context) { return "$" + node.position(); } @Override protected String visitStringLiteral(StringLiteral node, Void context) { return Literals.quoteStringLiteral(node.getValue()); } @Override protected String visitLongLiteral(LongLiteral node, Void context) { return Long.toString(node.getValue()); } @Override protected String visitDoubleLiteral(DoubleLiteral node, Void context) { return Double.toString(node.getValue()); } @Override protected String visitTimeLiteral(TimeLiteral node, Void context) { return "TIME '" + node.getValue() + "'"; } @Override protected String visitTimestampLiteral(TimestampLiteral node, Void context) { return "TIMESTAMP '" + node.getValue() + "'"; } @Override protected String visitNullLiteral(NullLiteral node, Void context) { return "null"; } @Override protected String visitDateLiteral(DateLiteral node, Void context) { return "DATE '" + node.getValue() + "'"; } @Override public String visitArrayLiteral(ArrayLiteral node, Void context) { StringBuilder builder = new StringBuilder("["); boolean first = true; for (Expression element : node.values()) { if (!first) { builder.append(","); } else { first = false; } builder.append(element.accept(this, context)); } return builder.append("]").toString(); } @Override public String visitObjectLiteral(ObjectLiteral node, Void context) { StringBuilder builder = new StringBuilder("{"); boolean first = true; TreeMultimap<String, Expression> sorted = TreeMultimap.create( Ordering.natural().nullsLast(), Ordering.usingToString().nullsLast() ); sorted.putAll(node.values()); for (Map.Entry<String, Expression> entry : sorted.entries()) { if (!first) { builder.append(", "); } else { first = false; } builder.append(formatIdentifier(entry.getKey())) .append("= ") .append(entry.getValue().accept(this, context)); } return builder.append("}").toString(); } @Override protected String visitSubqueryExpression(SubqueryExpression node, Void context) { return "(" + formatSql(node.getQuery()) + ")"; } @Override protected String visitExists(ExistsPredicate node, Void context) { return "EXISTS (" + formatSql(node.getSubquery()) + ")"; } @Override protected String visitQualifiedNameReference(QualifiedNameReference node, Void context) { return node.getName().getParts().stream() .map(Formatter::formatIdentifier) .collect(Collectors.joining(".")); } @Override protected String visitFunctionCall(FunctionCall node, Void context) { StringBuilder builder = new StringBuilder(); String arguments = joinExpressions(node.getArguments()); if (node.getArguments().isEmpty() && "count".equalsIgnoreCase(node.getName().getSuffix())) { arguments = "*"; } if (node.isDistinct()) { arguments = "DISTINCT " + arguments; } builder.append(node.getName()); if (!FUNCTION_CALLS_WITHOUT_PARENTHESIS.contains(node.getName().toString())) { builder.append('(').append(arguments).append(')'); } return builder.toString(); } @Override protected String visitLogicalBinaryExpression(LogicalBinaryExpression node, Void context) { return formatBinaryExpression(node.getType().toString(), node.getLeft(), node.getRight()); } @Override protected String visitNotExpression(NotExpression node, Void context) { return "(NOT " + process(node.getValue(), null) + ")"; } @Override protected String visitComparisonExpression(ComparisonExpression node, Void context) { return formatBinaryExpression(node.getType().getValue(), node.getLeft(), node.getRight()); } @Override protected String visitIsNullPredicate(IsNullPredicate node, Void context) { return "(" + process(node.getValue(), null) + " IS NULL)"; } @Override protected String visitIsNotNullPredicate(IsNotNullPredicate node, Void context) { return "(" + process(node.getValue(), null) + " IS NOT NULL)"; } @Override protected String visitIfExpression(IfExpression node, Void context) { StringBuilder builder = new StringBuilder(); builder.append("IF(") .append(process(node.getCondition(), context)) .append(", ") .append(process(node.getTrueValue(), context)); if (node.getFalseValue().isPresent()) { builder.append(", ") .append(process(node.getFalseValue().get(), context)); } builder.append(")"); return builder.toString(); } @Override protected String visitNegativeExpression(NegativeExpression node, Void context) { return "- " + process(node.getValue(), null); } @Override protected String visitArithmeticExpression(ArithmeticExpression node, Void context) { return formatBinaryExpression(node.getType().getValue(), node.getLeft(), node.getRight()); } @Override protected String visitLikePredicate(LikePredicate node, Void context) { StringBuilder builder = new StringBuilder(); builder.append('(') .append(process(node.getValue(), null)) .append(" LIKE ") .append(process(node.getPattern(), null)); if (node.getEscape() != null) { builder.append(" ESCAPE ") .append(process(node.getEscape(), null)); } builder.append(')'); return builder.toString(); } @Override public String visitArrayLikePredicate(ArrayLikePredicate node, Void context) { StringBuilder builder = new StringBuilder(); builder.append('(') .append(process(node.getPattern(), null)) .append(node.inverse() ? " NOT" : "") .append(" LIKE ") .append(node.quantifier().name()) .append(" (") .append(process(node.getValue(), null)) .append(") "); if (node.getEscape() != null) { builder.append("ESCAPE ") .append(process(node.getEscape(), null)); } builder.append(')'); return builder.toString(); } @Override public String visitMatchPredicate(MatchPredicate node, Void context) { StringBuilder builder = new StringBuilder(); builder.append("MATCH ("); if (node.idents().size() == 1) { builder.append(process(node.idents().get(0).columnIdent(), context)); } else { builder.append("("); List<MatchPredicateColumnIdent> idents = node.idents(); for (int i = 0, identsSize = idents.size(); i < identsSize; i++) { MatchPredicateColumnIdent ident = idents.get(i); builder.append(ident.accept(this, null)); if (i < (identsSize - 1)) { builder.append(", "); } } builder.append(")"); } builder.append(", ").append(process(node.value(), context)); builder.append(")"); if (node.matchType() != null) { builder.append(" USING ").append(node.matchType()).append(" "); if (node.properties().properties().size() > 0) { builder.append(process(node.properties(), context)); } } return builder.toString(); } @Override public String visitMatchPredicateColumnIdent(MatchPredicateColumnIdent node, Void context) { String column = process(node.columnIdent(), null); if (!(node.boost() instanceof NullLiteral)) { column = column + " " + node.boost().toString(); } return column; } @Override public String visitGenericProperties(GenericProperties node, Void context) { return " WITH (" + node.properties().entrySet().stream() .map(prop -> prop.getKey() + "=" + process(prop.getValue(), null)) .collect(COMMA_JOINER) + ")"; } @Override protected String visitAllColumns(AllColumns node, Void context) { if (node.getPrefix().isPresent()) { return node.getPrefix().get() + ".*"; } return "*"; } @Override public String visitCast(Cast node, Void context) { return "CAST(" + process(node.getExpression(), context) + " AS " + process(node.getType(), context) + ")"; } @Override protected String visitTryCast(TryCast node, Void context) { return "TRY_CAST(" + process(node.getExpression(), context) + " AS " + process(node.getType(), context) + ")"; } @Override protected String visitSearchedCaseExpression(SearchedCaseExpression node, Void context) { ImmutableList.Builder<String> parts = ImmutableList.builder(); parts.add("CASE"); for (WhenClause whenClause : node.getWhenClauses()) { parts.add(process(whenClause, context)); } if (node.getDefaultValue() != null) { parts.add("ELSE") .add(process(node.getDefaultValue(), context)); } parts.add("END"); return "(" + String.join(" ", parts.build()) + ")"; } @Override protected String visitSimpleCaseExpression(SimpleCaseExpression node, Void context) { ImmutableList.Builder<String> parts = ImmutableList.builder(); parts.add("CASE") .add(process(node.getOperand(), context)); for (WhenClause whenClause : node.getWhenClauses()) { parts.add(process(whenClause, context)); } if (node.getDefaultValue() != null) { parts.add("ELSE") .add(process(node.getDefaultValue(), context)); } parts.add("END"); return "(" + String.join(" ", parts.build()) + ")"; } @Override protected String visitWhenClause(WhenClause node, Void context) { return "WHEN " + process(node.getOperand(), context) + " THEN " + process(node.getResult(), context); } @Override protected String visitBetweenPredicate(BetweenPredicate node, Void context) { return "(" + process(node.getValue(), context) + " BETWEEN " + process(node.getMin(), context) + " AND " + process(node.getMax(), context) + ")"; } @Override protected String visitInPredicate(InPredicate node, Void context) { return "(" + process(node.getValue(), context) + " IN " + process(node.getValueList(), context) + ")"; } @Override protected String visitInListExpression(InListExpression node, Void context) { return "(" + joinExpressions(node.getValues()) + ")"; } @Override public String visitColumnType(ColumnType node, Void context) { return node.name(); } @Override public String visitCollectionColumnType(CollectionColumnType node, Void context) { return node.name() + "(" + process(node.innerType(), context) + ")"; } @Override public String visitObjectColumnType(ObjectColumnType node, Void context) { return node.name(); } private String formatBinaryExpression(String operator, Expression left, Expression right) { return '(' + process(left, null) + ' ' + operator + ' ' + process(right, null) + ')'; } private String joinExpressions(List<Expression> expressions) { return expressions.stream() .map(expression -> process(expression, null)) .collect(COMMA_JOINER); } private static String formatIdentifier(String s) { return Identifiers.quote(s); } } }