Tom Lane писал 2022-01-18 23:01:
I wrote:
Alexander Pyhalov <a.pyha...@postgrespro.ru> writes:
This means we'll translate something like
explain select * from t where d > now() - '1 day'::interval;
to
select * from t where d > $1;

Right.

After thinking about that a bit more, I see that this will result
in a major redefinition of what is "shippable".  Right now, we do not
consider the above WHERE clause to be shippable, not only because of
now() but because the timestamptz-minus-interval operator is dependent
on the timezone setting, which might be different at the remote.
But if we evaluate that operator locally and send its result as a
parameter, the objection vanishes.  In fact, I don't think we even
need to require the subexpression to contain only built-in functions.
Its result still has to be of a built-in type, but that's a much
weaker restriction.


Hi.
So far I have the following prototype. It seems to be working, but I think it can be enhanced. At least, some sort of caching seems to be necessary for is_stable_expr().

1) Now expression can be either 'stable shippable' or 'shippable according to old rules'. We check if it's 'stable shippable' in foreign_expr_walker(), is_foreign_param() and deparseExpr(). All such exprs are replaced by params while deparsing. 2) contain_mutable_functions() now is calculated only for current node, if node is not considered 'stable shippable'.

Is it step in the right direction or do I miss something?
--
Best regards,
Alexander Pyhalov,
Postgres Professional
From 60e6e0bf98326cb557c70a365797026e9925b7a3 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <a.pyha...@postgrespro.ru>
Date: Thu, 29 Jul 2021 11:45:28 +0300
Subject: [PATCH] Push down stable expressions

Stable expressions can be computed locally and sent to remote side as parameters values.
---
 contrib/postgres_fdw/deparse.c                | 1162 +++++++++--------
 .../postgres_fdw/expected/postgres_fdw.out    |   87 ++
 contrib/postgres_fdw/postgres_fdw.c           |    9 +-
 contrib/postgres_fdw/postgres_fdw.h           |    1 +
 contrib/postgres_fdw/shippable.c              |   74 ++
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   22 +
 6 files changed, 811 insertions(+), 544 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index bf12eac0288..33a79026574 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -158,6 +158,7 @@ static void deparseDistinctExpr(DistinctExpr *node, deparse_expr_cxt *context);
 static void deparseScalarArrayOpExpr(ScalarArrayOpExpr *node,
 									 deparse_expr_cxt *context);
 static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
+static void deparseStableExpr(Expr *node, deparse_expr_cxt *context);
 static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
 static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
 static void deparseCaseExpr(CaseExpr *node, deparse_expr_cxt *context);
@@ -267,20 +268,17 @@ is_foreign_expr(PlannerInfo *root,
 	if (loc_cxt.state == FDW_COLLATE_UNSAFE)
 		return false;
 
-	/*
-	 * An expression which includes any mutable functions can't be sent over
-	 * because its result is not stable.  For example, sending now() remote
-	 * side could cause confusion from clock offsets.  Future versions might
-	 * be able to make this choice with more granularity.  (We check this last
-	 * because it requires a lot of expensive catalog lookups.)
-	 */
-	if (contain_mutable_functions((Node *) expr))
-		return false;
 
 	/* OK to evaluate on the remote server */
 	return true;
 }
 
+static bool
+contain_mutable_functions_checker(Oid func_id, void *context)
+{
+	return (func_volatile(func_id) != PROVOLATILE_IMMUTABLE);
+}
+
 /*
  * Check if expression is safe to execute remotely, and return true if so.
  *
@@ -321,616 +319,650 @@ foreign_expr_walker(Node *node,
 	inner_cxt.collation = InvalidOid;
 	inner_cxt.state = FDW_COLLATE_NONE;
 
-	switch (nodeTag(node))
+	if (is_stable_expr(node))
+	{
+		collation = exprCollation(node);
+		if (collation == InvalidOid || collation == DEFAULT_COLLATION_OID)
+			state = FDW_COLLATE_NONE;
+		else
+			state = FDW_COLLATE_UNSAFE;
+	}
+	else
 	{
-		case T_Var:
-			{
-				Var		   *var = (Var *) node;
 
-				/*
-				 * If the Var is from the foreign table, we consider its
-				 * collation (if any) safe to use.  If it is from another
-				 * table, we treat its collation the same way as we would a
-				 * Param's collation, ie it's not safe for it to have a
-				 * non-default collation.
-				 */
-				if (bms_is_member(var->varno, glob_cxt->relids) &&
-					var->varlevelsup == 0)
+		/*
+		 * An expression which includes any mutable functions can't be sent
+		 * over because its result is not stable.
+		 */
+		if (check_functions_in_node(node, contain_mutable_functions_checker, NULL))
+			return false;
+
+		switch (nodeTag(node))
+		{
+			case T_Var:
 				{
-					/* Var belongs to foreign table */
+					Var		   *var = (Var *) node;
 
 					/*
-					 * System columns other than ctid should not be sent to
-					 * the remote, since we don't make any effort to ensure
-					 * that local and remote values match (tableoid, in
-					 * particular, almost certainly doesn't match).
+					 * If the Var is from the foreign table, we consider its
+					 * collation (if any) safe to use.  If it is from another
+					 * table, we treat its collation the same way as we would
+					 * a Param's collation, ie it's not safe for it to have a
+					 * non-default collation.
 					 */
-					if (var->varattno < 0 &&
-						var->varattno != SelfItemPointerAttributeNumber)
-						return false;
-
-					/* Else check the collation */
-					collation = var->varcollid;
-					state = OidIsValid(collation) ? FDW_COLLATE_SAFE : FDW_COLLATE_NONE;
-				}
-				else
-				{
-					/* Var belongs to some other table */
-					collation = var->varcollid;
-					if (collation == InvalidOid ||
-						collation == DEFAULT_COLLATION_OID)
+					if (bms_is_member(var->varno, glob_cxt->relids) &&
+						var->varlevelsup == 0)
 					{
+						/* Var belongs to foreign table */
+
 						/*
-						 * It's noncollatable, or it's safe to combine with a
-						 * collatable foreign Var, so set state to NONE.
+						 * System columns other than ctid should not be sent
+						 * to the remote, since we don't make any effort to
+						 * ensure that local and remote values match
+						 * (tableoid, in particular, almost certainly doesn't
+						 * match).
 						 */
-						state = FDW_COLLATE_NONE;
+						if (var->varattno < 0 &&
+							var->varattno != SelfItemPointerAttributeNumber)
+							return false;
+
+						/* Else check the collation */
+						collation = var->varcollid;
+						state = OidIsValid(collation) ? FDW_COLLATE_SAFE : FDW_COLLATE_NONE;
 					}
 					else
 					{
-						/*
-						 * Do not fail right away, since the Var might appear
-						 * in a collation-insensitive context.
-						 */
-						state = FDW_COLLATE_UNSAFE;
+						/* Var belongs to some other table */
+						collation = var->varcollid;
+						if (collation == InvalidOid ||
+							collation == DEFAULT_COLLATION_OID)
+						{
+							/*
+							 * It's noncollatable, or it's safe to combine
+							 * with a collatable foreign Var, so set state to
+							 * NONE.
+							 */
+							state = FDW_COLLATE_NONE;
+						}
+						else
+						{
+							/*
+							 * Do not fail right away, since the Var might
+							 * appear in a collation-insensitive context.
+							 */
+							state = FDW_COLLATE_UNSAFE;
+						}
 					}
 				}
-			}
-			break;
-		case T_Const:
-			{
-				Const	   *c = (Const *) node;
+				break;
+			case T_Const:
+				{
+					Const	   *c = (Const *) node;
 
-				/*
-				 * If the constant has nondefault collation, either it's of a
-				 * non-builtin type, or it reflects folding of a CollateExpr.
-				 * It's unsafe to send to the remote unless it's used in a
-				 * non-collation-sensitive context.
-				 */
-				collation = c->constcollid;
-				if (collation == InvalidOid ||
-					collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_Param:
-			{
-				Param	   *p = (Param *) node;
+					/*
+					 * If the constant has nondefault collation, either it's
+					 * of a non-builtin type, or it reflects folding of a
+					 * CollateExpr. It's unsafe to send to the remote unless
+					 * it's used in a non-collation-sensitive context.
+					 */
+					collation = c->constcollid;
+					if (collation == InvalidOid ||
+						collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_Param:
+				{
+					Param	   *p = (Param *) node;
 
-				/*
-				 * If it's a MULTIEXPR Param, punt.  We can't tell from here
-				 * whether the referenced sublink/subplan contains any remote
-				 * Vars; if it does, handling that is too complicated to
-				 * consider supporting at present.  Fortunately, MULTIEXPR
-				 * Params are not reduced to plain PARAM_EXEC until the end of
-				 * planning, so we can easily detect this case.  (Normal
-				 * PARAM_EXEC Params are safe to ship because their values
-				 * come from somewhere else in the plan tree; but a MULTIEXPR
-				 * references a sub-select elsewhere in the same targetlist,
-				 * so we'd be on the hook to evaluate it somehow if we wanted
-				 * to handle such cases as direct foreign updates.)
-				 */
-				if (p->paramkind == PARAM_MULTIEXPR)
-					return false;
+					/*
+					 * If it's a MULTIEXPR Param, punt.  We can't tell from
+					 * here whether the referenced sublink/subplan contains
+					 * any remote Vars; if it does, handling that is too
+					 * complicated to consider supporting at present.
+					 * Fortunately, MULTIEXPR Params are not reduced to plain
+					 * PARAM_EXEC until the end of planning, so we can easily
+					 * detect this case.  (Normal PARAM_EXEC Params are safe
+					 * to ship because their values come from somewhere else
+					 * in the plan tree; but a MULTIEXPR references a
+					 * sub-select elsewhere in the same targetlist, so we'd be
+					 * on the hook to evaluate it somehow if we wanted to
+					 * handle such cases as direct foreign updates.)
+					 */
+					if (p->paramkind == PARAM_MULTIEXPR)
+						return false;
 
-				/*
-				 * Collation rule is same as for Consts and non-foreign Vars.
-				 */
-				collation = p->paramcollid;
-				if (collation == InvalidOid ||
-					collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_SubscriptingRef:
-			{
-				SubscriptingRef *sr = (SubscriptingRef *) node;
+					/*
+					 * Collation rule is same as for Consts and non-foreign
+					 * Vars.
+					 */
+					collation = p->paramcollid;
+					if (collation == InvalidOid ||
+						collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_SubscriptingRef:
+				{
+					SubscriptingRef *sr = (SubscriptingRef *) node;
 
-				/* Assignment should not be in restrictions. */
-				if (sr->refassgnexpr != NULL)
-					return false;
+					/* Assignment should not be in restrictions. */
+					if (sr->refassgnexpr != NULL)
+						return false;
 
-				/*
-				 * Recurse into the remaining subexpressions.  The container
-				 * subscripts will not affect collation of the SubscriptingRef
-				 * result, so do those first and reset inner_cxt afterwards.
-				 */
-				if (!foreign_expr_walker((Node *) sr->refupperindexpr,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
-				inner_cxt.collation = InvalidOid;
-				inner_cxt.state = FDW_COLLATE_NONE;
-				if (!foreign_expr_walker((Node *) sr->reflowerindexpr,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
-				inner_cxt.collation = InvalidOid;
-				inner_cxt.state = FDW_COLLATE_NONE;
-				if (!foreign_expr_walker((Node *) sr->refexpr,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/*
+					 * Recurse into the remaining subexpressions.  The
+					 * container subscripts will not affect collation of the
+					 * SubscriptingRef result, so do those first and reset
+					 * inner_cxt afterwards.
+					 */
+					if (!foreign_expr_walker((Node *) sr->refupperindexpr,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
+					inner_cxt.collation = InvalidOid;
+					inner_cxt.state = FDW_COLLATE_NONE;
+					if (!foreign_expr_walker((Node *) sr->reflowerindexpr,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
+					inner_cxt.collation = InvalidOid;
+					inner_cxt.state = FDW_COLLATE_NONE;
+					if (!foreign_expr_walker((Node *) sr->refexpr,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
 
-				/*
-				 * Container subscripting typically yields same collation as
-				 * refexpr's, but in case it doesn't, use same logic as for
-				 * function nodes.
-				 */
-				collation = sr->refcollid;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (inner_cxt.state == FDW_COLLATE_SAFE &&
-						 collation == inner_cxt.collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_FuncExpr:
-			{
-				FuncExpr   *fe = (FuncExpr *) node;
+					/*
+					 * Container subscripting typically yields same collation
+					 * as refexpr's, but in case it doesn't, use same logic as
+					 * for function nodes.
+					 */
+					collation = sr->refcollid;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (inner_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == inner_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_FuncExpr:
+				{
+					FuncExpr   *fe = (FuncExpr *) node;
 
-				/*
-				 * If function used by the expression is not shippable, it
-				 * can't be sent to remote because it might have incompatible
-				 * semantics on remote side.
-				 */
-				if (!is_shippable(fe->funcid, ProcedureRelationId, fpinfo))
-					return false;
+					/*
+					 * If function used by the expression is not shippable, it
+					 * can't be sent to remote because it might have
+					 * incompatible semantics on remote side.
+					 */
+					if (!is_shippable(fe->funcid, ProcedureRelationId, fpinfo))
+						return false;
 
-				/*
-				 * Recurse to input subexpressions.
-				 */
-				if (!foreign_expr_walker((Node *) fe->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/*
+					 * Recurse to input subexpressions.
+					 */
+					if (!foreign_expr_walker((Node *) fe->args,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
 
-				/*
-				 * If function's input collation is not derived from a foreign
-				 * Var, it can't be sent to remote.
-				 */
-				if (fe->inputcollid == InvalidOid)
-					 /* OK, inputs are all noncollatable */ ;
-				else if (inner_cxt.state != FDW_COLLATE_SAFE ||
-						 fe->inputcollid != inner_cxt.collation)
-					return false;
+					/*
+					 * If function's input collation is not derived from a
+					 * foreign Var, it can't be sent to remote.
+					 */
+					if (fe->inputcollid == InvalidOid)
+						 /* OK, inputs are all noncollatable */ ;
+					else if (inner_cxt.state != FDW_COLLATE_SAFE ||
+							 fe->inputcollid != inner_cxt.collation)
+						return false;
 
-				/*
-				 * Detect whether node is introducing a collation not derived
-				 * from a foreign Var.  (If so, we just mark it unsafe for now
-				 * rather than immediately returning false, since the parent
-				 * node might not care.)
-				 */
-				collation = fe->funccollid;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (inner_cxt.state == FDW_COLLATE_SAFE &&
-						 collation == inner_cxt.collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_OpExpr:
-		case T_DistinctExpr:	/* struct-equivalent to OpExpr */
-			{
-				OpExpr	   *oe = (OpExpr *) node;
+					/*
+					 * Detect whether node is introducing a collation not
+					 * derived from a foreign Var.  (If so, we just mark it
+					 * unsafe for now rather than immediately returning false,
+					 * since the parent node might not care.)
+					 */
+					collation = fe->funccollid;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (inner_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == inner_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_OpExpr:
+			case T_DistinctExpr:	/* struct-equivalent to OpExpr */
+				{
+					OpExpr	   *oe = (OpExpr *) node;
 
-				/*
-				 * Similarly, only shippable operators can be sent to remote.
-				 * (If the operator is shippable, we assume its underlying
-				 * function is too.)
-				 */
-				if (!is_shippable(oe->opno, OperatorRelationId, fpinfo))
-					return false;
+					/*
+					 * Similarly, only shippable operators can be sent to
+					 * remote. (If the operator is shippable, we assume its
+					 * underlying function is too.)
+					 */
+					if (!is_shippable(oe->opno, OperatorRelationId, fpinfo))
+						return false;
 
-				/*
-				 * Recurse to input subexpressions.
-				 */
-				if (!foreign_expr_walker((Node *) oe->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/*
+					 * Recurse to input subexpressions.
+					 */
+					if (!foreign_expr_walker((Node *) oe->args,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
 
-				/*
-				 * If operator's input collation is not derived from a foreign
-				 * Var, it can't be sent to remote.
-				 */
-				if (oe->inputcollid == InvalidOid)
-					 /* OK, inputs are all noncollatable */ ;
-				else if (inner_cxt.state != FDW_COLLATE_SAFE ||
-						 oe->inputcollid != inner_cxt.collation)
-					return false;
-
-				/* Result-collation handling is same as for functions */
-				collation = oe->opcollid;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (inner_cxt.state == FDW_COLLATE_SAFE &&
-						 collation == inner_cxt.collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_ScalarArrayOpExpr:
-			{
-				ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+					/*
+					 * If operator's input collation is not derived from a
+					 * foreign Var, it can't be sent to remote.
+					 */
+					if (oe->inputcollid == InvalidOid)
+						 /* OK, inputs are all noncollatable */ ;
+					else if (inner_cxt.state != FDW_COLLATE_SAFE ||
+							 oe->inputcollid != inner_cxt.collation)
+						return false;
 
-				/*
-				 * Again, only shippable operators can be sent to remote.
-				 */
-				if (!is_shippable(oe->opno, OperatorRelationId, fpinfo))
-					return false;
+					/* Result-collation handling is same as for functions */
+					collation = oe->opcollid;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (inner_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == inner_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_ScalarArrayOpExpr:
+				{
+					ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
 
-				/*
-				 * Recurse to input subexpressions.
-				 */
-				if (!foreign_expr_walker((Node *) oe->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/*
+					 * Again, only shippable operators can be sent to remote.
+					 */
+					if (!is_shippable(oe->opno, OperatorRelationId, fpinfo))
+						return false;
 
-				/*
-				 * If operator's input collation is not derived from a foreign
-				 * Var, it can't be sent to remote.
-				 */
-				if (oe->inputcollid == InvalidOid)
-					 /* OK, inputs are all noncollatable */ ;
-				else if (inner_cxt.state != FDW_COLLATE_SAFE ||
-						 oe->inputcollid != inner_cxt.collation)
-					return false;
-
-				/* Output is always boolean and so noncollatable. */
-				collation = InvalidOid;
-				state = FDW_COLLATE_NONE;
-			}
-			break;
-		case T_RelabelType:
-			{
-				RelabelType *r = (RelabelType *) node;
+					/*
+					 * Recurse to input subexpressions.
+					 */
+					if (!foreign_expr_walker((Node *) oe->args,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
 
-				/*
-				 * Recurse to input subexpression.
-				 */
-				if (!foreign_expr_walker((Node *) r->arg,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/*
+					 * If operator's input collation is not derived from a
+					 * foreign Var, it can't be sent to remote.
+					 */
+					if (oe->inputcollid == InvalidOid)
+						 /* OK, inputs are all noncollatable */ ;
+					else if (inner_cxt.state != FDW_COLLATE_SAFE ||
+							 oe->inputcollid != inner_cxt.collation)
+						return false;
 
-				/*
-				 * RelabelType must not introduce a collation not derived from
-				 * an input foreign Var (same logic as for a real function).
-				 */
-				collation = r->resultcollid;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (inner_cxt.state == FDW_COLLATE_SAFE &&
-						 collation == inner_cxt.collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
+					/* Output is always boolean and so noncollatable. */
+					collation = InvalidOid;
 					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_BoolExpr:
-			{
-				BoolExpr   *b = (BoolExpr *) node;
-
-				/*
-				 * Recurse to input subexpressions.
-				 */
-				if (!foreign_expr_walker((Node *) b->args,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+				}
+				break;
+			case T_RelabelType:
+				{
+					RelabelType *r = (RelabelType *) node;
 
-				/* Output is always boolean and so noncollatable. */
-				collation = InvalidOid;
-				state = FDW_COLLATE_NONE;
-			}
-			break;
-		case T_NullTest:
-			{
-				NullTest   *nt = (NullTest *) node;
+					/*
+					 * Recurse to input subexpression.
+					 */
+					if (!foreign_expr_walker((Node *) r->arg,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
 
-				/*
-				 * Recurse to input subexpressions.
-				 */
-				if (!foreign_expr_walker((Node *) nt->arg,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/*
+					 * RelabelType must not introduce a collation not derived
+					 * from an input foreign Var (same logic as for a real
+					 * function).
+					 */
+					collation = r->resultcollid;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (inner_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == inner_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_BoolExpr:
+				{
+					BoolExpr   *b = (BoolExpr *) node;
 
-				/* Output is always boolean and so noncollatable. */
-				collation = InvalidOid;
-				state = FDW_COLLATE_NONE;
-			}
-			break;
-		case T_CaseExpr:
-			{
-				CaseExpr   *ce = (CaseExpr *) node;
-				foreign_loc_cxt arg_cxt;
-				foreign_loc_cxt tmp_cxt;
-				ListCell   *lc;
+					/*
+					 * Recurse to input subexpressions.
+					 */
+					if (!foreign_expr_walker((Node *) b->args,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
 
-				/*
-				 * Recurse to CASE's arg expression, if any.  Its collation
-				 * has to be saved aside for use while examining CaseTestExprs
-				 * within the WHEN expressions.
-				 */
-				arg_cxt.collation = InvalidOid;
-				arg_cxt.state = FDW_COLLATE_NONE;
-				if (ce->arg)
+					/* Output is always boolean and so noncollatable. */
+					collation = InvalidOid;
+					state = FDW_COLLATE_NONE;
+				}
+				break;
+			case T_NullTest:
 				{
-					if (!foreign_expr_walker((Node *) ce->arg,
-											 glob_cxt, &arg_cxt, case_arg_cxt))
+					NullTest   *nt = (NullTest *) node;
+
+					/*
+					 * Recurse to input subexpressions.
+					 */
+					if (!foreign_expr_walker((Node *) nt->arg,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
 						return false;
-				}
 
-				/* Examine the CaseWhen subexpressions. */
-				foreach(lc, ce->args)
+					/* Output is always boolean and so noncollatable. */
+					collation = InvalidOid;
+					state = FDW_COLLATE_NONE;
+				}
+				break;
+			case T_CaseExpr:
 				{
-					CaseWhen   *cw = lfirst_node(CaseWhen, lc);
+					CaseExpr   *ce = (CaseExpr *) node;
+					foreign_loc_cxt arg_cxt;
+					foreign_loc_cxt tmp_cxt;
+					ListCell   *lc;
 
+					/*
+					 * Recurse to CASE's arg expression, if any.  Its
+					 * collation has to be saved aside for use while examining
+					 * CaseTestExprs within the WHEN expressions.
+					 */
+					arg_cxt.collation = InvalidOid;
+					arg_cxt.state = FDW_COLLATE_NONE;
 					if (ce->arg)
 					{
+						if (!foreign_expr_walker((Node *) ce->arg,
+												 glob_cxt, &arg_cxt, case_arg_cxt))
+							return false;
+					}
+
+					/* Examine the CaseWhen subexpressions. */
+					foreach(lc, ce->args)
+					{
+						CaseWhen   *cw = lfirst_node(CaseWhen, lc);
+
+						if (ce->arg)
+						{
+							/*
+							 * In a CASE-with-arg, the parser should have
+							 * produced WHEN clauses of the form "CaseTestExpr
+							 * = RHS", possibly with an implicit coercion
+							 * inserted above the CaseTestExpr.  However in an
+							 * expression that's been through the optimizer,
+							 * the WHEN clause could be almost anything (since
+							 * the equality operator could have been expanded
+							 * into an inline function). In such cases forbid
+							 * pushdown, because deparseCaseExpr can't handle
+							 * it.
+							 */
+							Node	   *whenExpr = (Node *) cw->expr;
+							List	   *opArgs;
+
+							if (!IsA(whenExpr, OpExpr))
+								return false;
+
+							opArgs = ((OpExpr *) whenExpr)->args;
+							if (list_length(opArgs) != 2 ||
+								!IsA(strip_implicit_coercions(linitial(opArgs)),
+									 CaseTestExpr))
+								return false;
+						}
+
 						/*
-						 * In a CASE-with-arg, the parser should have produced
-						 * WHEN clauses of the form "CaseTestExpr = RHS",
-						 * possibly with an implicit coercion inserted above
-						 * the CaseTestExpr.  However in an expression that's
-						 * been through the optimizer, the WHEN clause could
-						 * be almost anything (since the equality operator
-						 * could have been expanded into an inline function).
-						 * In such cases forbid pushdown, because
-						 * deparseCaseExpr can't handle it.
+						 * Recurse to WHEN expression, passing down the arg
+						 * info. Its collation doesn't affect the result
+						 * (really, it should be boolean and thus not have a
+						 * collation).
 						 */
-						Node	   *whenExpr = (Node *) cw->expr;
-						List	   *opArgs;
-
-						if (!IsA(whenExpr, OpExpr))
+						tmp_cxt.collation = InvalidOid;
+						tmp_cxt.state = FDW_COLLATE_NONE;
+						if (!foreign_expr_walker((Node *) cw->expr,
+												 glob_cxt, &tmp_cxt, &arg_cxt))
 							return false;
 
-						opArgs = ((OpExpr *) whenExpr)->args;
-						if (list_length(opArgs) != 2 ||
-							!IsA(strip_implicit_coercions(linitial(opArgs)),
-								 CaseTestExpr))
+						/* Recurse to THEN expression. */
+						if (!foreign_expr_walker((Node *) cw->result,
+												 glob_cxt, &inner_cxt, case_arg_cxt))
 							return false;
 					}
 
-					/*
-					 * Recurse to WHEN expression, passing down the arg info.
-					 * Its collation doesn't affect the result (really, it
-					 * should be boolean and thus not have a collation).
-					 */
-					tmp_cxt.collation = InvalidOid;
-					tmp_cxt.state = FDW_COLLATE_NONE;
-					if (!foreign_expr_walker((Node *) cw->expr,
-											 glob_cxt, &tmp_cxt, &arg_cxt))
-						return false;
-
-					/* Recurse to THEN expression. */
-					if (!foreign_expr_walker((Node *) cw->result,
+					/* Recurse to ELSE expression. */
+					if (!foreign_expr_walker((Node *) ce->defresult,
 											 glob_cxt, &inner_cxt, case_arg_cxt))
 						return false;
+
+					/*
+					 * Detect whether node is introducing a collation not
+					 * derived from a foreign Var.  (If so, we just mark it
+					 * unsafe for now rather than immediately returning false,
+					 * since the parent node might not care.)  This is the
+					 * same as for function nodes, except that the input
+					 * collation is derived from only the THEN and ELSE
+					 * subexpressions.
+					 */
+					collation = ce->casecollid;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (inner_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == inner_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
 				}
+				break;
+			case T_CaseTestExpr:
+				{
+					CaseTestExpr *c = (CaseTestExpr *) node;
 
-				/* Recurse to ELSE expression. */
-				if (!foreign_expr_walker((Node *) ce->defresult,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/* Punt if we seem not to be inside a CASE arg WHEN. */
+					if (!case_arg_cxt)
+						return false;
 
-				/*
-				 * Detect whether node is introducing a collation not derived
-				 * from a foreign Var.  (If so, we just mark it unsafe for now
-				 * rather than immediately returning false, since the parent
-				 * node might not care.)  This is the same as for function
-				 * nodes, except that the input collation is derived from only
-				 * the THEN and ELSE subexpressions.
-				 */
-				collation = ce->casecollid;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (inner_cxt.state == FDW_COLLATE_SAFE &&
-						 collation == inner_cxt.collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_CaseTestExpr:
-			{
-				CaseTestExpr *c = (CaseTestExpr *) node;
+					/*
+					 * Otherwise, any nondefault collation attached to the
+					 * CaseTestExpr node must be derived from foreign Var(s)
+					 * in the CASE arg.
+					 */
+					collation = c->collation;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (case_arg_cxt->state == FDW_COLLATE_SAFE &&
+							 collation == case_arg_cxt->collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_ArrayExpr:
+				{
+					ArrayExpr  *a = (ArrayExpr *) node;
 
-				/* Punt if we seem not to be inside a CASE arg WHEN. */
-				if (!case_arg_cxt)
-					return false;
+					/*
+					 * Recurse to input subexpressions.
+					 */
+					if (!foreign_expr_walker((Node *) a->elements,
+											 glob_cxt, &inner_cxt, case_arg_cxt))
+						return false;
 
-				/*
-				 * Otherwise, any nondefault collation attached to the
-				 * CaseTestExpr node must be derived from foreign Var(s) in
-				 * the CASE arg.
-				 */
-				collation = c->collation;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (case_arg_cxt->state == FDW_COLLATE_SAFE &&
-						 collation == case_arg_cxt->collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_ArrayExpr:
-			{
-				ArrayExpr  *a = (ArrayExpr *) node;
+					/*
+					 * ArrayExpr must not introduce a collation not derived
+					 * from an input foreign Var (same logic as for a
+					 * function).
+					 */
+					collation = a->array_collid;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (inner_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == inner_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
+				}
+				break;
+			case T_List:
+				{
+					List	   *l = (List *) node;
+					ListCell   *lc;
 
-				/*
-				 * Recurse to input subexpressions.
-				 */
-				if (!foreign_expr_walker((Node *) a->elements,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
+					/*
+					 * Recurse to component subexpressions.
+					 */
+					foreach(lc, l)
+					{
+						if (!foreign_expr_walker((Node *) lfirst(lc),
+												 glob_cxt, &inner_cxt, case_arg_cxt))
+							return false;
+					}
 
-				/*
-				 * ArrayExpr must not introduce a collation not derived from
-				 * an input foreign Var (same logic as for a function).
-				 */
-				collation = a->array_collid;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (inner_cxt.state == FDW_COLLATE_SAFE &&
-						 collation == inner_cxt.collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		case T_List:
-			{
-				List	   *l = (List *) node;
-				ListCell   *lc;
+					/*
+					 * When processing a list, collation state just bubbles up
+					 * from the list elements.
+					 */
+					collation = inner_cxt.collation;
+					state = inner_cxt.state;
 
-				/*
-				 * Recurse to component subexpressions.
-				 */
-				foreach(lc, l)
+					/* Don't apply exprType() to the list. */
+					check_type = false;
+				}
+				break;
+			case T_Aggref:
 				{
-					if (!foreign_expr_walker((Node *) lfirst(lc),
-											 glob_cxt, &inner_cxt, case_arg_cxt))
+					Aggref	   *agg = (Aggref *) node;
+					ListCell   *lc;
+
+					/* Not safe to pushdown when not in grouping context */
+					if (!IS_UPPER_REL(glob_cxt->foreignrel))
 						return false;
-				}
 
-				/*
-				 * When processing a list, collation state just bubbles up
-				 * from the list elements.
-				 */
-				collation = inner_cxt.collation;
-				state = inner_cxt.state;
+					/* Only non-split aggregates are pushable. */
+					if (agg->aggsplit != AGGSPLIT_SIMPLE)
+						return false;
 
-				/* Don't apply exprType() to the list. */
-				check_type = false;
-			}
-			break;
-		case T_Aggref:
-			{
-				Aggref	   *agg = (Aggref *) node;
-				ListCell   *lc;
+					/* As usual, it must be shippable. */
+					if (!is_shippable(agg->aggfnoid, ProcedureRelationId, fpinfo))
+						return false;
 
-				/* Not safe to pushdown when not in grouping context */
-				if (!IS_UPPER_REL(glob_cxt->foreignrel))
-					return false;
+					/*
+					 * Recurse to input args. aggdirectargs, aggorder and
+					 * aggdistinct are all present in args, so no need to
+					 * check their shippability explicitly.
+					 */
+					foreach(lc, agg->args)
+					{
+						Node	   *n = (Node *) lfirst(lc);
 
-				/* Only non-split aggregates are pushable. */
-				if (agg->aggsplit != AGGSPLIT_SIMPLE)
-					return false;
+						/* If TargetEntry, extract the expression from it */
+						if (IsA(n, TargetEntry))
+						{
+							TargetEntry *tle = (TargetEntry *) n;
 
-				/* As usual, it must be shippable. */
-				if (!is_shippable(agg->aggfnoid, ProcedureRelationId, fpinfo))
-					return false;
+							n = (Node *) tle->expr;
+						}
 
-				/*
-				 * Recurse to input args. aggdirectargs, aggorder and
-				 * aggdistinct are all present in args, so no need to check
-				 * their shippability explicitly.
-				 */
-				foreach(lc, agg->args)
-				{
-					Node	   *n = (Node *) lfirst(lc);
+						if (!foreign_expr_walker(n,
+												 glob_cxt, &inner_cxt, case_arg_cxt))
+							return false;
+					}
 
-					/* If TargetEntry, extract the expression from it */
-					if (IsA(n, TargetEntry))
+					/*
+					 * For aggorder elements, check whether the sort operator,
+					 * if specified, is shippable or not.
+					 */
+					if (agg->aggorder)
 					{
-						TargetEntry *tle = (TargetEntry *) n;
-
-						n = (Node *) tle->expr;
+						ListCell   *lc;
+
+						foreach(lc, agg->aggorder)
+						{
+							SortGroupClause *srt = (SortGroupClause *) lfirst(lc);
+							Oid			sortcoltype;
+							TypeCacheEntry *typentry;
+							TargetEntry *tle;
+
+							tle = get_sortgroupref_tle(srt->tleSortGroupRef,
+													   agg->args);
+							sortcoltype = exprType((Node *) tle->expr);
+							typentry = lookup_type_cache(sortcoltype,
+														 TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);
+
+							/*
+							 * Check shippability of non-default sort
+							 * operator.
+							 */
+							if (srt->sortop != typentry->lt_opr &&
+								srt->sortop != typentry->gt_opr &&
+								!is_shippable(srt->sortop, OperatorRelationId,
+											  fpinfo))
+								return false;
+						}
 					}
 
-					if (!foreign_expr_walker(n,
+					/* Check aggregate filter */
+					if (!foreign_expr_walker((Node *) agg->aggfilter,
 											 glob_cxt, &inner_cxt, case_arg_cxt))
 						return false;
-				}
 
-				/*
-				 * For aggorder elements, check whether the sort operator, if
-				 * specified, is shippable or not.
-				 */
-				if (agg->aggorder)
-				{
-					ListCell   *lc;
+					/*
+					 * If aggregate's input collation is not derived from a
+					 * foreign Var, it can't be sent to remote.
+					 */
+					if (agg->inputcollid == InvalidOid)
+						 /* OK, inputs are all noncollatable */ ;
+					else if (inner_cxt.state != FDW_COLLATE_SAFE ||
+							 agg->inputcollid != inner_cxt.collation)
+						return false;
 
-					foreach(lc, agg->aggorder)
-					{
-						SortGroupClause *srt = (SortGroupClause *) lfirst(lc);
-						Oid			sortcoltype;
-						TypeCacheEntry *typentry;
-						TargetEntry *tle;
-
-						tle = get_sortgroupref_tle(srt->tleSortGroupRef,
-												   agg->args);
-						sortcoltype = exprType((Node *) tle->expr);
-						typentry = lookup_type_cache(sortcoltype,
-													 TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);
-						/* Check shippability of non-default sort operator. */
-						if (srt->sortop != typentry->lt_opr &&
-							srt->sortop != typentry->gt_opr &&
-							!is_shippable(srt->sortop, OperatorRelationId,
-										  fpinfo))
-							return false;
-					}
+					/*
+					 * Detect whether node is introducing a collation not
+					 * derived from a foreign Var.  (If so, we just mark it
+					 * unsafe for now rather than immediately returning false,
+					 * since the parent node might not care.)
+					 */
+					collation = agg->aggcollid;
+					if (collation == InvalidOid)
+						state = FDW_COLLATE_NONE;
+					else if (inner_cxt.state == FDW_COLLATE_SAFE &&
+							 collation == inner_cxt.collation)
+						state = FDW_COLLATE_SAFE;
+					else if (collation == DEFAULT_COLLATION_OID)
+						state = FDW_COLLATE_NONE;
+					else
+						state = FDW_COLLATE_UNSAFE;
 				}
-
-				/* Check aggregate filter */
-				if (!foreign_expr_walker((Node *) agg->aggfilter,
-										 glob_cxt, &inner_cxt, case_arg_cxt))
-					return false;
-
-				/*
-				 * If aggregate's input collation is not derived from a
-				 * foreign Var, it can't be sent to remote.
-				 */
-				if (agg->inputcollid == InvalidOid)
-					 /* OK, inputs are all noncollatable */ ;
-				else if (inner_cxt.state != FDW_COLLATE_SAFE ||
-						 agg->inputcollid != inner_cxt.collation)
-					return false;
+				break;
+			default:
 
 				/*
-				 * Detect whether node is introducing a collation not derived
-				 * from a foreign Var.  (If so, we just mark it unsafe for now
-				 * rather than immediately returning false, since the parent
-				 * node might not care.)
+				 * If it's anything else, assume it's unsafe.  This list can
+				 * be expanded later, but don't forget to add deparse support
+				 * below.
 				 */
-				collation = agg->aggcollid;
-				if (collation == InvalidOid)
-					state = FDW_COLLATE_NONE;
-				else if (inner_cxt.state == FDW_COLLATE_SAFE &&
-						 collation == inner_cxt.collation)
-					state = FDW_COLLATE_SAFE;
-				else if (collation == DEFAULT_COLLATION_OID)
-					state = FDW_COLLATE_NONE;
-				else
-					state = FDW_COLLATE_UNSAFE;
-			}
-			break;
-		default:
-
-			/*
-			 * If it's anything else, assume it's unsafe.  This list can be
-			 * expanded later, but don't forget to add deparse support below.
-			 */
-			return false;
+				return false;
+		}
 	}
 
 	/*
@@ -1009,6 +1041,15 @@ is_foreign_param(PlannerInfo *root,
 	if (expr == NULL)
 		return false;
 
+	if (is_stable_expr((Node *) expr))
+	{
+		/*
+		 * We can get here only if is_foreign_expr(expr) returned true, so
+		 * it's a supported stable expression.
+		 */
+		return true;
+	}
+
 	switch (nodeTag(expr))
 	{
 		case T_Var:
@@ -2575,6 +2616,12 @@ deparseExpr(Expr *node, deparse_expr_cxt *context)
 	if (node == NULL)
 		return;
 
+	if (is_stable_expr((Node *) node))
+	{
+		deparseStableExpr(node, context);
+		return;
+	}
+
 	switch (nodeTag(node))
 	{
 		case T_Var:
@@ -3192,6 +3239,45 @@ deparseRelabelType(RelabelType *node, deparse_expr_cxt *context)
 										   node->resulttypmod));
 }
 
+/*
+ * Deparse a stable expression as param
+ */
+static void
+deparseStableExpr(Expr *node, deparse_expr_cxt *context)
+{
+	int32		type = exprType((Node *) node);
+	int32		typmod = exprTypmod((Node *) node);
+
+	Assert(type != InvalidOid);
+
+	/* Treat like a Param */
+	if (context->params_list)
+	{
+		int			pindex = 0;
+		ListCell   *lc;
+
+		/* find its index in params_list */
+		foreach(lc, *context->params_list)
+		{
+			pindex++;
+			if (equal(node, (Node *) lfirst(lc)))
+				break;
+		}
+		if (lc == NULL)
+		{
+			/* not in list, so add it */
+			pindex++;
+			*context->params_list = lappend(*context->params_list, node);
+		}
+
+		printRemoteParam(pindex, type, typmod, context);
+	}
+	else
+	{
+		printRemotePlaceholder(type, typmod, context);
+	}
+}
+
 /*
  * Deparse a BoolExpr node.
  */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 7d6f7d9e3df..5defc2d3758 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -1073,6 +1073,93 @@ SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2 order by t1.c2 limit 1;
   1 |  1 | 00001 | Fri Jan 02 00:00:00 1970 PST | Fri Jan 02 00:00:00 1970 | 1  | 1          | foo
 (1 row)
 
+-- Test stable expressions pushdown
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c1,c2,c4 FROM ft2 WHERE c4 = current_timestamp;
+                                           QUERY PLAN                                            
+-------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft2
+   Output: c1, c2, c4
+   Remote SQL: SELECT "C 1", c2, c4 FROM "S 1"."T 1" WHERE ((c4 = $1::timestamp with time zone))
+(3 rows)
+
+SELECT c1,c2,c4 FROM ft2 WHERE c4 = current_timestamp;
+ c1 | c2 | c4 
+----+----+----
+(0 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c1,c2,c5 FROM ft2 WHERE c5 > localtimestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+                                                                      QUERY PLAN                                                                      
+------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft2
+   Output: c1, c2, c5
+   Remote SQL: SELECT "C 1", c2, c5 FROM "S 1"."T 1" WHERE (("C 1" > 990)) AND ((c5 > $1::timestamp without time zone)) ORDER BY "C 1" ASC NULLS LAST
+(3 rows)
+
+SELECT c1,c2,c5 FROM ft2 WHERE c5 > localtimestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+  c1  | c2 |            c5            
+------+----+--------------------------
+  991 |  1 | Thu Apr 02 00:00:00 1970
+  992 |  2 | Fri Apr 03 00:00:00 1970
+  993 |  3 | Sat Apr 04 00:00:00 1970
+  994 |  4 | Sun Apr 05 00:00:00 1970
+  995 |  5 | Mon Apr 06 00:00:00 1970
+  996 |  6 | Tue Apr 07 00:00:00 1970
+  997 |  7 | Wed Apr 08 00:00:00 1970
+  998 |  8 | Thu Apr 09 00:00:00 1970
+  999 |  9 | Fri Apr 10 00:00:00 1970
+ 1000 |  0 | Thu Jan 01 00:00:00 1970
+(10 rows)
+
+-- also shippable as whole expression is executed locally
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c1,c2,c4 FROM ft2 WHERE c4 > current_timestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+                                                                    QUERY PLAN                                                                     
+---------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan on public.ft2
+   Output: c1, c2, c4
+   Remote SQL: SELECT "C 1", c2, c4 FROM "S 1"."T 1" WHERE (("C 1" > 990)) AND ((c4 > $1::timestamp with time zone)) ORDER BY "C 1" ASC NULLS LAST
+(3 rows)
+
+SELECT c1,c2,c4 FROM ft2 WHERE c4 > current_timestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+  c1  | c2 |              c4              
+------+----+------------------------------
+  991 |  1 | Thu Apr 02 00:00:00 1970 PST
+  992 |  2 | Fri Apr 03 00:00:00 1970 PST
+  993 |  3 | Sat Apr 04 00:00:00 1970 PST
+  994 |  4 | Sun Apr 05 00:00:00 1970 PST
+  995 |  5 | Mon Apr 06 00:00:00 1970 PST
+  996 |  6 | Tue Apr 07 00:00:00 1970 PST
+  997 |  7 | Wed Apr 08 00:00:00 1970 PST
+  998 |  8 | Thu Apr 09 00:00:00 1970 PST
+  999 |  9 | Fri Apr 10 00:00:00 1970 PST
+ 1000 |  0 | Thu Jan 01 00:00:00 1970 PST
+(10 rows)
+
+-- direct modify
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE ft2 SET c4 = current_timestamp WHERE c4 < current_timestamp;
+                                                        QUERY PLAN                                                        
+--------------------------------------------------------------------------------------------------------------------------
+ Update on public.ft2
+   ->  Foreign Update on public.ft2
+         Remote SQL: UPDATE "S 1"."T 1" SET c4 = $1::timestamp with time zone WHERE ((c4 < $1::timestamp with time zone))
+(3 rows)
+
+-- check that we don't try to push down parameter in group by
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT sum(c1), current_timestamp FROM ft2 WHERE c1 > 990 GROUP BY current_timestamp;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ GroupAggregate
+   Output: sum(c1), (CURRENT_TIMESTAMP)
+   Group Key: CURRENT_TIMESTAMP
+   ->  Foreign Scan on public.ft2
+         Output: CURRENT_TIMESTAMP, c1
+         Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" > 990))
+(6 rows)
+
 -- Test CASE pushdown
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT c1,c2,c3 FROM ft2 WHERE CASE WHEN c1 > 990 THEN c1 END < 1000 ORDER BY c1;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 09a3f5e23cb..6aa3a20f20c 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4834,12 +4834,9 @@ prepare_query_params(PlanState *node,
 	}
 
 	/*
-	 * Prepare remote-parameter expressions for evaluation.  (Note: in
-	 * practice, we expect that all these expressions will be just Params, so
-	 * we could possibly do something more efficient than using the full
-	 * expression-eval machinery for this.  But probably there would be little
-	 * benefit, and it'd require postgres_fdw to know more than is desirable
-	 * about Param evaluation.)
+	 * Prepare remote-parameter expressions for evaluation.  (Note: we cannot
+	 * expect that all these expressions will be just Params, so we should use
+	 * the full expression-eval machinery for this).
 	 */
 	*param_exprs = ExecInitExprList(fdw_exprs, node);
 
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 8ae79e97e44..acb36394fc6 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -231,5 +231,6 @@ extern const char *get_jointype_name(JoinType jointype);
 /* in shippable.c */
 extern bool is_builtin(Oid objectId);
 extern bool is_shippable(Oid objectId, Oid classId, PgFdwRelationInfo *fpinfo);
+extern bool is_stable_expr(Node *node);
 
 #endif							/* POSTGRES_FDW_H */
diff --git a/contrib/postgres_fdw/shippable.c b/contrib/postgres_fdw/shippable.c
index 8e759da00d5..5173d750035 100644
--- a/contrib/postgres_fdw/shippable.c
+++ b/contrib/postgres_fdw/shippable.c
@@ -25,9 +25,14 @@
 
 #include "access/transam.h"
 #include "catalog/dependency.h"
+#include "catalog/pg_proc.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "optimizer/optimizer.h"
 #include "postgres_fdw.h"
 #include "utils/hsearch.h"
 #include "utils/inval.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 /* Hash table for caching the results of shippability lookups */
@@ -52,6 +57,7 @@ typedef struct
 	bool		shippable;
 } ShippableCacheEntry;
 
+static bool contain_params_walker(Node *node, bool *context);
 
 /*
  * Flush cache entries when pg_foreign_server is updated.
@@ -209,3 +215,71 @@ is_shippable(Oid objectId, Oid classId, PgFdwRelationInfo *fpinfo)
 
 	return entry->shippable;
 }
+
+/*
+ * contain_params
+ *   Does this node has params?
+ */
+static bool
+contain_params(Node *node)
+{
+	bool		contains = false;
+
+	contain_params_walker(node, (void *) &contains);
+
+	return contains;
+}
+
+static bool
+contain_params_walker(Node *node, bool *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Param))
+	{
+		*context = true;
+
+		return false;
+	}
+	return expression_tree_walker(node, contain_params_walker,
+								  (void *) context);
+}
+
+/*
+ * Check if expression is stable
+ */
+bool
+is_stable_expr(Node *node)
+{
+	if (node == NULL)
+		return false;
+
+	/* No need to turn on 'ship stable expression' machinery in these cases */
+	if (IsA(node, Const) || IsA(node, List))
+		return false;
+
+	/* Expression shouldn't reference any table */
+	if (contain_var_clause(node))
+		return false;
+
+	if (contain_volatile_functions(node))
+		return false;
+
+	if (contain_subplans(node))
+		return false;
+
+	if (contain_params(node))
+		return false;
+
+	if (contain_agg_clause(node))
+		return false;
+
+	if (contain_mutable_functions(node))
+	{
+		/* These are not volatile functions, so they are stable */
+		if (exprType(node) != InvalidOid)
+			return true;
+	}
+
+	return false;
+}
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 9eb673e3693..de084bc4ece 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -413,6 +413,28 @@ EXPLAIN (VERBOSE, COSTS OFF)
   SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2 order by t1.c2 limit 1;
 SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2 order by t1.c2 limit 1;
 
+-- Test stable expressions pushdown
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c1,c2,c4 FROM ft2 WHERE c4 = current_timestamp;
+SELECT c1,c2,c4 FROM ft2 WHERE c4 = current_timestamp;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c1,c2,c5 FROM ft2 WHERE c5 > localtimestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+SELECT c1,c2,c5 FROM ft2 WHERE c5 > localtimestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+
+-- also shippable as whole expression is executed locally
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT c1,c2,c4 FROM ft2 WHERE c4 > current_timestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+SELECT c1,c2,c4 FROM ft2 WHERE c4 > current_timestamp - interval '1000 years' AND c1 > 990 ORDER BY c1;
+
+-- direct modify
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE ft2 SET c4 = current_timestamp WHERE c4 < current_timestamp;
+
+-- check that we don't try to push down parameter in group by
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT sum(c1), current_timestamp FROM ft2 WHERE c1 > 990 GROUP BY current_timestamp;
+
 -- Test CASE pushdown
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT c1,c2,c3 FROM ft2 WHERE CASE WHEN c1 > 990 THEN c1 END < 1000 ORDER BY c1;
-- 
2.25.1

Reply via email to