/*

   Derby - Class org.apache.derbyTesting.functionTests.tests.memorydb.BasicInMemoryDbTest

       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.derbyTesting.functionTests.tests.memorydb;

import java.io.File;
import java.io.IOException;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import junit.framework.Test;
import org.apache.derbyTesting.functionTests.util.PrivilegedFileOpsForTests;
import org.apache.derbyTesting.functionTests.util.streams.CharAlphabet;
import org.apache.derbyTesting.functionTests.util.streams.LoopingAlphabetReader;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.BaseTestSuite;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.SupportFilesSetup;

/**
 * Basic tests of the in-memory db storage back end.
 */
public class BasicInMemoryDbTest
        extends BaseJDBCTestCase {

    /**
     * Helper for dealing with memory databases. For now we use a single
     * instance for all test classes / cases, as the tests are run single
     * threaded.
     */
    private static final MemoryDbManager dbm =
            MemoryDbManager.getSharedInstance();

    public BasicInMemoryDbTest(String name) {
        super(name);
    }

    /**
     * Closes all opened statements and connections that are known, and also
     * deletes all known in-memory databases.
     *
     * @throws Exception if something goes wrong
     */
    public void tearDown()
            throws Exception {
        dbm.cleanUp();
        super.tearDown();
    }

    /**
     * Helper method to make sure the driver is loaded. May be needed when
     * calling DriverManager.getConnection() directly since the driver may
     * not already be loaded (in particular when running from classes so that
     * driver auto-loading doesn't come into play), or it may have been
     * unloaded by an engine shutdown in an earlier test case.
     */
    private void loadDriver() throws SQLException {
        // If we get a default connection through the framework, the driver
        // will be loaded implicitly.
        getConnection();
    }

    /**
     * Tries to connect to a non-existing database with the in-memory protocol,
     * expecting an error saying the database doesn't exist.
     */
    public void testFunctionalityPresent() {
        try {
            dbm.getConnection("nonExistingDb");
        } catch (SQLException e) {
            // Expect a database not found exception.
            assertSQLState("XJ004", e);
        }
    }

    /**
     * Performs a cycle to test that the in-memory db is compatible with the
     * deafult directory protocol.
     * <p>
     * <ol> <li>Create an in-memory db and add a table with a few rows.</li>
     *      <li>Backup to disk.</li>
     *      <li>Boot the database with the directory (default) protocol.</li>
     *      <li>Verify content, add a new row, shutdown.</li>
     *      <li>Use createFrom to restore database from disk into the in-memory
     *          representation.</li>
     *      <li>Verify new content.</li>
     * </ol>
     *
     * @throws IOException if something goes wrong
     * @throws SQLException if something goes wrong
     */
    public void testCreateBackupBootRestore()
            throws IOException, SQLException {
        // 1. Create the database with the in-memory protocol.
        Connection memCon = dbm.createDatabase("newMemDb");
        // Make sure the database is newly created.
        assertNull(memCon.getWarnings());
        Statement stmt = dbm.createStatement(memCon);
        stmt.executeUpdate("create table toverify(" +
                "id int, val1 varchar(10), val2 clob, primary key(id))");
        PreparedStatement ps = dbm.prepareStatement(memCon,
                "insert into toverify values (?,?,?)");
        // The content to insert into the table.
        String[][] firstContent = new String[][] {
            {"1", "one", getString(1000, CharAlphabet.modernLatinLowercase())},
            {"2", "two", getString(10000, CharAlphabet.tamil())},
            {"3", "three", getString(50000, CharAlphabet.cjkSubset())}
        };
        for (int i=0; i < firstContent.length; i++) {
            ps.setString(1, firstContent[i][0]);
            ps.setString(2, firstContent[i][1]);
            ps.setString(3, firstContent[i][2]);
            ps.executeUpdate();
        }
        ResultSet rs = stmt.executeQuery("select * from toverify");
        JDBC.assertFullResultSet(rs, firstContent);
        ps.close();
        stmt.close();

        // 2. Backup the database.
        String dbPath = SupportFilesSetup.getReadWrite("backedUpDb").getPath();
        CallableStatement cs = memCon.prepareCall(
                "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)");
        cs.setString(1, dbPath);
        cs.execute();
        memCon.close();

        // 3. Open the database with the default protocol.
        String dbPathBackedUp = PrivilegedFileOpsForTests.getAbsolutePath(
                new File(dbPath, "newMemDb"));
        Connection dirCon = DriverManager.getConnection(
                "jdbc:derby:" + dbPathBackedUp);
        // 4. Verify content, then add one more row.
        stmt = dirCon.createStatement();
        rs = stmt.executeQuery("select * from toverify");
        JDBC.assertFullResultSet(rs, firstContent);
        ps = dirCon.prepareStatement("insert into toverify values (?,?,?)");
        String[] rowToAdd = new String[] {
            "4", "four", getString(32*1024, CharAlphabet.tamil())};
        ps.setString(1, rowToAdd[0]);
        ps.setString(2, rowToAdd[1]);
        ps.setString(3, rowToAdd[2]);
        ps.executeUpdate();
        ps.close();
        dirCon.close();
        // Shutdown.
        try {
            DriverManager.getConnection(
                "jdbc:derby:" + dbPathBackedUp + ";shutdown=true");
        } catch (SQLException sqle) {
            assertSQLState("08006", sqle);
        }

        // 5. Restore modified backup into memory.
        memCon = dbm.getConnection("newMemDb2;createFrom=" + dbPathBackedUp);

        // 6. Verify the new content, where the original in-memory database was
        //    backed up and the directory protocol was used to add one more row
        //    to the backed up database. Now we have restored the on-disk
        //    modified backup, again representing it as an in-memory database.
        stmt = dbm.createStatement(memCon);
        rs = stmt.executeQuery("select * from toverify");
        String[][] secondContent = new String[4][3];
        System.arraycopy(firstContent, 0, secondContent, 0, 3);
        System.arraycopy(rowToAdd, 0, secondContent[3], 0, 3);
        JDBC.assertFullResultSet(rs, secondContent);

        // Delete the second in memory database.
        dbm.dropDatabase("newMemDb2");
    }

    /**
     * Makes sure shutting down an in-memory database works.
     *
     * @throws SQLException if something goes wrong
     */
    public void testShutdown()
            throws SQLException {
        dbm.createDatabase("/tmp/myDB");
        try {
            DriverManager.getConnection(
                    "jdbc:derby:memory:/tmp/myDB;shutdown=true");
            fail("Engine shutdown should have caused exception");
        } catch (SQLException sqle) {
            assertSQLState("08006", sqle);
        }
    }

    /**
     * Makes sure shutting down the Derby engine with an in-memory database
     * already booted works.
     * <p>
     * Related to DERBY-4093
     *
     * @throws SQLException if something goes wrong
     */
    public void testEnginehutdown()
            throws SQLException {
        dbm.createDatabase("/tmp/myDB");
        try {
            DriverManager.getConnection(
                    "jdbc:derby:;shutdown=true");
            fail("Engine shutdown should have caused exception");
        } catch (SQLException sqle) {
            assertSQLState("XJ015", sqle);
        }
        // Another hack, to make sure later tests in this class doesn't fail.
        // Get a connection to the default database to reload the engine.
        loadDriver();
    }

    /**
     * Verify that booting two databases with the same name but with different
     * subsubprotocols doesn't result in two connections to the same database.
     *
     * @throws SQLException if something goes wrong
     */
    public void testBootSameDbDifferentSubSubProtocol()
            throws SQLException {
        final String dbName = "BSDDSSP";
        // Connect to the in-memory database and create a table.
        Connection con1 = dbm.createDatabase(dbName);
        Statement stmt1 = dbm.createStatement(con1);
        stmt1.execute("create table t (text varchar(255))");
        stmt1.execute("insert into t values ('Inserted into in-memory db')");
        // Connect to the on-disk database. The table we created in the
        // in-memory database shouldn't exist in the on-disk database.
        Connection con2 = DriverManager.getConnection(
                "jdbc:derby:" + dbName + ";create=true");
        // Table t should not exist.
        Statement stmt2 = con2.createStatement();
        try {
            stmt2.executeQuery("select * from t");
            fail("Table 't' should not exist");
        } catch (SQLException sqle) {
            assertSQLState("42X05", sqle);
        }
        con2.close();
    }

    /**
     * Test deletion of an in-memory database:
     *  - create database
     *  - delete database
     *  - try to connection to database, should fail
     *  - recreate and delete again
     *
     * @throws SQLException if something else goes wrong
     */
    public void testDelete()
            throws SQLException {
            loadDriver();
            Connection conCreate = DriverManager.getConnection(
                    "jdbc:derby:memory:deleteDbSimple;create=true");
            Statement stmt = dbm.createStatement(conCreate);
            JDBC.assertDrainResults(stmt.executeQuery(
                    "select * from sys.systables"));
            // Delete the database.
            try {
                DriverManager.getConnection(
                    "jdbc:derby:memory:deleteDbSimple;drop=true");
                fail("Dropping database should have raised exception.");
            } catch (SQLException sqle) {
                assertSQLState("08006", sqle);
            }
            // Try to connect to the database again, without creation.
            try {
                DriverManager.getConnection(
                    "jdbc:derby:memory:deleteDbSimple;create=false");
                fail("Database should not exist after deletion.");
            } catch (SQLException sqle) {
                assertSQLState("XJ004", sqle);
            }

            // Recreate and delete again.
            conCreate = DriverManager.getConnection(
                    "jdbc:derby:memory:deleteDbSimple;create=true");
            stmt = dbm.createStatement(conCreate);
            JDBC.assertDrainResults(stmt.executeQuery(
                    "select * from sys.systables"));
            // Delete the database.
            try {
                DriverManager.getConnection(
                    "jdbc:derby:memory:deleteDbSimple;drop=true");
                fail("Dropping database should have raised exception.");
            } catch (SQLException sqle) {
                assertSQLState("08006", sqle);
            }
    }

    /**
     * Deletes the database when in use by a different connection.
     * <p>
     * The expected outcome is that the first connection will be closed when
     * the second one deletes the database.
     *
     * @throws IOException if something goes wrong
     * @throws SQLException if something goes wrong
     */
    public void testDeleteWhenInUse()
            throws IOException, SQLException {
        loadDriver();
        Connection con = DriverManager.getConnection(
                "jdbc:derby:memory:deleteDb;create=true");
        PreparedStatement ps = dbm.prepareStatement(con,
                "select * from sys.systables");
        JDBC.assertDrainResults(ps.executeQuery());
        // Delete the database.
        try {
            DriverManager.getConnection(
                "jdbc:derby:memory:deleteDb;drop=true");
            fail("Dropping database should have raised exception.");
        } catch (SQLException sqle) {
            assertSQLState("08006", sqle);
        }
        // Execute query from first connection again.
        assertTrue(con.isClosed());
        try {
            JDBC.assertDrainResults(ps.executeQuery());
            fail("Database has been dropped, query shouldn't work.");
        } catch (SQLException sqle) {
            // Expect no current connection.
            assertSQLState("08003", sqle);
        }
    }

    /**
     * Shuts down the database when in use by a different connection.
     * <p>
     * The expected outcome is that the first connection will be closed when
     * the second one shuts down the database.
     *
     * @throws IOException if something goes wrong
     * @throws SQLException if something goes wrong
     */
    public void testShutdownWhenInUse()
            throws IOException, SQLException {
        Connection con = dbm.createDatabase("deleteDb");
        PreparedStatement ps = dbm.prepareStatement(con,
                "select * from sys.systables");
        JDBC.assertDrainResults(ps.executeQuery());
        // Delete the database.
        try {
            DriverManager.getConnection(
                "jdbc:derby:memory:deleteDb;shutdown=true");
            fail("Database shutdown should have raised exception.");
        } catch (SQLException sqle) {
            assertSQLState("08006", sqle);
        }
        // Execute query from first connection again.
        assertTrue(con.isClosed());
        try {
            JDBC.assertDrainResults(ps.executeQuery());
            fail("Database has been shut down, query shouldn't work.");
        } catch (SQLException sqle) {
            // Expect no current connection.
            assertSQLState("08003", sqle);
        }
    }

    public static Test suite() {
        // Run only in embedded-mode for now.
        return new SupportFilesSetup(
            new BaseTestSuite(BasicInMemoryDbTest.class));
    }

    /**
     * Generates a string.
     *
     * @param length length of the string
     * @param alphabet the alphabet to use for the content
     * @return A string.
     * @throws IOException if reading from the source stream fails
     */
    public static String getString(int length, CharAlphabet alphabet)
            throws IOException {
        LoopingAlphabetReader reader =
                new LoopingAlphabetReader(length, alphabet);
        char[] strChar = new char[length];
        int read = 0;
        while (read < length) {
            int readNow = reader.read(strChar, read, length - read);
            if (readNow < 1) {
                fail("Creating string failed, stream returned " + readNow);
            }
            read += readNow;
        }
        return String.copyValueOf(strChar);
    }
}
