From 0b640db58ababd973790c3c2ef3f422a45e0c4b4 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Tue, 27 Oct 2026 21:32:59 -0400 Subject: [PATCH 1/3] Do not trust not-null constraints newer than the snapshot in use. --- src/backend/executor/execUtils.c | 21 +++++++++++ src/backend/executor/nodeBitmapHeapscan.c | 5 +-- src/backend/executor/nodeIndexscan.c | 5 +-- src/backend/executor/nodeSamplescan.c | 6 +-- src/backend/executor/nodeSeqscan.c | 6 +-- src/backend/executor/nodeTidrangescan.c | 5 +-- src/backend/executor/nodeTidscan.c | 5 +-- src/backend/optimizer/util/clauses.c | 6 ++- src/backend/optimizer/util/plancat.c | 28 ++++++++++++-- src/backend/utils/cache/relcache.c | 45 +++++++++++++++++++++-- src/include/executor/executor.h | 2 + src/include/utils/rel.h | 2 + src/include/utils/relcache.h | 3 ++ 13 files changed, 107 insertions(+), 32 deletions(-) diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c index c0c4276acbf..f7160c0e878 100644 --- a/src/backend/executor/execUtils.c +++ b/src/backend/executor/execUtils.c @@ -788,6 +788,27 @@ ExecOpenScanRelation(EState *estate, Index scanrelid, int eflags) return rel; } +/* ---------------------------------------------------------------- + * ExecInitScanTupleSlotFromRel + * + * Create the scan slot for a base-level scan of rel. The slot may + * skip null checks for columns with valid not-null constraints, but + * not if a snapshot older than those constraints could see nulls. + * ---------------------------------------------------------------- + */ +void +ExecInitScanTupleSlotFromRel(EState *estate, ScanState *scanstate, + Relation rel) +{ + uint16 flags = 0; + + if (RelationCanTrustNotNulls(rel, estate->es_snapshot)) + flags |= TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS; + + ExecInitScanTupleSlot(estate, scanstate, RelationGetDescr(rel), + table_slot_callbacks(rel), flags); +} + /* * ExecInitRangeTable * Set up executor's range-table-related data diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c index a9e83f30687..ea2b7ac1787 100644 --- a/src/backend/executor/nodeBitmapHeapscan.c +++ b/src/backend/executor/nodeBitmapHeapscan.c @@ -433,10 +433,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags) /* * get the scan type from the relation descriptor. */ - ExecInitScanTupleSlot(estate, &scanstate->ss, - RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation), - TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); + ExecInitScanTupleSlotFromRel(estate, &scanstate->ss, currentRelation); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c index 129d005f187..79a3dd6a8cf 100644 --- a/src/backend/executor/nodeIndexscan.c +++ b/src/backend/executor/nodeIndexscan.c @@ -945,10 +945,7 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags) /* * get the scan type from the relation descriptor. */ - ExecInitScanTupleSlot(estate, &indexstate->ss, - RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation), - TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); + ExecInitScanTupleSlotFromRel(estate, &indexstate->ss, currentRelation); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeSamplescan.c b/src/backend/executor/nodeSamplescan.c index f3d273e1c5e..28cba7e0abf 100644 --- a/src/backend/executor/nodeSamplescan.c +++ b/src/backend/executor/nodeSamplescan.c @@ -126,10 +126,8 @@ ExecInitSampleScan(SampleScan *node, EState *estate, int eflags) scanstate->ss.ss_currentScanDesc = NULL; /* and create slot with appropriate rowtype */ - ExecInitScanTupleSlot(estate, &scanstate->ss, - RelationGetDescr(scanstate->ss.ss_currentRelation), - table_slot_callbacks(scanstate->ss.ss_currentRelation), - TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); + ExecInitScanTupleSlotFromRel(estate, &scanstate->ss, + scanstate->ss.ss_currentRelation); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c index b8c528ca089..8005a462281 100644 --- a/src/backend/executor/nodeSeqscan.c +++ b/src/backend/executor/nodeSeqscan.c @@ -251,10 +251,8 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags) eflags); /* and create slot with the appropriate rowtype */ - ExecInitScanTupleSlot(estate, &scanstate->ss, - RelationGetDescr(scanstate->ss.ss_currentRelation), - table_slot_callbacks(scanstate->ss.ss_currentRelation), - TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); + ExecInitScanTupleSlotFromRel(estate, &scanstate->ss, + scanstate->ss.ss_currentRelation); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeTidrangescan.c b/src/backend/executor/nodeTidrangescan.c index b387ed6c308..5a8281bccc0 100644 --- a/src/backend/executor/nodeTidrangescan.c +++ b/src/backend/executor/nodeTidrangescan.c @@ -417,10 +417,7 @@ ExecInitTidRangeScan(TidRangeScan *node, EState *estate, int eflags) /* * get the scan type from the relation descriptor. */ - ExecInitScanTupleSlot(estate, &tidrangestate->ss, - RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation), - TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); + ExecInitScanTupleSlotFromRel(estate, &tidrangestate->ss, currentRelation); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeTidscan.c b/src/backend/executor/nodeTidscan.c index b63329de330..16d7da962e9 100644 --- a/src/backend/executor/nodeTidscan.c +++ b/src/backend/executor/nodeTidscan.c @@ -527,10 +527,7 @@ ExecInitTidScan(TidScan *node, EState *estate, int eflags) /* * get the scan type from the relation descriptor. */ - ExecInitScanTupleSlot(estate, &tidstate->ss, - RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation), - TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); + ExecInitScanTupleSlotFromRel(estate, &tidstate->ss, currentRelation); /* * Initialize result type and projection. diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 95134bfeda3..4a40f767087 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -61,6 +61,7 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/typcache.h" @@ -4958,7 +4959,10 @@ var_is_nonnullable(PlannerInfo *root, Var *var, NotNullSource source) rel = table_open(rte->relid, NoLock); attr = TupleDescCompactAttr(RelationGetDescr(rel), var->varattno - 1); - result = (attr->attnullability == ATTNULLABLE_VALID); + result = (attr->attnullability == ATTNULLABLE_VALID && + RelationCanTrustNotNulls(rel, + ActiveSnapshotSet() ? + GetActiveSnapshot() : NULL)); /* * We cannot trust a NOT NULL constraint on a virtual diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 22e0e8b11f8..669ac75d837 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -678,6 +678,26 @@ get_relation_foreign_keys(PlannerInfo *root, RelOptInfo *rel, } } +/* + * notnull_constraints_usable - + * May the planner rely on the relation's not-null constraints? + * + * The plan runs under the active snapshot or a later one. If the constraints + * are too new for it, the plan is marked transient so that it gets rebuilt + * once they are old enough, as for indexes with indcheckxmin set. + */ +static bool +notnull_constraints_usable(PlannerInfo *root, Relation relation) +{ + Snapshot snapshot = ActiveSnapshotSet() ? GetActiveSnapshot() : NULL; + + if (RelationCanTrustNotNulls(relation, snapshot)) + return true; + + root->glob->transientPlan = true; + return false; +} + /* * get_relation_notnullatts - * Retrieves column not-null constraint information for a given relation. @@ -693,9 +713,10 @@ get_relation_notnullatts(PlannerInfo *root, Relation relation) bool found; Bitmapset *notnullattnums = NULL; - /* bail out if the relation has no not-null constraints */ + /* bail out if the relation has no not-null constraints we can use */ if (relation->rd_att->constr == NULL || - !relation->rd_att->constr->has_not_null) + !relation->rd_att->constr->has_not_null || + !notnull_constraints_usable(root, relation)) return; /* create the hash table if it hasn't been created yet */ @@ -1589,7 +1610,8 @@ get_relation_constraints(PlannerInfo *root, } /* Add NOT NULL constraints in expression form, if requested */ - if (include_notnull && constr->has_not_null) + if (include_notnull && constr->has_not_null && + notnull_constraints_usable(root, relation)) { int natts = relation->rd_att->natts; diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index ad8aee8447c..bb0e689fcec 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4601,6 +4601,8 @@ CheckNNConstraintFetch(Relation relation) HeapTuple htup; int found = 0; + relation->rd_notnullxmin = InvalidTransactionId; + /* Allocate array with room for as many entries as expected, if needed */ if (ncheck > 0) check = (ConstrCheck *) @@ -4628,10 +4630,11 @@ CheckNNConstraintFetch(Relation relation) /* * If this is a not-null constraint, then only look at it if it's * invalid, and if so, mark the TupleDesc entry as known invalid. - * Otherwise move on. We'll mark any remaining columns that are still - * in UNKNOWN state as known valid later. This allows us not to have - * to extract the attnum from this constraint tuple in the vast - * majority of cases. + * Otherwise just remember the newest xmin of the valid ones, for + * RelationCanTrustNotNulls. We'll mark any remaining columns that + * are still in UNKNOWN state as known valid later. This allows us + * not to have to extract the attnum from this constraint tuple in the + * vast majority of cases. */ if (conform->contype == CONSTRAINT_NOTNULL) { @@ -4645,6 +4648,15 @@ CheckNNConstraintFetch(Relation relation) relation->rd_att->compact_attrs[attnum - 1].attnullability = ATTNULLABLE_INVALID; } + else + { + TransactionId xmin = HeapTupleHeaderGetXmin(htup->t_data); + + /* our own are always trusted, see RelationCanTrustNotNulls */ + if (!TransactionIdIsCurrentTransactionId(xmin) && + TransactionIdFollows(xmin, relation->rd_notnullxmin)) + relation->rd_notnullxmin = xmin; + } continue; } @@ -4716,6 +4728,31 @@ CheckConstraintCmp(const void *a, const void *b) return strcmp(ca->ccname, cb->ccname); } +/* + * RelationCanTrustNotNulls -- do the relation's valid not-null constraints + * hold for every tuple that snapshot can see? + * + * The tuples with nulls were deleted before the constraint was validated, so + * they are invisible to any snapshot that sees the constraint committed. A + * snapshot taken earlier can still see them. A NULL snapshot asks about every + * snapshot of this backend. Constraints added by the current transaction + * are not in rd_notnullxmin: the snapshots of its later commands cannot see + * the nulls they replaced. + */ +bool +RelationCanTrustNotNulls(Relation relation, Snapshot snapshot) +{ + TransactionId xmin = relation->rd_notnullxmin; + + if (!TransactionIdIsValid(xmin) || + TransactionIdPrecedes(xmin, TransactionXmin)) + return true; + + return snapshot != NULL && + snapshot->snapshot_type == SNAPSHOT_MVCC && + !XidInMVCCSnapshot(xmin, snapshot); +} + /* * RelationGetFKeyList -- get a list of foreign key info for the relation * diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 152a1dfa568..74788f1e3db 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -698,6 +698,8 @@ extern bool ExecRelationIsTargetRelation(EState *estate, Index scanrelid); extern bool ScanRelIsReadOnly(ScanState *ss); extern Relation ExecOpenScanRelation(EState *estate, Index scanrelid, int eflags); +extern void ExecInitScanTupleSlotFromRel(EState *estate, ScanState *scanstate, + Relation rel); extern void ExecInitRangeTable(EState *estate, List *rangeTable, List *permInfos, Bitmapset *unpruned_relids); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 7b8c2b1e362..be26de228eb 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -117,6 +117,8 @@ typedef struct RelationData TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */ /* use "struct" here to avoid needing to include rowsecurity.h: */ struct RowSecurityDesc *rd_rsdesc; /* row security policies, or NULL */ + TransactionId rd_notnullxmin; /* xmin of newest valid not-null + * constraint of another xact, or 0 */ /* data managed by RelationGetFKeyList: */ List *rd_fkeylist; /* list of ForeignKeyCacheInfo (see below) */ diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h index 3e97da6a538..b25bc6baa7e 100644 --- a/src/include/utils/relcache.h +++ b/src/include/utils/relcache.h @@ -25,6 +25,7 @@ #define RELCACHE_INIT_FILENAME "pg_internal.init" typedef struct RelationData *Relation; +struct SnapshotData; /* ---------------- * RelationPtr is used in the executor to support index scans @@ -51,6 +52,8 @@ extern void RelationClose(Relation relation); /* * Routines to compute/retrieve additional cached information */ +extern bool RelationCanTrustNotNulls(Relation relation, + struct SnapshotData *snapshot); extern List *RelationGetFKeyList(Relation relation); extern List *RelationGetIndexList(Relation relation); extern List *RelationGetStatExtList(Relation relation); -- 2.37.1 (Apple Git-137.1)