You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/02/07 11:26:02 UTC

cayenne git commit: CAY-2221 Comparisons with NULL values (include CAY-1926)

Repository: cayenne
Updated Branches:
  refs/heads/master c2d815dd0 -> 8e43461e4


CAY-2221 Comparisons with NULL values (include CAY-1926)


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/8e43461e
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/8e43461e
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/8e43461e

Branch: refs/heads/master
Commit: 8e43461e4e473ae3137e5b263d50df3e2448206c
Parents: c2d815d
Author: Nikita Timofeev <st...@gmail.com>
Authored: Tue Feb 7 14:25:50 2017 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Tue Feb 7 14:25:50 2017 +0300

----------------------------------------------------------------------
 .../java/org/apache/cayenne/exp/Expression.java |   2 +-
 .../org/apache/cayenne/exp/parser/ASTAnd.java   |  13 ++-
 .../apache/cayenne/exp/parser/ASTBetween.java   |   2 +-
 .../org/apache/cayenne/exp/parser/ASTEqual.java |   4 +-
 .../org/apache/cayenne/exp/parser/ASTFalse.java |   4 +-
 .../apache/cayenne/exp/parser/ASTGreater.java   |   7 +-
 .../cayenne/exp/parser/ASTGreaterOrEqual.java   |   7 +-
 .../org/apache/cayenne/exp/parser/ASTIn.java    |  13 ++-
 .../org/apache/cayenne/exp/parser/ASTLess.java  |   7 +-
 .../cayenne/exp/parser/ASTLessOrEqual.java      |   7 +-
 .../org/apache/cayenne/exp/parser/ASTLike.java  |   2 +-
 .../cayenne/exp/parser/ASTLikeIgnoreCase.java   |   2 +-
 .../org/apache/cayenne/exp/parser/ASTNot.java   |   7 +-
 .../cayenne/exp/parser/ASTNotBetween.java       |   2 +-
 .../apache/cayenne/exp/parser/ASTNotEqual.java  |   4 +-
 .../org/apache/cayenne/exp/parser/ASTNotIn.java |   8 +-
 .../apache/cayenne/exp/parser/ASTNotLike.java   |   2 +-
 .../exp/parser/ASTNotLikeIgnoreCase.java        |   2 +-
 .../org/apache/cayenne/exp/parser/ASTOr.java    |  13 ++-
 .../org/apache/cayenne/exp/parser/ASTTrue.java  |   4 +-
 .../cayenne/exp/parser/ConditionNode.java       |   4 +-
 .../apache/cayenne/exp/parser/Evaluator.java    |  49 +++-----
 .../parser/ExpressionEvaluateInMemoryTest.java  |  63 ++++++++++
 .../exp/parser/ExpressionEvaluationIT.java      | 115 +++++++++++++++++--
 24 files changed, 258 insertions(+), 85 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
index b302a04..8491e23 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
@@ -507,7 +507,7 @@ public abstract class Expression implements Serializable, XMLSerializable {
 	@SuppressWarnings("unchecked")
 	public <T> List<T> filterObjects(Collection<T> objects) {
 		if (objects == null || objects.size() == 0) {
-			return new LinkedList<T>(); // returning Collections.emptyList() could cause random client exceptions if they try to mutate the resulting list
+			return new LinkedList<>(); // returning Collections.emptyList() could cause random client exceptions if they try to mutate the resulting list
 		}
 
 		return (List<T>) filter(objects, new LinkedList<T>());

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAnd.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAnd.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAnd.java
index 33fdcb1..e67957e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAnd.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAnd.java
@@ -74,13 +74,20 @@ public class ASTAnd extends AggregateConditionNode implements ValueInjector {
 			return Boolean.FALSE;
 		}
 
+		// https://en.wikipedia.org/wiki/Three-valued_logic
+		boolean unknown = false;
+		boolean result = true;
 		for (int i = 0; i < len; i++) {
-			if (!ConversionUtil.toBoolean(evaluateChild(i, o))) {
-				return Boolean.FALSE;
+			Object value = evaluateChild(i, o);
+			if (value == null) {
+				unknown = true;
+			} else if (!ConversionUtil.toBoolean(value)) {
+				result = false;
+				break;
 			}
 		}
 
-		return Boolean.TRUE;
+		return result ? (unknown ? null : Boolean.TRUE) : Boolean.FALSE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
index 0d65c84..ad4008f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
@@ -53,7 +53,7 @@ public class ASTBetween extends ConditionNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		Object o1 = evaluatedChildren[1];
 		Object o2 = evaluatedChildren[2];
 		Evaluator e = Evaluator.evaluator(o);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
index 3f313e5..f63a1ba 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
@@ -64,9 +64,9 @@ public class ASTEqual extends ConditionNode implements ValueInjector {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		Object o2 = evaluatedChildren[1];
-		return evaluateImpl(o, o2);
+		return evaluateImpl(o, o2) ? Boolean.TRUE : Boolean.FALSE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFalse.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFalse.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFalse.java
index 22145d0..5242764 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFalse.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFalse.java
@@ -56,8 +56,8 @@ public class ASTFalse extends ConditionNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
-		return false;
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+		return Boolean.FALSE;
 	}
 
 	@Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
index 25f1af9..ca60fc2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
@@ -52,10 +52,13 @@ public class ASTGreater extends ConditionNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		Object o2 = evaluatedChildren[1];
 		Integer c = Evaluator.evaluator(o).compare(o, o2);
-		return c != null && c > 0 ? Boolean.TRUE : Boolean.FALSE;
+		if(c == null) {
+			return null;
+		}
+		return c > 0 ? Boolean.TRUE : Boolean.FALSE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
index fb93fa8..ee71273 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
@@ -54,10 +54,13 @@ public class ASTGreaterOrEqual extends ConditionNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		Object o2 = evaluatedChildren[1];
 		Integer c = Evaluator.evaluator(o).compare(o, o2);
-		return c != null && c >= 0 ? Boolean.TRUE : Boolean.FALSE;
+		if(c == null) {
+			return null;
+		}
+		return c >= 0 ? Boolean.TRUE : Boolean.FALSE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
index 31a4ac36..e91c5a3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
@@ -54,21 +54,22 @@ public class ASTIn extends ConditionNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
-        // TODO: what if there's a NULL inside IN list?
-        // e.g. ASTEqual evals as "NULL == NULL"
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		if (o == null || evaluatedChildren[1] == null) {
-			return false;
+			// Even if there is NULL value in list we should return false,
+			// as check against NULL can be done only with IS NULL operator
+			// and moreover not all DB accept syntax like 'value IN (NULL)'
+			return Boolean.FALSE;
 		}
 
 		Object[] objects = (Object[]) evaluatedChildren[1];
 		for (Object object : objects) {
 			if (object != null && Evaluator.evaluator(o).eq(o, object)) {
-				return true;
+				return Boolean.TRUE;
 			}
 		}
 
-		return false;
+		return Boolean.FALSE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
index d8c75cc..81e5baf 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
@@ -54,10 +54,13 @@ public class ASTLess extends ConditionNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		Object o2 = evaluatedChildren[1];
 		Integer c = Evaluator.evaluator(o).compare(o, o2);
-		return c != null && c < 0 ? Boolean.TRUE : Boolean.FALSE;
+		if(c == null) {
+			return null;
+		}
+		return c < 0 ? Boolean.TRUE : Boolean.FALSE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
index 09d10cb..717e064 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
@@ -54,10 +54,13 @@ public class ASTLessOrEqual extends ConditionNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		Object o2 = evaluatedChildren[1];
 		Integer c = Evaluator.evaluator(o).compare(o, o2);
-		return c != null && c <= 0 ? Boolean.TRUE : Boolean.FALSE;
+		if(c == null) {
+			return null;
+		}
+		return c <= 0 ? Boolean.TRUE : Boolean.FALSE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
index 9d1eda6..bd45d9b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
@@ -61,7 +61,7 @@ public class ASTLike extends PatternMatchNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		String s1 = ConversionUtil.toString(o);
 		if (s1 == null) {
 			return Boolean.FALSE;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
index b0bd207..1cdc7f0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
@@ -62,7 +62,7 @@ public class ASTLikeIgnoreCase extends IgnoreCaseNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		String s1 = ConversionUtil.toString(o);
 		if (s1 == null) {
 			return Boolean.FALSE;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNot.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNot.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNot.java
index 0c493e4..9973ccc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNot.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNot.java
@@ -55,7 +55,12 @@ public class ASTNot extends AggregateConditionNode {
 			return Boolean.FALSE;
 		}
 
-		return ConversionUtil.toBoolean(evaluateChild(0, o)) ? Boolean.FALSE : Boolean.TRUE;
+		Object o1 = evaluateChild(0, o);
+		if (o1 == null) {
+			return null;
+		}
+
+		return ConversionUtil.toBoolean(o1) ? Boolean.FALSE : Boolean.TRUE;
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
index 62743e3..8f35c99 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
@@ -48,7 +48,7 @@ public class ASTNotBetween extends ConditionNode {
     }
 
     @Override
-    protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
         Object o1 = evaluatedChildren[1];
         Object o2 = evaluatedChildren[2];
         Evaluator e = Evaluator.evaluator(o);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
index 211b83e..0b7ffbe 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
@@ -51,9 +51,9 @@ public class ASTNotEqual extends ConditionNode {
     }
 
     @Override
-    protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
         Object o2 = evaluatedChildren[1];
-        return !ASTEqual.evaluateImpl(o, o2);
+        return ASTEqual.evaluateImpl(o, o2) ? Boolean.FALSE : Boolean.TRUE;
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
index 09cda00..09afa6d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
@@ -49,19 +49,19 @@ public class ASTNotIn extends ConditionNode {
     }
 
     @Override
-    protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
         if (o == null || evaluatedChildren[1] == null) {
-            return false;
+            return Boolean.FALSE;
         }
 
         Object[] objects = (Object[]) evaluatedChildren[1];
         for (Object object : objects) {
             if (object != null && Evaluator.evaluator(o).eq(o, object)) {
-                return false;
+                return Boolean.FALSE;
             }
         }
 
-        return true;
+        return Boolean.TRUE;
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
index ab90bc6..d01970c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
@@ -57,7 +57,7 @@ public class ASTNotLike extends PatternMatchNode {
     }
 
     @Override
-    protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
         String s1 = ConversionUtil.toString(o);
         if (s1 == null) {
             return Boolean.FALSE;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
index b962fc3..7af54eb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
@@ -58,7 +58,7 @@ public class ASTNotLikeIgnoreCase extends IgnoreCaseNode {
 	}
 
 	@Override
-	protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+	protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
 		String s1 = ConversionUtil.toString(o);
 		if (s1 == null) {
 			return Boolean.FALSE;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTOr.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTOr.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTOr.java
index 1f6ef24..e1cfb5d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTOr.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTOr.java
@@ -68,13 +68,20 @@ public class ASTOr extends AggregateConditionNode {
 			return Boolean.FALSE;
 		}
 
+		// https://en.wikipedia.org/wiki/Three-valued_logic
+		boolean unknown = false;
+		boolean result = false;
 		for (int i = 0; i < len; i++) {
-			if (ConversionUtil.toBoolean(evaluateChild(i, o))) {
-				return Boolean.TRUE;
+			Object value = evaluateChild(i, o);
+			if (value == null) {
+				unknown = true;
+			} else if (ConversionUtil.toBoolean(value)) {
+				result = true;
+				break;
 			}
 		}
 
-		return Boolean.FALSE;
+		return result ? Boolean.TRUE : (unknown ? null : Boolean.FALSE);
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrue.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrue.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrue.java
index ae9de66..e305df3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrue.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrue.java
@@ -52,8 +52,8 @@ public class ASTTrue extends ConditionNode {
     }
 
     @Override
-    protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
-        return true;
+    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
+        return Boolean.TRUE;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java
index 7a05940..4759e70 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java
@@ -74,7 +74,7 @@ public abstract class ConditionNode extends SimpleNode {
         }
         if (firstChild instanceof Collection) {
             for(Object c : (Collection)firstChild) {
-                if(evaluateSubNode(c, evaluatedChildren)) {
+                if(evaluateSubNode(c, evaluatedChildren) == Boolean.TRUE) {
                     return Boolean.TRUE;
                 }
             }
@@ -86,5 +86,5 @@ public abstract class ConditionNode extends SimpleNode {
 
     abstract protected int getRequiredChildrenCount();
 
-    abstract protected boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception;
+    abstract protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception;
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/Evaluator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/Evaluator.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/Evaluator.java
index df6785b..8b1b619 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/Evaluator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/Evaluator.java
@@ -47,9 +47,9 @@ abstract class Evaluator {
     private static final Evaluator PERSISTENT_EVALUATOR;
     private static final Evaluator BIG_DECIMAL_EVALUATOR;
     private static final Evaluator NUMBER_EVALUATOR;
-    private static final Evaluator COMPAREABLE_EVALUATOR;
+    private static final Evaluator COMPARABLE_EVALUATOR;
 	
-    static double EPSILON = 0.0000001;
+    private static final double EPSILON = 0.0000001;
 
     /**
      * A decorator of an evaluator that presumes non-null 'lhs' argument and
@@ -64,22 +64,20 @@ abstract class Evaluator {
 
         @Override
         Integer compare(Object lhs, Object rhs) {
-
             if (rhs == null) {
-                return 1;
-            } else {
-                return delegate.compare(lhs, rhs);
+                return null;
             }
+            return delegate.compare(lhs, rhs);
         }
 
         @Override
         boolean eq(Object lhs, Object rhs) {
-            return rhs == null ? false : delegate.eq(lhs, rhs);
+            return rhs != null && delegate.eq(lhs, rhs);
         }
     }
 
     static {
-        evaluators = new ConcurrentHashMap<Class<?>, Evaluator>();
+        evaluators = new ConcurrentHashMap<>();
 
         NULL_LHS_EVALUATOR = new Evaluator() {
             @Override
@@ -159,12 +157,9 @@ abstract class Evaluator {
 
             @Override
             boolean eq(Object lhs, Object rhs) {
-
-                // BigDecimals must be compared using compareTo (
-                // see CAY-280 and BigDecimal.equals JavaDoc)
-
+                // BigDecimals must be compared using compareTo (see CAY-280 and BigDecimal.equals JavaDoc)
                 Integer c = compare(lhs, rhs);
-                return c != null && c == 0;
+                return c == 0;
             }
         });
 
@@ -178,19 +173,14 @@ abstract class Evaluator {
 
             @Override
             boolean eq(Object lhs, Object rhs) {
-                return equalNumbers((Number)lhs, (Number)rhs);
+                return equalNumbers((Number)lhs, rhs);
             }
             
             private final List WHOLE_NUMBER_CLASSES = Arrays.asList(Byte.class, Short.class, Integer.class, AtomicInteger.class, Long.class, AtomicLong.class, BigInteger.class);
-            private final List FLOATING_POINT_CLASSES = Arrays.asList(Float.class, Double.class);
 
             /**
              * Returns the widest primitive wrapper class given the two operands, used in preparation for 
              * comparing two boxed numbers of different types, like java.lang.Short and java.lang.Integer.
-             * 
-             * @param lhs
-             * @param rhs
-             * @return
              */
             Class<?> widestNumberType(Number lhs, Number rhs) {
             	if (lhs.getClass().equals(rhs.getClass())) return lhs.getClass();
@@ -201,15 +191,8 @@ abstract class Evaluator {
             	Class<?> widestClass;
             	if (lhsIndex != -1 && rhsIndex != -1) {
             		widestClass = (Class<?>) WHOLE_NUMBER_CLASSES.get(Math.max(lhsIndex, rhsIndex));
-            		
-            	} else if (lhsIndex == -1 && rhsIndex == -1) {
-            		widestClass = Double.class; // must be one float and one double;
-            		
-            	} else if (lhsIndex != -1 || rhsIndex != -1){ // must be whole number and a float or double
-            		widestClass = Double.class;
-            		
             	} else {
-            		widestClass = null;
+            		widestClass = Double.class;
             	}
             	
             	return widestClass;
@@ -217,9 +200,6 @@ abstract class Evaluator {
             
             /**
              * Enables equality tests for two boxed numbers of different types, like java.lang.Short and java.lang.Integer.
-             * @param lhs
-             * @param _rhs
-             * @return
              */
             boolean equalNumbers(Number lhs, Object _rhs) {
             	if (!Number.class.isAssignableFrom(_rhs.getClass())) {
@@ -252,9 +232,6 @@ abstract class Evaluator {
             
             /**
              * Enables comparison of two boxed numbers of different types, like java.lang.Short and java.lang.Integer.
-             * @param lhs
-             * @param _rhs
-             * @return
              */
             Integer compareNumbers(Number lhs, Object _rhs) {
             	if (!Number.class.isAssignableFrom(_rhs.getClass())) {
@@ -299,7 +276,7 @@ abstract class Evaluator {
             }
         });
         
-        COMPAREABLE_EVALUATOR = new NonNullLhsEvaluator(new Evaluator() {
+        COMPARABLE_EVALUATOR = new NonNullLhsEvaluator(new Evaluator() {
 
             @SuppressWarnings({ "unchecked", "rawtypes" })
             @Override
@@ -314,7 +291,7 @@ abstract class Evaluator {
         });
     }
 
-    static <T> Evaluator evaluator(Object lhs) {
+    static Evaluator evaluator(Object lhs) {
 
         if (lhs == null) {
             return NULL_LHS_EVALUATOR;
@@ -354,7 +331,7 @@ abstract class Evaluator {
         }
         
         if (Comparable.class.isAssignableFrom(lhsType)) {
-            return COMPAREABLE_EVALUATOR;
+            return COMPARABLE_EVALUATOR;
         }
 
         // nothing we recognize... return default

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluateInMemoryTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluateInMemoryTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluateInMemoryTest.java
index 25afcf0..1d9e826 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluateInMemoryTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluateInMemoryTest.java
@@ -18,11 +18,15 @@
  ****************************************************************/
 package org.apache.cayenne.exp.parser;
 
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
 import static org.junit.Assert.assertEquals;
 
 import java.math.BigDecimal;
 
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.testdo.testmap.Artist;
 import org.junit.Test;
 
 // TODO: split it between AST* unit tests (partially done already)
@@ -68,4 +72,63 @@ public class ExpressionEvaluateInMemoryTest {
 		assertEquals(Boolean.FALSE, new ASTFalse().evaluate(null));
 	}
 
+	@Test
+    public void testEvaluateNullCompare() throws Exception {
+        Expression expression = new ASTGreater(new ASTObjPath("artistName"), "A");
+        assertFalse(expression.match(new Artist()));
+        assertFalse(expression.notExp().match(new Artist()));
+    }
+
+    @Test
+    public void testEvaluateCompareNull() throws Exception {
+        Artist a1 = new Artist();
+        a1.setArtistName("Name");
+        Expression expression = new ASTGreater(new ASTObjPath("artistName"), null);
+        assertFalse(expression.match(a1));
+        assertFalse(expression.notExp().match(a1));
+        a1.setSomeOtherObjectProperty(new BigDecimal(1));
+        expression = ExpressionFactory.exp("someOtherObjectProperty > null");
+        assertFalse(expression.match(a1));
+    }
+
+    @Test
+    public void testEvaluateEqualsNull() throws Exception {
+        Artist a1 = new Artist();
+        Expression isNull = Artist.ARTIST_NAME.isNull();
+        assertTrue(isNull.match(a1));
+        assertFalse(isNull.notExp().match(a1));
+    }
+
+    @Test
+    public void testEvaluateNotEqualsNullColumn() throws Exception {
+        Expression notEquals = ExpressionFactory.exp("artistName <> someOtherProperty");
+        assertFalse(notEquals.match(new Artist()));
+        assertTrue(notEquals.notExp().match(new Artist()));
+    }
+
+    @Test
+    public void testNullAnd() {
+        Expression nullExp = ExpressionFactory.exp("null > 0");
+
+        ASTAnd nullAndTrue = new ASTAnd(new Object[] {nullExp, new ASTTrue()});
+        assertFalse(nullAndTrue.match(null));
+        assertFalse(nullAndTrue.notExp().match(null));
+
+        ASTAnd nullAndFalse = new ASTAnd(new Object[] {nullExp, new ASTFalse()});
+        assertFalse(nullAndFalse.match(null));
+        assertTrue(nullAndFalse.notExp().match(null));
+    }
+
+    @Test
+    public void testNullOr() {
+        Expression nullExp = ExpressionFactory.exp("null > 0");
+
+        ASTOr nullOrTrue = new ASTOr(new Object[] {nullExp, new ASTTrue()});
+        assertTrue(nullOrTrue.match(null));
+        assertFalse(nullOrTrue.notExp().match(null));
+
+        ASTOr nullOrFalse = new ASTOr(new Object[] {nullExp, new ASTFalse()});
+        assertFalse(nullOrFalse.match(null));
+        assertFalse(nullOrFalse.notExp().match(null));
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8e43461e/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluationIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluationIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluationIT.java
index e33dba3..d8bc19d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluationIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ExpressionEvaluationIT.java
@@ -36,7 +36,6 @@ import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
@@ -45,8 +44,9 @@ import static org.junit.Assert.assertEquals;
  * Here we compare Expression evaluation in-memory vs execution in database.
  * Results should be same for both cases.
  * Here is primary collection of complex expressions:
- *  - To-Many relationships
- *  - Nulls
+ *  - To-Many relationships comparisons
+ *  - Null comparisons
+ *  - Null in AND and OR expressions
  *
  * @since 4.0
  */
@@ -82,6 +82,7 @@ public class ExpressionEvaluationIT extends ServerCase {
         }
 
         tPaintings.insert(11, "painting11", null, 1, 10000);
+        tPaintings.insert(12, "painting12", 1, 1, null);
     }
 
     @Test
@@ -246,16 +247,116 @@ public class ExpressionEvaluationIT extends ServerCase {
     }
 
     @Test
-    @Ignore("Null comparisons are partially broken for now")
-    public void testNull() throws Exception {
+    public void testCollectionWithNull() {
+        Expression exp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE)
+                .lt(new BigDecimal(200));
+
+        compareSqlAndEval(exp, 1);
+    }
+
+    @Test
+    public void testGreaterWithNull() throws Exception {
+        tPaintings.deleteAll();
+        tArtist.deleteAll();
+        tArtist.insert(7, "artist7", null);
+
+        Expression expression = Artist.DATE_OF_BIRTH
+                .gt(new java.sql.Date(System.currentTimeMillis()));
+
+        compareSqlAndEval(expression, 0);
+
+        Expression expression1 = expression.notExp();
+        compareSqlAndEval(expression1, 0);
+    }
+
+    @Test
+    public void testGreaterEqualWithNull() throws Exception {
+        tPaintings.deleteAll();
+        tArtist.deleteAll();
+        tArtist.insert(7, "artist7", null);
+
+        Expression expression = Artist.DATE_OF_BIRTH
+                .gte(new java.sql.Date(System.currentTimeMillis()));
+
+        compareSqlAndEval(expression, 0);
+
+        Expression expression1 = expression.notExp();
+        compareSqlAndEval(expression1, 0);
+    }
+
+    @Test
+    public void testLessWithNull() throws Exception {
+        tPaintings.deleteAll();
+        tArtist.deleteAll();
+        tArtist.insert(7, "artist7", null);
+
+        Expression expression = Artist.DATE_OF_BIRTH
+                .lt(new java.sql.Date(System.currentTimeMillis()));
+
+        compareSqlAndEval(expression, 0);
+
+        Expression expression1 = expression.notExp();
+        compareSqlAndEval(expression1, 0);
+    }
+
+    @Test
+    public void testLessEqualWithNull() throws Exception {
+        tPaintings.deleteAll();
         tArtist.deleteAll();
         tArtist.insert(7, "artist7", null);
 
-        Expression expression = Artist.DATE_OF_BIRTH.gt(new java.sql.Date(System.currentTimeMillis()));
+        Expression expression = Artist.DATE_OF_BIRTH
+                .lte(new java.sql.Date(System.currentTimeMillis()));
+
         compareSqlAndEval(expression, 0);
 
         Expression expression1 = expression.notExp();
-        compareSqlAndEval(expression1, 0); // this one will fail, in-memory filter will match one Artist
+        compareSqlAndEval(expression1, 0);
+    }
+
+    @Test
+    public void testAndWithNull() throws Exception {
+        tPaintings.deleteAll();
+        tArtist.deleteAll();
+        tArtist.insert(7, "artist7", null);
+        tArtist.insert(8, "artist8", null);
+        tArtist.insert(9, "artist9", null);
+
+        Expression nullExp = Artist.DATE_OF_BIRTH.lt(new java.sql.Date(System.currentTimeMillis()));
+        Expression and = ExpressionFactory.and(nullExp, Artist.ARTIST_NAME.eq("artist7"));
+
+        compareSqlAndEval(and, 0);
+        compareSqlAndEval(and.notExp(), 2);
+    }
+
+    @Test
+    public void testAndWithNull2() throws Exception {
+        tPaintings.deleteAll();
+        tArtist.deleteAll();
+        tArtist.insert(7, "artist7", null);
+        tArtist.insert(8, "artist8", null);
+        tArtist.insert(9, "artist9", null);
+
+        Expression nullExp = Artist.DATE_OF_BIRTH.lt(new java.sql.Date(System.currentTimeMillis()));
+        Expression and = ExpressionFactory.and(nullExp, Artist.ARTIST_NAME.eq("artist10"));
+
+        compareSqlAndEval(and, 0);
+        compareSqlAndEval(and.notExp(), 3);
+    }
+
+    @Test
+    public void testOrWithNull() throws Exception {
+        tPaintings.deleteAll();
+        tArtist.deleteAll();
+        tArtist.insert(7, "artist7", null);
+        tArtist.insert(8, "artist8", null);
+        tArtist.insert(9, "artist9", null);
+
+        Expression nullExp = Artist.DATE_OF_BIRTH.lt(new java.sql.Date(System.currentTimeMillis()));
+        Expression and = ExpressionFactory.or(nullExp, Artist.ARTIST_NAME.eq("artist7"));
+
+        compareSqlAndEval(and, 1);
+        compareSqlAndEval(and.notExp(), 0);
     }
 
     private void compareSqlAndEval(Expression exp, int expectedCount) {