/*
 * 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.jackrabbit.oak.plugins.document.rdb;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.jackrabbit.oak.commons.properties.SystemPropertySupplier;
import org.apache.jackrabbit.oak.plugins.document.rdb.RDBJDBCTools.PreparedStatementComponent;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Defines variation in the capabilities of different RDBs.
 */
public enum RDBDocumentStoreDB {

    DEFAULT("default") {
    },

    H2("H2") {
        @Override
        public String checkVersion(DatabaseMetaData md) throws SQLException {
            return RDBJDBCTools.versionCheck(md, 1, 4, description);
        }

        @Override
        public String getInitializationStatement() {
            return "create alias if not exists unix_timestamp as $$ long unix_timestamp() { return System.currentTimeMillis()/1000L; } $$;";
        }

        @Override
        public String getCurrentTimeStampInSecondsSyntax() {
            return "select unix_timestamp()";
        }
    },

    DERBY("Apache Derby") {
        @Override
        public String checkVersion(DatabaseMetaData md) throws SQLException {
            return RDBJDBCTools.versionCheck(md, 10, 11, description);
        }

        @Override
        public Map<String, String> getAdditionalStatistics(RDBConnectionHandler ch, String catalog, String tableName) {
            Map<String, String> result = new HashMap<String, String>();
            Connection con = null;

            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("SELECT T.* FROM TABLE (SYSCS_DIAG.SPACE_TABLE(?,?)) AS T")) {
                    stmt.setString(1, catalog.toUpperCase(Locale.ENGLISH));
                    stmt.setString(2, tableName.toUpperCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        long totalSize = 0;
                        long totalIndexSize = 0;
                        int nindexes = 0;
                        while (rs.next()) {
                            String conglomerateName = rs.getString("CONGLOMERATENAME");
                            int isIndex = rs.getInt("ISINDEX");
                            long numAllocatedPages = rs.getLong("NUMALLOCATEDPAGES");
                            long numFreePages = rs.getLong("NUMFREEPAGES");
                            long numUnfilledPages = rs.getLong("NUMUNFILLEDPAGES");
                            int pageSize = rs.getInt("PAGESIZE");
                            String raw = "NUMALLOCATEDPAGES=" + numAllocatedPages + ", NUMFREEPAGES=" + numFreePages
                                    + ", NUMUNFILLEDPAGES=" + numUnfilledPages + ", PAGESIZE=" + pageSize;
                            long size = pageSize * (numAllocatedPages + numFreePages);
                            if (isIndex == 0) {
                                result.put("_data", raw);
                                result.put("storageSize", Long.toString(size));
                            } else {
                                result.put(conglomerateName + "._data", raw);
                                result.put("indexSizes." + conglomerateName, Long.toString(size));
                                totalIndexSize += size;
                                nindexes += 1;
                            }
                            totalSize += pageSize * (numAllocatedPages + numFreePages);
                        }
                        result.put("totalSize", Long.toString(totalSize));
                        result.put("totalIndexSize", Long.toString(totalIndexSize));
                        result.put("nindexes", Long.toString(nindexes));
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            return result;
        }
    },

    POSTGRES("PostgreSQL", RDBCommonVendorSpecificCode.POSTGRES) {
        @Override
        public String checkVersion(DatabaseMetaData md) throws SQLException {
            return RDBJDBCTools.versionCheck(md, 14, 0, 42, 7, description);
        }

        @Override
        public String getCurrentTimeStampInSecondsSyntax() {
            return "select extract(epoch from now())::integer";
        }

        @Override
        public String getTableCreationStatement(String tableName, int schema) {
            return ("create table " + tableName
                    + " (ID varchar(512) not null primary key, MODIFIED bigint, HASBINARY smallint, DELETEDONCE smallint, MODCOUNT bigint, CMODCOUNT bigint, DSIZE bigint, "
                    + (schema >= 1 ? "VERSION smallint, " : "")
                    + (schema >= 2 ? "SDTYPE smallint, SDMAXREVTIME bigint, " : "")
                    + "DATA varchar(16384), BDATA bytea)");
        }

        @Override
        public Map<String, String> getAdditionalStatistics(RDBConnectionHandler ch, String catalog, String tableName) {
            Map<String, String> result = new HashMap<String, String>();
            Connection con = null;
            SortedSet<String> indexNames = Collections.emptySortedSet();

            // get index names
            try {
                SortedSet<String> in = new TreeSet<String>();
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("SELECT indexname FROM pg_indexes WHERE tablename=?")) {
                    stmt.setString(1, tableName.toLowerCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            in.add(rs.getString(1));
                        }
                    }
                }
                con.commit();
                indexNames = in;
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            // table data
            try {
                StringBuilder query = new StringBuilder("SELECT pg_total_relation_size(?), pg_table_size(?), pg_indexes_size(?)");
                indexNames.forEach(name -> query.append(", pg_relation_size(?)"));
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement(query.toString())) {
                    int i = 1;
                    stmt.setString(i++, tableName);
                    stmt.setString(i++, tableName);
                    stmt.setString(i++, tableName);
                    for (String name : indexNames) {
                        stmt.setString(i++, name);
                    }
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            i = 1;
                            result.put("storageSize", rs.getString(i++));
                            result.put("size", rs.getString(i++));
                            result.put("totalIndexSize", rs.getString(i++));
                            for (String name : indexNames) {
                                result.put("indexSizes." + name, rs.getString(i++));
                            }
                        }
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }
            return result;
        }
    },

    DB2("DB2", RDBCommonVendorSpecificCode.DB2) {
        @Override
        public String checkVersion(DatabaseMetaData md) throws SQLException {
            return RDBJDBCTools.versionCheck(md, 11, 5, 4, 26, description);
        }

        @Override
        public String getCurrentTimeStampInSecondsSyntax() {
            return "select cast (days(current_timestamp - current_timezone) - days('1970-01-01') as integer) * 86400 + midnight_seconds(current_timestamp - current_timezone) from sysibm.sysdummy1";
        }

        @Override
        public String getTableCreationStatement(String tableName, int schema) {
            return "create table " + tableName
                    + " (ID varchar(512) not null, MODIFIED bigint, HASBINARY smallint, DELETEDONCE smallint, MODCOUNT bigint, CMODCOUNT bigint, DSIZE bigint, "
                    + (schema >= 1 ? "VERSION smallint, " : "")
                    + (schema >= 2 ? "SDTYPE smallint, SDMAXREVTIME bigint, " : "")
                    + "DATA varchar(16384), BDATA blob(" + 1024 * 1024 * 1024 + "))";
        }

        @Override
        public List<String> getIndexCreationStatements(String tableName, int schema) {
            List<String> statements = new ArrayList<String>();
            String pkName = tableName + "_pk";
            statements.add("create unique index " + pkName + " on " + tableName + " ( ID ) cluster");
            statements.add("alter table " + tableName + " add constraint " + pkName + " primary key ( ID )");
            statements.addAll(super.getIndexCreationStatements(tableName, schema));
            return statements;
        }

        @Override
        public Map<String, String> getAdditionalStatistics(RDBConnectionHandler ch, String catalog, String tableName) {

            Map<String, String> result = new HashMap<String, String>();

            Connection con = null;

            // table data
            String tableStats = SystemPropertySupplier
                    .create(SYSPROP_PREFIX + ".DB2.TABLE_STATS", "card npages mpages fpages overflow pctfree avgrowsize stats_time")
                    .loggingTo(LOG).get();

            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("SELECT * FROM syscat.tables WHERE tabschema=? and tabname=?")) {
                    stmt.setString(1, catalog.toUpperCase(Locale.ENGLISH));
                    stmt.setString(2, tableName.toUpperCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String data = extractFields(rs, tableStats);
                            result.put("_data", data);
                        }
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            // index data
            String indexStats = SystemPropertySupplier.create(SYSPROP_PREFIX + ".DB2.INDEX_STATS",
                    "indextype colnames pctfree clusterratio nleaf nlevels fullkeycard density indcard numrids numrids_deleted avgleafkeysize avgnleafkeysize remarks stats_time")
                    .loggingTo(LOG).get();

            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con
                        .prepareStatement("SELECT * FROM syscat.indexes WHERE tabschema=? and tabname=?")) {
                    stmt.setString(1, catalog.toUpperCase(Locale.ENGLISH));
                    stmt.setString(2, tableName.toUpperCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String index = rs.getString("indname");
                            String data = extractFields(rs, indexStats);
                            result.put("index." + index + "._data", data);
                        }
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            return result;
        }

        public String makeIndexConditionalForColumn(String columnName) {
            return " exclude null keys";
        }
    },

    ORACLE("Oracle", RDBCommonVendorSpecificCode.ORACLE) {
        @Override
        public String checkVersion(DatabaseMetaData md) throws SQLException {
            return RDBJDBCTools.versionCheck(md, 12, 1, 12, 2, description);
        }

        @Override
        public String getCurrentTimeStampInSecondsSyntax() {
            return "select (trunc(sys_extract_utc(systimestamp)) - to_date('01/01/1970', 'MM/DD/YYYY')) * 24 * 60 * 60 + to_number(to_char(sys_extract_utc(systimestamp), 'SSSSS')) from dual";
        }

        @Override
        public String getInitializationStatement() {
            // see https://issues.apache.org/jira/browse/OAK-1914
            // for some reason, the default for NLS_SORT is incorrect
            return ("ALTER SESSION SET NLS_SORT='BINARY'");
        }

        @Override
        public String getTableCreationStatement(String tableName, int schema) {
            // see https://issues.apache.org/jira/browse/OAK-1914
            return ("create table " + tableName
                    + " (ID varchar(512) not null primary key, MODIFIED number, HASBINARY number, DELETEDONCE number, MODCOUNT number, CMODCOUNT number, DSIZE number, "
                    + (schema >= 1 ? "VERSION number, " : "")
                    + (schema >= 2 ? "SDTYPE number, SDMAXREVTIME number, " : "")
                    + "DATA varchar(4000), BDATA blob)");
        }

        @Override
        public Map<String, String> getAdditionalStatistics(RDBConnectionHandler ch, String catalog, String tableName) {

            Map<String, String> result = new HashMap<String, String>();

            Connection con = null;

            // table data
            String tableStats = SystemPropertySupplier
                    .create(SYSPROP_PREFIX + ".ORACLE.TABLE_STATS", "num_rows blocks avg_row_len sample_size last_analyzed")
                    .loggingTo(LOG).get();

            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("SELECT * FROM user_tables WHERE table_name=?")) {
                    stmt.setString(1, tableName.toUpperCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String data = extractFields(rs, tableStats);
                            result.put("_data", data.toString());
                        }
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            // index data
            String indexStats = SystemPropertySupplier.create(SYSPROP_PREFIX + ".ORACLE.INDEX_STATS",
                    "blevel leaf_blocks distinct_keys avg_leaf_blocks_per_key avg_data_blocks_per_key clustering_factor num_rows sample_size last_analyzed")
                    .loggingTo(LOG).get();

            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("SELECT * FROM user_indexes WHERE table_name=?")) {
                    stmt.setString(1, tableName.toUpperCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String index = rs.getString("index_name");
                            String data = extractFields(rs, indexStats);
                            result.put("index." + index + "._data", data);
                        }
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            return result;
        }

        @Override
        public String getSmallintType() {
            return "number";
        }

        @Override
        public String getBigintType() {
            return "number";
        }
    },

    MYSQL("MySQL", RDBCommonVendorSpecificCode.MYSQL) {
        @Override
        public String checkVersion(DatabaseMetaData md) throws SQLException {
            return RDBJDBCTools.versionCheck(md, 5, 5, description);
        }

        @Override
        public String getCurrentTimeStampInSecondsSyntax() {
            return "select unix_timestamp()";
        }

        @Override
        public String getTableCreationStatement(String tableName, int schema) {
            // see https://issues.apache.org/jira/browse/OAK-1913
            return ("create table " + tableName
                    + " (ID varbinary(512) not null primary key, MODIFIED bigint, HASBINARY smallint, DELETEDONCE smallint, MODCOUNT bigint, CMODCOUNT bigint, DSIZE bigint, "
                    + (schema >= 1 ? "VERSION smallint, " : "")
                    + (schema >= 2 ? "SDTYPE smallint, SDMAXREVTIME bigint, " : "")
                    + "DATA varchar(16000), BDATA longblob)");
        }

        @Override
        public FETCHFIRSTSYNTAX getFetchFirstSyntax() {
            return FETCHFIRSTSYNTAX.LIMIT;
        }

        @Override
        public PreparedStatementComponent getConcatQuery(final String appendData, final int dataOctetLimit) {
            return new PreparedStatementComponent() {

                @Override
                public String getStatementComponent() {
                    return "CONCAT(DATA, ?)";
                }

                @Override
                public int setParameters(PreparedStatement stmt, int startIndex) throws SQLException {
                    stmt.setString(startIndex++, appendData);
                    return startIndex;
                }
            };
        }

        @Override
        public Map<String, String> getAdditionalStatistics(RDBConnectionHandler ch, String catalog, String tableName) {

            Map<String, String> result = new HashMap<String, String>();

            Connection con = null;

            // table data
            String tableStats = SystemPropertySupplier
                    .create(SYSPROP_PREFIX + ".MYSQL.TABLE_STATS",
                            "engine version row_format rows avg_row_length data_length index_length data_free collation")
                    .loggingTo(LOG).get();

            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("show table status from " + catalog + " where name=?")) {
                    stmt.setString(1, tableName.toUpperCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String data = extractFields(rs, tableStats);
                            result.put("_data", data.toString());
                        }
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            // index data
            String indexStats = SystemPropertySupplier
                    .create(SYSPROP_PREFIX + ".MYSQL.INDEX_STATS", "column_name cardinality index_type sub_part").loggingTo(LOG)
                    .get();

            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("show index from " + tableName + " in " + catalog)) {
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String index = rs.getString("key_name");
                            String data = extractFields(rs, indexStats);
                            result.put("index." + index + "._data", data);
                        }
                    }
                }
                con.commit();
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            return result;
        }
    },

    MSSQL("Microsoft SQL Server", RDBCommonVendorSpecificCode.MSSQL) {
        @Override
        public String checkVersion(DatabaseMetaData md) throws SQLException {
            return RDBJDBCTools.versionCheck(md, 11, 0, description);
        }

        @Override
        public String getTableCreationStatement(String tableName, int schema) {
            // see https://issues.apache.org/jira/browse/OAK-2395
            return ("create table " + tableName
                    + " (ID varbinary(512) not null, MODIFIED bigint, HASBINARY smallint, DELETEDONCE smallint, MODCOUNT bigint, CMODCOUNT bigint, DSIZE bigint, "
                    + (schema >= 1 ? "VERSION smallint, " : "")
                    + (schema >= 2 ? "SDTYPE smallint, SDMAXREVTIME bigint, " : "")
                    + "DATA nvarchar(4000), BDATA varbinary(max), "
                    + "constraint "  + tableName + "_PK primary key clustered (ID ASC))");
        }

        @Override
        public FETCHFIRSTSYNTAX getFetchFirstSyntax() {
            return FETCHFIRSTSYNTAX.TOP;
        }

        @Override
        public PreparedStatementComponent getConcatQuery(final String appendData, final int dataOctetLimit) {
            return new PreparedStatementComponent() {

                @Override
                // this statement ensures that SQL server will generate an exception on overflow
                public String getStatementComponent() {
                    return "CASE WHEN LEN(DATA) < ? THEN (DATA + CAST(? AS nvarchar(" + dataOctetLimit
                            + "))) ELSE (DATA + CAST(DATA AS nvarchar(max))) END";
                }

                @Override
                public int setParameters(PreparedStatement stmt, int startIndex) throws SQLException {
                    stmt.setInt(startIndex++, dataOctetLimit - appendData.length());
                    stmt.setString(startIndex++, appendData);
                    return startIndex;
                }
            };
        }

        @Override
        public String getCurrentTimeStampInSecondsSyntax() {
            return "select datediff(second, dateadd(second, datediff(second, getutcdate(), getdate()), '1970-01-01'), getdate())";
        }

        private long parseSize(String readable) {
            try {
                if (readable != null && readable.endsWith(" KB")) {
                    return 1024 * Long.parseLong(readable.substring(0, readable.length() - 3));
                } else {
                    return -1;
                }
            } catch (NumberFormatException ex) {
                return -1;
            }
        }

        @Override
        public Map<String, String> getAdditionalStatistics(RDBConnectionHandler ch, String catalog, String tableName) {
            Map<String, String> result = new HashMap<String, String>();
            Connection con = null;

            // table data
            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement("exec sp_spaceused ?")) {
                    stmt.setString(1, tableName.toLowerCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            long treserved = parseSize(rs.getString("reserved"));
                            long tdata = parseSize(rs.getString("data"));
                            long tindexSize = parseSize(rs.getString("index_size"));
                            long tunused = parseSize(rs.getString("unused"));
                            if (treserved >= 0 && tdata >= 0 && tindexSize >= 0 && tunused >= 0) {
                                result.put("storageSize", Long.toString(treserved + tdata + tindexSize + tunused));
                                result.put("size", Long.toString(treserved + tdata + tunused));
                                result.put("totalIndexSize", Long.toString(tindexSize));
                            }
                            String data = extractFields(rs, "rows reserved data index_size unused");
                            result.put("_data", data);
                        }
                    }
                    con.commit();
                }
            } catch (SQLException ex) {
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            // index data
            try {
                con = ch.getROConnection();
                try (PreparedStatement stmt = con.prepareStatement(
                        "SELECT i.[name] AS name, i.[fill_factor] as fill_factor, i.[type_desc] as type_desc, SUM(s.[row_count]) as rows, SUM(s.[used_page_count] * 8) as usedKB, SUM(s.[reserved_page_count] * 8) as reservedKB "
                                + "FROM sys.dm_db_partition_stats AS s "
                                + "INNER JOIN sys.indexes AS i ON s.[object_id] = i.[object_id] "
                                + "AND s.[index_id] = i.[index_id] WHERE i.[object_id]=OBJECT_ID(?) "
                                + "GROUP BY i.[name], i.[fill_factor], i.[type_desc]")) {
                    stmt.setString(1, tableName.toLowerCase(Locale.ENGLISH));
                    try (ResultSet rs = stmt.executeQuery()) {
                        while (rs.next()) {
                            String index = rs.getString("name");
                            String data = extractFields(rs, "rows usedKB reservedKB type_desc fill_factor");
                            result.put("index." + index + "._data", data);
                        }
                    }
                    con.commit();
                }
                con.commit();
            } catch (SQLException ex) {
                ex.printStackTrace();
                LOG.debug("while getting diagnostics", ex);
            } finally {
                ch.closeConnection(con);
            }

            return result;
        }

        public String makeIndexConditionalForColumn(String columnName) {
            return " where " + columnName + " is not null";
        }
    };

    private static final Logger LOG = LoggerFactory.getLogger(RDBDocumentStoreDB.class);

    private static final String SYSPROP_PREFIX = "org.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore";

    public enum FETCHFIRSTSYNTAX {
        FETCHFIRST, LIMIT, TOP
    };

    /**
     * Check the database brand and version
     */
    public String checkVersion(DatabaseMetaData md) throws SQLException {
        return "Unknown database type: " + md.getDatabaseProductName();
    }

    /**
     * Query syntax for "FETCH FIRST"
     */
    public FETCHFIRSTSYNTAX getFetchFirstSyntax() {
        return FETCHFIRSTSYNTAX.FETCHFIRST;
    }

    /**
     * Query syntax for current time in ms since the epoch
     * 
     * @return the query syntax or empty string when no such syntax is available
     */
    public String getCurrentTimeStampInSecondsSyntax() {
        // unfortunately, we don't have a portable statement for this
        return "";
    }

    /**
     * Returns the CONCAT function or its equivalent function or sub-query. Note
     * that the function MUST NOT cause a truncated value to be written!
     *
     * @param appendData
     *            string to be inserted
     * @param dataOctetLimit
     *            expected capacity of data column
     */
    public PreparedStatementComponent getConcatQuery(final String appendData, final int dataOctetLimit) {

        return new PreparedStatementComponent() {

            @Override
            public String getStatementComponent() {
                return "DATA || CAST(? AS varchar(" + dataOctetLimit + "))";
            }

            @Override
            public int setParameters(PreparedStatement stmt, int startIndex) throws SQLException {
                stmt.setString(startIndex++, appendData);
                return startIndex;
            }
        };
    }

    /**
     * Query for any required initialization of the DB.
     * 
     * @return the DB initialization SQL string
     */
    public @NotNull String getInitializationStatement() {
        return "";
    }

    /**
     * Table creation statement string
     *
     * @param tableName
     * @return the table creation string
     */
    public String getTableCreationStatement(String tableName, int schema) {
        return "create table " + tableName
                + " (ID varchar(512) not null primary key, MODIFIED bigint, HASBINARY smallint, DELETEDONCE smallint, MODCOUNT bigint, CMODCOUNT bigint, DSIZE bigint, "
                + (schema >= 1 ? "VERSION smallint, " : "")
                + (schema >= 2 ? "SDTYPE smallint, SDMAXREVTIME bigint, " : "")
                + "DATA varchar(16384), BDATA blob(" + 1024 * 1024 * 1024 + "))";
    }

    public List<String> getIndexCreationStatements(String tableName, int level) {
        List<String> result = new ArrayList<>();
        result.add("create index " + tableName + "_MOD on " + tableName + " (MODIFIED)");
        if (level == 2) {
            result.add("create index " + tableName + "_VSN on " + tableName + " (VERSION)");
            result.add(
                    "create index " + tableName + "_SDT on " + tableName + " (SDTYPE)" + makeIndexConditionalForColumn("SDTYPE"));
            result.add("create index " + tableName + "_SDM on " + tableName + " (SDMAXREVTIME)"
                    + makeIndexConditionalForColumn("SDMAXREVTIME"));
        }
        return result;
    }

    @NotNull
    public Map<String, String> getAdditionalDiagnostics(RDBConnectionHandler ch, String tableName) {
        return vendorCode.getAdditionalDiagnostics(ch, tableName);
    }

    /**
     * Returns additional DB-specific statistics, augmenting the return value of
     * {@link RDBDocumentStore#getStats()}.
     * <p>
     * Where applicable, the following fields are returned similar to the output
     * for MongoDB:
     * <dl>
     * <dt>storageSize</dt>
     * <dd>total size of table</dd>
     * <dt>size</dt>
     * <dd>size of table (excl. indexes)</dd>
     * <dt>totalIndexSize</dt>
     * <dd>total size of all indexes</dd>
     * <dt>indexSizes.<em>indexName</em></dt>
     * <dd>size of individual indexes</dd>
     * </dl>
     * <p>
     * Additionally, a information obtained from the databases system
     * tables/views can be included:
     * <dl>
     * <dt>_data</dt>
     * <dd>table specific data</dd>
     * <dt><em>indexName</em>._data</dt>
     * <dd>index specific data</dd>
     * </dl>
     * <p>
     * These fields will just contain DB-specific name/value pairs obtained from
     * the database. The exact fields to fetch are preconfigured by can be tuned
     * using system properties, such as:
     * 
     * <pre>
     * -Dorg.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.DB2.TABLE_STATS="card npages mpages fpages overflow pctfree avgrowsize stats_time"
     * -Dorg.apache.jackrabbit.oak.plugins.document.rdb.RDBDocumentStore.DB2.INDEX_STATS="indextype colnames pctfree clusterratio nleaf nlevels fullkeycard density indcard numrids numrids_deleted avgleafkeysize avgnleafkeysize remarks stats_time"
     * </pre>
     * <p>
     * (this currently applies to DB types: {@link #DB2}, {@link #ORACLE}, and
     * {@link #MYSQL}).
     * <p>
     * See links below for the definition of the individual fields:
     * <ul>
     * <li>{@link #POSTGRES} - <a href=
     * "https://www.postgresql.org/docs/9.6/static/functions-admin.html#FUNCTIONS-ADMIN-DBOBJECT">PostgreSQL
     * 9.6.6 Documentation - 9.26.7. Database Object Management Functions</a>
     * <li>{@link #DB2} - DB2 10.5 for Linux, UNIX, and Windows: <a href=
     * "https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.5.0/com.ibm.db2.luw.sql.ref.doc/doc/r0001063.html">SYSCAT.TABLES
     * catalog view</a>, <a href=
     * "https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.5.0/com.ibm.db2.luw.sql.ref.doc/doc/r0001047.html">SYSCAT.INDEXES
     * catalog view</a>
     * <li>{@link #ORACLE} - Oracle Database Online Documentation 12c Release 1
     * (12.1): <a href=
     * "https://docs.oracle.com/database/121/REFRN/GUID-6823CD28-0681-468E-950B-966C6F71325D.htm#REFRN20286">3.118
     * ALL_TABLES</a>, <a href=
     * "https://docs.oracle.com/database/121/REFRN/GUID-E39825BA-70AC-45D8-AF30-C7FF561373B6.htm#REFRN20088">2.127
     * ALL_INDEXES</a>
     * <li>{@link #MYSQL} - MySQL 5.7 Reference Manual: <a href=
     * "https://dev.mysql.com/doc/refman/5.7/en/show-table-status.html">13.7.5.36
     * SHOW TABLE STATUS Syntax</a>, <a href=
     * "https://dev.mysql.com/doc/refman/5.7/en/show-index.html">13.7.5.22 SHOW
     * INDEX Syntax</a>
     * <li>{@link #MSSQL} - Developer Reference for SQL Server: <a href=
     * "https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-db-partition-stats-transact-sql">sys.dm_db_partition_stats
     * (Transact-SQL)</a>, <a href=
     * "https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-spaceused-transact-sql">sp_spaceused
     * (Transact-SQL)</a>
     * <li>{@link #DERBY} - Derby Reference Manual: <a href=
     * "http://db.apache.org/derby/docs/10.14/ref/rrefsyscsdiagspacetable.html">SYSCS_DIAG.SPACE_TABLE diagnostic table function</a>
     * </ul>
     */
    public Map<String, String> getAdditionalStatistics(RDBConnectionHandler ch, String catalog, String tableName) {
        return Collections.emptyMap();
    }

    public String getSmallintType() {
        return "smallint";
    }

    public String getBigintType() {
        return "bigint";
    }

    public String makeIndexConditionalForColumn(String columnName) {
        return "";
    }

    public String getModifiedIndexStatement(String tableName) {
        return "create index " + tableName + "_MOD on " + tableName + " (MODIFIED)";
    }

    /**
     * Statements needed to upgrade the DB
     *
     * @return the table modification string
     */
    public List<String> getTableUpgradeStatements(String tableName, int level) {
        String smallint = getSmallintType();
        String bigint = getBigintType();
        if (level == 1) {
            return Collections.singletonList("alter table " + tableName + " add VERSION " + smallint);
        } else if (level == 2) {
            String[] statements = new String[] { "alter table " + tableName + " add SDTYPE " + smallint,
                    "alter table " + tableName + " add SDMAXREVTIME " + bigint,
                    "create index " + tableName + "_VSN on " + tableName + " (VERSION)",
                    "create index " + tableName + "_SDT on " + tableName + " (SDTYPE)" + makeIndexConditionalForColumn("SDTYPE"),
                    "create index " + tableName + "_SDM on " + tableName + " (SDMAXREVTIME)"
                            + makeIndexConditionalForColumn("SDMAXREVTIME"), };
            return Arrays.asList(statements);
        } else {
            throw new IllegalArgumentException("level must be 1 or 2");
        }
    }

    protected String description;

    protected RDBCommonVendorSpecificCode vendorCode = RDBCommonVendorSpecificCode.DEFAULT;

    protected String extractFields(ResultSet rs, String indexStats) throws SQLException {
        StringBuilder data = new StringBuilder();
        for (String f : indexStats.split(" ")) {
            String fn = f.trim();
            if (!fn.isEmpty()) {
                if (data.length() != 0) {
                    data.append(", ");
                }
                String v = rs.getString(fn);
                data.append(fn).append(": ").append(v == null ? v : v.trim());
            }
        }
        return data.toString();
    }

    private RDBDocumentStoreDB(String description) {
        this.description = description;
        this.vendorCode = RDBCommonVendorSpecificCode.DEFAULT;
    }

    private RDBDocumentStoreDB(String description, RDBCommonVendorSpecificCode vendorCode) {
        this.description = description;
        this.vendorCode = vendorCode;
    }

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

    @NotNull
    public static RDBDocumentStoreDB getValue(String desc) {
        for (RDBDocumentStoreDB db : RDBDocumentStoreDB.values()) {
            if (db.description.equals(desc)) {
                return db;
            } else if (db == DB2 && desc.startsWith("DB2/")) {
                return db;
            }
        }

        LOG.error("DB type " + desc + " unknown, trying default settings");
        DEFAULT.description = desc + " - using default settings";
        return DEFAULT;
    }
}
