diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 07214bfd76a77e2889e0f4d92fbdecc68423c828..d0f43c64af3e7430e28c4c236d0a0a8c1c5c23eb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -6816,6 +6816,29 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-operator-precedence-warning" xreflabel="operator_precedence_warning">
+      <term><varname>operator_precedence_warning</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>operator_precedence_warning</> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        When on, the parser will emit a warning for any construct that might
+        have changed meanings since <productname>PostgreSQL</> 9.4 as a result
+        of changes in operator precedence.  This is useful for auditing
+        applications to see if precedence changes have broken anything; but it
+        is not meant to be kept turned on in production, since it will warn
+        about some perfectly valid, standard-compliant SQL code.
+        The default is <literal>off</>.
+       </para>
+
+       <para>
+        See <xref linkend="sql-precedence"> for more information.
+       </para>
+      </listitem>
+     </varlistentry>
+
     <varlistentry id="guc-quote-all-identifiers" xreflabel="quote-all-identifiers">
       <term><varname>quote_all_identifiers</varname> (<type>boolean</type>)
       <indexterm>
diff --git a/doc/src/sgml/syntax.sgml b/doc/src/sgml/syntax.sgml
index 8283e252582b39aa8708fb1cf3c63dacef3a4ef6..ff2c3e2b9a371bab67594b89ac3d462503551627 100644
--- a/doc/src/sgml/syntax.sgml
+++ b/doc/src/sgml/syntax.sgml
@@ -984,10 +984,11 @@ CAST ( '<replaceable>string</replaceable>' AS <replaceable>type</replaceable> )
     associativity of the operators in <productname>PostgreSQL</>.
     Most operators have the same precedence and are left-associative.
     The precedence and associativity of the operators is hard-wired
-    into the parser.  This can lead to non-intuitive behavior; for
-    example the Boolean operators <literal>&lt;</> and
-    <literal>&gt;</> have a different precedence than the Boolean
-    operators <literal>&lt;=</> and <literal>&gt;=</>.  Also, you will
+    into the parser.
+   </para>
+
+   <para>
+    You will
     sometimes need to add parentheses when using combinations of
     binary and unary operators.  For instance:
 <programlisting>
@@ -1008,7 +1009,7 @@ SELECT (5 !) - 6;
    </para>
 
    <table id="sql-precedence-table">
-    <title>Operator Precedence (decreasing)</title>
+    <title>Operator Precedence (highest to lowest)</title>
 
     <tgroup cols="3">
      <thead>
@@ -1063,41 +1064,11 @@ SELECT (5 !) - 6;
       </row>
 
       <row>
-       <entry><token>IS</token></entry>
-       <entry></entry>
-       <entry><literal>IS TRUE</>, <literal>IS FALSE</>, <literal>IS NULL</>, etc</entry>
-      </row>
-
-      <row>
-       <entry><token>ISNULL</token></entry>
-       <entry></entry>
-       <entry>test for null</entry>
-      </row>
-
-      <row>
-       <entry><token>NOTNULL</token></entry>
-       <entry></entry>
-       <entry>test for not null</entry>
-      </row>
-
-      <row>
-       <entry>(any other)</entry>
+       <entry>(any other operator)</entry>
        <entry>left</entry>
        <entry>all other native and user-defined operators</entry>
       </row>
 
-      <row>
-       <entry><token>IN</token></entry>
-       <entry></entry>
-       <entry>set membership</entry>
-      </row>
-
-      <row>
-       <entry><token>BETWEEN</token></entry>
-       <entry></entry>
-       <entry>range containment</entry>
-      </row>
-
       <row>
        <entry><token>OVERLAPS</token></entry>
        <entry></entry>
@@ -1105,21 +1076,23 @@ SELECT (5 !) - 6;
       </row>
 
       <row>
-       <entry><token>LIKE</token> <token>ILIKE</token> <token>SIMILAR</token></entry>
+       <entry><token>BETWEEN</token> <token>IN</token> <token>LIKE</token> <token>ILIKE</token> <token>SIMILAR</token></entry>
        <entry></entry>
-       <entry>string pattern matching</entry>
+       <entry>range containment, set membership, string matching</entry>
       </row>
 
       <row>
-       <entry><token>&lt;</token> <token>&gt;</token></entry>
+       <entry><token>&lt;</token> <token>&gt;</token> <token>=</token> <token>&lt;=</token> <token>&gt;=</token> <token>&lt;&gt;</token>
+</entry>
        <entry></entry>
-       <entry>less than, greater than</entry>
+       <entry>comparison operators</entry>
       </row>
 
       <row>
-       <entry><token>=</token></entry>
-       <entry>right</entry>
-       <entry>equality, assignment</entry>
+       <entry><token>IS</token> <token>ISNULL</token> <token>NOTNULL</token></entry>
+       <entry></entry>
+       <entry><literal>IS TRUE</>, <literal>IS FALSE</>, <literal>IS
+       NULL</>, <literal>IS DISTINCT FROM</>, etc</entry>
       </row>
 
       <row>
@@ -1159,9 +1132,32 @@ SELECT (5 !) - 6;
 SELECT 3 OPERATOR(pg_catalog.+) 4;
 </programlisting>
     the <literal>OPERATOR</> construct is taken to have the default precedence
-    shown in <xref linkend="sql-precedence-table"> for <quote>any other</> operator.  This is true no matter
+    shown in <xref linkend="sql-precedence-table"> for
+    <quote>any other operator</>.  This is true no matter
     which specific operator appears inside <literal>OPERATOR()</>.
    </para>
+
+   <note>
+    <para>
+     <productname>PostgreSQL</> versions before 9.5 used slightly different
+     operator precedence rules.  In particular, <token>&lt;=</token>
+     <token>&gt;=</token> and <token>&lt;&gt;</token> used to be treated as
+     generic operators; <literal>IS</> tests used to have higher priority;
+     and <literal>NOT BETWEEN</> and related constructs acted inconsistently,
+     being taken in some cases as having the precedence of <literal>NOT</>
+     rather than <literal>BETWEEN</>.  These rules were changed for better
+     compliance with the SQL standard and to reduce confusion from
+     inconsistent treatment of logically equivalent constructs.  In most
+     cases, these changes will result in no behavioral change, or perhaps
+     in <quote>no such operator</> failures which can be resolved by adding
+     parentheses.  However there are corner cases in which a query might
+     change behavior without any parsing error being reported.  If you are
+     concerned about whether these changes have silently broken something,
+     you can test your application with the configuration
+     parameter <xref linkend="guc-operator-precedence-warning"> turned on
+     to see if any warnings are logged.
+    </para>
+   </note>
   </sect2>
  </sect1>
 
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 775f482abda73baad1d98dedf3febc8f765bdf13..03f8adaae3ee010a508beb6a5c91cb981ba8cb67 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2546,6 +2546,9 @@ _outAExpr(StringInfo str, const A_Expr *node)
 			appendStringInfoString(str, " NOT_BETWEEN_SYM ");
 			WRITE_NODE_FIELD(name);
 			break;
+		case AEXPR_PAREN:
+			appendStringInfoString(str, " PAREN");
+			break;
 		default:
 			appendStringInfoString(str, " ??");
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 21b8897038f2602d09bce5f9e5df175f82a3c985..cf0d31744e15a571a0082cb7972ea59715d4c860 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -58,6 +58,7 @@
 #include "nodes/nodeFuncs.h"
 #include "parser/gramparse.h"
 #include "parser/parser.h"
+#include "parser/parse_expr.h"
 #include "storage/lmgr.h"
 #include "utils/date.h"
 #include "utils/datetime.h"
@@ -534,6 +535,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token <str>	IDENT FCONST SCONST BCONST XCONST Op
 %token <ival>	ICONST PARAM
 %token			TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER
+%token			LESS_EQUALS GREATER_EQUALS NOT_EQUALS
 
 /*
  * If you want to make any keyword changes, update the keyword table in
@@ -636,8 +638,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  * The grammar thinks these are keywords, but they are not in the kwlist.h
  * list and so can never be entered directly.  The filter in parser.c
  * creates these tokens when required (based on looking one token ahead).
+ *
+ * NOT_LA exists so that productions such as NOT LIKE can be given the same
+ * precedence as LIKE; otherwise they'd effectively have the same precedence
+ * as NOT, at least with respect to their left-hand subexpression.
+ * NULLS_LA and WITH_LA are needed to make the grammar LALR(1).
  */
-%token			NULLS_LA WITH_LA
+%token		NOT_LA NULLS_LA WITH_LA
 
 
 /* Precedence: lowest to highest */
@@ -647,13 +654,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %left		OR
 %left		AND
 %right		NOT
-%right		'='
-%nonassoc	'<' '>'
-%nonassoc	LIKE ILIKE SIMILAR
-%nonassoc	ESCAPE
+%nonassoc	IS ISNULL NOTNULL	/* IS sets precedence for IS NULL, etc */
+%nonassoc	'<' '>' '=' LESS_EQUALS GREATER_EQUALS NOT_EQUALS
+%nonassoc	BETWEEN IN_P LIKE ILIKE SIMILAR NOT_LA
+%nonassoc	ESCAPE			/* ESCAPE must be just above LIKE/ILIKE/SIMILAR */
 %nonassoc	OVERLAPS
-%nonassoc	BETWEEN
-%nonassoc	IN_P
 %left		POSTFIXOP		/* dummy for postfix Op rules */
 /*
  * To support target_el without AS, we must give IDENT an explicit priority
@@ -678,9 +683,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %nonassoc	UNBOUNDED		/* ideally should have same precedence as IDENT */
 %nonassoc	IDENT NULL_P PARTITION RANGE ROWS PRECEDING FOLLOWING
 %left		Op OPERATOR		/* multi-character ops and user-defined operators */
-%nonassoc	NOTNULL
-%nonassoc	ISNULL
-%nonassoc	IS				/* sets precedence for IS NULL, etc */
 %left		'+' '-'
 %left		'*' '/' '%'
 %left		'^'
@@ -11147,6 +11149,12 @@ interval_second:
  *
  * c_expr is all the productions that are common to a_expr and b_expr;
  * it's factored out just to eliminate redundant coding.
+ *
+ * Be careful of productions involving more than one terminal token.
+ * By default, bison will assign such productions the precedence of their
+ * last terminal, but in nearly all cases you want it to be the precedence
+ * of the first terminal instead; otherwise you will not get the behavior
+ * you expect!  So we use %prec annotations freely to set precedences.
  */
 a_expr:		c_expr									{ $$ = $1; }
 			| a_expr TYPECAST Typename
@@ -11196,6 +11204,12 @@ a_expr:		c_expr									{ $$ = $1; }
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">", $1, $3, @2); }
 			| a_expr '=' a_expr
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", $1, $3, @2); }
+			| a_expr LESS_EQUALS a_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<=", $1, $3, @2); }
+			| a_expr GREATER_EQUALS a_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">=", $1, $3, @2); }
+			| a_expr NOT_EQUALS a_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<>", $1, $3, @2); }
 
 			| a_expr qual_Op a_expr				%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); }
@@ -11210,13 +11224,15 @@ a_expr:		c_expr									{ $$ = $1; }
 				{ $$ = makeOrExpr($1, $3, @2); }
 			| NOT a_expr
 				{ $$ = makeNotExpr($2, @1); }
+			| NOT_LA a_expr						%prec NOT
+				{ $$ = makeNotExpr($2, @1); }
 
 			| a_expr LIKE a_expr
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_LIKE, "~~",
 												   $1, $3, @2);
 				}
-			| a_expr LIKE a_expr ESCAPE a_expr
+			| a_expr LIKE a_expr ESCAPE a_expr					%prec LIKE
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("like_escape"),
 											   list_make2($3, $5),
@@ -11224,12 +11240,12 @@ a_expr:		c_expr									{ $$ = $1; }
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_LIKE, "~~",
 												   $1, (Node *) n, @2);
 				}
-			| a_expr NOT LIKE a_expr
+			| a_expr NOT_LA LIKE a_expr							%prec NOT_LA
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_LIKE, "!~~",
 												   $1, $4, @2);
 				}
-			| a_expr NOT LIKE a_expr ESCAPE a_expr
+			| a_expr NOT_LA LIKE a_expr ESCAPE a_expr			%prec NOT_LA
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("like_escape"),
 											   list_make2($4, $6),
@@ -11242,7 +11258,7 @@ a_expr:		c_expr									{ $$ = $1; }
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_ILIKE, "~~*",
 												   $1, $3, @2);
 				}
-			| a_expr ILIKE a_expr ESCAPE a_expr
+			| a_expr ILIKE a_expr ESCAPE a_expr					%prec ILIKE
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("like_escape"),
 											   list_make2($3, $5),
@@ -11250,12 +11266,12 @@ a_expr:		c_expr									{ $$ = $1; }
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_ILIKE, "~~*",
 												   $1, (Node *) n, @2);
 				}
-			| a_expr NOT ILIKE a_expr
+			| a_expr NOT_LA ILIKE a_expr						%prec NOT_LA
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_ILIKE, "!~~*",
 												   $1, $4, @2);
 				}
-			| a_expr NOT ILIKE a_expr ESCAPE a_expr
+			| a_expr NOT_LA ILIKE a_expr ESCAPE a_expr			%prec NOT_LA
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("like_escape"),
 											   list_make2($4, $6),
@@ -11264,7 +11280,7 @@ a_expr:		c_expr									{ $$ = $1; }
 												   $1, (Node *) n, @2);
 				}
 
-			| a_expr SIMILAR TO a_expr				%prec SIMILAR
+			| a_expr SIMILAR TO a_expr							%prec SIMILAR
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("similar_escape"),
 											   list_make2($4, makeNullAConst(-1)),
@@ -11272,7 +11288,7 @@ a_expr:		c_expr									{ $$ = $1; }
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_SIMILAR, "~",
 												   $1, (Node *) n, @2);
 				}
-			| a_expr SIMILAR TO a_expr ESCAPE a_expr
+			| a_expr SIMILAR TO a_expr ESCAPE a_expr			%prec SIMILAR
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("similar_escape"),
 											   list_make2($4, $6),
@@ -11280,7 +11296,7 @@ a_expr:		c_expr									{ $$ = $1; }
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_SIMILAR, "~",
 												   $1, (Node *) n, @2);
 				}
-			| a_expr NOT SIMILAR TO a_expr			%prec SIMILAR
+			| a_expr NOT_LA SIMILAR TO a_expr					%prec NOT_LA
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("similar_escape"),
 											   list_make2($5, makeNullAConst(-1)),
@@ -11288,7 +11304,7 @@ a_expr:		c_expr									{ $$ = $1; }
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_SIMILAR, "!~",
 												   $1, (Node *) n, @2);
 				}
-			| a_expr NOT SIMILAR TO a_expr ESCAPE a_expr
+			| a_expr NOT_LA SIMILAR TO a_expr ESCAPE a_expr		%prec NOT_LA
 				{
 					FuncCall *n = makeFuncCall(SystemFuncName("similar_escape"),
 											   list_make2($5, $7),
@@ -11420,7 +11436,7 @@ a_expr:		c_expr									{ $$ = $1; }
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_OF, "<>", $1, (Node *) $6, @2);
 				}
-			| a_expr BETWEEN opt_asymmetric b_expr AND b_expr		%prec BETWEEN
+			| a_expr BETWEEN opt_asymmetric b_expr AND a_expr		%prec BETWEEN
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_BETWEEN,
 												   "BETWEEN",
@@ -11428,7 +11444,7 @@ a_expr:		c_expr									{ $$ = $1; }
 												   (Node *) list_make2($4, $6),
 												   @2);
 				}
-			| a_expr NOT BETWEEN opt_asymmetric b_expr AND b_expr	%prec BETWEEN
+			| a_expr NOT_LA BETWEEN opt_asymmetric b_expr AND a_expr %prec NOT_LA
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_NOT_BETWEEN,
 												   "NOT BETWEEN",
@@ -11436,7 +11452,7 @@ a_expr:		c_expr									{ $$ = $1; }
 												   (Node *) list_make2($5, $7),
 												   @2);
 				}
-			| a_expr BETWEEN SYMMETRIC b_expr AND b_expr			%prec BETWEEN
+			| a_expr BETWEEN SYMMETRIC b_expr AND a_expr			%prec BETWEEN
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_BETWEEN_SYM,
 												   "BETWEEN SYMMETRIC",
@@ -11444,7 +11460,7 @@ a_expr:		c_expr									{ $$ = $1; }
 												   (Node *) list_make2($4, $6),
 												   @2);
 				}
-			| a_expr NOT BETWEEN SYMMETRIC b_expr AND b_expr		%prec BETWEEN
+			| a_expr NOT_LA BETWEEN SYMMETRIC b_expr AND a_expr		%prec NOT_LA
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_NOT_BETWEEN_SYM,
 												   "NOT BETWEEN SYMMETRIC",
@@ -11472,7 +11488,7 @@ a_expr:		c_expr									{ $$ = $1; }
 						$$ = (Node *) makeSimpleA_Expr(AEXPR_IN, "=", $1, $3, @2);
 					}
 				}
-			| a_expr NOT IN_P in_expr
+			| a_expr NOT_LA IN_P in_expr						%prec NOT_LA
 				{
 					/* in_expr returns a SubLink or a list of a_exprs */
 					if (IsA($4, SubLink))
@@ -11576,6 +11592,12 @@ b_expr:		c_expr
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">", $1, $3, @2); }
 			| b_expr '=' b_expr
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "=", $1, $3, @2); }
+			| b_expr LESS_EQUALS b_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<=", $1, $3, @2); }
+			| b_expr GREATER_EQUALS b_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">=", $1, $3, @2); }
+			| b_expr NOT_EQUALS b_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<>", $1, $3, @2); }
 			| b_expr qual_Op b_expr				%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); }
 			| qual_Op b_expr					%prec Op
@@ -11647,6 +11669,24 @@ c_expr:		columnref								{ $$ = $1; }
 						n->indirection = check_indirection($4, yyscanner);
 						$$ = (Node *)n;
 					}
+					else if (operator_precedence_warning)
+					{
+						/*
+						 * If precedence warnings are enabled, insert
+						 * AEXPR_PAREN nodes wrapping all explicitly
+						 * parenthesized subexpressions; this prevents bogus
+						 * warnings from being issued when the ordering has
+						 * been forced by parentheses.
+						 *
+						 * In principle we should not be relying on a GUC to
+						 * decide whether to insert AEXPR_PAREN nodes.
+						 * However, since they have no effect except to
+						 * suppress warnings, it's probably safe enough; and
+						 * we'd just as soon not waste cycles on dummy parse
+						 * nodes if we don't have to.
+						 */
+						$$ = (Node *) makeA_Expr(AEXPR_PAREN, NIL, $2, NULL, @1);
+					}
 					else
 						$$ = $2;
 				}
@@ -12495,6 +12535,9 @@ MathOp:		 '+'									{ $$ = "+"; }
 			| '<'									{ $$ = "<"; }
 			| '>'									{ $$ = ">"; }
 			| '='									{ $$ = "="; }
+			| LESS_EQUALS							{ $$ = "<="; }
+			| GREATER_EQUALS						{ $$ = ">="; }
+			| NOT_EQUALS							{ $$ = "<>"; }
 		;
 
 qual_Op:	Op
@@ -12517,11 +12560,11 @@ subquery_Op:
 					{ $$ = $3; }
 			| LIKE
 					{ $$ = list_make1(makeString("~~")); }
-			| NOT LIKE
+			| NOT_LA LIKE
 					{ $$ = list_make1(makeString("!~~")); }
 			| ILIKE
 					{ $$ = list_make1(makeString("~~*")); }
-			| NOT ILIKE
+			| NOT_LA ILIKE
 					{ $$ = list_make1(makeString("!~~*")); }
 /* cannot put SIMILAR TO here, because SIMILAR TO is a hack.
  * the regular expression is preprocessed by a function (similar_escape),
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 130e52b26c4f20fe909811baa70d5a854f12792c..f759606f88bc9c53a5527c60cdcb3d75fbc5cc10 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -37,8 +37,55 @@
 #include "utils/xml.h"
 
 
+/* GUC parameters */
+bool		operator_precedence_warning = false;
 bool		Transform_null_equals = false;
 
+/*
+ * Node-type groups for operator precedence warnings
+ * We use zero for everything not otherwise classified
+ */
+#define PREC_GROUP_POSTFIX_IS	1		/* postfix IS tests (NullTest, etc) */
+#define PREC_GROUP_INFIX_IS		2		/* infix IS (IS DISTINCT FROM, etc) */
+#define PREC_GROUP_LESS			3		/* < > */
+#define PREC_GROUP_EQUAL		4		/* = */
+#define PREC_GROUP_LESS_EQUAL	5		/* <= >= <> */
+#define PREC_GROUP_LIKE			6		/* LIKE ILIKE SIMILAR */
+#define PREC_GROUP_BETWEEN		7		/* BETWEEN */
+#define PREC_GROUP_IN			8		/* IN */
+#define PREC_GROUP_NOT_LIKE		9		/* NOT LIKE/ILIKE/SIMILAR */
+#define PREC_GROUP_NOT_BETWEEN	10		/* NOT BETWEEN */
+#define PREC_GROUP_NOT_IN		11		/* NOT IN */
+#define PREC_GROUP_POSTFIX_OP	12		/* generic postfix operators */
+#define PREC_GROUP_INFIX_OP		13		/* generic infix operators */
+#define PREC_GROUP_PREFIX_OP	14		/* generic prefix operators */
+
+/*
+ * Map precedence groupings to old precedence ordering
+ *
+ * Old precedence order:
+ * 1. NOT
+ * 2. =
+ * 3. < >
+ * 4. LIKE ILIKE SIMILAR
+ * 5. BETWEEN
+ * 6. IN
+ * 7. generic postfix Op
+ * 8. generic Op, including <= => <>
+ * 9. generic prefix Op
+ * 10. IS tests (NullTest, BooleanTest, etc)
+ *
+ * NOT BETWEEN etc map to BETWEEN etc when considered as being on the left,
+ * but to NOT when considered as being on the right, because of the buggy
+ * precedence handling of those productions in the old grammar.
+ */
+static const int oldprecedence_l[] = {
+	0, 10, 10, 3, 2, 8, 4, 5, 6, 4, 5, 6, 7, 8, 9
+};
+static const int oldprecedence_r[] = {
+	0, 10, 10, 3, 2, 8, 4, 5, 6, 1, 1, 1, 7, 8, 9
+};
+
 static Node *transformExprRecurse(ParseState *pstate, Node *expr);
 static Node *transformParamRef(ParseState *pstate, ParamRef *pref);
 static Node *transformAExprOp(ParseState *pstate, A_Expr *a);
@@ -76,6 +123,11 @@ static Node *make_row_distinct_op(ParseState *pstate, List *opname,
 					 RowExpr *lrow, RowExpr *rrow, int location);
 static Expr *make_distinct_op(ParseState *pstate, List *opname,
 				 Node *ltree, Node *rtree, int location);
+static int	operator_precedence_group(Node *node, const char **nodename);
+static void emit_precedence_warnings(ParseState *pstate,
+						 int opgroup, const char *opname,
+						 Node *lchild, Node *rchild,
+						 int location);
 
 
 /*
@@ -194,6 +246,9 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 					case AEXPR_NOT_BETWEEN_SYM:
 						result = transformAExprBetween(pstate, a);
 						break;
+					case AEXPR_PAREN:
+						result = transformExprRecurse(pstate, a->lexpr);
+						break;
 					default:
 						elog(ERROR, "unrecognized A_Expr kind: %d", a->kind);
 						result = NULL;	/* keep compiler quiet */
@@ -255,6 +310,11 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 			{
 				NullTest   *n = (NullTest *) expr;
 
+				if (operator_precedence_warning)
+					emit_precedence_warnings(pstate, PREC_GROUP_POSTFIX_IS, "IS",
+											 (Node *) n->arg, NULL,
+											 n->location);
+
 				n->arg = (Expr *) transformExprRecurse(pstate, (Node *) n->arg);
 				/* the argument can be any type, so don't coerce it */
 				n->argisrow = type_is_rowtype(exprType((Node *) n->arg));
@@ -782,6 +842,18 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	Node	   *rexpr = a->rexpr;
 	Node	   *result;
 
+	if (operator_precedence_warning)
+	{
+		int			opgroup;
+		const char *opname;
+
+		opgroup = operator_precedence_group((Node *) a, &opname);
+		if (opgroup > 0)
+			emit_precedence_warnings(pstate, opgroup, opname,
+									 lexpr, rexpr,
+									 a->location);
+	}
+
 	/*
 	 * Special-case "foo = NULL" and "NULL = foo" for compatibility with
 	 * standards-broken products (like Microsoft's).  Turn these into IS NULL
@@ -858,8 +930,17 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 static Node *
 transformAExprOpAny(ParseState *pstate, A_Expr *a)
 {
-	Node	   *lexpr = transformExprRecurse(pstate, a->lexpr);
-	Node	   *rexpr = transformExprRecurse(pstate, a->rexpr);
+	Node	   *lexpr = a->lexpr;
+	Node	   *rexpr = a->rexpr;
+
+	if (operator_precedence_warning)
+		emit_precedence_warnings(pstate, PREC_GROUP_POSTFIX_OP,
+								 strVal(llast(a->name)),
+								 lexpr, NULL,
+								 a->location);
+
+	lexpr = transformExprRecurse(pstate, lexpr);
+	rexpr = transformExprRecurse(pstate, rexpr);
 
 	return (Node *) make_scalar_array_op(pstate,
 										 a->name,
@@ -872,8 +953,17 @@ transformAExprOpAny(ParseState *pstate, A_Expr *a)
 static Node *
 transformAExprOpAll(ParseState *pstate, A_Expr *a)
 {
-	Node	   *lexpr = transformExprRecurse(pstate, a->lexpr);
-	Node	   *rexpr = transformExprRecurse(pstate, a->rexpr);
+	Node	   *lexpr = a->lexpr;
+	Node	   *rexpr = a->rexpr;
+
+	if (operator_precedence_warning)
+		emit_precedence_warnings(pstate, PREC_GROUP_POSTFIX_OP,
+								 strVal(llast(a->name)),
+								 lexpr, NULL,
+								 a->location);
+
+	lexpr = transformExprRecurse(pstate, lexpr);
+	rexpr = transformExprRecurse(pstate, rexpr);
 
 	return (Node *) make_scalar_array_op(pstate,
 										 a->name,
@@ -886,8 +976,16 @@ transformAExprOpAll(ParseState *pstate, A_Expr *a)
 static Node *
 transformAExprDistinct(ParseState *pstate, A_Expr *a)
 {
-	Node	   *lexpr = transformExprRecurse(pstate, a->lexpr);
-	Node	   *rexpr = transformExprRecurse(pstate, a->rexpr);
+	Node	   *lexpr = a->lexpr;
+	Node	   *rexpr = a->rexpr;
+
+	if (operator_precedence_warning)
+		emit_precedence_warnings(pstate, PREC_GROUP_INFIX_IS, "IS",
+								 lexpr, rexpr,
+								 a->location);
+
+	lexpr = transformExprRecurse(pstate, lexpr);
+	rexpr = transformExprRecurse(pstate, rexpr);
 
 	if (lexpr && IsA(lexpr, RowExpr) &&
 		rexpr && IsA(rexpr, RowExpr))
@@ -944,20 +1042,27 @@ transformAExprNullIf(ParseState *pstate, A_Expr *a)
 	return (Node *) result;
 }
 
+/*
+ * Checking an expression for match to a list of type names. Will result
+ * in a boolean constant node.
+ */
 static Node *
 transformAExprOf(ParseState *pstate, A_Expr *a)
 {
-	/*
-	 * Checking an expression for match to a list of type names. Will result
-	 * in a boolean constant node.
-	 */
-	Node	   *lexpr = transformExprRecurse(pstate, a->lexpr);
+	Node	   *lexpr = a->lexpr;
 	Const	   *result;
 	ListCell   *telem;
 	Oid			ltype,
 				rtype;
 	bool		matched = false;
 
+	if (operator_precedence_warning)
+		emit_precedence_warnings(pstate, PREC_GROUP_POSTFIX_IS, "IS",
+								 lexpr, NULL,
+								 a->location);
+
+	lexpr = transformExprRecurse(pstate, lexpr);
+
 	ltype = exprType(lexpr);
 	foreach(telem, (List *) a->rexpr)
 	{
@@ -1001,6 +1106,13 @@ transformAExprIn(ParseState *pstate, A_Expr *a)
 	else
 		useOr = true;
 
+	if (operator_precedence_warning)
+		emit_precedence_warnings(pstate,
+								 useOr ? PREC_GROUP_IN : PREC_GROUP_NOT_IN,
+								 "IN",
+								 a->lexpr, NULL,
+								 a->location);
+
 	/*
 	 * We try to generate a ScalarArrayOpExpr from IN/NOT IN, but this is only
 	 * possible if there is a suitable array type available.  If not, we fall
@@ -1153,6 +1265,22 @@ transformAExprBetween(ParseState *pstate, A_Expr *a)
 	bexpr = (Node *) linitial(args);
 	cexpr = (Node *) lsecond(args);
 
+	if (operator_precedence_warning)
+	{
+		int			opgroup;
+		const char *opname;
+
+		opgroup = operator_precedence_group((Node *) a, &opname);
+		emit_precedence_warnings(pstate, opgroup, opname,
+								 aexpr, cexpr,
+								 a->location);
+		/* We can ignore bexpr thanks to syntactic restrictions */
+		/* Wrap subexpressions to prevent extra warnings */
+		aexpr = (Node *) makeA_Expr(AEXPR_PAREN, NIL, aexpr, NULL, -1);
+		bexpr = (Node *) makeA_Expr(AEXPR_PAREN, NIL, bexpr, NULL, -1);
+		cexpr = (Node *) makeA_Expr(AEXPR_PAREN, NIL, cexpr, NULL, -1);
+	}
+
 	/*
 	 * Build the equivalent comparison expression.  Make copies of
 	 * multiply-referenced subexpressions for safety.  (XXX this is really
@@ -1657,6 +1785,19 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		List	   *right_list;
 		ListCell   *l;
 
+		if (operator_precedence_warning)
+		{
+			if (sublink->operName == NIL)
+				emit_precedence_warnings(pstate, PREC_GROUP_IN, "IN",
+										 sublink->testexpr, NULL,
+										 sublink->location);
+			else
+				emit_precedence_warnings(pstate, PREC_GROUP_POSTFIX_OP,
+										 strVal(llast(sublink->operName)),
+										 sublink->testexpr, NULL,
+										 sublink->location);
+		}
+
 		/*
 		 * If the source was "x IN (select)", convert to "x = ANY (select)".
 		 */
@@ -2000,6 +2141,11 @@ transformXmlExpr(ParseState *pstate, XmlExpr *x)
 	ListCell   *lc;
 	int			i;
 
+	if (operator_precedence_warning && x->op == IS_DOCUMENT)
+		emit_precedence_warnings(pstate, PREC_GROUP_POSTFIX_IS, "IS",
+								 (Node *) linitial(x->args), NULL,
+								 x->location);
+
 	newx = makeNode(XmlExpr);
 	newx->op = x->op;
 	if (x->name)
@@ -2172,6 +2318,11 @@ transformBooleanTest(ParseState *pstate, BooleanTest *b)
 {
 	const char *clausename;
 
+	if (operator_precedence_warning)
+		emit_precedence_warnings(pstate, PREC_GROUP_POSTFIX_IS, "IS",
+								 (Node *) b->arg, NULL,
+								 b->location);
+
 	switch (b->booltesttype)
 	{
 		case IS_TRUE:
@@ -2688,6 +2839,309 @@ make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree,
 	return result;
 }
 
+/*
+ * Identify node's group for operator precedence warnings
+ *
+ * For items in nonzero groups, also return a suitable node name into *nodename
+ *
+ * Note: group zero is used for nodes that are higher or lower precedence
+ * than everything that changed precedence; we need never issue warnings
+ * related to such nodes.
+ */
+static int
+operator_precedence_group(Node *node, const char **nodename)
+{
+	int			group = 0;
+
+	*nodename = NULL;
+	if (node == NULL)
+		return 0;
+
+	if (IsA(node, A_Expr))
+	{
+		A_Expr	   *aexpr = (A_Expr *) node;
+
+		if (aexpr->kind == AEXPR_OP &&
+			aexpr->lexpr != NULL &&
+			aexpr->rexpr != NULL)
+		{
+			/* binary operator */
+			if (list_length(aexpr->name) == 1)
+			{
+				*nodename = strVal(linitial(aexpr->name));
+				/* Ignore if op was always higher priority than IS-tests */
+				if (strcmp(*nodename, "+") == 0 ||
+					strcmp(*nodename, "-") == 0 ||
+					strcmp(*nodename, "*") == 0 ||
+					strcmp(*nodename, "/") == 0 ||
+					strcmp(*nodename, "%") == 0 ||
+					strcmp(*nodename, "^") == 0)
+					group = 0;
+				else if (strcmp(*nodename, "<") == 0 ||
+						 strcmp(*nodename, ">") == 0)
+					group = PREC_GROUP_LESS;
+				else if (strcmp(*nodename, "=") == 0)
+					group = PREC_GROUP_EQUAL;
+				else if (strcmp(*nodename, "<=") == 0 ||
+						 strcmp(*nodename, ">=") == 0 ||
+						 strcmp(*nodename, "<>") == 0)
+					group = PREC_GROUP_LESS_EQUAL;
+				else
+					group = PREC_GROUP_INFIX_OP;
+			}
+			else
+			{
+				/* schema-qualified operator syntax */
+				*nodename = "OPERATOR()";
+				group = PREC_GROUP_INFIX_OP;
+			}
+		}
+		else if (aexpr->kind == AEXPR_OP &&
+				 aexpr->lexpr == NULL &&
+				 aexpr->rexpr != NULL)
+		{
+			/* prefix operator */
+			if (list_length(aexpr->name) == 1)
+			{
+				*nodename = strVal(linitial(aexpr->name));
+				/* Ignore if op was always higher priority than IS-tests */
+				if (strcmp(*nodename, "+") == 0 ||
+					strcmp(*nodename, "-"))
+					group = 0;
+				else
+					group = PREC_GROUP_PREFIX_OP;
+			}
+			else
+			{
+				/* schema-qualified operator syntax */
+				*nodename = "OPERATOR()";
+				group = PREC_GROUP_PREFIX_OP;
+			}
+		}
+		else if (aexpr->kind == AEXPR_OP &&
+				 aexpr->lexpr != NULL &&
+				 aexpr->rexpr == NULL)
+		{
+			/* postfix operator */
+			if (list_length(aexpr->name) == 1)
+			{
+				*nodename = strVal(linitial(aexpr->name));
+				group = PREC_GROUP_POSTFIX_OP;
+			}
+			else
+			{
+				/* schema-qualified operator syntax */
+				*nodename = "OPERATOR()";
+				group = PREC_GROUP_POSTFIX_OP;
+			}
+		}
+		else if (aexpr->kind == AEXPR_OP_ANY ||
+				 aexpr->kind == AEXPR_OP_ALL)
+		{
+			*nodename = strVal(llast(aexpr->name));
+			group = PREC_GROUP_POSTFIX_OP;
+		}
+		else if (aexpr->kind == AEXPR_DISTINCT)
+		{
+			*nodename = "IS";
+			group = PREC_GROUP_INFIX_IS;
+		}
+		else if (aexpr->kind == AEXPR_OF)
+		{
+			*nodename = "IS";
+			group = PREC_GROUP_POSTFIX_IS;
+		}
+		else if (aexpr->kind == AEXPR_IN)
+		{
+			*nodename = "IN";
+			if (strcmp(strVal(linitial(aexpr->name)), "=") == 0)
+				group = PREC_GROUP_IN;
+			else
+				group = PREC_GROUP_NOT_IN;
+		}
+		else if (aexpr->kind == AEXPR_LIKE)
+		{
+			*nodename = "LIKE";
+			if (strcmp(strVal(linitial(aexpr->name)), "~~") == 0)
+				group = PREC_GROUP_LIKE;
+			else
+				group = PREC_GROUP_NOT_LIKE;
+		}
+		else if (aexpr->kind == AEXPR_ILIKE)
+		{
+			*nodename = "ILIKE";
+			if (strcmp(strVal(linitial(aexpr->name)), "~~*") == 0)
+				group = PREC_GROUP_LIKE;
+			else
+				group = PREC_GROUP_NOT_LIKE;
+		}
+		else if (aexpr->kind == AEXPR_SIMILAR)
+		{
+			*nodename = "SIMILAR";
+			if (strcmp(strVal(linitial(aexpr->name)), "~") == 0)
+				group = PREC_GROUP_LIKE;
+			else
+				group = PREC_GROUP_NOT_LIKE;
+		}
+		else if (aexpr->kind == AEXPR_BETWEEN ||
+				 aexpr->kind == AEXPR_BETWEEN_SYM)
+		{
+			Assert(list_length(aexpr->name) == 1);
+			*nodename = strVal(linitial(aexpr->name));
+			group = PREC_GROUP_BETWEEN;
+		}
+		else if (aexpr->kind == AEXPR_NOT_BETWEEN ||
+				 aexpr->kind == AEXPR_NOT_BETWEEN_SYM)
+		{
+			Assert(list_length(aexpr->name) == 1);
+			*nodename = strVal(linitial(aexpr->name));
+			group = PREC_GROUP_NOT_BETWEEN;
+		}
+	}
+	else if (IsA(node, NullTest) ||
+			 IsA(node, BooleanTest))
+	{
+		*nodename = "IS";
+		group = PREC_GROUP_POSTFIX_IS;
+	}
+	else if (IsA(node, XmlExpr))
+	{
+		XmlExpr    *x = (XmlExpr *) node;
+
+		if (x->op == IS_DOCUMENT)
+		{
+			*nodename = "IS";
+			group = PREC_GROUP_POSTFIX_IS;
+		}
+	}
+	else if (IsA(node, SubLink))
+	{
+		SubLink    *s = (SubLink *) node;
+
+		if (s->subLinkType == ANY_SUBLINK ||
+			s->subLinkType == ALL_SUBLINK)
+		{
+			if (s->operName == NIL)
+			{
+				*nodename = "IN";
+				group = PREC_GROUP_IN;
+			}
+			else
+			{
+				*nodename = strVal(llast(s->operName));
+				group = PREC_GROUP_POSTFIX_OP;
+			}
+		}
+	}
+	else if (IsA(node, BoolExpr))
+	{
+		/*
+		 * Must dig into NOTs to see if it's IS NOT DOCUMENT or NOT IN.  This
+		 * opens us to possibly misrecognizing, eg, NOT (x IS DOCUMENT) as a
+		 * problematic construct.  We can tell the difference by checking
+		 * whether the parse locations of the two nodes are identical.
+		 *
+		 * Note that when we are comparing the child node to its own children,
+		 * we will not know that it was a NOT.  Fortunately, that doesn't
+		 * matter for these cases.
+		 */
+		BoolExpr   *b = (BoolExpr *) node;
+
+		if (b->boolop == NOT_EXPR)
+		{
+			Node	   *child = (Node *) linitial(b->args);
+
+			if (IsA(child, XmlExpr))
+			{
+				XmlExpr    *x = (XmlExpr *) child;
+
+				if (x->op == IS_DOCUMENT &&
+					x->location == b->location)
+				{
+					*nodename = "IS";
+					group = PREC_GROUP_POSTFIX_IS;
+				}
+			}
+			else if (IsA(child, SubLink))
+			{
+				SubLink    *s = (SubLink *) child;
+
+				if (s->subLinkType == ANY_SUBLINK && s->operName == NIL &&
+					s->location == b->location)
+				{
+					*nodename = "IN";
+					group = PREC_GROUP_NOT_IN;
+				}
+			}
+		}
+	}
+	return group;
+}
+
+/*
+ * helper routine for delivering 9.4-to-9.5 operator precedence warnings
+ *
+ * opgroup/opname/location represent some parent node
+ * lchild, rchild are its left and right children (either could be NULL)
+ *
+ * This should be called before transforming the child nodes, since if a
+ * precedence-driven parsing change has occurred in a query that used to work,
+ * it's quite possible that we'll get a semantic failure while analyzing the
+ * child expression.  We want to produce the warning before that happens.
+ * In any case, operator_precedence_group() expects untransformed input.
+ */
+static void
+emit_precedence_warnings(ParseState *pstate,
+						 int opgroup, const char *opname,
+						 Node *lchild, Node *rchild,
+						 int location)
+{
+	int			cgroup;
+	const char *copname;
+
+	Assert(opgroup > 0);
+
+	/*
+	 * Complain if left child, which should be same or higher precedence
+	 * according to current rules, used to be lower precedence.
+	 *
+	 * Exception to precedence rules: if left child is IN or NOT IN or a
+	 * postfix operator, the grouping is syntactically forced regardless of
+	 * precedence.
+	 */
+	cgroup = operator_precedence_group(lchild, &copname);
+	if (cgroup > 0)
+	{
+		if (oldprecedence_l[cgroup] < oldprecedence_r[opgroup] &&
+			cgroup != PREC_GROUP_IN &&
+			cgroup != PREC_GROUP_NOT_IN &&
+			cgroup != PREC_GROUP_POSTFIX_OP &&
+			cgroup != PREC_GROUP_POSTFIX_IS)
+			ereport(WARNING,
+					(errmsg("operator precedence change: %s is now lower precedence than %s",
+							opname, copname),
+					 parser_errposition(pstate, location)));
+	}
+
+	/*
+	 * Complain if right child, which should be higher precedence according to
+	 * current rules, used to be same or lower precedence.
+	 *
+	 * Exception to precedence rules: if right child is a prefix operator, the
+	 * grouping is syntactically forced regardless of precedence.
+	 */
+	cgroup = operator_precedence_group(rchild, &copname);
+	if (cgroup > 0)
+	{
+		if (oldprecedence_r[cgroup] <= oldprecedence_l[opgroup] &&
+			cgroup != PREC_GROUP_PREFIX_OP)
+			ereport(WARNING,
+					(errmsg("operator precedence change: %s is now lower precedence than %s",
+							opname, copname),
+					 parser_errposition(pstate, location)));
+	}
+}
+
 /*
  * Produce a string identifying an expression by kind.
  *
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 3724330dc842561a1162a10c9f365ee99120b5ac..2d85cf08e706b374fcd835347d162fd41bc809f5 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1654,12 +1654,17 @@ FigureColnameInternal(Node *node, char **name)
 			*name = strVal(llast(((FuncCall *) node)->funcname));
 			return 2;
 		case T_A_Expr:
-			/* make nullif() act like a regular function */
 			if (((A_Expr *) node)->kind == AEXPR_NULLIF)
 			{
+				/* make nullif() act like a regular function */
 				*name = "nullif";
 				return 2;
 			}
+			if (((A_Expr *) node)->kind == AEXPR_PAREN)
+			{
+				/* look through dummy parenthesis node */
+				return FigureColnameInternal(((A_Expr *) node)->lexpr, name);
+			}
 			break;
 		case T_TypeCast:
 			strength = FigureColnameInternal(((TypeCast *) node)->arg,
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index b17771d4cca2ee532e6b519dab7ba92324b9e6ea..fdf5a6a1cafc6f5559a9ce2438cbc45a36ec0e60 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -107,6 +107,9 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
 	 */
 	switch (cur_token)
 	{
+		case NOT:
+			cur_token_length = 3;
+			break;
 		case NULLS_P:
 			cur_token_length = 5;
 			break;
@@ -151,6 +154,20 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
 	/* Replace cur_token if needed, based on lookahead */
 	switch (cur_token)
 	{
+		case NOT:
+			/* Replace NOT by NOT_LA if it's followed by BETWEEN, IN, etc */
+			switch (next_token)
+			{
+				case BETWEEN:
+				case IN_P:
+				case LIKE:
+				case ILIKE:
+				case SIMILAR:
+					cur_token = NOT_LA;
+					break;
+			}
+			break;
+
 		case NULLS_P:
 			/* Replace NULLS_P by NULLS_LA if it's followed by FIRST or LAST */
 			switch (next_token)
diff --git a/src/backend/parser/scan.l b/src/backend/parser/scan.l
index a0e20434ef392c1fd68b3926c1eb42186e8854a4..82b20c6e5f695054acbebf7e6f40a169fd7c5597 100644
--- a/src/backend/parser/scan.l
+++ b/src/backend/parser/scan.l
@@ -331,10 +331,15 @@ ident_cont		[A-Za-z\200-\377_0-9\$]
 
 identifier		{ident_start}{ident_cont}*
 
+/* Assorted special-case operators and operator-like tokens */
 typecast		"::"
 dot_dot			\.\.
 colon_equals	":="
 equals_greater	"=>"
+less_equals		"<="
+greater_equals	">="
+less_greater	"<>"
+not_equals		"!="
 
 /*
  * "self" is the set of chars that should be returned as single-character
@@ -814,6 +819,28 @@ other			.
 					return EQUALS_GREATER;
 				}
 
+{less_equals}	{
+					SET_YYLLOC();
+					return LESS_EQUALS;
+				}
+
+{greater_equals} {
+					SET_YYLLOC();
+					return GREATER_EQUALS;
+				}
+
+{less_greater}	{
+					/* We accept both "<>" and "!=" as meaning NOT_EQUALS */
+					SET_YYLLOC();
+					return NOT_EQUALS;
+				}
+
+{not_equals}	{
+					/* We accept both "<>" and "!=" as meaning NOT_EQUALS */
+					SET_YYLLOC();
+					return NOT_EQUALS;
+				}
+
 {self}			{
 					SET_YYLLOC();
 					return yytext[0];
@@ -891,11 +918,7 @@ other			.
 					if (nchars >= NAMEDATALEN)
 						yyerror("operator too long");
 
-					/* Convert "!=" operator to "<>" for compatibility */
-					if (strcmp(yytext, "!=") == 0)
-						yylval->str = pstrdup("<>");
-					else
-						yylval->str = pstrdup(yytext);
+					yylval->str = pstrdup(yytext);
 					return Op;
 				}
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 6eaab4352aa53febf7b813bdc12af2c5b1b08951..7196b0b2157b2bcf8de50e5e1196effd94bf46de 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1599,6 +1599,16 @@ static struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"operator_precedence_warning", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS,
+			gettext_noop("Emit a warning for constructs that changed meaning since PostgreSQL 9.4."),
+			NULL,
+		},
+		&operator_precedence_warning,
+		false,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"quote_all_identifiers", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS,
 			gettext_noop("When generating SQL fragments, quote all identifiers."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 7590a6f056d9ae7b76266925ec313a4e108fe731..d7a61f1fc4b8da58e424cd9baa0838f93e7ba0cc 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -586,6 +586,7 @@
 #default_with_oids = off
 #escape_string_warning = on
 #lo_compat_privileges = off
+#operator_precedence_warning = off
 #quote_all_identifiers = off
 #sql_inheritance = on
 #standard_conforming_strings = on
diff --git a/src/bin/psql/psqlscan.l b/src/bin/psql/psqlscan.l
index 2b2dec95be72a7222230e37f7b2ee14900a76bd9..bb134a42d8a983e40a6a2e929703c6aba90aea71 100644
--- a/src/bin/psql/psqlscan.l
+++ b/src/bin/psql/psqlscan.l
@@ -355,10 +355,15 @@ ident_cont		[A-Za-z\200-\377_0-9\$]
 
 identifier		{ident_start}{ident_cont}*
 
+/* Assorted special-case operators and operator-like tokens */
 typecast		"::"
 dot_dot			\.\.
 colon_equals	":="
 equals_greater	"=>"
+less_equals		"<="
+greater_equals	">="
+less_greater	"<>"
+not_equals		"!="
 
 /*
  * "self" is the set of chars that should be returned as single-character
@@ -674,6 +679,22 @@ other			.
 					ECHO;
 				}
 
+{less_equals}	{
+					ECHO;
+				}
+
+{greater_equals} {
+					ECHO;
+				}
+
+{less_greater}	{
+					ECHO;
+				}
+
+{not_equals}	{
+					ECHO;
+				}
+
 	/*
 	 * These rules are specific to psql --- they implement parenthesis
 	 * counting and detection of command-ending semicolon.  These must
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 497559df5887982b12386d37881ed3b6045e0bff..c226b039cf1c023ebec09d87e083833b13b80726 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -239,7 +239,8 @@ typedef enum A_Expr_Kind
 	AEXPR_BETWEEN,				/* name must be "BETWEEN" */
 	AEXPR_NOT_BETWEEN,			/* name must be "NOT BETWEEN" */
 	AEXPR_BETWEEN_SYM,			/* name must be "BETWEEN SYMMETRIC" */
-	AEXPR_NOT_BETWEEN_SYM		/* name must be "NOT BETWEEN SYMMETRIC" */
+	AEXPR_NOT_BETWEEN_SYM,		/* name must be "NOT BETWEEN SYMMETRIC" */
+	AEXPR_PAREN					/* nameless dummy node for parentheses */
 } A_Expr_Kind;
 
 typedef struct A_Expr
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index 66391dfc74d1eb58c653f09e2324e763932a51a3..fbc3f17c688e93ce1bc467d59566cf49f4a0388f 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -16,6 +16,7 @@
 #include "parser/parse_node.h"
 
 /* GUC parameters */
+extern bool operator_precedence_warning;
 extern bool Transform_null_equals;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
diff --git a/src/include/parser/scanner.h b/src/include/parser/scanner.h
index 0e22d031f140d6a7472d33c8b03a17b02de82423..f941977865ae4c9bef86037427915cc19198f2c9 100644
--- a/src/include/parser/scanner.h
+++ b/src/include/parser/scanner.h
@@ -51,6 +51,7 @@ typedef union core_YYSTYPE
  *	%token <str>	IDENT FCONST SCONST BCONST XCONST Op
  *	%token <ival>	ICONST PARAM
  *	%token			TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER
+ *	%token			LESS_EQUALS GREATER_EQUALS NOT_EQUALS
  * The above token definitions *must* be the first ones declared in any
  * bison parser built atop this scanner, so that they will have consistent
  * numbers assigned to them (specifically, IDENT = 258 and so on).
diff --git a/src/interfaces/ecpg/preproc/parse.pl b/src/interfaces/ecpg/preproc/parse.pl
index 7ae7acc6130193604a4794ff3f4b017c70f7f60a..588bb63e53ff1f914cee5658d0963b7d494c3471 100644
--- a/src/interfaces/ecpg/preproc/parse.pl
+++ b/src/interfaces/ecpg/preproc/parse.pl
@@ -42,12 +42,17 @@ my %replace_token = (
 
 # or in the block
 my %replace_string = (
+	'NOT_LA'          => 'not',
 	'NULLS_LA'        => 'nulls',
 	'WITH_LA'         => 'with',
 	'TYPECAST'        => '::',
 	'DOT_DOT'         => '..',
 	'COLON_EQUALS'    => ':=',
-	'EQUALS_GREATER'  => '=>',);
+	'EQUALS_GREATER'  => '=>',
+	'LESS_EQUALS'     => '<=',
+	'GREATER_EQUALS'  => '>=',
+	'NOT_EQUALS'      => '<>',
+);
 
 # specific replace_types for specific non-terminals - never include the ':'
 # ECPG-only replace_types are defined in ecpg-replace_types
diff --git a/src/interfaces/ecpg/preproc/parser.c b/src/interfaces/ecpg/preproc/parser.c
index 099a213d11872f01796d1f83621996c2dc2f4ea6..662a90a3f6bd311b3b8fe996e12401f4dc40b58b 100644
--- a/src/interfaces/ecpg/preproc/parser.c
+++ b/src/interfaces/ecpg/preproc/parser.c
@@ -75,6 +75,9 @@ filtered_base_yylex(void)
 	 */
 	switch (cur_token)
 	{
+		case NOT:
+			cur_token_length = 3;
+			break;
 		case NULLS_P:
 			cur_token_length = 5;
 			break;
@@ -119,6 +122,20 @@ filtered_base_yylex(void)
 	/* Replace cur_token if needed, based on lookahead */
 	switch (cur_token)
 	{
+		case NOT:
+			/* Replace NOT by NOT_LA if it's followed by BETWEEN, IN, etc */
+			switch (next_token)
+			{
+				case BETWEEN:
+				case IN_P:
+				case LIKE:
+				case ILIKE:
+				case SIMILAR:
+					cur_token = NOT_LA;
+					break;
+			}
+			break;
+
 		case NULLS_P:
 			/* Replace NULLS_P by NULLS_LA if it's followed by FIRST or LAST */
 			switch (next_token)
diff --git a/src/interfaces/ecpg/preproc/pgc.l b/src/interfaces/ecpg/preproc/pgc.l
index a8cc3d877c091f33bc14624a3ad7190f4b59692b..c70f2986962d6323b8ae197e8a21f1985649cacc 100644
--- a/src/interfaces/ecpg/preproc/pgc.l
+++ b/src/interfaces/ecpg/preproc/pgc.l
@@ -233,10 +233,16 @@ ident_cont		[A-Za-z\200-\377_0-9\$]
 identifier		{ident_start}{ident_cont}*
 
 array			({ident_cont}|{whitespace}|[\[\]\+\-\*\%\/\(\)\>\.])*
+
+/* Assorted special-case operators and operator-like tokens */
 typecast		"::"
 dot_dot			\.\.
 colon_equals	":="
 equals_greater	"=>"
+less_equals		"<="
+greater_equals	">="
+less_greater	"<>"
+not_equals		"!="
 
 /*
  * "self" is the set of chars that should be returned as single-character
@@ -622,6 +628,10 @@ cppline			{space}*#([^i][A-Za-z]*|{if}|{ifdef}|{ifndef}|{import})(.*\\{space})*.
 <SQL>{dot_dot}		{ return DOT_DOT; }
 <SQL>{colon_equals}	{ return COLON_EQUALS; }
 <SQL>{equals_greater} { return EQUALS_GREATER; }
+<SQL>{less_equals}	{ return LESS_EQUALS; }
+<SQL>{greater_equals} { return GREATER_EQUALS; }
+<SQL>{less_greater}	{ return NOT_EQUALS; }
+<SQL>{not_equals}	{ return NOT_EQUALS; }
 <SQL>{informix_special}	{
 			  /* are we simulating Informix? */
 				if (INFORMIX_MODE)
@@ -701,11 +711,7 @@ cppline			{space}*#([^i][A-Za-z]*|{if}|{ifdef}|{ifndef}|{import})(.*\\{space})*.
 								return yytext[0];
 						}
 
-						/* Convert "!=" operator to "<>" for compatibility */
-						if (strcmp(yytext, "!=") == 0)
-							yylval.str = mm_strdup("<>");
-						else
-							yylval.str = mm_strdup(yytext);
+						yylval.str = mm_strdup(yytext);
 						return Op;
 					}
 <SQL>{param}		{
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index a1758e03c3f0398dcae8d7e38ddc4bef9c6e234f..46217fd64bd7a2c109d7190152e53b69ad90b2b5 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -227,6 +227,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %token <str>	IDENT FCONST SCONST BCONST XCONST Op
 %token <ival>	ICONST PARAM
 %token			TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER
+%token			LESS_EQUALS GREATER_EQUALS NOT_EQUALS
 
 /*
  * Other tokens recognized by plpgsql's lexer interface layer (pl_scanner.c).