This is an automated email from the ASF dual-hosted git repository.
jackietien pushed a commit to branch ty/TableModelGrammar
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/ty/TableModelGrammar by this
push:
new c00011d0285 Add format
c00011d0285 is described below
commit c00011d0285f55579ac38a401bf2ea10bb2da34e
Author: JackieTien97 <[email protected]>
AuthorDate: Thu Feb 29 16:35:19 2024 +0800
Add format
---
.../relational/sql/util/ExpressionFormatter.java | 621 ++++++++++++++++
.../iotdb/db/relational/sql/util/SqlFormatter.java | 783 +++++++++++++++++++++
2 files changed, 1404 insertions(+)
diff --git
a/iotdb-core/relational-parser/src/main/java/org/apache/iotdb/db/relational/sql/util/ExpressionFormatter.java
b/iotdb-core/relational-parser/src/main/java/org/apache/iotdb/db/relational/sql/util/ExpressionFormatter.java
index d2fe0cc7e5c..71ef8e51cab 100644
---
a/iotdb-core/relational-parser/src/main/java/org/apache/iotdb/db/relational/sql/util/ExpressionFormatter.java
+++
b/iotdb-core/relational-parser/src/main/java/org/apache/iotdb/db/relational/sql/util/ExpressionFormatter.java
@@ -19,18 +19,87 @@
package org.apache.iotdb.db.relational.sql.util;
+import org.apache.iotdb.db.relational.sql.tree.AllColumns;
+import org.apache.iotdb.db.relational.sql.tree.AllRows;
+import org.apache.iotdb.db.relational.sql.tree.ArithmeticBinaryExpression;
+import org.apache.iotdb.db.relational.sql.tree.ArithmeticUnaryExpression;
import org.apache.iotdb.db.relational.sql.tree.AstVisitor;
+import org.apache.iotdb.db.relational.sql.tree.BetweenPredicate;
+import org.apache.iotdb.db.relational.sql.tree.BinaryLiteral;
+import org.apache.iotdb.db.relational.sql.tree.BooleanLiteral;
+import org.apache.iotdb.db.relational.sql.tree.Cast;
+import org.apache.iotdb.db.relational.sql.tree.CoalesceExpression;
+import org.apache.iotdb.db.relational.sql.tree.ComparisonExpression;
+import org.apache.iotdb.db.relational.sql.tree.CurrentDatabase;
+import org.apache.iotdb.db.relational.sql.tree.CurrentTime;
+import org.apache.iotdb.db.relational.sql.tree.CurrentUser;
+import org.apache.iotdb.db.relational.sql.tree.DecimalLiteral;
+import org.apache.iotdb.db.relational.sql.tree.DereferenceExpression;
+import org.apache.iotdb.db.relational.sql.tree.DoubleLiteral;
+import org.apache.iotdb.db.relational.sql.tree.ExistsPredicate;
import org.apache.iotdb.db.relational.sql.tree.Expression;
+import org.apache.iotdb.db.relational.sql.tree.FieldReference;
+import org.apache.iotdb.db.relational.sql.tree.FunctionCall;
+import org.apache.iotdb.db.relational.sql.tree.GenericDataType;
+import org.apache.iotdb.db.relational.sql.tree.GenericLiteral;
+import org.apache.iotdb.db.relational.sql.tree.GroupingElement;
+import org.apache.iotdb.db.relational.sql.tree.GroupingSets;
+import org.apache.iotdb.db.relational.sql.tree.Identifier;
+import org.apache.iotdb.db.relational.sql.tree.IfExpression;
+import org.apache.iotdb.db.relational.sql.tree.InListExpression;
+import org.apache.iotdb.db.relational.sql.tree.InPredicate;
+import org.apache.iotdb.db.relational.sql.tree.IsNotNullPredicate;
+import org.apache.iotdb.db.relational.sql.tree.IsNullPredicate;
+import org.apache.iotdb.db.relational.sql.tree.LikePredicate;
import org.apache.iotdb.db.relational.sql.tree.Literal;
+import org.apache.iotdb.db.relational.sql.tree.LogicalExpression;
+import org.apache.iotdb.db.relational.sql.tree.LongLiteral;
+import org.apache.iotdb.db.relational.sql.tree.Node;
+import org.apache.iotdb.db.relational.sql.tree.NotExpression;
+import org.apache.iotdb.db.relational.sql.tree.NullIfExpression;
+import org.apache.iotdb.db.relational.sql.tree.NullLiteral;
+import org.apache.iotdb.db.relational.sql.tree.NumericParameter;
+import org.apache.iotdb.db.relational.sql.tree.OrderBy;
+import org.apache.iotdb.db.relational.sql.tree.Parameter;
+import org.apache.iotdb.db.relational.sql.tree.QualifiedName;
+import org.apache.iotdb.db.relational.sql.tree.QuantifiedComparisonExpression;
+import org.apache.iotdb.db.relational.sql.tree.Row;
+import org.apache.iotdb.db.relational.sql.tree.SearchedCaseExpression;
+import org.apache.iotdb.db.relational.sql.tree.SimpleCaseExpression;
+import org.apache.iotdb.db.relational.sql.tree.SimpleGroupBy;
+import org.apache.iotdb.db.relational.sql.tree.SortItem;
+import org.apache.iotdb.db.relational.sql.tree.StringLiteral;
+import org.apache.iotdb.db.relational.sql.tree.SubqueryExpression;
import org.apache.iotdb.db.relational.sql.tree.SymbolReference;
+import org.apache.iotdb.db.relational.sql.tree.Trim;
+import org.apache.iotdb.db.relational.sql.tree.TypeParameter;
+import org.apache.iotdb.db.relational.sql.tree.WhenClause;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.List;
+import java.util.Locale;
import java.util.Optional;
import java.util.function.Function;
+import static com.google.common.collect.Iterables.getOnlyElement;
import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static
org.apache.iotdb.db.relational.sql.util.ReservedIdentifiers.reserved;
+import static org.apache.iotdb.db.relational.sql.util.SqlFormatter.formatName;
+import static org.apache.iotdb.db.relational.sql.util.SqlFormatter.formatSql;
public final class ExpressionFormatter {
+ private static final ThreadLocal<DecimalFormat> doubleFormatter =
+ ThreadLocal.withInitial(
+ () ->
+ new DecimalFormat("0.###################E0###", new
DecimalFormatSymbols(Locale.US)));
+
private ExpressionFormatter() {}
public static String formatExpression(Expression expression) {
@@ -48,5 +117,557 @@ public final class ExpressionFormatter {
this.symbolReferenceFormatter =
requireNonNull(symbolReferenceFormatter, "symbolReferenceFormatter
is null");
}
+
+ @Override
+ protected String visitNode(Node node, Void context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected String visitRow(Row node, Void context) {
+ return node.getItems().stream()
+ .map(child -> process(child, context))
+ .collect(joining(", ", "ROW (", ")"));
+ }
+
+ @Override
+ protected String visitExpression(Expression node, Void context) {
+ throw new UnsupportedOperationException(
+ String.format(
+ "not yet implemented: %s.visit%s",
+ getClass().getName(), node.getClass().getSimpleName()));
+ }
+
+ @Override
+ protected String visitCurrentDatabase(CurrentDatabase node, Void context) {
+ return "CURRENT_DATABASE";
+ }
+
+ @Override
+ protected String visitCurrentUser(CurrentUser node, Void context) {
+ return "CURRENT_USER";
+ }
+
+ @Override
+ protected String visitTrim(Trim node, Void context) {
+ if (!node.getTrimCharacter().isPresent()) {
+ return String.format(
+ "trim(%s FROM %s)", node.getSpecification(),
process(node.getTrimSource(), context));
+ }
+
+ return String.format(
+ "trim(%s %s FROM %s)",
+ node.getSpecification(),
+ process(node.getTrimCharacter().get(), context),
+ process(node.getTrimSource(), context));
+ }
+
+ @Override
+ protected String visitCurrentTime(CurrentTime node, Void context) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(node.getFunction().getName());
+
+ if (node.getPrecision().isPresent()) {
+ builder.append('(').append(node.getPrecision()).append(')');
+ }
+
+ return builder.toString();
+ }
+
+ @Override
+ protected String visitBooleanLiteral(BooleanLiteral node, Void context) {
+ return literalFormatter
+ .map(formatter -> formatter.apply(node))
+ .orElseGet(() -> String.valueOf(node.getValue()));
+ }
+
+ @Override
+ protected String visitStringLiteral(StringLiteral node, Void context) {
+ return literalFormatter
+ .map(formatter -> formatter.apply(node))
+ .orElseGet(() -> formatStringLiteral(node.getValue()));
+ }
+
+ @Override
+ protected String visitBinaryLiteral(BinaryLiteral node, Void context) {
+ return literalFormatter
+ .map(formatter -> formatter.apply(node))
+ .orElseGet(() -> "X'" + node.toHexString() + "'");
+ }
+
+ @Override
+ protected String visitParameter(Parameter node, Void context) {
+ return "?";
+ }
+
+ @Override
+ protected String visitAllRows(AllRows node, Void context) {
+ return "ALL";
+ }
+
+ @Override
+ protected String visitLongLiteral(LongLiteral node, Void context) {
+ return literalFormatter.map(formatter ->
formatter.apply(node)).orElseGet(node::getValue);
+ }
+
+ @Override
+ protected String visitDoubleLiteral(DoubleLiteral node, Void context) {
+ return literalFormatter
+ .map(formatter -> formatter.apply(node))
+ .orElseGet(() -> doubleFormatter.get().format(node.getValue()));
+ }
+
+ @Override
+ protected String visitDecimalLiteral(DecimalLiteral node, Void context) {
+ return literalFormatter
+ .map(formatter -> formatter.apply(node))
+ // TODO return node value without "DECIMAL '..'" when
+ // FeaturesConfig#parseDecimalLiteralsAsDouble switch is removed
+ .orElseGet(() -> "DECIMAL '" + node.getValue() + "'");
+ }
+
+ @Override
+ protected String visitGenericLiteral(GenericLiteral node, Void context) {
+ return literalFormatter
+ .map(formatter -> formatter.apply(node))
+ .orElseGet(() -> node.getType() + " " +
formatStringLiteral(node.getValue()));
+ }
+
+ @Override
+ protected String visitNullLiteral(NullLiteral node, Void context) {
+ return literalFormatter.map(formatter ->
formatter.apply(node)).orElse("null");
+ }
+
+ @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 visitIdentifier(Identifier node, Void context) {
+ if (node.isDelimited() || reserved(node.getValue())) {
+ return '"' + node.getValue().replace("\"", "\"\"") + '"';
+ }
+ return node.getValue();
+ }
+
+ @Override
+ protected String visitSymbolReference(SymbolReference node, Void context) {
+ if (symbolReferenceFormatter.isPresent()) {
+ return symbolReferenceFormatter.get().apply(node);
+ }
+ return formatIdentifier(node.getName());
+ }
+
+ private String formatIdentifier(String s) {
+ return '"' + s.replace("\"", "\"\"") + '"';
+ }
+
+ @Override
+ protected String visitDereferenceExpression(DereferenceExpression node,
Void context) {
+ String baseString = process(node.getBase(), context);
+ return baseString + "." + node.getField().map(this::process).orElse("*");
+ }
+
+ @Override
+ public String visitFieldReference(FieldReference node, Void context) {
+ // add colon so this won't parse
+ return ":input(" + node.getFieldIndex() + ")";
+ }
+
+ @Override
+ protected String visitFunctionCall(FunctionCall node, Void context) {
+ if (QualifiedName.of("LISTAGG").equals(node.getName())) {
+ return visitListagg(node);
+ }
+
+ 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(formatName(node.getName())).append('(').append(arguments);
+
+ builder.append(')');
+
+ return builder.toString();
+ }
+
+ @Override
+ protected String visitLogicalExpression(LogicalExpression node, Void
context) {
+ return "("
+ + node.getTerms().stream()
+ .map(term -> process(term, context))
+ .collect(joining(" " + node.getOperator().toString() + " "))
+ + ")";
+ }
+
+ @Override
+ protected String visitNotExpression(NotExpression node, Void context) {
+ return "(NOT " + process(node.getValue(), context) + ")";
+ }
+
+ @Override
+ protected String visitComparisonExpression(ComparisonExpression node, Void
context) {
+ return formatBinaryExpression(node.getOperator().getValue(),
node.getLeft(), node.getRight());
+ }
+
+ @Override
+ protected String visitIsNullPredicate(IsNullPredicate node, Void context) {
+ return "(" + process(node.getValue(), context) + " IS NULL)";
+ }
+
+ @Override
+ protected String visitIsNotNullPredicate(IsNotNullPredicate node, Void
context) {
+ return "(" + process(node.getValue(), context) + " IS NOT NULL)";
+ }
+
+ @Override
+ protected String visitNullIfExpression(NullIfExpression node, Void
context) {
+ return "NULLIF("
+ + process(node.getFirst(), context)
+ + ", "
+ + process(node.getSecond(), context)
+ + ')';
+ }
+
+ @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));
+ node.getFalseValue()
+ .map(expression -> builder.append(", ").append(process(expression,
context)));
+ builder.append(")");
+ return builder.toString();
+ }
+
+ @Override
+ protected String visitCoalesceExpression(CoalesceExpression node, Void
context) {
+ return "COALESCE(" + joinExpressions(node.getOperands()) + ")";
+ }
+
+ @Override
+ protected String visitArithmeticUnary(ArithmeticUnaryExpression node, Void
context) {
+ String value = process(node.getValue(), context);
+
+ switch (node.getSign()) {
+ // Unary is ambiguous with respect to negative numbers. "-1" parses
as a number, but
+ // "-(1)" parses as "unaryMinus(number)"
+ // The parentheses are needed to ensure the parsing roundtrips
properly.
+ case MINUS:
+ return "-(" + value + ")";
+ case PLUS:
+ return "+" + value;
+ default:
+ throw new IllegalArgumentException("Unknown sign: " +
node.getSign());
+ }
+ }
+
+ @Override
+ protected String visitArithmeticBinary(ArithmeticBinaryExpression node,
Void context) {
+ return formatBinaryExpression(node.getOperator().getValue(),
node.getLeft(), node.getRight());
+ }
+
+ @Override
+ protected String visitLikePredicate(LikePredicate node, Void context) {
+ StringBuilder builder = new StringBuilder();
+
+ builder
+ .append('(')
+ .append(process(node.getValue(), context))
+ .append(" LIKE ")
+ .append(process(node.getPattern(), context));
+
+ node.getEscape()
+ .ifPresent(escape -> builder.append(" ESCAPE
").append(process(escape, context)));
+
+ builder.append(')');
+
+ return builder.toString();
+ }
+
+ @Override
+ protected String visitAllColumns(AllColumns node, Void context) {
+ StringBuilder builder = new StringBuilder();
+ if (node.getTarget().isPresent()) {
+ builder.append(process(node.getTarget().get(), context));
+ builder.append(".*");
+ } else {
+ builder.append("*");
+ }
+
+ if (!node.getAliases().isEmpty()) {
+ builder.append(" AS (");
+ Joiner.on(", ")
+ .appendTo(
+ builder,
+ node.getAliases().stream().map(alias -> process(alias,
context)).collect(toList()));
+ builder.append(")");
+ }
+
+ return builder.toString();
+ }
+
+ @Override
+ public String visitCast(Cast node, Void context) {
+ return (node.isSafe() ? "TRY_CAST" : "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));
+ }
+
+ node.getDefaultValue().ifPresent(value ->
parts.add("ELSE").add(process(value, context)));
+
+ parts.add("END");
+
+ return "(" + Joiner.on(' ').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));
+ }
+
+ node.getDefaultValue().ifPresent(value ->
parts.add("ELSE").add(process(value, context)));
+
+ parts.add("END");
+
+ return "(" + Joiner.on(' ').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
+ protected String visitQuantifiedComparisonExpression(
+ QuantifiedComparisonExpression node, Void context) {
+ return String.format(
+ "(%s %s %s %s)",
+ process(node.getValue(), context),
+ node.getOperator().getValue(),
+ node.getQuantifier(),
+ process(node.getSubquery(), context));
+ }
+
+ @Override
+ protected String visitGenericDataType(GenericDataType node, Void context) {
+ StringBuilder result = new StringBuilder();
+ result.append(node.getName());
+
+ if (!node.getArguments().isEmpty()) {
+ result.append(
+ node.getArguments().stream().map(this::process).collect(joining(",
", "(", ")")));
+ }
+
+ return result.toString();
+ }
+
+ @Override
+ protected String visitTypeParameter(TypeParameter node, Void context) {
+ return process(node.getValue(), context);
+ }
+
+ @Override
+ protected String visitNumericTypeParameter(NumericParameter node, Void
context) {
+ return node.getValue();
+ }
+
+ 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(e -> process(e,
null)).collect(joining(", "));
+ }
+
+ /**
+ * Returns the formatted `LISTAGG` function call corresponding to the
specified node.
+ *
+ * <p>During the parsing of the syntax tree, the `LISTAGG` expression is
synthetically converted
+ * to a function call. This method formats the specified {@link
FunctionCall} node to correspond
+ * to the standardised syntax of the `LISTAGG` expression.
+ *
+ * @param node the `LISTAGG` function call
+ */
+ private String visitListagg(FunctionCall node) {
+ StringBuilder builder = new StringBuilder();
+
+ List<Expression> arguments = node.getArguments();
+ Expression expression = arguments.get(0);
+ Expression separator = arguments.get(1);
+ BooleanLiteral overflowError = (BooleanLiteral) arguments.get(2);
+ Expression overflowFiller = arguments.get(3);
+ BooleanLiteral showOverflowEntryCount = (BooleanLiteral)
arguments.get(4);
+
+ String innerArguments = joinExpressions(ImmutableList.of(expression,
separator));
+ if (node.isDistinct()) {
+ innerArguments = "DISTINCT " + innerArguments;
+ }
+
+ builder.append("LISTAGG").append('(').append(innerArguments);
+
+ builder.append(" ON OVERFLOW ");
+ if (overflowError.getValue()) {
+ builder.append(" ERROR");
+ } else {
+ builder.append(" TRUNCATE").append(' ').append(process(overflowFiller,
null));
+ if (showOverflowEntryCount.getValue()) {
+ builder.append(" WITH COUNT");
+ } else {
+ builder.append(" WITHOUT COUNT");
+ }
+ }
+
+ builder.append(')');
+
+ return builder.toString();
+ }
+ }
+
+ static String formatStringLiteral(String s) {
+ return "'" + s.replace("'", "''") + "'";
+ }
+
+ public static String formatOrderBy(OrderBy orderBy) {
+ return "ORDER BY " + formatSortItems(orderBy.getSortItems());
+ }
+
+ public static String formatSortItems(List<SortItem> sortItems) {
+ return
sortItems.stream().map(sortItemFormatterFunction()).collect(joining(", "));
+ }
+
+ static String formatGroupBy(List<GroupingElement> groupingElements) {
+ return groupingElements.stream()
+ .map(
+ groupingElement -> {
+ String result = "";
+ if (groupingElement instanceof SimpleGroupBy) {
+ List<Expression> columns = groupingElement.getExpressions();
+ if (columns.size() == 1) {
+ result = formatExpression(getOnlyElement(columns));
+ } else {
+ result = formatGroupingSet(columns);
+ }
+ } else if (groupingElement instanceof GroupingSets) {
+ GroupingSets groupingSets = (GroupingSets) groupingElement;
+ String type = null;
+ switch (groupingSets.getType()) {
+ case EXPLICIT:
+ type = "GROUPING SETS";
+ break;
+ case CUBE:
+ type = "CUBE";
+ break;
+ case ROLLUP:
+ type = "ROLLUP";
+ break;
+ }
+
+ result =
+ groupingSets.getSets().stream()
+ .map(ExpressionFormatter::formatGroupingSet)
+ .collect(joining(", ", type + " (", ")"));
+ }
+ return result;
+ })
+ .collect(joining(", "));
+ }
+
+ private static boolean isAsciiPrintable(int codePoint) {
+ return codePoint >= 0x20 && codePoint < 0x7F;
+ }
+
+ private static String formatGroupingSet(List<Expression> groupingSet) {
+ return groupingSet.stream()
+ .map(ExpressionFormatter::formatExpression)
+ .collect(joining(", ", "(", ")"));
+ }
+
+ private static Function<SortItem, String> sortItemFormatterFunction() {
+ return input -> {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(formatExpression(input.getSortKey()));
+
+ switch (input.getOrdering()) {
+ case ASCENDING:
+ builder.append(" ASC");
+ break;
+ case DESCENDING:
+ builder.append(" DESC");
+ break;
+ }
+
+ switch (input.getNullOrdering()) {
+ case FIRST:
+ builder.append(" NULLS FIRST");
+ break;
+ case LAST:
+ builder.append(" NULLS LAST");
+ break;
+ }
+
+ return builder.toString();
+ };
}
}
diff --git
a/iotdb-core/relational-parser/src/main/java/org/apache/iotdb/db/relational/sql/util/SqlFormatter.java
b/iotdb-core/relational-parser/src/main/java/org/apache/iotdb/db/relational/sql/util/SqlFormatter.java
new file mode 100644
index 00000000000..ed430724411
--- /dev/null
+++
b/iotdb-core/relational-parser/src/main/java/org/apache/iotdb/db/relational/sql/util/SqlFormatter.java
@@ -0,0 +1,783 @@
+/*
+ * 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.iotdb.db.relational.sql.util;
+
+import org.apache.iotdb.db.relational.sql.tree.AddColumn;
+import org.apache.iotdb.db.relational.sql.tree.AliasedRelation;
+import org.apache.iotdb.db.relational.sql.tree.AllColumns;
+import org.apache.iotdb.db.relational.sql.tree.AstVisitor;
+import org.apache.iotdb.db.relational.sql.tree.ColumnDefinition;
+import org.apache.iotdb.db.relational.sql.tree.CreateDB;
+import org.apache.iotdb.db.relational.sql.tree.CreateFunction;
+import org.apache.iotdb.db.relational.sql.tree.CreateTable;
+import org.apache.iotdb.db.relational.sql.tree.Delete;
+import org.apache.iotdb.db.relational.sql.tree.DropColumn;
+import org.apache.iotdb.db.relational.sql.tree.DropDB;
+import org.apache.iotdb.db.relational.sql.tree.DropFunction;
+import org.apache.iotdb.db.relational.sql.tree.DropTable;
+import org.apache.iotdb.db.relational.sql.tree.Except;
+import org.apache.iotdb.db.relational.sql.tree.Explain;
+import org.apache.iotdb.db.relational.sql.tree.ExplainAnalyze;
+import org.apache.iotdb.db.relational.sql.tree.Expression;
+import org.apache.iotdb.db.relational.sql.tree.Identifier;
+import org.apache.iotdb.db.relational.sql.tree.Insert;
+import org.apache.iotdb.db.relational.sql.tree.Intersect;
+import org.apache.iotdb.db.relational.sql.tree.Join;
+import org.apache.iotdb.db.relational.sql.tree.JoinCriteria;
+import org.apache.iotdb.db.relational.sql.tree.JoinOn;
+import org.apache.iotdb.db.relational.sql.tree.JoinUsing;
+import org.apache.iotdb.db.relational.sql.tree.Limit;
+import org.apache.iotdb.db.relational.sql.tree.NaturalJoin;
+import org.apache.iotdb.db.relational.sql.tree.Node;
+import org.apache.iotdb.db.relational.sql.tree.Offset;
+import org.apache.iotdb.db.relational.sql.tree.OrderBy;
+import org.apache.iotdb.db.relational.sql.tree.Property;
+import org.apache.iotdb.db.relational.sql.tree.QualifiedName;
+import org.apache.iotdb.db.relational.sql.tree.Query;
+import org.apache.iotdb.db.relational.sql.tree.QuerySpecification;
+import org.apache.iotdb.db.relational.sql.tree.Relation;
+import org.apache.iotdb.db.relational.sql.tree.RenameColumn;
+import org.apache.iotdb.db.relational.sql.tree.RenameTable;
+import org.apache.iotdb.db.relational.sql.tree.Row;
+import org.apache.iotdb.db.relational.sql.tree.Select;
+import org.apache.iotdb.db.relational.sql.tree.SelectItem;
+import org.apache.iotdb.db.relational.sql.tree.SetProperties;
+import org.apache.iotdb.db.relational.sql.tree.ShowDB;
+import org.apache.iotdb.db.relational.sql.tree.ShowFunctions;
+import org.apache.iotdb.db.relational.sql.tree.ShowTables;
+import org.apache.iotdb.db.relational.sql.tree.SingleColumn;
+import org.apache.iotdb.db.relational.sql.tree.Table;
+import org.apache.iotdb.db.relational.sql.tree.TableSubquery;
+import org.apache.iotdb.db.relational.sql.tree.Union;
+import org.apache.iotdb.db.relational.sql.tree.Update;
+import org.apache.iotdb.db.relational.sql.tree.UpdateAssignment;
+import org.apache.iotdb.db.relational.sql.tree.Values;
+import org.apache.iotdb.db.relational.sql.tree.WithQuery;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+import static
org.apache.iotdb.db.relational.sql.util.ExpressionFormatter.formatGroupBy;
+import static
org.apache.iotdb.db.relational.sql.util.ExpressionFormatter.formatOrderBy;
+
+public final class SqlFormatter {
+
+ private static final String INDENT = " ";
+
+ private SqlFormatter() {}
+
+ public static String formatSql(Node root) {
+ StringBuilder builder = new StringBuilder();
+ new Formatter(builder).process(root, 0);
+ return builder.toString();
+ }
+
+ private static String formatName(Identifier identifier) {
+ return ExpressionFormatter.formatExpression(identifier);
+ }
+
+ public static String formatName(QualifiedName name) {
+ return
name.getOriginalParts().stream().map(SqlFormatter::formatName).collect(joining("."));
+ }
+
+ private static String formatExpression(Expression expression) {
+ return ExpressionFormatter.formatExpression(expression);
+ }
+
+ private static class Formatter extends AstVisitor<Void, Integer> {
+ private static class SqlBuilder {
+ private final StringBuilder builder;
+
+ public SqlBuilder(StringBuilder builder) {
+ this.builder = requireNonNull(builder, "builder is null");
+ }
+
+ @CanIgnoreReturnValue
+ public SqlBuilder append(CharSequence value) {
+ builder.append(value);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public SqlBuilder append(char c) {
+ builder.append(c);
+ return this;
+ }
+ }
+
+ private final SqlBuilder builder;
+
+ public Formatter(StringBuilder builder) {
+ this.builder = new SqlBuilder(builder);
+ }
+
+ @Override
+ protected Void visitNode(Node node, Integer indent) {
+ throw new UnsupportedOperationException("not yet implemented: " + node);
+ }
+
+ @Override
+ protected Void visitExpression(Expression node, Integer indent) {
+ checkArgument(indent == 0, "visitExpression should only be called at
root");
+ builder.append(formatExpression(node));
+ return null;
+ }
+
+ @Override
+ protected Void visitQuery(Query node, Integer indent) {
+
+ node.getWith()
+ .ifPresent(
+ with -> {
+ append(indent, "WITH");
+ if (with.isRecursive()) {
+ builder.append(" RECURSIVE");
+ }
+ builder.append("\n ");
+ Iterator<WithQuery> queries = with.getQueries().iterator();
+ while (queries.hasNext()) {
+ WithQuery query = queries.next();
+ append(indent, formatName(query.getName()));
+ query
+ .getColumnNames()
+ .ifPresent(columnNames -> appendAliasColumns(builder,
columnNames));
+ builder.append(" AS ");
+ process(new TableSubquery(query.getQuery()), indent);
+ builder.append('\n');
+ if (queries.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ });
+
+ processRelation(node.getQueryBody(), indent);
+ node.getOrderBy().ifPresent(orderBy -> process(orderBy, indent));
+ node.getOffset().ifPresent(offset -> process(offset, indent));
+ node.getLimit().ifPresent(limit -> process(limit, indent));
+ return null;
+ }
+
+ @Override
+ protected Void visitQuerySpecification(QuerySpecification node, Integer
indent) {
+ process(node.getSelect(), indent);
+
+ node.getFrom()
+ .ifPresent(
+ from -> {
+ append(indent, "FROM");
+ builder.append('\n');
+ append(indent, " ");
+ process(from, indent);
+ });
+
+ builder.append('\n');
+
+ node.getWhere()
+ .ifPresent(where -> append(indent, "WHERE " +
formatExpression(where)).append('\n'));
+
+ node.getGroupBy()
+ .ifPresent(
+ groupBy ->
+ append(
+ indent,
+ "GROUP BY "
+ + (groupBy.isDistinct() ? " DISTINCT " : "")
+ + formatGroupBy(groupBy.getGroupingElements()))
+ .append('\n'));
+
+ node.getHaving()
+ .ifPresent(having -> append(indent, "HAVING " +
formatExpression(having)).append('\n'));
+
+ node.getOrderBy().ifPresent(orderBy -> process(orderBy, indent));
+ node.getOffset().ifPresent(offset -> process(offset, indent));
+ node.getLimit().ifPresent(limit -> process(limit, indent));
+ return null;
+ }
+
+ @Override
+ protected Void visitOrderBy(OrderBy node, Integer indent) {
+ append(indent, formatOrderBy(node)).append('\n');
+ return null;
+ }
+
+ @Override
+ protected Void visitOffset(Offset node, Integer indent) {
+ append(indent, "OFFSET
").append(formatExpression(node.getRowCount())).append(" ROWS\n");
+ return null;
+ }
+
+ @Override
+ protected Void visitLimit(Limit node, Integer indent) {
+ append(indent, "LIMIT
").append(formatExpression(node.getRowCount())).append('\n');
+ return null;
+ }
+
+ @Override
+ protected Void visitSelect(Select node, Integer indent) {
+ append(indent, "SELECT");
+ if (node.isDistinct()) {
+ builder.append(" DISTINCT");
+ }
+
+ if (node.getSelectItems().size() > 1) {
+ boolean first = true;
+ for (SelectItem item : node.getSelectItems()) {
+ builder.append("\n").append(indentString(indent)).append(first ? "
" : ", ");
+
+ process(item, indent);
+ first = false;
+ }
+ } else {
+ builder.append(' ');
+ process(getOnlyElement(node.getSelectItems()), indent);
+ }
+
+ builder.append('\n');
+
+ return null;
+ }
+
+ @Override
+ protected Void visitSingleColumn(SingleColumn node, Integer indent) {
+ builder.append(formatExpression(node.getExpression()));
+ node.getAlias().ifPresent(alias -> builder.append('
').append(formatName(alias)));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitAllColumns(AllColumns node, Integer indent) {
+ node.getTarget().ifPresent(value ->
builder.append(formatExpression(value)).append("."));
+ builder.append("*");
+
+ if (!node.getAliases().isEmpty()) {
+ builder
+ .append(" AS (")
+ .append(
+ Joiner.on(", ")
+ .join(
+ node.getAliases().stream()
+ .map(SqlFormatter::formatName)
+ .collect(toImmutableList())))
+ .append(")");
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Void visitTable(Table node, Integer indent) {
+ builder.append(formatName(node.getName()));
+ return null;
+ }
+
+ @Override
+ protected Void visitJoin(Join node, Integer indent) {
+ JoinCriteria criteria = node.getCriteria().orElse(null);
+ String type = node.getType().toString();
+ if (criteria instanceof NaturalJoin) {
+ type = "NATURAL " + type;
+ }
+
+ if (node.getType() != Join.Type.IMPLICIT) {
+ builder.append('(');
+ }
+ process(node.getLeft(), indent);
+
+ builder.append('\n');
+ if (node.getType() == Join.Type.IMPLICIT) {
+ append(indent, ", ");
+ } else {
+ append(indent, type).append(" JOIN ");
+ }
+
+ process(node.getRight(), indent);
+
+ if (node.getType() != Join.Type.CROSS && node.getType() !=
Join.Type.IMPLICIT) {
+ if (criteria instanceof JoinUsing) {
+ JoinUsing using = (JoinUsing) criteria;
+ builder.append(" USING (").append(Joiner.on(",
").join(using.getColumns())).append(")");
+ } else if (criteria instanceof JoinOn) {
+ JoinOn on = (JoinOn) criteria;
+ builder.append(" ON ").append(formatExpression(on.getExpression()));
+ } else if (!(criteria instanceof NaturalJoin)) {
+ throw new UnsupportedOperationException("unknown join criteria: " +
criteria);
+ }
+ }
+
+ if (node.getType() != Join.Type.IMPLICIT) {
+ builder.append(")");
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Void visitAliasedRelation(AliasedRelation node, Integer indent) {
+ builder.append("( ");
+ process(node, indent + 1);
+ append(indent, ")");
+
+ builder.append(' ').append(formatName(node.getAlias()));
+ appendAliasColumns(builder, node.getColumnNames());
+
+ return null;
+ }
+
+ @Override
+ protected Void visitValues(Values node, Integer indent) {
+ builder.append(" VALUES ");
+
+ boolean first = true;
+ for (Expression row : node.getRows()) {
+ builder.append("\n").append(indentString(indent)).append(first ? " "
: ", ");
+
+ builder.append(formatExpression(row));
+ first = false;
+ }
+ builder.append('\n');
+
+ return null;
+ }
+
+ @Override
+ protected Void visitTableSubquery(TableSubquery node, Integer indent) {
+ builder.append('(').append('\n');
+
+ process(node.getQuery(), indent + 1);
+
+ append(indent, ") ");
+
+ return null;
+ }
+
+ @Override
+ protected Void visitUnion(Union node, Integer indent) {
+ Iterator<Relation> relations = node.getRelations().iterator();
+
+ while (relations.hasNext()) {
+ processRelation(relations.next(), indent);
+
+ if (relations.hasNext()) {
+ builder.append("UNION ");
+ if (!node.isDistinct()) {
+ builder.append("ALL ");
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Void visitExcept(Except node, Integer indent) {
+ processRelation(node.getLeft(), indent);
+
+ builder.append("EXCEPT ");
+ if (!node.isDistinct()) {
+ builder.append("ALL ");
+ }
+
+ processRelation(node.getRight(), indent);
+
+ return null;
+ }
+
+ @Override
+ protected Void visitIntersect(Intersect node, Integer indent) {
+ Iterator<Relation> relations = node.getRelations().iterator();
+
+ while (relations.hasNext()) {
+ processRelation(relations.next(), indent);
+
+ if (relations.hasNext()) {
+ builder.append("INTERSECT ");
+ if (!node.isDistinct()) {
+ builder.append("ALL ");
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Void visitExplain(Explain node, Integer indent) {
+ builder.append("EXPLAIN ");
+
+ builder.append("\n");
+
+ process(node.getStatement(), indent);
+
+ return null;
+ }
+
+ @Override
+ protected Void visitExplainAnalyze(ExplainAnalyze node, Integer indent) {
+ builder.append("EXPLAIN ANALYZE");
+ if (node.isVerbose()) {
+ builder.append(" VERBOSE");
+ }
+ builder.append("\n");
+
+ process(node.getStatement(), indent);
+
+ return null;
+ }
+
+ @Override
+ protected Void visitShowDB(ShowDB node, Integer indent) {
+ builder.append("SHOW DATABASE");
+
+ return null;
+ }
+
+ @Override
+ protected Void visitShowTables(ShowTables node, Integer indent) {
+ builder.append("SHOW TABLES");
+
+ node.getDbName().ifPresent(db -> builder.append(" FROM
").append(formatName(db)));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitShowFunctions(ShowFunctions node, Integer indent) {
+ builder.append("SHOW FUNCTIONS");
+
+ return null;
+ }
+
+ @Override
+ protected Void visitDelete(Delete node, Integer indent) {
+ builder.append("DELETE FROM
").append(formatName(node.getTable().getName()));
+
+ node.getWhere().ifPresent(where -> builder.append(" WHERE
").append(formatExpression(where)));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitCreateDB(CreateDB node, Integer indent) {
+ builder.append("CREATE DATABASE ");
+ if (node.isSetIfNotExists()) {
+ builder.append("IF NOT EXISTS ");
+ }
+ builder.append(node.getDbName()).append(" ");
+
+ builder.append(formatPropertiesMultiLine(node.getProperties()));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitDropDB(DropDB node, Integer indent) {
+ builder.append("DROP DATABASE ");
+ if (node.isExists()) {
+ builder.append("IF EXISTS ");
+ }
+ builder.append(formatName(node.getDbName()));
+ return null;
+ }
+
+ @Override
+ protected Void visitCreateTable(CreateTable node, Integer indent) {
+ builder.append("CREATE TABLE ");
+ if (node.isIfNotExists()) {
+ builder.append("IF NOT EXISTS ");
+ }
+ String tableName = formatName(node.getName());
+ builder.append(tableName).append(" (\n");
+
+ String elementIndent = indentString(indent + 1);
+ String columnList =
+ node.getElements().stream()
+ .map(
+ element -> {
+ if (element != null) {
+ return elementIndent + formatColumnDefinition(element);
+ }
+
+ throw new UnsupportedOperationException("unknown table
element: " + element);
+ })
+ .collect(joining(",\n"));
+ builder.append(columnList);
+ builder.append("\n").append(")");
+
+ builder.append(formatPropertiesMultiLine(node.getProperties()));
+
+ return null;
+ }
+
+ private String formatPropertiesMultiLine(List<Property> properties) {
+ if (properties.isEmpty()) {
+ return "";
+ }
+
+ String propertyList =
+ properties.stream()
+ .map(
+ element ->
+ INDENT
+ + formatName(element.getName())
+ + " = "
+ + (element.isSetToDefault()
+ ? "DEFAULT"
+ :
formatExpression(element.getNonDefaultValue())))
+ .collect(joining(",\n"));
+
+ return "\nWITH (\n" + propertyList + "\n)";
+ }
+
+ private String formatPropertiesSingleLine(List<Property> properties) {
+ if (properties.isEmpty()) {
+ return "";
+ }
+
+ return " WITH ( " + joinProperties(properties) + " )";
+ }
+
+ private String formatColumnDefinition(ColumnDefinition column) {
+ StringBuilder stringBuilder =
+ new StringBuilder()
+ .append(formatName(column.getName()))
+ .append(" ")
+ .append(column.getType())
+ .append(" ")
+ .append(column.getColumnCategory());
+
+ column
+ .getCharsetName()
+ .ifPresent(charset -> stringBuilder.append(" CHARSET
").append(charset));
+ return stringBuilder.toString();
+ }
+
+ @Override
+ protected Void visitDropTable(DropTable node, Integer indent) {
+ builder.append("DROP TABLE ");
+ if (node.isExists()) {
+ builder.append("IF EXISTS ");
+ }
+ builder.append(formatName(node.getTableName()));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitRenameTable(RenameTable node, Integer indent) {
+ builder.append("ALTER TABLE ");
+ builder
+ .append(formatName(node.getSource()))
+ .append(" RENAME TO ")
+ .append(formatName(node.getTarget()));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitSetProperties(SetProperties node, Integer context) {
+ SetProperties.Type type = node.getType();
+ builder.append("ALTER ");
+ switch (type) {
+ case TABLE:
+ builder.append("TABLE ");
+ case MATERIALIZED_VIEW:
+ builder.append("MATERIALIZED VIEW ");
+ }
+
+ builder
+ .append(formatName(node.getName()))
+ .append(" SET PROPERTIES ")
+ .append(joinProperties(node.getProperties()));
+
+ return null;
+ }
+
+ private String joinProperties(List<Property> properties) {
+ return properties.stream()
+ .map(
+ element ->
+ formatName(element.getName())
+ + " = "
+ + (element.isSetToDefault()
+ ? "DEFAULT"
+ : formatExpression(element.getNonDefaultValue())))
+ .collect(joining(", "));
+ }
+
+ @Override
+ protected Void visitRenameColumn(RenameColumn node, Integer indent) {
+ builder.append("ALTER TABLE ");
+ builder.append(formatName(node.getTable())).append(" RENAME COLUMN ");
+ builder
+ .append(formatName(node.getSource()))
+ .append(" TO ")
+ .append(formatName(node.getTarget()));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitDropColumn(DropColumn node, Integer indent) {
+ builder.append("ALTER TABLE ");
+ builder.append(formatName(node.getTable())).append(" DROP COLUMN ");
+ builder.append(formatName(node.getField()));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitAddColumn(AddColumn node, Integer indent) {
+ builder.append("ALTER TABLE ");
+
+ builder.append(formatName(node.getTableName())).append(" ADD COLUMN ");
+ builder.append(formatColumnDefinition(node.getColumn()));
+
+ return null;
+ }
+
+ @Override
+ protected Void visitInsert(Insert node, Integer indent) {
+ builder.append("INSERT INTO ").append(formatName(node.getTarget()));
+
+ node.getColumns()
+ .ifPresent(
+ columns -> builder.append(" (").append(Joiner.on(",
").join(columns)).append(")"));
+
+ builder.append("\n");
+
+ process(node.getQuery(), indent);
+
+ return null;
+ }
+
+ @Override
+ protected Void visitUpdate(Update node, Integer indent) {
+ builder.append("UPDATE
").append(formatName(node.getTable().getName())).append(" SET");
+ int setCounter = node.getAssignments().size() - 1;
+ for (UpdateAssignment assignment : node.getAssignments()) {
+ builder
+ .append("\n")
+ .append(indentString(indent + 1))
+ .append(assignment.getName().getValue())
+ .append(" = ")
+ .append(formatExpression(assignment.getValue()));
+ if (setCounter > 0) {
+ builder.append(",");
+ }
+ setCounter--;
+ }
+ node.getWhere()
+ .ifPresent(
+ where ->
+ builder
+ .append("\n")
+ .append(indentString(indent))
+ .append("WHERE ")
+ .append(formatExpression(where)));
+ return null;
+ }
+
+ @Override
+ protected Void visitRow(Row node, Integer indent) {
+ builder.append("ROW(");
+ boolean firstItem = true;
+ for (Expression item : node.getItems()) {
+ if (!firstItem) {
+ builder.append(", ");
+ }
+ process(item, indent);
+ firstItem = false;
+ }
+ builder.append(")");
+ return null;
+ }
+
+ @Override
+ protected Void visitCreateFunction(CreateFunction node, Integer indent) {
+ builder
+ .append("CREATE FUNCTION ")
+ .append(node.getUdfName())
+ .append(" AS ")
+ .append(node.getClassName());
+ node.getUriString().ifPresent(uri -> builder.append(" USING URI
").append(uri));
+ return null;
+ }
+
+ @Override
+ protected Void visitDropFunction(DropFunction node, Integer indent) {
+ builder.append("DROP FUNCTION ");
+ builder.append(node.getUdfName());
+ return null;
+ }
+
+ private void appendBeginLabel(Optional<Identifier> label) {
+ label.ifPresent(value -> builder.append(formatName(value)).append(": "));
+ }
+
+ private void processRelation(Relation relation, Integer indent) {
+ // TODO: handle this properly
+ if (relation instanceof Table) {
+ builder.append("TABLE ").append(formatName(((Table)
relation).getName())).append('\n');
+ } else {
+ process(relation, indent);
+ }
+ }
+
+ private SqlBuilder append(int indent, String value) {
+ return builder.append(indentString(indent)).append(value);
+ }
+
+ private static String indentString(int indent) {
+ return Strings.repeat(INDENT, indent);
+ }
+
+ private void formatDefinitionList(List<String> elements, int indent) {
+ if (elements.size() == 1) {
+ builder.append(" ").append(getOnlyElement(elements)).append("\n");
+ } else {
+ builder.append("\n");
+ for (int i = 0; i < elements.size() - 1; i++) {
+ append(indent, elements.get(i)).append(",\n");
+ }
+ append(indent, elements.get(elements.size() - 1)).append("\n");
+ }
+ }
+
+ private void appendAliasColumns(Formatter.SqlBuilder builder,
List<Identifier> columns) {
+ if ((columns != null) && !columns.isEmpty()) {
+ String formattedColumns =
+ columns.stream().map(SqlFormatter::formatName).collect(joining(",
"));
+
+ builder.append(" (").append(formattedColumns).append(')');
+ }
+ }
+ }
+}