summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/backend/access/transam/xlogreader.c20
-rw-r--r--src/backend/optimizer/plan/initsplan.c24
-rw-r--r--src/backend/optimizer/plan/planner.c38
-rw-r--r--src/backend/optimizer/plan/subselect.c46
-rw-r--r--src/backend/optimizer/prep/prepjointree.c320
-rw-r--r--src/backend/optimizer/util/clauses.c92
-rw-r--r--src/backend/optimizer/util/inherit.c10
-rw-r--r--src/backend/optimizer/util/plancat.c127
-rw-r--r--src/backend/replication/libpqwalreceiver/libpqwalreceiver.c3
-rw-r--r--src/backend/storage/aio/README.md5
-rw-r--r--src/backend/utils/adt/selfuncs.c18
-rw-r--r--src/bin/pg_dump/pg_dump.c160
-rw-r--r--src/bin/pg_dump/pg_dump.h4
-rw-r--r--src/bin/pg_dump/pg_dump_sort.c15
-rw-r--r--src/bin/pg_dump/t/002_pg_dump.pl30
-rw-r--r--src/include/access/amapi.h2
-rw-r--r--src/include/libpq/libpq-be-fe-helpers.h30
-rw-r--r--src/include/nodes/pathnodes.h12
-rw-r--r--src/include/optimizer/optimizer.h2
-rw-r--r--src/include/optimizer/plancat.h4
-rw-r--r--src/include/optimizer/prep.h2
-rw-r--r--src/include/replication/slot.h19
-rw-r--r--src/include/storage/aio.h2
-rw-r--r--src/interfaces/ecpg/ecpglib/connect.c53
-rw-r--r--src/interfaces/ecpg/ecpglib/descriptor.c12
-rw-r--r--src/interfaces/ecpg/ecpglib/ecpglib_extern.h2
-rw-r--r--src/interfaces/ecpg/ecpglib/execute.c42
-rw-r--r--src/interfaces/ecpg/ecpglib/memory.c11
-rw-r--r--src/interfaces/ecpg/ecpglib/prepare.c47
-rw-r--r--src/port/pgmkdirp.c2
-rw-r--r--src/test/perl/PostgreSQL/Test/Cluster.pm26
-rw-r--r--src/test/recovery/meson.build1
-rw-r--r--src/test/recovery/t/027_stream_regress.pl11
-rw-r--r--src/test/recovery/t/046_checkpoint_logical_slot.pl142
-rw-r--r--src/test/recovery/t/047_checkpoint_physical_slot.pl7
-rw-r--r--src/test/regress/expected/generated_virtual.out28
-rw-r--r--src/test/regress/expected/join.out6
-rw-r--r--src/test/regress/expected/predicate.out54
-rw-r--r--src/test/regress/sql/generated_virtual.sql9
-rw-r--r--src/test/regress/sql/predicate.sql18
-rw-r--r--src/tools/pgindent/typedefs.list1
41 files changed, 1125 insertions, 332 deletions
diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c
index ac1f801b1eb..dcc8d4f9c1b 100644
--- a/src/backend/access/transam/xlogreader.c
+++ b/src/backend/access/transam/xlogreader.c
@@ -723,11 +723,12 @@ restart:
/* Calculate pointer to beginning of next page */
targetPagePtr += XLOG_BLCKSZ;
- /* Wait for the next page to become available */
- readOff = ReadPageInternal(state, targetPagePtr,
- Min(total_len - gotlen + SizeOfXLogShortPHD,
- XLOG_BLCKSZ));
-
+ /*
+ * Read the page header before processing the record data, so we
+ * can handle the case where the previous record ended as being a
+ * partial one.
+ */
+ readOff = ReadPageInternal(state, targetPagePtr, SizeOfXLogShortPHD);
if (readOff == XLREAD_WOULDBLOCK)
return XLREAD_WOULDBLOCK;
else if (readOff < 0)
@@ -776,6 +777,15 @@ restart:
goto err;
}
+ /* Wait for the next page to become available */
+ readOff = ReadPageInternal(state, targetPagePtr,
+ Min(total_len - gotlen + SizeOfXLogShortPHD,
+ XLOG_BLCKSZ));
+ if (readOff == XLREAD_WOULDBLOCK)
+ return XLREAD_WOULDBLOCK;
+ else if (readOff < 0)
+ goto err;
+
/* Append the continuation from this page to the buffer */
pageHeaderSize = XLogPageHeaderSize(pageHeader);
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 01804b085b3..3e3fec89252 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -3048,36 +3048,16 @@ add_base_clause_to_rel(PlannerInfo *root, Index relid,
* expr_is_nonnullable
* Check to see if the Expr cannot be NULL
*
- * If the Expr is a simple Var that is defined NOT NULL and meanwhile is not
- * nulled by any outer joins, then we can know that it cannot be NULL.
+ * Currently we only support simple Vars.
*/
static bool
expr_is_nonnullable(PlannerInfo *root, Expr *expr)
{
- RelOptInfo *rel;
- Var *var;
-
/* For now only check simple Vars */
if (!IsA(expr, Var))
return false;
- var = (Var *) expr;
-
- /* could the Var be nulled by any outer joins? */
- if (!bms_is_empty(var->varnullingrels))
- return false;
-
- /* system columns cannot be NULL */
- if (var->varattno < 0)
- return true;
-
- /* is the column defined NOT NULL? */
- rel = find_base_rel(root, var->varno);
- if (var->varattno > 0 &&
- bms_is_member(var->varattno, rel->notnullattnums))
- return true;
-
- return false;
+ return var_is_nonnullable(root, (Var *) expr, true);
}
/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 549aedcfa99..c989e72cac5 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
glob->transientPlan = false;
glob->dependsOnRole = false;
glob->partition_directory = NULL;
+ glob->rel_notnullatts_hash = NULL;
/*
* Assess whether it's feasible to use parallel mode for this query. We
@@ -721,6 +722,18 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
transform_MERGE_to_join(parse);
/*
+ * Scan the rangetable for relation RTEs and retrieve the necessary
+ * catalog information for each relation. Using this information, clear
+ * the inh flag for any relation that has no children, collect not-null
+ * attribute numbers for any relation that has column not-null
+ * constraints, and expand virtual generated columns for any relation that
+ * contains them. Note that this step does not descend into sublinks and
+ * subqueries; if we pull up any sublinks or subqueries below, their
+ * relation RTEs are processed just before pulling them up.
+ */
+ parse = root->parse = preprocess_relation_rtes(root);
+
+ /*
* If the FROM clause is empty, replace it with a dummy RTE_RESULT RTE, so
* that we don't need so many special cases to deal with that situation.
*/
@@ -744,14 +757,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
preprocess_function_rtes(root);
/*
- * Scan the rangetable for relations with virtual generated columns, and
- * replace all Var nodes in the query that reference these columns with
- * the generation expressions. Recursion issues here are handled in the
- * same way as for SubLinks.
- */
- parse = root->parse = expand_virtual_generated_columns(root);
-
- /*
* Check to see if any subqueries in the jointree can be merged into this
* query.
*/
@@ -787,23 +792,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
switch (rte->rtekind)
{
- case RTE_RELATION:
- if (rte->inh)
- {
- /*
- * Check to see if the relation actually has any children;
- * if not, clear the inh flag so we can treat it as a
- * plain base relation.
- *
- * Note: this could give a false-positive result, if the
- * rel once had children but no longer does. We used to
- * be able to clear rte->inh later on when we discovered
- * that, but no more; we have to handle such cases as
- * full-fledged inheritance.
- */
- rte->inh = has_subclass(rte->relid);
- }
- break;
case RTE_JOIN:
root->hasJoinRTEs = true;
if (IS_OUTER_JOIN(rte->jointype))
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index e7cb3fede66..d71ed958e31 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1454,6 +1454,7 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink,
Query *parse = root->parse;
Query *subselect = (Query *) sublink->subselect;
Node *whereClause;
+ PlannerInfo subroot;
int rtoffset;
int varno;
Relids clause_varnos;
@@ -1516,6 +1517,35 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink,
return NULL;
/*
+ * Scan the rangetable for relation RTEs and retrieve the necessary
+ * catalog information for each relation. Using this information, clear
+ * the inh flag for any relation that has no children, collect not-null
+ * attribute numbers for any relation that has column not-null
+ * constraints, and expand virtual generated columns for any relation that
+ * contains them.
+ *
+ * Note: we construct up an entirely dummy PlannerInfo for use here. This
+ * is fine because only the "glob" and "parse" links will be used in this
+ * case.
+ *
+ * Note: we temporarily assign back the WHERE clause so that any virtual
+ * generated column references within it can be expanded. It should be
+ * separated out again afterward.
+ */
+ MemSet(&subroot, 0, sizeof(subroot));
+ subroot.type = T_PlannerInfo;
+ subroot.glob = root->glob;
+ subroot.parse = subselect;
+ subselect->jointree->quals = whereClause;
+ subselect = preprocess_relation_rtes(&subroot);
+
+ /*
+ * Now separate out the WHERE clause again.
+ */
+ whereClause = subselect->jointree->quals;
+ subselect->jointree->quals = NULL;
+
+ /*
* The subquery must have a nonempty jointree, but we can make it so.
*/
replace_empty_jointree(subselect);
@@ -1732,6 +1762,7 @@ convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect,
Node **testexpr, List **paramIds)
{
Node *whereClause;
+ PlannerInfo subroot;
List *leftargs,
*rightargs,
*opids,
@@ -1791,12 +1822,15 @@ convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect,
* parent aliases were flattened already, and we're not going to pull any
* child Vars (of any description) into the parent.
*
- * Note: passing the parent's root to eval_const_expressions is
- * technically wrong, but we can get away with it since only the
- * boundParams (if any) are used, and those would be the same in a
- * subroot.
- */
- whereClause = eval_const_expressions(root, whereClause);
+ * Note: we construct up an entirely dummy PlannerInfo to pass to
+ * eval_const_expressions. This is fine because only the "glob" and
+ * "parse" links are used by eval_const_expressions.
+ */
+ MemSet(&subroot, 0, sizeof(subroot));
+ subroot.type = T_PlannerInfo;
+ subroot.glob = root->glob;
+ subroot.parse = subselect;
+ whereClause = eval_const_expressions(&subroot, whereClause);
whereClause = (Node *) canonicalize_qual((Expr *) whereClause, false);
whereClause = (Node *) make_ands_implicit((Expr *) whereClause);
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 87dc6f56b57..35e8d3c183b 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -4,10 +4,10 @@
* Planner preprocessing for subqueries and join tree manipulation.
*
* NOTE: the intended sequence for invoking these operations is
+ * preprocess_relation_rtes
* replace_empty_jointree
* pull_up_sublinks
* preprocess_function_rtes
- * expand_virtual_generated_columns
* pull_up_subqueries
* flatten_simple_union_all
* do expression preprocessing (including flattening JOIN alias vars)
@@ -36,6 +36,7 @@
#include "optimizer/clauses.h"
#include "optimizer/optimizer.h"
#include "optimizer/placeholder.h"
+#include "optimizer/plancat.h"
#include "optimizer/prep.h"
#include "optimizer/subselect.h"
#include "optimizer/tlist.h"
@@ -102,6 +103,9 @@ typedef struct reduce_outer_joins_partial_state
Relids unreduced_side; /* relids in its still-nullable side */
} reduce_outer_joins_partial_state;
+static Query *expand_virtual_generated_columns(PlannerInfo *root, Query *parse,
+ RangeTblEntry *rte, int rt_index,
+ Relation relation);
static Node *pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode,
Relids *relids);
static Node *pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node,
@@ -393,6 +397,181 @@ transform_MERGE_to_join(Query *parse)
}
/*
+ * preprocess_relation_rtes
+ * Do the preprocessing work for any relation RTEs in the FROM clause.
+ *
+ * This scans the rangetable for relation RTEs and retrieves the necessary
+ * catalog information for each relation. Using this information, it clears
+ * the inh flag for any relation that has no children, collects not-null
+ * attribute numbers for any relation that has column not-null constraints, and
+ * expands virtual generated columns for any relation that contains them.
+ *
+ * Note that expanding virtual generated columns may cause the query tree to
+ * have new copies of rangetable entries. Therefore, we have to use list_nth
+ * instead of foreach when iterating over the query's rangetable.
+ *
+ * Returns a modified copy of the query tree, if any relations with virtual
+ * generated columns are present.
+ */
+Query *
+preprocess_relation_rtes(PlannerInfo *root)
+{
+ Query *parse = root->parse;
+ int rtable_size;
+ int rt_index;
+
+ rtable_size = list_length(parse->rtable);
+
+ for (rt_index = 0; rt_index < rtable_size; rt_index++)
+ {
+ RangeTblEntry *rte = rt_fetch(rt_index + 1, parse->rtable);
+ Relation relation;
+
+ /* We only care about relation RTEs. */
+ if (rte->rtekind != RTE_RELATION)
+ continue;
+
+ /*
+ * We need not lock the relation since it was already locked by the
+ * rewriter.
+ */
+ relation = table_open(rte->relid, NoLock);
+
+ /*
+ * Check to see if the relation actually has any children; if not,
+ * clear the inh flag so we can treat it as a plain base relation.
+ *
+ * Note: this could give a false-positive result, if the rel once had
+ * children but no longer does. We used to be able to clear rte->inh
+ * later on when we discovered that, but no more; we have to handle
+ * such cases as full-fledged inheritance.
+ */
+ if (rte->inh)
+ rte->inh = relation->rd_rel->relhassubclass;
+
+ /*
+ * Check to see if the relation has any column not-null constraints;
+ * if so, retrieve the constraint information and store it in a
+ * relation OID based hash table.
+ */
+ get_relation_notnullatts(root, relation);
+
+ /*
+ * Check to see if the relation has any virtual generated columns; if
+ * so, replace all Var nodes in the query that reference these columns
+ * with the generation expressions.
+ */
+ parse = expand_virtual_generated_columns(root, parse,
+ rte, rt_index + 1,
+ relation);
+
+ table_close(relation, NoLock);
+ }
+
+ return parse;
+}
+
+/*
+ * expand_virtual_generated_columns
+ * Expand virtual generated columns for the given relation.
+ *
+ * This checks whether the given relation has any virtual generated columns,
+ * and if so, replaces all Var nodes in the query that reference those columns
+ * with their generation expressions.
+ *
+ * Returns a modified copy of the query tree if the relation contains virtual
+ * generated columns.
+ */
+static Query *
+expand_virtual_generated_columns(PlannerInfo *root, Query *parse,
+ RangeTblEntry *rte, int rt_index,
+ Relation relation)
+{
+ TupleDesc tupdesc;
+
+ /* Only normal relations can have virtual generated columns */
+ Assert(rte->rtekind == RTE_RELATION);
+
+ tupdesc = RelationGetDescr(relation);
+ if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+ {
+ List *tlist = NIL;
+ pullup_replace_vars_context rvcontext;
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+ TargetEntry *tle;
+
+ if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ {
+ Node *defexpr;
+
+ defexpr = build_generation_expression(relation, i + 1);
+ ChangeVarNodes(defexpr, 1, rt_index, 0);
+
+ tle = makeTargetEntry((Expr *) defexpr, i + 1, 0, false);
+ tlist = lappend(tlist, tle);
+ }
+ else
+ {
+ Var *var;
+
+ var = makeVar(rt_index,
+ i + 1,
+ attr->atttypid,
+ attr->atttypmod,
+ attr->attcollation,
+ 0);
+
+ tle = makeTargetEntry((Expr *) var, i + 1, 0, false);
+ tlist = lappend(tlist, tle);
+ }
+ }
+
+ Assert(list_length(tlist) > 0);
+ Assert(!rte->lateral);
+
+ /*
+ * The relation's targetlist items are now in the appropriate form to
+ * insert into the query, except that we may need to wrap them in
+ * PlaceHolderVars. Set up required context data for
+ * pullup_replace_vars.
+ */
+ rvcontext.root = root;
+ rvcontext.targetlist = tlist;
+ rvcontext.target_rte = rte;
+ rvcontext.result_relation = parse->resultRelation;
+ /* won't need these values */
+ rvcontext.relids = NULL;
+ rvcontext.nullinfo = NULL;
+ /* pass NULL for outer_hasSubLinks */
+ rvcontext.outer_hasSubLinks = NULL;
+ rvcontext.varno = rt_index;
+ /* this flag will be set below, if needed */
+ rvcontext.wrap_option = REPLACE_WRAP_NONE;
+ /* initialize cache array with indexes 0 .. length(tlist) */
+ rvcontext.rv_cache = palloc0((list_length(tlist) + 1) *
+ sizeof(Node *));
+
+ /*
+ * If the query uses grouping sets, we need a PlaceHolderVar for each
+ * expression of the relation's targetlist items. (See comments in
+ * pull_up_simple_subquery().)
+ */
+ if (parse->groupingSets)
+ rvcontext.wrap_option = REPLACE_WRAP_ALL;
+
+ /*
+ * Apply pullup variable replacement throughout the query tree.
+ */
+ parse = (Query *) pullup_replace_vars((Node *) parse, &rvcontext);
+ }
+
+ return parse;
+}
+
+/*
* replace_empty_jointree
* If the Query's jointree is empty, replace it with a dummy RTE_RESULT
* relation.
@@ -950,128 +1129,6 @@ preprocess_function_rtes(PlannerInfo *root)
}
/*
- * expand_virtual_generated_columns
- * Expand all virtual generated column references in a query.
- *
- * This scans the rangetable for relations with virtual generated columns, and
- * replaces all Var nodes in the query that reference these columns with the
- * generation expressions. Note that we do not descend into subqueries; that
- * is taken care of when the subqueries are planned.
- *
- * This has to be done after we have pulled up any SubLinks within the query's
- * quals; otherwise any virtual generated column references within the SubLinks
- * that should be transformed into joins wouldn't get expanded.
- *
- * Returns a modified copy of the query tree, if any relations with virtual
- * generated columns are present.
- */
-Query *
-expand_virtual_generated_columns(PlannerInfo *root)
-{
- Query *parse = root->parse;
- int rt_index;
- ListCell *lc;
-
- rt_index = 0;
- foreach(lc, parse->rtable)
- {
- RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
- Relation rel;
- TupleDesc tupdesc;
-
- ++rt_index;
-
- /*
- * Only normal relations can have virtual generated columns.
- */
- if (rte->rtekind != RTE_RELATION)
- continue;
-
- rel = table_open(rte->relid, NoLock);
-
- tupdesc = RelationGetDescr(rel);
- if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
- {
- List *tlist = NIL;
- pullup_replace_vars_context rvcontext;
-
- for (int i = 0; i < tupdesc->natts; i++)
- {
- Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
- TargetEntry *tle;
-
- if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
- {
- Node *defexpr;
-
- defexpr = build_generation_expression(rel, i + 1);
- ChangeVarNodes(defexpr, 1, rt_index, 0);
-
- tle = makeTargetEntry((Expr *) defexpr, i + 1, 0, false);
- tlist = lappend(tlist, tle);
- }
- else
- {
- Var *var;
-
- var = makeVar(rt_index,
- i + 1,
- attr->atttypid,
- attr->atttypmod,
- attr->attcollation,
- 0);
-
- tle = makeTargetEntry((Expr *) var, i + 1, 0, false);
- tlist = lappend(tlist, tle);
- }
- }
-
- Assert(list_length(tlist) > 0);
- Assert(!rte->lateral);
-
- /*
- * The relation's targetlist items are now in the appropriate form
- * to insert into the query, except that we may need to wrap them
- * in PlaceHolderVars. Set up required context data for
- * pullup_replace_vars.
- */
- rvcontext.root = root;
- rvcontext.targetlist = tlist;
- rvcontext.target_rte = rte;
- rvcontext.result_relation = parse->resultRelation;
- /* won't need these values */
- rvcontext.relids = NULL;
- rvcontext.nullinfo = NULL;
- /* pass NULL for outer_hasSubLinks */
- rvcontext.outer_hasSubLinks = NULL;
- rvcontext.varno = rt_index;
- /* this flag will be set below, if needed */
- rvcontext.wrap_option = REPLACE_WRAP_NONE;
- /* initialize cache array with indexes 0 .. length(tlist) */
- rvcontext.rv_cache = palloc0((list_length(tlist) + 1) *
- sizeof(Node *));
-
- /*
- * If the query uses grouping sets, we need a PlaceHolderVar for
- * each expression of the relation's targetlist items. (See
- * comments in pull_up_simple_subquery().)
- */
- if (parse->groupingSets)
- rvcontext.wrap_option = REPLACE_WRAP_ALL;
-
- /*
- * Apply pullup variable replacement throughout the query tree.
- */
- parse = (Query *) pullup_replace_vars((Node *) parse, &rvcontext);
- }
-
- table_close(rel, NoLock);
- }
-
- return parse;
-}
-
-/*
* pull_up_subqueries
* Look for subqueries in the rangetable that can be pulled up into
* the parent query. If the subquery has no special features like
@@ -1334,6 +1391,16 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
Assert(subquery->cteList == NIL);
/*
+ * Scan the rangetable for relation RTEs and retrieve the necessary
+ * catalog information for each relation. Using this information, clear
+ * the inh flag for any relation that has no children, collect not-null
+ * attribute numbers for any relation that has column not-null
+ * constraints, and expand virtual generated columns for any relation that
+ * contains them.
+ */
+ subquery = subroot->parse = preprocess_relation_rtes(subroot);
+
+ /*
* If the FROM clause is empty, replace it with a dummy RTE_RESULT RTE, so
* that we don't need so many special cases to deal with that situation.
*/
@@ -1353,13 +1420,6 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
preprocess_function_rtes(subroot);
/*
- * Scan the rangetable for relations with virtual generated columns, and
- * replace all Var nodes in the query that reference these columns with
- * the generation expressions.
- */
- subquery = subroot->parse = expand_virtual_generated_columns(subroot);
-
- /*
* Recursively pull up the subquery's subqueries, so that
* pull_up_subqueries' processing is complete for its jointree and
* rangetable.
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index f45131c34c5..6f0b338d2cd 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -20,6 +20,7 @@
#include "postgres.h"
#include "access/htup_details.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_language.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_proc.h"
@@ -36,6 +37,7 @@
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
+#include "optimizer/pathnode.h"
#include "optimizer/plancat.h"
#include "optimizer/planmain.h"
#include "parser/analyze.h"
@@ -43,6 +45,7 @@
#include "parser/parse_collate.h"
#include "parser/parse_func.h"
#include "parser/parse_oper.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "rewrite/rewriteManip.h"
#include "tcop/tcopprot.h"
@@ -2242,7 +2245,8 @@ rowtype_field_matches(Oid rowtypeid, int fieldnum,
* only operators and functions that are reasonable to try to execute.
*
* NOTE: "root" can be passed as NULL if the caller never wants to do any
- * Param substitutions nor receive info about inlined functions.
+ * Param substitutions nor receive info about inlined functions nor reduce
+ * NullTest for Vars to constant true or constant false.
*
* NOTE: the planner assumes that this will always flatten nested AND and
* OR clauses into N-argument form. See comments in prepqual.c.
@@ -3544,6 +3548,31 @@ eval_const_expressions_mutator(Node *node,
return makeBoolConst(result, false);
}
+ if (!ntest->argisrow && arg && IsA(arg, Var) && context->root)
+ {
+ Var *varg = (Var *) arg;
+ bool result;
+
+ if (var_is_nonnullable(context->root, varg, false))
+ {
+ switch (ntest->nulltesttype)
+ {
+ case IS_NULL:
+ result = false;
+ break;
+ case IS_NOT_NULL:
+ result = true;
+ break;
+ default:
+ elog(ERROR, "unrecognized nulltesttype: %d",
+ (int) ntest->nulltesttype);
+ result = false; /* keep compiler quiet */
+ break;
+ }
+
+ return makeBoolConst(result, false);
+ }
+ }
newntest = makeNode(NullTest);
newntest->arg = (Expr *) arg;
@@ -4163,6 +4192,67 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
}
/*
+ * var_is_nonnullable: check to see if the Var cannot be NULL
+ *
+ * If the Var is defined NOT NULL and meanwhile is not nulled by any outer
+ * joins or grouping sets, then we can know that it cannot be NULL.
+ *
+ * use_rel_info indicates whether the corresponding RelOptInfo is available for
+ * use.
+ */
+bool
+var_is_nonnullable(PlannerInfo *root, Var *var, bool use_rel_info)
+{
+ Relids notnullattnums = NULL;
+
+ Assert(IsA(var, Var));
+
+ /* skip upper-level Vars */
+ if (var->varlevelsup != 0)
+ return false;
+
+ /* could the Var be nulled by any outer joins or grouping sets? */
+ if (!bms_is_empty(var->varnullingrels))
+ return false;
+
+ /* system columns cannot be NULL */
+ if (var->varattno < 0)
+ return true;
+
+ /*
+ * Check if the Var is defined as NOT NULL. We retrieve the column NOT
+ * NULL constraint information from the corresponding RelOptInfo if it is
+ * available; otherwise, we search the hash table for this information.
+ */
+ if (use_rel_info)
+ {
+ RelOptInfo *rel = find_base_rel(root, var->varno);
+
+ notnullattnums = rel->notnullattnums;
+ }
+ else
+ {
+ RangeTblEntry *rte = planner_rt_fetch(var->varno, root);
+
+ /*
+ * We must skip inheritance parent tables, as some child tables may
+ * have a NOT NULL constraint for a column while others may not. This
+ * cannot happen with partitioned tables, though.
+ */
+ if (rte->inh && rte->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ notnullattnums = find_relation_notnullatts(root, rte->relid);
+ }
+
+ if (var->varattno > 0 &&
+ bms_is_member(var->varattno, notnullattnums))
+ return true;
+
+ return false;
+}
+
+/*
* expand_function_arguments: convert named-notation args to positional args
* and/or insert default args, as needed
*
diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c
index 17e51cd75d7..30d158069e3 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -466,8 +466,7 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte,
Index *childRTindex_p)
{
Query *parse = root->parse;
- Oid parentOID PG_USED_FOR_ASSERTS_ONLY =
- RelationGetRelid(parentrel);
+ Oid parentOID = RelationGetRelid(parentrel);
Oid childOID = RelationGetRelid(childrel);
RangeTblEntry *childrte;
Index childRTindex;
@@ -514,6 +513,13 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte,
*childRTindex_p = childRTindex;
/*
+ * Retrieve column not-null constraint information for the child relation
+ * if its relation OID is different from the parent's.
+ */
+ if (childOID != parentOID)
+ get_relation_notnullatts(root, childrel);
+
+ /*
* Build an AppendRelInfo struct for each parent/child pair.
*/
appinfo = make_append_rel_info(parentrel, childrel,
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 59233b64730..c6a58afc5e5 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -59,6 +59,12 @@ int constraint_exclusion = CONSTRAINT_EXCLUSION_PARTITION;
/* Hook for plugins to get control in get_relation_info() */
get_relation_info_hook_type get_relation_info_hook = NULL;
+typedef struct NotnullHashEntry
+{
+ Oid relid; /* OID of the relation */
+ Relids notnullattnums; /* attnums of NOT NULL columns */
+} NotnullHashEntry;
+
static void get_relation_foreign_keys(PlannerInfo *root, RelOptInfo *rel,
Relation relation, bool inhparent);
@@ -172,27 +178,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
* RangeTblEntry does get populated.
*/
if (!inhparent || relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- {
- for (int i = 0; i < relation->rd_att->natts; i++)
- {
- CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
-
- Assert(attr->attnullability != ATTNULLABLE_UNKNOWN);
-
- if (attr->attnullability == ATTNULLABLE_VALID)
- {
- rel->notnullattnums = bms_add_member(rel->notnullattnums,
- i + 1);
-
- /*
- * Per RemoveAttributeById(), dropped columns will have their
- * attnotnull unset, so we needn't check for dropped columns
- * in the above condition.
- */
- Assert(!attr->attisdropped);
- }
- }
- }
+ rel->notnullattnums = find_relation_notnullatts(root, relationObjectId);
/*
* Estimate relation size --- unless it's an inheritance parent, in which
@@ -684,6 +670,105 @@ get_relation_foreign_keys(PlannerInfo *root, RelOptInfo *rel,
}
/*
+ * get_relation_notnullatts -
+ * Retrieves column not-null constraint information for a given relation.
+ *
+ * We do this while we have the relcache entry open, and store the column
+ * not-null constraint information in a hash table based on the relation OID.
+ */
+void
+get_relation_notnullatts(PlannerInfo *root, Relation relation)
+{
+ Oid relid = RelationGetRelid(relation);
+ NotnullHashEntry *hentry;
+ bool found;
+ Relids notnullattnums = NULL;
+
+ /* bail out if the relation has no not-null constraints */
+ if (relation->rd_att->constr == NULL ||
+ !relation->rd_att->constr->has_not_null)
+ return;
+
+ /* create the hash table if it hasn't been created yet */
+ if (root->glob->rel_notnullatts_hash == NULL)
+ {
+ HTAB *hashtab;
+ HASHCTL hash_ctl;
+
+ hash_ctl.keysize = sizeof(Oid);
+ hash_ctl.entrysize = sizeof(NotnullHashEntry);
+ hash_ctl.hcxt = CurrentMemoryContext;
+
+ hashtab = hash_create("Relation NOT NULL attnums",
+ 64L, /* arbitrary initial size */
+ &hash_ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ root->glob->rel_notnullatts_hash = hashtab;
+ }
+
+ /*
+ * Create a hash entry for this relation OID, if we don't have one
+ * already.
+ */
+ hentry = (NotnullHashEntry *) hash_search(root->glob->rel_notnullatts_hash,
+ &relid,
+ HASH_ENTER,
+ &found);
+
+ /* bail out if a hash entry already exists for this relation OID */
+ if (found)
+ return;
+
+ /* collect the column not-null constraint information for this relation */
+ for (int i = 0; i < relation->rd_att->natts; i++)
+ {
+ CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
+
+ Assert(attr->attnullability != ATTNULLABLE_UNKNOWN);
+
+ if (attr->attnullability == ATTNULLABLE_VALID)
+ {
+ notnullattnums = bms_add_member(notnullattnums, i + 1);
+
+ /*
+ * Per RemoveAttributeById(), dropped columns will have their
+ * attnotnull unset, so we needn't check for dropped columns in
+ * the above condition.
+ */
+ Assert(!attr->attisdropped);
+ }
+ }
+
+ /* ... and initialize the new hash entry */
+ hentry->notnullattnums = notnullattnums;
+}
+
+/*
+ * find_relation_notnullatts -
+ * Searches the hash table and returns the column not-null constraint
+ * information for a given relation.
+ */
+Relids
+find_relation_notnullatts(PlannerInfo *root, Oid relid)
+{
+ NotnullHashEntry *hentry;
+ bool found;
+
+ if (root->glob->rel_notnullatts_hash == NULL)
+ return NULL;
+
+ hentry = (NotnullHashEntry *) hash_search(root->glob->rel_notnullatts_hash,
+ &relid,
+ HASH_FIND,
+ &found);
+ if (!found)
+ return NULL;
+
+ return hentry->notnullattnums;
+}
+
+/*
* infer_arbiter_indexes -
* Determine the unique indexes used to arbitrate speculative insertion.
*
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index f7b5d093681..886d99951dd 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -232,6 +232,9 @@ libpqrcv_connect(const char *conninfo, bool replication, bool logical,
errhint("Target server's authentication method must be changed, or set password_required=false in the subscription parameters.")));
}
+ PQsetNoticeReceiver(conn->streamConn, libpqsrv_notice_receiver,
+ "received message via replication");
+
/*
* Set always-secure search path for the cases where the connection is
* used to run SQL queries, so malicious users can't get control.
diff --git a/src/backend/storage/aio/README.md b/src/backend/storage/aio/README.md
index f10b5c7e31e..72ae3b3737d 100644
--- a/src/backend/storage/aio/README.md
+++ b/src/backend/storage/aio/README.md
@@ -94,7 +94,7 @@ pgaio_io_register_callbacks(ioh, PGAIO_HCB_SHARED_BUFFER_READV, 0);
*
* In this example we're reading only a single buffer, hence the 1.
*/
-pgaio_io_set_handle_data_32(ioh, (uint32 *) buffer, 1);
+pgaio_io_set_handle_data_32(ioh, (uint32 *) &buffer, 1);
/*
* Pass the AIO handle to lower-level function. When operating on the level of
@@ -119,8 +119,9 @@ pgaio_io_set_handle_data_32(ioh, (uint32 *) buffer, 1);
* e.g. due to reaching a limit on the number of unsubmitted IOs, and even
* complete before smgrstartreadv() returns.
*/
+void *page = BufferGetBlock(buffer);
smgrstartreadv(ioh, operation->smgr, forknum, blkno,
- BufferGetBlock(buffer), 1);
+ &page, 1);
/*
* To benefit from AIO, it is beneficial to perform other work, including
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index ce6a626eba2..17fbfa9b410 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -3798,18 +3798,25 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner,
List *hashclauses,
Selectivity *innerbucketsize)
{
- List *clauses = list_copy(hashclauses);
- List *otherclauses = NIL;
- double ndistinct = 1.0;
+ List *clauses;
+ List *otherclauses;
+ double ndistinct;
if (list_length(hashclauses) <= 1)
-
+ {
/*
* Nothing to do for a single clause. Could we employ univariate
* extended stat here?
*/
return hashclauses;
+ }
+ /* "clauses" is the list of hashclauses we've not dealt with yet */
+ clauses = list_copy(hashclauses);
+ /* "otherclauses" holds clauses we are going to return to caller */
+ otherclauses = NIL;
+ /* current estimate of ndistinct */
+ ndistinct = 1.0;
while (clauses != NIL)
{
ListCell *lc;
@@ -3874,12 +3881,13 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner,
group_rel = root->simple_rel_array[relid];
}
else if (group_relid != relid)
-
+ {
/*
* Being in the group forming state we don't need other
* clauses.
*/
continue;
+ }
/*
* We're going to add the new clause to the varinfos list. We
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 604fc109416..ede10e5291e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -47,6 +47,7 @@
#include "catalog/pg_authid_d.h"
#include "catalog/pg_cast_d.h"
#include "catalog/pg_class_d.h"
+#include "catalog/pg_constraint_d.h"
#include "catalog/pg_default_acl_d.h"
#include "catalog/pg_largeobject_d.h"
#include "catalog/pg_largeobject_metadata_d.h"
@@ -6187,6 +6188,7 @@ getTypes(Archive *fout)
*/
tyinfo[i].nDomChecks = 0;
tyinfo[i].domChecks = NULL;
+ tyinfo[i].notnull = NULL;
if ((tyinfo[i].dobj.dump & DUMP_COMPONENT_DEFINITION) &&
tyinfo[i].typtype == TYPTYPE_DOMAIN)
getDomainConstraints(fout, &(tyinfo[i]));
@@ -8312,27 +8314,33 @@ addConstrChildIdxDeps(DumpableObject *dobj, const IndxInfo *refidx)
static void
getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
{
- int i;
ConstraintInfo *constrinfo;
PQExpBuffer query = createPQExpBuffer();
PGresult *res;
int i_tableoid,
i_oid,
i_conname,
- i_consrc;
+ i_consrc,
+ i_convalidated,
+ i_contype;
int ntups;
if (!fout->is_prepared[PREPQUERY_GETDOMAINCONSTRAINTS])
{
- /* Set up query for constraint-specific details */
- appendPQExpBufferStr(query,
- "PREPARE getDomainConstraints(pg_catalog.oid) AS\n"
- "SELECT tableoid, oid, conname, "
- "pg_catalog.pg_get_constraintdef(oid) AS consrc, "
- "convalidated "
- "FROM pg_catalog.pg_constraint "
- "WHERE contypid = $1 AND contype = 'c' "
- "ORDER BY conname");
+ /*
+ * Set up query for constraint-specific details. For servers 17 and
+ * up, domains have constraints of type 'n' as well as 'c', otherwise
+ * just the latter.
+ */
+ appendPQExpBuffer(query,
+ "PREPARE getDomainConstraints(pg_catalog.oid) AS\n"
+ "SELECT tableoid, oid, conname, "
+ "pg_catalog.pg_get_constraintdef(oid) AS consrc, "
+ "convalidated, contype "
+ "FROM pg_catalog.pg_constraint "
+ "WHERE contypid = $1 AND contype IN (%s) "
+ "ORDER BY conname",
+ fout->remoteVersion < 170000 ? "'c'" : "'c', 'n'");
ExecuteSqlStatement(fout, query->data);
@@ -8351,33 +8359,50 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
i_oid = PQfnumber(res, "oid");
i_conname = PQfnumber(res, "conname");
i_consrc = PQfnumber(res, "consrc");
+ i_convalidated = PQfnumber(res, "convalidated");
+ i_contype = PQfnumber(res, "contype");
constrinfo = (ConstraintInfo *) pg_malloc(ntups * sizeof(ConstraintInfo));
-
- tyinfo->nDomChecks = ntups;
tyinfo->domChecks = constrinfo;
- for (i = 0; i < ntups; i++)
+ /* 'i' tracks result rows; 'j' counts CHECK constraints */
+ for (int i = 0, j = 0; i < ntups; i++)
{
- bool validated = PQgetvalue(res, i, 4)[0] == 't';
-
- constrinfo[i].dobj.objType = DO_CONSTRAINT;
- constrinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
- constrinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
- AssignDumpId(&constrinfo[i].dobj);
- constrinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_conname));
- constrinfo[i].dobj.namespace = tyinfo->dobj.namespace;
- constrinfo[i].contable = NULL;
- constrinfo[i].condomain = tyinfo;
- constrinfo[i].contype = 'c';
- constrinfo[i].condef = pg_strdup(PQgetvalue(res, i, i_consrc));
- constrinfo[i].confrelid = InvalidOid;
- constrinfo[i].conindex = 0;
- constrinfo[i].condeferrable = false;
- constrinfo[i].condeferred = false;
- constrinfo[i].conislocal = true;
-
- constrinfo[i].separate = !validated;
+ bool validated = PQgetvalue(res, i, i_convalidated)[0] == 't';
+ char contype = (PQgetvalue(res, i, i_contype))[0];
+ ConstraintInfo *constraint;
+
+ if (contype == CONSTRAINT_CHECK)
+ {
+ constraint = &constrinfo[j++];
+ tyinfo->nDomChecks++;
+ }
+ else
+ {
+ Assert(contype == CONSTRAINT_NOTNULL);
+ Assert(tyinfo->notnull == NULL);
+ /* use last item in array for the not-null constraint */
+ tyinfo->notnull = &(constrinfo[ntups - 1]);
+ constraint = tyinfo->notnull;
+ }
+
+ constraint->dobj.objType = DO_CONSTRAINT;
+ constraint->dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+ constraint->dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+ AssignDumpId(&(constraint->dobj));
+ constraint->dobj.name = pg_strdup(PQgetvalue(res, i, i_conname));
+ constraint->dobj.namespace = tyinfo->dobj.namespace;
+ constraint->contable = NULL;
+ constraint->condomain = tyinfo;
+ constraint->contype = contype;
+ constraint->condef = pg_strdup(PQgetvalue(res, i, i_consrc));
+ constraint->confrelid = InvalidOid;
+ constraint->conindex = 0;
+ constraint->condeferrable = false;
+ constraint->condeferred = false;
+ constraint->conislocal = true;
+
+ constraint->separate = !validated;
/*
* Make the domain depend on the constraint, ensuring it won't be
@@ -8386,8 +8411,7 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
* anyway, so this doesn't matter.
*/
if (validated)
- addObjectDependency(&tyinfo->dobj,
- constrinfo[i].dobj.dumpId);
+ addObjectDependency(&tyinfo->dobj, constraint->dobj.dumpId);
}
PQclear(res);
@@ -12597,8 +12621,36 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo)
appendPQExpBuffer(q, " COLLATE %s", fmtQualifiedDumpable(coll));
}
+ /*
+ * Print a not-null constraint if there's one. In servers older than 17
+ * these don't have names, so just print it unadorned; in newer ones they
+ * do, but most of the time it's going to be the standard generated one,
+ * so omit the name in that case also.
+ */
if (typnotnull[0] == 't')
- appendPQExpBufferStr(q, " NOT NULL");
+ {
+ if (fout->remoteVersion < 170000 || tyinfo->notnull == NULL)
+ appendPQExpBufferStr(q, " NOT NULL");
+ else
+ {
+ ConstraintInfo *notnull = tyinfo->notnull;
+
+ if (!notnull->separate)
+ {
+ char *default_name;
+
+ /* XXX should match ChooseConstraintName better */
+ default_name = psprintf("%s_not_null", tyinfo->dobj.name);
+
+ if (strcmp(default_name, notnull->dobj.name) == 0)
+ appendPQExpBufferStr(q, " NOT NULL");
+ else
+ appendPQExpBuffer(q, " CONSTRAINT %s %s",
+ fmtId(notnull->dobj.name), notnull->condef);
+ free(default_name);
+ }
+ }
+ }
if (typdefault != NULL)
{
@@ -12618,7 +12670,7 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo)
{
ConstraintInfo *domcheck = &(tyinfo->domChecks[i]);
- if (!domcheck->separate)
+ if (!domcheck->separate && domcheck->contype == 'c')
appendPQExpBuffer(q, "\n\tCONSTRAINT %s %s",
fmtId(domcheck->dobj.name), domcheck->condef);
}
@@ -12682,6 +12734,25 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo)
destroyPQExpBuffer(conprefix);
}
+ /*
+ * And a comment on the not-null constraint, if there's one -- but only if
+ * the constraint itself was dumped here
+ */
+ if (tyinfo->notnull != NULL && !tyinfo->notnull->separate)
+ {
+ PQExpBuffer conprefix = createPQExpBuffer();
+
+ appendPQExpBuffer(conprefix, "CONSTRAINT %s ON DOMAIN",
+ fmtId(tyinfo->notnull->dobj.name));
+
+ if (tyinfo->notnull->dobj.dump & DUMP_COMPONENT_COMMENT)
+ dumpComment(fout, conprefix->data, qtypname,
+ tyinfo->dobj.namespace->dobj.name,
+ tyinfo->rolname,
+ tyinfo->notnull->dobj.catId, 0, tyinfo->dobj.dumpId);
+ destroyPQExpBuffer(conprefix);
+ }
+
destroyPQExpBuffer(q);
destroyPQExpBuffer(delq);
destroyPQExpBuffer(query);
@@ -18543,14 +18614,23 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
.dropStmt = delq->data));
}
}
- else if (coninfo->contype == 'c' && tbinfo == NULL)
+ else if (tbinfo == NULL)
{
- /* CHECK constraint on a domain */
+ /* CHECK, NOT NULL constraint on a domain */
TypeInfo *tyinfo = coninfo->condomain;
+ Assert(coninfo->contype == 'c' || coninfo->contype == 'n');
+
/* Ignore if not to be dumped separately */
if (coninfo->separate)
{
+ const char *keyword;
+
+ if (coninfo->contype == 'c')
+ keyword = "CHECK CONSTRAINT";
+ else
+ keyword = "CONSTRAINT";
+
appendPQExpBuffer(q, "ALTER DOMAIN %s\n",
fmtQualifiedDumpable(tyinfo));
appendPQExpBuffer(q, " ADD CONSTRAINT %s %s;\n",
@@ -18569,7 +18649,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
ARCHIVE_OPTS(.tag = tag,
.namespace = tyinfo->dobj.namespace->dobj.name,
.owner = tyinfo->rolname,
- .description = "CHECK CONSTRAINT",
+ .description = keyword,
.section = SECTION_POST_DATA,
.createStmt = q->data,
.dropStmt = delq->data));
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 39eef1d6617..2370c98d192 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -222,7 +222,9 @@ typedef struct _typeInfo
bool isDefined; /* true if typisdefined */
/* If needed, we'll create a "shell type" entry for it; link that here: */
struct _shellTypeInfo *shellType; /* shell-type entry, or NULL */
- /* If it's a domain, we store links to its constraints here: */
+ /* If it's a domain, its not-null constraint is here: */
+ struct _constraintInfo *notnull;
+ /* If it's a domain, we store links to its CHECK constraints here: */
int nDomChecks;
struct _constraintInfo *domChecks;
} TypeInfo;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 538e7dcb493..f99a0797ea7 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -907,7 +907,7 @@ repairTableAttrDefMultiLoop(DumpableObject *tableobj,
}
/*
- * CHECK constraints on domains work just like those on tables ...
+ * CHECK, NOT NULL constraints on domains work just like those on tables ...
*/
static void
repairDomainConstraintLoop(DumpableObject *domainobj,
@@ -1173,11 +1173,12 @@ repairDependencyLoop(DumpableObject **loop,
}
}
- /* Domain and CHECK constraint */
+ /* Domain and CHECK or NOT NULL constraint */
if (nLoop == 2 &&
loop[0]->objType == DO_TYPE &&
loop[1]->objType == DO_CONSTRAINT &&
- ((ConstraintInfo *) loop[1])->contype == 'c' &&
+ (((ConstraintInfo *) loop[1])->contype == 'c' ||
+ ((ConstraintInfo *) loop[1])->contype == 'n') &&
((ConstraintInfo *) loop[1])->condomain == (TypeInfo *) loop[0])
{
repairDomainConstraintLoop(loop[0], loop[1]);
@@ -1186,14 +1187,15 @@ repairDependencyLoop(DumpableObject **loop,
if (nLoop == 2 &&
loop[1]->objType == DO_TYPE &&
loop[0]->objType == DO_CONSTRAINT &&
- ((ConstraintInfo *) loop[0])->contype == 'c' &&
+ (((ConstraintInfo *) loop[0])->contype == 'c' ||
+ ((ConstraintInfo *) loop[0])->contype == 'n') &&
((ConstraintInfo *) loop[0])->condomain == (TypeInfo *) loop[1])
{
repairDomainConstraintLoop(loop[1], loop[0]);
return;
}
- /* Indirect loop involving domain and CHECK constraint */
+ /* Indirect loop involving domain and CHECK or NOT NULL constraint */
if (nLoop > 2)
{
for (i = 0; i < nLoop; i++)
@@ -1203,7 +1205,8 @@ repairDependencyLoop(DumpableObject **loop,
for (j = 0; j < nLoop; j++)
{
if (loop[j]->objType == DO_CONSTRAINT &&
- ((ConstraintInfo *) loop[j])->contype == 'c' &&
+ (((ConstraintInfo *) loop[j])->contype == 'c' ||
+ ((ConstraintInfo *) loop[j])->contype == 'n') &&
((ConstraintInfo *) loop[j])->condomain == (TypeInfo *) loop[i])
{
repairDomainConstraintMultiLoop(loop[i], loop[j]);
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d8330e2bd17..6c7ec80e271 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2379,17 +2379,19 @@ my %tests = (
create_sql => 'CREATE DOMAIN dump_test.us_postal_code AS TEXT
COLLATE "C"
DEFAULT \'10014\'
+ CONSTRAINT nn NOT NULL
CHECK(VALUE ~ \'^\d{5}$\' OR
VALUE ~ \'^\d{5}-\d{4}$\');
+ COMMENT ON CONSTRAINT nn
+ ON DOMAIN dump_test.us_postal_code IS \'not null\';
COMMENT ON CONSTRAINT us_postal_code_check
ON DOMAIN dump_test.us_postal_code IS \'check it\';',
regexp => qr/^
- \QCREATE DOMAIN dump_test.us_postal_code AS text COLLATE pg_catalog."C" DEFAULT '10014'::text\E\n\s+
+ \QCREATE DOMAIN dump_test.us_postal_code AS text COLLATE pg_catalog."C" CONSTRAINT nn NOT NULL DEFAULT '10014'::text\E\n\s+
\QCONSTRAINT us_postal_code_check CHECK \E
\Q(((VALUE ~ '^\d{5}\E
\$\Q'::text) OR (VALUE ~ '^\d{5}-\d{4}\E\$
\Q'::text)));\E(.|\n)*
- \QCOMMENT ON CONSTRAINT us_postal_code_check ON DOMAIN dump_test.us_postal_code IS 'check it';\E
/xm,
like =>
{ %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
@@ -2399,6 +2401,30 @@ my %tests = (
},
},
+ 'COMMENT ON CONSTRAINT ON DOMAIN (1)' => {
+ regexp => qr/^
+ \QCOMMENT ON CONSTRAINT nn ON DOMAIN dump_test.us_postal_code IS 'not null';\E
+ /xm,
+ like =>
+ { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+ unlike => {
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
+ 'COMMENT ON CONSTRAINT ON DOMAIN (2)' => {
+ regexp => qr/^
+ \QCOMMENT ON CONSTRAINT us_postal_code_check ON DOMAIN dump_test.us_postal_code IS 'check it';\E
+ /xm,
+ like =>
+ { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+ unlike => {
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
'CREATE FUNCTION dump_test.pltestlang_call_handler' => {
create_order => 17,
create_sql => 'CREATE FUNCTION dump_test.pltestlang_call_handler()
diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h
index 52916bab7a3..70949de56ac 100644
--- a/src/include/access/amapi.h
+++ b/src/include/access/amapi.h
@@ -293,7 +293,7 @@ typedef struct IndexAmRoutine
ambuild_function ambuild;
ambuildempty_function ambuildempty;
aminsert_function aminsert;
- aminsertcleanup_function aminsertcleanup;
+ aminsertcleanup_function aminsertcleanup; /* can be NULL */
ambulkdelete_function ambulkdelete;
amvacuumcleanup_function amvacuumcleanup;
amcanreturn_function amcanreturn; /* can be NULL */
diff --git a/src/include/libpq/libpq-be-fe-helpers.h b/src/include/libpq/libpq-be-fe-helpers.h
index 16205b824fa..af13bd6bf3d 100644
--- a/src/include/libpq/libpq-be-fe-helpers.h
+++ b/src/include/libpq/libpq-be-fe-helpers.h
@@ -454,4 +454,34 @@ exit: ;
return error;
}
+/*
+ * libpqsrv_notice_receiver
+ *
+ * Custom notice receiver for libpq connections.
+ *
+ * This function is intended to be set via PQsetNoticeReceiver() so that
+ * NOTICE, WARNING, and similar messages from the connection are reported via
+ * ereport(), instead of being printed to stderr.
+ */
+static inline void
+libpqsrv_notice_receiver(void *arg, const PGresult *res)
+{
+ char *message;
+ int len;
+ char *prefix = (char *) arg;
+
+ /*
+ * Trim the trailing newline from the message text returned from
+ * PQresultErrorMessage(), as it always includes one, to produce cleaner
+ * log output.
+ */
+ message = PQresultErrorMessage(res);
+ len = strlen(message);
+ if (len > 0 && message[len - 1] == '\n')
+ len--;
+
+ ereport(LOG,
+ errmsg_internal("%s: %.*s", prefix, len, message));
+}
+
#endif /* LIBPQ_BE_FE_HELPERS_H */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 6567759595d..e5dd15098f6 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -179,6 +179,9 @@ typedef struct PlannerGlobal
/* partition descriptors */
PartitionDirectory partition_directory pg_node_attr(read_write_ignore);
+
+ /* hash table for NOT NULL attnums of relations */
+ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore);
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -719,6 +722,9 @@ typedef struct PartitionSchemeData *PartitionScheme;
* the attribute is needed as part of final targetlist
* attr_widths - cache space for per-attribute width estimates;
* zero means not computed yet
+ * notnullattnums - zero-based set containing attnums of NOT NULL
+ * columns (not populated for rels corresponding to
+ * non-partitioned inh==true RTEs)
* nulling_relids - relids of outer joins that can null this rel
* lateral_vars - lateral cross-references of rel, if any (list of
* Vars and PlaceHolderVars)
@@ -952,11 +958,7 @@ typedef struct RelOptInfo
Relids *attr_needed pg_node_attr(read_write_ignore);
/* array indexed [min_attr .. max_attr] */
int32 *attr_widths pg_node_attr(read_write_ignore);
-
- /*
- * Zero-based set containing attnums of NOT NULL columns. Not populated
- * for rels corresponding to non-partitioned inh==true RTEs.
- */
+ /* zero-based set containing attnums of NOT NULL columns */
Bitmapset *notnullattnums;
/* relids of outer joins that can null this baserel */
Relids nulling_relids;
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 546828b54bd..37bc13c2cbd 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -154,6 +154,8 @@ extern Node *estimate_expression_value(PlannerInfo *root, Node *node);
extern Expr *evaluate_expr(Expr *expr, Oid result_type, int32 result_typmod,
Oid result_collation);
+extern bool var_is_nonnullable(PlannerInfo *root, Var *var, bool use_rel_info);
+
extern List *expand_function_arguments(List *args, bool include_out_arguments,
Oid result_type,
struct HeapTupleData *func_tuple);
diff --git a/src/include/optimizer/plancat.h b/src/include/optimizer/plancat.h
index cd74e4b1e8b..d6f6f4ad2d7 100644
--- a/src/include/optimizer/plancat.h
+++ b/src/include/optimizer/plancat.h
@@ -28,6 +28,10 @@ extern PGDLLIMPORT get_relation_info_hook_type get_relation_info_hook;
extern void get_relation_info(PlannerInfo *root, Oid relationObjectId,
bool inhparent, RelOptInfo *rel);
+extern void get_relation_notnullatts(PlannerInfo *root, Relation relation);
+
+extern Relids find_relation_notnullatts(PlannerInfo *root, Oid relid);
+
extern List *infer_arbiter_indexes(PlannerInfo *root);
extern void estimate_rel_size(Relation rel, int32 *attr_widths,
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
index df56202777c..4fbecdb4462 100644
--- a/src/include/optimizer/prep.h
+++ b/src/include/optimizer/prep.h
@@ -22,10 +22,10 @@
* prototypes for prepjointree.c
*/
extern void transform_MERGE_to_join(Query *parse);
+extern Query *preprocess_relation_rtes(PlannerInfo *root);
extern void replace_empty_jointree(Query *parse);
extern void pull_up_sublinks(PlannerInfo *root);
extern void preprocess_function_rtes(PlannerInfo *root);
-extern Query *expand_virtual_generated_columns(PlannerInfo *root);
extern void pull_up_subqueries(PlannerInfo *root);
extern void flatten_simple_union_all(PlannerInfo *root);
extern void reduce_outer_joins(PlannerInfo *root);
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 76aeeb92242..19b4e8b6a03 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -220,6 +220,25 @@ typedef struct ReplicationSlot
* Latest restart_lsn that has been flushed to disk. For persistent slots
* the flushed LSN should be taken into account when calculating the
* oldest LSN for WAL segments removal.
+ *
+ * Do not assume that restart_lsn will always move forward, i.e., that the
+ * previously flushed restart_lsn is always behind data.restart_lsn. In
+ * streaming replication using a physical slot, the restart_lsn is updated
+ * based on the flushed WAL position reported by the walreceiver.
+ *
+ * This replication mode allows duplicate WAL records to be received and
+ * overwritten. If the walreceiver receives older WAL records and then
+ * reports them as flushed to the walsender, the restart_lsn may appear to
+ * move backward.
+ *
+ * This typically occurs at the beginning of replication. One reason is
+ * that streaming replication starts at the beginning of a segment, so, if
+ * restart_lsn is in the middle of a segment, it will be updated to an
+ * earlier LSN, see RequestXLogStreaming. Another reason is that the
+ * walreceiver chooses its startpoint based on the replayed LSN, so, if
+ * some records have been received but not yet applied, they will be
+ * received again and leads to updating the restart_lsn to an earlier
+ * position.
*/
XLogRecPtr last_saved_restart_lsn;
diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h
index e7a0a234b6c..2933eea0649 100644
--- a/src/include/storage/aio.h
+++ b/src/include/storage/aio.h
@@ -201,7 +201,7 @@ typedef enum PgAioHandleCallbackID
} PgAioHandleCallbackID;
#define PGAIO_HCB_MAX PGAIO_HCB_LOCAL_BUFFER_READV
-StaticAssertDecl(PGAIO_HCB_MAX <= (1 << PGAIO_RESULT_ID_BITS),
+StaticAssertDecl(PGAIO_HCB_MAX < (1 << PGAIO_RESULT_ID_BITS),
"PGAIO_HCB_MAX is too big for PGAIO_RESULT_ID_BITS");
diff --git a/src/interfaces/ecpg/ecpglib/connect.c b/src/interfaces/ecpg/ecpglib/connect.c
index 2bbb70333dc..78de9f298ba 100644
--- a/src/interfaces/ecpg/ecpglib/connect.c
+++ b/src/interfaces/ecpg/ecpglib/connect.c
@@ -58,7 +58,12 @@ ecpg_get_connection_nr(const char *connection_name)
for (con = all_connections; con != NULL; con = con->next)
{
- if (strcmp(connection_name, con->name) == 0)
+ /*
+ * Check for the case of a NULL connection name, stored as such in
+ * the connection information by ECPGconnect() when the database
+ * name is not specified by its caller.
+ */
+ if (con->name != NULL && strcmp(connection_name, con->name) == 0)
break;
}
ret = con;
@@ -259,7 +264,8 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
struct connection *this;
int i,
connect_params = 0;
- char *dbname = name ? ecpg_strdup(name, lineno) : NULL,
+ bool alloc_failed = (sqlca == NULL);
+ char *dbname = name ? ecpg_strdup(name, lineno, &alloc_failed) : NULL,
*host = NULL,
*tmp,
*port = NULL,
@@ -268,11 +274,12 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
const char **conn_keywords;
const char **conn_values;
- if (sqlca == NULL)
+ if (alloc_failed)
{
ecpg_raise(lineno, ECPG_OUT_OF_MEMORY,
ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL);
- ecpg_free(dbname);
+ if (dbname)
+ ecpg_free(dbname);
return false;
}
@@ -297,7 +304,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
if (envname)
{
ecpg_free(dbname);
- dbname = ecpg_strdup(envname, lineno);
+ dbname = ecpg_strdup(envname, lineno, &alloc_failed);
}
}
@@ -349,7 +356,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
tmp = strrchr(dbname + offset, '?');
if (tmp != NULL) /* options given */
{
- options = ecpg_strdup(tmp + 1, lineno);
+ options = ecpg_strdup(tmp + 1, lineno, &alloc_failed);
*tmp = '\0';
}
@@ -358,7 +365,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
{
if (tmp[1] != '\0') /* non-empty database name */
{
- realname = ecpg_strdup(tmp + 1, lineno);
+ realname = ecpg_strdup(tmp + 1, lineno, &alloc_failed);
connect_params++;
}
*tmp = '\0';
@@ -368,7 +375,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
if (tmp != NULL) /* port number given */
{
*tmp = '\0';
- port = ecpg_strdup(tmp + 1, lineno);
+ port = ecpg_strdup(tmp + 1, lineno, &alloc_failed);
connect_params++;
}
@@ -402,7 +409,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
{
if (*(dbname + offset) != '\0')
{
- host = ecpg_strdup(dbname + offset, lineno);
+ host = ecpg_strdup(dbname + offset, lineno, &alloc_failed);
connect_params++;
}
}
@@ -414,7 +421,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
tmp = strrchr(dbname, ':');
if (tmp != NULL) /* port number given */
{
- port = ecpg_strdup(tmp + 1, lineno);
+ port = ecpg_strdup(tmp + 1, lineno, &alloc_failed);
connect_params++;
*tmp = '\0';
}
@@ -422,14 +429,14 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
tmp = strrchr(dbname, '@');
if (tmp != NULL) /* host name given */
{
- host = ecpg_strdup(tmp + 1, lineno);
+ host = ecpg_strdup(tmp + 1, lineno, &alloc_failed);
connect_params++;
*tmp = '\0';
}
if (strlen(dbname) > 0)
{
- realname = ecpg_strdup(dbname, lineno);
+ realname = ecpg_strdup(dbname, lineno, &alloc_failed);
connect_params++;
}
else
@@ -460,7 +467,18 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
*/
conn_keywords = (const char **) ecpg_alloc((connect_params + 1) * sizeof(char *), lineno);
conn_values = (const char **) ecpg_alloc(connect_params * sizeof(char *), lineno);
- if (conn_keywords == NULL || conn_values == NULL)
+
+ /* Decide on a connection name */
+ if (connection_name != NULL || realname != NULL)
+ {
+ this->name = ecpg_strdup(connection_name ? connection_name : realname,
+ lineno, &alloc_failed);
+ }
+ else
+ this->name = NULL;
+
+ /* Deal with any failed allocations above */
+ if (conn_keywords == NULL || conn_values == NULL || alloc_failed)
{
if (host)
ecpg_free(host);
@@ -476,6 +494,8 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
ecpg_free(conn_keywords);
if (conn_values)
ecpg_free(conn_values);
+ if (this->name)
+ ecpg_free(this->name);
free(this);
return false;
}
@@ -510,17 +530,14 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
ecpg_free(conn_keywords);
if (conn_values)
ecpg_free(conn_values);
+ if (this->name)
+ ecpg_free(this->name);
free(this);
return false;
}
}
#endif
- if (connection_name != NULL)
- this->name = ecpg_strdup(connection_name, lineno);
- else
- this->name = ecpg_strdup(realname, lineno);
-
this->cache_head = NULL;
this->prep_stmts = NULL;
diff --git a/src/interfaces/ecpg/ecpglib/descriptor.c b/src/interfaces/ecpg/ecpglib/descriptor.c
index 651d5c8b2ed..466428edfeb 100644
--- a/src/interfaces/ecpg/ecpglib/descriptor.c
+++ b/src/interfaces/ecpg/ecpglib/descriptor.c
@@ -240,8 +240,9 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...)
act_tuple;
struct variable data_var;
struct sqlca_t *sqlca = ECPGget_sqlca();
+ bool alloc_failed = (sqlca == NULL);
- if (sqlca == NULL)
+ if (alloc_failed)
{
ecpg_raise(lineno, ECPG_OUT_OF_MEMORY,
ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL);
@@ -493,7 +494,14 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...)
#ifdef WIN32
stmt.oldthreadlocale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
#endif
- stmt.oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno);
+ stmt.oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL),
+ lineno, &alloc_failed);
+ if (alloc_failed)
+ {
+ va_end(args);
+ return false;
+ }
+
setlocale(LC_NUMERIC, "C");
#endif
diff --git a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
index 75cc68275bd..949ff66cefc 100644
--- a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
+++ b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
@@ -175,7 +175,7 @@ void ecpg_free(void *ptr);
bool ecpg_init(const struct connection *con,
const char *connection_name,
const int lineno);
-char *ecpg_strdup(const char *string, int lineno);
+char *ecpg_strdup(const char *string, int lineno, bool *alloc_failed);
const char *ecpg_type_name(enum ECPGttype typ);
int ecpg_dynamic_type(Oid type);
int sqlda_dynamic_type(Oid type, enum COMPAT_MODE compat);
diff --git a/src/interfaces/ecpg/ecpglib/execute.c b/src/interfaces/ecpg/ecpglib/execute.c
index f52da06de9a..84a4a9fc578 100644
--- a/src/interfaces/ecpg/ecpglib/execute.c
+++ b/src/interfaces/ecpg/ecpglib/execute.c
@@ -860,9 +860,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari
numeric *nval;
if (var->arrsize > 1)
- mallocedval = ecpg_strdup("{", lineno);
+ mallocedval = ecpg_strdup("{", lineno, NULL);
else
- mallocedval = ecpg_strdup("", lineno);
+ mallocedval = ecpg_strdup("", lineno, NULL);
if (!mallocedval)
return false;
@@ -923,9 +923,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari
int slen;
if (var->arrsize > 1)
- mallocedval = ecpg_strdup("{", lineno);
+ mallocedval = ecpg_strdup("{", lineno, NULL);
else
- mallocedval = ecpg_strdup("", lineno);
+ mallocedval = ecpg_strdup("", lineno, NULL);
if (!mallocedval)
return false;
@@ -970,9 +970,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari
int slen;
if (var->arrsize > 1)
- mallocedval = ecpg_strdup("{", lineno);
+ mallocedval = ecpg_strdup("{", lineno, NULL);
else
- mallocedval = ecpg_strdup("", lineno);
+ mallocedval = ecpg_strdup("", lineno, NULL);
if (!mallocedval)
return false;
@@ -1017,9 +1017,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari
int slen;
if (var->arrsize > 1)
- mallocedval = ecpg_strdup("{", lineno);
+ mallocedval = ecpg_strdup("{", lineno, NULL);
else
- mallocedval = ecpg_strdup("", lineno);
+ mallocedval = ecpg_strdup("", lineno, NULL);
if (!mallocedval)
return false;
@@ -2001,7 +2001,8 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator,
return false;
}
#endif
- stmt->oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno);
+ stmt->oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno,
+ NULL);
if (stmt->oldlocale == NULL)
{
ecpg_do_epilogue(stmt);
@@ -2030,7 +2031,14 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator,
statement_type = ECPGst_execute;
}
else
- stmt->command = ecpg_strdup(query, lineno);
+ {
+ stmt->command = ecpg_strdup(query, lineno, NULL);
+ if (!stmt->command)
+ {
+ ecpg_do_epilogue(stmt);
+ return false;
+ }
+ }
stmt->name = NULL;
@@ -2042,7 +2050,12 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator,
if (command)
{
stmt->name = stmt->command;
- stmt->command = ecpg_strdup(command, lineno);
+ stmt->command = ecpg_strdup(command, lineno, NULL);
+ if (!stmt->command)
+ {
+ ecpg_do_epilogue(stmt);
+ return false;
+ }
}
else
{
@@ -2175,7 +2188,12 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator,
if (!is_prepared_name_set && stmt->statement_type == ECPGst_prepare)
{
- stmt->name = ecpg_strdup(var->value, lineno);
+ stmt->name = ecpg_strdup(var->value, lineno, NULL);
+ if (!stmt->name)
+ {
+ ecpg_do_epilogue(stmt);
+ return false;
+ }
is_prepared_name_set = true;
}
}
diff --git a/src/interfaces/ecpg/ecpglib/memory.c b/src/interfaces/ecpg/ecpglib/memory.c
index 6979be2c988..2112e55b6e4 100644
--- a/src/interfaces/ecpg/ecpglib/memory.c
+++ b/src/interfaces/ecpg/ecpglib/memory.c
@@ -43,8 +43,15 @@ ecpg_realloc(void *ptr, long size, int lineno)
return new;
}
+/*
+ * Wrapper for strdup(), with NULL in input treated as a correct case.
+ *
+ * "alloc_failed" can be optionally specified by the caller to check for
+ * allocation failures. The caller is responsible for its initialization,
+ * as ecpg_strdup() may be called repeatedly across multiple allocations.
+ */
char *
-ecpg_strdup(const char *string, int lineno)
+ecpg_strdup(const char *string, int lineno, bool *alloc_failed)
{
char *new;
@@ -54,6 +61,8 @@ ecpg_strdup(const char *string, int lineno)
new = strdup(string);
if (!new)
{
+ if (alloc_failed)
+ *alloc_failed = true;
ecpg_raise(lineno, ECPG_OUT_OF_MEMORY, ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL);
return NULL;
}
diff --git a/src/interfaces/ecpg/ecpglib/prepare.c b/src/interfaces/ecpg/ecpglib/prepare.c
index ea1146f520f..dd6fd1fe7f4 100644
--- a/src/interfaces/ecpg/ecpglib/prepare.c
+++ b/src/interfaces/ecpg/ecpglib/prepare.c
@@ -85,9 +85,22 @@ ecpg_register_prepared_stmt(struct statement *stmt)
/* create statement */
prep_stmt->lineno = lineno;
prep_stmt->connection = con;
- prep_stmt->command = ecpg_strdup(stmt->command, lineno);
+ prep_stmt->command = ecpg_strdup(stmt->command, lineno, NULL);
+ if (!prep_stmt->command)
+ {
+ ecpg_free(prep_stmt);
+ ecpg_free(this);
+ return false;
+ }
prep_stmt->inlist = prep_stmt->outlist = NULL;
- this->name = ecpg_strdup(stmt->name, lineno);
+ this->name = ecpg_strdup(stmt->name, lineno, NULL);
+ if (!this->name)
+ {
+ ecpg_free(prep_stmt->command);
+ ecpg_free(prep_stmt);
+ ecpg_free(this);
+ return false;
+ }
this->stmt = prep_stmt;
this->prepared = true;
@@ -177,14 +190,27 @@ prepare_common(int lineno, struct connection *con, const char *name, const char
/* create statement */
stmt->lineno = lineno;
stmt->connection = con;
- stmt->command = ecpg_strdup(variable, lineno);
+ stmt->command = ecpg_strdup(variable, lineno, NULL);
+ if (!stmt->command)
+ {
+ ecpg_free(stmt);
+ ecpg_free(this);
+ return false;
+ }
stmt->inlist = stmt->outlist = NULL;
/* if we have C variables in our statement replace them with '?' */
replace_variables(&(stmt->command), lineno);
/* add prepared statement to our list */
- this->name = ecpg_strdup(name, lineno);
+ this->name = ecpg_strdup(name, lineno, NULL);
+ if (!this->name)
+ {
+ ecpg_free(stmt->command);
+ ecpg_free(stmt);
+ ecpg_free(this);
+ return false;
+ }
this->stmt = stmt;
/* and finally really prepare the statement */
@@ -540,7 +566,9 @@ AddStmtToCache(int lineno, /* line # of statement */
/* add the query to the entry */
entry = &stmtCacheEntries[entNo];
entry->lineno = lineno;
- entry->ecpgQuery = ecpg_strdup(ecpgQuery, lineno);
+ entry->ecpgQuery = ecpg_strdup(ecpgQuery, lineno, NULL);
+ if (!entry->ecpgQuery)
+ return -1;
entry->connection = connection;
entry->execs = 0;
memcpy(entry->stmtID, stmtID, sizeof(entry->stmtID));
@@ -567,6 +595,9 @@ ecpg_auto_prepare(int lineno, const char *connection_name, const int compat, cha
ecpg_log("ecpg_auto_prepare on line %d: statement found in cache; entry %d\n", lineno, entNo);
stmtID = stmtCacheEntries[entNo].stmtID;
+ *name = ecpg_strdup(stmtID, lineno, NULL);
+ if (*name == NULL)
+ return false;
con = ecpg_get_connection(connection_name);
prep = ecpg_find_prepared_statement(stmtID, con, NULL);
@@ -574,7 +605,6 @@ ecpg_auto_prepare(int lineno, const char *connection_name, const int compat, cha
if (!prep && !prepare_common(lineno, con, stmtID, query))
return false;
- *name = ecpg_strdup(stmtID, lineno);
}
else
{
@@ -584,6 +614,9 @@ ecpg_auto_prepare(int lineno, const char *connection_name, const int compat, cha
/* generate a statement ID */
sprintf(stmtID, "ecpg%d", nextStmtID++);
+ *name = ecpg_strdup(stmtID, lineno, NULL);
+ if (*name == NULL)
+ return false;
if (!ECPGprepare(lineno, connection_name, 0, stmtID, query))
return false;
@@ -591,8 +624,6 @@ ecpg_auto_prepare(int lineno, const char *connection_name, const int compat, cha
entNo = AddStmtToCache(lineno, stmtID, connection_name, compat, query);
if (entNo < 0)
return false;
-
- *name = ecpg_strdup(stmtID, lineno);
}
/* increase usage counter */
diff --git a/src/port/pgmkdirp.c b/src/port/pgmkdirp.c
index d943559760d..7d7cea4dd0e 100644
--- a/src/port/pgmkdirp.c
+++ b/src/port/pgmkdirp.c
@@ -73,7 +73,7 @@ pg_mkdir_p(char *path, int omode)
if (p[0] == '/' && p[1] == '/')
{
/* network drive */
- p = strstr(p + 2, "/");
+ p = strchr(p + 2, '/');
if (p == NULL)
{
errno = EINVAL;
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 301766d2ed9..61f68e0cc2e 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -290,6 +290,32 @@ sub connstr
=pod
+=item $node->is_alive()
+
+Check if the node is alive, using pg_isready.
+Returns 1 if successful, 0 on failure.
+
+=cut
+
+sub is_alive
+{
+ my ($self) = @_;
+ local %ENV = $self->_get_env();
+
+ my $ret = PostgreSQL::Test::Utils::system_log(
+ 'pg_isready',
+ '--host' => $self->host,
+ '--port' => $self->port);
+
+ if ($ret != 0)
+ {
+ return 0;
+ }
+ return 1;
+}
+
+=pod
+
=item $node->raw_connect()
Open a raw TCP or Unix domain socket connection to the server. This is
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 6e78ff1a030..52993c32dbb 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -54,6 +54,7 @@ tests += {
't/043_no_contrecord_switch.pl',
't/044_invalidate_inactive_slots.pl',
't/045_archive_restartpoint.pl',
+ 't/046_checkpoint_logical_slot.pl',
't/047_checkpoint_physical_slot.pl',
't/048_vacuum_horizon_floor.pl'
],
diff --git a/src/test/recovery/t/027_stream_regress.pl b/src/test/recovery/t/027_stream_regress.pl
index 83def062d11..5d2c06ba06e 100644
--- a/src/test/recovery/t/027_stream_regress.pl
+++ b/src/test/recovery/t/027_stream_regress.pl
@@ -81,7 +81,14 @@ my $rc =
. "--max-concurrent-tests=20 "
. "--inputdir=../regress "
. "--outputdir=\"$outputdir\"");
-if ($rc != 0)
+
+# Regression diffs are only meaningful if both the primary and the standby
+# are still alive after a regression test failure. A crash would cause a
+# useless increase in the log quantity, mostly filled with information
+# related to queries that could not run.
+my $primary_alive = $node_primary->is_alive;
+my $standby_alive = $node_standby_1->is_alive;
+if ($rc != 0 && $primary_alive && $standby_alive)
{
# Dump out the regression diffs file, if there is one
my $diffs = "$outputdir/regression.diffs";
@@ -93,6 +100,8 @@ if ($rc != 0)
}
}
is($rc, 0, 'regression tests pass');
+is($primary_alive, 1, 'primary alive after regression test run');
+is($standby_alive, 1, 'standby alive after regression test run');
# Clobber all sequences with their next value, so that we don't have
# differences between nodes due to caching.
diff --git a/src/test/recovery/t/046_checkpoint_logical_slot.pl b/src/test/recovery/t/046_checkpoint_logical_slot.pl
new file mode 100644
index 00000000000..4fd709e3a03
--- /dev/null
+++ b/src/test/recovery/t/046_checkpoint_logical_slot.pl
@@ -0,0 +1,142 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+#
+# This test verifies the case when the logical slot is advanced during
+# checkpoint. The test checks that the logical slot's restart_lsn still refers
+# to an existed WAL segment after immediate restart.
+#
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+ plan skip_all => 'Injection points not supported by this build';
+}
+
+my ($node, $result);
+
+$node = PostgreSQL::Test::Cluster->new('mike');
+$node->init;
+$node->append_conf('postgresql.conf', "wal_level = 'logical'");
+$node->start;
+
+# Check if the extension injection_points is available, as it may be
+# possible that this script is run with installcheck, where the module
+# would not be installed by default.
+if (!$node->check_extension('injection_points'))
+{
+ plan skip_all => 'Extension injection_points not installed';
+}
+
+$node->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+
+# Create the two slots we'll need.
+$node->safe_psql('postgres',
+ q{select pg_create_logical_replication_slot('slot_logical', 'test_decoding')}
+);
+$node->safe_psql('postgres',
+ q{select pg_create_physical_replication_slot('slot_physical', true)});
+
+# Advance both slots to the current position just to have everything "valid".
+$node->safe_psql('postgres',
+ q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null)}
+);
+$node->safe_psql('postgres',
+ q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
+);
+
+# Run checkpoint to flush current state to disk and set a baseline.
+$node->safe_psql('postgres', q{checkpoint});
+
+# Generate some transactions to get RUNNING_XACTS.
+my $xacts = $node->background_psql('postgres');
+$xacts->query_until(
+ qr/run_xacts/,
+ q(\echo run_xacts
+SELECT 1 \watch 0.1
+\q
+));
+
+$node->advance_wal(20);
+
+# Run another checkpoint to set a new restore LSN.
+$node->safe_psql('postgres', q{checkpoint});
+
+$node->advance_wal(20);
+
+# Run another checkpoint, this time in the background, and make it wait
+# on the injection point) so that the checkpoint stops right before
+# removing old WAL segments.
+note('starting checkpoint');
+
+my $checkpoint = $node->background_psql('postgres');
+$checkpoint->query_safe(
+ q(select injection_points_attach('checkpoint-before-old-wal-removal','wait'))
+);
+$checkpoint->query_until(
+ qr/starting_checkpoint/,
+ q(\echo starting_checkpoint
+checkpoint;
+\q
+));
+
+# Wait until the checkpoint stops right before removing WAL segments.
+note('waiting for injection_point');
+$node->wait_for_event('checkpointer', 'checkpoint-before-old-wal-removal');
+note('injection_point is reached');
+
+# Try to advance the logical slot, but make it stop when it moves to the next
+# WAL segment (this has to happen in the background, too).
+my $logical = $node->background_psql('postgres');
+$logical->query_safe(
+ q{select injection_points_attach('logical-replication-slot-advance-segment','wait');}
+);
+$logical->query_until(
+ qr/get_changes/,
+ q(
+\echo get_changes
+select count(*) from pg_logical_slot_get_changes('slot_logical', null, null) \watch 1
+\q
+));
+
+# Wait until the slot's restart_lsn points to the next WAL segment.
+note('waiting for injection_point');
+$node->wait_for_event('client backend',
+ 'logical-replication-slot-advance-segment');
+note('injection_point is reached');
+
+# OK, we're in the right situation: time to advance the physical slot, which
+# recalculates the required LSN, and then unblock the checkpoint, which
+# removes the WAL still needed by the logical slot.
+$node->safe_psql('postgres',
+ q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
+);
+
+# Generate a long WAL record, spawning at least two pages for the follow-up
+# post-recovery check.
+$node->safe_psql('postgres',
+ q{select pg_logical_emit_message(false, '', repeat('123456789', 1000))});
+
+# Continue the checkpoint and wait for its completion.
+my $log_offset = -s $node->logfile;
+$node->safe_psql('postgres',
+ q{select injection_points_wakeup('checkpoint-before-old-wal-removal')});
+$node->wait_for_log(qr/checkpoint complete/, $log_offset);
+
+# Abruptly stop the server.
+$node->stop('immediate');
+
+$node->start;
+
+eval {
+ $node->safe_psql('postgres',
+ q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null);}
+ );
+};
+is($@, '', "Logical slot still valid");
+
+done_testing();
diff --git a/src/test/recovery/t/047_checkpoint_physical_slot.pl b/src/test/recovery/t/047_checkpoint_physical_slot.pl
index a1332b5d44c..9e98383e30e 100644
--- a/src/test/recovery/t/047_checkpoint_physical_slot.pl
+++ b/src/test/recovery/t/047_checkpoint_physical_slot.pl
@@ -94,9 +94,11 @@ $node->safe_psql('postgres',
q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
);
-# Continue the checkpoint.
+# Continue the checkpoint and wait for its completion.
+my $log_offset = -s $node->logfile;
$node->safe_psql('postgres',
q{select injection_points_wakeup('checkpoint-before-old-wal-removal')});
+$node->wait_for_log(qr/checkpoint complete/, $log_offset);
my $restart_lsn_old = $node->safe_psql('postgres',
q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'}
@@ -104,8 +106,7 @@ my $restart_lsn_old = $node->safe_psql('postgres',
chomp($restart_lsn_old);
note("restart lsn before stop: $restart_lsn_old");
-# Abruptly stop the server (1 second should be enough for the checkpoint
-# to finish; it would be better).
+# Abruptly stop the server.
$node->stop('immediate');
$node->start;
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 3b40e15a95a..aca6347babe 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1550,11 +1550,11 @@ where coalesce(t2.b, 1) = 2;
explain (costs off)
select t1.a from gtest32 t1 left join gtest32 t2 on t1.a = t2.a
where coalesce(t2.b, 1) = 2 or t1.a is null;
- QUERY PLAN
--------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------
Hash Left Join
Hash Cond: (t1.a = t2.a)
- Filter: ((COALESCE((t2.a * 2), 1) = 2) OR (t1.a IS NULL))
+ Filter: (COALESCE((t2.a * 2), 1) = 2)
-> Seq Scan on gtest32 t1
-> Hash
-> Seq Scan on gtest32 t2
@@ -1613,4 +1613,26 @@ select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20;
-- Ensure that the virtual generated columns in ALTER COLUMN TYPE USING expression are expanded
alter table gtest32 alter column e type bigint using b;
+-- Ensure that virtual generated column references within SubLinks that should
+-- be transformed into joins can get expanded
+explain (costs off)
+select 1 from gtest32 t1 where exists
+ (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2);
+ QUERY PLAN
+-------------------------------------
+ Nested Loop Semi Join
+ Join Filter: (t1.a > t2.a)
+ -> Seq Scan on gtest32 t1
+ -> Materialize
+ -> Seq Scan on gtest32 t2
+ Filter: ((a * 2) = 2)
+(6 rows)
+
+select 1 from gtest32 t1 where exists
+ (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2);
+ ?column?
+----------
+ 1
+(1 row)
+
drop table gtest32;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 46ddfa844c5..4d5d35d0727 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -3639,8 +3639,8 @@ from nt3 as nt3
) as ss2
on ss2.id = nt3.nt2_id
where nt3.id = 1 and ss2.b3;
- QUERY PLAN
------------------------------------------------
+ QUERY PLAN
+----------------------------------------------
Nested Loop
-> Nested Loop
-> Index Scan using nt3_pkey on nt3
@@ -3649,7 +3649,7 @@ where nt3.id = 1 and ss2.b3;
Index Cond: (id = nt3.nt2_id)
-> Index Only Scan using nt1_pkey on nt1
Index Cond: (id = nt2.nt1_id)
- Filter: (nt2.b1 AND (id IS NOT NULL))
+ Filter: (nt2.b1 AND true)
(9 rows)
select nt3.id
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index b79037748b7..59bfe33bb1c 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -84,10 +84,10 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;
-- are provably false
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab t WHERE t.b IS NULL OR t.c IS NULL;
- QUERY PLAN
-----------------------------------------
+ QUERY PLAN
+------------------------
Seq Scan on pred_tab t
- Filter: ((b IS NULL) OR (c IS NULL))
+ Filter: (b IS NULL)
(2 rows)
--
@@ -231,6 +231,54 @@ SELECT * FROM pred_tab t1
-> Seq Scan on pred_tab t3
(9 rows)
+--
+-- Tests for NullTest reduction in EXISTS sublink
+--
+-- Ensure the IS_NOT_NULL qual is ignored
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1
+ LEFT JOIN pred_tab t2 ON EXISTS
+ (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
+ WHERE t1.a = t3.a AND t6.a IS NOT NULL);
+ QUERY PLAN
+---------------------------------------------------------
+ Nested Loop Left Join
+ Join Filter: EXISTS(SubPlan 1)
+ -> Seq Scan on pred_tab t1
+ -> Materialize
+ -> Seq Scan on pred_tab t2
+ SubPlan 1
+ -> Nested Loop
+ -> Nested Loop
+ -> Nested Loop
+ -> Seq Scan on pred_tab t4
+ -> Materialize
+ -> Seq Scan on pred_tab t3
+ Filter: (t1.a = a)
+ -> Materialize
+ -> Seq Scan on pred_tab t5
+ -> Materialize
+ -> Seq Scan on pred_tab t6
+(17 rows)
+
+-- Ensure the IS_NULL qual is reduced to constant-FALSE
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1
+ LEFT JOIN pred_tab t2 ON EXISTS
+ (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
+ WHERE t1.a = t3.a AND t6.a IS NULL);
+ QUERY PLAN
+-------------------------------------
+ Nested Loop Left Join
+ Join Filter: (InitPlan 1).col1
+ InitPlan 1
+ -> Result
+ One-Time Filter: false
+ -> Seq Scan on pred_tab t1
+ -> Materialize
+ -> Seq Scan on pred_tab t2
+(8 rows)
+
DROP TABLE pred_tab;
-- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
-- parents.
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index e2b31853e01..ba19bc4c701 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -858,4 +858,13 @@ select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20;
-- Ensure that the virtual generated columns in ALTER COLUMN TYPE USING expression are expanded
alter table gtest32 alter column e type bigint using b;
+-- Ensure that virtual generated column references within SubLinks that should
+-- be transformed into joins can get expanded
+explain (costs off)
+select 1 from gtest32 t1 where exists
+ (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2);
+
+select 1 from gtest32 t1 where exists
+ (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2);
+
drop table gtest32;
diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql
index 9dcb81b1bc5..d92277353a0 100644
--- a/src/test/regress/sql/predicate.sql
+++ b/src/test/regress/sql/predicate.sql
@@ -115,6 +115,24 @@ SELECT * FROM pred_tab t1
LEFT JOIN pred_tab t2 ON t1.a = 1
LEFT JOIN pred_tab t3 ON t2.a IS NULL OR t2.c IS NULL;
+--
+-- Tests for NullTest reduction in EXISTS sublink
+--
+
+-- Ensure the IS_NOT_NULL qual is ignored
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1
+ LEFT JOIN pred_tab t2 ON EXISTS
+ (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
+ WHERE t1.a = t3.a AND t6.a IS NOT NULL);
+
+-- Ensure the IS_NULL qual is reduced to constant-FALSE
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1
+ LEFT JOIN pred_tab t2 ON EXISTS
+ (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6
+ WHERE t1.a = t3.a AND t6.a IS NULL);
+
DROP TABLE pred_tab;
-- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ff050e93a50..cd897467088 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1759,6 +1759,7 @@ NonEmptyRange
Notification
NotificationList
NotifyStmt
+NotnullHashEntry
Nsrt
NtDllRoutine
NtFlushBuffersFileEx_t