SqlPrettyWriter.java

/*
 * 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.calcite.sql.pretty;

import org.apache.calcite.avatica.util.Spaces;
import org.apache.calcite.sql.SqlBinaryOperator;
import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlWriter;
import org.apache.calcite.sql.SqlWriterConfig;
import org.apache.calcite.sql.dialect.AnsiSqlDialect;
import org.apache.calcite.sql.dialect.CalciteSqlDialect;
import org.apache.calcite.sql.util.SqlString;
import org.apache.calcite.util.Util;
import org.apache.calcite.util.trace.CalciteLogger;

import com.google.common.collect.ImmutableList;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.LoggerFactory;

import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;

import static com.google.common.base.Preconditions.checkArgument;

import static java.util.Objects.requireNonNull;

/**
 * Pretty printer for SQL statements.
 *
 * <p>There are several options to control the format.
 *
 * <table>
 * <caption>Formatting options</caption>
 * <tr>
 * <th>Option</th>
 * <th>Description</th>
 * <th>Default</th>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#clauseStartsLine()} ClauseStartsLine}</td>
 * <td>Whether a clause ({@code FROM}, {@code WHERE}, {@code GROUP BY},
 * {@code HAVING}, {@code WINDOW}, {@code ORDER BY}) starts a new line.
 * {@code SELECT} is always at the start of a line.</td>
 * <td>true</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#clauseEndsLine ClauseEndsLine}</td>
 * <td>Whether a clause ({@code SELECT}, {@code FROM}, {@code WHERE},
 * {@code GROUP BY}, {@code HAVING}, {@code WINDOW}, {@code ORDER BY}) is
 * followed by a new line.</td>
 * <td>false</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#caseClausesOnNewLines CaseClausesOnNewLines}</td>
 * <td>Whether the WHEN, THEN and ELSE clauses of a CASE expression appear at
 * the start of a new line.</td>
 * <td>false</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#indentation Indentation}</td>
 * <td>Number of spaces to indent</td>
 * <td>4</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#keywordsLowerCase KeywordsLowerCase}</td>
 * <td>Whether to print keywords (SELECT, AS, etc.) in lower-case.</td>
 * <td>false</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#alwaysUseParentheses AlwaysUseParentheses}</td>
 * <td>Whether to enclose all expressions in parentheses, even if the
 * operator has high enough precedence that the parentheses are not required.
 *
 * <p>For example, the parentheses are required in the expression
 * {@code (a + b) * c} because the '*' operator has higher precedence than the
 * '+' operator, and so without the parentheses, the expression would be
 * equivalent to {@code a + (b * c)}. The fully-parenthesized expression,
 * {@code ((a + b) * c)} is unambiguous even if you don't know the precedence
 * of every operator.</td>
 * <td>false</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#quoteAllIdentifiers QuoteAllIdentifiers}</td>
 * <td>Whether to quote all identifiers, even those which would be correct
 * according to the rules of the {@link SqlDialect} if quotation marks were
 * omitted.</td>
 * <td>true</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#subQueryStyle SubQueryStyle}</td>
 * <td>Style for formatting sub-queries. Values are:
 * {@link org.apache.calcite.sql.SqlWriter.SubQueryStyle#HYDE Hyde},
 * {@link org.apache.calcite.sql.SqlWriter.SubQueryStyle#BLACK Black}.</td>
 *
 * <td>{@link org.apache.calcite.sql.SqlWriter.SubQueryStyle#HYDE Hyde}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#lineLength LineLength}</td>
 * <td>The desired maximum length for lines (to look nice in editors,
 * printouts, etc.).</td>
 * <td>-1 (no maximum)</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#foldLength FoldLength}</td>
 * <td>The line length at which lines are folded or chopped down
 * (see {@code LineFolding}). Only has an effect if clauses are marked
 * {@link SqlWriterConfig.LineFolding#CHOP CHOP} or
 * {@link SqlWriterConfig.LineFolding#FOLD FOLD}.</td>
 * <td>80</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#lineFolding LineFolding}</td>
 * <td>How long lines are to be handled. Options are lines are
 * WIDE (do not wrap),
 * FOLD (wrap if long),
 * CHOP (chop down if long),
 * and TALL (wrap always).</td>
 * <td>WIDE</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#selectFolding() SelectFolding}</td>
 * <td>How the {@code SELECT} clause is to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#fromFolding FromFolding}</td>
 * <td>How the {@code FROM} clause and nested {@code JOIN} clauses are to be
 * folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#whereFolding WhereFolding}</td>
 * <td>How the {@code WHERE} clause is to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#groupByFolding GroupByFolding}</td>
 * <td>How the {@code GROUP BY} clause is to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#havingFolding HavingFolding}</td>
 * <td>How the {@code HAVING} clause is to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#orderByFolding OrderByFolding}</td>
 * <td>How the {@code ORDER BY} clause is to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#windowFolding WindowFolding}</td>
 * <td>How the {@code WINDOW} clause is to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#overFolding OverFolding}</td>
 * <td>How window declarations in the {@code WINDOW} clause
 * and in the {@code OVER} clause of aggregate functions are to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#valuesFolding ValuesFolding}</td>
 * <td>How lists of values in the {@code VALUES} clause are to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * <tr>
 * <td>{@link SqlWriterConfig#updateSetFolding UpdateSetFolding}</td>
 * <td>How assignments in the {@code SET} clause of an {@code UPDATE} statement
 * are to be folded.</td>
 * <td>{@code LineFolding}</td>
 * </tr>
 *
 * </table>
 *
 * <p>The following options exist for backwards compatibility. They are
 * used if {@link SqlWriterConfig#lineFolding LineFolding} and clause-specific
 * options such as {@link SqlWriterConfig#selectFolding SelectFolding} are not
 * specified:
 *
 * <ul>
 *
 * <li>{@link SqlWriterConfig#selectListItemsOnSeparateLines SelectListItemsOnSeparateLines}
 * replaced by {@link SqlWriterConfig#selectFolding SelectFolding},
 * {@link SqlWriterConfig#groupByFolding GroupByFolding}, and
 * {@link SqlWriterConfig#orderByFolding OrderByFolding};
 *
 * <li>{@link SqlWriterConfig#updateSetListNewline UpdateSetListNewline}
 * replaced by {@link SqlWriterConfig#updateSetFolding UpdateSetFolding};
 *
 * <li>{@link SqlWriterConfig#windowDeclListNewline WindowDeclListNewline}
 * replaced by {@link SqlWriterConfig#windowFolding WindowFolding};
 *
 * <li>{@link SqlWriterConfig#windowNewline WindowNewline}
 * replaced by {@link SqlWriterConfig#overFolding OverFolding};
 *
 * <li>{@link SqlWriterConfig#valuesListNewline ValuesListNewline}
 * replaced by {@link SqlWriterConfig#valuesFolding ValuesFolding}.
 *
 * </ul>
 */
public class SqlPrettyWriter implements SqlWriter {
  //~ Static fields/initializers ---------------------------------------------

  protected static final CalciteLogger LOGGER =
      new CalciteLogger(
          LoggerFactory.getLogger("org.apache.calcite.sql.pretty.SqlPrettyWriter"));

  /**
   * Default SqlWriterConfig, reduce the overhead of "ImmutableBeans.create"
   */
  private static final SqlWriterConfig CONFIG =
      SqlWriterConfig.of()
          .withDialect(CalciteSqlDialect.DEFAULT);

  /**
   * Bean holding the default property values.
   */
  private static final Bean DEFAULT_BEAN =
      new SqlPrettyWriter(SqlPrettyWriter.config()
          .withDialect(AnsiSqlDialect.DEFAULT)).getBean();
  protected static final String NL = System.lineSeparator();

  //~ Instance fields --------------------------------------------------------

  private final SqlDialect dialect;
  private final StringBuilder buf;
  private final Deque<FrameImpl> listStack = new ArrayDeque<>();
  private ImmutableList.@Nullable Builder<Integer> dynamicParameters;
  protected @Nullable FrameImpl frame;
  private boolean needWhitespace;
  protected @Nullable String nextWhitespace;
  private SqlWriterConfig config;
  private @Nullable Bean bean;
  private int currentIndent;

  private int lineStart;

  //~ Constructors -----------------------------------------------------------

  @SuppressWarnings("method.invocation.invalid")
  private SqlPrettyWriter(SqlWriterConfig config,
      StringBuilder buf, @SuppressWarnings("unused") boolean ignore) {
    this.buf = requireNonNull(buf, "buf");
    this.dialect = requireNonNull(config.dialect());
    this.config = requireNonNull(config, "config");
    lineStart = 0;
    reset();
  }

  /** Creates a writer with the given configuration
   * and a given buffer to write to. */
  public SqlPrettyWriter(SqlWriterConfig config, StringBuilder buf) {
    this(config, buf, false);
  }

  /** Creates a writer with the given configuration and dialect,
   * and a given print writer (or a private print writer if it is null). */
  public SqlPrettyWriter(
      SqlDialect dialect,
      SqlWriterConfig config,
      StringBuilder buf) {
    this(config.withDialect(requireNonNull(dialect, "dialect")), buf);
  }

  /** Creates a writer with the given configuration
   * and a private print writer. */
  @Deprecated
  public SqlPrettyWriter(SqlDialect dialect, SqlWriterConfig config) {
    this(config.withDialect(requireNonNull(dialect, "dialect")));
  }

  @Deprecated
  public SqlPrettyWriter(
      SqlDialect dialect,
      boolean alwaysUseParentheses,
      PrintWriter pw) {
    // NOTE that 'pw' is ignored; there is no place for it in the new API
    this(config().withDialect(requireNonNull(dialect, "dialect"))
        .withAlwaysUseParentheses(alwaysUseParentheses));
  }

  @Deprecated
  public SqlPrettyWriter(
      SqlDialect dialect,
      boolean alwaysUseParentheses) {
    this(config().withDialect(requireNonNull(dialect, "dialect"))
        .withAlwaysUseParentheses(alwaysUseParentheses));
  }

  /** Creates a writer with a given dialect, the default configuration
   * and a private print writer. */
  @Deprecated
  public SqlPrettyWriter(SqlDialect dialect) {
    this(config().withDialect(requireNonNull(dialect, "dialect")));
  }

  /** Creates a writer with the given configuration,
   * and a private builder. */
  public SqlPrettyWriter(SqlWriterConfig config) {
    this(config, new StringBuilder(), true);
  }

  /** Creates a writer with the default configuration.
   *
   * @see #config() */
  public SqlPrettyWriter() {
    this(config());
  }

  /** Creates a {@link SqlWriterConfig} with Calcite's SQL dialect. */
  public static SqlWriterConfig config() {
    return CONFIG;
  }

  //~ Methods ----------------------------------------------------------------

  @Deprecated
  public void setCaseClausesOnNewLines(boolean caseClausesOnNewLines) {
    this.config = config.withCaseClausesOnNewLines(caseClausesOnNewLines);
  }

  @Deprecated
  public void setSubQueryStyle(SubQueryStyle subQueryStyle) {
    this.config = config.withSubQueryStyle(subQueryStyle);
  }

  @Deprecated
  public void setWindowNewline(boolean windowNewline) {
    this.config = config.withWindowNewline(windowNewline);
  }

  @Deprecated
  public void setWindowDeclListNewline(boolean windowDeclListNewline) {
    this.config = config.withWindowDeclListNewline(windowDeclListNewline);
  }

  @Deprecated
  @Override public int getIndentation() {
    return config.indentation();
  }

  @Deprecated
  @Override public boolean isAlwaysUseParentheses() {
    return config.alwaysUseParentheses();
  }

  @Override public boolean inQuery() {
    return (frame == null)
        || (frame.frameType == FrameTypeEnum.SELECT)
        || (frame.frameType == FrameTypeEnum.ORDER_BY)
        || (frame.frameType == FrameTypeEnum.WITH_BODY)
        || (frame.frameType == FrameTypeEnum.SETOP);
  }

  @Deprecated
  @Override public boolean isQuoteAllIdentifiers() {
    return config.quoteAllIdentifiers();
  }

  @Deprecated
  @Override public boolean isClauseStartsLine() {
    return config.clauseStartsLine();
  }

  @Deprecated
  @Override public boolean isSelectListItemsOnSeparateLines() {
    return config.selectListItemsOnSeparateLines();
  }

  @Deprecated
  public boolean isWhereListItemsOnSeparateLines() {
    return config.whereListItemsOnSeparateLines();
  }

  @Deprecated
  public boolean isSelectListExtraIndentFlag() {
    return config.selectListExtraIndentFlag();
  }

  @Deprecated
  @Override public boolean isKeywordsLowerCase() {
    return config.keywordsLowerCase();
  }

  @Deprecated
  public int getLineLength() {
    return config.lineLength();
  }

  @Override public void resetSettings() {
    reset();
    config = config();
  }

  @Override public void reset() {
    buf.setLength(0);
    lineStart = 0;
    dynamicParameters = null;
    setNeedWhitespace(false);
    nextWhitespace = " ";
  }

  /**
   * Returns an object which encapsulates each property as a get/set method.
   */
  private Bean getBean() {
    if (bean == null) {
      bean = new Bean(this);
    }
    return bean;
  }

  @Deprecated
  public void setIndentation(int indentation) {
    this.config = config.withIndentation(indentation);
  }

  /**
   * Prints the property settings of this pretty-writer to a writer.
   *
   * @param pw           Writer
   * @param omitDefaults Whether to omit properties whose value is the same as
   *                     the default
   */
  public void describe(PrintWriter pw, boolean omitDefaults) {
    final Bean properties = getBean();
    final String[] propertyNames = properties.getPropertyNames();
    int count = 0;
    for (String key : propertyNames) {
      final Object value = properties.get(key);
      final Object defaultValue = DEFAULT_BEAN.get(key);
      if (Objects.equals(value, defaultValue)) {
        continue;
      }
      if (count++ > 0) {
        pw.print(",");
      }
      pw.print(key + "=" + value);
    }
  }

  /**
   * Sets settings from a properties object.
   */
  public void setSettings(Properties properties) {
    resetSettings();
    final Bean bean = getBean();
    final String[] propertyNames = bean.getPropertyNames();
    for (String propertyName : propertyNames) {
      final String value = properties.getProperty(propertyName);
      if (value != null) {
        bean.set(propertyName, value);
      }
    }
  }

  @Deprecated
  public void setClauseStartsLine(boolean clauseStartsLine) {
    this.config = config.withClauseStartsLine(clauseStartsLine);
  }

  @Deprecated
  public void setSelectListItemsOnSeparateLines(boolean b) {
    this.config = config.withSelectListItemsOnSeparateLines(b);
  }

  @Deprecated
  public void setSelectListExtraIndentFlag(boolean selectListExtraIndentFlag) {
    this.config =
        config.withSelectListExtraIndentFlag(selectListExtraIndentFlag);
  }

  @Deprecated
  public void setKeywordsLowerCase(boolean keywordsLowerCase) {
    this.config = config.withKeywordsLowerCase(keywordsLowerCase);
  }

  @Deprecated
  public void setWhereListItemsOnSeparateLines(
      boolean whereListItemsOnSeparateLines) {
    this.config =
        config.withWhereListItemsOnSeparateLines(whereListItemsOnSeparateLines);
  }

  @Deprecated
  public void setAlwaysUseParentheses(boolean alwaysUseParentheses) {
    this.config = config.withAlwaysUseParentheses(alwaysUseParentheses);
  }

  @Override public void newlineAndIndent() {
    newlineAndIndent(currentIndent);
  }

  public void newlineAndIndent(int indent) {
    buf.append(NL);
    lineStart = buf.length();
    indent(indent);
    setNeedWhitespace(false); // no further whitespace necessary
  }

  void indent(int indent) {
    if (indent < 0) {
      throw new IllegalArgumentException("negative indent " + indent);
    }
    Spaces.append(buf, indent);
  }

  @Deprecated
  public void setQuoteAllIdentifiers(boolean quoteAllIdentifiers) {
    this.config = config.withQuoteAllIdentifiers(quoteAllIdentifiers);
  }

  /**
   * Creates a list frame.
   *
   * <p>Derived classes should override this method to specify the indentation
   * of the list.
   *
   * @param frameType What type of list
   * @param keyword   The keyword to be printed at the start of the list
   * @param open      The string to print at the start of the list
   * @param close     The string to print at the end of the list
   * @return A frame
   */
  protected FrameImpl createListFrame(
      FrameType frameType,
      @Nullable String keyword,
      String open,
      String close) {
    final FrameTypeEnum frameTypeEnum =
        frameType instanceof FrameTypeEnum ? (FrameTypeEnum) frameType
            : FrameTypeEnum.OTHER;
    final int indentation = config.indentation();
    boolean newlineAfterOpen = false;
    boolean newlineBeforeSep = false;
    boolean newlineAfterSep = false;
    boolean newlineBeforeClose = false;
    int left = column();
    int sepIndent = indentation;
    int extraIndent = 0;
    final SqlWriterConfig.LineFolding fold = fold(frameTypeEnum);
    final boolean newline = fold == SqlWriterConfig.LineFolding.TALL;

    switch (frameTypeEnum) {
    case SELECT:
      extraIndent = indentation;
      newlineAfterOpen = false;
      newlineBeforeSep = config.clauseStartsLine(); // newline before FROM, WHERE etc.
      newlineAfterSep = false;
      sepIndent = 0; // all clauses appear below SELECT
      break;

    case SETOP:
      extraIndent = 0;
      newlineAfterOpen = false;
      newlineBeforeSep = config.clauseStartsLine(); // newline before UNION, EXCEPT
      newlineAfterSep = config.clauseStartsLine(); // newline after UNION, EXCEPT
      sepIndent = 0; // all clauses appear below SELECT
      break;

    case SELECT_LIST:
    case FROM_LIST:
    case JOIN:
    case GROUP_BY_LIST:
    case ORDER_BY_LIST:
    case WINDOW_DECL_LIST:
    case VALUES:
      if (config.selectListExtraIndentFlag()) {
        extraIndent = indentation;
      }
      left = frame == null ? 0 : frame.left;
      newlineAfterOpen = config.clauseEndsLine()
          && (fold == SqlWriterConfig.LineFolding.TALL
              || fold == SqlWriterConfig.LineFolding.STEP);
      newlineBeforeSep = false;
      newlineAfterSep = newline;
      if (config.leadingComma() && newline) {
        newlineBeforeSep = true;
        newlineAfterSep = false;
        sepIndent = -", ".length();
      }
      break;

    case WHERE_LIST:
    case WINDOW:
      extraIndent = indentation;
      newlineAfterOpen = newline && config.clauseEndsLine();
      newlineBeforeSep = newline;
      sepIndent = 0;
      newlineAfterSep = false;
      break;

    case ORDER_BY:
    case OFFSET:
    case FETCH:
      newlineAfterOpen = false;
      newlineBeforeSep = true;
      sepIndent = 0;
      newlineAfterSep = false;
      break;

    case UPDATE_SET_LIST:
      extraIndent = indentation;
      newlineAfterOpen = newline;
      newlineBeforeSep = false;
      sepIndent = 0;
      newlineAfterSep = newline;
      break;

    case CASE:
      newlineAfterOpen = newline;
      newlineBeforeSep = newline;
      newlineBeforeClose = newline;
      sepIndent = 0;
      break;

    default:
      break;
    }

    final int chopColumn;
    SqlWriterConfig.LineFolding lineFolding = config.lineFolding();
    if (lineFolding == null) {
      lineFolding = SqlWriterConfig.LineFolding.WIDE;
      chopColumn = -1;
    } else {
      if (config.foldLength() > 0
          && (lineFolding == SqlWriterConfig.LineFolding.CHOP
              || lineFolding == SqlWriterConfig.LineFolding.FOLD
              || lineFolding == SqlWriterConfig.LineFolding.STEP)) {
        chopColumn = left + config.foldLength();
      } else {
        chopColumn = -1;
      }
    }

    switch (frameTypeEnum) {
    case UPDATE_SET_LIST:
    case WINDOW_DECL_LIST:
    case VALUES:
    case SELECT:
    case SETOP:
    case SELECT_LIST:
    case WHERE_LIST:
    case ORDER_BY_LIST:
    case GROUP_BY_LIST:
    case WINDOW:
    case ORDER_BY:
    case OFFSET:
    case FETCH:
      return new FrameImpl(frameType, keyword, open, close,
          left, extraIndent, chopColumn, lineFolding, newlineAfterOpen,
          newlineBeforeSep, sepIndent, newlineAfterSep, false, false);

    case SUB_QUERY:
      switch (config.subQueryStyle()) {
      case BLACK:
        // Generate, e.g.:
        //
        // WHERE foo = bar IN
        // (   SELECT ...
        open = Spaces.padRight("(", indentation - 1);
        return new FrameImpl(frameType, keyword, open, close,
            left, 0, chopColumn, lineFolding, false, true, indentation,
            false, false, false) {
          protected void _before() {
            newlineAndIndent();
          }
        };

      case HYDE:
        // Generate, e.g.:
        //
        // WHERE foo IN (
        //     SELECT ...
        return new FrameImpl(frameType, keyword, open, close, left, 0,
            chopColumn, lineFolding, false, true, 0, false, false, false) {
          protected void _before() {
            nextWhitespace = NL;
          }
        };

      default:
        throw Util.unexpected(config.subQueryStyle());
      }

    case FUN_CALL:
      setNeedWhitespace(false);
      return new FrameImpl(frameType, keyword, open, close,
          left, indentation, chopColumn, lineFolding, false, false,
          indentation, false, false, false);

    case PARENTHESES:
      open = "(";
      close = ")";
      // fall through
    case IDENTIFIER:
    case SIMPLE:
      return new FrameImpl(frameType, keyword, open, close,
          left, indentation, chopColumn, lineFolding, false, false,
          indentation, false, false, false);

    case FROM_LIST:
    case JOIN:
      newlineBeforeSep = newline;
      sepIndent = 0; // all clauses appear below SELECT
      newlineAfterSep = false;
      final boolean newlineBeforeComma = config.leadingComma() && newline;
      final boolean newlineAfterComma = !config.leadingComma() && newline;
      return new FrameImpl(frameType, keyword, open, close, left,
          indentation, chopColumn, lineFolding, newlineAfterOpen,
          newlineBeforeSep, sepIndent, newlineAfterSep, false, false) {
        @Override protected void sep(boolean printFirst, String sep) {
          final boolean newlineBeforeSep;
          final boolean newlineAfterSep;
          if (sep.equals(",")) {
            newlineBeforeSep = newlineBeforeComma;
            newlineAfterSep = newlineAfterComma;
          } else {
            newlineBeforeSep = this.newlineBeforeSep;
            newlineAfterSep = this.newlineAfterSep;
          }
          if (itemCount == 0) {
            if (newlineAfterOpen) {
              newlineAndIndent(currentIndent);
            }
          } else {
            if (newlineBeforeSep) {
              newlineAndIndent(currentIndent + sepIndent);
            }
          }
          if ((itemCount > 0) || printFirst) {
            keyword(sep);
            nextWhitespace = newlineAfterSep ? NL : " ";
          }
          ++itemCount;
        }
      };

    default:
    case OTHER:
      return new FrameImpl(frameType, keyword, open, close, left, indentation,
          chopColumn, lineFolding, newlineAfterOpen, newlineBeforeSep,
          sepIndent, newlineAfterSep, newlineBeforeClose, false);
    }
  }

  private SqlWriterConfig.LineFolding fold(FrameTypeEnum frameType) {
    switch (frameType) {
    case SELECT_LIST:
      return f3(config.selectFolding(), config.lineFolding(),
          config.selectListItemsOnSeparateLines());
    case GROUP_BY_LIST:
      return f3(config.groupByFolding(), config.lineFolding(),
          config.selectListItemsOnSeparateLines());
    case ORDER_BY_LIST:
      return f3(config.orderByFolding(), config.lineFolding(),
          config.selectListItemsOnSeparateLines());
    case UPDATE_SET_LIST:
      return f3(config.updateSetFolding(), config.lineFolding(),
          config.updateSetListNewline());
    case WHERE_LIST:
      return f3(config.whereFolding(), config.lineFolding(),
          config.whereListItemsOnSeparateLines());
    case WINDOW_DECL_LIST:
      return f3(config.windowFolding(), config.lineFolding(),
          config.clauseStartsLine() && config.windowDeclListNewline());
    case WINDOW:
      return f3(config.overFolding(), config.lineFolding(),
          config.windowNewline());
    case VALUES:
      return f3(config.valuesFolding(), config.lineFolding(),
          config.valuesListNewline());
    case FROM_LIST:
    case JOIN:
      return f3(config.fromFolding(), config.lineFolding(),
          config.caseClausesOnNewLines());
    case CASE:
      return f3(null, null, config.caseClausesOnNewLines());
    default:
      return SqlWriterConfig.LineFolding.WIDE;
    }
  }

  private static SqlWriterConfig.LineFolding f3(SqlWriterConfig.@Nullable LineFolding folding0,
      SqlWriterConfig.@Nullable LineFolding folding1, boolean opt) {
    return folding0 != null ? folding0
        : folding1 != null ? folding1
            : opt ? SqlWriterConfig.LineFolding.TALL
                : SqlWriterConfig.LineFolding.WIDE;
  }

  /**
   * Starts a list.
   *
   * @param frameType Type of list. For example, a SELECT list will be
   *                  governed according to SELECT-list formatting preferences.
   * @param open      String to print at the start of the list; typically "(" or
   *                  the empty string.
   * @param close     String to print at the end of the list.
   */
  protected Frame startList(
      FrameType frameType,
      @Nullable String keyword,
      String open,
      String close) {
    requireNonNull(frameType, "frameType");
    FrameImpl frame = this.frame;
    if (frame != null) {
      if (frame.itemCount++ == 0 && frame.newlineAfterOpen) {
        newlineAndIndent();
      } else if (frameType == FrameTypeEnum.SUB_QUERY
          && config.subQueryStyle() == SubQueryStyle.BLACK) {
        newlineAndIndent(currentIndent - frame.extraIndent);
      }

      // REVIEW jvs 9-June-2006:  This is part of the fix for FRG-149
      // (extra frame for identifier was leading to extra indentation,
      // causing select list to come out raggedy with identifiers
      // deeper than literals); are there other frame types
      // for which extra indent should be suppressed?
      currentIndent += frame.extraIndent(frameType);
      assert !listStack.contains(frame);
      listStack.push(frame);
    }
    frame = createListFrame(frameType, keyword, open, close);
    this.frame = frame;
    frame.before();
    return frame;
  }

  @Override public void endList(@Nullable Frame frame) {
    FrameImpl endedFrame = (FrameImpl) frame;
    checkArgument(frame == this.frame,
        "Frame does not match current frame");
    if (endedFrame == null) {
      throw new RuntimeException("No list started");
    }
    if (endedFrame.open.equals("(")) {
      if (!endedFrame.close.equals(")")) {
        throw new RuntimeException("Expected ')'");
      }
    }
    if (endedFrame.newlineBeforeClose) {
      newlineAndIndent();
    }
    keyword(endedFrame.close);
    if (endedFrame.newlineAfterClose) {
      newlineAndIndent();
    }

    // Pop the frame, and move to the previous indentation level.
    if (listStack.isEmpty()) {
      this.frame = null;
      assert currentIndent == 0 : currentIndent;
    } else {
      this.frame = listStack.pop();
      currentIndent -= this.frame.extraIndent(endedFrame.frameType);
    }
  }

  public String format(SqlNode node) {
    assert frame == null;
    node.unparse(this, 0, 0);
    assert frame == null;
    return toString();
  }

  @Override public String toString() {
    return buf.toString();
  }

  @Override public SqlString toSqlString() {
    ImmutableList<Integer> dynamicParameters =
        this.dynamicParameters == null ? null : this.dynamicParameters.build();
    return new SqlString(dialect, toString(), dynamicParameters);
  }

  @Override public SqlDialect getDialect() {
    return dialect;
  }

  @Override public void literal(String s) {
    print(s);
    setNeedWhitespace(true);
  }

  @Override public void keyword(String s) {
    maybeWhitespace(s);
    buf.append(
        isKeywordsLowerCase()
            ? s.toLowerCase(Locale.ROOT)
            : s.toUpperCase(Locale.ROOT));
    if (!s.isEmpty()) {
      setNeedWhitespace(needWhitespaceAfter(s));
    }
  }

  private void maybeWhitespace(String s) {
    if (tooLong(s) || (needWhitespace && needWhitespaceBefore(s))) {
      whiteSpace();
    }
  }

  private static boolean needWhitespaceBefore(String s) {
    return !(s.equals(",")
        || s.equals(".")
        || s.equals(")")
        || s.equals("[")
        || s.equals("]")
        || s.isEmpty());
  }

  private static boolean needWhitespaceAfter(String s) {
    return !(s.equals("(")
        || s.equals("[")
        || s.equals("."));
  }

  protected void whiteSpace() {
    if (needWhitespace) {
      if (NL.equals(nextWhitespace)) {
        newlineAndIndent();
      } else {
        buf.append(nextWhitespace);
      }
      nextWhitespace = " ";
      setNeedWhitespace(false);
    }
  }

  /** Returns the number of characters appended since the last newline. */
  private int column() {
    return buf.length() - lineStart;
  }

  protected boolean tooLong(String s) {
    final int lineLength = config.lineLength();
    boolean result =
        lineLength > 0
            && (column() > currentIndent)
            && ((column() + s.length()) >= lineLength);
    if (result) {
      nextWhitespace = NL;
    }
    LOGGER.trace("Token is '{}'; result is {}", s, result);
    return result;
  }

  @Override public void print(String s) {
    maybeWhitespace(s);
    buf.append(s);
  }

  @Override public void print(int x) {
    maybeWhitespace("0");
    buf.append(x);
  }

  @Override public void identifier(String name, boolean quoted) {
    // If configured globally or the original identifier is quoted,
    // then quotes the identifier.
    maybeWhitespace(name);
    if (isQuoteAllIdentifiers() || quoted) {
      dialect.quoteIdentifier(buf, name);
    } else {
      buf.append(name);
    }
    setNeedWhitespace(true);
  }

  @Override public void dynamicParam(int index) {
    if (dynamicParameters == null) {
      dynamicParameters = ImmutableList.builder();
    }
    dynamicParameters.add(index);
    print("?");
    setNeedWhitespace(true);
  }

  @Override public void fetchOffset(@Nullable SqlNode fetch, @Nullable SqlNode offset) {
    if (fetch == null && offset == null) {
      return;
    }
    dialect.unparseOffsetFetch(this, offset, fetch);
  }

  @Override public void topN(@Nullable SqlNode fetch, @Nullable SqlNode offset) {
    if (fetch == null && offset == null) {
      return;
    }
    dialect.unparseTopN(this, offset, fetch);
  }

  @Override public Frame startFunCall(String funName) {
    keyword(funName);
    setNeedWhitespace(false);
    return startList(FrameTypeEnum.FUN_CALL, "(", ")");
  }

  @Override public void endFunCall(Frame frame) {
    endList(this.frame);
  }

  @Override public Frame startList(String open, String close) {
    return startList(FrameTypeEnum.SIMPLE, null, open, close);
  }

  @Override public Frame startList(FrameTypeEnum frameType) {
    return startList(requireNonNull(frameType, "frameType"), null, "", "");
  }

  @Override public Frame startList(FrameType frameType, String open,
      String close) {
    return startList(requireNonNull(frameType, "frameType"), null, open, close);
  }

  @Override public SqlWriter list(FrameTypeEnum frameType, Consumer<SqlWriter> action) {
    final SqlWriter.Frame selectListFrame =
        startList(SqlWriter.FrameTypeEnum.SELECT_LIST);
    final SqlWriter w = this;
    action.accept(w);
    endList(selectListFrame);
    return this;
  }

  @Override public SqlWriter list(FrameTypeEnum frameType, SqlBinaryOperator sepOp,
      SqlNodeList list) {
    final SqlWriter.Frame frame = startList(frameType);
    ((FrameImpl) frame).list(list, sepOp);
    endList(frame);
    return this;
  }

  @Override public void sep(String sep) {
    sep(sep, !(sep.equals(",") || sep.equals(".")));
  }

  @Override public void sep(String sep, boolean printFirst) {
    if (frame == null) {
      throw new RuntimeException("No list started");
    }
    if (sep.startsWith(" ") || sep.endsWith(" ")) {
      throw new RuntimeException("Separator must not contain whitespace");
    }
    frame.sep(printFirst, sep);
  }

  @Override public void setNeedWhitespace(boolean needWhitespace) {
    this.needWhitespace = needWhitespace;
  }

  @Deprecated
  public void setLineLength(int lineLength) {
    this.config = config.withLineLength(lineLength);
  }

  public void setFormatOptions(@Nullable SqlFormatOptions options) {
    if (options == null) {
      return;
    }
    setAlwaysUseParentheses(options.isAlwaysUseParentheses());
    setCaseClausesOnNewLines(options.isCaseClausesOnNewLines());
    setClauseStartsLine(options.isClauseStartsLine());
    setKeywordsLowerCase(options.isKeywordsLowercase());
    setQuoteAllIdentifiers(options.isQuoteAllIdentifiers());
    setSelectListItemsOnSeparateLines(
        options.isSelectListItemsOnSeparateLines());
    setWhereListItemsOnSeparateLines(
        options.isWhereListItemsOnSeparateLines());
    setWindowNewline(options.isWindowDeclarationStartsLine());
    setWindowDeclListNewline(options.isWindowListItemsOnSeparateLines());
    setIndentation(options.getIndentation());
    setLineLength(options.getLineLength());
  }

  //~ Inner Classes ----------------------------------------------------------

  /**
   * Implementation of {@link org.apache.calcite.sql.SqlWriter.Frame}.
   */
  protected class FrameImpl implements Frame {
    final FrameType frameType;
    final @Nullable String keyword;
    final String open;
    final String close;

    private final int left;
    /**
     * Indent of sub-frame with respect to this one.
     */
    final int extraIndent;

    /**
     * Indent of separators with respect to this frame's indent. Typically
     * zero.
     */
    final int sepIndent;

    /**
     * Number of items which have been printed in this list so far.
     */
    int itemCount;

    /**
     * Whether to print a newline before each separator.
     */
    public final boolean newlineBeforeSep;

    /**
     * Whether to print a newline after each separator.
     */
    public final boolean newlineAfterSep;
    protected final boolean newlineBeforeClose;
    protected final boolean newlineAfterClose;
    protected final boolean newlineAfterOpen;

    /** Character count after which we should move an item to the
     * next line. Or {@link Integer#MAX_VALUE} if we are not chopping. */
    private final int chopLimit;

    /** How lines are to be folded. */
    private final SqlWriterConfig.LineFolding lineFolding;

    FrameImpl(FrameType frameType, @Nullable String keyword, String open, String close,
        int left, int extraIndent, int chopLimit,
        SqlWriterConfig.LineFolding lineFolding, boolean newlineAfterOpen,
        boolean newlineBeforeSep, int sepIndent, boolean newlineAfterSep,
        boolean newlineBeforeClose, boolean newlineAfterClose) {
      this.frameType = frameType;
      this.keyword = keyword;
      this.open = requireNonNull(open, "open");
      this.close = requireNonNull(close, "close");
      this.left = left;
      this.extraIndent = extraIndent;
      this.chopLimit = chopLimit;
      this.lineFolding = requireNonNull(lineFolding, "lineFolding");
      this.newlineAfterOpen = newlineAfterOpen;
      this.newlineBeforeSep = newlineBeforeSep;
      this.newlineAfterSep = newlineAfterSep;
      this.newlineBeforeClose = newlineBeforeClose;
      this.newlineAfterClose = newlineAfterClose;
      this.sepIndent = sepIndent;
      assert chopLimit >= 0
          == (lineFolding == SqlWriterConfig.LineFolding.CHOP
          || lineFolding == SqlWriterConfig.LineFolding.FOLD
          || lineFolding == SqlWriterConfig.LineFolding.STEP);
    }

    protected void before() {
      if (!open.isEmpty()) {
        keyword(open);
      }
    }

    protected void after() {
    }

    protected void sep(boolean printFirst, String sep) {
      if (itemCount == 0) {
        if (newlineAfterOpen) {
          newlineAndIndent(currentIndent);
        }
      } else {
        if (newlineBeforeSep) {
          newlineAndIndent(currentIndent + sepIndent);
        }
      }
      if ((itemCount > 0) || printFirst) {
        keyword(sep);
        nextWhitespace = newlineAfterSep ? NL : " ";
      }
      ++itemCount;
    }

    /** Returns the extra indent required for a given type of sub-frame. */
    int extraIndent(FrameType subFrameType) {
      if (this.frameType == FrameTypeEnum.ORDER_BY
          && subFrameType == FrameTypeEnum.ORDER_BY_LIST) {
        return config.indentation();
      }
      if (subFrameType.needsIndent()) {
        return extraIndent;
      }
      return 0;
    }

    void list(SqlNodeList list, SqlBinaryOperator sepOp) {
      final Save save;
      switch (lineFolding) {
      case CHOP:
      case FOLD:
        save = new Save();
        if (!list2(list, sepOp)) {
          save.restore();
          final boolean newlineAfterOpen = config.clauseEndsLine();
          final SqlWriterConfig.LineFolding lineFolding;
          final int chopLimit;
          if (this.lineFolding == SqlWriterConfig.LineFolding.CHOP) {
            lineFolding = SqlWriterConfig.LineFolding.TALL;
            chopLimit = -1;
          } else {
            lineFolding = SqlWriterConfig.LineFolding.FOLD;
            chopLimit = this.chopLimit;
          }
          final boolean newline =
              lineFolding == SqlWriterConfig.LineFolding.TALL;
          final boolean newlineBeforeSep;
          final boolean newlineAfterSep;
          final int sepIndent;
          if (config.leadingComma() && newline) {
            newlineBeforeSep = true;
            newlineAfterSep = false;
            sepIndent = -", ".length();
          } else if (newline) {
            newlineBeforeSep = false;
            newlineAfterSep = true;
            sepIndent = this.sepIndent;
          } else {
            newlineBeforeSep = false;
            newlineAfterSep = false;
            sepIndent = this.sepIndent;
          }
          final FrameImpl frame2 =
              new FrameImpl(frameType, keyword, open, close, left, extraIndent,
                  chopLimit, lineFolding, newlineAfterOpen, newlineBeforeSep,
                  sepIndent, newlineAfterSep, newlineBeforeClose,
                  newlineAfterClose);
          frame2.list2(list, sepOp);
        }
        break;
      default:
        list2(list, sepOp);
      }
    }

    /** Tries to write a list. If the line runs too long, returns false,
     * indicating to retry. */
    private boolean list2(SqlNodeList list, SqlBinaryOperator sepOp) {
      // The precedence pulling on the LHS of a node is the
      // right-precedence of the separator operator. Similarly RHS.
      //
      // At the start and end of the list precedence should be 0, but non-zero
      // precedence is useful, because it forces parentheses around
      // sub-queries and empty lists, e.g. "SELECT a, (SELECT * FROM t), b",
      // "GROUP BY ()".
      final int lprec = sepOp.getRightPrec();
      final int rprec = sepOp.getLeftPrec();
      if (chopLimit < 0) {
        for (SqlNode node : list) {
          sep(false, sepOp.getName());
          node.unparse(SqlPrettyWriter.this, lprec, rprec);
        }
      } else if (newlineBeforeSep) {
        for (SqlNode node : list) {
          sep(false, sepOp.getName());
          final Save prevSize = new Save();
          node.unparse(SqlPrettyWriter.this, lprec, rprec);
          if (column() > chopLimit) {
            if (lineFolding == SqlWriterConfig.LineFolding.CHOP
                || lineFolding == SqlWriterConfig.LineFolding.TALL) {
              return false;
            }
            prevSize.restore();
            newlineAndIndent();
            node.unparse(SqlPrettyWriter.this, lprec, rprec);
          }
        }
      } else {
        for (int i = 0; i < list.size(); i++) {
          SqlNode node = list.get(i);
          if (i == 0) {
            sep(false, sepOp.getName());
          }
          final Save save = new Save();
          node.unparse(SqlPrettyWriter.this, lprec, rprec);
          if (i + 1 < list.size()) {
            sep(false, sepOp.getName());
          }
          if (column() > chopLimit) {
            switch (lineFolding) {
            case CHOP:
              return false;
            case FOLD:
              if (newlineAfterOpen != config.clauseEndsLine()) {
                return false;
              }
              break;
            default:
              break;
            }
            save.restore();
            newlineAndIndent();
            node.unparse(SqlPrettyWriter.this, lprec, rprec);
            if (i + 1 < list.size()) {
              sep(false, sepOp.getName());
            }
          }
        }
      }
      return true;
    }

    /** Remembers the state of the current frame and writer.
     *
     * <p>You can call {@link #restore} to restore to that state, or just
     * continue. It is useful if you wish to re-try with different options
     * (for example, with lines wrapped). */
    class Save {
      final int bufLength;

      Save() {
        this.bufLength = buf.length();
      }

      void restore() {
        buf.setLength(bufLength);
      }
    }
  }

  /**
   * Helper class which exposes the get/set methods of an object as
   * properties.
   */
  private static class Bean {
    private final SqlPrettyWriter o;
    private final Map<String, Method> getterMethods = new HashMap<>();
    private final Map<String, Method> setterMethods = new HashMap<>();

    Bean(SqlPrettyWriter o) {
      this.o = o;

      // Figure out the getter/setter methods for each attribute.
      for (Method method : o.getClass().getMethods()) {
        if (method.getName().startsWith("set")
            && (method.getReturnType() == Void.class)
            && (method.getParameterCount() == 1)) {
          String attributeName =
              stripPrefix(
                  method.getName(),
                  3);
          setterMethods.put(attributeName, method);
        }
        if (method.getName().startsWith("get")
            && (method.getReturnType() != Void.class)
            && (method.getParameterCount() == 0)) {
          String attributeName =
              stripPrefix(
                  method.getName(),
                  3);
          getterMethods.put(attributeName, method);
        }
        if (method.getName().startsWith("is")
            && (method.getReturnType() == Boolean.class)
            && (method.getParameterCount() == 0)) {
          String attributeName =
              stripPrefix(
                  method.getName(),
                  2);
          getterMethods.put(attributeName, method);
        }
      }
    }

    private static String stripPrefix(String name, int offset) {
      return name.substring(offset, offset + 1).toLowerCase(Locale.ROOT)
          + name.substring(offset + 1);
    }

    public void set(String name, String value) {
      final Method method =
          requireNonNull(setterMethods.get(name),
              () -> "setter method " + name + " not found");
      try {
        method.invoke(o, value);
      } catch (IllegalAccessException | InvocationTargetException e) {
        throw Util.throwAsRuntime(Util.causeOrSelf(e));
      }
    }

    public @Nullable Object get(String name) {
      final Method method =
          requireNonNull(getterMethods.get(name),
              () -> "getter method " + name + " not found");
      try {
        return method.invoke(o);
      } catch (IllegalAccessException | InvocationTargetException e) {
        throw Util.throwAsRuntime(Util.causeOrSelf(e));
      }
    }

    public String[] getPropertyNames() {
      final Set<String> names = new HashSet<>();
      names.addAll(getterMethods.keySet());
      names.addAll(setterMethods.keySet());
      return names.toArray(new String[0]);
    }
  }

}