* We assume that the remote session's search_path is exactly "pg_catalog",
* and thus we need schema-qualify all and only names outside pg_catalog.
*
+ * We do not consider that it is ever safe to send COLLATE expressions to
+ * the remote server: it might not have the same collation names we do.
+ * (Later we might consider it safe to send COLLATE "C", but even that would
+ * fail on old remote servers.) An expression is considered safe to send only
+ * if all collations used in it are traceable to Var(s) of the foreign table.
+ * That implies that if the remote server gets a different answer than we do,
+ * the foreign table's columns are not marked with collations that match the
+ * remote table's columns, which we can consider to be user error.
+ *
* Portions Copyright (c) 2012-2013, PostgreSQL Global Development Group
*
* IDENTIFICATION
#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/transam.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_proc.h"
/*
- * Context for foreign_expr_walker's search of an expression tree.
+ * Global context for foreign_expr_walker's search of an expression tree.
*/
-typedef struct foreign_expr_cxt
+typedef struct foreign_glob_cxt
{
/* Input values */
PlannerInfo *root;
RelOptInfo *foreignrel;
/* Result values */
List *param_numbers; /* Param IDs of PARAM_EXTERN Params */
-} foreign_expr_cxt;
+} foreign_glob_cxt;
+
+/*
+ * Local (per-tree-level) context for foreign_expr_walker's search.
+ * This is concerned with identifying collations used in the expression.
+ */
+typedef enum
+{
+ FDW_COLLATE_NONE, /* expression is of a noncollatable type */
+ FDW_COLLATE_SAFE, /* collation derives from a foreign Var */
+ FDW_COLLATE_UNSAFE /* collation derives from something else */
+} FDWCollateState;
+
+typedef struct foreign_loc_cxt
+{
+ Oid collation; /* OID of current collation, if any */
+ FDWCollateState state; /* state of current collation choice */
+} foreign_loc_cxt;
/*
* Functions to determine whether an expression can be evaluated safely on
*/
static bool is_foreign_expr(PlannerInfo *root, RelOptInfo *baserel,
Expr *expr, List **param_numbers);
-static bool foreign_expr_walker(Node *node, foreign_expr_cxt *context);
+static bool foreign_expr_walker(Node *node,
+ foreign_glob_cxt *glob_cxt,
+ foreign_loc_cxt *outer_cxt);
static bool is_builtin(Oid procid);
/*
Expr *expr,
List **param_numbers)
{
- foreign_expr_cxt context;
+ foreign_glob_cxt glob_cxt;
+ foreign_loc_cxt loc_cxt;
*param_numbers = NIL; /* default result */
* Check that the expression consists of nodes that are safe to execute
* remotely.
*/
- context.root = root;
- context.foreignrel = baserel;
- context.param_numbers = NIL;
- if (foreign_expr_walker((Node *) expr, &context))
+ glob_cxt.root = root;
+ glob_cxt.foreignrel = baserel;
+ glob_cxt.param_numbers = NIL;
+ loc_cxt.collation = InvalidOid;
+ loc_cxt.state = FDW_COLLATE_NONE;
+ if (!foreign_expr_walker((Node *) expr, &glob_cxt, &loc_cxt))
return false;
+ /* Expressions examined here should be boolean, ie noncollatable */
+ Assert(loc_cxt.collation == InvalidOid);
+ Assert(loc_cxt.state == FDW_COLLATE_NONE);
+
/*
* An expression which includes any mutable functions can't be sent over
* because its result is not stable. For example, sending now() remote
/*
* OK, so return list of param IDs too.
*/
- *param_numbers = context.param_numbers;
+ *param_numbers = glob_cxt.param_numbers;
return true;
}
/*
- * Return true if expression includes any node that is not safe to execute
- * remotely. (We use this convention because expression_tree_walker is
- * designed to abort the tree walk as soon as a TRUE result is detected.)
+ * Check if expression is safe to execute remotely, and return true if so.
+ *
+ * In addition, glob_cxt->param_numbers and *outer_cxt are updated.
+ *
+ * We must check that the expression contains only node types we can deparse,
+ * that all types/functions/operators are safe to send (which we approximate
+ * as being built-in), and that all collations used in the expression derive
+ * from Vars of the foreign table. Because of the latter, the logic is
+ * pretty close to assign_collations_walker() in parse_collate.c, though we
+ * can assume here that the given expression is valid.
*/
static bool
-foreign_expr_walker(Node *node, foreign_expr_cxt *context)
+foreign_expr_walker(Node *node,
+ foreign_glob_cxt *glob_cxt,
+ foreign_loc_cxt *outer_cxt)
{
bool check_type = true;
+ foreign_loc_cxt inner_cxt;
+ Oid collation;
+ FDWCollateState state;
+ /* Need do nothing for empty subexpressions */
if (node == NULL)
- return false;
+ return true;
+
+ /* Set up inner_cxt for possible recursion to child nodes */
+ inner_cxt.collation = InvalidOid;
+ inner_cxt.state = FDW_COLLATE_NONE;
switch (nodeTag(node))
{
case T_Var:
{
+ Var *var = (Var *) node;
+
/*
* Var can be used if it is in the foreign table (we shouldn't
* really see anything else in baserestrict clauses, but let's
* check anyway).
*/
- Var *var = (Var *) node;
-
- if (var->varno != context->foreignrel->relid ||
+ if (var->varno != glob_cxt->foreignrel->relid ||
var->varlevelsup != 0)
- return true;
+ return false;
+
+ /*
+ * If Var has a collation, consider that safe to use.
+ */
+ collation = var->varcollid;
+ state = OidIsValid(collation) ? FDW_COLLATE_SAFE : FDW_COLLATE_NONE;
}
break;
case T_Const:
- /* OK */
+ {
+ 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;
+ * either way, it's unsafe to send to the remote.
+ */
+ if (c->constcollid != InvalidOid &&
+ c->constcollid != DEFAULT_COLLATION_OID)
+ return false;
+
+ /* Otherwise, we can consider that it doesn't set collation */
+ collation = InvalidOid;
+ state = FDW_COLLATE_NONE;
+ }
break;
case T_Param:
{
* runs, we should only see PARAM_EXTERN Params anyway.)
*/
if (p->paramkind != PARAM_EXTERN)
- return true;
+ return false;
+
+ /*
+ * Collation handling is same as for Consts.
+ */
+ if (p->paramcollid != InvalidOid &&
+ p->paramcollid != DEFAULT_COLLATION_OID)
+ return false;
+ collation = InvalidOid;
+ state = FDW_COLLATE_NONE;
/*
* Report IDs of PARAM_EXTERN Params. We don't bother to
* eliminate duplicate list elements here; classifyConditions
* will do that.
*/
- context->param_numbers = lappend_int(context->param_numbers,
- p->paramid);
+ glob_cxt->param_numbers = lappend_int(glob_cxt->param_numbers,
+ p->paramid);
}
break;
case T_ArrayRef:
/* Assignment should not be in restrictions. */
if (ar->refassgnexpr != NULL)
- return true;
+ return false;
+
+ /*
+ * Recurse to remaining subexpressions. Since the array
+ * subscripts must yield (noncollatable) integers, they won't
+ * affect the inner_cxt state.
+ */
+ if (!foreign_expr_walker((Node *) ar->refupperindexpr,
+ glob_cxt, &inner_cxt))
+ return false;
+ if (!foreign_expr_walker((Node *) ar->reflowerindexpr,
+ glob_cxt, &inner_cxt))
+ return false;
+ if (!foreign_expr_walker((Node *) ar->refexpr,
+ glob_cxt, &inner_cxt))
+ return false;
+
+ /*
+ * Array subscripting should yield same collation as input,
+ * but for safety use same logic as for function nodes.
+ */
+ collation = ar->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
+ state = FDW_COLLATE_UNSAFE;
}
break;
case T_FuncExpr:
{
+ FuncExpr *fe = (FuncExpr *) node;
+
/*
* If function used by the expression is not built-in, it
* can't be sent to remote because it might have incompatible
* semantics on remote side.
*/
- FuncExpr *fe = (FuncExpr *) node;
-
if (!is_builtin(fe->funcid))
- return true;
+ return false;
+
+ /*
+ * Recurse to input subexpressions.
+ */
+ if (!foreign_expr_walker((Node *) fe->args,
+ glob_cxt, &inner_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;
+
+ /*
+ * 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
+ state = FDW_COLLATE_UNSAFE;
}
break;
case T_OpExpr:
case T_DistinctExpr: /* struct-equivalent to OpExpr */
{
+ OpExpr *oe = (OpExpr *) node;
+
/*
* Similarly, only built-in operators can be sent to remote.
* (If the operator is, surely its underlying function is
* too.)
*/
- OpExpr *oe = (OpExpr *) node;
-
if (!is_builtin(oe->opno))
- return true;
+ return false;
+
+ /*
+ * Recurse to input subexpressions.
+ */
+ if (!foreign_expr_walker((Node *) oe->args,
+ glob_cxt, &inner_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
+ state = FDW_COLLATE_UNSAFE;
}
break;
case T_ScalarArrayOpExpr:
{
+ ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
+
/*
* Again, only built-in operators can be sent to remote.
*/
- ScalarArrayOpExpr *oe = (ScalarArrayOpExpr *) node;
-
if (!is_builtin(oe->opno))
- return true;
+ return false;
+
+ /*
+ * Recurse to input subexpressions.
+ */
+ if (!foreign_expr_walker((Node *) oe->args,
+ glob_cxt, &inner_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;
+
+ /* Output is always boolean and so noncollatable. */
+ collation = InvalidOid;
+ state = FDW_COLLATE_NONE;
}
break;
case T_RelabelType:
+ {
+ RelabelType *r = (RelabelType *) node;
+
+ /*
+ * Recurse to input subexpression.
+ */
+ if (!foreign_expr_walker((Node *) r->arg,
+ glob_cxt, &inner_cxt))
+ return false;
+
+ /*
+ * RelabelType must not introduce a collation not derived from
+ * an input foreign Var.
+ */
+ 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
+ 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))
+ return false;
+
+ /* Output is always boolean and so noncollatable. */
+ collation = InvalidOid;
+ state = FDW_COLLATE_NONE;
+ }
+ break;
case T_NullTest:
+ {
+ NullTest *nt = (NullTest *) node;
+
+ /*
+ * Recurse to input subexpressions.
+ */
+ if (!foreign_expr_walker((Node *) nt->arg,
+ glob_cxt, &inner_cxt))
+ return false;
+
+ /* Output is always boolean and so noncollatable. */
+ collation = InvalidOid;
+ state = FDW_COLLATE_NONE;
+ }
+ break;
case T_ArrayExpr:
- /* OK */
+ {
+ ArrayExpr *a = (ArrayExpr *) node;
+
+ /*
+ * Recurse to input subexpressions.
+ */
+ if (!foreign_expr_walker((Node *) a->elements,
+ glob_cxt, &inner_cxt))
+ return false;
+
+ /*
+ * ArrayExpr must not introduce a collation not derived from
+ * an input foreign Var.
+ */
+ 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
+ state = FDW_COLLATE_UNSAFE;
+ }
break;
case T_List:
+ {
+ List *l = (List *) node;
+ ListCell *lc;
- /*
- * We need only fall through to let expression_tree_walker scan
- * the list elements --- but don't apply exprType() to the list.
- */
- check_type = false;
+ /*
+ * Recurse to component subexpressions.
+ */
+ foreach(lc, l)
+ {
+ if (!foreign_expr_walker((Node *) lfirst(lc),
+ glob_cxt, &inner_cxt))
+ return false;
+ }
+
+ /*
+ * When processing a list, collation state just bubbles up
+ * from the list elements.
+ */
+ collation = inner_cxt.collation;
+ state = inner_cxt.state;
+
+ /* Don't apply exprType() to the list. */
+ check_type = false;
+ }
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 true;
+ return false;
}
/*
* remote because it might have incompatible semantics on remote side.
*/
if (check_type && !is_builtin(exprType(node)))
- return true;
+ return false;
+
+ /*
+ * Now, merge my collation information into my parent's state.
+ */
+ if (state > outer_cxt->state)
+ {
+ /* Override previous parent state */
+ outer_cxt->collation = collation;
+ outer_cxt->state = state;
+ }
+ else if (state == outer_cxt->state)
+ {
+ /* Merge, or detect error if there's a collation conflict */
+ switch (state)
+ {
+ case FDW_COLLATE_NONE:
+ /* Nothing + nothing is still nothing */
+ break;
+ case FDW_COLLATE_SAFE:
+ if (collation != outer_cxt->collation)
+ {
+ /*
+ * Non-default collation always beats default.
+ */
+ if (outer_cxt->collation == DEFAULT_COLLATION_OID)
+ {
+ /* Override previous parent state */
+ outer_cxt->collation = collation;
+ }
+ else if (collation != DEFAULT_COLLATION_OID)
+ {
+ /*
+ * Conflict; show state as indeterminate. We don't
+ * want to "return false" right away, since parent
+ * node might not care about collation.
+ */
+ outer_cxt->state = FDW_COLLATE_UNSAFE;
+ }
+ }
+ break;
+ case FDW_COLLATE_UNSAFE:
+ /* We're still conflicted ... */
+ break;
+ }
+ }
- /* Recurse to examine sub-nodes */
- return expression_tree_walker(node, foreign_expr_walker, context);
+ /* It looks OK */
+ return true;
}
/*
(1 row)
-- subquery using stable function (can't be sent to remote)
-PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c4) = 6) ORDER BY c1;
+PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c4) = '1970-01-17'::date) ORDER BY c1;
EXPLAIN (VERBOSE, COSTS false) EXECUTE st2(10, 20);
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
Output: t2.c3
-> Foreign Scan on public.ft2 t2
Output: t2.c3
- Filter: (date_part('dow'::text, t2.c4) = 6::double precision)
+ Filter: (date(t2.c4) = '01-17-1970'::date)
Remote SQL: SELECT NULL, NULL, c3, c4, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" > 10))
(15 rows)
16 | 6 | 00016 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo
(1 row)
-EXECUTE st1(101, 101);
- c3 | c3
--------+-------
- 00101 | 00101
+EXECUTE st2(101, 121);
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+-----+----+-------+------------------------------+--------------------------+----+------------+-----
+ 116 | 6 | 00116 | Sat Jan 17 00:00:00 1970 PST | Sat Jan 17 00:00:00 1970 | 6 | 6 | foo
(1 row)
-- subquery using immutable function (can be sent to remote)
-PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND EXTRACT(dow FROM c5) = 6) ORDER BY c1;
+PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1;
EXPLAIN (VERBOSE, COSTS false) EXECUTE st3(10, 20);
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort
Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
Sort Key: t1.c1
Output: t2.c3
-> Foreign Scan on public.ft2 t2
Output: t2.c3
- Remote SQL: SELECT NULL, NULL, c3, NULL, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" > 10)) AND ((date_part('dow'::text, c5) = 6::double precision))
+ Remote SQL: SELECT NULL, NULL, c3, NULL, NULL, NULL, NULL, NULL FROM "S 1"."T 1" WHERE (("C 1" > 10)) AND ((date(c5) = '1970-01-17'::date))
(14 rows)
EXECUTE st3(10, 20);
(1 row)
EXECUTE st3(20, 30);
- c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
-----+----+-------+------------------------------+--------------------------+----+------------+-----
- 23 | 3 | 00023 | Sat Jan 24 00:00:00 1970 PST | Sat Jan 24 00:00:00 1970 | 3 | 3 | foo
-(1 row)
+ c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8
+----+----+----+----+----+----+----+----
+(0 rows)
-- custom plan should be chosen initially
PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1;
(1 row)
COMMIT;
+-- ===================================================================
+-- test handling of collations
+-- ===================================================================
+create table loct3 (f1 text collate "C", f2 text);
+create foreign table ft3 (f1 text collate "C", f2 text)
+ server loopback options (table_name 'loct3');
+-- can be sent to remote
+explain (verbose, costs off) select * from ft3 where f1 = 'foo';
+ QUERY PLAN
+--------------------------------------------------------------------------
+ Foreign Scan on public.ft3
+ Output: f1, f2
+ Remote SQL: SELECT f1, f2 FROM public.loct3 WHERE ((f1 = 'foo'::text))
+(3 rows)
+
+explain (verbose, costs off) select * from ft3 where f1 COLLATE "C" = 'foo';
+ QUERY PLAN
+--------------------------------------------------------------------------
+ Foreign Scan on public.ft3
+ Output: f1, f2
+ Remote SQL: SELECT f1, f2 FROM public.loct3 WHERE ((f1 = 'foo'::text))
+(3 rows)
+
+explain (verbose, costs off) select * from ft3 where f2 = 'foo';
+ QUERY PLAN
+--------------------------------------------------------------------------
+ Foreign Scan on public.ft3
+ Output: f1, f2
+ Remote SQL: SELECT f1, f2 FROM public.loct3 WHERE ((f2 = 'foo'::text))
+(3 rows)
+
+-- can't be sent to remote
+explain (verbose, costs off) select * from ft3 where f1 COLLATE "POSIX" = 'foo';
+ QUERY PLAN
+-----------------------------------------------
+ Foreign Scan on public.ft3
+ Output: f1, f2
+ Filter: ((ft3.f1)::text = 'foo'::text)
+ Remote SQL: SELECT f1, f2 FROM public.loct3
+(4 rows)
+
+explain (verbose, costs off) select * from ft3 where f1 = 'foo' COLLATE "C";
+ QUERY PLAN
+-----------------------------------------------
+ Foreign Scan on public.ft3
+ Output: f1, f2
+ Filter: (ft3.f1 = 'foo'::text COLLATE "C")
+ Remote SQL: SELECT f1, f2 FROM public.loct3
+(4 rows)
+
+explain (verbose, costs off) select * from ft3 where f2 COLLATE "C" = 'foo';
+ QUERY PLAN
+-----------------------------------------------
+ Foreign Scan on public.ft3
+ Output: f1, f2
+ Filter: ((ft3.f2)::text = 'foo'::text)
+ Remote SQL: SELECT f1, f2 FROM public.loct3
+(4 rows)
+
+explain (verbose, costs off) select * from ft3 where f2 = 'foo' COLLATE "C";
+ QUERY PLAN
+-----------------------------------------------
+ Foreign Scan on public.ft3
+ Output: f1, f2
+ Filter: (ft3.f2 = 'foo'::text COLLATE "C")
+ Remote SQL: SELECT f1, f2 FROM public.loct3
+(4 rows)
+
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================