[WIP] Caching for stable expressions with constant arguments v2
Hi!
Here's my work on this so far. The patch is still incomplete, but
works well and passes all regression tests (except plpgsql, which is
not a bug, but will be addressed in the future).
As Tom Lane suggested, I rewrote the whole thing and added a new
CacheExpr type. A "bool *cachable" argument is added to many
expression tree mutation functions to return information whether the
subtree is safe to cache. Hopefully this is more approachable than
"stableconst" :) The caller initializes this to true and callees set
it to false when proven otherwise.
If you build it with -DDEBUG_CACHEEXPR=1 then EXPLAIN VERBOSE will
show CACHE[...] around cached (sub)expressions.
----
In boolean expressions (lists of OR/AND expressions), the cahcable and
un-cachable elements are accumulated into separate lists. A new
boolean expression is then built for the cacheable part and prepended
-- so that it's checked first, in the hopes that the cachable check is
cheaper and short-circuits the rest. Not sure if there are significant
downsides to this.
For example:
Input: stable_expr() AND volatile_expr() AND stable_expr() AND volatile_expr()
Output: CACHE[(stable_expr() AND stable_expr())] AND volatile_expr()
AND volatile_expr()
----
To balance my feature karma, I also took the liberty to clean up some
duplicate code. Previously, all callers of simplify_function() did
their own mutation of argument lists prior to calling. Also, arguments
added later in reorder_function_arguments() and
add_function_defaults() were mutated separately in those functions.
Now all of that is done in one place, in simplify_function().
reorder_function_arguments/add_function_defaults functions don't need
a context argument anymore.
----
Work still to left do:
* Rename eval_const_expressions_mutator to const_expressions_mutator
since it doesn't just evaluate constant expressions anymore?
* Should CacheExpr.subexpr be renamed to "arg" for consistency with others?
* CaseExpr, CoalesceExpr, CoerceViaIO and some others aren't cachable yet
* Clean up some hairy code that I've marked with "XXX"
* Some comments are obsolete/incomplete, should be revised
* Remove the "enable_cacheexpr" GUC, it's useful for testing, but
probably not for real usage
* Currently a cachable expression is forbidden from being a "simple
expression". This probably causes the plpgsql regression. I think the
"real" solution is removing CacheExpr nodes afterwards if the
expression is indeed deemed simple; that gives us the best of both
worlds
---
PS: I still don't know how to get git to produce context diff, so
please excuse my unified diff.
This is also available on my GitHub "cache" branch:
https://github.com/intgr/postgres/commits/cache
But don't rely on that too much, I *will* rebase and alter history.
Regards,
Marti
Attachments:
cacheexpr-v2.patchtext/x-patch; charset=US-ASCII; name=cacheexpr-v2.patchDownload
diff --git a/src/backend/executor/execQual.c b/src/backend/executor/execQual.c
index 80f08d8..c565ecb 100644
--- a/src/backend/executor/execQual.c
+++ b/src/backend/executor/execQual.c
@@ -51,6 +51,7 @@
#include "pgstat.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/datum.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/typcache.h"
@@ -157,6 +158,14 @@ static Datum ExecEvalCoerceToDomain(CoerceToDomainState *cstate,
static Datum ExecEvalCoerceToDomainValue(ExprState *exprstate,
ExprContext *econtext,
bool *isNull, ExprDoneCond *isDone);
+static Datum ExecEvalCacheExpr(CacheExprState *cstate,
+ ExprContext *econtext,
+ bool *isNull,
+ ExprDoneCond *isDone);
+static Datum ExecEvalCacheExprResult(CacheExprState *cstate,
+ ExprContext *econtext,
+ bool *isNull,
+ ExprDoneCond *isDone);
static Datum ExecEvalFieldSelect(FieldSelectState *fstate,
ExprContext *econtext,
bool *isNull, ExprDoneCond *isDone);
@@ -3746,6 +3755,61 @@ ExecEvalBooleanTest(GenericExprState *bstate,
}
}
+/* ----------------------------------------------------------------
+ * ExecEvalCacheExpr
+ *
+ * XXX
+ * ----------------------------------------------------------------
+ */
+static Datum
+ExecEvalCacheExpr(CacheExprState *cstate,
+ ExprContext *econtext,
+ bool *isNull,
+ ExprDoneCond *isDone)
+{
+ MemoryContext oldcontext;
+ Datum result;
+ bool resultTypByVal;
+ int16 resultTypLen;
+ Oid resultType;
+
+ result = ExecEvalExpr(cstate->subexpr, econtext, isNull, isDone);
+
+ /* Set-returning expressions can't be cached */
+ Assert(isDone == NULL || *isDone == ExprSingleResult);
+
+ resultType = exprType((Node *) ((CacheExpr *) cstate->xprstate.expr)->subexpr);
+ get_typlenbyval(resultType, &resultTypLen, &resultTypByVal);
+
+ /* This cached datum has to persist for the whole query */
+ oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+ cstate->result = datumCopy(result, resultTypByVal, resultTypLen);
+ cstate->isNull = *isNull;
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Subsequent calls will return the cached result */
+ cstate->xprstate.evalfunc = (ExprStateEvalFunc) ExecEvalCacheExprResult;
+
+ return cstate->result;
+}
+
+/*
+ * ExecEvalCacheExprResult
+ *
+ * Return the already-cached result, computed in ExecEvalCacheExpr
+ */
+static Datum
+ExecEvalCacheExprResult(CacheExprState *cstate,
+ ExprContext *econtext,
+ bool *isNull,
+ ExprDoneCond *isDone)
+{
+ if (isDone)
+ *isDone = ExprSingleResult;
+ *isNull = cstate->isNull;
+ return cstate->result;
+}
+
/*
* ExecEvalCoerceToDomain
*
@@ -4850,6 +4914,17 @@ ExecInitExpr(Expr *node, PlanState *parent)
state = (ExprState *) gstate;
}
break;
+ case T_CacheExpr:
+ {
+ CacheExpr *cache = (CacheExpr *) node;
+ CacheExprState *cstate = makeNode(CacheExprState);
+
+ cstate->xprstate.evalfunc = (ExprStateEvalFunc) ExecEvalCacheExpr;
+ cstate->subexpr = ExecInitExpr(cache->subexpr, parent);
+
+ state = (ExprState *) cstate;
+ }
+ break;
case T_CoerceToDomain:
{
CoerceToDomain *ctest = (CoerceToDomain *) node;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 661a516..f3e1113 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1658,6 +1658,19 @@ _copyBooleanTest(BooleanTest *from)
}
/*
+ * _copyCacheExpr
+ */
+static CacheExpr *
+_copyCacheExpr(CacheExpr *from)
+{
+ CacheExpr *newnode = makeNode(CacheExpr);
+
+ COPY_NODE_FIELD(subexpr);
+
+ return newnode;
+}
+
+/*
* _copyCoerceToDomain
*/
static CoerceToDomain *
@@ -4066,6 +4079,9 @@ copyObject(void *from)
case T_BooleanTest:
retval = _copyBooleanTest(from);
break;
+ case T_CacheExpr:
+ retval = _copyCacheExpr(from);
+ break;
case T_CoerceToDomain:
retval = _copyCoerceToDomain(from);
break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 4052a9a..6a8ce05 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -677,6 +677,14 @@ _equalBooleanTest(BooleanTest *a, BooleanTest *b)
}
static bool
+_equalCacheExpr(CacheExpr *a, CacheExpr *b)
+{
+ COMPARE_NODE_FIELD(subexpr);
+
+ return true;
+}
+
+static bool
_equalCoerceToDomain(CoerceToDomain *a, CoerceToDomain *b)
{
COMPARE_NODE_FIELD(arg);
@@ -2636,6 +2644,9 @@ equal(void *a, void *b)
case T_BooleanTest:
retval = _equalBooleanTest(a, b);
break;
+ case T_CacheExpr:
+ retval = _equalCacheExpr(a, b);
+ break;
case T_CoerceToDomain:
retval = _equalCoerceToDomain(a, b);
break;
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 9070cd2..a070ccd 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -470,6 +470,21 @@ makeFuncExpr(Oid funcid, Oid rettype, List *args,
}
/*
+ * makeCacheExpr -
+ * build an expression node for a cachable expression.
+ */
+CacheExpr *
+makeCacheExpr(Expr *subexpr)
+{
+ CacheExpr *cacheexpr;
+
+ cacheexpr = makeNode(CacheExpr);
+ cacheexpr->subexpr = subexpr;
+
+ return cacheexpr;
+}
+
+/*
* makeDefElem -
* build a DefElem node
*
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 0e57f6c..c288e75 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -212,6 +212,9 @@ exprType(Node *expr)
case T_BooleanTest:
type = BOOLOID;
break;
+ case T_CacheExpr:
+ type = exprType((Node *) ((CacheExpr *) expr)->subexpr);
+ break;
case T_CoerceToDomain:
type = ((CoerceToDomain *) expr)->resulttype;
break;
@@ -792,6 +795,9 @@ exprCollation(Node *expr)
case T_BooleanTest:
coll = InvalidOid; /* result is always boolean */
break;
+ case T_CacheExpr:
+ coll = exprCollation((Node *) ((CacheExpr *) expr)->subexpr);
+ break;
case T_CoerceToDomain:
coll = ((CoerceToDomain *) expr)->resultcollid;
break;
@@ -999,6 +1005,10 @@ exprSetCollation(Node *expr, Oid collation)
case T_CurrentOfExpr:
Assert(!OidIsValid(collation)); /* result is always boolean */
break;
+ case T_CacheExpr:
+ /* I think this should never occur, but leave this here just in case */
+ elog(ERROR, "Can't set collation on CacheExpr");
+ break;
default:
elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
break;
@@ -1263,6 +1273,10 @@ exprLocation(Node *expr)
/* just use argument's location */
loc = exprLocation((Node *) ((BooleanTest *) expr)->arg);
break;
+ case T_CacheExpr:
+ /* original expression location */
+ loc = exprLocation((Node *) ((CacheExpr *) expr)->subexpr);
+ break;
case T_CoerceToDomain:
{
CoerceToDomain *cexpr = (CoerceToDomain *) expr;
@@ -1722,6 +1736,8 @@ expression_tree_walker(Node *node,
return walker(((NullTest *) node)->arg, context);
case T_BooleanTest:
return walker(((BooleanTest *) node)->arg, context);
+ case T_CacheExpr:
+ return walker(((CacheExpr *) node)->subexpr, context);
case T_CoerceToDomain:
return walker(((CoerceToDomain *) node)->arg, context);
case T_TargetEntry:
@@ -2374,6 +2390,16 @@ expression_tree_mutator(Node *node,
return (Node *) newnode;
}
break;
+ case T_CacheExpr:
+ {
+ CacheExpr *cache = (CacheExpr *) node;
+ CacheExpr *newnode;
+
+ FLATCOPY(newnode, cache, CacheExpr);
+ MUTATE(newnode->subexpr, cache->subexpr, Expr *);
+ return (Node *) newnode;
+ }
+ break;
case T_CoerceToDomain:
{
CoerceToDomain *ctest = (CoerceToDomain *) node;
@@ -2522,7 +2548,6 @@ expression_tree_mutator(Node *node,
return NULL;
}
-
/*
* query_tree_mutator --- initiate modification of a Query's expressions
*
@@ -2781,6 +2806,8 @@ bool
return walker(((NullTest *) node)->arg, context);
case T_BooleanTest:
return walker(((BooleanTest *) node)->arg, context);
+ case T_CacheExpr:
+ return walker(((CacheExpr *) node)->subexpr, context);
case T_JoinExpr:
{
JoinExpr *join = (JoinExpr *) node;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 0d0ce3c..131cc47 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1352,6 +1352,14 @@ _outBooleanTest(StringInfo str, BooleanTest *node)
}
static void
+_outCacheExpr(StringInfo str, CacheExpr *node)
+{
+ WRITE_NODE_TYPE("CACHEEXPR");
+
+ WRITE_NODE_FIELD(subexpr);
+}
+
+static void
_outCoerceToDomain(StringInfo str, CoerceToDomain *node)
{
WRITE_NODE_TYPE("COERCETODOMAIN");
@@ -2897,6 +2905,9 @@ _outNode(StringInfo str, void *obj)
case T_BooleanTest:
_outBooleanTest(str, obj);
break;
+ case T_CacheExpr:
+ _outCacheExpr(str, obj);
+ break;
case T_CoerceToDomain:
_outCoerceToDomain(str, obj);
break;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index baa90fa..e0ae622 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -48,6 +48,9 @@
#include "utils/typcache.h"
+bool enable_cacheexpr = true;
+
+
typedef struct
{
PlannerInfo *root;
@@ -61,6 +64,7 @@ typedef struct
List *active_fns;
Node *case_val;
bool estimate;
+ bool cache; /* insert cache nodes where possible? */
} eval_const_expressions_context;
typedef struct
@@ -96,43 +100,49 @@ static Relids find_nonnullable_rels_walker(Node *node, bool top_level);
static List *find_nonnullable_vars_walker(Node *node, bool top_level);
static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK);
static bool set_coercionform_dontcare_walker(Node *node, void *context);
+static Node *caching_const_expressions_mutator(Node *node,
+ eval_const_expressions_context *context);
static Node *eval_const_expressions_mutator(Node *node,
- eval_const_expressions_context *context);
+ eval_const_expressions_context *context,
+ bool *cachable);
static List *simplify_or_arguments(List *args,
eval_const_expressions_context *context,
- bool *haveNull, bool *forceTrue);
+ bool *haveNull, bool *forceTrue, bool *cachable);
static List *simplify_and_arguments(List *args,
eval_const_expressions_context *context,
- bool *haveNull, bool *forceFalse);
+ bool *haveNull, bool *forceFalse, bool *cachable);
static Node *simplify_boolean_equality(Oid opno, List *args);
static Expr *simplify_function(Expr *oldexpr, Oid funcid,
Oid result_type, int32 result_typmod, Oid result_collid,
Oid input_collid, List **args,
- bool has_named_args,
bool allow_inline,
- eval_const_expressions_context *context);
+ eval_const_expressions_context *context,
+ bool *cachable);
+static List *simplify_copy_function_arguments(List *old_args, Oid result_type,
+ HeapTuple func_tuple);
static List *reorder_function_arguments(List *args, Oid result_type,
- HeapTuple func_tuple,
- eval_const_expressions_context *context);
+ HeapTuple func_tuple);
static List *add_function_defaults(List *args, Oid result_type,
- HeapTuple func_tuple,
- eval_const_expressions_context *context);
+ HeapTuple func_tuple);
static List *fetch_function_defaults(HeapTuple func_tuple);
static void recheck_cast_function_args(List *args, Oid result_type,
HeapTuple func_tuple);
static Expr *evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
HeapTuple func_tuple,
- eval_const_expressions_context *context);
+ eval_const_expressions_context *context,
+ bool *cachable);
static Expr *inline_function(Oid funcid, Oid result_type, Oid result_collid,
Oid input_collid, List *args,
HeapTuple func_tuple,
- eval_const_expressions_context *context);
+ eval_const_expressions_context *context,
+ bool *cachable);
static Node *substitute_actual_parameters(Node *expr, int nargs, List *args,
int *usecounts);
static Node *substitute_actual_parameters_mutator(Node *node,
substitute_actual_parameters_context *context);
static void sql_inline_error_callback(void *arg);
+static Expr *insert_cache(Expr *expr);
static Expr *evaluate_expr(Expr *expr, Oid result_type, int32 result_typmod,
Oid result_collation);
static Query *substitute_actual_srf_parameters(Query *expr,
@@ -2065,7 +2075,9 @@ eval_const_expressions(PlannerInfo *root, Node *node)
context.active_fns = NIL; /* nothing being recursively simplified */
context.case_val = NULL; /* no CASE being examined */
context.estimate = false; /* safe transformations only */
- return eval_const_expressions_mutator(node, &context);
+ context.cache = enable_cacheexpr; /* XXX this should be passed in here as argument? */
+
+ return caching_const_expressions_mutator(node, &context);
}
/*--------------------
@@ -2089,6 +2101,7 @@ Node *
estimate_expression_value(PlannerInfo *root, Node *node)
{
eval_const_expressions_context context;
+ bool isCachable = false; /* short-circuit some checks */
context.boundParams = root->glob->boundParams; /* bound Params */
/* we do not need to mark the plan as depending on inlined functions */
@@ -2096,19 +2109,53 @@ estimate_expression_value(PlannerInfo *root, Node *node)
context.active_fns = NIL; /* nothing being recursively simplified */
context.case_val = NULL; /* no CASE being examined */
context.estimate = true; /* unsafe transformations OK */
- return eval_const_expressions_mutator(node, &context);
+ context.cache = false; /* no caching, planner only evaluates once */
+
+ return eval_const_expressions_mutator(node, &context, &isCachable);
+}
+
+/*
+ * Calls eval_const_expressions_mutator on the expression tree and
+ * automatically adds a CacheExpr node if the expression is cachable.
+ */
+static Node *
+caching_const_expressions_mutator(Node *node,
+ eval_const_expressions_context *context)
+{
+ bool isCachable = true;
+
+ if (node == NULL)
+ return NULL;
+
+ node = eval_const_expressions_mutator(node, context, &isCachable);
+ if (isCachable && context->cache)
+ node = (Node *) insert_cache((Expr *) node);
+
+ return node;
}
+
static Node *
eval_const_expressions_mutator(Node *node,
- eval_const_expressions_context *context)
+ eval_const_expressions_context *context,
+ bool *cachable)
{
+ if (context->cache)
+ Assert(*cachable == true);
+
if (node == NULL)
return NULL;
if (IsA(node, Param))
{
Param *param = (Param *) node;
+ /*
+ * Only externally-supplied parameters are stable. Other params are
+ * used for passing changing values within the executor
+ */
+ if (param->paramkind != PARAM_EXTERN)
+ *cachable = false;
+
/* Look to see if we've been given a value for this Param */
if (param->paramkind == PARAM_EXTERN &&
context->boundParams != NULL &&
@@ -2148,33 +2195,16 @@ eval_const_expressions_mutator(Node *node,
}
}
}
+
/* Not replaceable, so just copy the Param (no need to recurse) */
return (Node *) copyObject(param);
}
if (IsA(node, FuncExpr))
{
FuncExpr *expr = (FuncExpr *) node;
- List *args;
- bool has_named_args;
+ List *args = expr->args;
Expr *simple;
FuncExpr *newexpr;
- ListCell *lc;
-
- /*
- * Reduce constants in the FuncExpr's arguments, and check to see if
- * there are any named args.
- */
- args = NIL;
- has_named_args = false;
- foreach(lc, expr->args)
- {
- Node *arg = (Node *) lfirst(lc);
-
- arg = eval_const_expressions_mutator(arg, context);
- if (IsA(arg, NamedArgExpr))
- has_named_args = true;
- args = lappend(args, arg);
- }
/*
* Code for op/func reduction is pretty bulky, so split it out as a
@@ -2188,7 +2218,8 @@ eval_const_expressions_mutator(Node *node,
expr->funccollid,
expr->inputcollid,
&args,
- has_named_args, true, context);
+ true, context,
+ cachable); /* XXX untested */
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2212,20 +2243,11 @@ eval_const_expressions_mutator(Node *node,
if (IsA(node, OpExpr))
{
OpExpr *expr = (OpExpr *) node;
- List *args;
+ List *args = expr->args;
Expr *simple;
OpExpr *newexpr;
/*
- * Reduce constants in the OpExpr's arguments. We know args is either
- * NIL or a List node, so we can call expression_tree_mutator directly
- * rather than recursing to self.
- */
- args = (List *) expression_tree_mutator((Node *) expr->args,
- eval_const_expressions_mutator,
- (void *) context);
-
- /*
* Need to get OID of underlying function. Okay to scribble on input
* to this extent.
*/
@@ -2241,7 +2263,8 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, true, context);
+ true, context,
+ cachable);
if (simple) /* successfully simplified it */
return (Node *) simple;
@@ -2277,7 +2300,7 @@ eval_const_expressions_mutator(Node *node,
if (IsA(node, DistinctExpr))
{
DistinctExpr *expr = (DistinctExpr *) node;
- List *args;
+ List *args = expr->args;
ListCell *arg;
bool has_null_input = false;
bool all_null_input = true;
@@ -2286,15 +2309,6 @@ eval_const_expressions_mutator(Node *node,
DistinctExpr *newexpr;
/*
- * Reduce constants in the DistinctExpr's arguments. We know args is
- * either NIL or a List node, so we can call expression_tree_mutator
- * directly rather than recursing to self.
- */
- args = (List *) expression_tree_mutator((Node *) expr->args,
- eval_const_expressions_mutator,
- (void *) context);
-
- /*
* We must do our own check for NULLs because DistinctExpr has
* different results for NULL input than the underlying operator does.
*/
@@ -2339,7 +2353,11 @@ eval_const_expressions_mutator(Node *node,
expr->opcollid,
expr->inputcollid,
&args,
- false, false, context);
+ false, context,
+ cachable);
+
+ *cachable = false; /* XXX cachable? */
+
if (simple) /* successfully simplified it */
{
/*
@@ -2354,6 +2372,8 @@ eval_const_expressions_mutator(Node *node,
return (Node *) csimple;
}
}
+ else
+ *cachable = false; /* XXX cachable? */
/*
* The expression cannot be simplified any further, so build and
@@ -2384,7 +2404,8 @@ eval_const_expressions_mutator(Node *node,
bool forceTrue = false;
newargs = simplify_or_arguments(expr->args, context,
- &haveNull, &forceTrue);
+ &haveNull, &forceTrue,
+ cachable);
if (forceTrue)
return makeBoolConst(true, false);
if (haveNull)
@@ -2405,7 +2426,8 @@ eval_const_expressions_mutator(Node *node,
bool forceFalse = false;
newargs = simplify_and_arguments(expr->args, context,
- &haveNull, &forceFalse);
+ &haveNull, &forceFalse,
+ cachable);
if (forceFalse)
return makeBoolConst(false, false);
if (haveNull)
@@ -2425,7 +2447,8 @@ eval_const_expressions_mutator(Node *node,
Assert(list_length(expr->args) == 1);
arg = eval_const_expressions_mutator(linitial(expr->args),
- context);
+ context,
+ cachable);
/*
* Use negate_clause() to see if we can simplify away the
@@ -2448,6 +2471,7 @@ eval_const_expressions_mutator(Node *node,
* XXX should we ereport() here instead? Probably this routine should
* never be invoked after SubPlan creation.
*/
+ *cachable = false; /* XXX cachable? */
return node;
}
if (IsA(node, RelabelType))
@@ -2461,7 +2485,8 @@ eval_const_expressions_mutator(Node *node,
Node *arg;
arg = eval_const_expressions_mutator((Node *) relabel->arg,
- context);
+ context,
+ cachable);
/*
* If we find stacked RelabelTypes (eg, from foo :: int :: oid) we can
@@ -2495,7 +2520,6 @@ eval_const_expressions_mutator(Node *node,
if (IsA(node, CoerceViaIO))
{
CoerceViaIO *expr = (CoerceViaIO *) node;
- Expr *arg;
List *args;
Oid outfunc;
bool outtypisvarlena;
@@ -2503,13 +2527,14 @@ eval_const_expressions_mutator(Node *node,
Oid intypioparam;
Expr *simple;
CoerceViaIO *newexpr;
+ bool isCachable = true;
+
+ *cachable = false; /* XXX cachable? */
/*
* Reduce constants in the CoerceViaIO's argument.
*/
- arg = (Expr *) eval_const_expressions_mutator((Node *) expr->arg,
- context);
- args = list_make1(arg);
+ args = list_make1(expr->arg);
/*
* CoerceViaIO represents calling the source type's output function
@@ -2520,7 +2545,7 @@ eval_const_expressions_mutator(Node *node,
* Note that the coercion functions are assumed not to care about
* input collation, so we just pass InvalidOid for that.
*/
- getTypeOutputInfo(exprType((Node *) arg), &outfunc, &outtypisvarlena);
+ getTypeOutputInfo(exprType((Node *) expr->arg), &outfunc, &outtypisvarlena);
getTypeInputInfo(expr->resulttype, &infunc, &intypioparam);
simple = simplify_function(NULL,
@@ -2529,9 +2554,11 @@ eval_const_expressions_mutator(Node *node,
InvalidOid,
InvalidOid,
&args,
- false, true, context);
+ true, context,
+ &isCachable);
if (simple) /* successfully simplified output fn */
{
+ isCachable = true;
/*
* Input functions may want 1 to 3 arguments. We always supply
* all three, trusting that nothing downstream will complain.
@@ -2550,7 +2577,8 @@ eval_const_expressions_mutator(Node *node,
expr->resultcollid,
InvalidOid,
&args,
- false, true, context);
+ true, context,
+ &isCachable);
if (simple) /* successfully simplified input fn */
return (Node *) simple;
}
@@ -2561,7 +2589,7 @@ eval_const_expressions_mutator(Node *node,
* argument.
*/
newexpr = makeNode(CoerceViaIO);
- newexpr->arg = arg;
+ newexpr->arg = (Expr *) linitial(args); /* XXX this didn't use simplified arg before? */
newexpr->resulttype = expr->resulttype;
newexpr->resultcollid = expr->resultcollid;
newexpr->coerceformat = expr->coerceformat;
@@ -2579,7 +2607,8 @@ eval_const_expressions_mutator(Node *node,
* new ArrayCoerceExpr.
*/
arg = (Expr *) eval_const_expressions_mutator((Node *) expr->arg,
- context);
+ context,
+ cachable);
newexpr = makeNode(ArrayCoerceExpr);
newexpr->arg = arg;
@@ -2603,11 +2632,24 @@ eval_const_expressions_mutator(Node *node,
newexpr->resulttypmod,
newexpr->resultcollid);
+ /*
+ * If the argument is cachable, but conversion isn't, insert a
+ * CacheExpr above the argument
+ */
+ if (context->cache && arg && *cachable &&
+ (OidIsValid(newexpr->elemfuncid) &&
+ func_volatile(newexpr->elemfuncid) == PROVOLATILE_VOLATILE))
+ {
+ *cachable = false;
+ newexpr->arg = insert_cache(arg);
+ }
+
/* Else we must return the partially-simplified node */
return (Node *) newexpr;
}
if (IsA(node, CollateExpr))
{
+ /* XXX cachable? */
/*
* If we can simplify the input to a constant, then we don't need the
* CollateExpr node at all: just change the constcollid field of the
@@ -2619,7 +2661,8 @@ eval_const_expressions_mutator(Node *node,
Node *arg;
arg = eval_const_expressions_mutator((Node *) collate->arg,
- context);
+ context,
+ cachable); /* XXX cachable? */
if (arg && IsA(arg, Const))
{
@@ -2691,8 +2734,10 @@ eval_const_expressions_mutator(Node *node,
Node *defresult = NULL;
ListCell *arg;
+ *cachable = false;
+
/* Simplify the test expression, if any */
- newarg = eval_const_expressions_mutator((Node *) caseexpr->arg,
+ newarg = caching_const_expressions_mutator((Node *) caseexpr->arg,
context);
/* Set up for contained CaseTestExpr nodes */
@@ -2718,8 +2763,8 @@ eval_const_expressions_mutator(Node *node,
/* Simplify this alternative's test condition */
casecond =
- eval_const_expressions_mutator((Node *) oldcasewhen->expr,
- context);
+ caching_const_expressions_mutator((Node *) oldcasewhen->expr,
+ context);
/*
* If the test condition is constant FALSE (or NULL), then drop
@@ -2738,7 +2783,7 @@ eval_const_expressions_mutator(Node *node,
/* Simplify this alternative's result value */
caseresult =
- eval_const_expressions_mutator((Node *) oldcasewhen->result,
+ caching_const_expressions_mutator((Node *) oldcasewhen->result,
context);
/* If non-constant test condition, emit a new WHEN node */
@@ -2764,7 +2809,7 @@ eval_const_expressions_mutator(Node *node,
/* Simplify the default result, unless we replaced it above */
if (!const_true_cond)
defresult =
- eval_const_expressions_mutator((Node *) caseexpr->defresult,
+ caching_const_expressions_mutator((Node *) caseexpr->defresult,
context);
context->case_val = save_case_val;
@@ -2792,7 +2837,10 @@ eval_const_expressions_mutator(Node *node,
if (context->case_val)
return copyObject(context->case_val);
else
+ {
+ *cachable = false;
return copyObject(node);
+ }
}
if (IsA(node, ArrayExpr))
{
@@ -2802,13 +2850,15 @@ eval_const_expressions_mutator(Node *node,
List *newelems;
ListCell *element;
+ *cachable = false; /* XXX cachable! */
+
newelems = NIL;
foreach(element, arrayexpr->elements)
{
Node *e;
- e = eval_const_expressions_mutator((Node *) lfirst(element),
- context);
+ e = caching_const_expressions_mutator((Node *) lfirst(element),
+ context);
if (!IsA(e, Const))
all_const = false;
newelems = lappend(newelems, e);
@@ -2837,13 +2887,15 @@ eval_const_expressions_mutator(Node *node,
List *newargs;
ListCell *arg;
+ *cachable = false; /* XXX cachable? */
+
newargs = NIL;
foreach(arg, coalesceexpr->args)
{
Node *e;
- e = eval_const_expressions_mutator((Node *) lfirst(arg),
- context);
+ e = caching_const_expressions_mutator((Node *) lfirst(arg),
+ context);
/*
* We can remove null constants from the list. For a non-null
@@ -2894,8 +2946,10 @@ eval_const_expressions_mutator(Node *node,
FieldSelect *newfselect;
Node *arg;
- arg = eval_const_expressions_mutator((Node *) fselect->arg,
- context);
+ *cachable = false; /* XXX cachable? */
+
+ arg = caching_const_expressions_mutator((Node *) fselect->arg,
+ context);
if (arg && IsA(arg, Var) &&
((Var *) arg)->varattno == InvalidAttrNumber)
{
@@ -2947,7 +3001,8 @@ eval_const_expressions_mutator(Node *node,
Node *arg;
arg = eval_const_expressions_mutator((Node *) ntest->arg,
- context);
+ context,
+ cachable);
if (arg && IsA(arg, RowExpr))
{
/*
@@ -3030,7 +3085,8 @@ eval_const_expressions_mutator(Node *node,
Node *arg;
arg = eval_const_expressions_mutator((Node *) btest->arg,
- context);
+ context,
+ cachable);
if (arg && IsA(arg, Const))
{
Const *carg = (Const *) arg;
@@ -3075,7 +3131,7 @@ eval_const_expressions_mutator(Node *node,
newbtest->booltesttype = btest->booltesttype;
return (Node *) newbtest;
}
- if (IsA(node, PlaceHolderVar) &&context->estimate)
+ if (IsA(node, PlaceHolderVar) && context->estimate)
{
/*
* In estimation mode, just strip the PlaceHolderVar node altogether;
@@ -3087,17 +3143,35 @@ eval_const_expressions_mutator(Node *node,
PlaceHolderVar *phv = (PlaceHolderVar *) node;
return eval_const_expressions_mutator((Node *) phv->phexpr,
- context);
+ context,
+ cachable);
+ }
+ if (IsA(node, CacheExpr))
+ {
+ /*
+ * The planner asked us to re-simplify a simplified expression tree.
+ * Strip CacheExpr nodes since the planner only evaluates the
+ * expression once.
+ */
+ CacheExpr *cache = (CacheExpr *) node;
+
+ Assert(context->estimate && !context->cache);
+
+ return eval_const_expressions_mutator((Node *) cache->subexpr, context, cachable);
}
+ if (!IsA(node, Const))
+ *cachable = false; /* Everything else is not cachable */
+
/*
* For any node type not handled above, we recurse using
* expression_tree_mutator, which will copy the node unchanged but try to
* simplify its arguments (if any) using this routine. For example: we
* cannot eliminate an ArrayRef node, but we might be able to simplify
- * constant expressions in its subscripts.
+ * or cache constant expressions in its subscripts.
*/
- return expression_tree_mutator(node, eval_const_expressions_mutator,
+ return expression_tree_mutator(node,
+ caching_const_expressions_mutator,
(void *) context);
}
@@ -3119,16 +3193,24 @@ eval_const_expressions_mutator(Node *node,
* The output arguments *haveNull and *forceTrue must be initialized FALSE
* by the caller. They will be set TRUE if a null constant or true constant,
* respectively, is detected anywhere in the argument list.
+ *
+ * XXX update comment
*/
static List *
simplify_or_arguments(List *args,
eval_const_expressions_context *context,
- bool *haveNull, bool *forceTrue)
+ bool *haveNull, bool *forceTrue, bool *cachable)
{
- List *newargs = NIL;
+ List *nocache_args = NIL;
+ List *cachable_args = NIL;
List *unprocessed_args;
/*
+ * XXX we could group cachable expressions together here and cache the
+ * whole list as one boolean expression
+ */
+
+ /*
* Since the parser considers OR to be a binary operator, long OR lists
* become deeply nested expressions. We must flatten these into long
* argument lists of a single OR operator. To avoid blowing out the stack
@@ -3140,6 +3222,7 @@ simplify_or_arguments(List *args,
while (unprocessed_args)
{
Node *arg = (Node *) linitial(unprocessed_args);
+ bool isCachable = true; /* XXX return to caller */
unprocessed_args = list_delete_first(unprocessed_args);
@@ -3162,7 +3245,7 @@ simplify_or_arguments(List *args,
}
/* If it's not an OR, simplify it */
- arg = eval_const_expressions_mutator(arg, context);
+ arg = eval_const_expressions_mutator(arg, context, &isCachable);
/*
* It is unlikely but not impossible for simplification of a non-OR
@@ -3203,11 +3286,42 @@ simplify_or_arguments(List *args,
continue;
}
- /* else emit the simplified arg into the result list */
- newargs = lappend(newargs, arg);
+ /* else emit the simplified arg into the result list XXX update comment */
+ if (isCachable)
+ cachable_args = lappend(cachable_args, arg);
+ else
+ nocache_args = lappend(nocache_args, arg);
}
- return newargs;
+ if (cachable_args && nocache_args)
+ {
+ Expr *arg;
+
+ /* Build a new expression for cachable sub-list */
+ if (list_length(cachable_args) == 1)
+ arg = linitial(cachable_args); /* XXX leaks list header */
+ else
+ arg = makeBoolExpr(OR_EXPR, cachable_args, -1);
+
+ /*
+ * Assume that the cachable expression is cheaper to evaluate, so put
+ * it first
+ */
+ nocache_args = lcons(insert_cache(arg), nocache_args);
+
+ *cachable = false;
+ return nocache_args;
+ }
+ else if (nocache_args)
+ {
+ *cachable = false;
+ return nocache_args;
+ }
+ else
+ {
+ *cachable = true;
+ return cachable_args;
+ }
}
/*
@@ -3228,13 +3342,16 @@ simplify_or_arguments(List *args,
* The output arguments *haveNull and *forceFalse must be initialized FALSE
* by the caller. They will be set TRUE if a null constant or false constant,
* respectively, is detected anywhere in the argument list.
+ *
+ * XXX update comment
*/
static List *
simplify_and_arguments(List *args,
eval_const_expressions_context *context,
- bool *haveNull, bool *forceFalse)
+ bool *haveNull, bool *forceFalse, bool *cachable)
{
- List *newargs = NIL;
+ List *nocache_args = NIL;
+ List *cachable_args = NIL;
List *unprocessed_args;
/* See comments in simplify_or_arguments */
@@ -3242,6 +3359,7 @@ simplify_and_arguments(List *args,
while (unprocessed_args)
{
Node *arg = (Node *) linitial(unprocessed_args);
+ bool isCachable = true; /* XXX return to caller */
unprocessed_args = list_delete_first(unprocessed_args);
@@ -3264,7 +3382,7 @@ simplify_and_arguments(List *args,
}
/* If it's not an AND, simplify it */
- arg = eval_const_expressions_mutator(arg, context);
+ arg = eval_const_expressions_mutator(arg, context, &isCachable);
/*
* It is unlikely but not impossible for simplification of a non-AND
@@ -3306,10 +3424,41 @@ simplify_and_arguments(List *args,
}
/* else emit the simplified arg into the result list */
- newargs = lappend(newargs, arg);
+ if (isCachable)
+ cachable_args = lappend(cachable_args, arg);
+ else
+ nocache_args = lappend(nocache_args, arg);
}
- return newargs;
+ if (cachable_args && nocache_args)
+ {
+ Expr *arg;
+
+ /* Build a new expression for cachable sub-list */
+ if (list_length(cachable_args) == 1)
+ arg = linitial(cachable_args); /* XXX leaks list header */
+ else
+ arg = makeBoolExpr(AND_EXPR, cachable_args, -1);
+
+ /*
+ * Assume that the cachable expression is cheaper to evaluate, so put
+ * it first
+ */
+ nocache_args = lcons(insert_cache(arg), nocache_args);
+
+ *cachable = false;
+ return nocache_args;
+ }
+ else if (nocache_args)
+ {
+ *cachable = false;
+ return nocache_args;
+ }
+ else
+ {
+ *cachable = true;
+ return cachable_args;
+ }
}
/*
@@ -3384,7 +3533,7 @@ simplify_boolean_equality(Oid opno, List *args)
* Inputs are the original expression (can be NULL), function OID, actual
* result type OID (which is needed for polymorphic functions), result typmod,
* result collation, the input collation to use for the function, the
- * pre-simplified argument list, and some flags; also the context data for
+ * un-simplified argument list, and some flags; also the context data for
* eval_const_expressions. In common cases, several of the arguments could be
* derived from the original expression. Sending them separately avoids
* duplicating NodeTag-specific knowledge, and it's necessary for CoerceViaIO.
@@ -3403,14 +3552,17 @@ simplify_boolean_equality(Oid opno, List *args)
static Expr *
simplify_function(Expr *oldexpr, Oid funcid,
Oid result_type, int32 result_typmod, Oid result_collid,
- Oid input_collid, List **args,
- bool has_named_args,
+ Oid input_collid, List **old_args,
bool allow_inline,
- eval_const_expressions_context *context)
+ eval_const_expressions_context *context,
+ bool *cachable)
{
HeapTuple func_tuple;
Expr *newexpr;
Oid transform;
+ ListCell *lc;
+ List *args = NIL;
+ List *cachable_args = NIL; /* XXX use bitmapset instead? */
/*
* We have three strategies for simplification: execute the function to
@@ -3424,19 +3576,37 @@ simplify_function(Expr *oldexpr, Oid funcid,
if (!HeapTupleIsValid(func_tuple))
elog(ERROR, "cache lookup failed for function %u", funcid);
+ if (oldexpr && IsA(oldexpr, FuncExpr))
+ /*
+ * Reorder named arguments and add defaults if needed. Returns a
+ * copied list, so we can mutate it later.
+ */
+ args = simplify_copy_function_arguments(*old_args, result_type, func_tuple);
+ else
+ /* Copy argument list before we start mutating it */
+ args = list_copy(*old_args);
+
+ /* Reduce constants in the FuncExpr's arguments */
+ foreach(lc, args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+ bool isCachable = true;
+
+ arg = eval_const_expressions_mutator(arg, context, &isCachable);
+ lfirst(lc) = arg;
+
+ if (isCachable && context->cache)
+ cachable_args = lappend(cachable_args, arg);
+ else
+ *cachable = false; /* One bad arg spoils the whole cache */
+ }
+
/*
- * While we have the tuple, reorder named arguments and add default
- * arguments if needed.
+ * evaluate_function tells us about the cachability of the function call
*/
- if (has_named_args)
- *args = reorder_function_arguments(*args, result_type, func_tuple,
- context);
- else if (((Form_pg_proc) GETSTRUCT(func_tuple))->pronargs > list_length(*args))
- *args = add_function_defaults(*args, result_type, func_tuple, context);
-
newexpr = evaluate_function(funcid, result_type, result_typmod,
- result_collid, input_collid, *args,
- func_tuple, context);
+ result_collid, input_collid, args,
+ func_tuple, context, cachable);
/*
* Some functions calls can be simplified at plan time based on properties
@@ -3473,30 +3643,112 @@ simplify_function(Expr *oldexpr, Oid funcid,
PointerGetDatum(oldexpr)));
if (!newexpr && allow_inline)
+ {
+ /*
+ * The inlined expression may be cachable regardless of the above, if
+ * the function's volatility was mis-labeled or if volatile parts are
+ * removed (possible due to constant folding of conditionals).
+ *
+ * inline_function() also takes care of caching all cachable subtrees
+ */
+ bool isCachable = true;
newexpr = inline_function(funcid, result_type, result_collid,
- input_collid, *args,
- func_tuple, context);
+ input_collid, args,
+ func_tuple, context, &isCachable);
+
+ if (newexpr)
+ *cachable = isCachable;
+ }
ReleaseSysCache(func_tuple);
+ /*
+ * If function call can't be cached/inlined, cache all cachable arguments
+ */
+ if (!newexpr && !(*cachable) && cachable_args != NIL)
+ {
+ foreach(lc, args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+
+ /* XXX ugly as hell and N^2 to the number of arguments */
+ if (list_member(cachable_args, arg))
+ lfirst(lc) = insert_cache((Expr *) arg);
+ }
+ }
+
+ /* Argument processing done, give it back to the caller */
+ *old_args = args;
+
return newexpr;
}
/*
+ * This function prepares a function's argument list -- converting
+ * named-notation argument list into positional notation while adding any
+ * needed default argument expressions.
+ *
+ * Always returns a copy of the argument list, the original list is not
+ * modified.
+ */
+static List *
+simplify_copy_function_arguments(List *old_args, Oid result_type,
+ HeapTuple func_tuple)
+{
+ List *args = NIL;
+ ListCell *lc;
+ bool has_named_args = false;
+ int nargs_before;
+
+ /* Do we need to reorder named arguments? */
+ foreach(lc, old_args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+
+ if (IsA(arg, NamedArgExpr))
+ {
+ has_named_args = true;
+ break;
+ }
+ }
+
+ nargs_before = list_length(old_args);
+
+ /*
+ * Reorder named arguments and add default arguments if needed.
+ */
+ if (has_named_args)
+ args = reorder_function_arguments(old_args, result_type, func_tuple);
+
+ else
+ {
+ args = list_copy(old_args);
+
+ /* Append missing default arguments to the list */
+ if (((Form_pg_proc) GETSTRUCT(func_tuple))->pronargs > list_length(args))
+ args = add_function_defaults(args, result_type, func_tuple);
+ }
+
+ if (list_length(args) != nargs_before)
+ /* Added defaults may need casts */
+ recheck_cast_function_args(args, result_type, func_tuple);
+
+ return args;
+}
+
+/*
* reorder_function_arguments: convert named-notation args to positional args
*
* This function also inserts default argument values as needed, since it's
* impossible to form a truly valid positional call without that.
*/
static List *
-reorder_function_arguments(List *args, Oid result_type, HeapTuple func_tuple,
- eval_const_expressions_context *context)
+reorder_function_arguments(List *args, Oid result_type, HeapTuple func_tuple)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
int pronargs = funcform->pronargs;
int nargsprovided = list_length(args);
Node *argarray[FUNC_MAX_ARGS];
- Bitmapset *defargnumbers;
ListCell *lc;
int i;
@@ -3530,7 +3782,6 @@ reorder_function_arguments(List *args, Oid result_type, HeapTuple func_tuple,
* Fetch default expressions, if needed, and insert into array at proper
* locations (they aren't necessarily consecutive or all used)
*/
- defargnumbers = NULL;
if (nargsprovided < pronargs)
{
List *defaults = fetch_function_defaults(func_tuple);
@@ -3539,10 +3790,7 @@ reorder_function_arguments(List *args, Oid result_type, HeapTuple func_tuple,
foreach(lc, defaults)
{
if (argarray[i] == NULL)
- {
argarray[i] = (Node *) lfirst(lc);
- defargnumbers = bms_add_member(defargnumbers, i);
- }
i++;
}
}
@@ -3555,32 +3803,6 @@ reorder_function_arguments(List *args, Oid result_type, HeapTuple func_tuple,
args = lappend(args, argarray[i]);
}
- /* Recheck argument types and add casts if needed */
- recheck_cast_function_args(args, result_type, func_tuple);
-
- /*
- * Lastly, we have to recursively simplify the defaults we just added (but
- * don't recurse on the args passed in, as we already did those). This
- * isn't merely an optimization, it's *necessary* since there could be
- * functions with named or defaulted arguments down in there.
- *
- * Note that we do this last in hopes of simplifying any typecasts that
- * were added by recheck_cast_function_args --- there shouldn't be any new
- * casts added to the explicit arguments, but casts on the defaults are
- * possible.
- */
- if (defargnumbers != NULL)
- {
- i = 0;
- foreach(lc, args)
- {
- if (bms_is_member(i, defargnumbers))
- lfirst(lc) = eval_const_expressions_mutator((Node *) lfirst(lc),
- context);
- i++;
- }
- }
-
return args;
}
@@ -3591,20 +3813,17 @@ reorder_function_arguments(List *args, Oid result_type, HeapTuple func_tuple,
* and so we know we just need to add defaults at the end.
*/
static List *
-add_function_defaults(List *args, Oid result_type, HeapTuple func_tuple,
- eval_const_expressions_context *context)
+add_function_defaults(List *args, Oid result_type, HeapTuple func_tuple)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
- int nargsprovided = list_length(args);
List *defaults;
int ndelete;
- ListCell *lc;
/* Get all the default expressions from the pg_proc tuple */
defaults = fetch_function_defaults(func_tuple);
/* Delete any unused defaults from the list */
- ndelete = nargsprovided + list_length(defaults) - funcform->pronargs;
+ ndelete = list_length(args) + list_length(defaults) - funcform->pronargs;
if (ndelete < 0)
elog(ERROR, "not enough default arguments");
while (ndelete-- > 0)
@@ -3613,28 +3832,6 @@ add_function_defaults(List *args, Oid result_type, HeapTuple func_tuple,
/* And form the combined argument list */
args = list_concat(args, defaults);
- /* Recheck argument types and add casts if needed */
- recheck_cast_function_args(args, result_type, func_tuple);
-
- /*
- * Lastly, we have to recursively simplify the defaults we just added (but
- * don't recurse on the args passed in, as we already did those). This
- * isn't merely an optimization, it's *necessary* since there could be
- * functions with named or defaulted arguments down in there.
- *
- * Note that we do this last in hopes of simplifying any typecasts that
- * were added by recheck_cast_function_args --- there shouldn't be any new
- * casts added to the explicit arguments, but casts on the defaults are
- * possible.
- */
- foreach(lc, args)
- {
- if (nargsprovided-- > 0)
- continue; /* skip original arg positions */
- lfirst(lc) = eval_const_expressions_mutator((Node *) lfirst(lc),
- context);
- }
-
return args;
}
@@ -3724,7 +3921,8 @@ static Expr *
evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
Oid result_collid, Oid input_collid, List *args,
HeapTuple func_tuple,
- eval_const_expressions_context *context)
+ eval_const_expressions_context *context,
+ bool *cachable)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
bool has_nonconst_input = false;
@@ -3736,7 +3934,10 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
* Can't simplify if it returns a set.
*/
if (funcform->proretset)
+ {
+ *cachable = false;
return NULL;
+ }
/*
* Can't simplify if it returns RECORD. The immediate problem is that it
@@ -3748,9 +3949,17 @@ evaluate_function(Oid funcid, Oid result_type, int32 result_typmod,
* the case of a NULL function result there doesn't seem to be any clean
* way to fix that. In view of the likelihood of there being still other
* gotchas, seems best to leave the function call unreduced.
+ *
+ * XXX does the above apply to "cachable" too?
*/
if (funcform->prorettype == RECORDOID)
+ {
+ *cachable = false;
return NULL;
+ }
+
+ if (funcform->provolatile == PROVOLATILE_VOLATILE)
+ *cachable = false;
/*
* Check for constant inputs and especially constant-NULL inputs.
@@ -3847,7 +4056,8 @@ static Expr *
inline_function(Oid funcid, Oid result_type, Oid result_collid,
Oid input_collid, List *args,
HeapTuple func_tuple,
- eval_const_expressions_context *context)
+ eval_const_expressions_context *context,
+ bool *cachable)
{
Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple);
char *src;
@@ -4126,7 +4336,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
* the current function to the context list of active functions.
*/
context->active_fns = lcons_oid(funcid, context->active_fns);
- newexpr = eval_const_expressions_mutator(newexpr, context);
+ newexpr = eval_const_expressions_mutator(newexpr, context, cachable);
context->active_fns = list_delete_first(context->active_fns);
error_context_stack = sqlerrcontext.previous;
@@ -4206,6 +4416,22 @@ sql_inline_error_callback(void *arg)
}
/*
+ * XXX
+ */
+static Expr *
+insert_cache(Expr *expr)
+{
+ /* Don't cache obviously cheap expressions */
+ if (IsA(expr, Const))
+ return expr;
+ if (IsA(expr, Param))
+ return expr;
+ Assert(!IsA(expr, CacheExpr));
+
+ return (Expr *) makeCacheExpr(expr);
+}
+
+/*
* evaluate_expr: pre-evaluate a constant expression
*
* We use the executor's routine ExecEvalExpr() to avoid duplication of
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 65d03ad..75a7292 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -323,6 +323,7 @@ transformExpr(ParseState *pstate, Node *expr)
case T_CollateExpr:
case T_CaseTestExpr:
case T_ArrayExpr:
+ /* case T_CacheExpr: XXX ??? */
case T_CoerceToDomain:
case T_CoerceToDomainValue:
case T_SetToDefault:
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c112a9c..6118e7f 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -4559,8 +4559,12 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
/* function-like: name(..) or name[..] */
return true;
- /* CASE keywords act as parentheses */
+ case T_CacheExpr:
+ /* hidden from user */
+ return true;
+
case T_CaseExpr:
+ /* CASE keywords act as parentheses */
return true;
case T_FieldSelect:
@@ -5681,6 +5685,20 @@ get_rule_expr(Node *node, deparse_context *context,
}
break;
+ case T_CacheExpr:
+ {
+ CacheExpr *cache = (CacheExpr *) node;
+
+#ifdef DEBUG_CACHEEXPR
+ appendStringInfo(buf, "CACHE[");
+ get_rule_expr((Node *) cache->subexpr, context, true);
+ appendStringInfoChar(buf, ']');
+#else
+ get_rule_expr((Node *) cache->subexpr, context, true);
+#endif
+ }
+ break;
+
case T_CoerceToDomain:
{
CoerceToDomain *ctest = (CoerceToDomain *) node;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a71729c..f5647e8 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -756,6 +756,16 @@ static struct config_bool ConfigureNamesBool[] =
NULL, NULL, NULL
},
{
+ /* XXX Probably won't need this, but it's useful for debugging */
+ {"enable_cacheexpr", PGC_USERSET, QUERY_TUNING_METHOD,
+ gettext_noop("Enables caching of stable execution-time-constant expressions."),
+ NULL
+ },
+ &enable_cacheexpr,
+ true,
+ NULL, NULL, NULL
+ },
+ {
{"geqo", PGC_USERSET, QUERY_TUNING_GEQO,
gettext_noop("Enables genetic query optimization."),
gettext_noop("This algorithm attempts to do planning without "
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index b3eed7d..fd6eb06 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -672,6 +672,22 @@ typedef struct FuncExprState
} FuncExprState;
/* ----------------
+ * CacheExprState node
+ *
+ * Takes care of caching execution-time constants (that cannot be cached at
+ * plan-time).
+ * ----------------
+ */
+typedef struct CacheExprState
+{
+ ExprState xprstate;
+ ExprState *subexpr; /* state of sub-expression */
+
+ Datum result; /* cached result */
+ bool isNull; /* is result NULL? */
+} CacheExprState;
+
+/* ----------------
* ScalarArrayOpExprState node
*
* This is a FuncExprState plus some additional data.
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 532681f..699b5ce 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -73,6 +73,7 @@ extern TypeName *makeTypeNameFromOid(Oid typeOid, int32 typmod);
extern FuncExpr *makeFuncExpr(Oid funcid, Oid rettype, List *args,
Oid funccollid, Oid inputcollid, CoercionForm fformat);
+extern CacheExpr *makeCacheExpr(Expr *subexpr);
extern DefElem *makeDefElem(char *name, Node *arg);
extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index ecf62b3..5649d27 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -170,6 +170,7 @@ typedef enum NodeTag
T_JoinExpr,
T_FromExpr,
T_IntoClause,
+ T_CacheExpr,
/*
* TAGS FOR EXPRESSION STATE NODES (execnodes.h)
@@ -203,6 +204,7 @@ typedef enum NodeTag
T_NullTestState,
T_CoerceToDomainState,
T_DomainConstraintState,
+ T_CacheExprState,
/*
* TAGS FOR PLANNER NODES (relation.h)
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f1e20ef..bd2c73e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1024,6 +1024,17 @@ typedef struct BooleanTest
} BooleanTest;
/*
+ * CacheExpr
+ *
+ * XXX
+ */
+typedef struct CacheExpr
+{
+ Expr xpr;
+ Expr *subexpr;
+} CacheExpr;
+
+/*
* CoerceToDomain
*
* CoerceToDomain represents the operation of coercing a value to a domain
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 604df33..02b91e8 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -60,6 +60,7 @@ extern bool enable_nestloop;
extern bool enable_material;
extern bool enable_mergejoin;
extern bool enable_hashjoin;
+extern bool enable_cacheexpr; /* XXX probably a wrong place for this */
extern int constraint_exclusion;
extern double clamp_row_est(double nrows);
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index df785c9..b90cc4e 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -5618,6 +5618,13 @@ exec_simple_check_node(Node *node)
case T_BooleanTest:
return exec_simple_check_node((Node *) ((BooleanTest *) node)->arg);
+ case T_CacheExpr:
+ /*
+ * XXX what do we do with simple expressions? Remove CacheExpr
+ * nodes after we discover that the expr is simple?
+ */
+ return FALSE;
+
case T_CoerceToDomain:
return exec_simple_check_node((Node *) ((CoerceToDomain *) node)->arg);
diff --git a/src/test/regress/expected/cache.out b/src/test/regress/expected/cache.out
new file mode 100644
index 0000000..1fa3bf6
--- /dev/null
+++ b/src/test/regress/expected/cache.out
@@ -0,0 +1,411 @@
+--
+-- Test cachable expressions
+--
+-- If the NOTICE outputs of these functions change, you've probably broken
+-- something with the CacheExpr optimization
+--
+create function stable_true() returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE TRUE'; return true; end;$$;
+create function volatile_true() returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE TRUE'; return true; end;$$;
+create function stable_false() returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE FALSE'; return false; end;$$;
+create function volatile_false() returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE FALSE'; return false; end;$$;
+create table two (i int);
+insert into two values (1), (2);
+-- Boolean expressions
+select stable_false() or volatile_true() or stable_true() as b from two;
+NOTICE: STABLE FALSE
+NOTICE: STABLE TRUE
+ b
+---
+ t
+ t
+(2 rows)
+
+select stable_true() or volatile_false() or stable_false() as b from two;
+NOTICE: STABLE TRUE
+ b
+---
+ t
+ t
+(2 rows)
+
+select stable_false() or volatile_true() as b from two;
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE TRUE
+ b
+---
+ t
+ t
+(2 rows)
+
+select stable_false() or stable_false() or volatile_true() as b from two;
+NOTICE: STABLE FALSE
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE TRUE
+ b
+---
+ t
+ t
+(2 rows)
+
+select volatile_true() or volatile_false() or stable_false() as b from two;
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE TRUE
+ b
+---
+ t
+ t
+(2 rows)
+
+select volatile_false() or volatile_true() or stable_false() as b from two;
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE TRUE
+ b
+---
+ t
+ t
+(2 rows)
+
+select stable_true() and volatile_false() and stable_false() as b from two;
+NOTICE: STABLE TRUE
+NOTICE: STABLE FALSE
+ b
+---
+ f
+ f
+(2 rows)
+
+select stable_false() and volatile_true() and stable_true() as b from two;
+NOTICE: STABLE FALSE
+ b
+---
+ f
+ f
+(2 rows)
+
+select stable_true() and volatile_false() as b from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE FALSE
+ b
+---
+ f
+ f
+(2 rows)
+
+select stable_true() and stable_true() and volatile_false() as b from two;
+NOTICE: STABLE TRUE
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE FALSE
+ b
+---
+ f
+ f
+(2 rows)
+
+select volatile_true() and volatile_false() and stable_true() as b from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+ b
+---
+ f
+ f
+(2 rows)
+
+select volatile_false() and volatile_true() and stable_true() as b from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE FALSE
+ b
+---
+ f
+ f
+(2 rows)
+
+select not stable_true() as b from two;
+NOTICE: STABLE TRUE
+ b
+---
+ f
+ f
+(2 rows)
+
+select not volatile_true() as b from two;
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE TRUE
+ b
+---
+ f
+ f
+(2 rows)
+
+prepare param_test(bool) as select $1 or stable_false() or volatile_true() as b from two;
+execute param_test(true);
+ b
+---
+ t
+ t
+(2 rows)
+
+execute param_test(false);
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE TRUE
+ b
+---
+ t
+ t
+(2 rows)
+
+-- Function calls
+create function stable(bool) returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE(%)', $1; return $1; end;$$;
+create function volatile(bool) returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE(%)', $1; return $1; end;$$;
+select volatile(volatile_true()) from two;
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE(t)
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE(t)
+ volatile
+----------
+ t
+ t
+(2 rows)
+
+select stable(stable_true()) from two;
+NOTICE: STABLE TRUE
+NOTICE: STABLE(t)
+ stable
+--------
+ t
+ t
+(2 rows)
+
+select stable(volatile_true()) from two;
+NOTICE: VOLATILE TRUE
+NOTICE: STABLE(t)
+NOTICE: VOLATILE TRUE
+NOTICE: STABLE(t)
+ stable
+--------
+ t
+ t
+(2 rows)
+
+select volatile(stable_true()) from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE(t)
+NOTICE: VOLATILE(t)
+ volatile
+----------
+ t
+ t
+(2 rows)
+
+create function stable(bool, bool) returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE(%, %)', $1, $2; return $1; end;$$;
+create function volatile(bool, bool) returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE(%, %)', $1, $2; return $1; end;$$;
+select stable(volatile_true(), volatile_false()) from two;
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE(t, f)
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE(t, f)
+ stable
+--------
+ t
+ t
+(2 rows)
+
+select stable(stable_true(), volatile_false()) from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE(t, f)
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE(t, f)
+ stable
+--------
+ t
+ t
+(2 rows)
+
+select stable(stable_true(), stable_false()) from two;
+NOTICE: STABLE TRUE
+NOTICE: STABLE FALSE
+NOTICE: STABLE(t, f)
+ stable
+--------
+ t
+ t
+(2 rows)
+
+select volatile(volatile_true(), volatile_false()) from two;
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE(t, f)
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE(t, f)
+ volatile
+----------
+ t
+ t
+(2 rows)
+
+select volatile(stable_true(), volatile_false()) from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE(t, f)
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE(t, f)
+ volatile
+----------
+ t
+ t
+(2 rows)
+
+select volatile(stable_true(), stable_false()) from two;
+NOTICE: STABLE TRUE
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE(t, f)
+NOTICE: VOLATILE(t, f)
+ volatile
+----------
+ t
+ t
+(2 rows)
+
+-- Default arguments
+create function stable_def(a bool = stable_false(), b bool = volatile_true())
+returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE(%, %)', $1, $2; return $1; end;$$;
+select stable_def() from two;
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: STABLE(f, t)
+NOTICE: VOLATILE TRUE
+NOTICE: STABLE(f, t)
+ stable_def
+------------
+ f
+ f
+(2 rows)
+
+select stable_def(b := stable_true()) from two;
+NOTICE: STABLE FALSE
+NOTICE: STABLE TRUE
+NOTICE: STABLE(f, t)
+ stable_def
+------------
+ f
+ f
+(2 rows)
+
+select stable_def(volatile_false()) from two;
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: STABLE(f, t)
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE TRUE
+NOTICE: STABLE(f, t)
+ stable_def
+------------
+ f
+ f
+(2 rows)
+
+-- Operators
+create function stable_eq(bool, bool) returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE % == %', $1, $2; return $1 = $2; end;$$;
+create function volatile_eq(bool, bool) returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE % =%%= %', $1, $2; return $1 = $2; end;$$;
+create operator == (procedure = stable_eq, leftarg=bool, rightarg=bool);
+create operator =%= (procedure = volatile_eq, leftarg=bool, rightarg=bool);
+select volatile_true() == volatile_false() from two;
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE t == f
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE t == f
+ ?column?
+----------
+ f
+ f
+(2 rows)
+
+select stable_true() == volatile_false() from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE t == f
+NOTICE: VOLATILE FALSE
+NOTICE: STABLE t == f
+ ?column?
+----------
+ f
+ f
+(2 rows)
+
+select stable_true() == stable_false() from two;
+NOTICE: STABLE TRUE
+NOTICE: STABLE FALSE
+NOTICE: STABLE t == f
+ ?column?
+----------
+ f
+ f
+(2 rows)
+
+select volatile_true() =%= volatile_false() from two;
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE t =%= f
+NOTICE: VOLATILE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE t =%= f
+ ?column?
+----------
+ f
+ f
+(2 rows)
+
+select stable_true() =%= volatile_false() from two;
+NOTICE: STABLE TRUE
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE t =%= f
+NOTICE: VOLATILE FALSE
+NOTICE: VOLATILE t =%= f
+ ?column?
+----------
+ f
+ f
+(2 rows)
+
+select stable_true() =%= stable_false() from two;
+NOTICE: STABLE TRUE
+NOTICE: STABLE FALSE
+NOTICE: VOLATILE t =%= f
+NOTICE: VOLATILE t =%= f
+ ?column?
+----------
+ f
+ f
+(2 rows)
+
+drop table two;
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 51d561b..1210693 100644
--- a/src/test/regress/expected/rangefuncs.out
+++ b/src/test/regress/expected/rangefuncs.out
@@ -2,6 +2,7 @@ SELECT name, setting FROM pg_settings WHERE name LIKE 'enable%';
name | setting
-------------------+---------
enable_bitmapscan | on
+ enable_cacheexpr | on
enable_hashagg | on
enable_hashjoin | on
enable_indexscan | on
@@ -11,7 +12,7 @@ SELECT name, setting FROM pg_settings WHERE name LIKE 'enable%';
enable_seqscan | on
enable_sort | on
enable_tidscan | on
-(10 rows)
+(11 rows)
CREATE TABLE foo2(fooid int, f2 int);
INSERT INTO foo2 VALUES(1, 11);
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 376f28d..39ed9ea 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -79,7 +79,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
# ----------
# Another group of parallel tests
# ----------
-test: privileges security_label collate
+test: privileges security_label collate cache
test: misc
# rules cannot run concurrently with any test that creates a view
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index bb654f9..a5f3a19 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -90,6 +90,7 @@ test: prepared_xacts
test: privileges
test: security_label
test: collate
+test: cache
test: misc
test: rules
test: select_views
diff --git a/src/test/regress/sql/cache.sql b/src/test/regress/sql/cache.sql
new file mode 100644
index 0000000..0698b95
--- /dev/null
+++ b/src/test/regress/sql/cache.sql
@@ -0,0 +1,90 @@
+--
+-- Test cachable expressions
+--
+-- If the NOTICE outputs of these functions change, you've probably broken
+-- something with the CacheExpr optimization
+--
+
+create function stable_true() returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE TRUE'; return true; end;$$;
+create function volatile_true() returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE TRUE'; return true; end;$$;
+create function stable_false() returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE FALSE'; return false; end;$$;
+create function volatile_false() returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE FALSE'; return false; end;$$;
+
+create table two (i int);
+insert into two values (1), (2);
+
+-- Boolean expressions
+select stable_false() or volatile_true() or stable_true() as b from two;
+select stable_true() or volatile_false() or stable_false() as b from two;
+select stable_false() or volatile_true() as b from two;
+select stable_false() or stable_false() or volatile_true() as b from two;
+select volatile_true() or volatile_false() or stable_false() as b from two;
+select volatile_false() or volatile_true() or stable_false() as b from two;
+
+select stable_true() and volatile_false() and stable_false() as b from two;
+select stable_false() and volatile_true() and stable_true() as b from two;
+select stable_true() and volatile_false() as b from two;
+select stable_true() and stable_true() and volatile_false() as b from two;
+select volatile_true() and volatile_false() and stable_true() as b from two;
+select volatile_false() and volatile_true() and stable_true() as b from two;
+
+select not stable_true() as b from two;
+select not volatile_true() as b from two;
+
+prepare param_test(bool) as select $1 or stable_false() or volatile_true() as b from two;
+execute param_test(true);
+execute param_test(false);
+
+-- Function calls
+create function stable(bool) returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE(%)', $1; return $1; end;$$;
+create function volatile(bool) returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE(%)', $1; return $1; end;$$;
+
+select volatile(volatile_true()) from two;
+select stable(stable_true()) from two;
+select stable(volatile_true()) from two;
+select volatile(stable_true()) from two;
+
+create function stable(bool, bool) returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE(%, %)', $1, $2; return $1; end;$$;
+create function volatile(bool, bool) returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE(%, %)', $1, $2; return $1; end;$$;
+
+select stable(volatile_true(), volatile_false()) from two;
+select stable(stable_true(), volatile_false()) from two;
+select stable(stable_true(), stable_false()) from two;
+select volatile(volatile_true(), volatile_false()) from two;
+select volatile(stable_true(), volatile_false()) from two;
+select volatile(stable_true(), stable_false()) from two;
+
+-- Default arguments
+create function stable_def(a bool = stable_false(), b bool = volatile_true())
+returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE(%, %)', $1, $2; return $1; end;$$;
+
+select stable_def() from two;
+select stable_def(b := stable_true()) from two;
+select stable_def(volatile_false()) from two;
+
+-- Operators
+create function stable_eq(bool, bool) returns bool STABLE language plpgsql as
+$$begin raise notice 'STABLE % == %', $1, $2; return $1 = $2; end;$$;
+create function volatile_eq(bool, bool) returns bool VOLATILE language plpgsql as
+$$begin raise notice 'VOLATILE % =%%= %', $1, $2; return $1 = $2; end;$$;
+
+create operator == (procedure = stable_eq, leftarg=bool, rightarg=bool);
+create operator =%= (procedure = volatile_eq, leftarg=bool, rightarg=bool);
+
+select volatile_true() == volatile_false() from two;
+select stable_true() == volatile_false() from two;
+select stable_true() == stable_false() from two;
+select volatile_true() =%= volatile_false() from two;
+select stable_true() =%= volatile_false() from two;
+select stable_true() =%= stable_false() from two;
+
+drop table two;