You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2016/09/16 04:48:16 UTC

[2/6] calcite git commit: [CALCITE-1095] NOT precedence

[CALCITE-1095] NOT precedence

Previously prefix operators such as NOT were not handled in the same
way as infix and postfix operators. With a new precedence climbing
parser, we can handle all operators uniformly.

Document precedence of all operators, adjusting some to match the SQL
standard; for example, LIKE now has slightly higher precedence than =,
and NOT has lower precedence than =.

Add a test to ensure that operators' real rankings match the documentation.


Project: http://git-wip-us.apache.org/repos/asf/calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/339ac13e
Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/339ac13e
Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/339ac13e

Branch: refs/heads/master
Commit: 339ac13eb23b5aac6cadc16c42e36deebbcd1e39
Parents: f9724c2
Author: Julian Hyde <jh...@apache.org>
Authored: Thu Feb 18 11:14:52 2016 -0800
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Sep 15 17:02:42 2016 -0700

----------------------------------------------------------------------
 core/src/main/codegen/templates/Parser.jj       |  69 +--
 .../apache/calcite/sql/SqlPrefixOperator.java   |   2 +-
 .../apache/calcite/sql/SqlSpecialOperator.java  |  40 +-
 .../calcite/sql/fun/SqlBetweenOperator.java     |  42 +-
 .../apache/calcite/sql/fun/SqlInOperator.java   |   2 +-
 .../apache/calcite/sql/fun/SqlItemOperator.java |  19 +-
 .../apache/calcite/sql/fun/SqlLikeOperator.java |  22 +-
 .../calcite/sql/fun/SqlStdOperatorTable.java    |  26 +-
 .../calcite/sql/parser/SqlParserUtil.java       | 387 ++++++++--------
 .../calcite/util/PrecedenceClimbingParser.java  | 447 +++++++++++++++++++
 .../main/java/org/apache/calcite/util/Util.java |   2 +-
 .../calcite/sql/parser/SqlParserTest.java       | 104 +++--
 .../apache/calcite/sql/test/SqlTesterImpl.java  |  26 +-
 .../org/apache/calcite/test/CalciteSuite.java   |   2 +
 .../apache/calcite/test/SqlValidatorTest.java   | 181 ++++++++
 .../util/PrecedenceClimbingParserTest.java      | 181 ++++++++
 core/src/test/resources/sql/operator.iq         |  69 +++
 site/_docs/reference.md                         |  18 +
 18 files changed, 1310 insertions(+), 329 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/codegen/templates/Parser.jj
----------------------------------------------------------------------
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 50f762c..43d6cd5 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -2556,13 +2556,30 @@ SqlNode Expression(ExprContext exprContext) :
 
 // TODO jvs 15-Nov-2003:  ANY/ALL
 
+void Expression2b(ExprContext exprContext, List<Object> list) :
+{
+    SqlNode e;
+    SqlOperator op;
+}
+{
+    (
+        op = PrefixRowOperator() {
+            checkNonQueryExpression(exprContext);
+            list.add(new SqlParserUtil.ToTreeListItem(op, getPos()));
+        }
+    )*
+    e = Expression3(exprContext) {
+        list.add(e);
+    }
+}
+
 /**
  * Parses a binary row expression, or a parenthesized expression of any
  * kind.
  *
  * <p>The result is as a flat list of operators and operands. The top-level
  * call to get an expression should call {@link #Expression}, but lower-level
- * calls should call this, to give the parser the opos to associate
+ * calls should call this, to give the parser the opportunity to associate
  * operator calls.
  *
  * <p>For example 'a = b like c = d' should come out '((a = b) like c) = d'
@@ -2572,17 +2589,15 @@ SqlNode Expression(ExprContext exprContext) :
  */
 List<Object> Expression2(ExprContext exprContext) :
 {
-    List<Object> list, list2;
+    final List<Object> list = new ArrayList();
+    List<Object> list2;
     SqlNodeList nodeList;
     SqlNode e;
     SqlOperator op;
-    SqlParserPos pos;
+    SqlParserPos pos = getPos();
 }
 {
-    e = Expression3(exprContext)
-    {
-        list = startList(e);
-    }
+    Expression2b(exprContext, list)
     (
         (
             LOOKAHEAD(2)
@@ -2599,7 +2614,7 @@ List<Object> Expression2(ExprContext exprContext) :
                         op = SqlStdOperatorTable.NOT_IN;
                         pos = getPos();
                     }
-                    |
+                |
                     <IN>
                     {
                         op = SqlStdOperatorTable.IN;
@@ -2622,7 +2637,7 @@ List<Object> Expression2(ExprContext exprContext) :
                         list.add(nodeList);
                     }
                 }
-                |
+            |
                 LOOKAHEAD(2)
                 {
                     checkNonQueryExpression(exprContext);
@@ -2635,10 +2650,10 @@ List<Object> Expression2(ExprContext exprContext) :
                     }
                     [
                         <SYMMETRIC> { op = SqlStdOperatorTable.SYMMETRIC_NOT_BETWEEN; }
-                        |
+                    |
                         <ASYMMETRIC>
                     ]
-                    |
+                |
                     <BETWEEN>
                     {
                         op = SqlStdOperatorTable.BETWEEN;
@@ -2646,7 +2661,7 @@ List<Object> Expression2(ExprContext exprContext) :
                     }
                     [
                         <SYMMETRIC> { op = SqlStdOperatorTable.SYMMETRIC_BETWEEN; }
-                        |
+                    |
                         <ASYMMETRIC>
                     ]
                 )
@@ -2655,7 +2670,7 @@ List<Object> Expression2(ExprContext exprContext) :
                     list.add(new SqlParserUtil.ToTreeListItem(op, pos));
                     list.add(e);
                 }
-                |
+            |
                 {
                     checkNonQueryExpression(exprContext);
                     pos = getPos();
@@ -2664,12 +2679,12 @@ List<Object> Expression2(ExprContext exprContext) :
                     <NOT>
                     (
                         <LIKE> { op = SqlStdOperatorTable.NOT_LIKE; }
-                        |
+                    |
                         <SIMILAR> <TO> { op = SqlStdOperatorTable.NOT_SIMILAR_TO; }
                     )
-                    |
+                |
                     <LIKE> { op = SqlStdOperatorTable.LIKE; }
-                    |
+                |
                     <SIMILAR> <TO> { op = SqlStdOperatorTable.SIMILAR_TO; }
                 )
                 list2 = Expression2(ExprContext.ACCEPT_SUBQUERY)
@@ -2688,17 +2703,14 @@ List<Object> Expression2(ExprContext exprContext) :
                         list.add(e);
                     }
                 ]
-                |
+            |
                 LOOKAHEAD(3) op = BinaryRowOperator()
                 {
                     checkNonQueryExpression(exprContext);
-                }
-                e = Expression3(ExprContext.ACCEPT_SUBQUERY)
-                {
                     list.add(new SqlParserUtil.ToTreeListItem(op, getPos()));
-                    list.add(e);
                 }
-                |
+                Expression2b(ExprContext.ACCEPT_SUBQUERY, list)
+            |
                 <LBRACKET>
                 e = Expression(ExprContext.ACCEPT_SUBQUERY)
                 <RBRACKET>
@@ -2708,7 +2720,7 @@ List<Object> Expression2(ExprContext exprContext) :
                             SqlStdOperatorTable.ITEM, getPos()));
                     list.add(e);
                 }
-                |
+            |
                 {
                     checkNonQueryExpression(exprContext);
                 }
@@ -2766,17 +2778,6 @@ SqlNode Expression3(ExprContext exprContext) :
         return SqlStdOperatorTable.ROW.createCall(list);
     }
     |
-    op = PrefixRowOperator()
-    {
-        prefixRowOpPos = getPos();
-        checkNonQueryExpression(exprContext);
-    }
-    e = Expression3(ExprContext.ACCEPT_SUBQUERY)
-    {
-        SqlParserPos callPos = prefixRowOpPos.plus(e.getParserPosition());
-        return op.createCall(callPos, e);
-    }
-    |
     {
         pos = getPos();
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
index 11eeb26..af44dd2 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlPrefixOperator.java
@@ -42,7 +42,7 @@ public class SqlPrefixOperator extends SqlOperator {
     super(
         name,
         kind,
-        leftPrec(0, true),
+        leftPrec(prec, true),
         rightPrec(prec, true),
         returnTypeInference,
         operandTypeInference,

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
index 85da998..83049ac 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSpecialOperator.java
@@ -16,12 +16,14 @@
  */
 package org.apache.calcite.sql;
 
+import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlOperandTypeChecker;
 import org.apache.calcite.sql.type.SqlOperandTypeInference;
 import org.apache.calcite.sql.type.SqlReturnTypeInference;
+import org.apache.calcite.util.PrecedenceClimbingParser;
 import org.apache.calcite.util.Util;
 
-import java.util.List;
+import com.google.common.base.Predicate;
 
 /**
  * Generic operator for nodes with special syntax.
@@ -81,11 +83,43 @@ public class SqlSpecialOperator extends SqlOperator {
    *     {@link SqlNode}
    * @return ordinal of the node which replaced the expression
    */
-  public int reduceExpr(
+  public ReduceResult reduceExpr(
       int ordinal,
-      List<Object> list) {
+      TokenSequence list) {
     throw Util.needToImplement(this);
   }
+
+  /** List of tokens: the input to a parser. Every token is either an operator
+   * ({@link SqlOperator}) or an expression ({@link SqlNode}), and every token
+   * has a position. */
+  public interface TokenSequence {
+    int size();
+    SqlOperator op(int i);
+    SqlParserPos pos(int i);
+    boolean isOp(int i);
+    SqlNode node(int i);
+    void replaceSublist(int start, int end, SqlNode e);
+
+    /** Creates a parser whose token sequence is a copy of a subset of this
+     * token sequence. */
+    PrecedenceClimbingParser parser(int start,
+        Predicate<PrecedenceClimbingParser.Token> predicate);
+  }
+
+  /** Result of applying
+   * {@link org.apache.calcite.util.PrecedenceClimbingParser.Special#apply}.
+   * Tells the caller which range of tokens to replace, and with what. */
+  public class ReduceResult {
+    public final int startOrdinal;
+    public final int endOrdinal;
+    public final SqlNode node;
+
+    public ReduceResult(int startOrdinal, int endOrdinal, SqlNode node) {
+      this.startOrdinal = startOrdinal;
+      this.endOrdinal = endOrdinal;
+      this.node = node;
+    }
+  }
 }
 
 // End SqlSpecialOperator.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
index 86660a2..bf23c3f 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlBetweenOperator.java
@@ -109,7 +109,7 @@ public class SqlBetweenOperator extends SqlInfixOperator {
   //~ Constructors -----------------------------------------------------------
 
   public SqlBetweenOperator(Flag flag, boolean negated) {
-    super(negated ? NOT_BETWEEN_NAMES : BETWEEN_NAMES, SqlKind.BETWEEN, 30,
+    super(negated ? NOT_BETWEEN_NAMES : BETWEEN_NAMES, SqlKind.BETWEEN, 32,
         null, InferTypes.FIRST_KNOWN, OTC_CUSTOM);
     this.flag = flag;
     this.negated = negated;
@@ -191,12 +191,8 @@ public class SqlBetweenOperator extends SqlInfixOperator {
     writer.endList(frame);
   }
 
-  public int reduceExpr(
-      int opOrdinal,
-      List<Object> list) {
-    final SqlParserUtil.ToTreeListItem betweenNode =
-        (SqlParserUtil.ToTreeListItem) list.get(opOrdinal);
-    SqlOperator op = betweenNode.getOperator();
+  public ReduceResult reduceExpr(int opOrdinal, TokenSequence list) {
+    SqlOperator op = list.op(opOrdinal);
     assert op == this;
 
     // Break the expression up into expressions. For example, a simple
@@ -208,26 +204,18 @@ public class SqlBetweenOperator extends SqlInfixOperator {
     //    |_____|       |_____|   |_____|
     //     exp0          exp1      exp2
     // Create the expression between 'BETWEEN' and 'AND'.
-    final SqlParserPos pos =
-        ((SqlNode) list.get(opOrdinal + 1)).getParserPosition();
     SqlNode exp1 =
         SqlParserUtil.toTreeEx(list, opOrdinal + 1, 0, SqlKind.AND);
     if ((opOrdinal + 2) >= list.size()) {
-      SqlParserPos lastPos =
-          ((SqlNode) list.get(list.size() - 1)).getParserPosition();
+      SqlParserPos lastPos = list.pos(list.size() - 1);
       final int line = lastPos.getEndLineNum();
       final int col = lastPos.getEndColumnNum() + 1;
       SqlParserPos errPos = new SqlParserPos(line, col, line, col);
       throw SqlUtil.newContextException(errPos, RESOURCE.betweenWithoutAnd());
     }
-    final Object o = list.get(opOrdinal + 2);
-    if (!(o instanceof SqlParserUtil.ToTreeListItem)) {
-      SqlParserPos errPos = ((SqlNode) o).getParserPosition();
-      throw SqlUtil.newContextException(errPos, RESOURCE.betweenWithoutAnd());
-    }
-    if (((SqlParserUtil.ToTreeListItem) o).getOperator().getKind()
-        != SqlKind.AND) {
-      SqlParserPos errPos = ((SqlParserUtil.ToTreeListItem) o).getPos();
+    if (!list.isOp(opOrdinal + 2)
+        || list.op(opOrdinal + 2).getKind() != SqlKind.AND) {
+      SqlParserPos errPos = list.pos(opOrdinal + 2);
       throw SqlUtil.newContextException(errPos, RESOURCE.betweenWithoutAnd());
     }
 
@@ -240,30 +228,22 @@ public class SqlBetweenOperator extends SqlInfixOperator {
     //   (a BETWEEN b AND c + d) OR e
     // because OR has lower precedence than BETWEEN.
     SqlNode exp2 =
-        SqlParserUtil.toTreeEx(
-            list,
+        SqlParserUtil.toTreeEx(list,
             opOrdinal + 3,
             getRightPrec(),
             SqlKind.OTHER);
 
     // Create the call.
-    SqlNode exp0 = (SqlNode) list.get(opOrdinal - 1);
+    SqlNode exp0 = list.node(opOrdinal - 1);
     SqlCall newExp =
         createCall(
-            betweenNode.getPos(),
+            list.pos(opOrdinal),
             exp0,
             exp1,
             exp2);
 
     // Replace all of the matched nodes with the single reduced node.
-    SqlParserUtil.replaceSublist(
-        list,
-        opOrdinal - 1,
-        opOrdinal + 4,
-        newExp);
-
-    // Return the ordinal of the new current node.
-    return opOrdinal - 1;
+    return new ReduceResult(opOrdinal - 1, opOrdinal + 4, newExp);
   }
 
   //~ Inner Classes ----------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
index a20cea3..716902b 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlInOperator.java
@@ -66,7 +66,7 @@ public class SqlInOperator extends SqlBinaryOperator {
     super(
         isNotIn ? "NOT IN" : "IN",
         SqlKind.IN,
-        30,
+        32,
         true,
         ReturnTypes.BOOLEAN_NULLABLE,
         InferTypes.FIRST_KNOWN,

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
index 6b07a35..7cd0e1e 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlItemOperator.java
@@ -26,15 +26,12 @@ import org.apache.calcite.sql.SqlOperandCountRange;
 import org.apache.calcite.sql.SqlOperatorBinding;
 import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.SqlWriter;
-import org.apache.calcite.sql.parser.SqlParserUtil;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.SqlOperandCountRanges;
 import org.apache.calcite.sql.type.SqlSingleOperandTypeChecker;
 import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
 
-import java.util.List;
-
 /**
  * The item operator {@code [ ... ]}, used to access a given element of an
  * array or map. For example, {@code myArray[3]} or {@code "myMap['foo']"}.
@@ -51,22 +48,18 @@ class SqlItemOperator extends SqlSpecialOperator {
     super("ITEM", SqlKind.OTHER_FUNCTION, 100, true, null, null, null);
   }
 
-  @Override public int reduceExpr(int ordinal, List<Object> list) {
-    SqlNode left = (SqlNode) list.get(ordinal - 1);
-    SqlNode right = (SqlNode) list.get(ordinal + 1);
-    final SqlParserUtil.ToTreeListItem treeListItem =
-        (SqlParserUtil.ToTreeListItem) list.get(ordinal);
-    SqlParserUtil.replaceSublist(
-        list,
-        ordinal - 1,
+  @Override public ReduceResult reduceExpr(int ordinal,
+      TokenSequence list) {
+    SqlNode left = list.node(ordinal - 1);
+    SqlNode right = list.node(ordinal + 1);
+    return new ReduceResult(ordinal - 1,
         ordinal + 2,
         createCall(
             left.getParserPosition()
                 .plus(right.getParserPosition())
-                .plus(treeListItem.getPos()),
+                .plus(list.pos(ordinal)),
             left,
             right));
-    return ordinal - 1;
   }
 
   @Override public void unparse(

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
index 1de1407..9a0187c 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLikeOperator.java
@@ -33,8 +33,6 @@ import org.apache.calcite.sql.type.SqlOperandCountRanges;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.util.Util;
 
-import java.util.List;
-
 /**
  * An operator describing the <code>LIKE</code> and <code>SIMILAR</code>
  * operators.
@@ -76,7 +74,7 @@ public class SqlLikeOperator extends SqlSpecialOperator {
     super(
         name,
         kind,
-        30,
+        32,
         false,
         ReturnTypes.BOOLEAN_NULLABLE,
         InferTypes.FIRST_KNOWN,
@@ -148,16 +146,15 @@ public class SqlLikeOperator extends SqlSpecialOperator {
     writer.endList(frame);
   }
 
-  public int reduceExpr(
+  public ReduceResult reduceExpr(
       final int opOrdinal,
-      List<Object> list) {
+      TokenSequence list) {
     // Example:
     //   a LIKE b || c ESCAPE d || e AND f
     // |  |    |      |      |      |
     //  exp0    exp1          exp2
-    SqlNode exp0 = (SqlNode) list.get(opOrdinal - 1);
-    SqlOperator op =
-        ((SqlParserUtil.ToTreeListItem) list.get(opOrdinal)).getOperator();
+    SqlNode exp0 = list.node(opOrdinal - 1);
+    SqlOperator op = list.op(opOrdinal);
     assert op instanceof SqlLikeOperator;
     SqlNode exp1 =
         SqlParserUtil.toTreeEx(
@@ -167,10 +164,8 @@ public class SqlLikeOperator extends SqlSpecialOperator {
             SqlKind.ESCAPE);
     SqlNode exp2 = null;
     if ((opOrdinal + 2) < list.size()) {
-      final Object o = list.get(opOrdinal + 2);
-      if (o instanceof SqlParserUtil.ToTreeListItem) {
-        final SqlOperator op2 =
-            ((SqlParserUtil.ToTreeListItem) o).getOperator();
+      if (list.isOp(opOrdinal + 2)) {
+        final SqlOperator op2 = list.op(opOrdinal + 2);
         if (op2.getKind() == SqlKind.ESCAPE) {
           exp2 =
               SqlParserUtil.toTreeEx(
@@ -191,8 +186,7 @@ public class SqlLikeOperator extends SqlSpecialOperator {
       end = opOrdinal + 2;
     }
     SqlCall call = createCall(SqlParserPos.ZERO, operands);
-    SqlParserUtil.replaceSublist(list, opOrdinal - 1, end, call);
-    return opOrdinal - 1;
+    return new ReduceResult(opOrdinal - 1, end, call);
   }
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index e94748f..d0e60d2 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -142,7 +142,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlBinaryOperator(
           "AND",
           SqlKind.AND,
-          28,
+          24,
           true,
           ReturnTypes.BOOLEAN_NULLABLE_OPTIMIZED,
           InferTypes.BOOLEAN,
@@ -431,7 +431,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlBinaryOperator(
           "OR",
           SqlKind.OR,
-          26,
+          22,
           true,
           ReturnTypes.BOOLEAN_NULLABLE_OPTIMIZED,
           InferTypes.BOOLEAN,
@@ -553,7 +553,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT NULL",
           SqlKind.IS_NOT_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.VARCHAR_1024,
           OperandTypes.ANY);
@@ -562,7 +562,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NULL",
           SqlKind.IS_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.VARCHAR_1024,
           OperandTypes.ANY);
@@ -571,7 +571,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT TRUE",
           SqlKind.IS_NOT_TRUE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -580,7 +580,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS TRUE",
           SqlKind.IS_TRUE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -589,7 +589,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT FALSE",
           SqlKind.IS_NOT_FALSE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -598,7 +598,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS FALSE",
           SqlKind.IS_FALSE,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -607,7 +607,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS NOT UNKNOWN",
           SqlKind.IS_NOT_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -616,7 +616,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS UNKNOWN",
           SqlKind.IS_NULL,
-          30,
+          28,
           ReturnTypes.BOOLEAN_NOT_NULL,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -625,7 +625,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPostfixOperator(
           "IS A SET",
           SqlKind.OTHER,
-          30,
+          28,
           ReturnTypes.BOOLEAN,
           null,
           OperandTypes.MULTISET);
@@ -657,7 +657,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
       new SqlPrefixOperator(
           "NOT",
           SqlKind.NOT,
-          30,
+          26,
           ReturnTypes.ARG0,
           InferTypes.BOOLEAN,
           OperandTypes.BOOLEAN);
@@ -1054,7 +1054,7 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
    * SIMILAR TO expression.
    */
   public static final SqlSpecialOperator ESCAPE =
-      new SqlSpecialOperator("Escape", SqlKind.ESCAPE, 30);
+      new SqlSpecialOperator("ESCAPE", SqlKind.ESCAPE, 0);
 
   public static final SqlCaseOperator CASE = SqlCaseOperator.INSTANCE;
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index 03b0ac3..3c605a5 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -20,25 +20,32 @@ import org.apache.calcite.avatica.util.Casing;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.sql.SqlBinaryOperator;
-import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlIntervalLiteral;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlNumericLiteral;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlPostfixOperator;
+import org.apache.calcite.sql.SqlPrefixOperator;
 import org.apache.calcite.sql.SqlSpecialOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.util.PrecedenceClimbingParser;
 import org.apache.calcite.util.SaffronProperties;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.trace.CalciteTrace;
 
+import com.google.common.base.Predicate;
+
 import org.slf4j.Logger;
 
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.charset.Charset;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.StringTokenizer;
@@ -99,6 +106,7 @@ public final class SqlParserUtil {
   /**
    * @deprecated this method is not localized for Farrago standards
    */
+  @Deprecated // to be removed before 2.0
   public static java.sql.Date parseDate(String s) {
     return java.sql.Date.valueOf(s);
   }
@@ -106,6 +114,7 @@ public final class SqlParserUtil {
   /**
    * @deprecated Does not parse SQL:99 milliseconds
    */
+  @Deprecated // to be removed before 2.0
   public static java.sql.Time parseTime(String s) {
     return java.sql.Time.valueOf(s);
   }
@@ -113,6 +122,7 @@ public final class SqlParserUtil {
   /**
    * @deprecated this method is not localized for Farrago standards
    */
+  @Deprecated // to be removed before 2.0
   public static java.sql.Timestamp parseTimestamp(String s) {
     return java.sql.Timestamp.valueOf(s);
   }
@@ -227,6 +237,7 @@ public final class SqlParserUtil {
    * Parses a Binary string. SQL:99 defines a binary string as a hexstring
    * with EVEN nbr of hex digits.
    */
+  @Deprecated // to be removed before 2.0
   public static byte[] parseBinaryString(String s) {
     s = s.replaceAll(" ", "");
     s = s.replaceAll("\n", "");
@@ -491,6 +502,7 @@ public final class SqlParserUtil {
     return new ParsedCollation(charset, locale, strength);
   }
 
+  @Deprecated // to be removed before 2.0
   public static String[] toStringArray(List<String> list) {
     return list.toArray(new String[list.size()]);
   }
@@ -503,6 +515,7 @@ public final class SqlParserUtil {
     return list.toArray();
   }
 
+  @Deprecated // to be removed before 2.0
   public static String rightTrim(
       String s,
       char c) {
@@ -541,8 +554,14 @@ public final class SqlParserUtil {
    * taking operator precedence and associativity into account.
    */
   public static SqlNode toTree(List<Object> list) {
+    if (list.size() == 1
+        && list.get(0) instanceof SqlNode) {
+      // Short-cut for the simple common case
+      return (SqlNode) list.get(0);
+    }
     LOGGER.trace("Attempting to reduce {}", list);
-    final SqlNode node = toTreeEx(list, 0, 0, SqlKind.OTHER);
+    final OldTokenSequenceImpl tokenSequence = new OldTokenSequenceImpl(list);
+    final SqlNode node = toTreeEx(tokenSequence, 0, 0, SqlKind.OTHER);
     LOGGER.debug("Reduced {}", node);
     return node;
   }
@@ -563,182 +582,58 @@ public final class SqlParserUtil {
    *                    we encounter a token of this kind.
    * @return the root node of the tree which the list condenses into
    */
-  public static SqlNode toTreeEx(
-      List<Object> list,
-      int start,
-      int minPrec,
-      SqlKind stopperKind) {
-// Make several passes over the list, and each pass, coalesce the
-// expressions with the highest precedence.
-  outer:
-    while (true) {
-      final int count = list.size();
-      if (count <= (start + 1)) {
-        break;
-      }
-      int i = start + 1;
-      while (i < count) {
-        SqlOperator previous;
-        SqlOperator current = ((ToTreeListItem) list.get(i)).op;
-        SqlParserPos currentPos = ((ToTreeListItem) list.get(i)).pos;
-        if ((stopperKind != SqlKind.OTHER)
-            && (current.getKind() == stopperKind)) {
-          break outer;
-        }
-        SqlOperator next;
-        int previousRight;
-        int left = current.getLeftPrec();
-        int right = current.getRightPrec();
-        if (left < minPrec) {
-          break outer;
-        }
-        int nextLeft;
-        if (current instanceof SqlBinaryOperator) {
-          if (i == (start + 1)) {
-            previous = null;
-            previousRight = 0;
-          } else {
-            previous = ((ToTreeListItem) list.get(i - 2)).op;
-            previousRight = previous.getRightPrec();
-          }
-          if (i == (count - 2)) {
-            next = null;
-            nextLeft = 0;
-          } else {
-            next = ((ToTreeListItem) list.get(i + 2)).op;
-            nextLeft = next.getLeftPrec();
-            if ((next.getKind() == stopperKind)
-                && (stopperKind != SqlKind.OTHER)) {
-              // Suppose we're looking at 'AND' in
-              //    a BETWEEN b OR c AND d
-              //
-              // Because 'AND' is our stopper token, we still
-              // want to reduce 'b OR c', even though 'AND' has
-              // higher precedence than 'OR'.
-              nextLeft = 0;
+  public static SqlNode toTreeEx(SqlSpecialOperator.TokenSequence list,
+      int start, final int minPrec, final SqlKind stopperKind) {
+    final Predicate<PrecedenceClimbingParser.Token> predicate =
+        new Predicate<PrecedenceClimbingParser.Token>() {
+          public boolean apply(PrecedenceClimbingParser.Token t) {
+            if (t instanceof PrecedenceClimbingParser.Op) {
+              final SqlOperator op = ((ToTreeListItem) t.o).op;
+              return stopperKind != SqlKind.OTHER
+                  && op.kind == stopperKind
+                  || minPrec > 0
+                  && op.getLeftPrec() < minPrec;
+            } else {
+              return false;
             }
           }
-          if ((previousRight < left) && (right >= nextLeft)) {
-            // For example,
-            //    i:  0 1 2 3 4 5 6 7 8
-            // list:  a + b * c * d + e
-            // prec: 0 1 2 3 4 3 4 1 2 0
-            //
-            // At i == 3, we have the first '*' operator, and its
-            // surrounding precedences obey the relation 2 < 3 and
-            // 4 >= 3, so we can reduce (b * c) to a single node.
-            SqlNode leftExp = (SqlNode) list.get(i - 1);
-
-            // For example,
-            //    i:  0 1 2 3 4 5 6 7 8
-            // list:  a + b * c * d + e
-            // prec: 0 1 2 3 4 3 4 1 2 0
-            //
-            // At i == 3, we have the first '*' operator, and its
-            // surrounding precedences obey the relation 2 < 3 and
-            // 4 >= 3, so we can reduce (b * c) to a single node.
-            SqlNode rightExp = (SqlNode) list.get(i + 1);
-            SqlParserPos callPos =
-                currentPos.plusAll(
-                    new SqlNode[]{leftExp, rightExp});
-            final SqlCall newExp =
-                current.createCall(callPos, leftExp, rightExp);
-            LOGGER.debug("Reduced infix: {}", newExp);
-
-            // Replace elements {i - 1, i, i + 1} with the new
-            // expression.
-            replaceSublist(list, i - 1, i + 2, newExp);
-            break;
-          }
-          i += 2;
-        } else if (current instanceof SqlPostfixOperator) {
-          if (i == (start + 1)) {
-            previous = null;
-            previousRight = 0;
-          } else {
-            previous = ((ToTreeListItem) list.get(i - 2)).op;
-            previousRight = previous.getRightPrec();
-          }
-          if (previousRight < left) {
-            // For example,
-            //    i:  0 1 2 3 4 5 6 7 8
-            // list:  a + b * c ! + d
-            // prec: 0 1 2 3 4 3 0 2
-            //
-            // At i == 3, we have the postfix '!' operator. Its
-            // high precedence determines that it binds with 'b *
-            // c'. The precedence of the following '+' operator is
-            // irrelevant.
-            SqlNode leftExp = (SqlNode) list.get(i - 1);
-
-            SqlParserPos callPos =
-                currentPos.plusAll(new SqlNode[]{leftExp});
-            final SqlCall newExp =
-                current.createCall(callPos, leftExp);
-            LOGGER.debug("Reduced postfix: {}", newExp);
-
-            // Replace elements {i - 1, i} with the new expression.
-            list.remove(i);
-            list.set(i - 1, newExp);
-            break;
-          }
-          ++i;
-
-          //
-        } else if (current instanceof SqlSpecialOperator) {
-          SqlSpecialOperator specOp = (SqlSpecialOperator) current;
-
-          // We decide to reduce a special operator only on the basis
-          // of what's to the left of it. The operator then decides
-          // how far to the right to chew off.
-          if (i == (start + 1)) {
-            previous = null;
-            previousRight = 0;
-          } else {
-            previous = ((ToTreeListItem) list.get(i - 2)).op;
-            previousRight = previous.getRightPrec();
-          }
-          int nextOrdinal = i + 2;
-          if (i == (count - 2)) {
-            next = null;
-            nextLeft = 0;
-          } else {
-            // find next op
-            next = null;
-            nextLeft = 0;
-            for (; nextOrdinal < count; nextOrdinal++) {
-              Object listItem = list.get(nextOrdinal);
-              if (listItem instanceof ToTreeListItem) {
-                next = ((ToTreeListItem) listItem).op;
-                nextLeft = next.getLeftPrec();
-                if ((stopperKind != SqlKind.OTHER)
-                    && (next.getKind() == stopperKind)) {
-                  break outer;
-                } else {
-                  break;
-                }
-              }
-            }
-          }
-          if (nextLeft < minPrec) {
-            break outer;
-          }
-          if ((previousRight < left) && (right >= nextLeft)) {
-            i = specOp.reduceExpr(i, list);
-            LOGGER.debug("Reduced special op: {}", list.get(i));
-            break;
-          }
-          i = nextOrdinal;
-        } else {
-          throw Util.newInternal("Unexpected operator type: " + current);
-        }
-      }
+        };
+    PrecedenceClimbingParser parser = list.parser(start, predicate);
+    final int beforeSize = parser.all().size();
+    parser.partialParse();
+    final int afterSize = parser.all().size();
+    final SqlNode node = convert(parser.all().get(0));
+    list.replaceSublist(start, start + beforeSize - afterSize + 1, node);
+    return node;
+  }
 
-      // Require the list shrinks each time around -- otherwise we will
-      // never terminate.
-      assert list.size() < count;
+  private static SqlNode convert(PrecedenceClimbingParser.Token token) {
+    switch (token.type) {
+    case ATOM:
+      return (SqlNode) token.o;
+    case CALL:
+      final PrecedenceClimbingParser.Call call =
+          (PrecedenceClimbingParser.Call) token;
+      final List<SqlNode> list = new ArrayList<>();
+      for (PrecedenceClimbingParser.Token arg : call.args) {
+        list.add(convert(arg));
+      }
+      final ToTreeListItem item = (ToTreeListItem) call.op.o;
+      if (item.op == SqlStdOperatorTable.UNARY_MINUS
+          && list.size() == 1
+          && list.get(0) instanceof SqlNumericLiteral) {
+        return SqlLiteral.createNegative((SqlNumericLiteral) list.get(0),
+            item.pos.plusAll(list));
+      }
+      if (item.op == SqlStdOperatorTable.UNARY_PLUS
+          && list.size() == 1
+          && list.get(0) instanceof SqlNumericLiteral) {
+        return list.get(0);
+      }
+      return item.op.createCall(item.pos.plusAll(list), list);
+    default:
+      throw new AssertionError(token);
     }
-    return (SqlNode) list.get(start);
   }
 
   /**
@@ -810,6 +705,10 @@ public final class SqlParserUtil {
       this.pos = pos;
     }
 
+    public String toString() {
+      return op.toString();
+    }
+
     public SqlOperator getOperator() {
       return op;
     }
@@ -834,6 +733,146 @@ public final class SqlParserUtil {
       this.pos = pos;
     }
   }
+
+  /** Implementation of {@link SqlSpecialOperator.TokenSequence} based on an
+   * existing parser. */
+  private static class TokenSequenceImpl
+      implements SqlSpecialOperator.TokenSequence {
+    final List<PrecedenceClimbingParser.Token> list;
+    final PrecedenceClimbingParser parser;
+
+    private TokenSequenceImpl(PrecedenceClimbingParser parser) {
+      this.parser = parser;
+      this.list = parser.all();
+    }
+
+    public PrecedenceClimbingParser parser(int start, Predicate
+        <PrecedenceClimbingParser.Token> predicate) {
+      return parser.copy(start, predicate);
+    }
+
+    public int size() {
+      return list.size();
+    }
+
+    public SqlOperator op(int i) {
+      return ((ToTreeListItem) list.get(i).o).getOperator();
+    }
+
+    private static SqlParserPos pos(PrecedenceClimbingParser.Token token) {
+      switch (token.type) {
+      case ATOM:
+        return ((SqlNode) token.o).getParserPosition();
+      case CALL:
+        final PrecedenceClimbingParser.Call call =
+            (PrecedenceClimbingParser.Call) token;
+        SqlParserPos pos = ((ToTreeListItem) call.op.o).pos;
+        for (PrecedenceClimbingParser.Token arg : call.args) {
+          pos = pos.plus(pos(arg));
+        }
+        return pos;
+      default:
+        return ((ToTreeListItem) token.o).getPos();
+      }
+    }
+
+    public SqlParserPos pos(int i) {
+      return pos(list.get(i));
+    }
+
+    public boolean isOp(int i) {
+      return list.get(i).o instanceof ToTreeListItem;
+    }
+
+    public SqlNode node(int i) {
+      return convert(list.get(i));
+    }
+
+    public void replaceSublist(int start, int end, SqlNode e) {
+      SqlParserUtil.replaceSublist(list, start, end, parser.atom(e));
+    }
+  }
+
+  /** Implementation of {@link SqlSpecialOperator.TokenSequence}. */
+  private static class OldTokenSequenceImpl
+      implements SqlSpecialOperator.TokenSequence {
+    final List<Object> list;
+
+    private OldTokenSequenceImpl(List<Object> list) {
+      this.list = list;
+    }
+
+    @Override public PrecedenceClimbingParser parser(int start,
+        Predicate<PrecedenceClimbingParser.Token> predicate) {
+      final PrecedenceClimbingParser.Builder builder =
+          new PrecedenceClimbingParser.Builder();
+      for (Object o : Util.skip(list, start)) {
+        if (o instanceof ToTreeListItem) {
+          final ToTreeListItem item = (ToTreeListItem) o;
+          final SqlOperator op = item.getOperator();
+          if (op instanceof SqlPrefixOperator) {
+            builder.prefix(item, op.getLeftPrec());
+          } else if (op instanceof SqlPostfixOperator) {
+            builder.postfix(item, op.getRightPrec());
+          } else if (op instanceof SqlBinaryOperator) {
+            builder.infix(item, op.getLeftPrec(),
+                op.getLeftPrec() < op.getRightPrec());
+          } else if (op instanceof SqlSpecialOperator) {
+            builder.special(item, op.getLeftPrec(), op.getRightPrec(),
+                new PrecedenceClimbingParser.Special() {
+                  public PrecedenceClimbingParser.Result apply(
+                      PrecedenceClimbingParser parser,
+                      PrecedenceClimbingParser.SpecialOp op) {
+                    final List<PrecedenceClimbingParser.Token> tokens =
+                        parser.all();
+                    final SqlSpecialOperator op1 =
+                        (SqlSpecialOperator) ((ToTreeListItem) op.o).op;
+                    SqlSpecialOperator.ReduceResult r =
+                        op1.reduceExpr(tokens.indexOf(op),
+                            new TokenSequenceImpl(parser));
+                    return new PrecedenceClimbingParser.Result(
+                        tokens.get(r.startOrdinal),
+                        tokens.get(r.endOrdinal - 1),
+                        parser.atom(r.node));
+                  }
+                });
+          } else {
+            throw new AssertionError();
+          }
+        } else {
+          builder.atom(o);
+        }
+      }
+      return builder.build();
+    }
+
+    public int size() {
+      return list.size();
+    }
+
+    public SqlOperator op(int i) {
+      return ((ToTreeListItem) list.get(i)).op;
+    }
+
+    public SqlParserPos pos(int i) {
+      final Object o = list.get(i);
+      return o instanceof ToTreeListItem
+          ? ((ToTreeListItem) o).pos
+          : ((SqlNode) o).getParserPosition();
+    }
+
+    public boolean isOp(int i) {
+      return list.get(i) instanceof ToTreeListItem;
+    }
+
+    public SqlNode node(int i) {
+      return (SqlNode) list.get(i);
+    }
+
+    public void replaceSublist(int start, int end, SqlNode e) {
+      SqlParserUtil.replaceSublist(list, start, end, e);
+    }
+  }
 }
 
 // End SqlParserUtil.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java b/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java
new file mode 100644
index 0000000..68b8be5
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/util/PrecedenceClimbingParser.java
@@ -0,0 +1,447 @@
+/*
+ * 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.calcite.util;
+
+import org.apache.calcite.linq4j.Ord;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parser that takes a collection of tokens (atoms and operators)
+ * and groups them together according to the operators' precedence
+ * and associativity.
+ */
+public class PrecedenceClimbingParser {
+  private Token first;
+  private Token last;
+
+  private PrecedenceClimbingParser(List<Token> tokens) {
+    Token p = null;
+    for (Token token : tokens) {
+      if (p != null) {
+        p.next = token;
+      } else {
+        first = token;
+      }
+      token.previous = p;
+      token.next = null;
+      p = token;
+    }
+    last = p;
+  }
+
+  public Token atom(Object o) {
+    return new Token(Type.ATOM, o, -1, -1);
+  }
+
+  public Call call(Op op, ImmutableList<Token> args) {
+    return new Call(op, args);
+  }
+
+  public Op infix(Object o, int precedence, boolean left) {
+    return new Op(Type.INFIX, o, precedence * 2 + (left ? 0 : 1),
+        precedence * 2 + (left ? 1 : 0));
+  }
+
+  public Op prefix(Object o, int precedence) {
+    return new Op(Type.PREFIX, o, -1, precedence * 2);
+  }
+
+  public Op postfix(Object o, int precedence) {
+    return new Op(Type.POSTFIX, o, precedence * 2, -1);
+  }
+
+  public SpecialOp special(Object o, int leftPrec, int rightPrec,
+      Special special) {
+    return new SpecialOp(o, leftPrec * 2, rightPrec * 2, special);
+  }
+
+  public Token parse() {
+    partialParse();
+    if (first != last) {
+      throw new AssertionError("could not find next operator to reduce: "
+          + this);
+    }
+    return first;
+  }
+
+  public void partialParse() {
+    for (;;) {
+      Op op = highest();
+      if (op == null) {
+        return;
+      }
+      final Token t;
+      switch (op.type) {
+      case POSTFIX:
+        t = call(op, ImmutableList.of(op.previous));
+        replace(t, op.previous.previous, op.next);
+        break;
+      case PREFIX:
+        t = call(op, ImmutableList.of(op.next));
+        replace(t, op.previous, op.next.next);
+        break;
+      case INFIX:
+        t = call(op, ImmutableList.of(op.previous, op.next));
+        replace(t, op.previous.previous, op.next.next);
+        break;
+      case SPECIAL:
+        Result r = ((SpecialOp) op).special.apply(this, (SpecialOp) op);
+        Preconditions.checkNotNull(r);
+        replace(r.replacement, r.first.previous, r.last.next);
+        break;
+      default:
+        throw new AssertionError();
+      }
+      // debug: System.out.println(this);
+    }
+  }
+
+  @Override public String toString() {
+    return Util.commaList(all());
+  }
+
+  /** Returns a list of all tokens. */
+  public List<Token> all() {
+    return new TokenList();
+  }
+
+  private void replace(Token t, Token previous, Token next) {
+    t.previous = previous;
+    t.next = next;
+    if (previous == null) {
+      first = t;
+    } else {
+      previous.next = t;
+    }
+    if (next == null) {
+      last = t;
+    } else {
+      next.previous = t;
+    }
+  }
+
+  private Op highest() {
+    int p = -1;
+    Op highest = null;
+    for (Token t = first; t != null; t = t.next) {
+      if ((t.left > p || t.right > p)
+          && (t.left < 0 || t.left >= prevRight(t.previous))
+          && (t.right < 0 || t.right >= nextLeft(t.next))) {
+        p = Math.max(t.left, t.right);
+        highest = (Op) t;
+      }
+    }
+    return highest;
+  }
+
+  /** Returns the right precedence of the preceding operator token. */
+  private int prevRight(Token token) {
+    for (; token != null; token = token.previous) {
+      if (token.type == Type.POSTFIX) {
+        return Integer.MAX_VALUE;
+      }
+      if (token.right >= 0) {
+        return token.right;
+      }
+    }
+    return -1;
+  }
+
+  /** Returns the left precedence of the following operator token. */
+  private int nextLeft(Token token) {
+    for (; token != null; token = token.next) {
+      if (token.type == Type.PREFIX) {
+        return Integer.MAX_VALUE;
+      }
+      if (token.left >= 0) {
+        return token.left;
+      }
+    }
+    return -1;
+  }
+
+  public String print(Token token) {
+    return token.toString();
+  }
+
+  public PrecedenceClimbingParser copy(int start, Predicate<Token> predicate) {
+    final List<Token> tokens = new ArrayList<>();
+    for (Token token : Util.skip(all(), start)) {
+      if (predicate.apply(token)) {
+        break;
+      }
+      tokens.add(token.copy());
+    }
+    return new PrecedenceClimbingParser(tokens);
+  }
+
+  /** Token type. */
+  public enum Type {
+    ATOM,
+    CALL,
+    PREFIX,
+    INFIX,
+    POSTFIX,
+    SPECIAL
+  }
+
+  /** A token: either an atom, a call to an operator with arguments,
+   * or an unmatched operator. */
+  public static class Token {
+    Token previous;
+    Token next;
+    public final Type type;
+    public final Object o;
+    final int left;
+    final int right;
+
+    Token(Type type, Object o, int left, int right) {
+      this.type = type;
+      this.o = o;
+      this.left = left;
+      this.right = right;
+    }
+
+    @Override public String toString() {
+      return o.toString();
+    }
+
+    protected StringBuilder print(StringBuilder b) {
+      return b.append(o);
+    }
+
+    public Token copy() {
+      return new Token(type, o, left, right);
+    }
+  }
+
+  /** An operator token. */
+  public static class Op extends Token {
+    Op(Type type, Object o, int left, int right) {
+      super(type, o, left, right);
+    }
+
+    @Override public Token copy() {
+      return new Op(type, o, left, right);
+    }
+  }
+
+  /** An token corresponding to a special operator. */
+  public static class SpecialOp extends Op {
+    public final Special special;
+
+    SpecialOp(Object o, int left, int right, Special special) {
+      super(Type.SPECIAL, o, left, right);
+      this.special = special;
+    }
+
+    @Override public Token copy() {
+      return new SpecialOp(o, left, right, special);
+    }
+  }
+
+
+  /** A token that is a call to an operator with arguments. */
+  public static class Call extends Token {
+    public final Op op;
+    public final ImmutableList<Token> args;
+
+    Call(Op op, ImmutableList<Token> args) {
+      super(Type.CALL, null, -1, -1);
+      this.op = op;
+      this.args = args;
+    }
+
+    @Override public Token copy() {
+      return new Call(op, args);
+    }
+
+    @Override public String toString() {
+      return print(new StringBuilder()).toString();
+    }
+
+    protected StringBuilder print(StringBuilder b) {
+      switch (op.type) {
+      case PREFIX:
+        b.append('(');
+        printOp(b, false, true);
+        args.get(0).print(b);
+        return b.append(')');
+      case POSTFIX:
+        b.append('(');
+        args.get(0).print(b);
+        return printOp(b, true, false).append(')');
+      case INFIX:
+        b.append('(');
+        args.get(0).print(b);
+        printOp(b, true, true);
+        args.get(1).print(b);
+        return b.append(')');
+      case SPECIAL:
+        printOp(b, false, false)
+            .append('(');
+        for (Ord<Token> arg : Ord.zip(args)) {
+          if (arg.i > 0) {
+            b.append(", ");
+          }
+          arg.e.print(b);
+        }
+        return b.append(')');
+      default:
+        throw new AssertionError();
+      }
+    }
+
+    private StringBuilder printOp(StringBuilder b, boolean leftSpace,
+        boolean rightSpace) {
+      String s = op.o.toString();
+      if (leftSpace) {
+        b.append(' ');
+      }
+      b.append(s);
+      if (rightSpace) {
+        b.append(' ');
+      }
+      return b;
+    }
+  }
+
+  /** Callback defining the behavior of a special function. */
+  public interface Special {
+    /** Given an occurrence of this operator, identifies the range of tokens to
+     * be collapsed into a call of this operator, and the arguments to that
+     * call. */
+    Result apply(PrecedenceClimbingParser parser, SpecialOp op);
+  }
+
+  /** Result of a call to {@link Special#apply}. */
+  public static class Result {
+    final Token first;
+    final Token last;
+    final Token replacement;
+
+    public Result(Token first, Token last, Token replacement) {
+      this.first = first;
+      this.last = last;
+      this.replacement = replacement;
+    }
+  }
+
+  /** Fluent helper to build a parser containing a list of tokens. */
+  public static class Builder {
+    final List<Token> tokens = new ArrayList<>();
+    private final PrecedenceClimbingParser dummy =
+        new PrecedenceClimbingParser(ImmutableList.<Token>of());
+
+    private Builder add(Token t) {
+      tokens.add(t);
+      return this;
+    }
+
+    public Builder atom(Object o) {
+      return add(dummy.atom(o));
+    }
+
+    public Builder call(Op op, Token arg0, Token arg1) {
+      return add(dummy.call(op, ImmutableList.of(arg0, arg1)));
+    }
+
+    public Builder infix(Object o, int precedence, boolean left) {
+      return add(dummy.infix(o, precedence, left));
+    }
+
+    public Builder prefix(Object o, int precedence) {
+      return add(dummy.prefix(o, precedence));
+    }
+
+    public Builder postfix(Object o, int precedence) {
+      return add(dummy.postfix(o, precedence));
+    }
+
+    public Builder special(Object o, int leftPrec, int rightPrec,
+        Special special) {
+      return add(dummy.special(o, leftPrec, rightPrec, special));
+    }
+
+    public PrecedenceClimbingParser build() {
+      return new PrecedenceClimbingParser(tokens);
+    }
+  }
+
+  /** List view onto the tokens in a parser. The view is semi-mutable; it
+   * supports {@link List#remove(int)} but not {@link List#set} or
+   * {@link List#add}. */
+  private class TokenList extends AbstractList<Token> {
+    @Override public Token get(int index) {
+      for (Token t = first; t != null; t = t.next) {
+        if (index-- == 0) {
+          return t;
+        }
+      }
+      throw new IndexOutOfBoundsException();
+    }
+
+    @Override public int size() {
+      int n = 0;
+      for (Token t = first; t != null; t = t.next) {
+        ++n;
+      }
+      return n;
+    }
+
+    @Override public Token remove(int index) {
+      Token t = get(index);
+      if (t.previous == null) {
+        first = t.next;
+      } else {
+        t.previous.next = t.next;
+      }
+      if (t.next == null) {
+        last = t.previous;
+      } else {
+        t.next.previous = t.previous;
+      }
+      return t;
+    }
+
+    @Override public Token set(int index, Token element) {
+      final Token t = get(index);
+      element.previous = t.previous;
+      if (t.previous == null) {
+        first = element;
+      } else {
+        t.previous.next = element;
+      }
+      element.next = t.next;
+      if (t.next == null) {
+        last = element;
+      } else {
+        t.next.previous = element;
+      }
+      return t;
+    }
+  }
+}
+
+// End PrecedenceClimbingParser.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/main/java/org/apache/calcite/util/Util.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/Util.java b/core/src/main/java/org/apache/calcite/util/Util.java
index 0eee37b..c01c772 100644
--- a/core/src/main/java/org/apache/calcite/util/Util.java
+++ b/core/src/main/java/org/apache/calcite/util/Util.java
@@ -2044,7 +2044,7 @@ public class Util {
 
   /** Returns all but the first {@code n} elements of a list. */
   public static <E> List<E> skip(List<E> list, int fromIndex) {
-    return list.subList(fromIndex, list.size());
+    return fromIndex == 0 ? list : list.subList(fromIndex, list.size());
   }
 
   public static List<Integer> range(final int end) {

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 590f0ee..7a3ea1a 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -744,6 +744,56 @@ public class SqlParserTest {
             + "WHERE (1 AND TRUE)");
   }
 
+  @Test public void testLessThanAssociativity() {
+    checkExp("NOT a = b", "(NOT (`A` = `B`))");
+
+    // comparison operators are left-associative
+    checkExp("x < y < z", "((`X` < `Y`) < `Z`)");
+    checkExp("x < y <= z = a", "(((`X` < `Y`) <= `Z`) = `A`)");
+    checkExp("a = x < y <= z = a", "((((`A` = `X`) < `Y`) <= `Z`) = `A`)");
+
+    // IS NULL has lower precedence than comparison
+    checkExp("a = x is null", "((`A` = `X`) IS NULL)");
+    checkExp("a = x is not null", "((`A` = `X`) IS NOT NULL)");
+
+    // BETWEEN, IN, LIKE have higher precedence than comparison
+    checkExp("a = x between y = b and z = c",
+        "((`A` = (`X` BETWEEN ASYMMETRIC (`Y` = `B`) AND `Z`)) = `C`)");
+    checkExp("a = x like y = b",
+        "((`A` = (`X` LIKE `Y`)) = `B`)");
+    checkExp("a = x not like y = b",
+        "((`A` = (`X` NOT LIKE `Y`)) = `B`)");
+    checkExp("a = x similar to y = b",
+        "((`A` = (`X` SIMILAR TO `Y`)) = `B`)");
+    checkExp("a = x not similar to y = b",
+        "((`A` = (`X` NOT SIMILAR TO `Y`)) = `B`)");
+    checkExp("a = x not in (y, z) = b",
+        "((`A` = (`X` NOT IN (`Y`, `Z`))) = `B`)");
+
+    // LIKE has higher precedence than IS NULL
+    checkExp("a like b is null", "((`A` LIKE `B`) IS NULL)");
+    checkExp("a not like b is not null",
+        "((`A` NOT LIKE `B`) IS NOT NULL)");
+
+    // = has higher precedence than NOT
+    checkExp("NOT a = b", "(NOT (`A` = `B`))");
+    checkExp("NOT a = NOT b", "(NOT (`A` = (NOT `B`)))");
+
+    // IS NULL has higher precedence than NOT
+    checkExp("NOT a IS NULL", "(NOT (`A` IS NULL))");
+    checkExp("NOT a = b IS NOT NULL", "(NOT ((`A` = `B`) IS NOT NULL))");
+
+    // NOT has higher precedence than AND, which  has higher precedence than OR
+    checkExp("NOT a AND NOT b", "((NOT `A`) AND (NOT `B`))");
+    checkExp("NOT a OR NOT b", "((NOT `A`) OR (NOT `B`))");
+    checkExp("NOT a = b AND NOT c = d OR NOT e = f",
+        "(((NOT (`A` = `B`)) AND (NOT (`C` = `D`))) OR (NOT (`E` = `F`)))");
+    checkExp("NOT a = b OR NOT c = d AND NOT e = f",
+        "((NOT (`A` = `B`)) OR ((NOT (`C` = `D`)) AND (NOT (`E` = `F`))))");
+    checkExp("NOT NOT a = b OR NOT NOT c = d",
+        "((NOT (NOT (`A` = `B`))) OR (NOT (NOT (`C` = `D`))))");
+  }
+
   @Test public void testIsBooleans() {
     String[] inOuts = {"NULL", "TRUE", "FALSE", "UNKNOWN"};
 
@@ -752,7 +802,7 @@ public class SqlParserTest {
           "select * from t where nOt fAlSe Is " + inOut,
           "SELECT *\n"
               + "FROM `T`\n"
-              + "WHERE ((NOT FALSE) IS " + inOut + ")");
+              + "WHERE (NOT (FALSE IS " + inOut + "))");
 
       check(
           "select * from t where c1=1.1 IS NOT " + inOut,
@@ -771,7 +821,7 @@ public class SqlParserTest {
     check("select 1 from t where not true is unknown",
         "SELECT 1\n"
             + "FROM `T`\n"
-            + "WHERE ((NOT TRUE) IS UNKNOWN)");
+            + "WHERE (NOT (TRUE IS UNKNOWN))");
 
     check(
         "select * from t where x is unknown is not unknown is false is not false"
@@ -781,11 +831,16 @@ public class SqlParserTest {
             + "WHERE ((((((((`X` IS UNKNOWN) IS NOT UNKNOWN) IS FALSE) IS NOT FALSE) IS TRUE) IS NOT TRUE) IS NULL) IS NOT NULL)");
 
     // combine IS postfix operators with infix (AND) and prefix (NOT) ops
-    check(
-        "select * from t where x is unknown is false and x is unknown is true or not y is unknown is not null",
-        "SELECT *\n"
-            + "FROM `T`\n"
-            + "WHERE ((((`X` IS UNKNOWN) IS FALSE) AND ((`X` IS UNKNOWN) IS TRUE)) OR (((NOT `Y`) IS UNKNOWN) IS NOT NULL))");
+    final String sql = "select * from t "
+        + "where x is unknown is false "
+        + "and x is unknown is true "
+        + "or not y is unknown is not null";
+    final String expected = "SELECT *\n"
+        + "FROM `T`\n"
+        + "WHERE ((((`X` IS UNKNOWN) IS FALSE)"
+        + " AND ((`X` IS UNKNOWN) IS TRUE))"
+        + " OR (NOT ((`Y` IS UNKNOWN) IS NOT NULL)))";
+    check(sql, expected);
   }
 
   @Test public void testEqualNotEqual() {
@@ -871,10 +926,10 @@ public class SqlParserTest {
         "values a between b and c + 2 or d and e",
         "VALUES (ROW(((`A` BETWEEN ASYMMETRIC `B` AND (`C` + 2)) OR (`D` AND `E`))))");
 
-    // '=' and BETWEEN have same precedence, and are left-assoc
+    // '=' has slightly lower precedence than BETWEEN; both are left-assoc
     check(
         "values x = a between b and c = d = e",
-        "VALUES (ROW(((((`X` = `A`) BETWEEN ASYMMETRIC `B` AND `C`) = `D`) = `E`)))");
+        "VALUES (ROW((((`X` = (`A` BETWEEN ASYMMETRIC `B` AND `C`)) = `D`) = `E`)))");
 
     // AND doesn't match BETWEEN if it's between parentheses!
     check(
@@ -1147,7 +1202,7 @@ public class SqlParserTest {
     // LIKE has same precedence as '='; LIKE is right-assoc, '=' is left
     check(
         "values a = b like c = d",
-        "VALUES (ROW(((`A` = `B`) LIKE (`C` = `D`))))");
+        "VALUES (ROW(((`A` = (`B` LIKE `C`)) = `D`)))");
 
     // Nested LIKE
     check(
@@ -1228,13 +1283,13 @@ public class SqlParserTest {
   @Test public void testFoo() {
   }
 
-  @Test public void testArthimeticOperators() {
+  @Test public void testArithmeticOperators() {
     checkExp("1-2+3*4/5/6-7", "(((1 - 2) + (((3 * 4) / 5) / 6)) - 7)");
     checkExp("power(2,3)", "POWER(2, 3)");
     checkExp("aBs(-2.3e-2)", "ABS(-2.3E-2)");
     checkExp("MOD(5             ,\t\f\r\n2)", "MOD(5, 2)");
     checkExp("ln(5.43  )", "LN(5.43)");
-    checkExp("log10(- -.2  )", "LOG10((- -0.2))");
+    checkExp("log10(- -.2  )", "LOG10(0.2)");
   }
 
   @Test public void testExists() {
@@ -2297,14 +2352,14 @@ public class SqlParserTest {
     check(
         "values (- -1\n"
             + ")",
-        "VALUES (ROW((- -1)))");
+        "VALUES (ROW(1))");
 
     check(
         "values (--1+\n"
             + "2)",
         "VALUES (ROW(2))");
 
-    // end of multiline commment without start
+    // end of multiline comment without start
     if (Bug.FRG73_FIXED) {
       checkFails("values (1 */ 2)", "xx");
     }
@@ -2380,7 +2435,7 @@ public class SqlParserTest {
     checkExp("1", "1");
     checkExp("+1.", "1");
     checkExp("-1", "-1");
-    checkExp("- -1", "(- -1)");
+    checkExp("- -1", "1");
     checkExp("1.0", "1.0");
     checkExp("-3.2", "-3.2");
     checkExp("1.", "1");
@@ -2437,7 +2492,11 @@ public class SqlParserTest {
   }
 
   @Test public void testPrecedence2() {
-    checkExp("- - 1", "(- -1)"); // two prefices
+    checkExp("- - 1", "1"); // special case for unary minus
+  }
+
+  @Test public void testPrecedence2b() {
+    checkExp("not not 1", "(NOT (NOT 1))"); // two prefixes
   }
 
   @Test public void testPrecedence3() {
@@ -2821,7 +2880,7 @@ public class SqlParserTest {
         "select ^cursor^(select * from emps) from emps",
         "CURSOR expression encountered in illegal context");
     checkFails(
-        "call p(^cursor^(select * from emps))",
+        "call list(^cursor^(select * from emps))",
         "CURSOR expression encountered in illegal context");
     checkFails(
         "select f(^cursor^(select * from emps)) from emps",
@@ -6887,11 +6946,7 @@ public class SqlParserTest {
       try {
         sqlNode = parseStmt(sql);
       } catch (SqlParseException e) {
-        e.printStackTrace();
-        String message = "Received error while parsing SQL '" + sql
-            + "'; error is:\n"
-            + e.toString();
-        throw new AssertionError(message);
+        throw new RuntimeException("Error while parsing SQL: " + sql, e);
       }
       return sqlNode;
     }
@@ -6912,10 +6967,7 @@ public class SqlParserTest {
       try {
         sqlNode = parseExpression(sql);
       } catch (SqlParseException e) {
-        String message = "Received error while parsing SQL '" + sql
-            + "'; error is:\n"
-            + e.toString();
-        throw new RuntimeException(message, e);
+        throw new RuntimeException("Error while parsing expression: " + sql, e);
       }
       return sqlNode;
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java b/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
index f9f1bd1..3c55d6d 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlTesterImpl.java
@@ -113,23 +113,16 @@ public class SqlTesterImpl implements SqlTester {
     } catch (SqlParseException e) {
       String errMessage = e.getMessage();
       if (expectedMsgPattern == null) {
-        e.printStackTrace();
-        throw new AssertionError(
-            "Error while parsing query [" + sap.sql + "]");
-      } else if (
-          (null == errMessage)
-              || !errMessage.matches(expectedMsgPattern)) {
-        e.printStackTrace();
-        throw new AssertionError(
-            "Error did not match expected ["
-                + expectedMsgPattern + "] while parsing query ["
-                + sap.sql + "]");
+        throw new RuntimeException("Error while parsing query:" + sap.sql, e);
+      } else if (errMessage == null
+          || !errMessage.matches(expectedMsgPattern)) {
+        throw new RuntimeException("Error did not match expected ["
+            + expectedMsgPattern + "] while parsing query ["
+            + sap.sql + "]", e);
       }
       return;
     } catch (Throwable e) {
-      e.printStackTrace();
-      throw new AssertionError(
-          "Error while parsing query [" + sap.sql + "]");
+      throw new RuntimeException("Error while parsing query: " + sap.sql, e);
     }
 
     Throwable thrown = null;
@@ -163,11 +156,8 @@ public class SqlTesterImpl implements SqlTester {
     SqlNode sqlNode;
     try {
       sqlNode = parseQuery(sql);
-    } catch (SqlParseException e) {
-      throw new RuntimeException("Error while parsing query [" + sql + "]", e);
     } catch (Throwable e) {
-      e.printStackTrace();
-      throw new AssertionError("Error while parsing query [" + sql + "]");
+      throw new RuntimeException("Error while parsing query: " + sql, e);
     }
     return validator.validate(sqlNode);
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
index 7211e05..7152553 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
+++ b/core/src/test/java/org/apache/calcite/test/CalciteSuite.java
@@ -53,6 +53,7 @@ import org.apache.calcite.util.ChunkListTest;
 import org.apache.calcite.util.ImmutableBitSetTest;
 import org.apache.calcite.util.PartiallyOrderedSetTest;
 import org.apache.calcite.util.PermutationTestCase;
+import org.apache.calcite.util.PrecedenceClimbingParserTest;
 import org.apache.calcite.util.ReflectVisitorTest;
 import org.apache.calcite.util.UtilTest;
 import org.apache.calcite.util.graph.DirectedGraphTest;
@@ -81,6 +82,7 @@ import org.junit.runners.Suite;
     RelOptUtilTest.class,
     RelCollationTest.class,
     UtilTest.class,
+    PrecedenceClimbingParserTest.class,
     MappingTest.class,
     CalciteResourceTest.class,
     FilteratorTest.class,

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 5d2ef4a..7fff2d9 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -23,6 +23,7 @@ import org.apache.calcite.config.Lex;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.test.SqlTester;
@@ -37,6 +38,7 @@ import org.apache.calcite.util.ImmutableBitSet;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
 
 import org.junit.BeforeClass;
 import org.junit.Ignore;
@@ -46,6 +48,7 @@ import org.slf4j.LoggerFactory;
 
 import java.nio.charset.Charset;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Locale;
 
@@ -7623,6 +7626,184 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
     }
   }
 
+  private static int prec(SqlOperator op) {
+    return Math.max(op.getLeftPrec(), op.getRightPrec());
+  }
+
+  /** Tests that operators, sorted by precedence, are in a sane order. Each
+   * operator has a {@link SqlOperator#getLeftPrec() left} and
+   * {@link SqlOperator#getRightPrec()} right} precedence, but we would like
+   * the order to remain the same even if we tweak particular operators'
+   * precedences. If you need to update the expected output, you might also
+   * need to change
+   * <a href="http://calcite.apache.org/docs/reference.html#operator-precedence">
+   * the documentation</a>. */
+  @Test public void testOperatorsSortedByPrecedence() {
+    final StringBuilder b = new StringBuilder();
+    final Comparator<SqlOperator> comparator =
+        new Comparator<SqlOperator>() {
+          public int compare(SqlOperator o1, SqlOperator o2) {
+            int c = Integer.compare(prec(o1), prec(o2));
+            if (c != 0) {
+              return -c;
+            }
+            c = o1.getName().compareTo(o2.getName());
+            if (c != 0) {
+              return c;
+            }
+            return o1.getSyntax().compareTo(o2.getSyntax());
+          }
+        };
+    final List<SqlOperator> operators =
+        SqlStdOperatorTable.instance().getOperatorList();
+    int p = -1;
+    for (SqlOperator op : Ordering.from(comparator).sortedCopy(operators)) {
+      final String type;
+      switch (op.getSyntax()) {
+      case FUNCTION:
+      case FUNCTION_ID:
+      case FUNCTION_STAR:
+      case INTERNAL:
+        continue;
+      case PREFIX:
+        type = "pre";
+        break;
+      case POSTFIX:
+        type = "post";
+        break;
+      case BINARY:
+        if (op.getLeftPrec() < op.getRightPrec()) {
+          type = "left";
+        } else {
+          type = "right";
+        }
+        break;
+      default:
+        if (op instanceof SqlSpecialOperator) {
+          type = "-";
+        } else {
+          continue;
+        }
+      }
+      if (prec(op) != p) {
+        b.append('\n');
+        p = prec(op);
+      }
+      b.append(op.getName())
+          .append(' ')
+          .append(type)
+          .append('\n');
+    }
+    final String expected = "\n"
+        + "ARRAY -\n"
+        + "ARRAY -\n"
+        + "COLUMN_LIST -\n"
+        + "CURSOR -\n"
+        + "LATERAL -\n"
+        + "MAP -\n"
+        + "MAP -\n"
+        + "MULTISET -\n"
+        + "MULTISET -\n"
+        + "ROW -\n"
+        + "TABLE -\n"
+        + "UNNEST -\n"
+        + "\n"
+        + "CURRENT_VALUE -\n"
+        + "DEFAULT -\n"
+        + "ITEM -\n"
+        + "NEXT_VALUE -\n"
+        + "\n"
+        + "$LiteralChain -\n"
+        + "+ pre\n"
+        + "- pre\n"
+        + ". left\n"
+        + "\n"
+        + "* left\n"
+        + "/ left\n"
+        + "/INT left\n"
+        + "|| left\n"
+        + "\n"
+        + "+ left\n"
+        + "- left\n"
+        + "- -\n"
+        + "DATETIME_PLUS -\n"
+        + "EXISTS pre\n"
+        + "\n"
+        + "BETWEEN ASYMMETRIC -\n"
+        + "BETWEEN SYMMETRIC -\n"
+        + "IN left\n"
+        + "LIKE -\n"
+        + "NOT BETWEEN ASYMMETRIC -\n"
+        + "NOT BETWEEN SYMMETRIC -\n"
+        + "NOT IN left\n"
+        + "NOT LIKE -\n"
+        + "NOT SIMILAR TO -\n"
+        + "SIMILAR TO -\n"
+        + "\n"
+        + "$IS_DIFFERENT_FROM left\n"
+        + "< left\n"
+        + "<= left\n"
+        + "<> left\n"
+        + "= left\n"
+        + "> left\n"
+        + ">= left\n"
+        + "IS DISTINCT FROM left\n"
+        + "IS NOT DISTINCT FROM left\n"
+        + "MEMBER OF left\n"
+        + "OVERLAPS -\n"
+        + "SUBMULTISET OF left\n"
+        + "\n"
+        + "IS A SET post\n"
+        + "IS FALSE post\n"
+        + "IS NOT FALSE post\n"
+        + "IS NOT NULL post\n"
+        + "IS NOT TRUE post\n"
+        + "IS NOT UNKNOWN post\n"
+        + "IS NULL post\n"
+        + "IS TRUE post\n"
+        + "IS UNKNOWN post\n"
+        + "\n"
+        + "NOT pre\n"
+        + "\n"
+        + "AND left\n"
+        + "\n"
+        + "OR left\n"
+        + "\n"
+        + "=> -\n"
+        + "AS -\n"
+        + "DESC post\n"
+        + "OVER left\n"
+        + "TABLESAMPLE -\n"
+        + "\n"
+        + "INTERSECT left\n"
+        + "INTERSECT ALL left\n"
+        + "MULTISET INTERSECT left\n"
+        + "MULTISET INTERSECT ALL left\n"
+        + "NULLS FIRST post\n"
+        + "NULLS LAST post\n"
+        + "\n"
+        + "EXCEPT left\n"
+        + "EXCEPT ALL left\n"
+        + "MULTISET EXCEPT left\n"
+        + "MULTISET EXCEPT ALL left\n"
+        + "MULTISET UNION left\n"
+        + "MULTISET UNION ALL left\n"
+        + "UNION left\n"
+        + "UNION ALL left\n"
+        + "\n"
+        + "$throw -\n"
+        + "EXTRACT_DATE -\n"
+        + "FILTER left\n"
+        + "Reinterpret -\n"
+        + "TABLE pre\n"
+        + "VALUES -\n"
+        + "\n"
+        + "CALL pre\n"
+        + "ESCAPE -\n"
+        + "NEW pre\n";
+    assertThat(b.toString(), is(expected));
+  }
+
   /** Tests that it is an error to insert into the same column twice, even using
    * case-insensitive matching. */
   @Test public void testCaseInsensitiveInsert() {

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java b/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java
new file mode 100644
index 0000000..3fbeac4
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/util/PrecedenceClimbingParserTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.calcite.util;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Unit test for {@link PrecedenceClimbingParser}.
+ */
+public class PrecedenceClimbingParserTest {
+  @Test public void testBasic() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("+", 1, true)
+        .prefix("-", 3)
+        .atom("b")
+        .infix("*", 2, true)
+        .atom("c")
+        .postfix("!", 4)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("(a + ((- b) * (c !)))"));
+  }
+
+  @Test public void testRepeatedPrefixPostfix() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .prefix("+", 3)
+        .prefix("-", 3)
+        .prefix("+", 3)
+        .prefix("+", 3)
+        .atom("a")
+        .postfix("!", 4)
+        .infix("+", 1, true)
+        .prefix("-", 3)
+        .prefix("-", 3)
+        .atom("b")
+        .postfix("!", 4)
+        .postfix("!", 4)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token),
+        is("((+ (- (+ (+ (a !))))) + (- (- ((b !) !))))"));
+  }
+
+  @Test public void testAtom() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("a"));
+  }
+
+  @Test public void testOnlyPrefix() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .prefix("-", 3)
+        .prefix("-", 3)
+        .atom(1)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("(- (- 1))"));
+  }
+
+  @Test public void testOnlyPostfix() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom(1)
+        .postfix("!", 33333)
+        .postfix("!", 33333)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("((1 !) !)"));
+  }
+
+  @Test public void testLeftAssociative() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("*", 2, true)
+        .atom("b")
+        .infix("+", 1, true)
+        .atom("c")
+        .infix("+", 1, true)
+        .atom("d")
+        .infix("+", 1, true)
+        .atom("e")
+        .infix("*", 2, true)
+        .atom("f")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("((((a * b) + c) + d) + (e * f))"));
+  }
+
+  @Test public void testRightAssociative() {
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("^", 3, false)
+        .atom("b")
+        .infix("^", 3, false)
+        .atom("c")
+        .infix("^", 3, false)
+        .atom("d")
+        .infix("+", 1, true)
+        .atom("e")
+        .infix("*", 2, true)
+        .atom("f")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("((a ^ (b ^ (c ^ d))) + (e * f))"));
+  }
+
+  @Test public void testSpecial() {
+    // price > 5 and price between 1 + 2 and 3 * 4 and price is null
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("price")
+        .infix(">", 4, true)
+        .atom("5")
+        .infix("and", 2, true)
+        .atom("price")
+        .special("between", 3, 3,
+            new PrecedenceClimbingParser.Special() {
+              public PrecedenceClimbingParser.Result apply(
+                  PrecedenceClimbingParser parser,
+                  PrecedenceClimbingParser.SpecialOp op) {
+                return new PrecedenceClimbingParser.Result(op.previous,
+                    op.next.next.next,
+                    parser.call(op,
+                        ImmutableList.of(op.previous, op.next,
+                            op.next.next.next)));
+              }
+            })
+        .atom("1")
+        .infix("+", 5, true)
+        .atom("2")
+        .infix("and", 2, true)
+        .atom("3")
+        .infix("*", 6, true)
+        .atom("4")
+        .infix("and", 2, true)
+        .atom("price")
+        .postfix("is null", 4)
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token),
+        is("(((price > 5) and between(price, (1 + 2), (3 * 4)))"
+            + " and (price is null))"));
+  }
+
+  @Test public void testEqualPrecedence() {
+    // LIKE has same precedence as '='; LIKE is right-assoc, '=' is left
+    final PrecedenceClimbingParser p = new PrecedenceClimbingParser.Builder()
+        .atom("a")
+        .infix("=", 3, true)
+        .atom("b")
+        .infix("like", 3, false)
+        .atom("c")
+        .infix("=", 3, true)
+        .atom("d")
+        .build();
+    final PrecedenceClimbingParser.Token token = p.parse();
+    assertThat(p.print(token), is("(((a = b) like c) = d)"));
+  }
+}
+
+// End PrecedenceClimbingParserTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/core/src/test/resources/sql/operator.iq
----------------------------------------------------------------------
diff --git a/core/src/test/resources/sql/operator.iq b/core/src/test/resources/sql/operator.iq
new file mode 100644
index 0000000..626d31d
--- /dev/null
+++ b/core/src/test/resources/sql/operator.iq
@@ -0,0 +1,69 @@
+# operator.iq - expressions involving operators
+#
+# 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.
+#
+!use scott
+!set outputformat mysql
+
+# [CALCITE-1095] NOT precedence
+select * from "scott".emp where not sal > 1300;
++-------+--------+----------+------+------------+---------+---------+--------+
+| EMPNO | ENAME  | JOB      | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO |
++-------+--------+----------+------+------------+---------+---------+--------+
+|  7369 | SMITH  | CLERK    | 7902 | 1980-12-17 |  800.00 |         |     20 |
+|  7521 | WARD   | SALESMAN | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30 |
+|  7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30 |
+|  7876 | ADAMS  | CLERK    | 7788 | 1987-05-23 | 1100.00 |         |     20 |
+|  7900 | JAMES  | CLERK    | 7698 | 1981-12-03 |  950.00 |         |     30 |
+|  7934 | MILLER | CLERK    | 7782 | 1982-01-23 | 1300.00 |         |     10 |
++-------+--------+----------+------+------------+---------+---------+--------+
+(6 rows)
+
+!ok
+
+select count(*) as c from "scott".emp where not ename = 'SMITH';
++----+
+| C  |
++----+
+| 13 |
++----+
+(1 row)
+
+!ok
+
+select count(*) as c from "scott".emp where not not ename = 'SMITH';
++---+
+| C |
++---+
+| 1 |
++---+
+(1 row)
+
+!ok
+
+select * from "scott".emp where not sal > 1300 and not sal < 1200;
++-------+--------+----------+------+------------+---------+---------+--------+
+| EMPNO | ENAME  | JOB      | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO |
++-------+--------+----------+------+------------+---------+---------+--------+
+|  7521 | WARD   | SALESMAN | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30 |
+|  7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30 |
+|  7934 | MILLER | CLERK    | 7782 | 1982-01-23 | 1300.00 |         |     10 |
++-------+--------+----------+------+------------+---------+---------+--------+
+(3 rows)
+
+!ok
+
+# End operator.iq

http://git-wip-us.apache.org/repos/asf/calcite/blob/339ac13e/site/_docs/reference.md
----------------------------------------------------------------------
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 8f9fd16..428ef83 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -831,6 +831,24 @@ Note:
 
 ## Operators and functions
 
+### Operator precedence
+
+The operator precedence and associativity, highest to lowest.
+
+| Operator                                          | Associativity
+|:------------------------------------------------- |:-------------
+| .                                                 | left
+| [ ] (array element)                               | left
+| + - (unary plus, minus)                           | right
+| * /                                               | left
+| + -                                               | left
+| BETWEEN, IN, LIKE, SIMILAR                        | -
+| < > = <= >= <>                                    | left
+| IS NULL, IS FALSE, IS NOT TRUE etc.               | -
+| NOT                                               | right
+| AND                                               | left
+| OR                                                | left
+
 ### Comparison operators
 
 | Operator syntax                                   | Description