This is an automated email from the ASF dual-hosted git repository. rajeshbabu pushed a commit to branch 4.x-HBase-1.4 in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/4.x-HBase-1.4 by this push: new d50991e PHOENIX-5136 Rows with null values inserted by UPSERT .. ON DUPLICATE KEY UPDATE are included in query results when they shouldn't be(Miles Spielberg) d50991e is described below commit d50991ec3606cadbd73e7c3c1c65a87ef7821cd4 Author: Rajeshbabu Chintaguntla <Rajeshbabu Chintaguntla> AuthorDate: Mon Aug 26 22:02:56 2019 +0530 PHOENIX-5136 Rows with null values inserted by UPSERT .. ON DUPLICATE KEY UPDATE are included in query results when they shouldn't be(Miles Spielberg) --- .../apache/phoenix/end2end/OnDuplicateKeyIT.java | 34 +++ .../apache/phoenix/expression/AndExpression.java | 2 +- .../apache/phoenix/expression/AndOrExpression.java | 25 +- .../phoenix/filter/BooleanExpressionFilter.java | 5 +- .../phoenix/expression/AndExpressionTest.java | 297 +++++++++++++++++++++ .../phoenix/expression/OrExpressionTest.java | 293 ++++++++++++++++++++ 6 files changed, 648 insertions(+), 8 deletions(-) diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/OnDuplicateKeyIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/OnDuplicateKeyIT.java index f1ee0e7..4782e57 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/OnDuplicateKeyIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/OnDuplicateKeyIT.java @@ -580,6 +580,40 @@ public class OnDuplicateKeyIT extends ParallelStatsDisabledIT { } } + @Test + public void testRowsCreatedViaUpsertOnDuplicateKeyShouldNotBeReturnedInQueryIfNotMatched() throws Exception { + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + Connection conn = DriverManager.getConnection(getUrl(), props); + String tableName = generateUniqueName(); + String ddl = "create table " + tableName + "(pk varchar primary key, counter1 bigint, counter2 smallint)"; + conn.createStatement().execute(ddl); + createIndex(conn, tableName); + // The data has to be specifically starting with null for the first counter to fail the test. If you reverse the values, the test passes. + String dml1 = "UPSERT INTO " + tableName + " VALUES('a',NULL,2) ON DUPLICATE KEY UPDATE " + + "counter1 = CASE WHEN (counter1 IS NULL) THEN NULL ELSE counter1 END, " + + "counter2 = CASE WHEN (counter1 IS NULL) THEN 2 ELSE counter2 END"; + conn.createStatement().execute(dml1); + conn.commit(); + + String dml2 = "UPSERT INTO " + tableName + " VALUES('b',1,2) ON DUPLICATE KEY UPDATE " + + "counter1 = CASE WHEN (counter1 IS NULL) THEN 1 ELSE counter1 END, " + + "counter2 = CASE WHEN (counter1 IS NULL) THEN 2 ELSE counter2 END"; + conn.createStatement().execute(dml2); + conn.commit(); + + // Using this statement causes the test to pass + //ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM " + tableName + " WHERE counter2 = 2 AND counter1 = 1"); + // This statement should be equivalent to the one above, but it selects both rows. + ResultSet rs = conn.createStatement().executeQuery("SELECT pk, counter1, counter2 FROM " + tableName + " WHERE counter2 = 2 AND (counter1 = 1 OR counter1 = 1)"); + assertTrue(rs.next()); + assertEquals("b",rs.getString(1)); + assertEquals(1,rs.getLong(2)); + assertEquals(2,rs.getLong(3)); + assertFalse(rs.next()); + + conn.close(); + } + } diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/AndExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/AndExpression.java index 70e94ca..2aa1827 100644 --- a/phoenix-core/src/main/java/org/apache/phoenix/expression/AndExpression.java +++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/AndExpression.java @@ -80,7 +80,7 @@ public class AndExpression extends AndOrExpression { @Override protected boolean isStopValue(Boolean value) { - return !Boolean.TRUE.equals(value); + return Boolean.FALSE.equals(value); } @Override diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/AndOrExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/AndOrExpression.java index ea8c375..07b07a2 100644 --- a/phoenix-core/src/main/java/org/apache/phoenix/expression/AndOrExpression.java +++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/AndOrExpression.java @@ -24,7 +24,7 @@ import org.apache.hadoop.hbase.io.ImmutableBytesWritable; import org.apache.phoenix.schema.types.PBoolean; import org.apache.phoenix.schema.types.PDataType; import org.apache.phoenix.schema.tuple.Tuple; - +import org.apache.phoenix.util.ByteUtil; /** * @@ -36,6 +36,8 @@ import org.apache.phoenix.schema.tuple.Tuple; public abstract class AndOrExpression extends BaseCompoundExpression { // Remember evaluation of child expression for partial evaluation private BitSet partialEvalState; + // true if we have seen NULL as the value of some child expression + private boolean seenNull = false; public AndOrExpression() { } @@ -56,34 +58,45 @@ public abstract class AndOrExpression extends BaseCompoundExpression { } else { partialEvalState.clear(); } + seenNull = false; super.reset(); } @Override public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) { - boolean isNull = false; + boolean childFailed = false; for (int i = 0; i < children.size(); i++) { - Expression child = children.get(i); // If partial state is available, then use that to know we've already evaluated this // child expression and do not need to do so again. if (partialEvalState == null || !partialEvalState.get(i)) { + Expression child = children.get(i); // Call through to child evaluate method matching parent call to allow child to optimize // evaluate versus getValue code path. if (child.evaluate(tuple, ptr)) { // Short circuit if we see our stop value if (isStopValue((Boolean) PBoolean.INSTANCE.toObject(ptr, child.getDataType()))) { return true; - } else if (partialEvalState != null) { + } + if (ptr.getLength() == 0) { + seenNull = true; + } + if (partialEvalState != null) { partialEvalState.set(i); } } else { - isNull = true; + childFailed = true; } } } - if (isNull) { + if (childFailed) { return false; } + + if (seenNull) { + // Some child evaluated to NULL and we never saw a stop value. + // The expression evaluates as NULL even if the last child evaluated was non-NULL. + ptr.set(ByteUtil.EMPTY_BYTE_ARRAY); + } return true; } diff --git a/phoenix-core/src/main/java/org/apache/phoenix/filter/BooleanExpressionFilter.java b/phoenix-core/src/main/java/org/apache/phoenix/filter/BooleanExpressionFilter.java index 678ccca..80710c4 100644 --- a/phoenix-core/src/main/java/org/apache/phoenix/filter/BooleanExpressionFilter.java +++ b/phoenix-core/src/main/java/org/apache/phoenix/filter/BooleanExpressionFilter.java @@ -96,7 +96,10 @@ abstract public class BooleanExpressionFilter extends FilterBase implements Writ } catch (IllegalDataException e) { return Boolean.FALSE; } - return (Boolean)expression.getDataType().toObject(tempPtr); + // If the entire Boolean expression evaluated to completion (evaluate returned true), + // but the result was SQL NULL, treat it as Java Boolean FALSE rather than returning null, + // which is used above to indicate incomplete evaluation. + return Boolean.TRUE.equals(expression.getDataType().toObject(tempPtr)); } @Override diff --git a/phoenix-core/src/test/java/org/apache/phoenix/expression/AndExpressionTest.java b/phoenix-core/src/test/java/org/apache/phoenix/expression/AndExpressionTest.java new file mode 100644 index 0000000..a8f1529 --- /dev/null +++ b/phoenix-core/src/test/java/org/apache/phoenix/expression/AndExpressionTest.java @@ -0,0 +1,297 @@ +package org.apache.phoenix.expression; + +import org.apache.hadoop.hbase.Cell; +import org.apache.hadoop.hbase.CellUtil; +import org.apache.hadoop.hbase.KeyValue; +import org.apache.hadoop.hbase.io.ImmutableBytesWritable; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.phoenix.query.QueryConstants; +import org.apache.phoenix.schema.PBaseColumn; +import org.apache.phoenix.schema.PColumn; +import org.apache.phoenix.schema.PName; +import org.apache.phoenix.schema.PNameFactory; +import org.apache.phoenix.schema.SortOrder; +import org.apache.phoenix.schema.tuple.MultiKeyValueTuple; +import org.apache.phoenix.schema.types.PBoolean; +import org.apache.phoenix.schema.types.PDataType; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AndExpressionTest { + + private AndExpression createAnd(Expression lhs, Expression rhs) { + return new AndExpression(Arrays.asList(lhs, rhs)); + } + + private AndExpression createAnd(Boolean x, Boolean y) { + return createAnd(LiteralExpression.newConstant(x), LiteralExpression.newConstant(y)); + } + + private void testImmediateSingle(Boolean expected, Boolean lhs, Boolean rhs) { + AndExpression and = createAnd(lhs, rhs); + ImmutableBytesWritable out = new ImmutableBytesWritable(); + MultiKeyValueTuple tuple = new MultiKeyValueTuple(); + boolean success = and.evaluate(tuple, out); + assertTrue(success); + assertEquals(expected, PBoolean.INSTANCE.toObject(out)); + } + + // Evaluating AND when values of both sides are known should immediately succeed + // and return the same result regardless of order. + private void testImmediate(Boolean expected, Boolean a, Boolean b) { + testImmediateSingle(expected, a, b); + testImmediateSingle(expected, b, a); + } + + private PColumn pcolumn(final String name) { + return new PBaseColumn() { + @Override public PName getName() { + return PNameFactory.newName(name); + } + + @Override public PDataType getDataType() { + return PBoolean.INSTANCE; + } + + @Override public PName getFamilyName() { + return PNameFactory.newName(QueryConstants.DEFAULT_COLUMN_FAMILY); + } + + @Override public int getPosition() { + return 0; + } + + @Override public Integer getArraySize() { + return null; + } + + @Override public byte[] getViewConstant() { + return new byte[0]; + } + + @Override public boolean isViewReferenced() { + return false; + } + + @Override public String getExpressionStr() { + return null; + } + + @Override public boolean isRowTimestamp() { + return false; + } + + @Override public boolean isDynamic() { + return false; + } + + @Override public byte[] getColumnQualifierBytes() { + return null; + } + + @Override public long getTimestamp() { + return 0; + } + + @Override public boolean isDerived() { + return false; + } + + @Override public boolean isExcluded() { + return false; + } + + @Override public SortOrder getSortOrder() { + return null; + } + }; + } + + private KeyValueColumnExpression kvExpr(final String name) { + return new KeyValueColumnExpression(pcolumn(name)); + } + + private Cell createCell(String name, Boolean value) { + byte[] valueBytes = value == null ? null : value ? PBoolean.TRUE_BYTES : PBoolean.FALSE_BYTES; + return CellUtil.createCell( + Bytes.toBytes("row"), + QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, + Bytes.toBytes(name), + 1, + KeyValue.Type.Put.getCode(), + valueBytes); + } + + private void testPartialOneSideFirst(Boolean expected, Boolean lhs, Boolean rhs, boolean leftFirst) { + KeyValueColumnExpression lhsExpr = kvExpr("LHS"); + KeyValueColumnExpression rhsExpr = kvExpr("RHS"); + AndExpression and = createAnd(lhsExpr, rhsExpr); + MultiKeyValueTuple tuple = new MultiKeyValueTuple(Collections.<Cell>emptyList()); + ImmutableBytesWritable out = new ImmutableBytesWritable(); + + // with no data available, should fail + boolean success = and.evaluate(tuple, out); + assertFalse(success); + + // with 1 datum available, should fail + if (leftFirst) { + tuple.setKeyValues(Collections.singletonList(createCell("LHS", lhs))); + } else { + tuple.setKeyValues(Collections.singletonList(createCell("RHS", rhs))); + } + success = and.evaluate(tuple, out); + assertFalse(success); + + // with 2 data available, should succeed + tuple.setKeyValues(Arrays.asList(createCell("LHS", lhs), createCell("RHS", rhs))); + success = and.evaluate(tuple, out); + assertTrue(success); + assertEquals(expected, PBoolean.INSTANCE.toObject(out)); + } + + private void testPartialEvaluation(Boolean expected, Boolean x, Boolean y, boolean xFirst) { + testPartialOneSideFirst(expected, x, y, xFirst); + testPartialOneSideFirst(expected, y, x, !xFirst); + } + + private void testShortCircuitOneSideFirst(Boolean expected, Boolean lhs, Boolean rhs, boolean leftFirst) { + KeyValueColumnExpression lhsExpr = kvExpr("LHS"); + KeyValueColumnExpression rhsExpr = kvExpr("RHS"); + AndExpression and = createAnd(lhsExpr, rhsExpr); + MultiKeyValueTuple tuple = new MultiKeyValueTuple(Collections.<Cell>emptyList()); + ImmutableBytesWritable out = new ImmutableBytesWritable(); + + // with no data available, should fail + boolean success = and.evaluate(tuple, out); + assertFalse(success); + + // with 1 datum available, should succeed + if (leftFirst) { + tuple.setKeyValues(Collections.singletonList(createCell("LHS", lhs))); + } else { + tuple.setKeyValues(Collections.singletonList(createCell("RHS", rhs))); + } + success = and.evaluate(tuple, out); + assertTrue(success); + assertEquals(expected, PBoolean.INSTANCE.toObject(out)); + } + + + private void testShortCircuit(Boolean expected, Boolean x, Boolean y, boolean xFirst) { + testShortCircuitOneSideFirst(expected, x, y, xFirst); + testShortCircuitOneSideFirst(expected, y, x, !xFirst); + } + + @Test + public void testImmediateCertainty() { + testImmediate(true, true, true); + testImmediate(false, false, true); + testImmediate(false, false, false); + } + + @Test + public void testImmediateUncertainty() { + testImmediate(null, true, null); + testImmediate(false, false, null); + testImmediate(null, null, null); + } + + @Test + public void testPartialCertainty() { + // T AND T = T + // must evaluate both sides, regardless of order + testPartialEvaluation(true, true, true, true); + testPartialEvaluation(true, true, true, false); + + // T AND F = F + // must evaluate both sides if TRUE is evaluated first + testPartialEvaluation(false, true, false, true); + testPartialEvaluation(false, false, true, false); + } + + @Test + public void testPartialUncertainty() { + // T AND NULL = NULL + // must evaluate both sides, regardless of order of values or evaluation + testPartialEvaluation(null, true, null, true); + testPartialEvaluation(null, true, null, false); + testPartialEvaluation(null, null, true, true); + testPartialEvaluation(null, null, true, false); + + // must evaluate both sides if NULL is evaluated first + + // F AND NULL = FALSE + testPartialEvaluation(false, null, false, true); + testPartialEvaluation(false, false, null, false); + + // NULL AND NULL = NULL + testPartialEvaluation(null, null, null, true); + testPartialEvaluation(null, null, null, false); + } + + @Test + public void testShortCircuitCertainty() { + // need only to evaluate one side if FALSE is evaluated first + + // F AND F = F + testShortCircuit(false, false, false, true); + testShortCircuit(false, false, false, false); + + // T AND F = F + testShortCircuit(false, false, true, true); + testShortCircuit(false, true, false, false); + } + + @Test + public void testShortCircuitUncertainty() { + // need only to evaluate one side if FALSE is evaluated first + + // F AND NULL = FALSE + testShortCircuit(false, false, null, true); + testShortCircuit(false, null, false, false); + } + + @Test + public void testTruthTable() { + // See: https://en.wikipedia.org/wiki/Null_(SQL)#Comparisons_with_NULL_and_the_three-valued_logic_(3VL) + Boolean[][] testCases = new Boolean[][] { + // should short circuit? + // X, Y, if X first, if Y first, X AND Y, + { true, true, false, false, true, }, + { true, false, false, true, false, }, + { false, false, true, true, false, }, + { true, null, false, false, null, }, + { false, null, true, false, false, }, + { null, null, false, false, null, }, + }; + + for (Boolean[] testCase : testCases) { + Boolean x = testCase[0]; + Boolean y = testCase[1]; + boolean shouldShortCircuitWhenXEvaluatedFirst = testCase[2]; + boolean shouldShortCircuitWhenYEvaluatedFirst = testCase[3]; + Boolean expected = testCase[4]; + + // test both directions + testImmediate(expected, x, y); + + if (shouldShortCircuitWhenXEvaluatedFirst) { + testShortCircuit(expected, x, y, true); + } else { + testPartialEvaluation(expected, x, y, true); + } + + if (shouldShortCircuitWhenYEvaluatedFirst) { + testShortCircuit(expected, x, y, false); + } else { + testPartialEvaluation(expected, x, y, false); + } + } + } +} diff --git a/phoenix-core/src/test/java/org/apache/phoenix/expression/OrExpressionTest.java b/phoenix-core/src/test/java/org/apache/phoenix/expression/OrExpressionTest.java new file mode 100644 index 0000000..7ab7a6d --- /dev/null +++ b/phoenix-core/src/test/java/org/apache/phoenix/expression/OrExpressionTest.java @@ -0,0 +1,293 @@ +package org.apache.phoenix.expression; + +import org.apache.hadoop.hbase.Cell; +import org.apache.hadoop.hbase.CellUtil; +import org.apache.hadoop.hbase.KeyValue; +import org.apache.hadoop.hbase.io.ImmutableBytesWritable; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.phoenix.query.QueryConstants; +import org.apache.phoenix.schema.PBaseColumn; +import org.apache.phoenix.schema.PColumn; +import org.apache.phoenix.schema.PName; +import org.apache.phoenix.schema.PNameFactory; +import org.apache.phoenix.schema.SortOrder; +import org.apache.phoenix.schema.tuple.MultiKeyValueTuple; +import org.apache.phoenix.schema.types.PBoolean; +import org.apache.phoenix.schema.types.PDataType; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class OrExpressionTest { + + private OrExpression createOr(Expression lhs, Expression rhs) { + return new OrExpression(Arrays.asList(lhs, rhs)); + } + + private OrExpression createOr(Boolean x, Boolean y) { + return createOr(LiteralExpression.newConstant(x), LiteralExpression.newConstant(y)); + } + + private void testImmediateSingle(Boolean expected, Boolean lhs, Boolean rhs) { + OrExpression or = createOr(lhs, rhs); + ImmutableBytesWritable out = new ImmutableBytesWritable(); + MultiKeyValueTuple tuple = new MultiKeyValueTuple(); + boolean success = or.evaluate(tuple, out); + assertTrue(success); + assertEquals(expected, PBoolean.INSTANCE.toObject(out)); + } + + // Evaluating OR when values of both sides are known should immediately succeed + // and return the same result regardless of order. + private void testImmediate(Boolean expected, Boolean a, Boolean b) { + testImmediateSingle(expected, a, b); + testImmediateSingle(expected, b, a); + } + + private PColumn pcolumn(final String name) { + return new PBaseColumn() { + @Override public PName getName() { + return PNameFactory.newName(name); + } + + @Override public PDataType getDataType() { + return PBoolean.INSTANCE; + } + + @Override public PName getFamilyName() { + return PNameFactory.newName(QueryConstants.DEFAULT_COLUMN_FAMILY); + } + + @Override public int getPosition() { + return 0; + } + + @Override public Integer getArraySize() { + return null; + } + + @Override public byte[] getViewConstant() { + return new byte[0]; + } + + @Override public boolean isViewReferenced() { + return false; + } + + @Override public String getExpressionStr() { + return null; + } + + @Override public boolean isRowTimestamp() { + return false; + } + + @Override public boolean isDynamic() { + return false; + } + + @Override public byte[] getColumnQualifierBytes() { + return null; + } + + @Override public long getTimestamp() { + return 0; + } + + @Override public boolean isDerived() { + return false; + } + + @Override public boolean isExcluded() { + return false; + } + + @Override public SortOrder getSortOrder() { + return null; + } + }; + } + + private KeyValueColumnExpression kvExpr(final String name) { + return new KeyValueColumnExpression(pcolumn(name)); + } + + private Cell createCell(String name, Boolean value) { + byte[] valueBytes = value == null ? null : value ? PBoolean.TRUE_BYTES : PBoolean.FALSE_BYTES; + return CellUtil.createCell( + Bytes.toBytes("row"), + QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, + Bytes.toBytes(name), + 1, + KeyValue.Type.Put.getCode(), + valueBytes); + } + + private void testPartialOneSideFirst(Boolean expected, Boolean lhs, Boolean rhs, boolean leftFirst) { + KeyValueColumnExpression lhsExpr = kvExpr("LHS"); + KeyValueColumnExpression rhsExpr = kvExpr("RHS"); + OrExpression or = createOr(lhsExpr, rhsExpr); + MultiKeyValueTuple tuple = new MultiKeyValueTuple(Collections.<Cell>emptyList()); + ImmutableBytesWritable out = new ImmutableBytesWritable(); + + // with no data available, should fail + boolean success = or.evaluate(tuple, out); + assertFalse(success); + + // with 1 datum available, should fail + if (leftFirst) { + tuple.setKeyValues(Collections.singletonList(createCell("LHS", lhs))); + } else { + tuple.setKeyValues(Collections.singletonList(createCell("RHS", rhs))); + } + success = or.evaluate(tuple, out); + assertFalse(success); + + // with 2 data available, should succeed + tuple.setKeyValues(Arrays.asList(createCell("LHS", lhs), createCell("RHS", rhs))); + success = or.evaluate(tuple, out); + assertTrue(success); + assertEquals(expected, PBoolean.INSTANCE.toObject(out)); + } + + private void testPartialEvaluation(Boolean expected, Boolean x, Boolean y, boolean xFirst) { + testPartialOneSideFirst(expected, x, y, xFirst); + testPartialOneSideFirst(expected, y, x, !xFirst); + } + + private void testShortCircuitOneSideFirst(Boolean expected, Boolean lhs, Boolean rhs, boolean leftFirst) { + KeyValueColumnExpression lhsExpr = kvExpr("LHS"); + KeyValueColumnExpression rhsExpr = kvExpr("RHS"); + OrExpression or = createOr(lhsExpr, rhsExpr); + MultiKeyValueTuple tuple = new MultiKeyValueTuple(Collections.<Cell>emptyList()); + ImmutableBytesWritable out = new ImmutableBytesWritable(); + + // with no data available, should fail + boolean success = or.evaluate(tuple, out); + assertFalse(success); + + // with 1 datum available, should succeed + if (leftFirst) { + tuple.setKeyValues(Collections.singletonList(createCell("LHS", lhs))); + } else { + tuple.setKeyValues(Collections.singletonList(createCell("RHS", rhs))); + } + success = or.evaluate(tuple, out); + assertTrue(success); + assertEquals(expected, PBoolean.INSTANCE.toObject(out)); + } + + + private void testShortCircuit(Boolean expected, Boolean x, Boolean y, boolean xFirst) { + testShortCircuitOneSideFirst(expected, x, y, xFirst); + testShortCircuitOneSideFirst(expected, y, x, !xFirst); + } + + @Test + public void testImmediateCertainty() { + testImmediate(true, true, true); + testImmediate(true, false, true); + testImmediate(false, false, false); + } + + @Test + public void testImmediateUncertainty() { + testImmediate(true, true, null); + testImmediate(null, false, null); + testImmediate(null, null, null); + } + + @Test + public void testPartialCertainty() { + // must evaluate both sides if FALSE is evaluated first + + // F OR F = F + testPartialEvaluation(false, false, false, true); + testPartialEvaluation(false, false, false, false); + + // T OR F = T + testPartialEvaluation(true, false, true, true); + testPartialEvaluation(true, true, false, false); + } + + @Test + public void testPartialUncertainty() { + // T OR NULL = NULL + testPartialEvaluation(true, null, true, true); + testPartialEvaluation(true, true, null, false); + + // must evaluate both sides if NULL is evaluated first + + // F OR NULL = NULL + testPartialEvaluation(null, null, false, true); + testPartialEvaluation(null, false, null, false); + + // NULL OR NULL = NULL + testPartialEvaluation(null, null, null, true); + testPartialEvaluation(null, null, null, false); + } + + @Test + public void testShortCircuitCertainty() { + // need only to evaluate one side if TRUE is evaluated first + + // T OR T = T + testShortCircuit(true, true, true, true); + testShortCircuit(true, true, true, false); + + + // T OR F = F + testShortCircuit(true, true, false, true); + testShortCircuit(true, false, true, false); + } + + @Test + public void testShortCircuitUncertainty() { + // need only to evaluate one side if TRUE is evaluated first + testShortCircuit(true, true, null, true); + testShortCircuit(true, null, true, false); + } + + @Test + public void testTruthTable() { + // See: https://en.wikipedia.org/wiki/Null_(SQL)#Comparisons_with_NULL_and_the_three-valued_logic_(3VL) + Boolean[][] testCases = new Boolean[][] { + // should short circuit? + // X, Y, if X first, if Y first, X OR Y, + { true, true, true, true, true, }, + { true, false, true, false, true, }, + { false, false, false, false, false, }, + { true, null, true, false, true, }, + { false, null, false, false, null, }, + { null, null, false, false, null, }, + }; + + for (Boolean[] testCase : testCases) { + Boolean x = testCase[0]; + Boolean y = testCase[1]; + boolean shouldShortCircuitWhenXEvaluatedFirst = testCase[2]; + boolean shouldShortCircuitWhenYEvaluatedFirst = testCase[3]; + Boolean expected = testCase[4]; + + // test both directions + testImmediate(expected, x, y); + + if (shouldShortCircuitWhenXEvaluatedFirst) { + testShortCircuit(expected, x, y, true); + } else { + testPartialEvaluation(expected, x, y, true); + } + + if (shouldShortCircuitWhenYEvaluatedFirst) { + testShortCircuit(expected, x, y, false); + } else { + testPartialEvaluation(expected, x, y, false); + } + } + } +}