/*

   Derby - Class org.apache.derbyTesting.functionTests.tests.lang.LuceneSupportPermsTest

   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.lang;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
import java.util.Locale;
import junit.framework.Test;
import org.apache.derby.iapi.sql.conn.ConnectionUtil;
import org.apache.derby.optional.api.LuceneIndexDescriptor;
import org.apache.derby.optional.api.LuceneUtils;
import org.apache.derbyTesting.junit.BaseTestSuite;
import org.apache.derbyTesting.junit.DatabasePropertyTestSetup;
import org.apache.derbyTesting.junit.LocaleTestSetup;
import org.apache.derbyTesting.junit.SecurityManagerSetup;
import org.apache.derbyTesting.junit.TestConfiguration;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.Version;

/**
 * <p>
 * Test permissions on objects created by the optional Lucene support tool.
 * </p>
 */
public class LuceneSupportPermsTest extends GeneratedColumnsHelper
{
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // CONSTANTS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    private static  final   String      LUCENE_VERSION_PROPERTY = "derby.tests.lucene.version";
    private static  String              LUCENE_VERSION = "LUCENE_47";

    private static  final   String      DB_NAME = "lucenesupportpermsdb";

    private static  final   String      TEST_DBO = "TEST_DBO";
    private static  final   String      RUTH = "RUTH";
    private static  final   String      ALICE = "ALICE";
    private static  final   String      FRANK = "FRANK";
    private static  final   String[]    LEGAL_USERS = { TEST_DBO, ALICE, RUTH, FRANK  };

	private static  final   String      AUTH_NO_ACCESS_NOT_OWNER    = "42507";
	private static  final   String      DBO_ONLY                                = "4251D";
    private static  final   String      FUNCTION_EXISTS                 = "X0Y68";

    private static  final   String      NOT_INDEXABLE                   = "42XBA";
    private static  final   String      NO_PRIMARY_KEY              = "42XBB";
    // LUCENE_UNSUPPORTED_TYPE = "42XBC": only raised if key type is unsupported. but all indexable types are supported.
    // LUCENE_INVALID_CHARACTER = "42XBD" is tested by LuceneSupportTest.
    private static  final   String      NONEXISTENT_INDEX           = "42XBE";
	private static  final   String      NO_DDL_PRIV                    = "42XBF";
	private static  final   String      DOUBLE_LOAD_ILLEGAL         = "42XBG";
	private static  final   String      DOUBLE_UNLOAD_ILLEGAL       = "42XBH";
	private static  final   String      BAD_DIRECTORY                      = "42XBI";
	private static  final   String      BAD_COLUMN_NAME                 = "42XBJ";
    private static  final   String      NONEXISTENT_TABLE_FUNCTION  ="42ZB4";
    private static  final   String      INCOMPATIBLE_ENCRYPTION = "42XBL";
    private static  final   String      ILLEGAL_NULL_ARG = "42XBM";
    private static  final   String      BAD_FIELD_NAME = "42XBN";
    private static  final   String      DUPLICATE_FIELD_NAME = "42XBO";
    private static  final   String      NULL_PRIMITIVE_ARG = "39004";

    private static  final   String      POLICY_FILE = "org/apache/derbyTesting/functionTests/tests/lang/luceneSupport.policy";

    private static  final   String      LOAD_TOOL = "call syscs_util.syscs_register_tool( 'luceneSupport', true )";
    private static  final   String      UNLOAD_TOOL = "call syscs_util.syscs_register_tool( 'luceneSupport', false )";
    private static  final   String      INDEX_POEMS = "call LuceneSupport.createIndex( 'ruth', 'poems', 'poemText', null )";
    private static  final   String      UPDATE_POEMS_INDEX = "call LuceneSupport.updateIndex( 'ruth', 'poems', 'poemText', null )";
    private static  final   String      DROP_POEMS_INDEX = "call LuceneSupport.dropIndex( 'ruth', 'poems', 'poemText' )";
    private static  final   String      DROP_PRIMARY_KEY = "alter table poems drop constraint poemsKey";

    private static  final   long        MILLIS_IN_HOUR = 1000L * 60L * 60L;
    private static  final   long        MILLIS_IN_DAY = MILLIS_IN_HOUR * 24L;

    private static  final   String      LANGUAGE = "en";
    private static  final   String      COUNTRY = "US";

    private static  final   String      DEFAULT_INDEX_DESCRIPTOR = "org.apache.derby.optional.api.LuceneUtils.defaultIndexDescriptor";
    private static  final   String      CONSTANT_QUERY_PARSER = "org.apache.derbyTesting.functionTests.tests.lang.LuceneSupportPermsTest.constantStringIndexDescriptor";
    
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // STATE
    //
    ///////////////////////////////////////////////////////////////////////////////////

    static  String[]    _fieldNames;

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // CONSTRUCTOR
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * Create a new instance.
     */

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

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // JUnit BEHAVIOR
    //
    ///////////////////////////////////////////////////////////////////////////////////


    /**
     * Construct top level suite in this JUnit test
     */
    public static Test suite()
    {
        String      luceneVersion = getSystemProperty( LUCENE_VERSION_PROPERTY );
        if ( luceneVersion != null ) { LUCENE_VERSION = luceneVersion; }
        
        BaseTestSuite suite = (BaseTestSuite)TestConfiguration.embeddedSuite(
            LuceneSupportPermsTest.class);

        Test        secureTest = new SecurityManagerSetup( suite, POLICY_FILE );
        Test        authenticatedTest = DatabasePropertyTestSetup.builtinAuthentication
            ( secureTest, LEGAL_USERS, "LuceneSupportPermissions" );
        Test        authorizedTest = TestConfiguration.sqlAuthorizationDecoratorSingleUse( authenticatedTest, DB_NAME, true );
        Test        localizedTest = new LocaleTestSetup( authorizedTest, new Locale( LANGUAGE, COUNTRY ) );

        return localizedTest;
    }

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // TESTS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Test baseline permissions where no grants are made.
     * </p>
     */
    public  void    test_001_basicNoGrant()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );
        Connection  aliceConnection = openUserConnection( ALICE );

        createSchema( ruthConnection, Types.INTEGER );

        // ruth does not have permission to load the tool
        expectExecutionError( ruthConnection, LACK_EXECUTE_PRIV, LOAD_TOOL );

        // but the dbo has permission
        goodStatement( dboConnection, LOAD_TOOL );

        // can't update a non-existent index
        expectExecutionError
            ( ruthConnection, NONEXISTENT_INDEX, "call LuceneSupport.updateIndex( 'ruth', 'poems', 'poemText', null )" );

        // alice does not have permission to index a table owned by ruth
        expectExecutionError( aliceConnection, LACK_COLUMN_PRIV, INDEX_POEMS );

        // but ruth can
        goodStatement( ruthConnection, INDEX_POEMS );

        // redundant index creation fails
        expectExecutionError( ruthConnection, FUNCTION_EXISTS, INDEX_POEMS );

        // can't update a non-existent index
        expectExecutionError( ruthConnection, NONEXISTENT_INDEX, "call LuceneSupport.updateIndex( 'ruth', 'poems', 'foo', null )" );
        expectExecutionError
            ( ruthConnection, NONEXISTENT_INDEX, "call LuceneSupport.updateIndex( 'ruth', 'poems', 'originalAuthor', null )" );

        // alice can't view an index created by ruth
        String  viewPoemsIndex = "select * from table ( ruth.poems__poemText( 'star', 1000, null ) ) luceneResults order by poemID";
        expectExecutionError( aliceConnection, LACK_EXECUTE_PRIV, viewPoemsIndex );

        // but ruth can
        assertResults
            (
             ruthConnection,
             viewPoemsIndex,
             new String[][]
             {
                 { "3", "3", "2", "0.22933942" },
                 { "4", "4", "3", "0.22933942" },
                 { "5", "5", "4", "0.26756266" },
             },
             false
             );

        // alice can list indexes even on tables owned by ruth
        String  listIndexes = "select schemaName, tableName, columnName from table ( LuceneSupport.listIndexes() ) listindexes";
        assertResults
            (
             aliceConnection,
             listIndexes,
             new String[][]
             {
                 { "RUTH", "POEMS", "POEMTEXT" },
             },
             false
             );

        // alice cannot update an index owned by ruth
        expectExecutionError( aliceConnection, NO_DDL_PRIV, UPDATE_POEMS_INDEX );
        
        // alice cannot drop an index owned by ruth
        expectExecutionError( aliceConnection, AUTH_NO_ACCESS_NOT_OWNER, DROP_POEMS_INDEX );

        // ruth can update the index
        goodStatement( ruthConnection, UPDATE_POEMS_INDEX );

        // dropping the key does NOT prevent you from re-indexing
        goodStatement( ruthConnection, DROP_PRIMARY_KEY );
        goodStatement( ruthConnection, UPDATE_POEMS_INDEX );

        // but dropping a key column DOES prevent you from re-indexing and from selecting
        goodStatement( ruthConnection, "alter table poems drop column versionStamp" );
        expectExecutionError( ruthConnection, COLUMN_OUT_OF_SCOPE, UPDATE_POEMS_INDEX );
        expectExecutionError( ruthConnection, COLUMN_OUT_OF_SCOPE, viewPoemsIndex );
        
        // ruth can drop the index
        goodStatement( ruthConnection, DROP_POEMS_INDEX );
        assertResults
            (
             ruthConnection,
             listIndexes,
             new String[][] {},
             false
             );

        // redundant drop fails, however
        expectExecutionError( ruthConnection, NONEXISTENT_OBJECT, DROP_POEMS_INDEX );

        // verify that a primary key is necessary in order to index a table
        dropSchema( ruthConnection );
        createSchema( ruthConnection, Types.INTEGER );
        goodStatement( ruthConnection, DROP_PRIMARY_KEY );
        expectExecutionError( ruthConnection, NO_PRIMARY_KEY, INDEX_POEMS );

        // ruth cannot unload the tool
        expectExecutionError( ruthConnection, LACK_EXECUTE_PRIV, UNLOAD_TOOL );

        // but the dbo can
        goodStatement( dboConnection, UNLOAD_TOOL );

        dropSchema( ruthConnection );
    }

    /**
     * <p>
     * Test that a user can grant access to her indexes.
     * </p>
     */
    public  void    test_002_userGrant()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );
        Connection  aliceConnection = openUserConnection( ALICE );

        createSchema( ruthConnection, Types.INTEGER );

        // load the Lucene plugin
        goodStatement( dboConnection, LOAD_TOOL );

        // ruth indexes her table and grants alice privilege to run the index reading function
        goodStatement( ruthConnection, INDEX_POEMS );

        Permission[]    permissions = new Permission[]
        {
            new Permission( "execute on function poems__poemText", NO_GENERIC_PERMISSION ),
            new Permission( "select ( poemID ) on poems", NO_SELECT_OR_UPDATE_PERMISSION ),
            new Permission( "select ( versionStamp ) on poems", NO_SELECT_OR_UPDATE_PERMISSION ),
            new Permission( "select ( poemText ) on poems", NO_SELECT_OR_UPDATE_PERMISSION ),
        };
        for ( Permission permission : permissions )
        {
            grantPermission( ruthConnection, permission.text, ALICE );
        }

        // but alice still needs select privilege on the base table columns
        String  viewPoemsIndex = "select * from table ( ruth.poems__poemText( 'star', 1000, null ) ) luceneResults order by poemid";
        String[][]  viewPoemsIndexResults = new String[][]
            {
                { "3", "3", "2", "0.22933942" },
                { "4", "4", "3", "0.22933942" },
                { "5", "5", "4", "0.26756266" },
            };

        // now alice can view the index
        assertResults( aliceConnection, viewPoemsIndex, viewPoemsIndexResults, false );

        // now revoke each permission and verify that it is needed
        for ( Permission permission : permissions )
        {
            vetPermission_002( permission, ruthConnection, aliceConnection, viewPoemsIndex, viewPoemsIndexResults );
        }

        // but alice still can't drop an index owned by ruth
        expectExecutionError( aliceConnection, AUTH_NO_ACCESS_NOT_OWNER, DROP_POEMS_INDEX );

        // unload the plugin
        goodStatement( dboConnection, UNLOAD_TOOL );

        dropSchema( ruthConnection );
    }
    private void    vetPermission_002
        (
         Permission permission,
         Connection ruthConnection,
         Connection aliceConnection,
         String statement,
         String[][] expectedResults
         )
        throws Exception
    {
        revokePermission( ruthConnection, permission.text, ALICE );
        expectExecutionError( aliceConnection, permission.sqlStateWhenMissing, statement );
        grantPermission( ruthConnection, permission.text, ALICE );
        assertResults( aliceConnection, statement, expectedResults, false );
    }
    private void    grantPermission( Connection conn, String permission, String grantee )
        throws Exception
    {
        String  command = "grant " + permission + " to " + grantee;

        goodStatement( conn, command );
    }
    private void    revokePermission( Connection conn, String permission, String grantee )
        throws Exception
    {
        String  command = "revoke " + permission + " from " + grantee;
        if ( permission.startsWith( "execute" ) || permission.startsWith( "usage" ) )   { command += " restrict"; }

        goodStatement( conn, command );
    }

    /**
     * <p>
     * Test that only the DBO can (un)load the tool and the tool
     * can't be (un)loaded twice.
     * </p>
     */
    public  void    test_003_loading()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        createSchema( ruthConnection, Types.INTEGER );

        goodStatement( dboConnection, "grant execute on procedure syscs_util.syscs_register_tool to public" );

        // only the DBO can load the tool
        expectExecutionError( ruthConnection, DBO_ONLY, LOAD_TOOL );

        goodStatement( dboConnection, LOAD_TOOL );
        expectExecutionError( dboConnection, DOUBLE_LOAD_ILLEGAL, LOAD_TOOL );

        // cannot index non-existent table or column
        expectExecutionError( ruthConnection, NOT_INDEXABLE, "call LuceneSupport.createIndex( 'ruth', 'foo', 'poemText', null )" );
        expectExecutionError( ruthConnection, NOT_INDEXABLE, "call LuceneSupport.createIndex( 'ruth', 'poems', 'fooText', null )" );
        expectExecutionError( ruthConnection, NOT_INDEXABLE, "call LuceneSupport.createIndex( 'ruth', 'poems', 'versionStamp', null )" );

        // cannot drop non-existent index
        expectExecutionError( ruthConnection, NONEXISTENT_OBJECT, "call LuceneSupport.dropIndex( 'ruth', 'foo', 'poemText' )" );
        expectExecutionError( ruthConnection, NONEXISTENT_OBJECT, "call LuceneSupport.dropIndex( 'ruth', 'poems', 'versionStamp' )" );
        
        // only the DBO can unload the tool
        expectExecutionError( ruthConnection, DBO_ONLY, UNLOAD_TOOL );

        goodStatement( dboConnection, "revoke execute on procedure syscs_util.syscs_register_tool from public restrict" );

        goodStatement( dboConnection, UNLOAD_TOOL );
        expectExecutionError( dboConnection, DOUBLE_UNLOAD_ILLEGAL, UNLOAD_TOOL );

        // try loading and unloading again for good measure
        goodStatement( dboConnection, LOAD_TOOL );
        goodStatement( dboConnection, UNLOAD_TOOL );
        dropSchema( ruthConnection );
    }
    
    /**
     * <p>
     * Test all datatypes as key types.
     * </p>
     */
    public  void    test_004_datatypes()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        goodStatement( dboConnection, LOAD_TOOL );

        vet_004( ruthConnection, Types.BIGINT );
        vet_004( ruthConnection, Types.BOOLEAN );
        vet_004( ruthConnection, Types.CHAR );
        vet_004( ruthConnection, Types.BINARY );
        vet_004( ruthConnection, Types.DATE );
        vet_004( ruthConnection, Types.DECIMAL );
        vet_004( ruthConnection, Types.DOUBLE );
        vet_004( ruthConnection, Types.FLOAT );
        vet_004( ruthConnection, Types.INTEGER );
        vet_004( ruthConnection, Types.NUMERIC );
        vet_004( ruthConnection, Types.REAL );
        vet_004( ruthConnection, Types.SMALLINT );
        vet_004( ruthConnection, Types.TIME );
        vet_004( ruthConnection, Types.TIMESTAMP );
        vet_004( ruthConnection, Types.VARCHAR );
        vet_004( ruthConnection, Types.VARBINARY );

        goodStatement( dboConnection, UNLOAD_TOOL );
    }
    private void    vet_004( Connection ruthConnection, int jdbcType )
        throws Exception
    {
        createSchema( ruthConnection, jdbcType );
        goodStatement( ruthConnection, INDEX_POEMS );

        // make sure that we can de-serialize the key
        assertResults
            (
             ruthConnection,
             "select p.originalAuthor, i.score\n" +
             "from ruth.poems p, table ( ruth.poems__poemText( 'star', 1000, null ) ) i\n" +
             "where p.poemID = i.poemID and p.versionStamp = i.versionStamp\n" +
             "order by i.score desc\n",
             new String[][]
             {
                 { "Walt Whitman", "0.26756266" },
                 { "Lord Byron", "0.22933942" },
                 { "John Milton", "0.22933942" },
             },
             false
             );

        goodStatement( ruthConnection, DROP_POEMS_INDEX );
        dropSchema( ruthConnection );
    }

    /**
     * <p>
     * Test error messages when a lucene directory has been deleted.
     * </p>
     */
    public  void    test_005_deleteDirectory()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        createSchema( ruthConnection, Types.INTEGER );
        goodStatement( dboConnection, LOAD_TOOL );
        goodStatement( ruthConnection, INDEX_POEMS );

        TestConfiguration   config = getTestConfiguration();

        String  dbName = config.getDefaultDatabaseName();
        String  physicalDBName = config.getPhysicalDatabaseName( dbName );
        String  dbPath = config.getDatabasePath( physicalDBName );
        File    dbDirectory = new File( dbPath );
        File    luceneDirectory = new File( dbDirectory, "LUCENE" );
        File    ruthDirectory = new File( luceneDirectory, "RUTH" );
        File    poemsDirectory = new File( ruthDirectory, "POEMS" );
        File    poemTextIndexDirectory = new File( poemsDirectory, "POEMTEXT" );

        assertTrue( deleteFile( poemTextIndexDirectory ) );

        // but that doesn't stop you from deleting the index
        goodStatement( ruthConnection, DROP_POEMS_INDEX );
        expectCompilationError
            (
             ruthConnection, NONEXISTENT_TABLE_FUNCTION,
             "select * from table( ruth.textTable__textCol( 'one two three four five six seven eight nine ten', 100, null ) ) t"
             );

        goodStatement( dboConnection, UNLOAD_TOOL );
        dropSchema( ruthConnection );
    }
    
    /**
     * <p>
     * Test that you can change the Analyzer.
     * </p>
     */
    public  void    test_006_changeAnalyzer()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        createSchema( ruthConnection, Types.INTEGER );
        goodStatement( dboConnection, LOAD_TOOL );
        goodStatement( ruthConnection, INDEX_POEMS );

        // verify that we are the correct locale
        assertResults
            (
             ruthConnection,
             "values ( substr( getDatabaseLocale(), 1, 2 ) )",
             new String[][]
             {
                 { LANGUAGE },
             },
             false
             );
        

        String  query =
            "select p.originalAuthor, i.score\n" +
            "from ruth.poems p, table ( ruth.poems__poemText( 'star', 1000, null ) ) i\n" +
            "where p.poemID = i.poemID and p.versionStamp = i.versionStamp\n" +
            "order by i.score desc\n";

        assertResults
            (
             ruthConnection,
             query,
             new String[][]
             {
                 { "Walt Whitman", "0.26756266" },
                 { "Lord Byron", "0.22933942" },
                 { "John Milton", "0.22933942" },
             },
             false
             );

        // now switch the Analyzer and re-run the query
        goodStatement
            ( ruthConnection,
              "call LuceneSupport.updateIndex( 'ruth', 'poems', 'poemText', '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );

        assertResults
            (
             ruthConnection,
             query,
             new String[][]
             {
                 { "Walt Whitman", "0.3304931" },
                 { "John Milton", "0.2832798" },
             },
             false
             );

        //
        // Add another index and inspect the values of listIndexes()
        //
        goodStatement( ruthConnection, "call LuceneSupport.createIndex( 'ruth', 'poems', 'originalAuthor', null )" );
        assertResults
            (
             ruthConnection,
             "select schemaName, tableName, columnName, luceneVersion, analyzer, indexDescriptorMaker\n" +
             "from table( LuceneSupport.listIndexes() ) l\n" +
             "order by schemaName, tableName, columnName\n",
             new String[][]
             {
                 {
                     "RUTH", "POEMS", "ORIGINALAUTHOR", LUCENE_VERSION,
                     "org.apache.lucene.analysis.en.EnglishAnalyzer",
                     DEFAULT_INDEX_DESCRIPTOR
                 },
                 {
                     "RUTH", "POEMS", "POEMTEXT", LUCENE_VERSION,
                     "org.apache.lucene.analysis.standard.StandardAnalyzer",
                     LuceneCoarseAuthorizationTest.STANDARD_ANALYZER
                 },
             },
             false
             );

        goodStatement( ruthConnection, DROP_POEMS_INDEX );
        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'poems', 'originalAuthor' )" );
        goodStatement( dboConnection, UNLOAD_TOOL );
        dropSchema( ruthConnection );
    }

    /**
     * <p>
     * Test that you can index views and index tables with alternative column lists.
     * </p>
     */
    public  void    test_007_indexViews()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        createSchema( ruthConnection, Types.INTEGER );
        createPoemView( ruthConnection );
        goodStatement( dboConnection, LOAD_TOOL );
        goodStatement( ruthConnection, INDEX_POEMS );

        // must supply some key columns if you're going to index a view
        expectExecutionError
            (
             ruthConnection,
             NO_PRIMARY_KEY,
             "call LuceneSupport.createIndex( 'ruth', 'poemView', 'poemText', null )"
             );

        // now index the view
        goodStatement
            (
             ruthConnection,
             "call LuceneSupport.createIndex( 'ruth', 'poemView', 'poemText', null, 'poemID', 'versionStamp' )"
             );

        // can't create a second index by the same name
        expectExecutionError
            (
             ruthConnection,
             FUNCTION_EXISTS,
             "call LuceneSupport.createIndex( 'ruth', 'poemView', 'poemText', null, 'poemID' )"
             );

        // vet index contents
        String  selectFromViewIndex =
            "select p.originalAuthor, i.score\n" +
            "from ruth.poems p, table ( ruth.poemView__poemText( 'star', 1000, null ) ) i\n" +
            "where p.poemID = i.poemID and p.versionStamp = i.versionStamp\n" +
            "order by i.score desc\n";
        assertResults
            (
             ruthConnection,
             selectFromViewIndex,
             new String[][]
             {
                 { "Walt Whitman", "0.26756266" },
                 { "Lord Byron", "0.22933942" },
                 { "John Milton", "0.22933942" },
             },
             false
             );

        // vet index list
        String  selectIndexes =
            "select schemaName, tableName, columnName, indexDescriptorMaker\n" +
            "from table( LuceneSupport.listIndexes() ) l\n" +
            "order by schemaName, tableName, columnName\n";
        assertResults
            (
             ruthConnection,
             selectIndexes,
             new String[][]
             {
                 {
                     "RUTH", "POEMS", "POEMTEXT",
                     DEFAULT_INDEX_DESCRIPTOR
                 },
                 {
                     "RUTH", "POEMVIEW", "POEMTEXT",
                     DEFAULT_INDEX_DESCRIPTOR
                 },
             },
             false
             );

        // update the view index, changing its analyzer
        goodStatement
            (
             ruthConnection,
             "call LuceneSupport.updateIndex( 'ruth', 'poemView', 'poemText', '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )"
             );
        assertResults
            (
             ruthConnection,
             selectFromViewIndex,
             new String[][]
             {
                 { "Walt Whitman", "0.3304931" },
                 { "John Milton", "0.2832798" },
             },
             false
             );
        assertResults
            (
             ruthConnection,
             selectIndexes,
             new String[][]
             {
                 {
                     "RUTH", "POEMS", "POEMTEXT",
                     DEFAULT_INDEX_DESCRIPTOR
                 },
                 {
                     "RUTH", "POEMVIEW", "POEMTEXT",
                     LuceneCoarseAuthorizationTest.STANDARD_ANALYZER
                 },
             },
             false
             );

        // drop the index on the view
        goodStatement
            (
             ruthConnection,
             "call LuceneSupport.dropIndex( 'ruth', 'poemView', 'poemText' )"
             );
        assertResults
            (
             ruthConnection,
             selectIndexes,
             new String[][]
             {
                 {
                     "RUTH", "POEMS", "POEMTEXT",
                     DEFAULT_INDEX_DESCRIPTOR
                 },
             },
             false
             );

        // now drop the index on the table and create one with just one key column
        goodStatement( ruthConnection, DROP_POEMS_INDEX );
        goodStatement
            (
             ruthConnection,
             "call LuceneSupport.createIndex( 'ruth', 'poems', 'poemText', null, 'poemID' )"
             );
        assertResults
            (
             ruthConnection,
             "select *\n" +
             "from table ( ruth.poems__poemText( 'star', 1000, null ) ) i\n" +
             "order by i.score desc\n",
             new String[][]
             {
                 { "5", "4", "0.26756266" },
                 { "4", "3", "0.22933942" },
                 { "3", "2", "0.22933942" },
             },
             false
             );
        
        goodStatement( ruthConnection, DROP_POEMS_INDEX );
        goodStatement( dboConnection, UNLOAD_TOOL );
        goodStatement( ruthConnection, "drop view poemView" );
        dropSchema( ruthConnection );
    }

    /**
     * <p>
     * Test that you can't create an index involving a column with the same name
     * as one of the system-supplied column names (documentID and score).
     * </p>
     */
    public  void    test_008_columnNames()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        goodStatement( dboConnection, LOAD_TOOL );

        goodStatement( ruthConnection, "create table badTable1( keyCol int primary key, score clob )" );
        goodStatement( ruthConnection, "create table badTable2( keyCol int primary key, documentID clob )" );
        goodStatement( ruthConnection, "create table badTable3( score int primary key, textCol clob )" );
        goodStatement( ruthConnection, "create table badTable4( documentID int primary key, textCol clob )" );

        expectExecutionError
            (
             ruthConnection, BAD_COLUMN_NAME,
             "call LuceneSupport.createIndex( 'ruth', 'badTable1', 'score', null )"
             );
        expectExecutionError
            (
             ruthConnection, BAD_COLUMN_NAME,
             "call LuceneSupport.createIndex( 'ruth', 'badTable2', 'documentID', null )"
             );
        expectExecutionError
            (
             ruthConnection, BAD_COLUMN_NAME,
             "call LuceneSupport.createIndex( 'ruth', 'badTable3', 'textCol', null )"
             );
        expectExecutionError
            (
             ruthConnection, BAD_COLUMN_NAME,
             "call LuceneSupport.createIndex( 'ruth', 'badTable4', 'textCol', null )"
             );

        goodStatement( dboConnection, UNLOAD_TOOL );
        goodStatement( ruthConnection, "drop table badTable1" );
        goodStatement( ruthConnection, "drop table badTable2" );
        goodStatement( ruthConnection, "drop table badTable3" );
        goodStatement( ruthConnection, "drop table badTable4" );
    }
    
   /**
     * <p>
     * Test changes to the arguments to the searching table function.
     * </p>
     */
    public  void    test_009_searchArgs()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        loadTestTable( ruthConnection );
        
        goodStatement( dboConnection, LOAD_TOOL );
        goodStatement( ruthConnection, "call LuceneSupport.createIndex( 'ruth', 'textTable', 'textCol', null )" );

        // get all the matches
        assertResults
            (
             ruthConnection,
             "select * from table( ruth.textTable__textCol( 'one two three four five six seven eight nine ten', 100, null ) ) t",
             new String[][]
             {
                 { "10", "9", "2.2791052" },
                 { "9", "8", "1.6305782" },
                 { "8", "7", "1.1616905" },
                 { "7", "6", "0.97469425" },
                 { "6", "5", "0.6597747" },
                 { "5", "4", "0.49575216" },
                 { "4", "3", "0.33803377" },
                 { "3", "2", "0.17799875" },
                 { "2", "1", "0.09289266" },
                 { "1", "0", "0.035006654" },
             },
             false
             );
        
        // get an initial 3-row window of the top results
        assertResults
            (
             ruthConnection,
             "select * from table( ruth.textTable__textCol( 'one two three four five six seven eight nine ten', 3, null ) ) t",
             new String[][]
             {
                 { "10", "9", "2.2791052" },
                 { "9", "8", "1.6305782" },
                 { "8", "7", "1.1616905" },
             },
             false
             );
        
        // get the next 4-row window of results
        assertResults
            (
             ruthConnection,
             "select * from table( ruth.textTable__textCol( 'one two three four five six seven eight nine ten', 4, 1.0 ) ) t",
             new String[][]
             {
                 { "7", "6", "0.97469425" },
                 { "6", "5", "0.6597747" },
                 { "5", "4", "0.49575216" },
                 { "4", "3", "0.33803377" },
             },
             false
             );

        // get the final window of results
        assertResults
            (
             ruthConnection,
             "select * from table( ruth.textTable__textCol( 'one two three four five six seven eight nine ten', 100, 0.2 ) ) t",
             new String[][]
             {
                 { "3", "2", "0.17799875" },
                 { "2", "1", "0.09289266" },
                 { "1", "0", "0.035006654" },
             },
             false
             );
        
        // try a different query parser
        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'textTable', 'textCol' )" );
        goodStatement
            (
             ruthConnection,
             "call LuceneSupport.createIndex( 'ruth', 'textTable', 'textCol', '" + CONSTANT_QUERY_PARSER + "' )"
             );
        assertResults
            (
             ruthConnection,
             "select * from table( ruth.textTable__textCol( 'one two three four five six seven eight nine ten', 100, null ) ) t",
             new String[][]
             {
                 { "1", "0", "1.597837" },
                 { "2", "1", "0.9986481" },
                 { "3", "2", "0.7989185" },
                 { "4", "3", "0.7989185" },
                 { "5", "4", "0.69905365" },
                 { "6", "5", "0.59918886" },
                 { "7", "6", "0.59918886" },
                 { "8", "7", "0.49932405" },
                 { "9", "8", "0.49932405" },
                 { "10", "9", "0.49932405" },
             },
             false
             );
        
        goodStatement( dboConnection, UNLOAD_TOOL );
        unloadTestTable( ruthConnection );
    }
    static  void    loadTestTable( Connection conn ) throws Exception
    {
        conn.prepareStatement
            (
             "create table textTable( keyCol int primary key, textCol clob )"
             ).execute();
        conn.prepareStatement
            (
             "insert into textTable values\n" +
             "( 1, 'one' ),\n" +
             "( 2, 'one two' ),\n" +
             "( 3, 'one two three' ),\n" +
             "( 4, 'one two three four' ),\n" +
             "( 5, 'one two three four five' ),\n" +
             "( 6, 'one two three four five six' ),\n" +
             "( 7, 'one two three four five six seven' ),\n" +
             "( 8, 'one two three four five six seven eight' ),\n" +
             "( 9, 'one two three four five six seven eight nine' ),\n" +
             "( 10, 'one two three four five six seven eight nine ten' ),\n" +
             "( 101, 'bricks' ),\n" +
             "( 102, 'bricks and mortar' ),\n" +
             "( 103, 'bricks and mortar, tea' ),\n" +
             "( 104, 'bricks and mortar, tea, tears' ),\n" +
             "( 105, 'bricks and mortar, tea, tears, turtle' ),\n" +
             "( 106, 'bricks and mortar, tea, tears, turtle, soup' ),\n" +
             "( 107, 'bricks and mortar, tea, tears, turtle, soup, when in the course' ),\n" +
             "( 108, 'bricks and mortar, tea, tears, turtle, soup, when in the course of human events' ),\n" +
             "( 109, 'bricks and mortar, tea, tears, turtle, soup, when in the course of human events you want' ),\n" +
             "( 110, 'bricks and mortar, tea, tears, turtle, soup, when in the course of human events you want better cell coverage' )\n"
             ).execute();
    }
    static  void    unloadTestTable( Connection conn ) throws Exception
    {
        conn.prepareStatement
            (
             "drop table textTable"
             ).execute();
    }

   /**
     * <p>
     * Test that encryption and the Lucene plugin are incompatible.
     * </p>
     */
    public  void    test_010_encryption()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        String      password = getTestConfiguration().getPassword( TEST_DBO );
        String      encryptDatabaseURL = "jdbc:derby:" + DB_NAME + ";user=" + TEST_DBO + ";password=" + password +
                ";dataEncryption=true;bootPassword=tryToGuessThis";
        String      decryptDatabaseURL = "jdbc:derby:" + DB_NAME + ";user=" + TEST_DBO + ";password=" + password +
                ";decryptDatabase=true;bootPassword=tryToGuessThis";

        goodStatement( dboConnection, LOAD_TOOL );
        getTestConfiguration().shutdownDatabase();

        // verify that you can't encrypt the database now
        try {
            DriverManager.getConnection( encryptDatabaseURL );
            fail( "Should not have been able to get a connection!" );
        }
        catch (SQLException se)
        {
            assertSQLState( INCOMPATIBLE_ENCRYPTION, se );
        }

        // now unload the tool and encrypt the database
        dboConnection = openUserConnection( TEST_DBO );
        goodStatement( dboConnection, UNLOAD_TOOL );
        getTestConfiguration().shutdownDatabase();
        dboConnection = DriverManager.getConnection( encryptDatabaseURL );

        // now you can't load the plugin
        expectExecutionError( dboConnection, INCOMPATIBLE_ENCRYPTION, LOAD_TOOL );

        // turn off encryption
        getTestConfiguration().shutdownDatabase();
        dboConnection = DriverManager.getConnection( decryptDatabaseURL );
    }
    
   /**
     * <p>
     * Test restrictions on field names.
     * </p>
     */
    public  void    test_011_fieldNames()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        loadTestTable( ruthConnection );
        goodStatement( dboConnection, LOAD_TOOL );

        String  indexTestTable =
            "call LuceneSupport.createIndex\n" +
            "(\n" +
            "    'ruth', 'textTable', 'textCol',\n" +
            "    'org.apache.derbyTesting.functionTests.tests.lang.LuceneSupportPermsTest.makeFieldNameIndexDescriptor'\n" +
            ")\n";
        
        goodStatement
            (
             ruthConnection,
             "create procedure setFieldNames( fieldName varchar( 32672 )... )\n" +
             "language java parameter style derby no sql\n" +
             "external name 'org.apache.derbyTesting.functionTests.tests.lang.LuceneSupportPermsTest.setFieldNames'\n"
             );

        // Field and key name conflict.
        goodStatement( ruthConnection, "call setFieldNames( 'KEYCOL' )" );
        expectExecutionError( ruthConnection, BAD_FIELD_NAME, indexTestTable );

        // Duplicate field names.
        goodStatement( ruthConnection, "call setFieldNames( 'FOO', 'FOO' )" );
        expectExecutionError( ruthConnection, DUPLICATE_FIELD_NAME, indexTestTable );

        // Null field name.
        goodStatement( ruthConnection, "call setFieldNames( 'FOO', null )" );
        expectExecutionError( ruthConnection, DUPLICATE_FIELD_NAME, indexTestTable );
        goodStatement( ruthConnection, "call setFieldNames( null, 'FOO' )" );
        expectExecutionError( ruthConnection, DUPLICATE_FIELD_NAME, indexTestTable );

        goodStatement( ruthConnection, "drop procedure setFieldNames" );
        goodStatement( dboConnection, UNLOAD_TOOL );
        unloadTestTable( ruthConnection );
    }

   /**
     * <p>
     * Test that nulls are allowed in keys. See DERBY-6602.
     * </p>
     */
    public  void    test_6602()
        throws Exception
    {
        CD[]    columnDescriptors = new CD[]
        {
            new CD( "bigintCol", "bigint" ),
            new CD( "blobCol", "blob" ),
            new CD( "booleanCol", "boolean" ),
            new CD( "charCol", "char( 10 )" ),
            new CD( "clobCol", "clob" ),
            new CD( "dataCol", "date" ),
            new CD( "decimalCol", "decimal" ),
            new CD( "doubleCol", "double" ),
            new CD( "floatCol", "float" ),
            new CD( "intCol", "int" ),
            new CD( "longvarcharCol", "long varchar" ),
            new CD( "longvarcharforbitdataCol", "long varchar for bit data" ),
            new CD( "numericCol", "numeric" ),
            new CD( "realCol", "real" ),
            new CD( "smallintCol", "smallint" ),
            new CD( "timeCol", "time" ),
            new CD( "varcharCol", "varchar( 10 )" ),
            new CD( "varcharforbitdataCol", "varchar( 10 ) for bit data" ),
        };

        StringBuilder   buffer = new StringBuilder();
        buffer.append( "create table t_6602\n(\ttextcol clob" );
        for ( CD cd : columnDescriptors )
        {
            buffer.append( ",\n\t" + cd.name + " " + cd.type );
        }
        buffer.append( "\n)" );
        
        Connection  dboConnection = openUserConnection( TEST_DBO );
        
        goodStatement( dboConnection, LOAD_TOOL );
        goodStatement( dboConnection, buffer.toString() );
        goodStatement( dboConnection, "insert into t_6602( textcol ) values ( 'abc'), ( 'def')" );

        for ( CD cd : columnDescriptors )
        {
            vet6602( dboConnection, cd );
        }

        goodStatement( dboConnection, "drop table t_6602" );
        goodStatement( dboConnection, UNLOAD_TOOL );
    }
    private void    vet6602( Connection conn, CD cd )   throws Exception
    {
        PreparedStatement   ps = chattyPrepare
            (
             conn,
             "call lucenesupport.createindex\n" +
             "( 'TEST_DBO', 't_6602', 'textcol', '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "', ? )"
             );
        ps.setString( 1, cd.name );
        ps.execute();

        assertResults
            (
             conn,
             "select * from table( t_6602__textcol( 'abc or def', 3, null ) ) tc order by documentid",
             new String[][]
             {
                 { null, "0", "0.35355338" },
                 { null, "1", "0.35355338" },
             },
             false
             );

        goodStatement( conn, "call lucenesupport.dropIndex( 'TEST_DBO', 't_6602', 'textcol' )" );
    }
    
   /**
     * <p>
     * Test that nulls are not allowed as the values of certain arguments. See DERBY-6596.
     * </p>
     */
    public  void    test_6596_null_args()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        
        goodStatement( dboConnection, LOAD_TOOL );
        goodStatement( dboConnection, "create table t_6596( x int primary key, c clob )" );
        goodStatement( dboConnection, "insert into t_6596 values ( 1, 'abc' ), ( 2, 'def' )" );

        // create index errors
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.createindex( null, 't_6596', 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.createindex( 'TEST_DBO', null, 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.createindex( 'TEST_DBO', 't_6596', null,  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.createindex( 'TEST_DBO', 't_6596', 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "', null )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.createindex( 'TEST_DBO', 't_6596', 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "', 'x', null )" );

        goodStatement( dboConnection, "call lucenesupport.createindex( 'TEST_DBO', 't_6596', 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );
        assertResults
            (
             dboConnection,
             "select * from table( t_6596__c( 'abc or def', 3, null ) ) tc order by documentid",
             new String[][]
             {
                 { "1", "0", "0.35355338" },
                 { "2", "1", "0.35355338" },
             },
             false
             );
        goodStatement( dboConnection, "call lucenesupport.updateindex( 'TEST_DBO', 't_6596', 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );
        
        // update index errors
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.updateindex( null, 't_6596', 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.updateindex( 'TEST_DBO', null, 'c',  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.updateindex( 'TEST_DBO', 't_6596', null,  '" + LuceneCoarseAuthorizationTest.STANDARD_ANALYZER + "' )" );

        // query errors
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "select * from table( t_6596__c( null, 3, null ) ) tc order by documentid" );
        expectExecutionError
            ( dboConnection, NULL_PRIMITIVE_ARG,
              "select * from table( t_6596__c( 'abc or def', null, null ) ) tc order by documentid" );

        // drop index errors
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.dropindex( null, 't_6596', 'c' )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.dropindex( 'TEST_DBO', null, 'c' )" );
        expectExecutionError
            ( dboConnection, ILLEGAL_NULL_ARG,
              "call lucenesupport.dropindex( 'TEST_DBO', 't_6596', null )" );

        goodStatement( dboConnection, "call lucenesupport.dropindex( 'TEST_DBO', 't_6596', 'c' )" );
        goodStatement( dboConnection, "drop table t_6596" );
        goodStatement( dboConnection, UNLOAD_TOOL );
    }
    
    /**
     * <p>
     * Test identifier casing for keys and text columns. See DERBY-6730.
     * </p>
     */
    public  void    test_6730()
        throws Exception
    {
        Connection  dboConnection = openUserConnection( TEST_DBO );
        Connection  ruthConnection = openUserConnection( RUTH );

        createSchema( ruthConnection, Types.INTEGER );
        goodStatement
            (
             ruthConnection,
             "create view v_6730_1 ( poemID, poemText ) as select poemID, poemText from ruth.poems"
             );
        goodStatement
            (
             ruthConnection,
             "create view v_6730_2 ( poemID, \"poemText\" ) as select poemID, poemText from ruth.poems"
             );
        goodStatement
            (
             ruthConnection,
             "create view v_6730_3 ( \"poemID\", poemText ) as select poemID, poemText from ruth.poems"
             );
        goodStatement
            (
             ruthConnection,
             "create view v_6730_4 ( \"poemID\", \"poemText\" ) as select poemID, poemText from ruth.poems"
             );
        goodStatement
            (
             ruthConnection,
             "create view v_6730_5 ( poemID, \"c l o b\" ) as select poemID, poemText from ruth.poems"
             );
        goodStatement
            (
             ruthConnection,
             "create view v_6730_6 ( \"k e y\", poemText ) as select poemID, poemText from ruth.poems"
             );

        goodStatement( dboConnection, LOAD_TOOL );

        // Index the views
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.createIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_1',\n" +
             "  'poemText',\n" +
             "  null,\n" +
             "  'poemID'\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.createIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_2',\n" +
             "  '\"poemText\"',\n" +
             "  null,\n" +
             "  'poemID'\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.createIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_3',\n" +
             "  'poemText',\n" +
             "  null,\n" +
             "  '\"poemID\"'\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.createIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_4',\n" +
             "  '\"poemText\"',\n" +
             "  null,\n" +
             "  '\"poemID\"'\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.createIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_5',\n" +
             "  '\"c l o b\"',\n" +
             "  null,\n" +
             "  'poemID'\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.createIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_6',\n" +
             "  'poemText',\n" +
             "  null,\n" +
             "  '\"k e y\"'\n" +
             ")\n"
             );

        // Verify the expected casing of identifiers
        vet6730( ruthConnection );

        // Update the indexes and re-verify
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.updateIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_1',\n" +
             "  'poemText',\n" +
             "  null\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.updateIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_2',\n" +
             "  '\"poemText\"',\n" +
             "  null\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.updateIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_3',\n" +
             "  'poemText',\n" +
             "  null\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.updateIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_4',\n" +
             "  '\"poemText\"',\n" +
             "  null\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.updateIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_5',\n" +
             "  '\"c l o b\"',\n" +
             "  null\n" +
             ")\n"
             );
        goodStatement
            (
             ruthConnection,
             "call luceneSupport.updateIndex\n" +
             "(\n" +
             "  'ruth',\n" +
             "  'v_6730_6',\n" +
             "  'poemText',\n" +
             "  null\n" +
             ")\n"
             );
        vet6730( ruthConnection );

        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'v_6730_1', 'poemText' )" );
        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'v_6730_2', '\"poemText\"' )" );
        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'v_6730_3', 'poemText' )" );
        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'v_6730_4', '\"poemText\"' )" );
        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'v_6730_5', '\"c l o b\"' )" );
        goodStatement( ruthConnection, "call LuceneSupport.dropIndex( 'ruth', 'v_6730_6', 'poemText' )" );
        goodStatement( dboConnection, UNLOAD_TOOL );
        goodStatement( ruthConnection, "drop view v_6730_6" );
        goodStatement( ruthConnection, "drop view v_6730_5" );
        goodStatement( ruthConnection, "drop view v_6730_4" );
        goodStatement( ruthConnection, "drop view v_6730_3" );
        goodStatement( ruthConnection, "drop view v_6730_2" );
        goodStatement( ruthConnection, "drop view v_6730_1" );
        dropSchema( ruthConnection );
    }
    private void vet6730( Connection conn ) throws Exception
    {
        // Verify the expected casing of identifiers
        String[][]  expectedResults = new String[][]
        {
            { "5", "0.26756266" },
            { "4", "0.22933942" },
            { "3", "0.22933942" },
        };
        assertResults
            (
             conn,
             "select i.poemID, i.score\n" +
             "from table ( ruth.v_6730_1__poemText( 'star', 1000, null ) ) i\n" +
             "order by i.score desc\n",
             expectedResults,
             false
             );
        assertResults
            (
             conn,
             "select i.poemID, i.score\n" +
             "from table ( ruth.\"V_6730_2__poemText\"( 'star', 1000, null ) ) i\n" +
             "order by i.score desc\n",
             expectedResults,
             false
             );
        assertResults
            (
             conn,
             "select i.\"poemID\", i.score\n" +
             "from table ( ruth.v_6730_3__poemText( 'star', 1000, null ) ) i\n" +
             "order by i.score desc\n",
             expectedResults,
             false
             );
        assertResults
            (
             conn,
             "select i.\"poemID\", i.score\n" +
             "from table ( ruth.\"V_6730_4__poemText\"( 'star', 1000, null ) ) i\n" +
             "order by i.score desc\n",
             expectedResults,
             false
             );
        assertResults
            (
             conn,
             "select i.poemID, i.score\n" +
             "from table ( ruth.\"V_6730_5__c l o b\"( 'star', 1000, null ) ) i\n" +
             "order by i.score desc\n",
            expectedResults,
             false
             );
        assertResults
            (
             conn,
             "select i.\"k e y\", i.score\n" +
             "from table ( ruth.v_6730_6__poemText( 'star', 1000, null ) ) i\n" +
             "order by i.score desc\n",
             expectedResults,
             false
             );
        
        String[][]  expectedListResults = new String[][]
        {
            { "RUTH", "V_6730_1", "POEMTEXT", "org.apache.lucene.analysis.en.EnglishAnalyzer" },
            { "RUTH", "V_6730_2", "poemText", "org.apache.lucene.analysis.en.EnglishAnalyzer" },
            { "RUTH", "V_6730_3", "POEMTEXT", "org.apache.lucene.analysis.en.EnglishAnalyzer" },
            { "RUTH", "V_6730_4", "poemText", "org.apache.lucene.analysis.en.EnglishAnalyzer" },
            { "RUTH", "V_6730_5", "c l o b", "org.apache.lucene.analysis.en.EnglishAnalyzer" },
            { "RUTH", "V_6730_6", "POEMTEXT", "org.apache.lucene.analysis.en.EnglishAnalyzer" },
        };
        assertResults
            (
             conn,
             "select schemaName, tableName, columnName, analyzer\n" +
             "from table( lucenesupport.listIndexes() ) t\n" +
             "order by schemaName, tableName, columnName\n",
             expectedListResults,
             false
             );
    }

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // MINIONS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    private void    createSchema( Connection ruthConnection, int jdbcType )  throws Exception
    {
        createPoemsTable( ruthConnection, jdbcType );
        createLocaleFunction( ruthConnection );
    }
    private void    createPoemsTable( Connection conn, int jdbcType )
        throws Exception
    {
        goodStatement
            (
             conn,
             "create table poems\n" +
             "(\n" +
             "    poemID " + getType( jdbcType ) + ",\n" +
             "    versionStamp int not null,\n" +
             "    originalAuthor       varchar( 50 ),\n" +
             "    lastEditor           varchar( 50 ),\n" +
             "    poemText            clob,\n" +
             "    constraint poemsKey primary key( poemID, versionStamp )\n" +
             ")\n"
             );

        PreparedStatement   ps = conn.prepareStatement( "insert into poems values ( ?, ?, ?, ?, ? )" );

        int     poemID = 1;
        int     versionStamp = 1;

        setNextPoemID( ps, jdbcType, poemID++ );
        ps.setInt( 2, versionStamp++ );
        ps.setString( 3, "Geoffrey Chaucer" );
        ps.setString( 4, "Geoffrey Chaucer" );
        ps.setString( 5, "Whan that Aprill, with his shoures soote The droghte of March hath perced to the roote And bathed every veyne in swich licour, Of which vertu engendred is the flour;" );
        ps.executeUpdate();

        setNextPoemID( ps, jdbcType, poemID++ );
        ps.setInt( 2, versionStamp++ );
        ps.setString( 3, "Andrew Marvell" );
        ps.setString( 4, "Andrew Marvell" );
        ps.setString( 5, "Had we but world enough, and time, This coyness, lady, were no crime." );
        ps.executeUpdate();

        setNextPoemID( ps, jdbcType, poemID++ );
        ps.setInt( 2, versionStamp++ );
        ps.setString( 3, "John Milton" );
        ps.setString( 4, "John Milton" );
        ps.setString( 5, "From morn to noon he fell, from noon to dewy eve, a summers day, and with the setting sun dropped from the ze4ith like a falling star on Lemnos, the Aegean isle" );
        ps.executeUpdate();

        setNextPoemID( ps, jdbcType, poemID++ );
        ps.setInt( 2, versionStamp++ );
        ps.setString( 3, "Lord Byron" );
        ps.setString( 4, "Lord Byron" );
        ps.setString( 5, "The Assyrian came down like the wolf on the fold, And his cohorts were gleaming in purple and gold; And the sheen of their spears was like stars on the sea, When the blue wave rolls nightly on deep Galilee." );
        ps.executeUpdate();

        setNextPoemID( ps, jdbcType, poemID++ );
        ps.setInt( 2, versionStamp++ );
        ps.setString( 3, "Walt Whitman" );
        ps.setString( 4, "Walt Whitman" );
        ps.setString( 5, "When lilacs last in the dooryard bloomd, And the great star early droopd in the western sky in the night, I mournd, and yet shall mourn with ever-returning spring." );
        ps.executeUpdate();

        ps.close();
    }

    private void    createPoemView( Connection conn )
        throws Exception
    {
        goodStatement
            (
             conn,
             "create view poemView as select poemID, versionStamp, poemText from poems"
             );
    }
    
    private void    createLocaleFunction( Connection conn )
        throws Exception
    {
        goodStatement
            (
             conn,
             "create function getDatabaseLocale() returns varchar( 20 )\n" +
             "language java parameter style java reads sql data\n" +
             "external name 'org.apache.derbyTesting.functionTests.tests.lang.LuceneSupportPermsTest.getDatabaseLocale()'\n"
             );
    }
    
    private String  getType( int jdbcType ) throws Exception
    {
        switch( jdbcType )
        {
        case Types.BINARY: return "char( 100 ) for bit data";
        case Types.BOOLEAN: return "boolean";
        case Types.DECIMAL: return "decimal";
        case Types.INTEGER: return "int";
        case Types.BIGINT: return "bigint";
        case Types.SMALLINT: return "smallint";
        case Types.DOUBLE: return "double";
        case Types.FLOAT: return "real";
        case Types.NUMERIC: return "numeric";
        case Types.CHAR: return "char( 5 )";
        case Types.REAL: return "real";
        case Types.VARCHAR: return "varchar( 5 )";
        case Types.VARBINARY: return "varchar( 256 ) for bit data";
        case Types.DATE: return "date";
        case Types.TIME: return "time";
        case Types.TIMESTAMP: return "timestamp";

        default:    throw new Exception( "Unsupported datatype: " + jdbcType );
        }
    }

    private void    setNextPoemID( PreparedStatement ps, int jdbcType, int intPoemID )
        throws Exception
    {
        switch( jdbcType )
        {
        case Types.BINARY:
            ps.setBytes( 1, makeBytes( intPoemID ) );
            break;

        case Types.BOOLEAN:
            ps.setBoolean( 1, (intPoemID % 2 == 0) ? true : false );
            break;

        case Types.DECIMAL:
        case Types.NUMERIC:
            ps.setBigDecimal( 1, new BigDecimal( intPoemID ) );
            break;

        case Types.INTEGER:
            ps.setInt( 1, intPoemID );
            break;

        case Types.BIGINT:
            ps.setLong( 1, (long) intPoemID + (long) Integer.MAX_VALUE );
            break;

        case Types.SMALLINT:
            ps.setShort( 1, (short) intPoemID );
            break;

        case Types.DOUBLE:
            ps.setDouble( 1, (double) intPoemID );
            break;

        case Types.FLOAT:
        case Types.REAL:
            ps.setFloat( 1, (float) intPoemID );
            break;

        case Types.CHAR:
        case Types.VARCHAR:
            ps.setString( 1, makeStringKey( intPoemID ) );
            break;

        case Types.VARBINARY:
            ps.setBytes( 1, makeAllBytes( intPoemID ) );
            break;

        case Types.DATE:
            ps.setDate( 1, new Date( MILLIS_IN_DAY * (long)(500 + intPoemID) ) );
            break;

        case Types.TIME:
            ps.setTime( 1, new Time( MILLIS_IN_HOUR * (long)(intPoemID) ) );
            break;

        case Types.TIMESTAMP:
            ps.setTimestamp( 1, new Timestamp( MILLIS_IN_DAY * (long)(500 + intPoemID) ) );
            break;

        default:    throw new Exception( "Unsupported datatype: " + jdbcType );
        }
    }
    private String  makeStringKey( int key )
    {
        String  digit = Integer.toString( key );
        return digit + digit + digit + digit + digit;
    }

    private void    dropSchema( Connection ruthConnection )    throws Exception
    {
        goodStatement( ruthConnection, "drop table poems" );
        goodStatement( ruthConnection, "drop function getDatabaseLocale" );
    }
    
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // PROCEDURES AND FUNCTIONS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /** Get the database locale */
    public  static  String  getDatabaseLocale()
        throws SQLException
    {
        return ConnectionUtil.getCurrentLCC().getDatabase().getLocale().toString();
    }
    
    public  static  String  toString( byte[] value )
    {
        if ( value == null ) { return null; }

        return Arrays.toString( value );
    }

    /** Make a byte array with all possible byte values in it */
    public  static  byte[]  makeAllBytes( int initialValue )
    {
        int     size = 2 * ( -Byte.MIN_VALUE );
        byte[]  result = new byte[ size ];
        int     value = initialValue;

        if ( value < Byte.MIN_VALUE ) { value = Byte.MIN_VALUE; }
        if ( value > Byte.MAX_VALUE ) { value = Byte.MAX_VALUE; }

        for ( int idx = 0; idx < size; idx++ )
        {
            result[ idx ] = (byte) (value++);

            if ( value > Byte.MAX_VALUE ) { value = Byte.MIN_VALUE; }
        }

        return result;
    }
    
    /** Make a byte array starting with the given byte */
    public  static  byte[]  makeBytes( int initialValue )
    {
        byte[]  result = new byte[ initialValue ];

        for ( int idx = 0; idx < initialValue; idx++ )
        {
            result[ idx ] = (byte) initialValue;
        }

        return result;
    }

    /** Alternative QueryParser maker, which forces the text to be a constant string */
    public  static  LuceneIndexDescriptor constantStringIndexDescriptor()
    {
        return new ConstantIndexDescriptor();
    }
    
    /**
     * Delete a file. If it's a directory, recursively delete all directories
     * and files underneath it first.
     */
    private boolean deleteFile( File file )
        throws IOException, PrivilegedActionException
    {
        boolean retval = true;
        
        if ( isDirectory( file ) )
        {
            for ( File child : listFiles( file ) ) { retval = retval && deleteFile( child ); }
        }

        return retval && clobberFile( file );
    }

    /** Return true if the file is a directory */
    private boolean isDirectory( final File file )
        throws IOException, PrivilegedActionException
    {
        return AccessController.doPrivileged
            (
             new PrivilegedExceptionAction<Boolean>()
             {
                public Boolean run() throws IOException
                {
                    if ( file == null ) { return false; }
                    else { return file.isDirectory(); }
                }
             }
             ).booleanValue();
    }

    /** Really delete a file */
    private boolean clobberFile( final File file )
        throws IOException, PrivilegedActionException
    {
        return AccessController.doPrivileged
            (
             new PrivilegedExceptionAction<Boolean>()
             {
                public Boolean run() throws IOException
                {
                    return file.delete();
                }
             }
             ).booleanValue();
    }

    /** List files */
    private File[]  listFiles( final File file )
        throws IOException, PrivilegedActionException
    {
        return AccessController.doPrivileged
            (
             new PrivilegedExceptionAction<File[]>()
             {
                public File[] run() throws IOException
                {
                    return file.listFiles();
                }
             }
             );
    }

    public  static  void    setFieldNames( String... fieldNames ) { _fieldNames = fieldNames; }

    public  static  LuceneIndexDescriptor   makeFieldNameIndexDescriptor() { return new FieldNameIndexDescriptor(); }

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // NESTED CLASSES
    //
    ///////////////////////////////////////////////////////////////////////////////////

    public  static  class   ConstantQueryParser extends MultiFieldQueryParser
    {
        public  ConstantQueryParser
            (
             Version version,
             String[] fieldNames,
             Analyzer analyzer
             )
        {
            super( version, fieldNames, analyzer );
        }

        public Query parse( String query )  throws ParseException
        {
            return super.parse( "one" );
        }
    }

    public  static  class   CD
    {
        public  final   String  name;
        public  final   String  type;

        public  CD( String Name, String Type )
        {
            name = Name;
            type = Type;
        }

        public  String  toString() { return "[ " + name + ", " + type + " ]"; }
    }

    public  static  class   ConstantIndexDescriptor extends LuceneUtils.DefaultIndexDescriptor
    {
        public  ConstantIndexDescriptor() { super(); }
        
        public  QueryParser getQueryParser()
            throws SQLException
        {
            return new ConstantQueryParser
                (
                 LuceneUtils.currentVersion(),
                 getFieldNames(),
                 getAnalyzer()
                 );
        }
    }

    public  static  class   FieldNameIndexDescriptor extends LuceneUtils.DefaultIndexDescriptor
    {
        public  FieldNameIndexDescriptor() { super(); }
        public  String[]    getFieldNames() { return LuceneSupportPermsTest._fieldNames; }
    }
    
}
