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");
}
}