/*
 * 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.openjpa.jdbc.sql;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.StringTokenizer;

import javax.sql.DataSource;

import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.kernel.exps.FilterValue;
import org.apache.openjpa.jdbc.kernel.exps.Lit;
import org.apache.openjpa.jdbc.kernel.exps.Param;
import org.apache.openjpa.jdbc.kernel.exps.Val;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.Index;
import org.apache.openjpa.jdbc.schema.Schema;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.kernel.Filters;
import org.apache.openjpa.kernel.MixedLockLevels;
import org.apache.openjpa.kernel.exps.Literal;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.StoreException;
import org.apache.openjpa.util.UnsupportedException;
import org.apache.openjpa.util.UserException;


/**
 * Dictionary for IBM DB2 database.
 */
public class DB2Dictionary
    extends AbstractDB2Dictionary {
    private static final Localizer _loc = Localizer.forPackage
        (DB2Dictionary.class);

    public static final String VENDOR_IBM = "ibm";
    public String optimizeClause = "optimize for";
    public String rowClause = "row";
    protected int db2ServerType = 0;
    public static final int db2ISeriesV5R3OrEarlier = 1;
    public static final int db2UDBV81OrEarlier = 2;
    public static final int db2ZOSV8xOrLater = 3;
    public static final int db2UDBV82OrLater = 4;
    public static final int db2ISeriesV5R4OrLater = 5;
    protected static final String forUpdate = "FOR UPDATE";
    protected static final String withURClause = "WITH UR";
    protected static final String withCSClause = "WITH CS";
    protected static final String withRSClause = "WITH RS";
    protected static final String withRRClause = "WITH RR";
    protected static final String useKeepShareLockClause     = "USE AND KEEP SHARE LOCKS";
    protected static final String useKeepUpdateLockClause    = "USE AND KEEP UPDATE LOCKS";
    protected static final String useKeepExclusiveLockClause = "USE AND KEEP EXCLUSIVE LOCKS";
    protected static final String forReadOnlyClause = "FOR READ ONLY";
    protected static final String defaultSequenceSQL
        = "SELECT SEQSCHEMA AS SEQUENCE_SCHEMA, SEQNAME AS SEQUENCE_NAME FROM SYSCAT.SEQUENCES";
    static final String SYSDUMMY = "SYSIBM.SYSDUMMY1";


    private int defaultBatchLimit = 100;
    public boolean appendExtendedExceptionText = true;

    /**
     * Affirms whether this dictionary uses {@code ROWNUM} feature.
     * {@code ROWNUM} feature is used to construct {@code SQL SELECT} query
     * that uses an offset or limits the number of resultant rows.
     * <br>
     * By default, this flag is set to {@code false}.
     */
    public boolean supportsRowNum = false;

    public DB2Dictionary() {
        platform = "DB2";
        validationSQL = "SELECT DISTINCT(CURRENT TIMESTAMP) FROM SYSIBM.SYSTABLES";
        supportsSelectEndIndex = true;

        nextSequenceQuery = "VALUES NEXTVAL FOR {0}";

        sequenceSQL = defaultSequenceSQL;
        sequenceSchemaSQL = "SEQSCHEMA = ?";
        sequenceNameSQL = "SEQNAME = ?";
        characterColumnSize = 254;

        binaryTypeName = "BLOB(1M)";
        longVarbinaryTypeName = "BLOB(1M)";
        varbinaryTypeName = "BLOB(1M)";
        clobTypeName = "CLOB(1M)";
        longVarcharTypeName = "LONG VARCHAR";
        datePrecision = MICRO;
        storeCharsAsNumbers = false;

        fixedSizeTypeNameSet.addAll(Arrays.asList(new String[]{
            "LONG VARCHAR FOR BIT DATA", "LONG VARCHAR", "LONG VARGRAPHIC",
        }));
        systemSchemas = "SYSCAT,SYSIBM,SYSSTAT,SYSIBMADM,SYSTOOLS";
        maxConstraintNameLength = 18;
        maxIndexNameLength = 128;
        maxColumnNameLength = 30;
        supportsDeferredConstraints = false;
        supportsDefaultDeleteAction = false;
        supportsAlterTableWithDropColumn = false;
        supportsLockingWithOrderClause = true;
        supportsNullUniqueColumn = false;

        supportsNullTableForGetColumns = false;
        requiresCastForMathFunctions = true;
        requiresCastForComparisons = true;

        reservedWordSet.addAll(Arrays.asList(new String[]{
            "AFTER", "ALIAS", "ALLOW", "APPLICATION", "ASSOCIATE", "ASUTIME",
            "AUDIT", "AUX", "AUXILIARY", "BEFORE", "BINARY", "BUFFERPOOL",
            "CACHE", "CALL", "CALLED", "CAPTURE", "CARDINALITY", "CCSID",
            "CLUSTER", "COLLECTION", "COLLID", "COMMENT", "CONCAT",
            "CONDITION", "CONTAINS", "COUNT_BIG", "CURRENT_LC_CTYPE",
            "CURRENT_PATH", "CURRENT_SERVER", "CURRENT_TIMEZONE", "CYCLE",
            "DATABASE", "DAYS", "DB2GENERAL", "DB2GENRL", "DB2SQL",
            "DBINFO", "DEFAULTS", "DEFINITION", "DETERMINISTIC", "DISALLOW",
            "DO", "DSNHATTR", "DSSIZE", "DYNAMIC", "EACH", "EDITPROC", "ELSEIF",
            "ENCODING", "END-EXEC1", "ERASE", "EXCLUDING", "EXIT", "FENCED",
            "FIELDPROC", "FILE", "FINAL", "FREE", "FUNCTION", "GENERAL",
            "GENERATED", "GRAPHIC", "HANDLER", "HOLD", "HOURS", "IF",
            "INCLUDING", "INCREMENT", "INDEX", "INHERIT", "INOUT", "INTEGRITY",
            "ISOBID", "ITERATE", "JAR", "JAVA", "LABEL", "LC_CTYPE", "LEAVE",
            "LINKTYPE", "LOCALE", "LOCATOR", "LOCATORS", "LOCK", "LOCKMAX",
            "LOCKSIZE", "LONG", "LOOP", "MAXVALUE", "MICROSECOND",
            "MICROSECONDS", "MINUTES", "MINVALUE", "MODE", "MODIFIES", "MONTHS",
            "NEW", "NEW_TABLE", "NOCACHE", "NOCYCLE", "NODENAME", "NODENUMBER",
            "NOMAXVALUE", "NOMINVALUE", "NOORDER", "NULLS", "NUMPARTS", "OBID",
            "OLD", "OLD_TABLE", "OPTIMIZATION", "OPTIMIZE", "OUT", "OVERRIDING",
            "PACKAGE", "PARAMETER", "PART", "PARTITION", "PATH", "PIECESIZE",
            "PLAN", "PRIQTY", "PROGRAM", "PSID", "QUERYNO", "READS", "RECOVERY",
            "REFERENCING", "RELEASE", "RENAME", "REPEAT", "RESET", "RESIGNAL",
            "RESTART", "RESULT", "RESULT_SET_LOCATOR", "RETURN", "RETURNS",
            "ROUTINE", "ROW", "RRN", "RUN", "SAVEPOINT", "SCRATCHPAD",
            "SECONDS", "SECQTY", "SECURITY", "SENSITIVE", "SIGNAL", "SIMPLE",
            "SOURCE", "SPECIFIC", "SQLID", "STANDARD", "START", "STATIC",
            "STAY", "STOGROUP", "STORES", "STYLE", "SUBPAGES", "SYNONYM",
            "SYSFUN", "SYSIBM", "SYSPROC", "SYSTEM", "TABLESPACE", "TRIGGER",
            "UNDO", "UNTIL", "VALIDPROC", "VARIABLE", "VARIANT", "VCAT",
            "VOLUMES", "WHILE", "WLM", "YEARS",
        }));

        // reservedWordSet subset that CANNOT be used as valid column names
        // (i.e., without surrounding them with double-quotes)
        invalidColumnWordSet.addAll(Arrays.asList(new String[] {
            "CONSTRAINT", "END-EXEC", "END-EXEC1",
        }));


        super.setBatchLimit(defaultBatchLimit);

        selectWordSet.add("WITH");
    }

    @Override
    public boolean supportsRandomAccessResultSet(Select sel,
        boolean forUpdate) {
        return !forUpdate
            && super.supportsRandomAccessResultSet(sel, forUpdate);
    }

    @Override
    protected void appendSelectRange(SQLBuffer buf, long start, long end,
        boolean subselect) {
        // appends the literal range string, since DB2 is unable to handle
        // a bound parameter for it
        // do not generate FETCH FIRST clause for subselect
        if (!subselect)
            buf.append(" FETCH FIRST ").append(Long.toString(end)).
                append(" ROWS ONLY");
    }

    @Override
    protected void appendSelect(SQLBuffer selectSQL, Object alias, Select sel,
        int idx) {
        // if this is a literal value, add a cast...
        Object val = sel.getSelects().get(idx);
        boolean toCast = (val instanceof Lit) &&
            ((Lit)val).getParseType() != Literal.TYPE_DATE &&
            ((Lit)val).getParseType() != Literal.TYPE_TIME &&
            ((Lit)val).getParseType() != Literal.TYPE_TIMESTAMP;

        if (toCast)
            selectSQL.append("CAST(");

        // ... and add the select per super's behavior...
        super.appendSelect(selectSQL, alias, sel, idx);

        // ... and finish the cast
        if (toCast) {
            Class<?> c = ((Lit) val).getType();
            int javaTypeCode = JavaTypes.getTypeCode(c);
            int jdbcTypeCode = getJDBCType(javaTypeCode, false);
            String typeName = getTypeName(jdbcTypeCode);
            selectSQL.append(" AS " + typeName);

            // if the literal is a string, use the default char col size
            // in the cast statement.
            if (String.class.equals(c))
                selectSQL.append("(" + getCastStringColumnSize(val) + ")");

            selectSQL.append(")");
        }
    }

    @Override
    protected String getSequencesSQL(String schemaName, String sequenceName) {
        return getSequencesSQL(DBIdentifier.newSchema(schemaName),
            DBIdentifier.newSequence(sequenceName));
    }

    @Override
    protected String getSequencesSQL(DBIdentifier schemaName, DBIdentifier sequenceName) {
        StringBuilder buf = new StringBuilder();
        buf.append(sequenceSQL);
        if (!DBIdentifier.isNull(schemaName) || !DBIdentifier.isNull(sequenceName))
            buf.append(" WHERE ");
        if (!DBIdentifier.isNull(schemaName)) {
            buf.append(sequenceSchemaSQL);
            if (!DBIdentifier.isNull(sequenceName))
                buf.append(" AND ");
        }
        if (!DBIdentifier.isNull(sequenceName))
            buf.append(sequenceNameSQL);
        return buf.toString();
    }

    @Override
    public Connection decorate(Connection conn)
        throws SQLException {
        // some versions of the DB2 driver seem to default to
        // READ_UNCOMMITTED, which will prevent locking from working
        // (multiple SELECT ... FOR UPDATE statements are allowed on
        // the same instance); if we have not overridden the
        // transaction isolation in the configuration, default to
        // TRANSACTION_READ_COMMITTED
        conn = super.decorate(conn);

        if (conf.getTransactionIsolationConstant() == -1 &&
                conn.getTransactionIsolation() <
                Connection.TRANSACTION_READ_COMMITTED)
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

        return conn;
    }

    @Override
    public void connectedConfiguration(Connection conn) throws SQLException {
        super.connectedConfiguration(conn);

        DatabaseMetaData metaData = conn.getMetaData();

        String driverName = metaData.getDriverName();
        if (driverName != null && driverName.startsWith("IBM DB2"))
            driverVendor = VENDOR_IBM;
        else
            driverVendor = VENDOR_OTHER;


        // Determine the type of DB2 database
        // First check for AS/400
        getProductVersionMajorMinorForISeries();

        if (versionLaterThan(0)) {
            if (isDB2ISeriesV5R3OrEarlier())
                db2ServerType = db2ISeriesV5R3OrEarlier;
            else if (isDB2ISeriesV5R4OrLater())
                db2ServerType = db2ISeriesV5R4OrLater;
        }

        if (db2ServerType == 0) {
            if (isJDBC3) {
                setMajorVersion(metaData.getDatabaseMajorVersion());
                setMinorVersion(metaData.getDatabaseMinorVersion());
            }
            else
                getProductVersionMajorMinor();

            // Determine the type of DB2 database for ZOS & UDB
            if (isDB2UDBV81OrEarlier())
                db2ServerType = db2UDBV81OrEarlier;
            else if (isDB2ZOSV8xOrLater())
                db2ServerType = db2ZOSV8xOrLater;
            else if (isDB2UDBV82OrLater())
                db2ServerType = db2UDBV82OrLater;
        }

        // verify that database product is supported
        if (db2ServerType == 0 || getMajorVersion() < 0)
            throw new UnsupportedException(_loc.get("db-not-supported",
                new Object[] {databaseProductName, databaseProductVersion }));
        if (versionEqualOrLaterThan(9, 2)) {
            supportsLockingWithMultipleTables = true;
            supportsLockingWithInnerJoin = true;
            supportsLockingWithOuterJoin = true;
            forUpdateClause = "WITH RR USE AND KEEP UPDATE LOCKS";

            supportsXMLColumn = versionEqualOrLaterThan(9, 0);

        }

        // platform specific settings
        switch (db2ServerType) {
        case db2UDBV82OrLater:
            lastGeneratedKeyQuery = "SELECT IDENTITY_VAL_LOCAL() FROM " + SYSDUMMY;
            break;
        case  db2ZOSV8xOrLater:
            // DB2 Z/OS
            characterColumnSize = 255;
            lastGeneratedKeyQuery = "SELECT IDENTITY_VAL_LOCAL() FROM " + SYSDUMMY;
            nextSequenceQuery = "SELECT NEXTVAL FOR {0} FROM " + SYSDUMMY;
            // allow users to set a non default sequenceSQL.
            if (defaultSequenceSQL.equals(sequenceSQL)){
	            sequenceSQL = "SELECT SCHEMA AS SEQUENCE_SCHEMA, "
	                + "NAME AS SEQUENCE_NAME FROM SYSIBM.SYSSEQUENCES";

                if (log.isTraceEnabled())
                    log.trace(_loc.get("sequencesql-override", new Object[] {defaultSequenceSQL, sequenceSQL}));
            }
            sequenceSchemaSQL = "SCHEMA = ?";
            sequenceNameSQL = "NAME = ?";
            if (getMajorVersion() == 8) {
                // DB2 Z/OS Version 8: no bigint support, hence map Java
                // long to decimal
                bigintTypeName = "DECIMAL(31,0)";
            }
            break;
        case db2ISeriesV5R3OrEarlier:
        case db2ISeriesV5R4OrLater:
            lastGeneratedKeyQuery = "SELECT IDENTITY_VAL_LOCAL() FROM " + SYSDUMMY;
            nextSequenceQuery = "SELECT NEXTVAL FOR {0} FROM " + SYSDUMMY;
            validationSQL = "SELECT DISTINCT(CURRENT TIMESTAMP) FROM "
                + "QSYS2.SYSTABLES";
            // allow users to set a non default sequenceSQL.
            if (defaultSequenceSQL.equals(sequenceSQL)){
	            sequenceSQL = "SELECT SEQUENCE_SCHEMA, "
	                + "SEQUENCE_NAME FROM QSYS2.SYSSEQUENCES";

                if (log.isTraceEnabled())
                    log.trace(_loc.get("sequencesql-override", new Object[] {defaultSequenceSQL, sequenceSQL}));
            }
            sequenceSchemaSQL = "SEQUENCE_SCHEMA = ?";
            sequenceNameSQL = "SEQUENCE_NAME = ?";
            // V5R4 and earlier systems do not support retrieval of generated keys
            if (isDB2ISeriesV5R4OrEarlier()) {
            	supportsGetGeneratedKeys = false;
            }

            break;
        }
    }


    @Override
    public boolean supportsIsolationForUpdate() {
        return true;
    }

    /**
     * Get the update clause for the query based on the
     * isolationLevel hints if it is for update.
     * It also handles the UR hint when it is not for update.
     */
    @Override
    protected String getForUpdateClause(JDBCFetchConfiguration fetch,
        boolean isForUpdate, Select sel) {
        int isolationLevel;
        // For db2UDBV81OrEarlier and db2ISeriesV5R3OrEarlier:
        // "optimize for" clause appears before "for update" clause.
        StringBuilder forUpdateString = new StringBuilder(getOptimizeClause(sel));
        // Determine the isolationLevel; the fetch
        // configuration data overrides the persistence.xml value
        if (fetch != null && fetch.getIsolation() != -1)
            isolationLevel = fetch.getIsolation();
        else
            isolationLevel = conf.getTransactionIsolationConstant();

        if (fetch != null && fetch.getReadLockLevel() >=
            MixedLockLevels.LOCK_PESSIMISTIC_WRITE)
            isolationLevel = Connection.TRANSACTION_SERIALIZABLE;

        if (isForUpdate) {
            switch (db2ServerType) {
            case db2ISeriesV5R3OrEarlier:
            case db2UDBV81OrEarlier:
                if (isolationLevel == Connection.TRANSACTION_SERIALIZABLE)
                    forUpdateString.append(" ").append(forUpdateClause);
                else
                    forUpdateString.append(" ").append(forUpdate).append(" ")
                        .append(withRSClause);
                break;
            case db2ZOSV8xOrLater:
            case db2UDBV82OrLater:
                if (isolationLevel == Connection.TRANSACTION_SERIALIZABLE) {
                    forUpdateString.append(" ").append(forReadOnlyClause)
                        .append(" ").append(withRRClause).append(" ").append(
                            useKeepUpdateLockClause);
                } else {
                    forUpdateString.append(" ").append(forReadOnlyClause)
                        .append(" ").append(withRSClause).append(" ").append(
                            useKeepUpdateLockClause);
                }
                break;
            case db2ISeriesV5R4OrLater:
                if (isolationLevel == Connection.TRANSACTION_SERIALIZABLE) {
                    forUpdateString.append(" ").append(forReadOnlyClause)
                        .append(" ").append(withRRClause).append(" ").append(
                            useKeepExclusiveLockClause);
                } else {
                    forUpdateString.append(" ").append(forReadOnlyClause)
                        .append(" ").append(withRSClause).append(" ").append(
                            useKeepExclusiveLockClause);
                }
                break;
            }
        } else {
        	if ( fetch != null && fetch.getIsolation() == Connection.TRANSACTION_READ_UNCOMMITTED
        			&& sel != null && sel.getParent() == null) { // i.e. not a subquery
	            forUpdateString.append(" ").append(forReadOnlyClause)
	            .append(" ").append(withURClause);
	    	}
        }

        return forUpdateString.toString();
    }

    public boolean isDB2UDBV82OrLater() {
        return (databaseProductVersion.indexOf("SQL") != -1
             || databaseProductName.indexOf("DB2/") != -1)
             && versionEqualOrLaterThan(8, 2);
    }

    public boolean isDB2ZOSV8xOrLater() {
       return (databaseProductVersion.indexOf("DSN") != -1
            || databaseProductName.indexOf("DB2/") == -1)
            && versionLaterThan(7);
    }

    public boolean isDB2ISeriesV5R3OrEarlier() {
       return databaseProductName.indexOf("AS") != -1
           && versionEqualOrEarlierThan(5, 3);
    }

    public boolean isDB2ISeriesV5R4OrLater() {
       return databaseProductName.indexOf("AS") != -1
           && versionEqualOrLaterThan(5, 4);
    }

    public boolean isDB2ISeriesV5R4OrEarlier() {
        return databaseProductName.indexOf("AS") != -1
            && versionEqualOrEarlierThan(5, 4);
     }

    public boolean isDB2UDBV81OrEarlier() {
        return (databaseProductVersion.indexOf("SQL") != -1
            || databaseProductName.indexOf("DB2/") != -1)
            && versionEqualOrEarlierThan(8,1);
    }

    /** Get the version Major/Minor for the ISeries
     */
    private void getProductVersionMajorMinorForISeries() {
        // ISeries    DBProdName                 DB2 UDB for AS/400
        //   (Toolbox)DBProdVersion              05.04.0000 V5R4m0
        // ISeries                               DB2 UDB for AS/400
        //   (Native)                            V5R4M0
        // new jcc    DBProdVersion              QSQ05040 or QSQ06010
        if (databaseProductName.indexOf("AS") != -1) {
            // default to V5R4
            setMajorVersion(5);
            setMinorVersion(4);
            int index = databaseProductVersion.indexOf('V');
            if (index != -1) {
                String s = databaseProductVersion.substring(index);
                s = s.toUpperCase(Locale.ENGLISH);

                StringTokenizer stringtokenizer = new StringTokenizer(s, "VRM"
                    , false);
                if (stringtokenizer.countTokens() == 3) {
                    String s1 = stringtokenizer.nextToken();
                    setMajorVersion(Integer.parseInt(s1));
                    String s2 =  stringtokenizer.nextToken();
                    setMinorVersion(Integer.parseInt(s2));
                }
            } else {
                index = databaseProductVersion.indexOf('0');
                if (index != -1) {
                    String s = databaseProductVersion.substring(index);
                    s = s.toUpperCase(Locale.ENGLISH);

                    StringTokenizer stringtokenizer = new StringTokenizer(s, "0"
                        , false);
                    if (stringtokenizer.countTokens() == 2) {
                        String s1 = stringtokenizer.nextToken();
                        setMajorVersion(Integer.parseInt(s1));
                        String s2 =  stringtokenizer.nextToken();
                        setMinorVersion(Integer.parseInt(s2));
                    }
                }
            }
        }
    }

    private void getProductVersionMajorMinor() {
        // In case JDBC driver version is lower than 3
        // use following info to determine Major and Minor
        //                        CLI    vs      JCC
        // ZDBV8 DBProdName       DB2            DB2
        //       DBProdVersion    08.01.0005     DSN08015
        // ZDBV9                  DB2            DB2
        //                        09.01.0005     DSN09015
        // WinV9                  DB2/NT         DB2/NT
        //                        09.01.0000     SQL09010
        // SolarisV9                             DB2/SUN64
        //                                       SQL0901
        // Linux                  DB2/LINUX      DB2/LINUX
        //                        09.01.0000     SQL0901
        if (databaseProductVersion.indexOf("09") != -1) {
            setMajorVersion(9);
            if (databaseProductVersion.indexOf("01") != -1) {
                setMinorVersion(1);
            }
        } else if (databaseProductVersion.indexOf("08") != -1) {
        	setMajorVersion(8);
        	setMinorVersion(2);
            if (databaseProductVersion.indexOf("01") != -1) {
            	setMinorVersion(1);
            }
        }
    }

    protected String getOptimizeClause(Select sel) {
        if (sel != null && sel.getExpectedResultCount() > 0) {
            StringBuilder buf = new StringBuilder();
            buf.append(" ").append(optimizeClause).append(" ")
                .append(sel.getExpectedResultCount())
                .append(" ").append(rowClause);
            return buf.toString();
        }

        return "";
    }

    @Override
    public OpenJPAException newStoreException(String msg, SQLException[] causes, Object failed) {
        if (appendExtendedExceptionText && causes != null && causes.length > 0) {
            msg = appendExtendedExceptionMsg(msg, causes[0]);
        }
        return super.newStoreException(msg, causes, failed);
    }

    /**
     *  Append exception information from SQLCA to the existing
     *  exception message
     */
    private String appendExtendedExceptionMsg(String msg, SQLException sqle){
       final String GETSQLCA ="getSqlca";
       try {
            Method sqlcaM2 = sqle.getNextException().getClass()
                             .getMethod(GETSQLCA,null);
            Object sqlca = sqlcaM2.invoke(sqle.getNextException(),
                                          new Object[] {});
            Method  getSqlErrpMethd = sqlca.getClass().
            getMethod("getSqlErrp", null);
            Method  getSqlWarnMethd = sqlca.getClass().
            getMethod("getSqlWarn", null);
            Method  getSqlErrdMethd = sqlca.getClass().
            getMethod("getSqlErrd", null);
            StringBuilder errdStr = new StringBuilder();

            int[] errds = (int[]) getSqlErrdMethd.invoke(sqlca, new Object[]{});
           for (int errd : errds)
               errdStr.append(errdStr.length() > 0 ? ", " : "").
                       append(errd);
            StringBuilder exceptionMsg = new StringBuilder();
            exceptionMsg.append("SQLCA OUTPUT");
            exceptionMsg.append("[Errp=");
            exceptionMsg.append(getSqlErrpMethd.invoke(sqlca, new Object[]{}));
            exceptionMsg.append(", Errd=");
            exceptionMsg.append(errdStr);

            String Warn = new String((char[]) getSqlWarnMethd.
                    invoke(sqlca, new Object[]{}));
            if (Warn.trim().length() != 0) {
                exceptionMsg.append(", Warn=");
                exceptionMsg.append(Warn);
                exceptionMsg.append("]");
            } else {
                exceptionMsg.append("]");
            }
            msg = msg.concat(exceptionMsg.toString());

            // for batched execution failures, SQLExceptions are nested
            SQLException sqle2 = sqle.getNextException();
            while (sqle2 != null) {
                msg = msg.concat("\n" + sqle2.getMessage());
                sqle2 = sqle2.getNextException();
            }

            return msg;
        } catch (Throwable t) {
            return sqle.getMessage();
        }
    }

    public int getDb2ServerType() {
        return db2ServerType;
    }

    @Override
    protected void appendLength(SQLBuffer buf, int type) {
        if (type == Types.VARCHAR)
            buf.append("(").append(Integer.toString(characterColumnSize)).
                append(")");
    }

    /**
     * If this dictionary supports XML type,
     * use this method to append xml predicate.
     *
     * @param buf the SQL buffer to write the comparison
     * @param op the comparison operation to perform
     * @param lhs the left hand side of the comparison
     * @param rhs the right hand side of the comparison
     * @param lhsxml indicates whether the left operand maps to xml
     * @param rhsxml indicates whether the right operand maps to xml
     */
    @Override
    public void appendXmlComparison(SQLBuffer buf, String op, FilterValue lhs,
        FilterValue rhs, boolean lhsxml, boolean rhsxml) {
        super.appendXmlComparison(buf, op, lhs, rhs, lhsxml, rhsxml);
        if (lhsxml && rhsxml)
            appendXmlComparison2(buf, op, lhs, rhs);
        else if (lhsxml)
            appendXmlComparison1(buf, op, lhs, rhs);
        else
            appendXmlComparison1(buf, op, rhs, lhs);
    }

    /**
     * Append an xml comparison predicate.
     *
     * @param buf the SQL buffer to write the comparison
     * @param op the comparison operation to perform
     * @param lhs the left hand side of the comparison (maps to xml column)
     * @param rhs the right hand side of the comparison
     */
    private void appendXmlComparison1(SQLBuffer buf, String op,
            FilterValue lhs, FilterValue rhs) {
        boolean castrhs = false;
        Class<?> rc = Filters.wrap(rhs.getType());
        int type = 0;
        if (rhs.isConstant()) {
            type = getJDBCType(JavaTypes.getTypeCode(rc), false);
            castrhs = true;
        }

        appendXmlExists(buf, lhs);

        buf.append(" ").append(op).append(" ");

        buf.append("$");
        if (castrhs)
            buf.append("Parm");
        else
            rhs.appendTo(buf);

        buf.append("]' PASSING ");
        appendXmlVar(buf, lhs);
        buf.append(", ");

        if (castrhs)
            appendCast(buf, rhs, type);
        else
            rhs.appendTo(buf);

        buf.append(" AS \"");
        if (castrhs)
            buf.append("Parm");
        else
            rhs.appendTo(buf);
        buf.append("\")");
    }

    /**
     * Append an xml comparison predicate. (both operands map to xml column)
     *
     * @param buf the SQL buffer to write the comparison
     * @param op the comparison operation to perform
     * @param lhs the left hand side of the comparison (maps to xml column)
     * @param rhs the right hand side of the comparison (maps to xml column)
     */
    private void appendXmlComparison2(SQLBuffer buf, String op,
            FilterValue lhs, FilterValue rhs) {
        appendXmlExists(buf, lhs);

        buf.append(" ").append(op).append(" ");

        buf.append("$").append(rhs.getColumnAlias(
            rhs.getFieldMapping().getColumns()[0])).
            append("/*/");
        rhs.appendTo(buf);

        buf.append("]' PASSING ");
        appendXmlVar(buf, lhs);
        buf.append(", ");
        appendXmlVar(buf, rhs);
        buf.append(")");
    }

    private void appendXmlVar(SQLBuffer buf, FilterValue val) {
        buf.append(val.getColumnAlias(
            val.getFieldMapping().getColumns()[0])).
            append(" AS ").
            append("\"").append(val.getColumnAlias(
            val.getFieldMapping().getColumns()[0])).
            append("\"");
    }

    private void appendXmlExists(SQLBuffer buf, FilterValue val) {
        buf.append("XMLEXISTS('");
        buf.append("$").append(val.getColumnAlias(
            val.getFieldMapping().getColumns()[0])).
            append("/*[");
        val.appendTo(buf);
    }

    /**
     * add CAST for a scalar function where operand is a param
     *
     * @param func original string
     * @param target substring to look for
     * @param asString
     * @return updated string (func)
     */
    private String addCastAsString(String func, String target,
            String asString) {
        String fstring = func;
        if (func.indexOf(target) != -1) {
            fstring = StringUtil.replace(func, target, "CAST(" + target + asString + ")");
        }
        return fstring;
    }

    /**
     * add CAST for a function operator where operand is a param
     *
     * @param func function name
     * @param val type
     * @return updated string (func)
     */
    @Override
    public String addCastAsType(String func, Val val) {
        String fstring = null;
        String type = getTypeName(getJDBCType(JavaTypes.getTypeCode(val
            .getType()), false));
        if (String.class.equals(val.getType()))
            type = type + "(" + getCastStringColumnSize(val) + ")";
        fstring = "CAST(? AS " + type + ")";
        return fstring;
    }

    /**
     * Return the batch limit. If the batchLimit is -1, change it to 100 for
     * best performance
     */
    @Override
    public int getBatchLimit() {
        int limit = super.getBatchLimit();
        if (limit == UNLIMITED) {
            limit = defaultBatchLimit;
            if (log.isTraceEnabled())
                log.trace(_loc.get("batch_unlimit", String.valueOf(limit)));
        }
        return limit;
    }

    /**
     * Return the correct CAST function syntax
     *
     * @param val operand of cast
     * @param func original string
     * @return a String with the correct CAST function syntax
     */
    @Override
    public String getCastFunction(Val val, String func) {
        if (val instanceof Lit || val instanceof Param) {
            if (func.indexOf("VARCHAR") == -1) {
                func = addCastAsString(func, "{0}", " AS VARCHAR(" + varcharCastLength + ")");
            }
        }
        return func;
    }

    /**
     * Return the correct CAST function syntax
     *
     * @param val operand of cast
     * @param func original string
     * @param col database column
     * @return a String with the correct CAST function syntax
     */
    @Override
    public String getCastFunction(Val val, String func, Column col) {
        boolean doCast = false;
        if (val instanceof Lit || val instanceof Param) {
            doCast = true;
        }
        // cast anything not already a VARCHAR to VARCHAR
        if (col.getType() != Types.VARCHAR) {
            doCast = true;
        }
        if (doCast) {
            if (func.indexOf("VARCHAR") == -1) {
                func = addCastAsString(func, "{0}", " AS VARCHAR(" + varcharCastLength + ")");
            }
        }
        return func;
    }

    @Override
    public void indexOf(SQLBuffer buf, FilterValue str, FilterValue find,
            FilterValue start) {
        if (find.getValue() != null) { // non constants
            buf.append("LOCATE(CAST((");
            find.appendTo(buf);
            buf.append(") AS VARCHAR(1000)), ");
        } else {
            // this is a constant
            buf.append("LOCATE(");
            find.appendTo(buf);
            buf.append(", ");
        }
        if (str.getValue() != null) {
            buf.append("CAST((");
            str.appendTo(buf);
            buf.append(") AS VARCHAR(1000))");
        } else {
            str.appendTo(buf);
        }
        if (start != null) {
            if (start.getValue() != null) {
                buf.append(", CAST((");
                start.appendTo(buf);
                buf.append(") AS INTEGER)");
            } else {
                buf.append(", ");
                start.appendTo(buf);
            }
        }
        buf.append(")");
    }

    /**
     * Cast the specified value to the specified type.
     *
     * @param buf the buffer to append the cast to
     * @param val the value to cast
     * @param type the type of the case, e.g. {@link Types#NUMERIC}
     */
    public void appendCast(SQLBuffer buf, FilterValue val, int type) {

        // Convert the cast function: "CAST({0} AS {1})"
        int firstParam = castFunction.indexOf("{0}");
        String pre = castFunction.substring(0, firstParam); // "CAST("
        String mid = castFunction.substring(firstParam + 3);
        int secondParam = mid.indexOf("{1}");
        String post;
        if (secondParam > -1) {
            post = mid.substring(secondParam + 3); // ")"
            mid = mid.substring(0, secondParam); // " AS "
        } else
            post = "";

        // No need to add CAST if the value is a constant
        if (val instanceof Lit || val instanceof Param) {
            buf.append(pre);
            val.appendTo(buf);
            buf.append(mid);
            buf.append(getTypeName(type));
            appendLength(buf, type);
            buf.append(post);
        } else {
            val.appendTo(buf);
            String sqlString = buf.getSQL(false);
            if (sqlString.endsWith("?")) {
                // case "(?" - convert to "CAST(? AS type"
                String typeName = getTypeName(type);
                if (String.class.equals(val.getType()))
                    typeName = typeName + "(" + getCastStringColumnSize(val) + ")";
                String str = "CAST(? AS " + typeName + ")";
                buf.replaceSqlString(sqlString.length() - 1,
                        sqlString.length(), str);
            }
        }
    }

    /**
     * Create an index if necessary for some database tables
     */
    @Override
    public void createIndexIfNecessary(Schema schema, String table,
        Column pkColumn) {
        createIndexIfNecessary(schema, DBIdentifier.newTable(table),
            pkColumn);
    }

    @Override
    public void createIndexIfNecessary(Schema schema, DBIdentifier table,
            Column pkColumn) {
        if (db2ServerType == db2ZOSV8xOrLater) {
            // build the index for the sequence tables
            // the index name will be the fully qualified table name + _IDX
            Table tab = schema.getTable(table);
            DBIdentifier fullIdxId = tab.getFullIdentifier().clone();
            DBIdentifier unQualifiedName = DBIdentifier.append(fullIdxId.getUnqualifiedName(), "IDX");
            fullIdxId.setName(getValidIndexName(unQualifiedName, tab));
            Index idx = tab.addIndex(fullIdxId);
            idx.setUnique(true);
            idx.addColumn(pkColumn);
        }
    }

    @Override
    public boolean isFatalException(int subtype, SQLException ex) {
        String errorState = ex.getSQLState();
        int errorCode = ex.getErrorCode();
        if (errorCode == -952 && "57014".equals(errorState))
            return false;
        /*
         * Check if this Exception was generated by a lock timeout expiration.
         * The following criteria are used to determine this:
         *
         * DB2 LUW Infocenter: SQLSTATE=57033 with reason code "80" indicates
         * the statement failed due to timeout. DB2 for z/OS Stored Procedures:
         * Through the CALL and Beyond, page 188: An ErrorCode of -913 with
         * SQLERR 00C9008E means a timeout has occurred.
         */
        if (subtype == StoreException.LOCK && "57033".equals(errorState)
            && ((ex.getMessage().indexOf("80") != -1)
                || (errorCode == -913 && ex.getMessage().contains("00C9008E")))) {
            return false;
        }
        if ((subtype == StoreException.QUERY && "57014".equals(errorState) &&
            (errorCode == -952 || errorCode == -905))) {
            return false;
        }
        return super.isFatalException(subtype, ex);
    }

    @Override
    protected void setDelimitedCase(DatabaseMetaData metaData) {
        delimitedCase = SCHEMA_CASE_PRESERVE;
    }

    /**
     * The Type 2 JDBC Driver may throw an SQLException when provided a non-
     * zero timeout if we're connected to Z/OS. The SQLException should be
     * logged but not thrown.
     */
    @Override
    public void setQueryTimeout(PreparedStatement stmnt, int timeout)
        throws SQLException {
        if (db2ServerType == db2ZOSV8xOrLater) {
            try {
                super.setQueryTimeout(stmnt, timeout);
            }
            catch (SQLException e) {
                if (log.isTraceEnabled()) {
                    log.trace(_loc.get("error-setting-query-timeout", timeout,
                        e.getMessage()), e);
                }
            }
        }
        else {
            super.setQueryTimeout(stmnt, timeout);
        }
    }

    /**
     * Set the given value as a parameter to the statement.
     */
    @Override
    public void setBytes(PreparedStatement stmnt, int idx, byte[] val,
        Column col)
        throws SQLException {
        // for DB2, if the column was defined as CHAR for BIT DATA, then
        // we want to use the setBytes in stead of the setBinaryStream
        if (useSetBytesForBlobs
                || (!DBIdentifier.isNull(col.getTypeIdentifier()) &&
                col.getTypeIdentifier().getName().contains("BIT DATA"))) {
            stmnt.setBytes(idx, val);
        } else {
            setBinaryStream(stmnt, idx, new ByteArrayInputStream(val), val.length, col);
        }
    }

    /**
     * Convert the specified column of the SQL ResultSet to the proper
     * java type.
     */
    @Override
    public byte[] getBytes(ResultSet rs, int column)
        throws SQLException {
        if (useGetBytesForBlobs) {
            return rs.getBytes(column);
        }
        if (useGetObjectForBlobs) {
            return (byte[]) rs.getObject(column);
        }

        int type = rs.getMetaData().getColumnType(column);
        switch (type) {
            case Types.BLOB:
                Blob blob = getBlob(rs, column);
                if (blob == null) {
                    return null;
                }

                int length = (int) blob.length();
                if (length == 0) {
                    return null;
                }
                return blob.getBytes(1, length);
            case Types.BINARY:
            default:
                return rs.getBytes(column);
        }
    }

    private int getCastStringColumnSize(Object val) {
        int colSize = characterColumnSize;
        if (val instanceof Lit) {
            String literal = (String) ((Lit) val).getValue();
            if (literal != null) {
                int literalLen = literal.length();
                if (literalLen > characterColumnSize) {
                    colSize = literalLen;
                }
            }
        }
        return colSize;
    }

    @Override
    public void insertBlobForStreamingLoad(Row row, Column col,
            JDBCStore store, Object ob, Select sel) throws SQLException {
        if (ob != null) {
            row.setBinaryStream(col, (InputStream)ob, -1);
        } else {
            row.setNull(col);
        }
    }

    @Override
    public void insertClobForStreamingLoad(Row row, Column col, Object ob)
    throws SQLException {
        if (ob != null) {
            row.setCharacterStream(col, (Reader)ob, -1);
        } else {
            row.setNull(col);
        }
    }

    @Override
    public void updateBlob(Select sel, JDBCStore store, InputStream is)
        throws SQLException {
        //NO-OP
    }

    @Override
    public void updateClob(Select sel, JDBCStore store, Reader reader)
        throws SQLException {
        //NO-OP
    }

    /**
     * Set the given date value as a parameter to the statement.
     */
    @Override
    public void setDate(PreparedStatement stmnt, int idx, Date val, Column col)
        throws SQLException {
        // When column metadata is not available, DB2 on z/OS does not like the value produced
        // by the default dictionary - java.util.Date is converted to java.sql.Timestamp.
        if (db2ServerType == db2ZOSV8xOrLater) {
            if (col == null && val != null && "java.util.Date".equals(val.getClass().getName())) {
                setDate(stmnt, idx, new java.sql.Date(val.getTime()), null, col);
                return;
            }
        }
        super.setDate(stmnt, idx, val, col);
    }

    public int getDB2MajorVersion() {
        return getMajorVersion();
    }

    public int getDB2MinorVersion() {
        return getMinorVersion();
    }

    @Override
    public String getDefaultSchemaName()  {
        if (defaultSchemaName == null) {
            Connection conn = null;
            Statement stmnt = null;
            ResultSet rs = null;
            try {
                String str = "SELECT CURRENT SCHEMA FROM " + SYSDUMMY;
                conn = getConnection();
                if (conn != null) {
                    stmnt = conn.createStatement();
                    rs = stmnt.executeQuery(str);
                    if (rs.next()) {
                        String currSchema = rs.getString(1);
                        if (currSchema != null) {
                            setDefaultSchemaName(currSchema.trim());
                        }
                    }
                } else {
                    if (log.isTraceEnabled()) {
                        log.trace(_loc.get("can_not_get_current_schema", "Unable to obtain a datasource"));
                    }
                }
            } catch (SQLException e) {
                if (log.isTraceEnabled()) {
                    log.trace(_loc.get("can_not_get_current_schema", e.getMessage()));
                }
            } finally {
                if (rs != null) {
                    try {
                        rs.close();
                    } catch (SQLException se) {
                        // ignore
                    }
                }
                if (stmnt != null) {
                    try {
                        stmnt.close();
                    } catch (SQLException se) {
                        // ignore
                    }
                }
                if (conn != null) {
                    try {
                        conn.close();
                    }
                    catch(SQLException se) {
                        // ignore
                    }
                }
            }
        }
        return defaultSchemaName;
    }

    /**
     * Obtain a connection from the configuration. Tries to use the jta-data-source first but falls back on the
     * non-jta-data-source if no jta-data-source has been defined.
     *
     * In practice this method is only called by getDefaultSchemaName which in turn is only used by the schema tool.
     *
     * @throws SQLException If neither datasource is available.
     * @return A connection which may be used to obtain the default schema name.
     */
    private Connection getConnection() throws SQLException {
    	DataSource  ds = null;
    	try {
            // try to obtain a connection from the primary datasource
            ds = conf.getDataSource(null);
    	} catch (UserException uex) {
    	}
    	if (ds==null) {
        	try {
                // use datasource 2 if available
                ds = conf.getDataSource2(null);
        	} catch (UserException uex2) {
        	}
        }
        if (ds != null) {
            return ds.getConnection();
        }
        return null;
    }

    @Override
    protected SQLBuffer toSelect(SQLBuffer select, JDBCFetchConfiguration fetch,
        SQLBuffer tables, SQLBuffer where, SQLBuffer group,
        SQLBuffer having, SQLBuffer order,
        boolean distinct, boolean forUpdate, long start, long end,
        Select sel) {
    	if (!supportsRowNum) {
    		return super.toSelect(select, fetch, tables, where, group, having, order,
    		        distinct, forUpdate, start, end, sel);
    	}
        // if no range, use standard select
        if (!isUsingRange(start, end)) {
            return super.toSelect(select, fetch, tables, where, group, having,
                order, distinct, forUpdate, 0, Long.MAX_VALUE, sel);
        }

        // if no skip, ordering, or distinct can use rownum directly
        SQLBuffer buf = new SQLBuffer(this);
        if (!requiresSubselectForRange(start, end, distinct, order)) {
            if (where != null && !where.isEmpty())
                buf.append(where).append(" AND ");
            buf.append("ROWNUM <= ").appendValue(end);
            return super.toSelect(select, fetch, tables, buf, group, having,
                order, distinct, forUpdate, 0, Long.MAX_VALUE, sel);
        }

        // if there is ordering, skip, or distinct we have to use subselects
        SQLBuffer newsel = super.toSelect(select, fetch, tables, where,
            group, having, order, distinct, forUpdate, 0, Long.MAX_VALUE,
            sel);

        // if no skip, can use single nested subselect
        if (!isUsingOffset(start)) {
            buf.append(getSelectOperation(fetch) + " * FROM (");
            buf.append(newsel);
            buf.append(") WHERE ROWNUM <= ").appendValue(end);
            return buf;
        }

        // with a skip, we have to use a double-nested subselect to put
        // where conditions on the rownum
        buf.append(getSelectOperation(fetch))
           .append(" * FROM (SELECT r.*, ROWNUM RNUM FROM (");
        buf.append(newsel);
        buf.append(") r");
        if (isUsingLimit(end))
            buf.append(" WHERE ROWNUM <= ").appendValue(end);
        buf.append(") WHERE RNUM > ").appendValue(start);
        return buf;
    }

    /**
     * Return true if the select with the given parameters needs a
     * subselect to apply a range.
     */
    private boolean requiresSubselectForRange(long start, long end, boolean distinct, SQLBuffer order) {
    	if (!isUsingRange(start, end))
    		return false;
        return isUsingOffset(start) || distinct || isUsingOrderBy(order);
    }


}
