/* * 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; import com.facebook.presto.sql.tree.AddColumn; import com.facebook.presto.sql.tree.AliasedRelation; import com.facebook.presto.sql.tree.AllColumns; import com.facebook.presto.sql.tree.AstVisitor; import com.facebook.presto.sql.tree.Call; import com.facebook.presto.sql.tree.CallArgument; import com.facebook.presto.sql.tree.ColumnDefinition; import com.facebook.presto.sql.tree.Commit; import com.facebook.presto.sql.tree.CreateSchema; import com.facebook.presto.sql.tree.CreateTable; import com.facebook.presto.sql.tree.CreateTableAsSelect; import com.facebook.presto.sql.tree.CreateView; import com.facebook.presto.sql.tree.Deallocate; import com.facebook.presto.sql.tree.Delete; import com.facebook.presto.sql.tree.DescribeInput; import com.facebook.presto.sql.tree.DescribeOutput; import com.facebook.presto.sql.tree.DropSchema; import com.facebook.presto.sql.tree.DropTable; import com.facebook.presto.sql.tree.DropView; import com.facebook.presto.sql.tree.Except; import com.facebook.presto.sql.tree.Execute; import com.facebook.presto.sql.tree.Explain; import com.facebook.presto.sql.tree.ExplainFormat; import com.facebook.presto.sql.tree.ExplainOption; import com.facebook.presto.sql.tree.ExplainType; import com.facebook.presto.sql.tree.Expression; import com.facebook.presto.sql.tree.Grant; import com.facebook.presto.sql.tree.Insert; import com.facebook.presto.sql.tree.Intersect; import com.facebook.presto.sql.tree.Isolation; import com.facebook.presto.sql.tree.Join; import com.facebook.presto.sql.tree.JoinCriteria; import com.facebook.presto.sql.tree.JoinOn; import com.facebook.presto.sql.tree.JoinUsing; import com.facebook.presto.sql.tree.LikeClause; import com.facebook.presto.sql.tree.NaturalJoin; import com.facebook.presto.sql.tree.Node; import com.facebook.presto.sql.tree.OrderBy; import com.facebook.presto.sql.tree.Prepare; import com.facebook.presto.sql.tree.QualifiedName; import com.facebook.presto.sql.tree.Query; import com.facebook.presto.sql.tree.QuerySpecification; import com.facebook.presto.sql.tree.Relation; import com.facebook.presto.sql.tree.RenameColumn; import com.facebook.presto.sql.tree.RenameSchema; import com.facebook.presto.sql.tree.RenameTable; import com.facebook.presto.sql.tree.ResetSession; import com.facebook.presto.sql.tree.Revoke; import com.facebook.presto.sql.tree.Rollback; import com.facebook.presto.sql.tree.Row; import com.facebook.presto.sql.tree.SampledRelation; import com.facebook.presto.sql.tree.Select; import com.facebook.presto.sql.tree.SelectItem; import com.facebook.presto.sql.tree.SetSession; import com.facebook.presto.sql.tree.ShowCatalogs; import com.facebook.presto.sql.tree.ShowColumns; import com.facebook.presto.sql.tree.ShowCreate; import com.facebook.presto.sql.tree.ShowFunctions; import com.facebook.presto.sql.tree.ShowGrants; import com.facebook.presto.sql.tree.ShowPartitions; import com.facebook.presto.sql.tree.ShowSchemas; import com.facebook.presto.sql.tree.ShowSession; import com.facebook.presto.sql.tree.ShowTables; import com.facebook.presto.sql.tree.SingleColumn; import com.facebook.presto.sql.tree.StartTransaction; import com.facebook.presto.sql.tree.Table; import com.facebook.presto.sql.tree.TableSubquery; import com.facebook.presto.sql.tree.TransactionAccessMode; import com.facebook.presto.sql.tree.TransactionMode; import com.facebook.presto.sql.tree.Union; import com.facebook.presto.sql.tree.Unnest; import com.facebook.presto.sql.tree.Values; import com.facebook.presto.sql.tree.With; import com.facebook.presto.sql.tree.WithQuery; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSortedMap; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; import static com.facebook.presto.sql.ExpressionFormatter.formatExpression; import static com.facebook.presto.sql.ExpressionFormatter.formatGroupBy; import static com.facebook.presto.sql.ExpressionFormatter.formatOrderBy; import static com.facebook.presto.sql.ExpressionFormatter.formatSortItems; import static com.facebook.presto.sql.ExpressionFormatter.formatStringLiteral; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.getOnlyElement; import static java.util.stream.Collectors.joining; public final class SqlFormatter { private static final String INDENT = " "; private static final Pattern NAME_PATTERN = Pattern.compile("[a-z_][a-z0-9_]*"); private SqlFormatter() {} public static String formatSql(Node root, Optional<List<Expression>> parameters) { StringBuilder builder = new StringBuilder(); new Formatter(builder, parameters).process(root, 0); return builder.toString(); } private static class Formatter extends AstVisitor<Void, Integer> { private final StringBuilder builder; private final Optional<List<Expression>> parameters; public Formatter(StringBuilder builder, Optional<List<Expression>> parameters) { this.builder = builder; this.parameters = parameters; } @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, parameters)); return null; } @Override protected Void visitUnnest(Unnest node, Integer indent) { builder.append(node.toString()); return null; } @Override protected Void visitPrepare(Prepare node, Integer indent) { append(indent, "PREPARE "); builder.append(node.getName()); builder.append(" FROM"); builder.append("\n"); process(node.getStatement(), indent + 1); return null; } @Override protected Void visitDeallocate(Deallocate node, Integer indent) { append(indent, "DEALLOCATE PREPARE "); builder.append(node.getName()); return null; } @Override protected Void visitExecute(Execute node, Integer indent) { append(indent, "EXECUTE "); builder.append(node.getName()); List<Expression> parameters = node.getParameters(); if (!parameters.isEmpty()) { builder.append(" USING "); Joiner.on(", ").appendTo(builder, parameters); } return null; } @Override protected Void visitDescribeOutput(DescribeOutput node, Integer indent) { append(indent, "DESCRIBE OUTPUT "); builder.append(node.getName()); return null; } @Override protected Void visitDescribeInput(DescribeInput node, Integer indent) { append(indent, "DESCRIBE INPUT "); builder.append(node.getName()); return null; } @Override protected Void visitQuery(Query node, Integer indent) { if (node.getWith().isPresent()) { With with = node.getWith().get(); 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, 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); if (node.getOrderBy().isPresent()) { process(node.getOrderBy().get(), indent); } if (node.getLimit().isPresent()) { append(indent, "LIMIT " + node.getLimit().get()) .append('\n'); } return null; } @Override protected Void visitQuerySpecification(QuerySpecification node, Integer indent) { process(node.getSelect(), indent); if (node.getFrom().isPresent()) { append(indent, "FROM"); builder.append('\n'); append(indent, " "); process(node.getFrom().get(), indent); } builder.append('\n'); if (node.getWhere().isPresent()) { append(indent, "WHERE " + formatExpression(node.getWhere().get(), parameters)) .append('\n'); } if (node.getGroupBy().isPresent()) { append(indent, "GROUP BY " + (node.getGroupBy().get().isDistinct() ? " DISTINCT " : "") + formatGroupBy(node.getGroupBy().get().getGroupingElements())).append('\n'); } if (node.getHaving().isPresent()) { append(indent, "HAVING " + formatExpression(node.getHaving().get(), parameters)) .append('\n'); } if (node.getOrderBy().isPresent()) { process(node.getOrderBy().get(), indent); } if (node.getLimit().isPresent()) { append(indent, "LIMIT " + node.getLimit().get()) .append('\n'); } return null; } @Override protected Void visitOrderBy(OrderBy node, Integer indent) { append(indent, formatOrderBy(node, parameters)) .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(), parameters)); if (node.getAlias().isPresent()) { builder.append(' ') .append('"') .append(node.getAlias().get()) .append('"'); // TODO: handle quoting properly } return null; } @Override protected Void visitAllColumns(AllColumns node, Integer context) { builder.append(node.toString()); 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(), parameters)); } 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) { process(node.getRelation(), indent); builder.append(' ') .append(formatName(node.getAlias())); appendAliasColumns(builder, node.getColumnNames()); return null; } @Override protected Void visitSampledRelation(SampledRelation node, Integer indent) { process(node.getRelation(), indent); builder.append(" TABLESAMPLE ") .append(node.getType()) .append(" (") .append(node.getSamplePercentage()) .append(')'); 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, parameters)); 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 visitCreateView(CreateView node, Integer indent) { builder.append("CREATE "); if (node.isReplace()) { builder.append("OR REPLACE "); } builder.append("VIEW ") .append(formatName(node.getName())) .append(" AS\n"); process(node.getQuery(), indent); return null; } @Override protected Void visitDropView(DropView node, Integer context) { builder.append("DROP VIEW "); if (node.isExists()) { builder.append("IF EXISTS "); } builder.append(node.getName()); return null; } @Override protected Void visitExplain(Explain node, Integer indent) { builder.append("EXPLAIN "); if (node.isAnalyze()) { builder.append("ANALYZE "); } List<String> options = new ArrayList<>(); for (ExplainOption option : node.getOptions()) { if (option instanceof ExplainType) { options.add("TYPE " + ((ExplainType) option).getType()); } else if (option instanceof ExplainFormat) { options.add("FORMAT " + ((ExplainFormat) option).getType()); } else { throw new UnsupportedOperationException("unhandled explain option: " + option); } } if (!options.isEmpty()) { builder.append("("); Joiner.on(", ").appendTo(builder, options); builder.append(")"); } builder.append("\n"); process(node.getStatement(), indent); return null; } @Override protected Void visitShowCatalogs(ShowCatalogs node, Integer context) { builder.append("SHOW CATALOGS"); node.getLikePattern().ifPresent((value) -> builder.append(" LIKE ") .append(formatStringLiteral(value))); return null; } @Override protected Void visitShowSchemas(ShowSchemas node, Integer context) { builder.append("SHOW SCHEMAS"); if (node.getCatalog().isPresent()) { builder.append(" FROM ") .append(node.getCatalog().get()); } node.getLikePattern().ifPresent((value) -> builder.append(" LIKE ") .append(formatStringLiteral(value))); return null; } @Override protected Void visitShowTables(ShowTables node, Integer context) { builder.append("SHOW TABLES"); node.getSchema().ifPresent(value -> builder.append(" FROM ") .append(formatName(value))); node.getLikePattern().ifPresent(value -> builder.append(" LIKE ") .append(formatStringLiteral(value))); return null; } @Override protected Void visitShowCreate(ShowCreate node, Integer context) { if (node.getType() == ShowCreate.Type.TABLE) { builder.append("SHOW CREATE TABLE ") .append(formatName(node.getName())); } else if (node.getType() == ShowCreate.Type.VIEW) { builder.append("SHOW CREATE VIEW ") .append(formatName(node.getName())); } return null; } @Override protected Void visitShowColumns(ShowColumns node, Integer context) { builder.append("SHOW COLUMNS FROM ") .append(formatName(node.getTable())); return null; } @Override protected Void visitShowPartitions(ShowPartitions node, Integer context) { builder.append("SHOW PARTITIONS FROM ") .append(formatName(node.getTable())); if (node.getWhere().isPresent()) { builder.append(" WHERE ") .append(formatExpression(node.getWhere().get(), parameters)); } if (!node.getOrderBy().isEmpty()) { builder.append(" ORDER BY ") .append(formatSortItems(node.getOrderBy(), parameters)); } if (node.getLimit().isPresent()) { builder.append(" LIMIT ") .append(node.getLimit().get()); } return null; } @Override protected Void visitShowFunctions(ShowFunctions node, Integer context) { builder.append("SHOW FUNCTIONS"); return null; } @Override protected Void visitShowSession(ShowSession node, Integer context) { builder.append("SHOW SESSION"); return null; } @Override protected Void visitDelete(Delete node, Integer context) { builder.append("DELETE FROM ") .append(formatName(node.getTable().getName())); if (node.getWhere().isPresent()) { builder.append(" WHERE ") .append(formatExpression(node.getWhere().get(), parameters)); } return null; } @Override protected Void visitCreateSchema(CreateSchema node, Integer context) { builder.append("CREATE SCHEMA "); if (node.isNotExists()) { builder.append("IF NOT EXISTS "); } builder.append(formatName(node.getSchemaName())); appendTableProperties(builder, node.getProperties()); return null; } @Override protected Void visitDropSchema(DropSchema node, Integer context) { builder.append("DROP SCHEMA "); if (node.isExists()) { builder.append("IF EXISTS "); } builder.append(formatName(node.getSchemaName())) .append(" ") .append(node.isCascade() ? "CASCADE" : "RESTRICT"); return null; } @Override protected Void visitRenameSchema(RenameSchema node, Integer context) { builder.append("ALTER SCHEMA ") .append(formatName(node.getSource())) .append(" RENAME TO ") .append(formatName(node.getTarget())); return null; } @Override protected Void visitCreateTableAsSelect(CreateTableAsSelect node, Integer indent) { builder.append("CREATE TABLE "); if (node.isNotExists()) { builder.append("IF NOT EXISTS "); } builder.append(formatName(node.getName())); if (node.getComment().isPresent()) { builder.append("\nCOMMENT " + formatStringLiteral(node.getComment().get())); } appendTableProperties(builder, node.getProperties()); builder.append(" AS "); process(node.getQuery(), indent); if (!node.isWithData()) { builder.append(" WITH NO DATA"); } return null; } @Override protected Void visitCreateTable(CreateTable node, Integer indent) { builder.append("CREATE TABLE "); if (node.isNotExists()) { 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 instanceof ColumnDefinition) { ColumnDefinition column = (ColumnDefinition) element; return elementIndent + formatName(column.getName()) + " " + column.getType() + column.getComment() .map(comment -> " COMMENT " + formatStringLiteral(comment)) .orElse(""); } if (element instanceof LikeClause) { LikeClause likeClause = (LikeClause) element; StringBuilder builder = new StringBuilder(elementIndent); builder.append("LIKE ") .append(formatName(likeClause.getTableName())); if (likeClause.getPropertiesOption().isPresent()) { builder.append(" ") .append(likeClause.getPropertiesOption().get().name()) .append(" PROPERTIES"); } return builder.toString(); } throw new UnsupportedOperationException("unknown table element: " + element); }) .collect(joining(",\n")); builder.append(columnList); builder.append("\n").append(")"); if (node.getComment().isPresent()) { builder.append("\nCOMMENT " + formatStringLiteral(node.getComment().get())); } appendTableProperties(builder, node.getProperties()); return null; } private void appendTableProperties(StringBuilder builder, Map<String, Expression> properties) { if (!properties.isEmpty()) { builder.append("\nWITH (\n"); // Always output the table properties in sorted order String propertyList = ImmutableSortedMap.copyOf(properties).entrySet().stream() .map(entry -> INDENT + formatName(entry.getKey()) + " = " + formatExpression(entry.getValue(), parameters)) .collect(joining(",\n")); builder.append(propertyList); builder.append("\n").append(")"); } } private static String formatName(String name) { if (NAME_PATTERN.matcher(name).matches()) { return name; } return "\"" + name + "\""; } private static String formatName(QualifiedName name) { return name.getOriginalParts().stream() .map(Formatter::formatName) .collect(joining(".")); } @Override protected Void visitDropTable(DropTable node, Integer context) { builder.append("DROP TABLE "); if (node.isExists()) { builder.append("IF EXISTS "); } builder.append(node.getTableName()); return null; } @Override protected Void visitRenameTable(RenameTable node, Integer context) { builder.append("ALTER TABLE ") .append(node.getSource()) .append(" RENAME TO ") .append(node.getTarget()); return null; } @Override protected Void visitRenameColumn(RenameColumn node, Integer context) { builder.append("ALTER TABLE ") .append(node.getTable()) .append(" RENAME COLUMN ") .append(node.getSource()) .append(" TO ") .append(node.getTarget()); return null; } @Override protected Void visitAddColumn(AddColumn node, Integer indent) { builder.append("ALTER TABLE ") .append(node.getName()) .append(" ADD COLUMN ") .append(node.getColumn().getName()) .append(" ") .append(node.getColumn().getType()); return null; } @Override protected Void visitInsert(Insert node, Integer indent) { builder.append("INSERT INTO ") .append(node.getTarget()) .append(" "); if (node.getColumns().isPresent()) { builder.append("(") .append(Joiner.on(", ").join(node.getColumns().get())) .append(") "); } process(node.getQuery(), indent); return null; } @Override public Void visitSetSession(SetSession node, Integer context) { builder.append("SET SESSION ") .append(node.getName()) .append(" = ") .append(formatExpression(node.getValue(), parameters)); return null; } @Override public Void visitResetSession(ResetSession node, Integer context) { builder.append("RESET SESSION ") .append(node.getName()); return null; } @Override protected Void visitCallArgument(CallArgument node, Integer indent) { if (node.getName().isPresent()) { builder.append(node.getName().get()) .append(" => "); } builder.append(formatExpression(node.getValue(), parameters)); return null; } @Override protected Void visitCall(Call node, Integer indent) { builder.append("CALL ") .append(node.getName()) .append("("); Iterator<CallArgument> arguments = node.getArguments().iterator(); while (arguments.hasNext()) { process(arguments.next(), indent); if (arguments.hasNext()) { builder.append(", "); } } builder.append(")"); 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 visitStartTransaction(StartTransaction node, Integer indent) { builder.append("START TRANSACTION"); Iterator<TransactionMode> iterator = node.getTransactionModes().iterator(); while (iterator.hasNext()) { builder.append(" "); process(iterator.next(), indent); if (iterator.hasNext()) { builder.append(","); } } return null; } @Override protected Void visitIsolationLevel(Isolation node, Integer indent) { builder.append("ISOLATION LEVEL ").append(node.getLevel().getText()); return null; } @Override protected Void visitTransactionAccessMode(TransactionAccessMode node, Integer context) { builder.append(node.isReadOnly() ? "READ ONLY" : "READ WRITE"); return null; } @Override protected Void visitCommit(Commit node, Integer context) { builder.append("COMMIT"); return null; } @Override protected Void visitRollback(Rollback node, Integer context) { builder.append("ROLLBACK"); return null; } @Override public Void visitGrant(Grant node, Integer indent) { builder.append("GRANT "); if (node.getPrivileges().isPresent()) { builder.append(node.getPrivileges().get().stream() .collect(joining(", "))); } else { builder.append("ALL PRIVILEGES"); } builder.append(" ON "); if (node.isTable()) { builder.append("TABLE "); } builder.append(node.getTableName()) .append(" TO ") .append(node.getGrantee()); if (node.isWithGrantOption()) { builder.append(" WITH GRANT OPTION"); } return null; } @Override public Void visitRevoke(Revoke node, Integer indent) { builder.append("REVOKE "); if (node.isGrantOptionFor()) { builder.append("GRANT OPTION FOR "); } if (node.getPrivileges().isPresent()) { builder.append(node.getPrivileges().get().stream() .collect(joining(", "))); } else { builder.append("ALL PRIVILEGES"); } builder.append(" ON "); if (node.isTable()) { builder.append("TABLE "); } builder.append(node.getTableName()) .append(" FROM ") .append(node.getGrantee()); return null; } @Override public Void visitShowGrants(ShowGrants node, Integer indent) { builder.append("SHOW GRANTS "); if (node.getTableName().isPresent()) { builder.append("ON "); if (node.getTable()) { builder.append("TABLE "); } builder.append(node.getTableName().get()); } return null; } private void processRelation(Relation relation, Integer indent) { // TODO: handle this properly if (relation instanceof Table) { builder.append("TABLE ") .append(((Table) relation).getName()) .append('\n'); } else { process(relation, indent); } } private StringBuilder append(int indent, String value) { return builder.append(indentString(indent)) .append(value); } private static String indentString(int indent) { return Strings.repeat(INDENT, indent); } } private static void appendAliasColumns(StringBuilder builder, List<String> columns) { if ((columns != null) && (!columns.isEmpty())) { builder.append(" ("); Joiner.on(", ").appendTo(builder, columns); builder.append(')'); } } }