/*
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// You must accept the terms of that agreement to use this software.
//
// Copyright (c) 2002-2014 Pentaho Corporation..  All rights reserved.
*/
package mondrian.spi.impl;

import mondrian.olap.MondrianProperties;
import mondrian.olap.Util;
import mondrian.rolap.SqlStatement;
import mondrian.spi.Dialect;
import mondrian.spi.Dialect.DatabaseProduct;
import mondrian.spi.StatisticsProvider;
import mondrian.util.ClassResolver;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.sql.*;
import java.sql.Date;
import java.util.*;
import java.util.regex.Pattern;

/**
 * Implementation of {@link Dialect} based on a JDBC connection and metadata.
 *
 * <p>If you are writing a class for a specific database dialect, we recommend
 * that you use this as a base class, so your dialect class will be
 * forwards-compatible. If methods are added to {@link Dialect} in future
 * revisions, default implementations of those methods will be added to this
 * class.</p>
 *
 * <p>Mondrian uses JdbcDialectImpl as a fallback if it cannot find a more
 * specific dialect. JdbcDialectImpl reads properties from the JDBC driver's
 * metadata, so can deduce some of the dialect's behavior.</p>
 *
 * @author jhyde
 * @since Oct 10, 2008
 */
public class JdbcDialectImpl implements Dialect {
    private static final Log LOGGER = LogFactory.getLog(JdbcDialectImpl.class);

    /**
     * String used to quote identifiers.
     */
    private final String quoteIdentifierString;

    /**
     * Product name per JDBC driver.
     */
    private final String productName;

    /**
     * Product version per JDBC driver.
     */
    protected final String productVersion;

    /**
     * Supported result set types.
     */
    private final Set<List<Integer>> supportedResultSetTypes;

    /**
     * Whether database is read-only
     */
    private final boolean readOnly;

    /**
     * Maximum column name length
     */
    private final int maxColumnNameLength;

    /**
     * Indicates whether the database allows selection of columns
     * not listed in the group by clause.
     */
    protected boolean permitsSelectNotInGroupBy;

    /**
     * Major database product (or null if product is not a common one)
     */
    protected final DatabaseProduct databaseProduct;

    /**
     * List of statistics providers.
     */
    private final List<StatisticsProvider> statisticsProviders;

    private static final int[] RESULT_SET_TYPE_VALUES = {
        ResultSet.TYPE_FORWARD_ONLY,
        ResultSet.TYPE_SCROLL_INSENSITIVE,
        ResultSet.TYPE_SCROLL_SENSITIVE};

    private static final int[] CONCURRENCY_VALUES = {
        ResultSet.CONCUR_READ_ONLY,
        ResultSet.CONCUR_UPDATABLE};

    /**
     * The size required to add quotes around a string - this ought to be
     * large enough to prevent a reallocation.
     */
    private static final int SINGLE_QUOTE_SIZE = 10;
    /**
     * Two strings are quoted and the character '.' is placed between them.
     */
    private static final int DOUBLE_QUOTE_SIZE = 2 * SINGLE_QUOTE_SIZE + 1;

    /**
     * The default mapping of java.sql.Types to SqlStatement.Type
     */
    private static final Map<Types, SqlStatement.Type> DEFAULT_TYPE_MAP;
    static {
        Map typeMapInitial = new HashMap<Types, SqlStatement.Type>();
        typeMapInitial.put(Types.SMALLINT, SqlStatement.Type.INT);
        typeMapInitial.put(Types.INTEGER, SqlStatement.Type.INT);
        typeMapInitial.put(Types.BOOLEAN, SqlStatement.Type.INT);
        typeMapInitial.put(Types.DOUBLE, SqlStatement.Type.DOUBLE);
        typeMapInitial.put(Types.FLOAT, SqlStatement.Type.DOUBLE);
        typeMapInitial.put(Types.BIGINT, SqlStatement.Type.DOUBLE);

        DEFAULT_TYPE_MAP = Collections.unmodifiableMap(typeMapInitial);
    }

    /**
     * Creates a JdbcDialectImpl.
     *
     * <p>To prevent connection leaks, this constructor does not hold a
     * reference to the connection after the call returns. It makes a copy of
     * everything useful during the call.  Derived classes must do the
     * same.</p>
     *
     * @param connection Connection
     *
     * @throws java.sql.SQLException on error
     */
    public JdbcDialectImpl(
        Connection connection)
        throws SQLException
    {
        final DatabaseMetaData metaData = connection.getMetaData();
        this.quoteIdentifierString = deduceIdentifierQuoteString(metaData);
        this.productName = deduceProductName(metaData);
        this.productVersion = deduceProductVersion(metaData);
        this.supportedResultSetTypes = deduceSupportedResultSetStyles(metaData);
        this.readOnly = deduceReadOnly(metaData);
        this.maxColumnNameLength = deduceMaxColumnNameLength(metaData);
        this.databaseProduct =
            getProduct(this.productName, this.productVersion);
        this.permitsSelectNotInGroupBy =
            deduceSupportsSelectNotInGroupBy(connection);
        this.statisticsProviders = computeStatisticsProviders();
    }

    public JdbcDialectImpl() {
        quoteIdentifierString = "";
        productName = "";
        productVersion = "";
        supportedResultSetTypes = null;
        readOnly = true;
        maxColumnNameLength = 0;
        databaseProduct = null;
        permitsSelectNotInGroupBy = true;
        statisticsProviders = null;
    }

    public DatabaseProduct getDatabaseProduct() {
        return databaseProduct;
    }

    public void appendHintsAfterFromClause(
        StringBuilder buf,
        Map<String, String> hints)
    {
        // Hints are always dialect-specific, so the default is a no-op
    }

    public boolean allowsDialectSharing() {
        return true;
    }

    protected int deduceMaxColumnNameLength(DatabaseMetaData databaseMetaData) {
        try {
            return databaseMetaData.getMaxColumnNameLength();
        } catch (SQLException e) {
            throw Util.newInternal(
                e,
                "while detecting maxColumnNameLength");
        }
    }

    protected boolean deduceReadOnly(DatabaseMetaData databaseMetaData) {
        try {
            return databaseMetaData.isReadOnly();
        } catch (SQLException e) {
            throw Util.newInternal(
                e,
                "while detecting isReadOnly");
        }
    }

    protected String deduceProductName(DatabaseMetaData databaseMetaData) {
        try {
            return databaseMetaData.getDatabaseProductName();
        } catch (SQLException e) {
            throw Util.newInternal(e, "while detecting database product");
        }
    }

    protected String deduceIdentifierQuoteString(
        DatabaseMetaData databaseMetaData)
    {
        try {
            final String quoteIdentifierString =
                databaseMetaData.getIdentifierQuoteString();
            return "".equals(quoteIdentifierString)
                // quoting not supported
                ? null
                : quoteIdentifierString;
        } catch (SQLException e) {
            throw Util.newInternal(e, "while quoting identifier");
        }
    }

    protected String deduceProductVersion(DatabaseMetaData databaseMetaData) {
        String productVersion;
        try {
            productVersion = databaseMetaData.getDatabaseProductVersion();
        } catch (SQLException e11) {
            throw Util.newInternal(
                e11,
                "while detecting database product version");
        }
        return productVersion;
    }

    protected Set<List<Integer>> deduceSupportedResultSetStyles(
        DatabaseMetaData databaseMetaData)
    {
        Set<List<Integer>> supports = new HashSet<List<Integer>>();
        for (int type : RESULT_SET_TYPE_VALUES) {
            for (int concurrency : CONCURRENCY_VALUES) {
                try {
                    if (databaseMetaData.supportsResultSetConcurrency(
                            type, concurrency))
                    {
                        String driverName =
                            databaseMetaData.getDriverName();
                        if (type != ResultSet.TYPE_FORWARD_ONLY
                            && driverName.equals(
                                "JDBC-ODBC Bridge (odbcjt32.dll)"))
                        {
                            // In JDK 1.6, the Jdbc-Odbc bridge announces
                            // that it can handle TYPE_SCROLL_INSENSITIVE
                            // but it does so by generating a 'COUNT(*)'
                            // query, and this query is invalid if the query
                            // contains a single-quote. So, override the
                            // driver.
                            continue;
                        }
                        supports.add(
                            new ArrayList<Integer>(
                                Arrays.asList(type, concurrency)));
                    }
                } catch (SQLException e) {
                    // DB2 throws "com.ibm.db2.jcc.b.SqlException: Unknown type
                    // or Concurrency" for certain values of type/concurrency.
                    // No harm in interpreting all such exceptions as 'this
                    // database does not support this type/concurrency
                    // combination'.
                    Util.discard(e);
                }
            }
        }
        return supports;
    }

     /**
      * <p>Detects whether the database is configured to permit queries
      * that include columns in the SELECT that are not also in the GROUP BY.
      * MySQL is an example of one that does, though this is configurable.</p>
      *
      * <p>The expectation is that this will not change while Mondrian is
      * running, though some databases (MySQL) allow changing it on the fly.</p>
      *
      * @param conn The database connection
      * @return Whether the feature is enabled.
      * @throws SQLException on error
      */
    protected boolean deduceSupportsSelectNotInGroupBy(Connection conn)
        throws SQLException
    {
        // Most simply don't support it
        return false;
    }

    public String toUpper(String expr) {
        return "UPPER(" + expr + ")";
    }

    public String caseWhenElse(String cond, String thenExpr, String elseExpr) {
        return "CASE WHEN " + cond + " THEN " + thenExpr + " ELSE " + elseExpr
            + " END";
    }

    public String quoteIdentifier(final String val) {
        int size = val.length() + SINGLE_QUOTE_SIZE;
        StringBuilder buf = new StringBuilder(size);

        quoteIdentifier(val, buf);

        return buf.toString();
    }

    public void quoteIdentifier(final String val, final StringBuilder buf) {
        String q = getQuoteIdentifierString();
        if (q == null) {
            // quoting is not supported
            buf.append(val);
            return;
        }
        // if the value is already quoted, do nothing
        //  if not, then check for a dot qualified expression
        //  like "owner.table".
        //  In that case, prefix the single parts separately.
        if (val.startsWith(q) && val.endsWith(q)) {
            // already quoted - nothing to do
            buf.append(val);
            return;
        }

        int k = val.indexOf('.');
        if (k > 0) {
            // qualified
            String val1 = Util.replace(val.substring(0, k), q, q + q);
            String val2 = Util.replace(val.substring(k + 1), q, q + q);
            buf.append(q);
            buf.append(val1);
            buf.append(q);
            buf.append(".");
            buf.append(q);
            buf.append(val2);
            buf.append(q);

        } else {
            // not Qualified
            String val2 = Util.replace(val, q, q + q);
            buf.append(q);
            buf.append(val2);
            buf.append(q);
        }
    }

    public String quoteIdentifier(final String qual, final String name) {
        // We know if the qalifier is null, then only the name is going
        // to be quoted.
        int size = name.length()
            + ((qual == null)
                ? SINGLE_QUOTE_SIZE
                : (qual.length() + DOUBLE_QUOTE_SIZE));
        StringBuilder buf = new StringBuilder(size);

        quoteIdentifier(buf, qual, name);

        return buf.toString();
    }

    public void quoteIdentifier(
        final StringBuilder buf,
        final String... names)
    {
        int nonNullNameCount = 0;
        for (String name : names) {
            if (name == null) {
                continue;
            }
            if (nonNullNameCount > 0) {
                buf.append('.');
            }
            assert name.length() > 0
                : "name should probably be null, not empty";
            quoteIdentifier(name, buf);
            ++nonNullNameCount;
        }
    }

    public String getQuoteIdentifierString() {
        return quoteIdentifierString;
    }

    public void quoteStringLiteral(
        StringBuilder buf,
        String s)
    {
        Util.singleQuoteString(s, buf);
    }

    public void quoteNumericLiteral(
        StringBuilder buf,
        String value)
    {
        buf.append(value);
    }

    public void quoteBooleanLiteral(StringBuilder buf, String value) {
        // NOTE jvs 1-Jan-2007:  See quoteDateLiteral for explanation.
        // In addition, note that we leave out UNKNOWN (even though
        // it is a valid SQL:2003 literal) because it's really
        // NULL in disguise, and NULL is always treated specially.
        if (!value.equalsIgnoreCase("TRUE")
            && !(value.equalsIgnoreCase("FALSE")))
        {
            throw new NumberFormatException(
                "Illegal BOOLEAN literal:  " + value);
        }
        buf.append(value);
    }

    public void quoteDateLiteral(StringBuilder buf, String value) {
        // NOTE jvs 1-Jan-2007: Check that the supplied literal is in valid
        // SQL:2003 date format.  A hack in
        // RolapSchemaReader.lookupMemberChildByName looks for
        // NumberFormatException to suppress it, so that is why
        // we convert the exception here.
        Date date;
        try {
              date = Date.valueOf(value);
        } catch (IllegalArgumentException ex) {
            // MONDRIAN-2038
            try {
                date = new Date(Timestamp.valueOf(value).getTime());
            } catch (IllegalArgumentException ex2) {
                throw new NumberFormatException(
                    "Illegal DATE literal:  " + value);
            }
        }
        quoteDateLiteral(buf, date.toString(), date);
    }

    /**
     * Helper method for {@link #quoteDateLiteral(StringBuilder, String)}.
     *
     * @param buf Buffer to append to
     * @param value Value as string
     * @param date Value as date
     */
    protected void quoteDateLiteral(
        StringBuilder buf,
        String value,
        Date date)
    {
        // SQL:2003 date format: DATE '2008-01-23'.
        buf.append("DATE ");
        Util.singleQuoteString(value, buf);
    }

    public void quoteTimeLiteral(StringBuilder buf, String value) {
        // NOTE jvs 1-Jan-2007:  See quoteDateLiteral for explanation.
        try {
            Time.valueOf(value);
        } catch (IllegalArgumentException ex) {
            throw new NumberFormatException(
                "Illegal TIME literal:  " + value);
        }
        buf.append("TIME ");
        Util.singleQuoteString(value, buf);
    }

    public void quoteTimestampLiteral(
        StringBuilder buf,
        String value)
    {
        // NOTE jvs 1-Jan-2007:  See quoteTimestampLiteral for explanation.
        try {
            Timestamp.valueOf(value);
        } catch (IllegalArgumentException ex) {
            throw new NumberFormatException(
                "Illegal TIMESTAMP literal:  " + value);
        }
        buf.append("TIMESTAMP ");
        Util.singleQuoteString(value, buf);
    }

    public boolean requiresAliasForFromQuery() {
        return false;
    }

    public boolean allowsAs() {
        return true;
    }

    public boolean allowsFromQuery() {
        return true;
    }

    public boolean allowsCompoundCountDistinct() {
        return false;
    }

    public boolean allowsCountDistinct() {
        return true;
    }

    public boolean allowsMultipleCountDistinct() {
        return allowsCountDistinct();
    }

    public boolean allowsMultipleDistinctSqlMeasures() {
        return allowsMultipleCountDistinct();
    }

    public boolean allowsCountDistinctWithOtherAggs() {
      return allowsCountDistinct();
    }

    public String generateInline(
        List<String> columnNames,
        List<String> columnTypes,
        List<String[]> valueList)
    {
        return generateInlineForAnsi(
            "t", columnNames, columnTypes, valueList, false);
    }

    /**
     * Generic algorithm to generate inline values list,
     * using an optional FROM clause, specified by the caller of this
     * method, appropriate to the dialect of SQL.
     *
     * @param columnNames Column names
     * @param columnTypes Column types
     * @param valueList List rows
     * @param fromClause FROM clause, or null
     * @param cast Whether to cast the values in the first row
     * @return Expression that returns the given values
     */
    protected String generateInlineGeneric(
        List<String> columnNames,
        List<String> columnTypes,
        List<String[]> valueList,
        String fromClause,
        boolean cast)
    {
        final StringBuilder buf = new StringBuilder();
        int columnCount = columnNames.size();
        assert columnTypes.size() == columnCount;

        // Some databases, e.g. Teradata, derives datatype from value of column
        // in first row, and truncates subsequent rows. Therefore, we need to
        // cast every value to the correct length. Figure out the maximum length
        // now.
        Integer[] maxLengths = new Integer[columnCount];
        if (cast) {
            for (int i = 0; i < columnTypes.size(); i++) {
                String columnType = columnTypes.get(i);
                Datatype datatype = Datatype.valueOf(columnType);
                if (datatype == Datatype.String) {
                    int maxLen = -1;
                    for (String[] strings : valueList) {
                        if (strings[i] != null
                            && strings[i].length() > maxLen)
                        {
                            maxLen = strings[i].length();
                        }
                    }
                    maxLengths[i] = maxLen;
                }
            }
        }

        for (int i = 0; i < valueList.size(); i++) {
            if (i > 0) {
                buf.append(" union all ");
            }
            String[] values = valueList.get(i);
            buf.append("select ");
            for (int j = 0; j < values.length; j++) {
                String value = values[j];
                if (j > 0) {
                    buf.append(", ");
                }
                final String columnType = columnTypes.get(j);
                final String columnName = columnNames.get(j);
                Datatype datatype = Datatype.valueOf(columnType);
                final Integer maxLength = maxLengths[j];
                if (maxLength != null) {
                    // Generate CAST for Teradata.
                    buf.append("CAST(");
                    quote(buf, value, datatype);
                    buf.append(" AS VARCHAR(").append(maxLength).append("))");
                } else {
                    quote(buf, value, datatype);
                }
                if (allowsAs()) {
                    buf.append(" as ");
                } else {
                    buf.append(' ');
                }
                quoteIdentifier(columnName, buf);
            }
            if (fromClause != null) {
                buf.append(fromClause);
            }
        }
        return buf.toString();
    }

    /**
     * Generates inline values list using ANSI 'VALUES' syntax.
     * For example,
     *
     * <blockquote><code>SELECT * FROM
     *   (VALUES (1, 'a'), (2, 'b')) AS t(x, y)</code></blockquote>
     *
     * <p>If NULL values are present, we use a CAST to ensure that they
     * have the same type as other columns:
     *
     * <blockquote><code>SELECT * FROM
     * (VALUES (1, 'a'), (2, CASE(NULL AS VARCHAR(1)))) AS t(x, y)
     * </code></blockquote>
     *
     * <p>This syntax is known to work on Derby, but not Oracle 10 or
     * Access.
     *
     * @param alias Table alias
     * @param columnNames Column names
     * @param columnTypes Column types
     * @param valueList List rows
     * @param cast Whether to generate casts
     * @return Expression that returns the given values
     */
    public String generateInlineForAnsi(
        String alias,
        List<String> columnNames,
        List<String> columnTypes,
        List<String[]> valueList,
        boolean cast)
    {
        final StringBuilder buf = new StringBuilder();
        buf.append("SELECT * FROM (VALUES ");
        // Derby pads out strings to a common length, so we cast the
        // string values to avoid this.  Determine the cast type for each
        // column.
        String[] castTypes = null;
        if (cast) {
            castTypes = new String[columnNames.size()];
            for (int i = 0; i < columnNames.size(); i++) {
                String columnType = columnTypes.get(i);
                if (columnType.equals("String")) {
                    castTypes[i] =
                        guessSqlType(columnType, valueList, i);
                }
            }
        }
        for (int i = 0; i < valueList.size(); i++) {
            if (i > 0) {
                buf.append(", ");
            }
            String[] values = valueList.get(i);
            buf.append("(");
            for (int j = 0; j < values.length; j++) {
                String value = values[j];
                if (j > 0) {
                    buf.append(", ");
                }
                final String columnType = columnTypes.get(j);
                Datatype datatype = Datatype.valueOf(columnType);
                if (value == null) {
                    String sqlType =
                        guessSqlType(columnType, valueList, j);
                    buf.append("CAST(NULL AS ")
                        .append(sqlType)
                        .append(")");
                } else if (cast && castTypes[j] != null) {
                    buf.append("CAST(");
                    quote(buf, value, datatype);
                    buf.append(" AS ")
                        .append(castTypes[j])
                        .append(")");
                } else {
                    quote(buf, value, datatype);
                }
            }
            buf.append(")");
        }
        buf.append(") AS ");
        quoteIdentifier(alias, buf);
        buf.append(" (");
        for (int j = 0; j < columnNames.size(); j++) {
            final String columnName = columnNames.get(j);
            if (j > 0) {
                buf.append(", ");
            }
            quoteIdentifier(columnName, buf);
        }
        buf.append(")");
        return buf.toString();
    }

    public boolean needsExponent(Object value, String valueString) {
        return false;
    }

    public void quote(
        StringBuilder buf,
        Object value,
        Datatype datatype)
    {
        if (value == null) {
            buf.append("null");
        } else {
            String valueString = value.toString();
            if (needsExponent(value, valueString)) {
                valueString += "E0";
            }
            datatype.quoteValue(buf, this, valueString);
        }
    }

    /**
     * Guesses the type of a column based upon (a) its basic type,
     * (b) a list of values.
     *
     * @param basicType Basic type
     * @param valueList Value list
     * @param column Column ordinal
     * @return SQL type
     */
    private static String guessSqlType(
        String basicType,
        List<String[]> valueList,
        int column)
    {
        if (basicType.equals("String")) {
            int maxLen = 1;
            for (String[] values : valueList) {
                final String value = values[column];
                if (value == null) {
                    continue;
                }
                maxLen = Math.max(maxLen, value.length());
            }
            return "VARCHAR(" + maxLen + ")";
        } else {
            return "INTEGER";
        }
    }

    public boolean allowsDdl() {
        return !readOnly;
    }

    public String generateOrderItem(
        String expr,
        boolean nullable,
        boolean ascending,
        boolean collateNullsLast)
    {
        if (nullable) {
            return generateOrderByNulls(expr, ascending, collateNullsLast);
        } else {
            if (ascending) {
                return expr + " ASC";
            } else {
                return expr + " DESC";
            }
        }
    }

    /**
     * Generates SQL to force null values to collate last.
     *
     * <p>This default implementation makes use of the ANSI
     * SQL 1999 CASE-WHEN-THEN-ELSE in conjunction with IS NULL
     * syntax. The resulting SQL will look something like this:
     *
     * <p><code>CASE WHEN "expr" IS NULL THEN 0 ELSE 1 END</code>
     *
     * <p>You can override this method for a particular database
     * to use something more efficient, like ISNULL().
     *
     * <p>ANSI SQL provides the syntax "ASC/DESC NULLS LAST" and
     * "ASC/DESC NULLS FIRST". If your database supports the ANSI
     * syntax, implement this method by calling
     * {@link #generateOrderByNullsAnsi}.
     *
     * <p>This method is only called from
     * {@link #generateOrderItem(String, boolean, boolean, boolean)}.
     * Some dialects override that method and therefore never call
     * this method.
     *
     * @param expr Expression.
     * @param ascending Whether ascending.
     * @param collateNullsLast Whether nulls should appear first or last.
     * @return Expression to force null values to collate last or first.
     */
    protected String generateOrderByNulls(
        String expr,
        boolean ascending,
        boolean collateNullsLast)
    {
        if (collateNullsLast) {
            if (ascending) {
                return
                    "CASE WHEN " + expr + " IS NULL THEN 1 ELSE 0 END, " + expr
                    + " ASC";
            } else {
                return
                    "CASE WHEN " + expr + " IS NULL THEN 1 ELSE 0 END, " + expr
                    + " DESC";
            }
        } else {
            if (ascending) {
                return
                    "CASE WHEN " + expr + " IS NULL THEN 0 ELSE 1 END, " + expr
                    + " ASC";
            } else {
                return
                    "CASE WHEN " + expr + " IS NULL THEN 0 ELSE 1 END, " + expr
                    + " DESC";
            }
        }
    }

    /**
     * Implementation for the {@link #generateOrderByNulls} method
     * that uses the ANSI syntax "expr direction NULLS LAST"
     * and "expr direction NULLS FIRST".
     *
     * @param expr Expression
     * @param ascending Whether ascending
     * @param collateNullsLast Whether nulls should appear first or last.
     * @return Expression "expr direction NULLS LAST"
     */
    protected final String generateOrderByNullsAnsi(
        String expr,
        boolean ascending,
        boolean collateNullsLast)
    {
        if (collateNullsLast) {
            return expr + (ascending ? " ASC" : " DESC") + " NULLS LAST";
        } else {
            return expr + (ascending ? " ASC" : " DESC") + " NULLS FIRST";
        }
    }

    public boolean supportsGroupByExpressions() {
        return true;
    }

    public boolean allowsSelectNotInGroupBy() {
        return permitsSelectNotInGroupBy;
    }

    public boolean allowsJoinOn() {
        return false;
    }

    public boolean supportsGroupingSets() {
        return false;
    }

    public boolean supportsUnlimitedValueList() {
        return false;
    }

    public boolean requiresGroupByAlias() {
        return false;
    }

    public boolean requiresOrderByAlias() {
        return false;
    }

    public boolean requiresHavingAlias() {
        return false;
    }

    public boolean allowsOrderByAlias() {
        return requiresOrderByAlias();
    }

    public boolean requiresUnionOrderByOrdinal() {
        return true;
    }

    public boolean requiresUnionOrderByExprToBeInSelectClause() {
        return true;
    }

    public boolean supportsMultiValueInExpr() {
        return false;
    }

    public boolean supportsResultSetConcurrency(
        int type,
        int concurrency)
    {
        return supportedResultSetTypes.contains(
            Arrays.asList(type, concurrency));
    }

    public String toString() {
        return productName;
    }

    public int getMaxColumnNameLength() {
        return maxColumnNameLength;
    }

    public boolean allowsRegularExpressionInWhereClause() {
        return false;
    }

    public String generateCountExpression(String exp) {
        return exp;
    }

    public String generateRegularExpression(
        String source,
        String javaRegExp)
    {
        return null;
    }

    public List<StatisticsProvider> getStatisticsProviders() {
        return statisticsProviders;
    }

    public SqlStatement.Type getType(
        ResultSetMetaData metaData, int columnIndex)
        throws SQLException
    {
        final int columnType = metaData.getColumnType(columnIndex + 1);

        SqlStatement.Type internalType = null;
        if (columnType != Types.NUMERIC && columnType != Types.DECIMAL) {
            internalType = DEFAULT_TYPE_MAP.get(columnType);
        } else {
            final int precision = metaData.getPrecision(columnIndex + 1);
            final int scale = metaData.getScale(columnIndex + 1);
            if (scale == 0 && precision <= 9) {
                // An int (up to 2^31 = 2.1B) can hold any NUMBER(10, 0) value
                // (up to 10^9 = 1B).
                internalType = SqlStatement.Type.INT;
            } else {
                internalType = SqlStatement.Type.DOUBLE;
            }
        }
        internalType =  internalType == null ? SqlStatement.Type.OBJECT
            : internalType;
        logTypeInfo(metaData, columnIndex, internalType);
        return internalType;
    }


    void logTypeInfo(
        ResultSetMetaData metaData, int columnIndex,
        SqlStatement.Type internalType)
    throws SQLException
    {
        if (LOGGER.isDebugEnabled()) {
            final int columnType = metaData.getColumnType(columnIndex + 1);
            final int precision = metaData.getPrecision(columnIndex + 1);
            final int scale = metaData.getScale(columnIndex + 1);
            final String columnName = metaData.getColumnName(columnIndex + 1);
            LOGGER.debug(
                "JdbcDialectImpl.getType "
                + "Dialect- " + this.getDatabaseProduct()
                + ", Column-"
                + columnName
                + " is of internal type "
                + internalType
                + ". JDBC type was "
                + columnType
                + ".  Column precision=" + precision
                + ".  Column scale=" + scale);
        }
    }

    protected List<StatisticsProvider> computeStatisticsProviders() {
        List<String> names = getStatisticsProviderNames();
        if (names == null) {
            return Collections.<StatisticsProvider>singletonList(
                new SqlStatisticsProvider());
        }
        final List<StatisticsProvider> providerList =
            new ArrayList<StatisticsProvider>();
        for (String name : names) {
            try {
                StatisticsProvider provider =
                    ClassResolver.INSTANCE.instantiateSafe(name);
                providerList.add(provider);
            } catch (Exception e) {
                LOGGER.info(
                    "Error instantiating statistics provider (class=" + name
                    + ")",
                    e);
            }
        }
        return providerList;
    }

    private List<String> getStatisticsProviderNames() {
        // Dialect-specific path, e.g. "mondrian.statistics.providers.MYSQL"
        final String path =
            MondrianProperties.instance().StatisticsProviders.getPath()
            + "."
            + getDatabaseProduct().name();
        String nameList = MondrianProperties.instance().getProperty(path);
        if (nameList != null && nameList.length() > 0) {
            return Arrays.asList(nameList.split(","));
        }

        // Generic property, "mondrian.statistics.providers"
        nameList = MondrianProperties.instance().StatisticsProviders.get();
        if (nameList != null && nameList.length() > 0) {
            return Arrays.asList(nameList.split(","));
        }
        return null;
    }

    /**
     * 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();
        if (productName.equals("ACCESS")) {
            return DatabaseProduct.ACCESS;
        } else if (upperProductName.trim().equals("APACHE DERBY")) {
            return DatabaseProduct.DERBY;
        } else if (upperProductName.trim().equals("DBMS:CLOUDSCAPE")) {
            return DatabaseProduct.DERBY;
        } else if (productName.startsWith("DB2")) {
            if (productName.startsWith("DB2 UDB for AS/400")) {
                // TB "04.03.0000 V4R3m0"
                // this version cannot handle subqueries and is considered "old"
                // DEUKA "05.01.0000 V5R1m0" is ok
                String[] version_release = productVersion.split("\\.", 3);
/*
                if (version_release.length > 2 &&
                    "04".compareTo(version_release[0]) > 0 ||
                    ("04".compareTo(version_release[0]) == 0
                    && "03".compareTo(version_release[1]) >= 0))
                    return true;
*/
                // assume, that version <= 04 is "old"
                if ("04".compareTo(version_release[0]) >= 0) {
                    return DatabaseProduct.DB2_OLD_AS400;
                } else {
                    return DatabaseProduct.DB2_AS400;
                }
            } else {
                // DB2 on NT returns "DB2/NT"
                return DatabaseProduct.DB2;
            }
        } else if (upperProductName.indexOf("FIREBIRD") >= 0) {
            return DatabaseProduct.FIREBIRD;
        } else if (upperProductName.equals("HIVE")
            || upperProductName.equals("APACHE HIVE"))
        {
            return DatabaseProduct.HIVE;
        } else if (productName.startsWith("Informix")) {
            return DatabaseProduct.INFORMIX;
        } else if (upperProductName.equals("INGRES")) {
            return DatabaseProduct.INGRES;
        } else if (productName.equals("Interbase")) {
            return DatabaseProduct.INTERBASE;
        } else if (upperProductName.equals("LUCIDDB")
            || upperProductName.equals("OPTIQ"))
        {
            return DatabaseProduct.LUCIDDB;
        } else if (upperProductName.indexOf("SQL SERVER") >= 0) {
            return DatabaseProduct.MSSQL;
        } else if (productName.equals("Oracle")) {
            return DatabaseProduct.ORACLE;
        } else if (upperProductName.indexOf("POSTGRE") >= 0) {
            return DatabaseProduct.POSTGRESQL;
        } else if (upperProductName.indexOf("NETEZZA") >= 0) {
            return DatabaseProduct.NETEZZA;
        } else if (upperProductName.equals("MYSQL (INFOBRIGHT)")) {
            return DatabaseProduct.INFOBRIGHT;
        } else if (upperProductName.equals("MYSQL")) {
            return DatabaseProduct.MYSQL;
        } else if (upperProductName.equals("MONETDB")) {
            return DatabaseProduct.MONETDB;
        } else if (upperProductName.equals("VERTICA")
            || upperProductName.equals("VERTICA DATABASE"))
        {
            return DatabaseProduct.VERTICA;
        } else if (upperProductName.equals("VECTORWISE")) {
            return DatabaseProduct.VECTORWISE;
        } else if (productName.startsWith("HP Neoview")) {
            return DatabaseProduct.NEOVIEW;
        } else if (upperProductName.indexOf("SYBASE") >= 0
            || upperProductName.indexOf("ADAPTIVE SERVER") >= 0)
        {
            // Sysbase Adaptive Server Enterprise 15.5 via jConnect 6.05 returns
            // "Adaptive Server Enterprise" as a product name.
            return DatabaseProduct.SYBASE;
        } else if (upperProductName.indexOf("TERADATA") >= 0) {
            return DatabaseProduct.TERADATA;
        } else if (upperProductName.indexOf("HSQL") >= 0) {
            return DatabaseProduct.HSQLDB;
        } else if (upperProductName.indexOf("VERTICA") >= 0) {
            return DatabaseProduct.VERTICA;
        } else if (upperProductName.indexOf("VECTORWISE") >= 0) {
            return DatabaseProduct.VECTORWISE;
        } else {
            return DatabaseProduct.getDatabaseProduct(upperProductName);
        }
    }

    /**
     * Helper method to determine if a connection would work with
     * a given database product. This can be used to differenciate
     * between databases which use the same driver as others.
     *
     * <p>It will first try to use
     * {@link DatabaseMetaData#getDatabaseProductName()} and match the
     * name of {@link DatabaseProduct} passed as an argument.
     *
     * <p>If that fails, it will try to execute <code>select version();</code>
     * and obtains some information directly from the server.
     *
     * @param databaseProduct Database product instance
     * @param connection SQL connection
     * @return true if a match was found. false otherwise.
     */
    protected static boolean isDatabase(
        DatabaseProduct databaseProduct,
        Connection connection)
    {
        Statement statement = null;
        ResultSet resultSet = null;

        String dbProduct = databaseProduct.name().toLowerCase();

        try {
            // Quick and dirty check first.
            if (connection.getMetaData().getDatabaseProductName()
                .toLowerCase().contains(dbProduct))
            {
                LOGGER.debug("Using " + databaseProduct.name() + " dialect");
                return true;
            }

            // Let's try using version().
            statement = connection.createStatement();
            resultSet = statement.executeQuery("select version()");
            if (resultSet.next()) {
                String version = resultSet.getString(1);
                LOGGER.debug("Version=" + version);
                if (version != null) {
                    if (version.toLowerCase().contains(dbProduct)) {
                        LOGGER.info(
                            "Using " + databaseProduct.name() + " dialect");
                        return true;
                    }
                }
            }
            LOGGER.debug("NOT Using " + databaseProduct.name() + " dialect");
            return false;
        } catch (SQLException e) {
            LOGGER.debug(
                "NOT Using " + databaseProduct.name() + " dialect.", e);
            return false;
        } finally {
            Util.close(resultSet, statement, null);
        }
    }
}

// End JdbcDialectImpl.java
