/**
 *  Derby - Class org.apache.derbyTesting.functionTests.tests.lang.CaseExpressionTest
 *
 * 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.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import junit.framework.Test;
import org.apache.derbyTesting.functionTests.util.streams.LoopingAlphabetReader;
import org.apache.derbyTesting.functionTests.util.streams.LoopingAlphabetStream;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.BaseTestSuite;
import org.apache.derbyTesting.junit.CleanDatabaseTestSetup;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.SQLUtilities;
import org.apache.derbyTesting.junit.TestConfiguration;


public class CaseExpressionTest extends BaseJDBCTestCase {

        // Results if the Case Expression evaluates to a column reference :
        //
        // 1. SELECT CASE WHEN 1 = 1 THEN <column reference> ELSE NULL END
        // 2. SELECT CASE WHEN 1 = 1 THEN
        //       (CASE WHEN 1 = 1 THEN <column reference> ELSE NULL END)
        //       ELSE NULL END
        //
        private static String[][] columnReferenceResults = {
            /*SMALLINT*/ {null,"0","1","2"},
            /*INTEGER*/ {null,"0","1","21"},
            /*BIGINT*/ {null,"0","1","22"},
            /*DECIMAL(10,5)*/ {null,"0.00000","1.00000","23.00000"},
            /*REAL*/ {null,"0.0","1.0","24.0"},
            /*DOUBLE*/ {null,"0.0","1.0","25.0"},
            /*CHAR(60)*/ {
                null,
                "0                                                           ",
                "aa                                                          ",
                "2.0                                                         "},
            /*VARCHAR(60)*/ {null,"0","aa","15:30:20"},
            /*LONG VARCHAR*/ {null,"0","aa","2000-01-01 15:30:20"},
            /*CHAR(60) FOR BIT DATA*/ {
                null,
                "10aa20202020202020202020202020202020202020202020202020202020" +
                "202020202020202020202020202020202020202020202020202020202020",
                null,
                "10aaaa202020202020202020202020202020202020202020202020202020" +
                "202020202020202020202020202020202020202020202020202020202020"},
            /*VARCHAR(60) FOR BIT DATA*/ {null,"10aa",null,"10aaba"},
            /*LONG VARCHAR FOR BIT DATA*/ {null,"10aa",null,"10aaca"},
            /*CLOB(1k)*/ {null,"13","14",null},
            /*DATE*/ {null,"2000-01-01","2000-01-01",null},
            /*TIME*/ {null,"15:30:20","15:30:20","15:30:20"},
            /*TIMESTAMP*/ {
                null,
                "2000-01-01 15:30:20.0",
                "2000-01-01 15:30:20.0",
                "2000-01-01 15:30:20.0"},
            /*BLOB(1k)*/ {null,null,null,null},
        };
        
       

        // Results if the Case Expression evaluates to a NULL value :
        //
        // 3. SELECT CASE WHEN 1 = 1 THEN NULL ELSE <column reference> END
        // 4. SELECT CASE WHEN 1 = 1 THEN
        //       (CASE WHEN 1 = 1 THEN NULL ELSE <column reference> END)
        //         ELSE NULL END
        // 5. SELECT CASE WHEN 1 = 1 THEN NULL ELSE
        //         (CASE WHEN 1 = 1 THEN <column reference> ELSE NULL END) END
        // 6. SELECT CASE WHEN 1 = 1 THEN NULL ELSE
        //         (CASE WHEN 1 = 1 THEN NULL ELSE <column reference> END) END
        //
        private static String[][] nullValueResults  = {
            /*SMALLINT*/ {null,null,null,null},
            /*INTEGER*/ {null,null,null,null},
            /*BIGINT*/ {null,null,null,null},
            /*DECIMAL(10,5)*/ {null,null,null,null},
            /*REAL*/ {null,null,null,null},
            /*DOUBLE*/ {null,null,null,null},
            /*CHAR(60)*/ {null,null,null,null},
            /*VARCHAR(60)*/ {null,null,null,null},
            /*LONG VARCHAR*/ {null,null,null,null},
            /*CHAR(60) FOR BIT DATA*/ {null,null,null,null},
            /*VARCHAR(60) FOR BIT DATA*/ {null,null,null,null},
            /*LONG VARCHAR FOR BIT DATA*/ {null,null,null,null},
            /*CLOB(1k)*/ {null,null,null,null},
            /*DATE*/ {null,null,null,null},
            /*TIME*/ {null,null,null,null},
            /*TIMESTAMP*/ {null,null,null,null},
            /*BLOB(1k)*/ {null,null,null,null},
        };

    public CaseExpressionTest(String name) {
        super(name);
    }
    
    /**
     * Test various statements that 
     *
     */
    public void testWhenNonBoolean() {
        
        // DERBY-2809: BOOLEAN datatype was forced upon
        // unary expressions that were not BOOLEAN, such
        // as SQRT(?)
        String[] unaryOperators = {
                "SQRT(?)", "SQRT(9)",
                "UPPER(?)", "UPPER('haight')",
                "LOWER(?)", "LOWER('HAIGHT')",
        };
        for (int i = 0; i < unaryOperators.length; i++)
        {
            assertCompileError("42X88",
               "VALUES CASE WHEN " + unaryOperators[i] +
               " THEN 3 ELSE 4 END");
        }
    }

    public void testAllDatatypesCombinationsForCaseExpressions()
    throws SQLException
    {
        Statement s = createStatement();

        /* 1. Column Reference in the THEN node, and NULL in
         * the ELSE node.
         */
        testCaseExpressionQuery(s, columnReferenceResults,
            "SELECT CASE WHEN 1 = 1 THEN ",
            " ELSE NULL END from AllDataTypesTable");

        /* 2. Test Column Reference nested in the THEN's node THEN node,
         * NULL's elsewhere.
         */
        testCaseExpressionQuery(s, columnReferenceResults,
            "SELECT CASE WHEN 1 = 1 THEN (CASE WHEN 1 = 1 THEN ",
            " ELSE NULL END) ELSE NULL END from AllDataTypesTable");

        /* 3. NULL in the THEN node, and a Column Reference in
         * the ELSE node.
         */
        testCaseExpressionQuery(s, nullValueResults,
            "SELECT CASE WHEN 1 = 1 THEN NULL ELSE ",
            " END from AllDataTypesTable");

        /* 4. Test Column Reference nested in the THEN's node ELSE node,
         * NULL's elsewhere.
         */
        testCaseExpressionQuery(s, nullValueResults,
            "SELECT CASE WHEN 1 = 1 THEN (CASE WHEN 1 = 1 THEN NULL ELSE ",
            " END) ELSE NULL END from AllDataTypesTable");

        /* 5. Test Column Reference nested in the ELSE's node THEN node,
         * NULL's elsewhere.
         */
        testCaseExpressionQuery(s, nullValueResults,
            "SELECT CASE WHEN 1 = 1 THEN NULL ELSE (CASE WHEN 1 = 1 THEN ",
            " ELSE NULL END) END from AllDataTypesTable");

        /* 6. Test Column Reference nested in the ELSE's node ELSE node,
         * NULL's elsewhere.
         */
        testCaseExpressionQuery(s, nullValueResults,
            "SELECT CASE WHEN 1 = 1 THEN NULL " +
            "ELSE (CASE WHEN 1 = 1 THEN NULL ELSE ",
            " END) END from AllDataTypesTable");

        s.close();
    }

    /**
     * Test a query that has many WHEN conditions in it.  This is mostly
     * checking for the performance regression filed as DERBY-2986.  That
     * regression may not be noticeable in the scope of the full regression
     * suite, but if this test is run standalone then this fixture could
     * still be useful.
     */
    public void testMultipleWhens() throws SQLException
    {
        Statement s = createStatement();
        JDBC.assertFullResultSet(
            s.executeQuery(
                "values CASE WHEN 10 = 1 THEN 'a' " +
                "WHEN 10 = 2 THEN 'b' " +
                "WHEN 10 = 3 THEN 'c' " +
                "WHEN 10 = 4 THEN 'd' " +
                "WHEN 10 = 5 THEN 'e' " +
                "WHEN 10 = 6 THEN 'f' " +
                "WHEN 10 = 7 THEN 'g' " +
                "WHEN 10 = 8 THEN 'h' " +
                "WHEN 10 = 11 THEN 'i' " +
                "WHEN 10 = 12 THEN 'j' " +
                "WHEN 10 = 15 THEN 'k' " +
                "WHEN 10 = 16 THEN 'l' " +
                "WHEN 10 = 23 THEN 'm' " +
                "WHEN 10 = 24 THEN 'n' " +
                "WHEN 10 = 27 THEN 'o' " +
                "WHEN 10 = 31 THEN 'p' " +
                "WHEN 10 = 41 THEN 'q' " +
                "WHEN 10 = 42 THEN 'r' " +
                "WHEN 10 = 50 THEN 's' " +
                "ELSE '*' END"),
            new String[][] {{"*"}});

        s.close();
    }

    /**
     * Before DERBY-6423, boolean expressions (such as A OR B, or A AND B)
     * were not accepted in THEN and ELSE clauses.
     */
    public void testBooleanExpressions() throws SQLException {
        Statement s = createStatement();

        // Test both with and without parentheses around the expressions.
        // Those with parentheses used to work, and those without used to
        // cause syntax errors. Now both should work.
        JDBC.assertFullResultSet(
            s.executeQuery(
                "select case when a or b then b or c else a or c end,\n" +
                "   case when a and b then b and c else a and c end,\n" +
                "   case when (a or b) then (b or c) else (a or c) end,\n" +
                "   case when (a and b) then (b and c) else (a and c) end\n" +
                "from (values (true, true, true), (true, true, false),\n" +
                "             (true, false, true), (true, false, false),\n" +
                "             (false, true, true), (false, true, false),\n" +
                "             (false, false, true), (false, false, false)\n" +
                "      ) v(a, b, c)\n" +
                "order by a desc, b desc, c desc"),
            new String[][] {
                { "true", "true", "true", "true" },
                { "true", "false", "true", "false" },
                { "true", "true", "true", "true" },
                { "false", "false", "false", "false" },
                { "true", "false", "true", "false" },
                { "true", "false", "true", "false" },
                { "true", "false", "true", "false" },
                { "false", "false", "false", "false" },
            });
    }

    /**
     * Runs the test fixtures in embedded.
     *
     * @return test suite
     */
    public static Test suite()
    {
        BaseTestSuite suite = (BaseTestSuite)
            TestConfiguration.embeddedSuite(CaseExpressionTest.class);

        return new CleanDatabaseTestSetup(suite) {
            /**
             * Creates the table used in the test cases.
             */
            protected void decorateSQL(Statement s) throws SQLException {
                SQLUtilities.createAndPopulateAllDataTypesTable(s);
                
            }
        };
    }

    /**
     * Execute the received caseExpression on the received Statement
     * and check the results against the receieved expected array.
     */
    private void testCaseExpressionQuery(Statement st,
        String [][] expRS, String caseExprBegin, String caseExprEnd)
        throws SQLException
    {
        ResultSet rs;
        int colType;
        int row;

        for (colType = 0;
            colType < SQLUtilities.SQLTypes.length;
            colType++)
        {
            rs = st.executeQuery(
                caseExprBegin +
                SQLUtilities.allDataTypesColumnNames[colType] +
                caseExprEnd);

            row = 0;
            
            while (rs.next()) {
                String val = rs.getString(1);
                assertEquals(expRS[colType][row], val);
                row++;
            }
            rs.close();
        }
     
    }

    
    /**
     * Test fix for DERBY-3032. Fix ClassCastException if SQL NULL is returned from conditional.
     * 
     * @throws SQLException
     */
    public void testDerby3032() throws SQLException 
    {
        Statement s = createStatement();
        

        s.executeUpdate("create table t (d date, vc varchar(30))");
        s.executeUpdate("insert into t values(CURRENT_DATE, 'hello')");
        ResultSet rs = s.executeQuery("SELECT d from t where d = (SELECT CASE WHEN 1 = 1 THEN CURRENT_DATE ELSE NULL END from t)");
        JDBC.assertDrainResults(rs,1);
        
        // Make sure null gets cast properly to date type to avoid cast exception. DERBY-3032
        rs = s.executeQuery("SELECT d from t where d = (SELECT CASE WHEN 1 = 1 THEN NULL  ELSE CURRENT_DATE  END from t)");
        JDBC.assertEmpty(rs);
        
        rs = s.executeQuery("SELECT d from t where d = (SELECT CASE WHEN 1 = 0 THEN CURRENT_DATE  ELSE NULL END from t)");
        JDBC.assertEmpty(rs);
        
        // Make sure metadata has correct type for various null handling
        rs = s.executeQuery("SELECT CASE WHEN 1 = 1 THEN NULL  ELSE CURRENT_DATE  END from t");
        ResultSetMetaData rsmd = rs.getMetaData();
        assertEquals(java.sql.Types.DATE, rsmd.getColumnType(1));
        // should be nullable since it returns NULL #:)
        assertEquals(ResultSetMetaData.columnNullable, rsmd.isNullable(1));
        JDBC.assertSingleValueResultSet(rs, null);    
        
        rs = s.executeQuery("SELECT CASE WHEN 1 = 0 THEN CURRENT_DATE ELSE NULL END from t");
        rsmd = rs.getMetaData();
        assertEquals(java.sql.Types.DATE, rsmd.getColumnType(1));
        // should be nullable since it returns NULL #:)
        assertEquals(ResultSetMetaData.columnNullable, rsmd.isNullable(1));
        JDBC.assertSingleValueResultSet(rs, null);  
        
        // and with an implicit NULL return.
        rs = s.executeQuery("SELECT CASE WHEN 1 = 0 THEN CURRENT_DATE END from t");
        rsmd = rs.getMetaData();
        assertEquals(java.sql.Types.DATE, rsmd.getColumnType(1));
        // should be nullable since it returns NULL #:)
        assertEquals(ResultSetMetaData.columnNullable, rsmd.isNullable(1));
        JDBC.assertSingleValueResultSet(rs, null);  
        
        // and no possible NULL return.
        
        rs = s.executeQuery("SELECT CASE WHEN 1 = 0 THEN 6 ELSE 4 END from t");
        rsmd = rs.getMetaData();
        assertEquals(java.sql.Types.INTEGER, rsmd.getColumnType(1));
        // should be nullable since it returns NULL #:)
        assertEquals(ResultSetMetaData.columnNoNulls, rsmd.isNullable(1));
        JDBC.assertSingleValueResultSet(rs, "4"); 
        
        rs = s.executeQuery("SELECT CASE WHEN 1 = 1 THEN 6 ELSE 4 END from t");
        rsmd = rs.getMetaData();
        assertEquals(java.sql.Types.INTEGER, rsmd.getColumnType(1));
        // should be nullable since it returns NULL #:)
        assertEquals(ResultSetMetaData.columnNoNulls, rsmd.isNullable(1));
        JDBC.assertSingleValueResultSet(rs, "6");
        
    }

    /**
     * Verify that NOT elimination produces the correct results.
     * DERBY-6563.
     */
    public void testNotElimination() throws SQLException {
        setAutoCommit(false);

        Statement s = createStatement();
        s.execute("create table d6563(b1 boolean, b2 boolean, b3 boolean)");

        // Fill the table with all possible combinations of true/false/null.
        Boolean[] universe = { true, false, null };
        PreparedStatement insert = prepareStatement(
                "insert into d6563 values (?, ?, ?)");
        for (Boolean v1 : universe) {
            insert.setObject(1, v1);
            for (Boolean v2 : universe) {
                insert.setObject(2, v2);
                for (Boolean v3 : universe) {
                    insert.setObject(3, v3);
                    insert.executeUpdate();
                }
            }
        }

        // Truth table for
        // B1, B2, B3, WHEN B1 THEN B2 ELSE B3, NOT (WHEN B1 THEN B2 ELSE B3).
        Object[][] expectedRows = {
            { false, false, false, false, true  },
            { false, false, true,  true,  false },
            { false, false, null,  null,  null  },
            { false, true,  false, false, true  },
            { false, true,  true,  true,  false },
            { false, true,  null,  null,  null  },
            { false, null,  false, false, true  },
            { false, null,  true,  true,  false },
            { false, null,  null,  null,  null  },
            { true,  false, false, false, true  },
            { true,  false, true,  false, true  },
            { true,  false, null,  false, true  },
            { true,  true,  false, true,  false },
            { true,  true,  true,  true,  false },
            { true,  true,  null,  true,  false },
            { true,  null,  false, null,  null  },
            { true,  null,  true,  null,  null  },
            { true,  null,  null,  null,  null  },
            { null,  false, false, false, true  },
            { null,  false, true,  true,  false },
            { null,  false, null,  null,  null  },
            { null,  true,  false, false, true  },
            { null,  true,  true,  true,  false },
            { null,  true,  null,  null,  null  },
            { null,  null,  false, false, true  },
            { null,  null,  true,  true,  false },
            { null,  null,  null,  null,  null  },
        };

        // Verify the truth table. Since NOT elimination is not performed on
        // expressions in the SELECT list, this passed even before the fix.
        JDBC.assertFullResultSet(
            s.executeQuery(
                "select b1, b2, b3, case when b1 then b2 else b3 end, "
                        + "not case when b1 then b2 else b3 end "
                        + "from d6563 order by b1, b2, b3"),
            expectedRows, false);

        // Now take only those rows where the NOT CASE expression evaluated
        // to TRUE, and strip off the expression columns at the end.
        ArrayList<Object[]> rows = new ArrayList<Object[]>();
        for (Object[] row : expectedRows) {
            if (row[4] == Boolean.TRUE) {
                rows.add(Arrays.copyOf(row, 3));
            }
        }

        // Assert that those are the only rows returned if the NOT CASE
        // expression is used as a predicate. This query used to return a
        // different set of rows before the fix.
        expectedRows = rows.toArray(new Object[rows.size()][]);
        JDBC.assertFullResultSet(
                s.executeQuery("select * from d6563 where "
                        + "not case when b1 then b2 else b3 end "
                        + "order by b1, b2, b3"),
                expectedRows, false);
    }

    /**
     * Test that parameters can be used in CASE expressions.
     */
    public void testParameters() throws SQLException {
        // If all of the result expressions are untyped parameters, the
        // type cannot be determined, and an error should be raised.
        assertCompileError("42X87", "values case when true then ? else ? end");

        // If at least one result expression is typed, the parameter should
        // get its type from it.
        PreparedStatement ps = prepareStatement(
                "values case when true then ? else 1 end");

        // DERBY-6567: The result should be nullable, since the parameter
        // could be set to null. It used to be reported as not nullable.
        assertEquals(ResultSetMetaData.columnNullable,
                     ps.getMetaData().isNullable(1));

        ps.setNull(1, Types.INTEGER);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        ps.setInt(1, 1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "1");

        ps = prepareStatement(
                "values case when true then ? else cast(? as integer) end");
        ParameterMetaData params = ps.getParameterMetaData();
        assertEquals(Types.INTEGER, params.getParameterType(1));
        assertEquals(Types.INTEGER, params.getParameterType(2));
        ps.setInt(1, 1);
        ps.setInt(2, 2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "1");

        // Parameters in the WHEN clause can be untyped. They will
        // implicitly get the BOOLEAN type.
        ps = prepareStatement("values case when ? then 1 else 0 end");
        assertEquals(Types.BOOLEAN,
                     ps.getParameterMetaData().getParameterType(1));

        ps.setBoolean(1, true);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "1");

        ps.setBoolean(1, false);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "0");

        ps.setNull(1, Types.BOOLEAN);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "0");
    }

    /**
     * Test how untyped NULLs are handled.
     */
    public void testUntypedNulls() throws SQLException {
        Statement s = createStatement();

        // Before DERBY-2002, Derby accepted a CASE expression to have an
        // untyped NULL in all the result branches. Verify that an error
        // is raised.
        String[] allUntyped = {
            // The SQL standard says at least one result should not be an
            // untyped NULL, so expect these to fail.
            "values case when true then null end",
            "values case when true then null else null end",
            "values case when true then null "
                + "when false then null else null end",

            // We're not able to tell the type if we have a mix of untyped
            // NULLs and untyped parameters.
            "values case when true then ? end", // implicit ELSE NULL
            "values case when true then null else ? end",
            "values case when true then ? when false then ? else null end",

            // These ones failed even before DERBY-2002.
            "values case when true then ? else ? end",
            "values case when true then ? when false then ? else ? end",
        };
        for (String sql : allUntyped) {
            assertCompileError("42X87", sql);
        }

        // Check that expressions with untyped NULLs compile as long as
        // there is at least one typed expression.
        JDBC.assertFullResultSet(s.executeQuery(
                "select case when a then 1 when b then null end, "
                    + "case when a then null when b then 1 end, "
                    + "case when a then null when b then null else 1 end "
                    + "from (values (false, false), (false, true), "
                    + " (true, false), (true, true)) v(a, b) order by a, b"),
            new Object[][] {
                { null, null, 1    },
                { null, 1,    null },
                { 1,    null, null },
                { 1,    null, null },
            },
            false);

        // When there is a typed NULL, its type has to be compatible with
        // the types of the other expressions.
        assertCompileError("42X89",
            "values case when 1<>1 then 'abc' else cast(null as smallint) end");
    }

    /** Regression test case for DERBY-6577. */
    public void testQuantifiedComparison() throws SQLException {
        // This query used to return wrong results.
        JDBC.assertUnorderedResultSet(createStatement().executeQuery(
                "select c, case when c = all (values 'Y') then true end "
                + "from (values 'Y', 'N') v(c)"),
            new String[][] { { "N", null }, { "Y", "true" }});
    }

    /**
     * Tests for the simple case syntax added in DERBY-1576.
     */
    public void testSimpleCaseSyntax() throws SQLException {
        Statement s = createStatement();

        // Simplest of the simple cases. SQL:1999 syntax, which allows a
        // single operand per WHEN clause, and the operand is a value
        // expression.
        JDBC.assertUnorderedResultSet(s.executeQuery(
                "select i, case i when 0 then 'zero' "
                + "when 1 then 'one' when 1+1 then 'two' "
                + "else 'many' end from "
                + "(values 0, 1, 2, 3, cast(null as int)) v(i)"),
            new String[][] {
                {"0", "zero"},
                {"1", "one"},
                {"2", "two"},
                {"3", "many"},
                {null, "many"}
            });

        // SQL:2003 added feature F262 Extended CASE Expression, which
        // allows more complex WHEN operands. Essentially, it allows any
        // last part of a predicate (everything after the left operand).
        JDBC.assertFullResultSet(s.executeQuery(
                "select i, case i when < 0 then 'negative' "
                        + "when < 10 then 'small' "
                        + "when between 10 and 20 then 'medium' "
                        + "when in (19, 23, 29, 37, 41) then 'prime' "
                        + "when = some (values 7, 42) then 'lucky number' "
                        + "when >= 40 then 'big' end "
                        + "from (values -1, 0, 1, 2, 3, 8, 9, 10, 17, 19, "
                        + "29, 37, 38, 39, 40, 41, 42, 50) v(i) order by i"),
            new String[][] {
                { "-1", "negative" },
                { "0", "small" },
                { "1", "small" },
                { "2", "small" },
                { "3", "small" },
                { "8", "small" },
                { "9", "small" },
                { "10", "medium" },
                { "17", "medium" },
                { "19", "medium" },
                { "29", "prime" },
                { "37", "prime" },
                { "38", null },
                { "39", null },
                { "40", "big" },
                { "41", "prime" },
                { "42", "lucky number" },
                { "50", "big" },
            });

        JDBC.assertUnorderedResultSet(s.executeQuery(
                "select c, case c "
                + "when like 'abc%' then 0 "
                + "when like 'x%%' escape 'x' then 1 "
                + "when = all (select ibmreqd from sysibm.sysdummy1) then 2 "
                + "when 'xyz' || 'zyx' then 3 "
                + "when is null then 4 "
                + "when is not null then 5 end "
                + "from (values 'abcdef', 'xyzzyx', '%s', 'hello', "
                + "cast(null as char(1)), 'Y', 'N') v(c)"),
            new String[][] {
                { "abcdef", "0" },
                { "xyzzyx", "3" },
                { "%s", "1" },
                { "hello", "5" },
                { null, "4" },
                { "Y", "2" },
                { "N", "5" },
            });

        // SQL:2011 added feature F263 Comma-separated predicates in simple
        // CASE expression, which allows multiple operands per WHEN clause.
        JDBC.assertFullResultSet(s.executeQuery(
                "select i, case i "
                + "when between 2 and 3, 5, =7 then 'prime' "
                + "when <1, >7 then 'out of range' "
                + "when is not null then 'small' end "
                + "from (values 0, 1, 2, 3, 4, 5, 6, 7, 8, cast(null as int)) "
                + "as v(i) order by i"),
            new String[][] {
                { "0", "out of range" },
                { "1", "small" },
                { "2", "prime" },
                { "3", "prime" },
                { "4", "small" },
                { "5", "prime" },
                { "6", "small" },
                { "7", "prime" },
                { "8", "out of range" },
                { null, null },
            });

        JDBC.assertUnorderedResultSet(s.executeQuery(
                "select c, case c "
                + "when in ('ab', 'cd'), like '_' then 'matched' "
                + "else 'not matched' end "
                + "from (values cast('a' as varchar(1)), 'b', 'c', 'ab', "
                + "'cd', 'ac', 'abc') v(c)"),
            new String[][] {
                { "a",   "matched" },
                { "b",   "matched" },
                { "c",   "matched" },
                { "ab",  "matched" },
                { "cd",  "matched" },
                { "ac",  "not matched" },
                { "abc", "not matched" },
            });

        // Untyped null is not allowed as CASE operand. Use typed null instead.
        assertCompileError("42X01", "values case null when 1 then 'one' end");
        JDBC.assertSingleValueResultSet(s.executeQuery(
                "values case cast(null as int) when 1 then 'one' end"),
            null);

        // Untyped null is not allowed as WHEN operand. Use IS NULL instead.
        assertCompileError("42X01", "values case 1 when null then 'null' end");
        JDBC.assertUnorderedResultSet(s.executeQuery(
                "select i, case i when is null then 1 when is not null "
                + "then 2 else 3 end from (values 1, cast(null as int)) v(i)"),
            new String[][] { { "1", "2" }, { null, "1" } });

        // Non-deterministic functions are not allowed in the case operand.
        assertCompileError("42Y98",
                "values case sysfun.random() when 1 then true else false end");
        assertCompileError("42Y98",
                "values case (values sysfun.random()) "
                + "when 1 then true else false end");

        // Deterministic functions, on the other hand, are allowed.
        JDBC.assertFullResultSet(s.executeQuery(
                "select case sysfun.sin(angle) when < 0 then 'negative' "
                        + "when > 0 then 'positive' end "
                        + "from (values -pi()/2, 0, pi()/2) v(angle) "
                        + "order by angle"),
            new String[][] { {"negative"}, {null}, {"positive"} });

        // Non-deterministic functions can be used outside of the case operand.
        JDBC.assertDrainResults(
            s.executeQuery(
                "values case 1 when sysfun.random() then sysfun.random() end"),
            1);

        // Scalar subqueries are allowed in the case operand.
        JDBC.assertSingleValueResultSet(
                s.executeQuery("values case (values 1) when 1 then true end"),
                "true");

        // Non-scalar subqueries are not allowed.
        assertCompileError(
                "42X39", "values case (values (1, 2)) when 1 then true end");
        assertStatementError(
                "21000", s, "values case (values 1, 2) when 1 then true end");

        // The type of the CASE operand must be compatible with the types
        // of all the WHEN operands.
        assertCompileError("42818", "values case 1 when true then 'yes' end");
        assertCompileError("42818",
                "values case 1 when 1 then 'yes' when 2 then 'no' "
                + "when 'three' then 'maybe' end");
        JDBC.assertSingleValueResultSet(s.executeQuery(
                "values case cast(1 as bigint)"
                + " when cast(1 as smallint) then 'yes' end"),
            "yes");

        // A sequence cannot be accessed anywhere in a CASE expression.
        s.execute("create sequence d1576_s start with 1");
        assertCompileError(
                "42XAH",
                "values case next value for d1576_s when 1 then 1 else 0 end");
        assertCompileError(
                "42XAH",
                "values case 1 when next value for d1576_s then 1 else 0 end");
        assertCompileError(
                "42XAH",
                "values case 1 when 1 then next value for d1576_s else 0 end");

        // Instead, access the sequence value in a nested query.
        JDBC.assertSingleValueResultSet(
                s.executeQuery(
                    "select case x when 1 then 1 else 0 end from "
                    + "(values next value for d1576_s) v(x)"),
                "1");

        s.execute("drop sequence d1576_s restrict");

        // Window functions are allowed.
        JDBC.assertFullResultSet(
                s.executeQuery(
                    "select case row_number() over () when 1 then 'one' "
                    + "when 2 then 'two' end from (values 1, 1, 1) v(x)"),
                new String[][] { {"one"}, {"two"}, {null} });

        // Test that you can have a typed parameter in the case operand.
        PreparedStatement ps = prepareStatement(
                "values case cast(? as integer) "
                + "when 1 then 'one' when 2 then 'two' end");
        ps.setInt(1, 1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "one");
        ps.setInt(1, 2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "two");
        ps.setInt(1, 3);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        // This one fails to compile because an integer cannot be checked
        // with LIKE.
        assertCompileError("42884",
                "values case cast(? as integer) "
                + "when 1 then 1 when like 'abc' then 2 end");

        // Untyped parameter in the case operand. Should be able to infer
        // the type from the WHEN clauses.
        ps = prepareStatement("values case ? when 1 then 2 when 3 then 4 end");
        ParameterMetaData pmd = ps.getParameterMetaData();
        assertEquals(Types.INTEGER, pmd.getParameterType(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));

        ps.setInt(1, 1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "2");

        ps.setInt(1, 2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        ps.setInt(1, 3);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "4");

        ps = prepareStatement(
                "values case ? when cast(1.1 as double) then true "
                + "when cast(1.2 as double) then false end");
        pmd = ps.getParameterMetaData();
        assertEquals(Types.DOUBLE, pmd.getParameterType(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));

        ps.setDouble(1, 1.1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "true");
        ps.setDouble(1, 1.2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "false");
        ps.setDouble(1, 1.3);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        // Mixed types are accepted, as long as they are compatible.
        ps = prepareStatement(
                "values case ? when 1 then 'one' when 2.1 then 'two' end");
        pmd = ps.getParameterMetaData();
        assertEquals(Types.DECIMAL, pmd.getParameterType(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));

        ps.setInt(1, 1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "one");
        ps.setInt(1, 2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        ps.setDouble(1, 1.1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);
        ps.setDouble(1, 2.1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "two");

        ps = prepareStatement(
                "values case ? when 1 then 'one' when 2.1 then 'two'"
                + " when cast(3 as bigint) then 'three' end");
        assertEquals(Types.DECIMAL, pmd.getParameterType(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));

        ps.setInt(1, 1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "one");
        ps.setInt(1, 2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);
        ps.setInt(1, 3);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "three");

        ps.setDouble(1, 1.1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);
        ps.setDouble(1, 2.1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "two");
        ps.setDouble(1, 3.1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        ps = prepareStatement(
                "values case ? when 'abcdef' then 1 "
                + "when cast('abcd' as varchar(4)) then 2 end");
        pmd = ps.getParameterMetaData();
        assertEquals(Types.VARCHAR, pmd.getParameterType(1));
        assertEquals(6, pmd.getPrecision(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));

        ps.setString(1, "abcdef");
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "1");
        ps.setString(1, "abcd");
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "2");
        ps.setString(1, "ab");
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);
        ps.setString(1, "abcdefghi");
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        // The types in the WHEN clauses are incompatible, so the type of
        // the case operand cannot be inferred.
        assertCompileError("42818",
            "values case ? when 1 then true when like 'abc' then false end");

        assertCompileError("42818",
            "values case ? when like 'abc' then true when 1 then false end");

        // BLOB and CLOB are not comparable with anything.
        assertCompileError("42818",
                "values case ? when cast(x'abcd' as blob) then true end");
        assertCompileError("42818",
                "values case ? when cast('abcd' as clob) then true end");

        // Cannot infer type if both sides of the comparison are untyped.
        assertCompileError("42X35", "values case ? when ? then true end");
        assertCompileError("42X35", "values case ? when ? then true "
                                    + "when 1 then false end");

        // Should be able to infer type when the untyped parameter is prefixed
        // with plus or minus.
        ps = prepareStatement(
                "values (case +? when 1 then 1 when 2.1 then 2 end, "
                        + "case -? when 1 then 1 when 2.1 then 2 end)");
        pmd = ps.getParameterMetaData();
        assertEquals(Types.DECIMAL, pmd.getParameterType(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));
        assertEquals(Types.DECIMAL, pmd.getParameterType(2));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(2));

        ps.setInt(1, 1);
        ps.setInt(2, -1);
        JDBC.assertFullResultSet(ps.executeQuery(),
                                 new String[][] {{ "1", "1" }});

        ps.setInt(1, 2);
        ps.setInt(2, -2);
        JDBC.assertFullResultSet(ps.executeQuery(),
                                 new String[][] {{ null, null }});

        ps.setDouble(1, 1.1);
        ps.setDouble(2, -1.1);
        JDBC.assertFullResultSet(ps.executeQuery(),
                                 new String[][] {{ null, null }});

        ps.setDouble(1, 2.1);
        ps.setDouble(2, -2.1);
        JDBC.assertFullResultSet(ps.executeQuery(),
                                 new String[][] {{ "2", "2" }});

        // If the untyped parameter is part of an arithmetic expression, its
        // type is inferred from that expression and not from the WHEN clause.
        ps = prepareStatement(
                "values case 2*? when 2 then true when 3.0 then false end");
        pmd = ps.getParameterMetaData();
        assertEquals(Types.INTEGER, pmd.getParameterType(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));

        ps.setInt(1, 1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "true");
        ps.setDouble(1, 1.5);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "true");
        ps.setInt(1, 2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        ps = prepareStatement(
                "values case 2.0*? when 2 then true when 3.0 then false end");
        pmd = ps.getParameterMetaData();
        assertEquals(Types.DECIMAL, pmd.getParameterType(1));
        assertEquals(ParameterMetaData.parameterNullable, pmd.isNullable(1));

        ps.setInt(1, 1);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "true");
        ps.setDouble(1, 1.5);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), "false");
        ps.setInt(1, 2);
        JDBC.assertSingleValueResultSet(ps.executeQuery(), null);

        // The EXISTS predicate can only be used in the WHEN operand if
        // the CASE operand is a BOOLEAN.
        JDBC.assertSingleValueResultSet(
                s.executeQuery("values case true when exists" +
                               "(select * from sysibm.sysdummy1) then 1 end"),
                "1");
        assertCompileError("42818",
                "values case 1 when exists" +
                "(select * from sysibm.sysdummy1) then 1 end");

        // Scalar subqueries are allowed in the operands.
        JDBC.assertSingleValueResultSet(
                s.executeQuery(
                        "values case (select ibmreqd from sysibm.sysdummy1) "
                        + "when 'N' then 'no' when 'Y' then 'yes' end"),
                "yes");
        JDBC.assertSingleValueResultSet(
                s.executeQuery("values case 'Y' when "
                                + "(select ibmreqd from sysibm.sysdummy1) "
                                + "then 'yes' end"),
                "yes");

        // Subquery returns two columns - fail.
        assertCompileError(
                "42X39",
                "values case (select ibmreqd, 1 from sysibm.sysdummy1)"
                + " when 'Y' then true end");
        assertCompileError(
                "42X39",
                "values case 'Y' when "
                + "(select ibmreqd, 1 from sysibm.sysdummy1) then true end");

        // Subquery returns multiple rows - fail.
        assertStatementError("21000", s,
            "values case (select 1 from sys.systables) when 1 then true end");
        assertStatementError("21000", s,
            "values case 1 when (select 1 from sys.systables) then true end");

        // Subquery returns zero rows, which is converted to NULL for scalar
        // subqueries.
        JDBC.assertSingleValueResultSet(s.executeQuery(
                "values case (select ibmreqd from sysibm.sysdummy1 where false)"
                + " when is null then 'yes' end"),
            "yes");
        JDBC.assertSingleValueResultSet(s.executeQuery(
                "values case true when true then "
                + "(select ibmreqd from sysibm.sysdummy1 where false) end"),
            null);

        // Simple case expressions should work in join conditions.
        JDBC.assertSingleValueResultSet(
                s.executeQuery("select x from (values 1, 2, 3) v1(x) "
                                + "join (values 13, 14) v2(y) "
                                + "on case y-x when 10 then true end"),
                "3");
    }

    /**
     * Verify that the case operand expression is evaluated only once per
     * evaluation of the CASE expression.
     */
    public void testSingleEvaluationOfCaseOperand() throws SQLException {
        setAutoCommit(false);
        Statement s = createStatement();

        s.execute("create function count_me(x int) returns int "
                + "language java parameter style java external name '"
                + getClass().getName() + ".countMe' no sql deterministic");

        callCount.set(0);

        JDBC.assertUnorderedResultSet(
            s.executeQuery(
                "select case count_me(x) when 1 then 'one' when 2 then 'two' "
                + "when 3 then 'three' end from (values 1, 2, 3) v(x)"),
            new String[][] { {"one"}, {"two"}, {"three"} });

        // The CASE expression is evaluated once per row. There are three
        // rows. Expect that the COUNT_ME function was only invoked once
        // per row.
        assertEquals(3, callCount.get());
    }

    /** Count how many times countMe() has been called. */
    private static final AtomicInteger callCount = new AtomicInteger();

    /**
     * Stored function that keeps track of how many times it has been called.
     * @param i an integer
     * @return the integer {@code i}
     */
    public static int countMe(int i) {
        callCount.incrementAndGet();
        return i;
    }

    /**
     * Test that large objects can be used as case operands.
     */
    public void testLobAsCaseOperand() throws SQLException {
        Statement s = createStatement();

        // BLOB and CLOB are allowed in the case operand.
        JDBC.assertSingleValueResultSet(s.executeQuery(
                "values case cast(null as blob) when is null then 'yes' end"),
            "yes");
        JDBC.assertSingleValueResultSet(s.executeQuery(
                "values case cast(null as clob) when is null then 'yes' end"),
            "yes");

        // Comparisons between BLOB and BLOB, or between CLOB and CLOB, are
        // not allowed, so expect a compile-time error for these queries.
        assertCompileError("42818",
                "values case cast(null as blob) "
                + "when cast(null as blob) then true end");
        assertCompileError("42818",
                "values case cast(null as clob) "
                + "when cast(null as clob) then true end");

        // Now create a table with some actual LOBs in them.
        s.execute("create table lobs_for_simple_case("
                + "id int generated always as identity, b blob, c clob)");

        PreparedStatement insert = prepareStatement(
                "insert into lobs_for_simple_case(b, c) values (?, ?)");

        // A small one.
        insert.setBytes(1, new byte[] {1, 2, 3});
        insert.setString(2, "small");
        insert.executeUpdate();

        // And a big one (larger than 32K means it will be streamed
        // from store, instead of being returned as a materialized value).
        insert.setBinaryStream(1, new LoopingAlphabetStream(40000));
        insert.setCharacterStream(2, new LoopingAlphabetReader(40000));
        insert.executeUpdate();

        // And a NULL.
        insert.setNull(1, Types.BLOB);
        insert.setNull(2, Types.CLOB);
        insert.executeUpdate();

        // IS [NOT] NULL can be used on both BLOB and CLOB. LIKE can be
        // used on CLOB. Those are the only predicates supported on BLOB
        // and CLOB in simple case expressions currently. Test that they
        // all work.
        JDBC.assertUnorderedResultSet(
            s.executeQuery(
                "select id, case b when is null then 'yes'"
                + " when is not null then 'no' end, "
                + "case c when is null then 'yes' when like 'abc' then 'abc'"
                + " when like 'abc%' then 'abc...' when is not null then 'no'"
                + " end "
                + "from lobs_for_simple_case"),
            new String[][] {
                { "1", "no", "no" },
                { "2", "no", "abc..." },
                { "3", "yes", "yes" },
            });

        s.execute("drop table lobs_for_simple_case");
    }
}
