SqlBuilder.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.util;

import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.util.UnmodifiableArrayList;

import java.sql.Timestamp;
import java.util.List;

import static java.util.Objects.requireNonNull;

/**
 * Extension to {@link StringBuilder} for the purposes of creating SQL queries
 * and expressions.
 *
 * <p>Using this class helps to prevent SQL injection attacks, incorrectly
 * quoted identifiers and strings. These problems occur when you build SQL by
 * concatenating strings, and you forget to treat identifers and string literals
 * correctly. SqlBuilder has special methods for appending identifiers and
 * literals.
 */
public class SqlBuilder {
  private final StringBuilder buf;
  private final SqlDialect dialect;

  /**
   * Creates a SqlBuilder.
   *
   * @param dialect Dialect
   */
  public SqlBuilder(SqlDialect dialect) {
    this.dialect = requireNonNull(dialect, "dialect");
    this.buf = new StringBuilder();
  }

  /**
   * Creates a SqlBuilder with a given string.
   *
   * @param dialect Dialect
   * @param s       Initial contents of the buffer
   */
  public SqlBuilder(SqlDialect dialect, String s) {
    this.dialect = requireNonNull(dialect, "dialect");
    this.buf = new StringBuilder(s);
  }

  /**
   * Returns the dialect.
   *
   * @return dialect
   */
  public SqlDialect getDialect() {
    return dialect;
  }

  /**
   * Returns the length (character count).
   *
   * @return the length of the sequence of characters currently
   * represented by this object
   */
  public int length() {
    return buf.length();
  }

  /**
   * Clears the contents of the buffer.
   */
  public void clear() {
    buf.setLength(0);
  }

  /**
   * {@inheritDoc}
   *
   * <p>Returns the SQL string.
   *
   * @return SQL string
   * @see #getSql()
   */
  @Override public String toString() {
    return getSql();
  }

  /**
   * Returns the SQL.
   */
  public String getSql() {
    return buf.toString();
  }

  /**
   * Returns the SQL and clears the buffer.
   *
   * <p>Convenient if you are reusing the same SQL builder in a loop.
   */
  public String getSqlAndClear() {
    final String str = buf.toString();
    clear();
    return str;
  }

  /**
   * Appends a hygienic SQL string.
   *
   * @param s SQL string to append
   * @return This builder
   */
  public SqlBuilder append(SqlString s) {
    buf.append(s.getSql());
    return this;
  }

  /**
   * Appends a string, without any quoting.
   *
   * <p>Calls to this method are dubious.
   *
   * @param s String to append
   * @return This builder
   */
  public SqlBuilder append(String s) {
    buf.append(s);
    return this;
  }

  /**
   * Appends a character, without any quoting.
   *
   * @param c Character to append
   * @return This builder
   */
  public SqlBuilder append(char c) {
    buf.append(c);
    return this;
  }

  /**
   * Appends a number, per {@link StringBuilder#append(long)}.
   */
  public SqlBuilder append(long n) {
    buf.append(n);
    return this;
  }

  /**
   * Appends an identifier to this buffer, quoting accordingly.
   *
   * @param name Identifier
   * @return This builder
   */
  public SqlBuilder identifier(String name) {
    dialect.quoteIdentifier(buf, name);
    return this;
  }

  /**
   * Appends one or more identifiers to this buffer, quoting accordingly.
   *
   * @param names Varargs array of identifiers
   * @return This builder
   */
  public SqlBuilder identifier(String... names) {
    dialect.quoteIdentifier(buf, UnmodifiableArrayList.of(names));
    return this;
  }

  /**
   * Appends a compound identifier to this buffer, quoting accordingly.
   *
   * @param names Parts of a compound identifier
   * @return This builder
   */
  public SqlBuilder identifier(List<String> names) {
    dialect.quoteIdentifier(buf, names);
    return this;
  }

  /**
   * Returns the contents of this SQL buffer as a 'certified kocher' SQL
   * string.
   *
   * <p>Use this method in preference to {@link #toString()}. It indicates
   * that the SQL string has been constructed using good hygiene, and is
   * therefore less likely to contain SQL injection or badly quoted
   * identifiers or strings.
   *
   * @return Contents of this builder as a SQL string.
   */
  public SqlString toSqlString() {
    return new SqlString(dialect, buf.toString());
  }

  /**
   * Appends a string literal to this buffer.
   *
   * <p>For example, calling <code>literal(&quot;can't&quot;)</code>
   * would convert the buffer
   * <blockquote><code>SELECT </code></blockquote>
   * to
   * <blockquote><code>SELECT 'can''t'</code></blockquote>
   *
   * @param s String to append
   * @return This buffer
   */
  public SqlBuilder literal(String s) {
    buf.append(dialect.quoteStringLiteral(s));
    return this;
  }

  /**
   * Appends a timestamp literal to this buffer.
   *
   * @param timestamp Timestamp to append
   * @return This buffer
   */
  public SqlBuilder literal(Timestamp timestamp) {
    buf.append(dialect.quoteTimestampLiteral(timestamp));
    return this;
  }

  /**
   * Returns the index within this string of the first occurrence of the
   * specified substring.
   *
   * @see StringBuilder#indexOf(String)
   */
  public int indexOf(String str) {
    return buf.indexOf(str);
  }

  /**
   * Returns the index within this string of the first occurrence of the
   * specified substring, starting at the specified index.
   *
   * @see StringBuilder#indexOf(String, int)
   */
  public int indexOf(String str, int fromIndex) {
    return buf.indexOf(str, fromIndex);
  }

  /**
   * Inserts the string into this character sequence.
   *
   * @see StringBuilder#insert(int, String)
   */
  public SqlBuilder insert(int offset, String str) {
    buf.insert(offset, str);
    return this;
  }
}