blob: 88e7f5108b1afc66c043ca6f91d45ad054440e0e [file] [log] [blame]
/*
* 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;
import org.apache.calcite.config.NullCollation;
import org.apache.calcite.rel.RelFieldCollation;
import com.google.common.base.Preconditions;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Pattern;
/**
* <code>SqlDialect</code> encapsulates the differences between dialects of SQL.
*
* <p>It is used by classes such as {@link SqlWriter} and
* {@link org.apache.calcite.sql.util.SqlBuilder}.
*/
public class SqlDialect {
//~ Static fields/initializers ---------------------------------------------
/**
* A dialect useful for generating generic SQL. If you need to do something
* database-specific like quoting identifiers, don't rely on this dialect to
* do what you want.
*/
public static final SqlDialect DUMMY =
DatabaseProduct.UNKNOWN.getDialect();
/**
* A dialect useful for generating SQL which can be parsed by the
* Calcite parser, in particular quoting literals and identifiers. If you
* want a dialect that knows the full capabilities of the database, create
* one from a connection.
*/
public static final SqlDialect CALCITE =
DatabaseProduct.CALCITE.getDialect();
//~ Instance fields --------------------------------------------------------
private final String identifierQuoteString;
private final String identifierEndQuoteString;
private final String identifierEscapedQuote;
private final DatabaseProduct databaseProduct;
private final NullCollation nullCollation;
//~ Constructors -----------------------------------------------------------
/**
* Creates a <code>SqlDialect</code> from a DatabaseMetaData.
*
* <p>Does not maintain a reference to the DatabaseMetaData -- or, more
* importantly, to its {@link java.sql.Connection} -- after this call has
* returned.
*
* @param databaseMetaData used to determine which dialect of SQL to
* generate
*/
public static SqlDialect create(DatabaseMetaData databaseMetaData) {
String identifierQuoteString;
try {
identifierQuoteString = databaseMetaData.getIdentifierQuoteString();
} catch (SQLException e) {
throw FakeUtil.newInternal(e, "while quoting identifier");
}
String databaseProductName;
try {
databaseProductName = databaseMetaData.getDatabaseProductName();
} catch (SQLException e) {
throw FakeUtil.newInternal(e, "while detecting database product");
}
String databaseProductVersion;
try {
databaseProductVersion = databaseMetaData.getDatabaseProductVersion();
} catch (SQLException e) {
throw FakeUtil.newInternal(e, "while detecting database version");
}
final DatabaseProduct databaseProduct =
getProduct(databaseProductName, databaseProductVersion);
NullCollation nullCollation;
try {
if (databaseMetaData.nullsAreSortedAtEnd()) {
nullCollation = NullCollation.LAST;
} else if (databaseMetaData.nullsAreSortedAtStart()) {
nullCollation = NullCollation.FIRST;
} else if (databaseMetaData.nullsAreSortedLow()) {
nullCollation = NullCollation.LOW;
} else if (databaseMetaData.nullsAreSortedHigh()) {
nullCollation = NullCollation.HIGH;
} else {
throw new IllegalArgumentException("cannot deduce null collation");
}
} catch (SQLException e) {
throw new IllegalArgumentException("cannot deduce null collation", e);
}
return new SqlDialect(databaseProduct, databaseProductName,
identifierQuoteString, nullCollation);
}
/**
* Creates a SqlDialect.
*
* @param databaseProduct Database product; may be UNKNOWN, never null
* @param databaseProductName Database product name from JDBC driver
* @param identifierQuoteString String to quote identifiers. Null if quoting
* is not supported. If "[", close quote is
* deemed to be "]".
*/
@Deprecated // to be removed before 2.0
public SqlDialect(
DatabaseProduct databaseProduct,
String databaseProductName,
String identifierQuoteString) {
this(databaseProduct, databaseProductName, identifierQuoteString,
NullCollation.HIGH);
}
/**
* Creates a SqlDialect.
*
* @param databaseProduct Database product; may be UNKNOWN, never null
* @param databaseProductName Database product name from JDBC driver
* @param identifierQuoteString String to quote identifiers. Null if quoting
* is not supported. If "[", close quote is
* deemed to be "]".
* @param nullCollation Whether NULL values appear first or last
*/
public SqlDialect(
DatabaseProduct databaseProduct,
String databaseProductName,
String identifierQuoteString, NullCollation nullCollation) {
Preconditions.checkNotNull(this.nullCollation = nullCollation);
Preconditions.checkNotNull(databaseProductName);
this.databaseProduct = Preconditions.checkNotNull(databaseProduct);
if (identifierQuoteString != null) {
identifierQuoteString = identifierQuoteString.trim();
if (identifierQuoteString.equals("")) {
identifierQuoteString = null;
}
}
this.identifierQuoteString = identifierQuoteString;
this.identifierEndQuoteString =
identifierQuoteString == null
? null
: identifierQuoteString.equals("[")
? "]"
: identifierQuoteString;
this.identifierEscapedQuote =
identifierQuoteString == null
? null
: this.identifierEndQuoteString + this.identifierEndQuoteString;
}
//~ Methods ----------------------------------------------------------------
/**
* Converts a product name and version (per the JDBC driver) into a product
* enumeration.
*
* @param productName Product name
* @param productVersion Product version
* @return database product
*/
public static DatabaseProduct getProduct(
String productName,
String productVersion) {
final String upperProductName = productName.toUpperCase().trim();
switch (upperProductName) {
case "ACCESS":
return DatabaseProduct.ACCESS;
case "APACHE DERBY":
return DatabaseProduct.DERBY;
case "DBMS:CLOUDSCAPE":
return DatabaseProduct.DERBY;
case "HIVE":
return DatabaseProduct.HIVE;
case "INGRES":
return DatabaseProduct.INGRES;
case "INTERBASE":
return DatabaseProduct.INTERBASE;
case "LUCIDDB":
return DatabaseProduct.LUCIDDB;
case "ORACLE":
return DatabaseProduct.ORACLE;
case "PHOENIX":
return DatabaseProduct.PHOENIX;
case "MYSQL (INFOBRIGHT)":
return DatabaseProduct.INFOBRIGHT;
case "MYSQL":
return DatabaseProduct.MYSQL;
case "REDSHIFT":
return DatabaseProduct.REDSHIFT;
}
// Now the fuzzy matches.
if (productName.startsWith("DB2")) {
return DatabaseProduct.DB2;
} else if (upperProductName.contains("FIREBIRD")) {
return DatabaseProduct.FIREBIRD;
} else if (productName.startsWith("Informix")) {
return DatabaseProduct.INFORMIX;
} else if (upperProductName.contains("NETEZZA")) {
return DatabaseProduct.NETEZZA;
} else if (upperProductName.contains("PARACCEL")) {
return DatabaseProduct.PARACCEL;
} else if (productName.startsWith("HP Neoview")) {
return DatabaseProduct.NEOVIEW;
} else if (upperProductName.contains("POSTGRE")) {
return DatabaseProduct.POSTGRESQL;
} else if (upperProductName.contains("SQL SERVER")) {
return DatabaseProduct.MSSQL;
} else if (upperProductName.contains("SYBASE")) {
return DatabaseProduct.SYBASE;
} else if (upperProductName.contains("TERADATA")) {
return DatabaseProduct.TERADATA;
} else if (upperProductName.contains("HSQL")) {
return DatabaseProduct.HSQLDB;
} else if (upperProductName.contains("H2")) {
return DatabaseProduct.H2;
} else if (upperProductName.contains("VERTICA")) {
return DatabaseProduct.VERTICA;
} else {
return DatabaseProduct.UNKNOWN;
}
}
// -- detect various databases --
/**
* Encloses an identifier in quotation marks appropriate for the current SQL
* dialect.
*
* <p>For example, <code>quoteIdentifier("emp")</code> yields a string
* containing <code>"emp"</code> in Oracle, and a string containing <code>
* [emp]</code> in Access.
*
* @param val Identifier to quote
* @return Quoted identifier
*/
public String quoteIdentifier(String val) {
if (identifierQuoteString == null) {
return val; // quoting is not supported
}
String val2 =
val.replaceAll(
identifierEndQuoteString,
identifierEscapedQuote);
return identifierQuoteString + val2 + identifierEndQuoteString;
}
/**
* Encloses an identifier in quotation marks appropriate for the current SQL
* dialect, writing the result to a {@link StringBuilder}.
*
* <p>For example, <code>quoteIdentifier("emp")</code> yields a string
* containing <code>"emp"</code> in Oracle, and a string containing <code>
* [emp]</code> in Access.
*
* @param buf Buffer
* @param val Identifier to quote
* @return The buffer
*/
public StringBuilder quoteIdentifier(
StringBuilder buf,
String val) {
if (identifierQuoteString == null) {
buf.append(val); // quoting is not supported
return buf;
}
String val2 =
val.replaceAll(
identifierEndQuoteString,
identifierEscapedQuote);
buf.append(identifierQuoteString);
buf.append(val2);
buf.append(identifierEndQuoteString);
return buf;
}
/**
* Quotes a multi-part identifier.
*
* @param buf Buffer
* @param identifiers List of parts of the identifier to quote
* @return The buffer
*/
public StringBuilder quoteIdentifier(
StringBuilder buf,
List<String> identifiers) {
int i = 0;
for (String identifier : identifiers) {
if (i++ > 0) {
buf.append('.');
}
quoteIdentifier(buf, identifier);
}
return buf;
}
/**
* Returns whether a given identifier needs to be quoted.
*/
public boolean identifierNeedsToBeQuoted(String val) {
return !Pattern.compile("^[A-Z_$0-9]+").matcher(val).matches();
}
/**
* Converts a string into a string literal. For example, <code>can't
* run</code> becomes <code>'can''t run'</code>.
*/
public String quoteStringLiteral(String val) {
if (containsNonAscii(val)) {
final StringBuilder buf = new StringBuilder();
quoteStringLiteralUnicode(buf, val);
return buf.toString();
} else {
val = FakeUtil.replace(val, "'", "''");
return "'" + val + "'";
}
}
/**
* Returns whether the string contains any characters outside the
* comfortable 7-bit ASCII range (32 through 127).
*
* @param s String
* @return Whether string contains any non-7-bit-ASCII characters
*/
private static boolean containsNonAscii(String s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c < 32 || c >= 128) {
return true;
}
}
return false;
}
/**
* Converts a string into a unicode string literal. For example,
* <code>can't{tab}run\</code> becomes <code>u'can''t\0009run\\'</code>.
*/
public void quoteStringLiteralUnicode(StringBuilder buf, String val) {
buf.append("u&'");
for (int i = 0; i < val.length(); i++) {
char c = val.charAt(i);
if (c < 32 || c >= 128) {
buf.append('\\');
buf.append(HEXITS[(c >> 12) & 0xf]);
buf.append(HEXITS[(c >> 8) & 0xf]);
buf.append(HEXITS[(c >> 4) & 0xf]);
buf.append(HEXITS[c & 0xf]);
} else if (c == '\'' || c == '\\') {
buf.append(c);
buf.append(c);
} else {
buf.append(c);
}
}
buf.append("'");
}
private static final char[] HEXITS = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
};
/**
* Converts a string literal back into a string. For example, <code>'can''t
* run'</code> becomes <code>can't run</code>.
*/
public String unquoteStringLiteral(String val) {
if ((val != null)
&& (val.charAt(0) == '\'')
&& (val.charAt(val.length() - 1) == '\'')) {
if (val.length() > 2) {
val = FakeUtil.replace(val, "''", "'");
return val.substring(1, val.length() - 1);
} else {
// zero length string
return "";
}
}
return val;
}
protected boolean allowsAs() {
switch (databaseProduct) {
case ORACLE:
case HIVE:
return false;
default:
return true;
}
}
// -- behaviors --
protected boolean requiresAliasForFromItems() {
return getDatabaseProduct() == DatabaseProduct.POSTGRESQL;
}
/** Returns whether a qualified table in the FROM clause has an implicit alias
* which consists of just the table name.
*
* <p>For example, in {@link DatabaseProduct#ORACLE}
*
* <blockquote>SELECT * FROM sales.emp</blockquote>
*
* <p>is equivalent to
*
* <blockquote>SELECT * FROM sales.emp AS emp</blockquote>
*
* <p>and therefore
*
* <blockquote>SELECT emp.empno FROM sales.emp</blockquote>
*
* <p>is valid. But {@link DatabaseProduct#DB2} does not have an implicit
* alias, so the previous query it not valid; you need to write
*
* <blockquote>SELECT sales.emp.empno FROM sales.emp</blockquote>
*
* <p>Returns true for all databases except DB2.
*/
public boolean hasImplicitTableAlias() {
switch (databaseProduct) {
case DB2:
return false;
default:
return true;
}
}
/**
* Converts a timestamp to a SQL timestamp literal, e.g.
* {@code TIMESTAMP '2009-12-17 12:34:56'}.
*
* <p>Timestamp values do not have a time zone. We therefore interpret them
* as the number of milliseconds after the UTC epoch, and the formatted
* value is that time in UTC.
*
* <p>In particular,
*
* <blockquote><code>quoteTimestampLiteral(new Timestamp(0));</code>
* </blockquote>
*
* returns {@code TIMESTAMP '1970-01-01 00:00:00'}, regardless of the JVM's
* time zone.
*
* @param timestamp Timestamp
* @return SQL timestamp literal
*/
public String quoteTimestampLiteral(Timestamp timestamp) {
final SimpleDateFormat format =
new SimpleDateFormat(
"'TIMESTAMP' ''yyyy-MM-DD HH:mm:SS''");
format.setTimeZone(TimeZone.getTimeZone("GMT"));
return format.format(timestamp);
}
/**
* Returns the database this dialect belongs to,
* {@link SqlDialect.DatabaseProduct#UNKNOWN} if not known, never null.
*
* @return Database product
*/
public DatabaseProduct getDatabaseProduct() {
return databaseProduct;
}
/**
* Returns whether the dialect supports character set names as part of a
* data type, for instance {@code VARCHAR(30) CHARACTER SET `ISO-8859-1`}.
*/
public boolean supportsCharSet() {
switch (databaseProduct) {
case DB2:
case H2:
case HSQLDB:
case MYSQL:
case ORACLE:
case PHOENIX:
case POSTGRESQL:
return false;
default:
return true;
}
}
/**
* Returns whether the dialect supports OFFSET/FETCH clauses
* introduced by SQL:2008, for instance
* {@code OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY}.
* If false, we assume that the dialect supports the alternative syntax
* {@code LIMIT 20 OFFSET 10}.
*/
public boolean supportsOffsetFetch() {
switch (databaseProduct) {
case MYSQL:
case HIVE:
case REDSHIFT:
return false;
default:
return true;
}
}
/** Returns how NULL values are sorted if an ORDER BY item does not contain
* NULLS ASCENDING or NULLS DESCENDING. */
public NullCollation getNullCollation() {
return nullCollation;
}
/** Returns whether NULL values are sorted first or last, in this dialect,
* in an ORDER BY item of a given direction. */
public RelFieldCollation.NullDirection defaultNullDirection(
RelFieldCollation.Direction direction) {
switch (direction) {
case ASCENDING:
case STRICTLY_ASCENDING:
return getNullCollation().last(false)
? RelFieldCollation.NullDirection.LAST
: RelFieldCollation.NullDirection.FIRST;
case DESCENDING:
case STRICTLY_DESCENDING:
return getNullCollation().last(true)
? RelFieldCollation.NullDirection.LAST
: RelFieldCollation.NullDirection.FIRST;
default:
return RelFieldCollation.NullDirection.UNSPECIFIED;
}
}
/**
* A few utility functions copied from org.apache.calcite.util.Util. We have
* copied them because we wish to keep SqlDialect's dependencies to a
* minimum.
*/
public static class FakeUtil {
public static Error newInternal(Throwable e, String s) {
String message = "Internal error: \u0000" + s;
AssertionError ae = new AssertionError(message);
ae.initCause(e);
return ae;
}
/**
* Replaces every occurrence of <code>find</code> in <code>s</code> with
* <code>replace</code>.
*/
public static String replace(
String s,
String find,
String replace) {
// let's be optimistic
int found = s.indexOf(find);
if (found == -1) {
return s;
}
StringBuilder sb = new StringBuilder(s.length());
int start = 0;
for (;;) {
for (; start < found; start++) {
sb.append(s.charAt(start));
}
if (found == s.length()) {
break;
}
sb.append(replace);
start += find.length();
found = s.indexOf(find, start);
if (found == -1) {
found = s.length();
}
}
return sb.toString();
}
}
/**
* Rough list of flavors of database.
*
* <p>These values cannot help you distinguish between features that exist
* in different versions or ports of a database, but they are sufficient
* to drive a {@code switch} statement if behavior is broadly different
* between say, MySQL and Oracle.
*
* <p>If possible, you should not refer to particular database at all; write
* extend the dialect to describe the particular capability, for example,
* whether the database allows expressions to appear in the GROUP BY clause.
*/
public enum DatabaseProduct {
ACCESS("Access", "\"", NullCollation.HIGH),
CALCITE("Apache Calcite", "\"", NullCollation.HIGH),
MSSQL("Microsoft SQL Server", "[", NullCollation.HIGH),
MYSQL("MySQL", "`", NullCollation.HIGH),
ORACLE("Oracle", "\"", NullCollation.HIGH),
DERBY("Apache Derby", null, NullCollation.HIGH),
DB2("IBM DB2", null, NullCollation.HIGH),
FIREBIRD("Firebird", null, NullCollation.HIGH),
H2("H2", "\"", NullCollation.HIGH),
HIVE("Apache Hive", null, NullCollation.HIGH),
INFORMIX("Informix", null, NullCollation.HIGH),
INGRES("Ingres", null, NullCollation.HIGH),
LUCIDDB("LucidDB", "\"", NullCollation.HIGH),
INTERBASE("Interbase", null, NullCollation.HIGH),
PHOENIX("Phoenix", "\"", NullCollation.HIGH),
POSTGRESQL("PostgreSQL", "\"", NullCollation.HIGH),
NETEZZA("Netezza", "\"", NullCollation.HIGH),
INFOBRIGHT("Infobright", "`", NullCollation.HIGH),
NEOVIEW("Neoview", null, NullCollation.HIGH),
SYBASE("Sybase", null, NullCollation.HIGH),
TERADATA("Teradata", "\"", NullCollation.HIGH),
HSQLDB("Hsqldb", null, NullCollation.HIGH),
VERTICA("Vertica", "\"", NullCollation.HIGH),
SQLSTREAM("SQLstream", "\"", NullCollation.HIGH),
/** Paraccel, now called Actian Matrix. Redshift is based on this, so
* presumably the dialect capabilities are similar. */
PARACCEL("Paraccel", "\"", NullCollation.HIGH),
REDSHIFT("Redshift", "\"", NullCollation.HIGH),
/**
* Placeholder for the unknown database.
*
* <p>Its dialect is useful for generating generic SQL. If you need to
* do something database-specific like quoting identifiers, don't rely
* on this dialect to do what you want.
*/
UNKNOWN("Unknown", "`", NullCollation.HIGH);
private SqlDialect dialect = null;
private String databaseProductName;
private String quoteString;
private final NullCollation nullCollation;
DatabaseProduct(String databaseProductName, String quoteString,
NullCollation nullCollation) {
this.databaseProductName = databaseProductName;
this.quoteString = quoteString;
this.nullCollation = nullCollation;
}
/**
* Returns a dummy dialect for this database.
*
* <p>Since databases have many versions and flavors, this dummy dialect
* is at best an approximation. If you want exact information, better to
* use a dialect created from an actual connection's metadata
* (see {@link SqlDialect#create(java.sql.DatabaseMetaData)}).
*
* @return Dialect representing lowest-common-demoninator behavior for
* all versions of this database
*/
public SqlDialect getDialect() {
if (dialect == null) {
dialect =
new SqlDialect(this, databaseProductName, quoteString,
nullCollation);
}
return dialect;
}
}
}
// End SqlDialect.java