You are viewing a plain text version of this content. The canonical link for it is here.
Posted to derby-commits@db.apache.org by ka...@apache.org on 2014/05/28 13:35:26 UTC

svn commit: r1597979 - in /db/derby/code/trunk/java: engine/org/apache/derby/impl/sql/compile/ testing/org/apache/derbyTesting/functionTests/tests/lang/

Author: kahatlen
Date: Wed May 28 11:35:25 2014
New Revision: 1597979

URL: http://svn.apache.org/r1597979
Log:
DERBY-1576: Extend the CASE expression syntax for "simple case"

Cache the case operand so that it is only evaluated once per
evaluation of the CASE expression.

Added:
    db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/CachedValueNode.java   (with props)
Modified:
    db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/ConditionalNode.java
    db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/sqlgrammar.jj
    db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/CaseExpressionTest.java

Added: db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/CachedValueNode.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/CachedValueNode.java?rev=1597979&view=auto
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/CachedValueNode.java (added)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/CachedValueNode.java Wed May 28 11:35:25 2014
@@ -0,0 +1,197 @@
+/*
+
+   Derby - Class org.apache.derby.impl.sql.compile.CachedValueNode
+
+   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.derby.impl.sql.compile;
+
+import java.lang.reflect.Modifier;
+import java.util.List;
+import org.apache.derby.iapi.error.StandardException;
+import org.apache.derby.iapi.reference.ClassName;
+import org.apache.derby.iapi.services.compiler.LocalField;
+import org.apache.derby.iapi.services.compiler.MethodBuilder;
+import org.apache.derby.iapi.sql.compile.Visitor;
+import org.apache.derby.iapi.types.DataTypeDescriptor;
+import org.apache.derby.iapi.util.JBitSet;
+
+/**
+ * <p>
+ * A wrapper class for a {@code ValueNode} that is referenced multiple
+ * places in the abstract syntax tree, but should only be evaluated once.
+ * This node will cache the return value the first time the expression
+ * is evaluated, and simply return the cached value the next time.
+ * </p>
+ *
+ * <p>For example, an expression such as</p>
+ *
+ * <pre>
+ *   CASE expr1
+ *     WHEN expr2 THEN expr3
+ *     WHEN expr4 THEN expr5
+ *   END
+ * </pre>
+ *
+ * <p>is rewritten by the parser to</p>
+ *
+ * <pre>
+ *   CASE
+ *     WHEN expr1 = expr2 THEN expr3
+ *     WHEN expr1 = expr4 THEN expr5
+ *   END
+ * </pre>
+ *
+ * <p>
+ * In this case, we want {@code expr1} to be evaluated only once, even
+ * though it's referenced twice in the rewritten tree. By wrapping the
+ * {@code ValueNode} for {@code expr1} in a {@code CachedValueNode}, we
+ * make sure {@code expr1} is only evaluated once, and the second reference
+ * to it will use the cached return value from the first evaluation.
+ * </p>
+ */
+class CachedValueNode extends ValueNode {
+
+    /** The node representing the expression whose value should be cached. */
+    private ValueNode value;
+
+    /** The field in the {@code Activation} class where the value is cached. */
+    private LocalField field;
+
+    /**
+     * Wrap the value in a {@code CachedValueNode}.
+     * @param value the value to wrap
+     */
+    CachedValueNode(ValueNode value) {
+        super(value.getContextManager());
+        this.value = value;
+    }
+
+    /**
+     * Generate code that returns the value that this expression evaluates
+     * to. For the first occurrence of this node in the abstract syntax
+     * tree, this method generates the code needed to evaluate the expression.
+     * Additionally, it stores the returned value in a field in the {@code
+     * Activation} class. For subsequent occurrences of this node, it will
+     * simply generate code that reads the value of that field, so that
+     * reevaluation is not performed.
+     *
+     * @param acb the class builder
+     * @param mb  the method builder
+     * @throws StandardException if an error occurs
+     */
+    @Override
+    void generateExpression(ExpressionClassBuilder acb, MethodBuilder mb)
+            throws StandardException {
+        if (field == null) {
+            // This is the first occurrence of the node, so we generate
+            // code for evaluating the expression and storing the returned
+            // value in a field.
+            field = acb.newFieldDeclaration(
+                    Modifier.PRIVATE, ClassName.DataValueDescriptor);
+            value.generateExpression(acb, mb);
+            mb.putField(field);
+        } else {
+            // This is not the first occurrence of the node, so we can
+            // simply read the cached value from the field instead of
+            // reevaluating the expression.
+            mb.getField(field);
+        }
+    }
+
+    /**
+     * Generate code that clears the field that holds the cached value, so
+     * that it can be garbage collected.
+     *
+     * @param mb the method builder that should have the code
+     */
+    void generateClearField(MethodBuilder mb) {
+        if (field != null) {
+            mb.pushNull(ClassName.DataValueDescriptor);
+            mb.setField(field);
+        }
+    }
+
+    // Overrides for various ValueNode methods. Simply forward the calls
+    // to the wrapped ValueNode.
+
+    @Override
+    ValueNode bindExpression(FromList fromList, SubqueryList subqueryList,
+                                 List<AggregateNode> aggregates)
+            throws StandardException {
+        value = value.bindExpression(fromList, subqueryList, aggregates);
+        return this;
+    }
+
+    @Override
+    ValueNode preprocess(int numTables,
+                         FromList outerFromList,
+                         SubqueryList outerSubqueryList,
+                         PredicateList outerPredicateList)
+            throws StandardException {
+        value = value.preprocess(numTables, outerFromList,
+                                 outerSubqueryList, outerPredicateList);
+        return this;
+    }
+
+    @Override
+    boolean isEquivalent(ValueNode other) throws StandardException {
+        if (other instanceof CachedValueNode) {
+            CachedValueNode that = (CachedValueNode) other;
+            return this.value.isEquivalent(that.value);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    void acceptChildren(Visitor v) throws StandardException {
+        super.acceptChildren(v);
+
+        if (value != null) {
+            value = (ValueNode) value.accept(v);
+        }
+    }
+
+    @Override
+    DataTypeDescriptor getTypeServices() {
+        return value.getTypeServices();
+    }
+
+    @Override
+    void setType(DataTypeDescriptor dtd) throws StandardException {
+        value.setType(dtd);
+    }
+
+    @Override
+    boolean requiresTypeFromContext() {
+        return value.requiresTypeFromContext();
+    }
+
+    @Override
+    ValueNode remapColumnReferencesToExpressions() throws StandardException {
+        value = value.remapColumnReferencesToExpressions();
+        return this;
+    }
+
+    @Override
+    boolean categorize(JBitSet referencedTabs, boolean simplePredsOnly)
+            throws StandardException {
+        return value.categorize(referencedTabs, simplePredsOnly);
+    }
+}

Propchange: db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/CachedValueNode.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/ConditionalNode.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/ConditionalNode.java?rev=1597979&r1=1597978&r2=1597979&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/ConditionalNode.java (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/ConditionalNode.java Wed May 28 11:35:25 2014
@@ -50,7 +50,7 @@ class ConditionalNode extends ValueNode
      * The case operand if this is a simple case expression. Otherwise, it
      * is {@code null}.
      */
-    private ValueNode caseOperand;
+    private CachedValueNode caseOperand;
 
     /** The list of test conditions in the WHEN clauses. */
     private ValueNodeList testConditions;
@@ -70,7 +70,7 @@ class ConditionalNode extends ValueNode
 	 * @param thenElseList		ValueNodeList with then and else expressions
      * @param cm                The context manager
 	 */
-    ConditionalNode(ValueNode caseOperand,
+    ConditionalNode(CachedValueNode caseOperand,
                     ValueNodeList testConditions,
                     ValueNodeList thenElseList,
                     ContextManager cm)
@@ -304,7 +304,7 @@ class ConditionalNode extends ValueNode
             int previousReliability = orReliability(
                     CompilerContext.CASE_OPERAND_RESTRICTION);
 
-            caseOperand = caseOperand.bindExpression(
+            caseOperand = (CachedValueNode) caseOperand.bindExpression(
                     fromList, subqueryList, aggregates);
 
             // For now, let's also forbid untyped parameters as case
@@ -512,6 +512,13 @@ class ConditionalNode extends ValueNode
         for (int i = 0; i < testConditions.size(); i++) {
             mb.completeConditional();
         }
+
+        // If we have a cached case operand, clear the field that holds
+        // the cached value after the case expression has been evaluated,
+        // so that the value can be garbage collected early.
+        if (caseOperand != null) {
+            caseOperand.generateClearField(mb);
+        }
 	}
 
 	/**

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/sqlgrammar.jj
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/sqlgrammar.jj?rev=1597979&r1=1597978&r2=1597979&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/sqlgrammar.jj (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/sql/compile/sqlgrammar.jj Wed May 28 11:35:25 2014
@@ -12577,6 +12577,7 @@ valueSpecification() throws StandardExce
         return new ConditionalNode(null, whenList, thenElseList, cm);
 	}
 |
+    // Searched CASE expression.
 	// CASE WHEN P1 THEN [T1 | NULL] (WHEN Pi THEN [Ti | NULL])* [ELSE E | NULL] END
     LOOKAHEAD({ getToken(1).kind == CASE && getToken(2).kind == WHEN })
     <CASE> value = searchedCaseExpression()
@@ -12584,8 +12585,13 @@ valueSpecification() throws StandardExce
 		return value;
 	}
 |
+    // Simple CASE expression.
+    // It will be rewritten to a searched CASE expression internally, for
+    // example: CASE x (WHEN Wi THEN Ti)+ -> CASE (WHEN x=Wi THEN Ti)+
+    // Wrap the case operand in a CachedValueNode to prevent it from being
+    // evaluated multiple times in the rewritten expression.
     <CASE> caseOperand = valueExpression()
-           value = simpleCaseExpression(caseOperand)
+           value = simpleCaseExpression(new CachedValueNode(caseOperand))
     {
         return value;
     }
@@ -12653,7 +12659,7 @@ thenElseExpression() throws StandardExce
 	}
 }
 
-ConditionalNode simpleCaseExpression(ValueNode caseOperand)
+ConditionalNode simpleCaseExpression(CachedValueNode caseOperand)
 throws StandardException :
 {
     ContextManager cm = getContextManager();

Modified: db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/CaseExpressionTest.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/CaseExpressionTest.java?rev=1597979&r1=1597978&r2=1597979&view=diff
==============================================================================
--- db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/CaseExpressionTest.java (original)
+++ db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/lang/CaseExpressionTest.java Wed May 28 11:35:25 2014
@@ -29,10 +29,13 @@ import java.sql.ResultSet;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import junit.framework.Test;
 import junit.framework.TestSuite;
 
+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.CleanDatabaseTestSetup;
 import org.apache.derbyTesting.junit.JDBC;
@@ -843,5 +846,118 @@ public class CaseExpressionTest extends 
                 "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");
     }
 }