On 2017/07/21 19:16, Etsuro Fujita wrote:
On 2017/07/20 11:21, Etsuro Fujita wrote:
On 2017/07/19 23:36, Tom Lane wrote:
Please put the responsibility of doing const-expression simplification
in these cases somewhere closer to where the problem is being created.
It would be reasonable that it's the FDW's responsibility to do that
const-simplification if necessary?
There seems to be no objections, so I removed the const-expression
simplification from the patch and I added the note to the docs for
AddForeignUpdateTargets.
Attached is an updated version of the patch.
I cleaned up the patch a bit. PFA a new version of the patch.
Best regards,
Etsuro Fujita
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 6962,6967 **** update bar set f2 = f2 + 100 returning *;
--- 6962,7026 ----
7 | 277
(6 rows)
+ -- Test that UPDATE/DELETE with inherited target works with row-level triggers
+ CREATE TRIGGER trig_row_before
+ BEFORE UPDATE OR DELETE ON bar2
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ CREATE TRIGGER trig_row_after
+ AFTER UPDATE OR DELETE ON bar2
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+ explain (verbose, costs off)
+ update bar set f2 = f2 + 100;
+ QUERY PLAN
+
--------------------------------------------------------------------------------------
+ Update on public.bar
+ Update on public.bar
+ Foreign Update on public.bar2
+ Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1 RETURNING
f1, f2, f3
+ -> Seq Scan on public.bar
+ Output: bar.f1, (bar.f2 + 100), bar.ctid
+ -> Foreign Scan on public.bar2
+ Output: bar2.f1, (bar2.f2 + 100), bar2.f3, bar2.ctid, bar2.*
+ Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
+ (9 rows)
+
+ update bar set f2 = f2 + 100;
+ NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
+ NOTICE: OLD: (3,333,33),NEW: (3,433,33)
+ NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
+ NOTICE: OLD: (4,344,44),NEW: (4,444,44)
+ NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
+ NOTICE: OLD: (7,277,77),NEW: (7,377,77)
+ NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
+ NOTICE: OLD: (3,333,33),NEW: (3,433,33)
+ NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
+ NOTICE: OLD: (4,344,44),NEW: (4,444,44)
+ NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
+ NOTICE: OLD: (7,277,77),NEW: (7,377,77)
+ explain (verbose, costs off)
+ delete from bar where f2 < 400;
+ QUERY PLAN
+
---------------------------------------------------------------------------------------------
+ Delete on public.bar
+ Delete on public.bar
+ Foreign Delete on public.bar2
+ Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 RETURNING f1, f2, f3
+ -> Seq Scan on public.bar
+ Output: bar.ctid
+ Filter: (bar.f2 < 400)
+ -> Foreign Scan on public.bar2
+ Output: bar2.ctid, bar2.*
+ Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 <
400)) FOR UPDATE
+ (10 rows)
+
+ delete from bar where f2 < 400;
+ NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2
+ NOTICE: OLD: (7,377,77)
+ NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON bar2
+ NOTICE: OLD: (7,377,77)
+ -- cleanup
+ DROP TRIGGER trig_row_before ON bar2;
+ DROP TRIGGER trig_row_after ON bar2;
drop table foo cascade;
NOTICE: drop cascades to foreign table foo2
drop table bar cascade;
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 1632,1637 **** explain (verbose, costs off)
--- 1632,1657 ----
update bar set f2 = f2 + 100 returning *;
update bar set f2 = f2 + 100 returning *;
+ -- Test that UPDATE/DELETE with inherited target works with row-level triggers
+ CREATE TRIGGER trig_row_before
+ BEFORE UPDATE OR DELETE ON bar2
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+ CREATE TRIGGER trig_row_after
+ AFTER UPDATE OR DELETE ON bar2
+ FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+ explain (verbose, costs off)
+ update bar set f2 = f2 + 100;
+ update bar set f2 = f2 + 100;
+
+ explain (verbose, costs off)
+ delete from bar where f2 < 400;
+ delete from bar where f2 < 400;
+
+ -- cleanup
+ DROP TRIGGER trig_row_before ON bar2;
+ DROP TRIGGER trig_row_after ON bar2;
drop table foo cascade;
drop table bar cascade;
drop table loct1;
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
***************
*** 428,438 **** AddForeignUpdateTargets (Query *parsetree,
Avoid using names matching <literal>ctid<replaceable>N</></literal>,
<literal>wholerow</literal>, or
<literal>wholerow<replaceable>N</></literal>, as the core system can
! generate junk columns of these names.
</para>
<para>
! This function is called in the rewriter, not the planner, so the
information available is a bit different from that available to the
planning routines.
<literal>parsetree</> is the parse tree for the <command>UPDATE</> or
--- 428,440 ----
Avoid using names matching <literal>ctid<replaceable>N</></literal>,
<literal>wholerow</literal>, or
<literal>wholerow<replaceable>N</></literal>, as the core system can
! generate junk columns of these names. Also, as it assumes that those
! expressions have already been simplified enough to execute,
! apply <function>eval_const_expressions</> if necessary.
</para>
<para>
! Although this function is called in the planner, the
information available is a bit different from that available to the
planning routines.
<literal>parsetree</> is the parse tree for the <command>UPDATE</> or
*** a/doc/src/sgml/rules.sgml
--- b/doc/src/sgml/rules.sgml
***************
*** 167,178 ****
<para>
<command>DELETE</command> commands don't need a normal target list
! because they don't produce any result. Instead, the rule system
adds a special <acronym>CTID</> entry to the empty target list,
to allow the executor to find the row to be deleted.
(<acronym>CTID</> is added when the result relation is an ordinary
! table. If it is a view, a whole-row variable is added instead,
! as described in <xref linkend="rules-views-update">.)
</para>
<para>
--- 167,178 ----
<para>
<command>DELETE</command> commands don't need a normal target list
! because they don't produce any result. Instead, the planner
adds a special <acronym>CTID</> entry to the empty target list,
to allow the executor to find the row to be deleted.
(<acronym>CTID</> is added when the result relation is an ordinary
! table. If it is a view, a whole-row variable is added instead, by
! the rule system, as described in <xref linkend="rules-views-update">.)
</para>
<para>
***************
*** 194,200 ****
column = expression</literal> part of the command. The planner will
handle missing columns by inserting expressions that copy the values
from the old row into the new one. Just as for <command>DELETE</>,
! the rule system adds a <acronym>CTID</> or whole-row variable so that
the executor can identify the old row to be updated.
</para>
--- 194,200 ----
column = expression</literal> part of the command. The planner will
handle missing columns by inserting expressions that copy the values
from the old row into the new one. Just as for <command>DELETE</>,
! a <acronym>CTID</> or whole-row variable is added so that
the executor can identify the old row to be updated.
</para>
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
***************
*** 1661,1666 **** ExecModifyTable(ModifyTableState *node)
--- 1661,1667 ----
EvalPlanQualSetSlot(&node->mt_epqstate, planSlot);
slot = planSlot;
+ tupleid = NULL;
oldtuple = NULL;
if (junkfilter != NULL)
{
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
***************
*** 1458,1464 **** grouping_planner(PlannerInfo *root, bool inheritance_update,
double tuple_fraction)
{
Query *parse = root->parse;
! List *tlist = parse->targetList;
int64 offset_est = 0;
int64 count_est = 0;
double limit_tuples = -1.0;
--- 1458,1464 ----
double tuple_fraction)
{
Query *parse = root->parse;
! List *tlist;
int64 offset_est = 0;
int64 count_est = 0;
double limit_tuples = -1.0;
***************
*** 1588,1594 **** grouping_planner(PlannerInfo *root, bool inheritance_update,
}
/* Preprocess targetlist */
! tlist = preprocess_targetlist(root, tlist);
if (parse->onConflict)
parse->onConflict->onConflictSet =
--- 1588,1594 ----
}
/* Preprocess targetlist */
! tlist = preprocess_targetlist(root);
if (parse->onConflict)
parse->onConflict->onConflictSet =
*** a/src/backend/optimizer/prep/preptlist.c
--- b/src/backend/optimizer/prep/preptlist.c
***************
*** 4,23 ****
* Routines to preprocess the parse tree target list
*
* For INSERT and UPDATE queries, the targetlist must contain an entry for
! * each attribute of the target relation in the correct order. For all query
! * types, we may need to add junk tlist entries for Vars used in the RETURNING
! * list and row ID information needed for SELECT FOR UPDATE locking and/or
! * EvalPlanQual checking.
*
! * The rewriter's rewriteTargetListIU and rewriteTargetListUD routines
! * also do preprocessing of the targetlist. The division of labor between
! * here and there is partially historical, but it's not entirely arbitrary.
! * In particular, consider an UPDATE across an inheritance tree. What the
! * rewriter does need be done only once (because it depends only on the
! * properties of the parent relation). What's done here has to be done over
! * again for each child relation, because it depends on the column list of
! * the child, which might have more columns and/or a different column order
! * than the parent.
*
* The fact that rewriteTargetListIU sorts non-resjunk tlist entries by column
* position, which expand_targetlist depends on, violates the above comment
--- 4,24 ----
* Routines to preprocess the parse tree target list
*
* For INSERT and UPDATE queries, the targetlist must contain an entry for
! * each attribute of the target relation in the correct order. For UPDATE and
! * DELETE queries, it must also contain a junk tlist entry needed to allow the
! * executor to identify the physical locations of the rows to be updated or
! * deleted. For all query types, we may need to add junk tlist entries for
! * Vars used in the RETURNING list and row ID information needed for SELECT
! * FOR UPDATE locking and/or EvalPlanQual checking.
*
! * The rewriter's rewriteTargetListIU routine also does preprocessing of the
! * targetlist. The division of labor between here and there is partially
! * historical, but it's not entirely arbitrary. In particular, consider an
! * UPDATE across an inheritance tree. What the rewriter does need be done
! * only once (because it depends only on the properties of the parent
! * relation). What's done here has to be done over again for each child
! * relation, because it depends on the column list of the child, which might
! * have more columns and/or a different column order than the parent.
*
* The fact that rewriteTargetListIU sorts non-resjunk tlist entries by column
* position, which expand_targetlist depends on, violates the above comment
***************
*** 41,47 ****
--- 42,50 ----
#include "access/heapam.h"
#include "access/sysattr.h"
#include "catalog/pg_type.h"
+ #include "foreign/fdwapi.h"
#include "nodes/makefuncs.h"
+ #include "optimizer/clauses.h"
#include "optimizer/prep.h"
#include "optimizer/tlist.h"
#include "optimizer/var.h"
***************
*** 52,57 ****
--- 55,62 ----
static List *expand_targetlist(List *tlist, int command_type,
Index result_relation, List *range_table);
+ static void rewrite_targetlist(Query *parse,
+ Index result_relation, List *range_table);
/*
***************
*** 61,72 **** static List *expand_targetlist(List *tlist, int command_type,
* Returns the new targetlist.
*/
List *
! preprocess_targetlist(PlannerInfo *root, List *tlist)
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
ListCell *lc;
/*
--- 66,78 ----
* Returns the new targetlist.
*/
List *
! preprocess_targetlist(PlannerInfo *root)
{
Query *parse = root->parse;
int result_relation = parse->resultRelation;
List *range_table = parse->rtable;
CmdType command_type = parse->commandType;
+ List *tlist;
ListCell *lc;
/*
***************
*** 82,87 **** preprocess_targetlist(PlannerInfo *root, List *tlist)
--- 88,103 ----
}
/*
+ * For UPDATE/DELETE, add a necessary junk column needed to allow the
+ * executor to identify the physical locations of the rows to be updated
+ * or deleted.
+ */
+ if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
+ rewrite_targetlist(parse, result_relation, range_table);
+
+ tlist = parse->targetList;
+
+ /*
* for heap_form_tuple to work, the targetlist must match the exact
order
* of the attributes. We also need to fill in any missing attributes.
-ay
* 10/94
***************
*** 391,396 **** expand_targetlist(List *tlist, int command_type,
--- 407,502 ----
return new_tlist;
}
+ /*
+ * rewrite_targetlist - rewrite UPDATE/DELETE targetlist as needed
+ *
+ * This function adds a "junk" TLE that is needed to allow the executor to
+ * find the original row for the update or delete. When the target relation
+ * is a regular table, the junk TLE emits the ctid attribute of the original
+ * row. When the target relation is a foreign table, we let the FDW decide
+ * what to add.
+ */
+ static void
+ rewrite_targetlist(Query *parse, Index result_relation, List *range_table)
+ {
+ Var *var = NULL;
+ const char *attrname;
+ TargetEntry *tle;
+ RangeTblEntry *target_rte;
+ Relation target_relation;
+
+ target_rte = rt_fetch(result_relation, range_table);
+
+ /*
+ * We assume that the relation was already locked by the rewriter, so
+ * we need no lock here.
+ */
+ target_relation = heap_open(target_rte->relid, NoLock);
+
+ if (target_relation->rd_rel->relkind == RELKIND_RELATION ||
+ target_relation->rd_rel->relkind == RELKIND_MATVIEW ||
+ target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ /*
+ * Emit CTID so that executor can find the row to update or
delete.
+ */
+ var = makeVar(result_relation,
+ SelfItemPointerAttributeNumber,
+ TIDOID,
+ -1,
+ InvalidOid,
+ 0);
+
+ attrname = "ctid";
+ }
+ else if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ /*
+ * Let the foreign table's FDW add whatever junk TLEs it wants.
+ */
+ FdwRoutine *fdwroutine;
+
+ fdwroutine = GetFdwRoutineForRelation(target_relation, false);
+
+ if (fdwroutine->AddForeignUpdateTargets != NULL)
+ fdwroutine->AddForeignUpdateTargets(parse, target_rte,
+
target_relation);
+
+ /*
+ * If we have a row-level trigger corresponding to the
operation, emit
+ * a whole-row Var so that executor will have the "old" row to
pass to
+ * the trigger. Alas, this misses system columns.
+ */
+ if (target_relation->trigdesc &&
+ ((parse->commandType == CMD_UPDATE &&
+ (target_relation->trigdesc->trig_update_after_row ||
+ target_relation->trigdesc->trig_update_before_row))
||
+ (parse->commandType == CMD_DELETE &&
+ (target_relation->trigdesc->trig_delete_after_row ||
+ target_relation->trigdesc->trig_delete_before_row))))
+ {
+ var = makeWholeRowVar(target_rte,
+
result_relation,
+ 0,
+ false);
+
+ attrname = "wholerow";
+ }
+ }
+
+ if (var != NULL)
+ {
+ tle = makeTargetEntry((Expr *) var,
+
list_length(parse->targetList) + 1,
+ pstrdup(attrname),
+ true);
+
+ parse->targetList = lappend(parse->targetList, tle);
+ }
+
+ heap_close(target_relation, NoLock);
+ }
+
/*
* Locate PlanRowMark for given RT index, or return NULL if none
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
***************
*** 72,79 **** static TargetEntry *process_matched_tle(TargetEntry *src_tle,
static Node *get_assignment_input(Node *node);
static void rewriteValuesRTE(RangeTblEntry *rte, Relation target_relation,
List *attrnos);
- static void rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
- Relation target_relation);
static void markQueryForLocking(Query *qry, Node *jtnode,
LockClauseStrength strength,
LockWaitPolicy waitPolicy,
bool pushedDown);
--- 72,77 ----
***************
*** 1288,1390 **** rewriteValuesRTE(RangeTblEntry *rte, Relation
target_relation, List *attrnos)
/*
- * rewriteTargetListUD - rewrite UPDATE/DELETE targetlist as needed
- *
- * This function adds a "junk" TLE that is needed to allow the executor to
- * find the original row for the update or delete. When the target relation
- * is a regular table, the junk TLE emits the ctid attribute of the original
- * row. When the target relation is a view, there is no ctid, so we instead
- * emit a whole-row Var that will contain the "old" values of the view row.
- * If it's a foreign table, we let the FDW decide what to add.
- *
- * For UPDATE queries, this is applied after rewriteTargetListIU. The
- * ordering isn't actually critical at the moment.
- */
- static void
- rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
- Relation target_relation)
- {
- Var *var = NULL;
- const char *attrname;
- TargetEntry *tle;
-
- if (target_relation->rd_rel->relkind == RELKIND_RELATION ||
- target_relation->rd_rel->relkind == RELKIND_MATVIEW ||
- target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- /*
- * Emit CTID so that executor can find the row to update or
delete.
- */
- var = makeVar(parsetree->resultRelation,
- SelfItemPointerAttributeNumber,
- TIDOID,
- -1,
- InvalidOid,
- 0);
-
- attrname = "ctid";
- }
- else if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
- {
- /*
- * Let the foreign table's FDW add whatever junk TLEs it wants.
- */
- FdwRoutine *fdwroutine;
-
- fdwroutine = GetFdwRoutineForRelation(target_relation, false);
-
- if (fdwroutine->AddForeignUpdateTargets != NULL)
- fdwroutine->AddForeignUpdateTargets(parsetree,
target_rte,
-
target_relation);
-
- /*
- * If we have a row-level trigger corresponding to the
operation, emit
- * a whole-row Var so that executor will have the "old" row to
pass to
- * the trigger. Alas, this misses system columns.
- */
- if (target_relation->trigdesc &&
- ((parsetree->commandType == CMD_UPDATE &&
- (target_relation->trigdesc->trig_update_after_row ||
- target_relation->trigdesc->trig_update_before_row))
||
- (parsetree->commandType == CMD_DELETE &&
- (target_relation->trigdesc->trig_delete_after_row ||
- target_relation->trigdesc->trig_delete_before_row))))
- {
- var = makeWholeRowVar(target_rte,
-
parsetree->resultRelation,
- 0,
- false);
-
- attrname = "wholerow";
- }
- }
- else
- {
- /*
- * Emit whole-row Var so that executor will have the "old" view
row to
- * pass to the INSTEAD OF trigger.
- */
- var = makeWholeRowVar(target_rte,
-
parsetree->resultRelation,
- 0,
- false);
-
- attrname = "wholerow";
- }
-
- if (var != NULL)
- {
- tle = makeTargetEntry((Expr *) var,
-
list_length(parsetree->targetList) + 1,
- pstrdup(attrname),
- true);
-
- parsetree->targetList = lappend(parsetree->targetList, tle);
- }
- }
-
-
- /*
* matchLocks -
* match the list of locks and returns the matching rules
*/
--- 1286,1291 ----
***************
*** 1497,1502 **** ApplyRetrieveRule(Query *parsetree,
--- 1398,1405 ----
parsetree->commandType == CMD_DELETE)
{
RangeTblEntry *newrte;
+ Var *var;
+ TargetEntry *tle;
rte = rt_fetch(rt_index, parsetree->rtable);
newrte = copyObject(rte);
***************
*** 1527,1532 **** ApplyRetrieveRule(Query *parsetree,
--- 1430,1448 ----
ChangeVarNodes((Node *) parsetree->returningList,
rt_index,
parsetree->resultRelation,
0);
+ /*
+ * To allow the executor to find the original view row
to pass to
+ * the INSTEAD OF trigger, we add a whole-row reference
to the
+ * original RTE to the query's targetlist.
+ */
+ var = makeWholeRowVar(rte, rt_index, 0, false);
+ tle = makeTargetEntry((Expr *) var,
+
list_length(parsetree->targetList) + 1,
+
pstrdup("wholerow"),
+ true);
+
+ parsetree->targetList = lappend(parsetree->targetList,
tle);
+
/* Now, continue with expanding the original view RTE */
}
else
***************
*** 2967,2992 **** rewriteTargetView(Query *parsetree, Relation view)
view_rte->securityQuals = NIL;
/*
- * For UPDATE/DELETE, rewriteTargetListUD will have added a wholerow
junk
- * TLE for the view to the end of the targetlist, which we no longer
need.
- * Remove it to avoid unnecessary work when we process the targetlist.
- * Note that when we recurse through rewriteQuery a new junk TLE will be
- * added to allow the executor to find the proper row in the new target
- * relation. (So, if we failed to do this, we might have multiple junk
- * TLEs with the same name, which would be disastrous.)
- */
- if (parsetree->commandType != CMD_INSERT)
- {
- TargetEntry *tle = (TargetEntry *) llast(parsetree->targetList);
-
- Assert(tle->resjunk);
- Assert(IsA(tle->expr, Var) &&
- ((Var *) tle->expr)->varno ==
parsetree->resultRelation &&
- ((Var *) tle->expr)->varattno == 0);
- parsetree->targetList = list_delete_ptr(parsetree->targetList,
tle);
- }
-
- /*
* Now update all Vars in the outer query that reference the view to
* reference the appropriate column of the base relation instead.
*/
--- 2883,2888 ----
***************
*** 3347,3357 **** RewriteQuery(Query *parsetree, List *rewrite_events)
parsetree->override,
rt_entry_relation,
parsetree->resultRelation, NULL);
- rewriteTargetListUD(parsetree, rt_entry,
rt_entry_relation);
}
else if (event == CMD_DELETE)
{
! rewriteTargetListUD(parsetree, rt_entry,
rt_entry_relation);
}
else
elog(ERROR, "unrecognized commandType: %d", (int)
event);
--- 3243,3252 ----
parsetree->override,
rt_entry_relation,
parsetree->resultRelation, NULL);
}
else if (event == CMD_DELETE)
{
! /* Nothing to do here */
}
else
elog(ERROR, "unrecognized commandType: %d", (int)
event);
*** a/src/include/optimizer/prep.h
--- b/src/include/optimizer/prep.h
***************
*** 38,44 **** extern Expr *canonicalize_qual(Expr *qual);
/*
* prototypes for preptlist.c
*/
! extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
extern List *preprocess_onconflict_targetlist(List *tlist,
int
result_relation, List *range_table);
--- 38,44 ----
/*
* prototypes for preptlist.c
*/
! extern List *preprocess_targetlist(PlannerInfo *root);
extern List *preprocess_onconflict_targetlist(List *tlist,
int
result_relation, List *range_table);
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers