/*
* 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 io.crate.sql.tree.AliasedRelation;
import io.crate.sql.tree.AllColumns;
import io.crate.sql.tree.AstVisitor;
import io.crate.sql.tree.Expression;
import io.crate.sql.tree.Join;
import io.crate.sql.tree.JoinCriteria;
import io.crate.sql.tree.JoinOn;
import io.crate.sql.tree.JoinUsing;
import io.crate.sql.tree.NaturalJoin;
import io.crate.sql.tree.Node;
import io.crate.sql.tree.Query;
import io.crate.sql.tree.QuerySpecification;
import io.crate.sql.tree.Relation;
import io.crate.sql.tree.SampledRelation;
import io.crate.sql.tree.Select;
import io.crate.sql.tree.SelectItem;
import io.crate.sql.tree.SingleColumn;
import io.crate.sql.tree.SortItem;
import io.crate.sql.tree.Table;
import io.crate.sql.tree.TableSubquery;
import io.crate.sql.tree.With;
import io.crate.sql.tree.WithQuery;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import java.util.Iterator;
import java.util.List;
import static io.crate.sql.ExpressionFormatter.expressionFormatterFunction;
import static io.crate.sql.ExpressionFormatter.formatExpression;
import static com.google.common.base.Preconditions.checkArgument;
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 class Formatter
extends AstVisitor<Void, Integer>
{
private final StringBuilder builder;
public Formatter(StringBuilder builder)
{
this.builder = 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)
{
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());
appendAliasColumns(builder, query.getColumnNames());
builder.append(" AS ");
process(new TableSubquery(query.getQuery()), indent);
builder.append('\n');
if (queries.hasNext()) {
builder.append(", ");
}
}
}
process(node.getQueryBody(), indent);
if (!node.getOrderBy().isEmpty()) {
append(indent, "ORDER BY " + Joiner.on(", ").join(Iterables.transform(node.getOrderBy(), orderByFormatterFunction())))
.append('\n');
}
if (node.getLimit().isPresent()) {
append(indent, "LIMIT " + node.getLimit().get())
.append('\n');
}
if (node.getOffset().isPresent()) {
append(indent, "OFFSET " + node.getOffset().get())
.append('\n');
}
return null;
}
@Override
protected Void visitQuerySpecification(QuerySpecification node, Integer indent)
{
process(node.getSelect(), indent);
if (node.getFrom() != null) {
append(indent, "FROM");
if (node.getFrom().size() > 1) {
builder.append('\n');
append(indent, " ");
Iterator<Relation> relations = node.getFrom().iterator();
while (relations.hasNext()) {
process(relations.next(), indent);
if (relations.hasNext()) {
builder.append('\n');
append(indent, ", ");
}
}
}
else {
builder.append(' ');
process(Iterables.getOnlyElement(node.getFrom()), indent);
}
}
builder.append('\n');
if (node.getWhere().isPresent()) {
append(indent, "WHERE " + formatExpression(node.getWhere().get()))
.append('\n');
}
if (!node.getGroupBy().isEmpty()) {
append(indent, "GROUP BY " + Joiner.on(", ").join(Iterables.transform(node.getGroupBy(), expressionFormatterFunction())))
.append('\n');
}
if (node.getHaving().isPresent()) {
append(indent, "HAVING " + formatExpression(node.getHaving().get()))
.append('\n');
}
if (!node.getOrderBy().isEmpty()) {
append(indent, "ORDER BY " + Joiner.on(", ").join(Iterables.transform(node.getOrderBy(), orderByFormatterFunction())))
.append('\n');
}
if (node.getLimit().isPresent()) {
append(indent, "LIMIT " + node.getLimit().get())
.append('\n');
}
if (node.getOffset().isPresent()) {
append(indent, "OFFSET " + node.getOffset().get())
.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(Iterables.getOnlyElement(node.getSelectItems()), indent);
}
builder.append('\n');
return null;
}
@Override
protected Void visitSingleColumn(SingleColumn node, Integer indent)
{
builder.append(formatExpression(node.getExpression()));
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(node.getName().toString());
return null;
}
@Override
protected Void visitJoin(Join node, Integer indent)
{
JoinCriteria criteria = node.getCriteria().orNull();
String type = node.getType().toString();
if (criteria instanceof NaturalJoin) {
type = "NATURAL " + type;
}
builder.append('(');
process(node.getLeft(), indent);
builder.append('\n');
append(indent, type).append(" JOIN ");
process(node.getRight(), indent);
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()))
.append(")");
}
else if (node.getType() != Join.Type.CROSS && !(criteria instanceof NaturalJoin)) {
throw new UnsupportedOperationException("unknown join criteria: " + criteria);
}
builder.append(")");
return null;
}
@Override
protected Void visitAliasedRelation(AliasedRelation node, Integer indent)
{
process(node.getRelation(), indent);
builder.append(' ')
.append(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(')');
if (node.getColumnsToStratifyOn().isPresent()) {
builder.append(" STRATIFY ON ")
.append(" (")
.append(Joiner.on(",").join(node.getColumnsToStratifyOn().get()));
builder.append(')');
}
return null;
}
@Override
protected Void visitTableSubquery(TableSubquery node, Integer indent)
{
builder.append('(')
.append('\n');
process(node.getQuery(), indent + 1);
append(indent, ")");
return null;
}
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);
}
}
static Function<SortItem, String> orderByFormatterFunction()
{
return new Function<SortItem, String>()
{
@Override
public String apply(SortItem 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;
default:
throw new UnsupportedOperationException("unknown ordering: " + input.getOrdering());
}
switch (input.getNullOrdering()) {
case FIRST:
builder.append(" NULLS FIRST");
break;
case LAST:
builder.append(" NULLS LAST");
break;
case UNDEFINED:
// no op
break;
default:
throw new UnsupportedOperationException("unknown null ordering: " + input.getNullOrdering());
}
return builder.toString();
}
};
}
private static void appendAliasColumns(StringBuilder builder, List<String> columns)
{
if ((columns != null) && (!columns.isEmpty())) {
builder.append(" (");
Joiner.on(", ").appendTo(builder, columns);
builder.append(')');
}
}
}