WIP: Join push-down for foreign tables
Hi all,
I'd like to propose $SUBJECT for further foreign query optimization.
I've not finished development, but I'd appreciate it if I got someone's
review on my WIP code and its design.
Changes I made
==============
(1) Add foreign server OID to RelOptInfo
I think it would be nice to know whether a join comes from one foreign
server or not without digging into child nodes during considering paths
for a query. So I added serverid field to RelOptInfo, which defaults to
InvalidOid ,and is set to OID of the server if the node and all of its
children are from same foreign server. This also avoids looking catalog
up for foreign table entry to determine FDW routine.
(2) Add new planner node, ForeignJoinPath
ForeignJoinPath derives JoinPath, like other Join nodes, and holds
FdwPlan like ForeignPath node.
This node is used to represent a pushed-down join between foreign tables
and/or another foreign join in early planning phase, for all of
combination such as table-table, table-join, join-table and join-join
will be considered. In addition, though they might generate same
internal (FDW-specific) plan, reversed combination is considered;
planner generates two ForeignJoinPath for both (A & B) and (B & A).
During creation of this node, planner calls new FDW handler function
PlanForeignJoin to get a FdwPlan which includes costs and internal plan
of a foreign join. If a FDW can't (or doesn't want to) handle this
join, just return NULL is OK, and then planner gives such optimization
up and considers other usual join methods such as nested loop and hash join.
A subtree which has a ForeignJoin on its top is translated into a
ForeignScan node during constructing a plan tree. This behavior is
different from other join path nodes such as NestPath and MergePath,
because they have child plan nodes correspond to path nodes.
(3) Add EXPALIN support for foreign join (currently just for debug)
ForeignScan might not be a simple foreign table scan, so
ExplainScanTarget() can't be used for it. An idea I have is adding
ExplainForeignScanTarget() to handle ForeignScan separately from other
scan nodes.
(4) Add new GUC parameter, enable_foreignjoin
If this was off, planner never generates ForeignJoinPath. In such case,
foreign tables will be joined with one of NestLoop, MergeJoin and HashJoin.
Known issue
===========
I'm sorry but attached patch, join_pushdown_v1.patch, is WIP, so
currently some kind of query fails. Known failure patterns are:
*) SELECT * FROM A JOIN B (...) doesn't work. Specifying columns in
SELECT clause explicitly like "SELECT A.col1, A.col2, ..." seems to work.
*) ORDER BY causes error if no column is specified in SELECT clause from
sort key's table.
Probably more problems still are there...
PG-wrapper as sample implementation
===================================
pgsql_fdw-0.1.0.tar.gz is an WIP implementation of PG-wrapper, which can
(hopefully) handle both simple foreign table scan and multiple foreign
joins. You can build it with placing in contrib/, or using pgxs. Note
that it has some issues such as memory leak of PGresult. I'm planning
to propose this wrapper as a contrib module, but it would be after
clearing such issues.
Regards,
--
Shigeru Hanada
Attachments:
join_pushdown_v1.patchtext/plain; name=join_pushdown_v1.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f95f9cc..70d25ae 100644
*** a/doc/src/sgml/config.sgml
--- b/doc/src/sgml/config.sgml
*************** SET ENABLE_SEQSCAN TO OFF;
*** 2360,2365 ****
--- 2360,2378 ----
</listitem>
</varlistentry>
+ <varlistentry id="guc-enable-foreignjoin" xreflabel="enable_foreignjoin">
+ <term><varname>enable_foreignjoin</varname> (<type>boolean</type>)</term>
+ <indexterm>
+ <primary><varname>enable_foreignjoin</> configuration parameter</primary>
+ </indexterm>
+ <listitem>
+ <para>
+ Enables or disables the query planner's use of foreign-join plan
+ types. The default is <literal>on</>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-enable-hashagg" xreflabel="enable_hashagg">
<term><varname>enable_hashagg</varname> (<type>boolean</type>)</term>
<indexterm>
diff --git a/doc/src/sgml/ref/postgres-ref.sgml b/doc/src/sgml/ref/postgres-ref.sgml
index b16bbdf..bebaebe 100644
*** a/doc/src/sgml/ref/postgres-ref.sgml
--- b/doc/src/sgml/ref/postgres-ref.sgml
*************** PostgreSQL documentation
*** 362,375 ****
<variablelist>
<varlistentry>
! <term><option>-f</option> <literal>{ s | i | m | n | h }</literal></term>
<listitem>
<para>
Forbids the use of particular scan and join methods:
<literal>s</literal> and <literal>i</literal>
disable sequential and index scans respectively, while
! <literal>n</literal>, <literal>m</literal>, and <literal>h</literal>
! disable nested-loop, merge and hash joins respectively.
</para>
<para>
--- 362,376 ----
<variablelist>
<varlistentry>
! <term><option>-f</option> <literal>{ s | i | m | n | h | f }</literal></term>
<listitem>
<para>
Forbids the use of particular scan and join methods:
<literal>s</literal> and <literal>i</literal>
disable sequential and index scans respectively, while
! <literal>n</literal>, <literal>m</literal>, <literal>h</literal>, and
! <literal>f</literal>
! disable nested-loop, merge, hash and foreign joins respectively.
</para>
<para>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 6408d16..1bd035d 100644
*** a/src/backend/commands/explain.c
--- b/src/backend/commands/explain.c
*************** static void show_hash_info(HashState *ha
*** 79,84 ****
--- 79,85 ----
static void show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es);
static const char *explain_get_index_name(Oid indexId);
static void ExplainScanTarget(Scan *plan, ExplainState *es);
+ static void ExplainForeignScanTarget(Scan *plan, ExplainState *es);
static void ExplainModifyTarget(ModifyTable *plan, ExplainState *es);
static void ExplainTargetRel(Plan *plan, Index rti, ExplainState *es);
static void ExplainMemberNodes(List *plans, PlanState **planstates,
*************** ExplainNode(PlanState *planstate, List *
*** 833,841 ****
case T_ValuesScan:
case T_CteScan:
case T_WorkTableScan:
- case T_ForeignScan:
ExplainScanTarget((Scan *) plan, es);
break;
case T_BitmapIndexScan:
{
BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan;
--- 834,844 ----
case T_ValuesScan:
case T_CteScan:
case T_WorkTableScan:
ExplainScanTarget((Scan *) plan, es);
break;
+ case T_ForeignScan:
+ ExplainForeignScanTarget((ForeignScan *) plan, es);
+ break;
case T_BitmapIndexScan:
{
BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan;
*************** ExplainScanTarget(Scan *plan, ExplainSta
*** 1556,1561 ****
--- 1559,1584 ----
}
/*
+ * Show the target of a ForeignScan node
+ */
+ static void
+ ExplainForeignScanTarget(Scan *plan, ExplainState *es)
+ {
+ Assert(IsA(plan, ForeignScan));
+
+ /*
+ * If scan target is an foreign table, show in normal scan format,
+ * otherwise, show in specific format.
+ */
+ if (plan->scanrelid > 0)
+ ExplainTargetRel((Plan *) plan, plan->scanrelid, es);
+ else
+ {
+ appendStringInfo(es->str, " on multiple foreign tables");
+ }
+ }
+
+ /*
* Show the target of a ModifyTable node
*/
static void
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 4dbf10b..351b746 100644
*** a/src/backend/executor/execUtils.c
--- b/src/backend/executor/execUtils.c
*************** ExecAssignScanTypeFromOuterPlan(ScanStat
*** 756,761 ****
--- 756,786 ----
ExecAssignScanType(scanstate, tupDesc);
}
+ /* ----------------
+ * ExecAssignScanTypeFromTL
+ * ----------------
+ */
+ void
+ ExecAssignScanTypeFromTL(ScanState *scanstate)
+ {
+ bool hasoid;
+ TupleTableSlot *slot = scanstate->ss_ScanTupleSlot;
+ TupleDesc tupDesc;
+
+ if (ExecContextForcesOids(&scanstate->ps, &hasoid))
+ {
+ /* context forces OID choice; hasoid is now set correctly */
+ }
+ else
+ {
+ /* given free choice, don't leave space for OIDs in result tuples */
+ hasoid = false;
+ }
+
+ tupDesc = ExecTypeFromTL(scanstate->ps.plan->targetlist, hasoid);
+ ExecSetSlotDescriptor(slot, tupDesc);
+ }
+
/* ----------------------------------------------------------------
* Scan node support
diff --git a/src/backend/executor/nodeForeignscan.c b/src/backend/executor/nodeForeignscan.c
index 841ae69..b13f0dd 100644
*** a/src/backend/executor/nodeForeignscan.c
--- b/src/backend/executor/nodeForeignscan.c
***************
*** 25,30 ****
--- 25,31 ----
#include "executor/executor.h"
#include "executor/nodeForeignscan.h"
#include "foreign/fdwapi.h"
+ #include "foreign/foreign.h"
#include "utils/rel.h"
static TupleTableSlot *ForeignNext(ForeignScanState *node);
*************** ExecForeignScan(ForeignScanState *node)
*** 101,109 ****
ForeignScanState *
ExecInitForeignScan(ForeignScan *node, EState *estate, int eflags)
{
! ForeignScanState *scanstate;
! Relation currentRelation;
! FdwRoutine *fdwroutine;
/* check for unsupported flags */
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
--- 102,112 ----
ForeignScanState *
ExecInitForeignScan(ForeignScan *node, EState *estate, int eflags)
{
! ForeignScanState *scanstate;
! Relation currentRelation;
! ForeignServer *server;
! ForeignDataWrapper *wrapper;
! FdwRoutine *fdwroutine;
/* check for unsupported flags */
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
*************** ExecInitForeignScan(ForeignScan *node, E
*** 140,166 ****
ExecInitResultTupleSlot(estate, &scanstate->ss.ps);
ExecInitScanTupleSlot(estate, &scanstate->ss);
! /*
! * open the base relation and acquire appropriate lock on it.
! */
! currentRelation = ExecOpenScanRelation(estate, node->scan.scanrelid);
! scanstate->ss.ss_currentRelation = currentRelation;
! /*
! * get the scan type from the relation descriptor.
! */
! ExecAssignScanType(&scanstate->ss, RelationGetDescr(currentRelation));
! /*
! * Initialize result tuple type and projection info.
! */
! ExecAssignResultTypeFromTL(&scanstate->ss.ps);
! ExecAssignScanProjectionInfo(&scanstate->ss);
/*
* Acquire function pointers from the FDW's handler, and init fdw_state.
*/
! fdwroutine = GetFdwRoutineByRelId(RelationGetRelid(currentRelation));
scanstate->fdwroutine = fdwroutine;
scanstate->fdw_state = NULL;
--- 143,193 ----
ExecInitResultTupleSlot(estate, &scanstate->ss.ps);
ExecInitScanTupleSlot(estate, &scanstate->ss);
! if (node->scan.scanrelid != InvalidOid)
! {
! /*
! * open the base relation and acquire appropriate lock on it.
! */
! currentRelation = ExecOpenScanRelation(estate, node->scan.scanrelid);
! scanstate->ss.ss_currentRelation = currentRelation;
! /*
! * get the scan type from the relation descriptor.
! */
! ExecAssignScanType(&scanstate->ss, RelationGetDescr(currentRelation));
! /*
! * Initialize result tuple type and projection info.
! */
! ExecAssignResultTypeFromTL(&scanstate->ss.ps);
! ExecAssignScanProjectionInfo(&scanstate->ss);
! }
! else
! {
! TupleDesc tupleDesc;
!
! /* TODO: open related relations and acquire appropriate lock on them. */
! scanstate->ss.ss_currentRelation = NULL;
!
! /*
! * get the scan type from the target list.
! */
! ExecAssignScanTypeFromTL(&scanstate->ss);
! tupleDesc = scanstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
!
! /*
! * Initialize result tuple type and projection info.
! */
! ExecAssignResultTypeFromTL(&scanstate->ss.ps);
! ExecAssignProjectionInfo(&scanstate->ss.ps, NULL);
! }
/*
* Acquire function pointers from the FDW's handler, and init fdw_state.
*/
! server = GetForeignServer(node->serverid);
! wrapper = GetForeignDataWrapper(server->fdwid);
! fdwroutine = GetFdwRoutine(wrapper->fdwhandler);
scanstate->fdwroutine = fdwroutine;
scanstate->fdw_state = NULL;
*************** ExecEndForeignScan(ForeignScanState *nod
*** 192,198 ****
ExecClearTuple(node->ss.ss_ScanTupleSlot);
/* close the relation. */
! ExecCloseScanRelation(node->ss.ss_currentRelation);
}
/* ----------------------------------------------------------------
--- 219,226 ----
ExecClearTuple(node->ss.ss_ScanTupleSlot);
/* close the relation. */
! if (node->ss.ss_currentRelation != NULL)
! ExecCloseScanRelation(node->ss.ss_currentRelation);
}
/* ----------------------------------------------------------------
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 661a516..30f3d36 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyForeignScan(ForeignScan *from)
*** 565,570 ****
--- 565,571 ----
/*
* copy remainder of node
*/
+ COPY_SCALAR_FIELD(serverid);
COPY_SCALAR_FIELD(fsSystemCol);
COPY_NODE_FIELD(fdwplan);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 0d0ce3c..f0532be 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outForeignScan(StringInfo str, ForeignS
*** 543,548 ****
--- 543,549 ----
_outScanInfo(str, (Scan *) node);
+ WRITE_OID_FIELD(serverid);
WRITE_BOOL_FIELD(fsSystemCol);
WRITE_NODE_FIELD(fdwplan);
}
*************** _outHashPath(StringInfo str, HashPath *n
*** 1647,1652 ****
--- 1648,1663 ----
}
static void
+ _outForeignJoinPath(StringInfo str, ForeignJoinPath *node)
+ {
+ WRITE_NODE_TYPE("FOREIGNJOINPATH");
+
+ _outJoinPathInfo(str, (JoinPath *) node);
+
+ WRITE_NODE_FIELD(fdwplan);
+ }
+
+ static void
_outPlannerGlobal(StringInfo str, PlannerGlobal *node)
{
WRITE_NODE_TYPE("PLANNERGLOBAL");
*************** _outRelOptInfo(StringInfo str, RelOptInf
*** 1734,1739 ****
--- 1745,1751 ----
WRITE_NODE_FIELD(baserestrictinfo);
WRITE_NODE_FIELD(joininfo);
WRITE_BOOL_FIELD(has_eclass_joins);
+ WRITE_OID_FIELD(serverid);
WRITE_BITMAPSET_FIELD(index_outer_relids);
WRITE_NODE_FIELD(index_inner_paths);
}
*************** _outNode(StringInfo str, void *obj)
*** 2967,2972 ****
--- 2979,2987 ----
case T_HashPath:
_outHashPath(str, obj);
break;
+ case T_ForeignJoinPath:
+ _outForeignJoinPath(str, obj);
+ break;
case T_PlannerGlobal:
_outPlannerGlobal(str, obj);
break;
diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README
index aaa754c..f06cb27 100644
*** a/src/backend/optimizer/README
--- b/src/backend/optimizer/README
*************** RelOptInfo - a relation or joined r
*** 356,361 ****
--- 356,362 ----
NestPath - nested-loop joins
MergePath - merge joins
HashPath - hash joins
+ ForeignJoinPath - foreign joins
EquivalenceClass - a data structure representing a set of values known equal
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index b421481..13f7165 100644
*** a/src/backend/optimizer/path/allpaths.c
--- b/src/backend/optimizer/path/allpaths.c
*************** print_path(PlannerInfo *root, Path *path
*** 1590,1595 ****
--- 1590,1599 ----
ptype = "HashJoin";
join = true;
break;
+ case T_ForeignJoinPath:
+ ptype = "ForeignJoin";
+ join = true;
+ break;
default:
ptype = "???Path";
break;
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7812a86..e843ffd 100644
*** a/src/backend/optimizer/path/costsize.c
--- b/src/backend/optimizer/path/costsize.c
*************** bool enable_nestloop = true;
*** 118,123 ****
--- 118,124 ----
bool enable_material = true;
bool enable_mergejoin = true;
bool enable_hashjoin = true;
+ bool enable_foreignjoin = true;
typedef struct
{
*************** cost_mergejoin(MergePath *path, PlannerI
*** 2071,2076 ****
--- 2072,2082 ----
}
/*
+ * cost_foreignjoin() is not defined here because the costs of a foreign join
+ * is estimated by each FDW via PlanForeignJoin.
+ */
+
+ /*
* run mergejoinscansel() with caching
*/
static MergeScanSelCache *
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 7d3cf42..3024565 100644
*** a/src/backend/optimizer/path/joinpath.c
--- b/src/backend/optimizer/path/joinpath.c
*************** match_unsorted_outer(PlannerInfo *root,
*** 541,546 ****
--- 541,566 ----
merge_pathkeys));
}
+ if (enable_foreignjoin &&
+ joinrel->serverid != InvalidOid &&
+ (IsA(outerpath, ForeignPath) || IsA(outerpath, ForeignJoinPath)) &&
+ (IsA(inner_cheapest_total, ForeignPath) ||
+ IsA(inner_cheapest_total, ForeignJoinPath)))
+
+ {
+ ForeignJoinPath *path;
+ path = create_foreignjoin_path(root,
+ joinrel,
+ jointype,
+ sjinfo,
+ outerpath,
+ inner_cheapest_total,
+ restrictlist,
+ merge_pathkeys);
+ if (path != NULL)
+ add_path(joinrel, (Path *) path);
+ }
+
/* Can't do anything else if outer path needs to be unique'd */
if (save_jointype == JOIN_UNIQUE_OUTER)
continue;
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index b674afe..ccca59c 100644
*** a/src/backend/optimizer/plan/createplan.c
--- b/src/backend/optimizer/plan/createplan.c
*************** static WorkTableScan *create_worktablesc
*** 74,79 ****
--- 74,81 ----
List *tlist, List *scan_clauses);
static ForeignScan *create_foreignscan_plan(PlannerInfo *root, ForeignPath *best_path,
List *tlist, List *scan_clauses);
+ static ForeignScan *create_foreignjoin_plan(PlannerInfo *root, ForeignJoinPath *best_path,
+ List *tlist, List *scan_clauses);
static NestLoop *create_nestloop_plan(PlannerInfo *root, NestPath *best_path,
Plan *outer_plan, Plan *inner_plan);
static MergeJoin *create_mergejoin_plan(PlannerInfo *root, MergePath *best_path,
*************** static CteScan *make_ctescan(List *qptli
*** 117,123 ****
static WorkTableScan *make_worktablescan(List *qptlist, List *qpqual,
Index scanrelid, int wtParam);
static ForeignScan *make_foreignscan(List *qptlist, List *qpqual,
! Index scanrelid, bool fsSystemCol, FdwPlan *fdwplan);
static BitmapAnd *make_bitmap_and(List *bitmapplans);
static BitmapOr *make_bitmap_or(List *bitmapplans);
static NestLoop *make_nestloop(List *tlist,
--- 119,126 ----
static WorkTableScan *make_worktablescan(List *qptlist, List *qpqual,
Index scanrelid, int wtParam);
static ForeignScan *make_foreignscan(List *qptlist, List *qpqual,
! Index scanrelid, Oid serverid, bool fsSystemCol,
! FdwPlan *fdwplan);
static BitmapAnd *make_bitmap_and(List *bitmapplans);
static BitmapOr *make_bitmap_or(List *bitmapplans);
static NestLoop *make_nestloop(List *tlist,
*************** create_plan_recurse(PlannerInfo *root, P
*** 214,219 ****
--- 217,223 ----
case T_CteScan:
case T_WorkTableScan:
case T_ForeignScan:
+ case T_ForeignJoin: /* ForeignJoinPath become a ForeignScan */
plan = create_scan_plan(root, best_path);
break;
case T_HashJoin:
*************** create_scan_plan(PlannerInfo *root, Path
*** 361,366 ****
--- 365,377 ----
scan_clauses);
break;
+ case T_ForeignJoin:
+ plan = (Plan *) create_foreignjoin_plan(root,
+ (ForeignJoinPath *) best_path,
+ tlist,
+ scan_clauses);
+ break;
+
default:
elog(ERROR, "unrecognized node type: %d",
(int) best_path->pathtype);
*************** create_foreignscan_plan(PlannerInfo *roo
*** 1813,1818 ****
--- 1824,1830 ----
scan_plan = make_foreignscan(tlist,
scan_clauses,
scan_relid,
+ rel->serverid,
fsSystemCol,
best_path->fdwplan);
*************** create_foreignscan_plan(PlannerInfo *roo
*** 1821,1826 ****
--- 1833,1884 ----
return scan_plan;
}
+ /*
+ * create_foreignjoin_plan
+ * Returns a foreignscan plan for the join relation joined by 'best_path'
+ * with restriction clauses 'scan_clauses' and targetlist 'tlist'.
+ */
+ static ForeignScan *
+ create_foreignjoin_plan(PlannerInfo *root, ForeignJoinPath *best_path,
+ List *tlist, List *scan_clauses)
+ {
+ ForeignScan *scan_plan;
+ RelOptInfo *rel = best_path->jpath.path.parent;
+ Index scan_relid = rel->relid;
+ bool fsSystemCol;
+ int i;
+
+ /* Sort clauses into best execution order */
+ scan_clauses = order_qual_clauses(root, scan_clauses);
+
+ /* Reduce RestrictInfo list to bare expressions; ignore pseudoconstants */
+ scan_clauses = extract_actual_clauses(scan_clauses, false);
+
+ fsSystemCol = false;
+ #ifdef NOT_USED
+ /* Detect whether any system columns are requested from rel */
+ for (i = rel->min_attr; i < 0; i++)
+ {
+ if (!bms_is_empty(rel->attr_needed[i - rel->min_attr]))
+ {
+ fsSystemCol = true;
+ break;
+ }
+ }
+ #endif
+
+ scan_plan = make_foreignscan(tlist,
+ scan_clauses,
+ scan_relid,
+ rel->serverid,
+ fsSystemCol,
+ best_path->fdwplan);
+
+ copy_path_costsize(&scan_plan->scan.plan, &best_path->jpath.path);
+
+ return scan_plan;
+ }
+
/*****************************************************************************
*
*************** static ForeignScan *
*** 3046,3051 ****
--- 3104,3110 ----
make_foreignscan(List *qptlist,
List *qpqual,
Index scanrelid,
+ Oid serverid,
bool fsSystemCol,
FdwPlan *fdwplan)
{
*************** make_foreignscan(List *qptlist,
*** 3058,3063 ****
--- 3117,3123 ----
plan->lefttree = NULL;
plan->righttree = NULL;
node->scan.scanrelid = scanrelid;
+ node->serverid = serverid;
node->fsSystemCol = fsSystemCol;
node->fdwplan = fdwplan;
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 4a1c94a..b2e2a7a 100644
*** a/src/backend/optimizer/util/pathnode.c
--- b/src/backend/optimizer/util/pathnode.c
***************
*** 17,22 ****
--- 17,23 ----
#include <math.h>
#include "foreign/fdwapi.h"
+ #include "foreign/foreign.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/clauses.h"
*************** create_hashjoin_path(PlannerInfo *root,
*** 1603,1605 ****
--- 1604,1686 ----
return pathnode;
}
+
+ /*
+ * create_foreignjoin_path
+ * Creates a pathnode corresponding to a foreign join between two
+ * relations.
+ *
+ * 'joinrel' is the join relation.
+ * 'jointype' is the type of join required
+ * 'sjinfo' is extra info about the join for selectivity estimation
+ * 'outer_path' is the outer path
+ * 'inner_path' is the inner path
+ * 'restrict_clauses' are the RestrictInfo nodes to apply at the join
+ * 'pathkeys' are the path keys of the new join path
+ *
+ * Returns the resulting path node, or NULL to indicate that this path is
+ * unavailable.
+ */
+ ForeignJoinPath *
+ create_foreignjoin_path(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ JoinType jointype,
+ SpecialJoinInfo *sjinfo,
+ Path *outer_path,
+ Path *inner_path,
+ List *restrict_clauses,
+ List *pathkeys)
+ {
+ ForeignJoinPath *pathnode;
+ ForeignServer *server;
+ ForeignDataWrapper *wrapper;
+ FdwRoutine *fdwroutine;
+ FdwPlan *fdwplan;
+
+ /* Both outer and inner of this join must come from same foreign server. */
+ Assert(IsA(outer_path, ForeignPath) || IsA(outer_path, ForeignJoinPath));
+ Assert(IsA(inner_path, ForeignPath) || IsA(inner_path, ForeignJoinPath));
+
+ /*
+ * First we try to get FDW's callback info. If the FDW has planner for
+ * foreign join, let the FDW plan this join.
+ */
+ server = GetForeignServer(joinrel->serverid);
+ wrapper = GetForeignDataWrapper(server->fdwid);
+ fdwroutine = GetFdwRoutine(wrapper->fdwhandler);
+ if (fdwroutine->PlanForeignJoin == NULL)
+ return NULL;
+
+ fdwplan = fdwroutine->PlanForeignJoin(joinrel->serverid,
+ root,
+ joinrel,
+ jointype,
+ sjinfo,
+ outer_path,
+ inner_path,
+ restrict_clauses,
+ pathkeys);
+ /* Returning NULL indicates that the FDW can't handle this join. */
+ if (fdwplan == NULL)
+ return NULL;
+ Assert(IsA(fdwplan, FdwPlan));
+
+ /* OK, this FDW can handle this join. */
+ pathnode = makeNode(ForeignJoinPath);
+ pathnode->jpath.path.pathtype = T_ForeignJoin;
+ pathnode->jpath.path.parent = joinrel;
+ pathnode->jpath.jointype = jointype;
+ pathnode->jpath.outerjoinpath = outer_path;
+ pathnode->jpath.innerjoinpath = inner_path;
+ pathnode->jpath.joinrestrictinfo = restrict_clauses;
+ pathnode->jpath.path.pathkeys = pathkeys;
+
+ /* Use costs estimated by FDW */
+ pathnode->jpath.path.startup_cost = fdwplan->startup_cost;
+ pathnode->jpath.path.total_cost = fdwplan->total_cost;
+
+ /* Store FDW-private information too. */
+ pathnode->fdwplan = fdwplan;
+
+ return pathnode;
+ }
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 8a3a5d8..32d1bb5 100644
*** a/src/backend/optimizer/util/plancat.c
--- b/src/backend/optimizer/util/plancat.c
***************
*** 22,27 ****
--- 22,28 ----
#include "access/sysattr.h"
#include "access/transam.h"
#include "catalog/catalog.h"
+ #include "foreign/foreign.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "optimizer/clauses.h"
*************** get_relation_info(PlannerInfo *root, Oid
*** 347,352 ****
--- 348,362 ----
rel->indexlist = indexinfos;
+ /* Get server oid for further planning, if this is a foreign table. */
+ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ ForeignTable *table;
+
+ table = GetForeignTable(relationObjectId);
+ rel->serverid = table->serverid;
+ }
+
heap_close(relation, NoLock);
/*
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 1df727d..2228ec4 100644
*** a/src/backend/optimizer/util/relnode.c
--- b/src/backend/optimizer/util/relnode.c
*************** build_simple_rel(PlannerInfo *root, int
*** 116,121 ****
--- 116,122 ----
rel->baserestrictcost.per_tuple = 0;
rel->joininfo = NIL;
rel->has_eclass_joins = false;
+ rel->serverid = InvalidOid;
rel->index_outer_relids = NULL;
rel->index_inner_paths = NIL;
*************** build_join_rel(PlannerInfo *root,
*** 369,374 ****
--- 370,376 ----
joinrel->baserestrictcost.per_tuple = 0;
joinrel->joininfo = NIL;
joinrel->has_eclass_joins = false;
+ joinrel->serverid = InvalidOid;
joinrel->index_outer_relids = NULL;
joinrel->index_inner_paths = NIL;
*************** build_join_rel(PlannerInfo *root,
*** 441,446 ****
--- 443,456 ----
lappend(root->join_rel_level[root->join_cur_level], joinrel);
}
+ /*
+ * If both outer and inner are from one oreign server, maybe this join can
+ * be pushed down, so remember the oid of the foreign server in this
+ * relation.
+ */
+ if (outer_rel->serverid == inner_rel->serverid)
+ joinrel->serverid = outer_rel->serverid;
+
return joinrel;
}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 072d50c..0fb6b60 100644
*** a/src/backend/tcop/postgres.c
--- b/src/backend/tcop/postgres.c
*************** set_plan_disabling_options(const char *a
*** 3168,3173 ****
--- 3168,3176 ----
case 'h': /* hashjoin */
tmp = "enable_hashjoin";
break;
+ case 'f': /* foreignjoin */
+ tmp = "enable_foreignjoin";
+ break;
}
if (tmp)
{
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a71729c..013870a 100644
*** a/src/backend/utils/misc/guc.c
--- b/src/backend/utils/misc/guc.c
*************** static struct config_bool ConfigureNames
*** 756,761 ****
--- 756,770 ----
NULL, NULL, NULL
},
{
+ {"enable_foreignjoin", PGC_USERSET, QUERY_TUNING_METHOD,
+ gettext_noop("Enables the planner's use of foreign join plans."),
+ NULL
+ },
+ &enable_foreignjoin,
+ 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/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a18f14a..34362cd 100644
*** a/src/backend/utils/misc/postgresql.conf.sample
--- b/src/backend/utils/misc/postgresql.conf.sample
***************
*** 240,245 ****
--- 240,246 ----
# - Planner Method Configuration -
#enable_bitmapscan = on
+ #enable_foreignjoin = on
#enable_hashagg = on
#enable_hashjoin = on
#enable_indexscan = on
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index bdd499b..e4d3426 100644
*** a/src/include/executor/executor.h
--- b/src/include/executor/executor.h
*************** extern void ExecFreeExprContext(PlanStat
*** 332,337 ****
--- 332,338 ----
extern TupleDesc ExecGetScanType(ScanState *scanstate);
extern void ExecAssignScanType(ScanState *scanstate, TupleDesc tupDesc);
extern void ExecAssignScanTypeFromOuterPlan(ScanState *scanstate);
+ extern void ExecAssignScanTypeFromTL(ScanState *scanstate);
extern bool ExecRelationIsTargetRelation(EState *estate, Index scanrelid);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 3378ba9..e535e12 100644
*** a/src/include/foreign/fdwapi.h
--- b/src/include/foreign/fdwapi.h
*************** typedef void (*ReScanForeignScan_functio
*** 68,73 ****
--- 68,83 ----
typedef void (*EndForeignScan_function) (ForeignScanState *node);
+ typedef FdwPlan *(*PlanForeignJoin_function) (Oid serverid,
+ PlannerInfo *root,
+ RelOptInfo *joinrel,
+ JoinType jointype,
+ SpecialJoinInfo *sjinfo,
+ Path *outer_path,
+ Path *inner_path,
+ List *restrict_clauses,
+ List *pathkeys);
+
/*
* FdwRoutine is the struct returned by a foreign-data wrapper's handler
*************** typedef struct FdwRoutine
*** 88,93 ****
--- 98,106 ----
IterateForeignScan_function IterateForeignScan;
ReScanForeignScan_function ReScanForeignScan;
EndForeignScan_function EndForeignScan;
+
+ /* functions below are optional */
+ PlanForeignJoin_function PlanForeignJoin;
} FdwRoutine;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index ecf62b3..2110ee4 100644
*** a/src/include/nodes/nodes.h
--- b/src/include/nodes/nodes.h
*************** typedef enum NodeTag
*** 66,71 ****
--- 66,72 ----
T_NestLoop,
T_MergeJoin,
T_HashJoin,
+ T_ForeignJoin,
T_Material,
T_Sort,
T_Group,
*************** typedef enum NodeTag
*** 219,224 ****
--- 220,226 ----
T_NestPath,
T_MergePath,
T_HashPath,
+ T_ForeignJoinPath,
T_TidPath,
T_ForeignPath,
T_AppendPath,
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 535eca7..853f827 100644
*** a/src/include/nodes/plannodes.h
--- b/src/include/nodes/plannodes.h
*************** typedef struct WorkTableScan
*** 440,445 ****
--- 440,446 ----
typedef struct ForeignScan
{
Scan scan;
+ Oid serverid; /* OID of foreign server */
bool fsSystemCol; /* true if any "system column" is needed */
/* use struct pointer to avoid including fdwapi.h here */
struct FdwPlan *fdwplan;
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index ecbbc1c..d3309ac 100644
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
*************** typedef struct RelOptInfo
*** 414,419 ****
--- 414,420 ----
List *joininfo; /* RestrictInfo structures for join clauses
* involving this rel */
bool has_eclass_joins; /* T means joininfo is incomplete */
+ Oid serverid; /* foriegn server, if foreign scan/join */
/* cached info about inner indexscan paths for relation: */
Relids index_outer_relids; /* other relids in indexable join
*************** typedef struct HashPath
*** 939,944 ****
--- 940,955 ----
} HashPath;
/*
+ * A foreignjoin path has no additional field.
+ */
+ typedef struct ForeignJoinPath
+ {
+ JoinPath jpath;
+ /* use struct pointer to avoid including fdwapi.h here */
+ struct FdwPlan *fdwplan; /* FDW-specific information */
+ } ForeignJoinPath;
+
+ /*
* Restriction clause info.
*
* We create one of these for each AND sub-clause of a restriction condition
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 604df33..58cc64a 100644
*** a/src/include/optimizer/cost.h
--- b/src/include/optimizer/cost.h
*************** extern bool enable_nestloop;
*** 60,65 ****
--- 60,66 ----
extern bool enable_material;
extern bool enable_mergejoin;
extern bool enable_hashjoin;
+ extern bool enable_foreignjoin;
extern int constraint_exclusion;
extern double clamp_row_est(double nrows);
*************** extern void cost_mergejoin(MergePath *pa
*** 112,117 ****
--- 113,122 ----
SpecialJoinInfo *sjinfo);
extern void cost_hashjoin(HashPath *path, PlannerInfo *root,
SpecialJoinInfo *sjinfo);
+ /*
+ * cost_foreignjoin() is not defined here because the costs of a foreign join
+ * is estimated by each FDW via PlanForeignJoin.
+ */
extern void cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan);
extern void cost_qual_eval(QualCost *cost, List *quals, PlannerInfo *root);
extern void cost_qual_eval_node(QualCost *cost, Node *qual, PlannerInfo *root);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index ee02732..f824f91 100644
*** a/src/include/optimizer/pathnode.h
--- b/src/include/optimizer/pathnode.h
*************** extern HashPath *create_hashjoin_path(Pl
*** 93,98 ****
--- 93,107 ----
List *restrict_clauses,
List *hashclauses);
+ extern ForeignJoinPath *create_foreignjoin_path(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ JoinType jointype,
+ SpecialJoinInfo *sjinfo,
+ Path *outer_path,
+ Path *inner_path,
+ List *restrict_clauses,
+ List *pathkeys);
+
/*
* prototypes for relnode.c
*/
diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out
index 51d561b..9b8eccb 100644
*** a/src/test/regress/expected/rangefuncs.out
--- b/src/test/regress/expected/rangefuncs.out
***************
*** 1,17 ****
SELECT name, setting FROM pg_settings WHERE name LIKE 'enable%';
! name | setting
! -------------------+---------
! enable_bitmapscan | on
! enable_hashagg | on
! enable_hashjoin | on
! enable_indexscan | on
! enable_material | on
! enable_mergejoin | on
! enable_nestloop | on
! enable_seqscan | on
! enable_sort | on
! enable_tidscan | on
! (10 rows)
CREATE TABLE foo2(fooid int, f2 int);
INSERT INTO foo2 VALUES(1, 11);
--- 1,18 ----
SELECT name, setting FROM pg_settings WHERE name LIKE 'enable%';
! name | setting
! --------------------+---------
! enable_bitmapscan | on
! enable_foreignjoin | on
! enable_hashagg | on
! enable_hashjoin | on
! enable_indexscan | on
! enable_material | on
! enable_mergejoin | on
! enable_nestloop | on
! enable_seqscan | on
! enable_sort | on
! enable_tidscan | on
! (11 rows)
CREATE TABLE foo2(fooid int, f2 int);
INSERT INTO foo2 VALUES(1, 11);
pgsql_fdw-0.1.0.tar.gzapplication/gzip; name=pgsql_fdw-0.1.0.tar.gzDownload
��opN pgsql_fdw-0.1.0.tar �\{wG��������d���DZe����D�$>I��.�-�nw7R4�|���[U�9�����I���U���U������<���L����x�g�����S���_�����8:���������� ��g'v�-� H>6�������� 'X,��������������s8�������;�G�{xtx��;qwl{�g��8��<�N��������i�6w��=�^�8|����d���;_�����p�/��9�o��|�=��?x\��Ge���<
�?���&.�/#;����c�����'ck;��r����f3����O��o�������o�������bo��w������y&R
HeoY���7W������l��}��A%7�D&^���+C;�%>9������h9�����p!��Q��{uuv1 ���_kso����S��W�������x�_�-����}3�to
���
��[[P�&�XV�}�o���7q�D��G����K��U��g�����Z�J���L���������p�s\�<��/]�/��-9���|'�Ry��l�$G��7w��fs������o����0+����6��[jrsqkI��&������o��_��V�>n��/�^����/�����9}��J��$���'ZA�y�Y"jN]����6�M���Y�W�����8�9�o���M[���^�6��[O�������'�E0�!������Lf�������\;��}d����j�s"#���^'�H^_5�(@���(�(��\�6�}wN@�>��l�����y��rl_��X�H��@��#�x"�Xv��"���/0� f�����{����d,D�<�V�)%���CE�;o_��E0~�h#�L<�Yf����a��9����?���� �/�%<_$ljs���QL�@�9Y�epO�up��g��xQ���`)|���1c������)L�<��r�Yz��q���F�M����,��H
"�"����>5s�<l��0��}'a���r�yW�P���Kf"Nd�������*�u�V����A��3���"YG&a����GK��������j�F�j��U���F��@,KS��F��4�X�evZ��!�lh�
XT��8X��8`���]��������r-'�I�F{�)+y��
n��h���"���kh�Ifv�,�%��C��;d2S��`�C[����S�Q>��=G}���d_`�����k�A��"��*�O����.8�*����?�����s���a[��`lU�76����{x4���K���H��a|�U����k?~t~����a���+���:�lUj�"
����+"v}�3���=���(�?%2�7`�9�,�A4��UP3�X(�g�����G�/��b����n�������l���Y@M!l=��Q�aLl(�6�HA����{���}���`��i��{V��������BI8�(��1�%��#�37`;�Ta���A��`fB�Xg������j��0NzU��Y�pS��I���H.`�O.G �RC�� d��V�DY@�[����aT��x�9M�9R��Z�|���8���L��\(]<29���4���?
��L�����f� �^�����m:F�;������HN�9$VW�|� %�"�����(����p����.�8�lh�������*z���Y�����iLI)j��}�AI;KA� �\lZ�\c?���Wma��ln��G��Qg2���pJ#QXxS/����.P�pkDA^c�M����]Ra���I�����mBr%wr� .I�&@��KL����&�J��Kx��1rnC�G��t��A��[fkf&�P�����R"���6��T�����M��������e����_bc����.����9x��-�
(u �6]O����)*�x��A#�z���/G)���t���-���~�=�?�Hx0�0�K�42K�`k�V�G,�&1�l��:���ACT�j*�*$J?E��b� �U-?`a���r
�y��DUU-������dlS��( ��T� 6����|�����M�
�nJ^��zE
����������o6������;��������N����[�p����o��$a��T�5�����N��>^=�axMjv���J��$\�Ae������1�3�D�������~,y��)�Q�|�L&d��0d��l���G�Sk�n���6H��~�wZ���S�L���F���E��
ND{9Kv)1�C�7�Q^��/ g����#H�L�X� x���U��[�g�v�TS`������=
���LqT�I8'I^�>X���H�s���0����M����
��}�z��]rG��I/�T?�Is�A_���q�P�\�)�I��#�J i��y���5ujY$������C���E6b�7�|4�C�^B����y���^����X �4c�;�.?L��_@�^�jw�������A �L!0#X����k�?2�n�C��g�(��a��;������`���j���EnA�o�� ���3���),
;�*�[o�9�0R(�7��T�
�*�RN���P)&Q�0��*.��J���4 K�R~}�F�m�h�[*�S�)c��2���d��e��QrY���Ml���S�d<o������^����B��*���,&���� ���Vu�-%�]�C7��u�i�Y�Xl�Q]1�^�r�����N��X�4����F�2�<`GZj���{���<���U��r�\w�z��-�yS������>g��mii���oR�4��*��s����T@��T�
AU���v�H����oV�!2Q#�"���2jy����m4��;5a�Qo��T��
b4�U�*�?6f����Z��3�oY�ans�EPB������;9?G�@#t?|��������FR�|s�j��'v]��U��D�G�@(sG$��d���������j���1�<���@�Zh�E�f�;���2���
<U[�!5��j�U�r�����AA����yx�9_G)��S����[�:u�T��2�I1���Q��������I�&%)����dL�m"g����,p�G|�C���n�8��vg
��hR��;�E�:&�V�d��Zi��-��gR��/c�� ;Z��sj���E�9R���eY�!��ios�I��N��b���
U��{�b��S�f�Q���FG/�O��s���d�h�s�>R5p
��2�D��L�r-?�T9ZP[je�������r.Zu�8`?�A�&s�)m�%'^<SI5�l,�����"���l�2���zH8"W����r�4��8�xf����� ,}�x��%���<��E���XF1+�������e�����X����d-�T+C���v��o�0L�F6�A�h��F`��W��������,�����j���U����@��>8�7y��?r�w������?��w�^������y��wx�7?�Y�x�Cl����/�+!�z��
�������:��;����Nh�DA��.(��`:���a�U��|��"��VB�=%��g��V����Y��
k�������%�[Y]�����A�)�� �"�(����Y,�H~�+Tm�b��w�8�3��Y���&�N���)�����|L��&�x�-!@�P����
���(�����g&^����P�MCu
� Z��MK���*`��E�_��PN���`u��[�T(������M��W���Nh].�� '*�@u�$����v$'� }�����y!� ��� �db�>���|!�+V�R"��0�2eD��07���B�r+ 5��G7i$��|/���jJBIs���E�
s�g����A%��s+��B�S��b�rx�Rl�d�%���'m�a$i_��;>�������Qxx��P�n�2���R�N�K>k������
�A�� ����z�K��� �"����Fb�������)�O����R"��C%�M5�@�.�k�Z�����B]���
��+f$s"gV%��J�u���JY=�rM#���D%�N�)������N��(�����������/�
��R?�te���l!���d�>�-�
���o�_vV��AM`�.]n(�����|W� �Q�A� ��<���Q�xT��+�<Q+3��[P�<\��W����i��;��M�#)���r-�������T������m��8���%S�_��E�0`E���u�"]]�N�O�;�`����`���m�)w�;L�H>���h.������k�kJ������u�:�Z|P�[��a��!��d��I>nb�Us�� �����oE��o�L>���)V$�Z��?T� �~C|��G��z�/J�;�����/�����~G�������y����������R]������~!|X����2�EnC�E� �y<��`��Ri������M�;r>�\T��;�P����?�9X���+B �If�Y3��� ���y �@>�)Y�2$'(�(��cD�;�t�"��a����5��s�
\B���S/��R0i�\(��V���E��!rI�x/�k�)[J��i�{�a���P�
?�KN�<��K��7��ZNthe�0P�4�6�?@V#�2k����f���[9Yu2����Tn�W��u���S^29�<�2"h^�3{�?&��U�(Z�FIE�/cQ���W�T�4%Vb1���e���� �g5�W?��~{4��w��e��7�F��1fE�/&��
?�����t@������� r���n�G�
�s��
i��e/U;{,I�BY�KV���pJ���k�z�A�TC1=� ��<���|Og������}ul�nmz�`c�Hg!�U��L:�l�����E\��������0�nNq���O�>\
A4gG.�������H�Nn\�I�ni�B�����G"�������z�cE���L�8����8�:���� U6 �u�Bv�W_��ax�����qg�������������}���*��V7������`��G�~����3l���5F����}
K�
�����ZOiOk)R73��I�6S�zS�*��E�z��x���"�u��@M)3�V��o�����4��=xOJ077KFF���*1��{|U�B��~��k�w'Ydj�B.�sL�j�_�AR�"�(DG-�@>E����>�?y�`����Sa
OE�6��@���9u� �8�W �.&:������g���d���J �����E��~�=vtK2�4+E���#]��t�2��'tXMQ��?Xp��n���m���avW3w���H��#UW�'� ��$������f�M��.�^��^�C�y���C���k����:����)����Ww��v�uh���S�CN���*�����B��\�|RiHn6���?t~��O�&�����40_'
����1"~g`�'b���]iK������\���}U�c����y����T�?`G�����(���a���o����78�������
B%x�h\��\�w[O����\�,�G����H�Z�����N:4���t�"���6k���i9��r��I�{��Rw�[?�!vL��^��VQ]�*�����M�E�����?�d��j����u�����
��
����.�w����j]��L������B�cFS��X�2��ZM���Z�Kpy�{�E�5�H{*-�#hQ�
� 9de+$�\�p[.ML
5]����*�D��X���]|��������������8��@��|� �Tc%�"�B@��
0������Bw(����UM��<e'�2�8;�x���x��N&��3q��������+�g�kUu7@P^"���[w9��s�~N���REVG��q�y}W���k��1 d���2�
6�D]c�����0�����.��2�b������
"��\d'��� ah�������$�r���r�E
��D�a�W|h�7t@����ZR���1pT��3w��U�/B�q�Q���zGQ[�U[E���7��� �"��B���'�O��"���I�l@7(`��Q��R�+���3��S&U|��9���y���MtuX1T�<�����u@S�3#$�[�s��;�����a�pD�i�(F>�r��n��1�6E�0h�l���D��y��]�n'^��]��i� �}��A�8"�20�;���\4�����pO���nc��2��w�7��)0U���R��Y�%�V������6�pB�����������J�s�o�<D��X����?������T�R�n� ��c04&&��H�1@GLr K�C�����7�7I��6""��S��y��j��38h-��F$>9�;QP�(���F�P�%�"��Z-Q�^e�I��(��*�
�� ����
�/� ��q��+���������I�#�/���=���
Do����N*�y��f������c��-X���RY��e�k-�=���5�]:�F��!8�%�b����e2��5J�EOs������U�k��E i�&� c�Mi4"���Q�Jw�����Z��J�����O��B#�P�X<��+h���z��W������k�������'����bt {GH�ls$hb ���|�e�BQO�$F�%G�= �!8Y����KL������rmt^�-D6
�'����C�-<�]%a���{5��Z��#oN"�|��%�hJOB���������j�cNZ�'s��fT���c��T�}�@h����#���(?#�?�/j�1�����s�?�_\�(�����D�?���?�����a} --����I ���<>�y�D+4��4:���*I[�in��Q�� ����p�n���g����4���������/_y.��������P����@��tIc��]v����!�J�-{���������^,�t]�>��/�)�K�w�3%]��^�Zi����-�0��J�0X�� �������f+*
(�vY�+6���)+cFD�K��-�N��������yMv���Q��/�n��0j���73T6�r��&