start of flag-based reporting advice_unstable
authorRobert Haas <[email protected]>
Mon, 29 Sep 2025 19:20:10 +0000 (15:20 -0400)
committerRobert Haas <[email protected]>
Mon, 29 Sep 2025 19:20:10 +0000 (15:20 -0400)
currently this only works for the join path hook, not the join rel
or scan hook

the results are emitted as warnings instead of being properly
structured

i have a feeling that there needs to be more interaction between
the join path stuff and the join rel stuff - e.g. PARTITIONWISE((a b))
conflicts with HASH_JOIN(b) but I don't think the current code can
detect that

we also need to split off the stuff that gets handled in the join
rel hook from a trove POV, to avoid making duplicate entries that
will confuse this reporting

an incidental note is that i think we should teach the advice
parser to ignore comments, so that we can emit something that looks
like a comment as a report, e.g.
   JOIN_ORDER(a b) /* matched, conflicted */
   JOIN_ORDER(a b) /* partial match only */
   JOIN_ORDER(a b) /* not matched */
   INDEX_SCAN(this that_idx) /* matched, inapplicable */

contrib/pg_plan_advice/pgpa_planner.c
contrib/pg_plan_advice/pgpa_trove.c
contrib/pg_plan_advice/pgpa_trove.h

index 2096332b6fc81757c0c210d78dff8c8adc82fc27..b71db3b02a63638aea04ad7dbe2d850999bf4d37 100644 (file)
@@ -156,17 +156,20 @@ static uint64 pgpa_join_strategy_mask_from_advice_tag(pgpa_advice_tag_type tag);
 static bool pgpa_join_order_permits_join(int outer_count, int inner_count,
                                                                                 pgpa_trove_key *tkeys,
                                                                                 char *plan_name,
-                                                                                pgpa_advice_target *target);
+                                                                                pgpa_trove_entry *entry);
 static bool pgpa_join_method_permits_join(int outer_count, int inner_count,
                                                                                  pgpa_trove_key *tkeys,
                                                                                  char *plan_name,
-                                                                                 pgpa_advice_target *target,
+                                                                                 pgpa_trove_entry *entry,
                                                                                  bool *restrict_method);
 
+static void pgpa_set_entry_flags(pgpa_trove_entry *entries, Bitmapset *indexes,
+                                                                int flags);
 static inline void pgpa_ri_checker_save(pgpa_planner_state *pps,
                                                                                PlannerInfo *root,
                                                                                RelOptInfo *rel);
-static void pgpa_ri_checker_validate(pgpa_planner_state *pps, PlannedStmt *pstmt);
+static void pgpa_ri_checker_validate(pgpa_planner_state *pps,
+                                                                        PlannedStmt *pstmt);
 
 /*
  * Install planner-related hooks.
@@ -515,6 +518,28 @@ pgpa_planner_setup(PlannerGlobal *glob, Query *parse, const char *query_string,
        }
 }
 
+/*
+ * XXX obviously not the right thing
+ *
+ * XXX note that any overlap between troves is a real problem here -- we need
+ * one trove entry per tag/target combo
+ */
+static void
+pgpa_planner_stupid_warning(pgpa_trove *trove, pgpa_trove_lookup_type type)
+{
+       pgpa_trove_entry *entries;
+       int nentries;
+
+       pgpa_trove_lookup_all(trove, type, &entries, &nentries);
+       for (int i = 0; i < nentries; ++i)
+       {
+               pgpa_trove_entry *entry = &entries[i];
+
+               elog(WARNING, "%s -> %d", pgpa_cstring_trove_entry(entry),
+                        entry->flags);
+       }
+}
+
 /*
  * Carry out whatever work we want to do after planning is complete.
  */
@@ -527,6 +552,12 @@ pgpa_planner_shutdown(PlannerGlobal *glob, Query *parse,
        /* Fetch our private state, set up by pgpa_planner_setup(). */
        pps = GetPlannerGlobalExtensionState(glob, planner_extension_id);
 
+       if (pps != NULL && pps->trove != NULL)
+       {
+               pgpa_planner_stupid_warning(pps->trove, PGPA_TROVE_LOOKUP_SCAN);
+               pgpa_planner_stupid_warning(pps->trove, PGPA_TROVE_LOOKUP_JOIN);
+       }
+
        /*
         * If assertions are enabled, cross-check the generated range table
         * identifiers.
@@ -723,26 +754,29 @@ pgpa_planner_apply_join_path_advice(JoinType jointype, uint64 *pgs_mask_p,
                                                                        pgpa_join_state *pjs)
 {
        int                     i = -1;
-       bool            join_ok = true;
+       Bitmapset  *jo_permit_indexes = NULL;
+       Bitmapset  *jo_deny_indexes = NULL;
+       Bitmapset  *jm_indexes = NULL;
+       bool            jm_conflict = false;
        uint32          pgs_mask = 0;
 
        /* Iterate over all possibly-relevant advice. */
        while ((i = bms_next_member(pjs->indexes, i)) >= 0)
        {
                pgpa_trove_entry *entry = &pjs->entries[i];
-               uint32          my_pgs_mask = 0;
+               uint32          my_pgs_mask;
 
                /* Handle join order advice. */
                if (entry->tag == PGPA_TAG_JOIN_ORDER)
                {
-                       if (!join_ok)
-                               continue;
-                       if (!pgpa_join_order_permits_join(pjs->outer_count,
-                                                                                         pjs->inner_count,
-                                                                                         pjs->tkeys,
-                                                                                         plan_name,
-                                                                                         entry->target))
-                               join_ok = false;
+                       if (pgpa_join_order_permits_join(pjs->outer_count,
+                                                                                        pjs->inner_count,
+                                                                                        pjs->tkeys,
+                                                                                        plan_name,
+                                                                                        entry))
+                               jo_permit_indexes = bms_add_member(jo_permit_indexes, i);
+                       else
+                               jo_deny_indexes = bms_add_member(jo_deny_indexes, i);
                        continue;
                }
 
@@ -756,15 +790,15 @@ pgpa_planner_apply_join_path_advice(JoinType jointype, uint64 *pgs_mask_p,
                                                                                           pjs->inner_count,
                                                                                           pjs->tkeys,
                                                                                           plan_name,
-                                                                                          entry->target,
+                                                                                          entry,
                                                                                           &restrict_method))
-                               join_ok = false;
+                               jo_deny_indexes = bms_add_member(jo_deny_indexes, i);
                        else if (restrict_method)
                        {
-                               /* XXX wrong way to report problems */
+                               jo_permit_indexes = bms_add_member(jo_permit_indexes, i);
+                               jm_indexes = bms_add_member(jo_permit_indexes, i);
                                if (pgs_mask != 0 && pgs_mask != my_pgs_mask)
-                                       elog(WARNING,
-                                                "conflicting masks: %u vs. %u", pgs_mask, my_pgs_mask);
+                                       jm_conflict = true;
                                pgs_mask = my_pgs_mask;
                        }
                        continue;
@@ -803,28 +837,62 @@ pgpa_planner_apply_join_path_advice(JoinType jointype, uint64 *pgs_mask_p,
                                                                                           pjs->inner_count,
                                                                                           pjs->tkeys,
                                                                                           plan_name,
-                                                                                          entry->target,
+                                                                                          entry,
                                                                                           &restrict_method))
-                               join_ok = false;
+                               jo_deny_indexes = bms_add_member(jo_deny_indexes, i);
                        else if (restrict_method)
                        {
+                               jo_permit_indexes = bms_add_member(jo_permit_indexes, i);
                                if (!jt_unique && !jt_non_unique)
                                {
-                                       /* XXX wrong way to report problems */
-                                       elog(WARNING, "%s not a semijoin jointype=%d",
-                                                pgpa_cstring_trove_entry(entry), jointype);
+                                       /*
+                                        * This doesn't seem to be a semijoin to which SJ_UNIQUE
+                                        * or SJ_NON_UNIQUE can be applied.
+                                        */
+                                       entry->flags |= PGPA_TE_INAPPLICABLE;
                                }
                                else if (advice_unique != jt_unique)
-                                       join_ok = false;
+                                       jo_deny_indexes = bms_add_member(jo_deny_indexes, i);
                        }
                        continue;
                }
        }
 
-       /* Apply computed pgs_mask. */
-       if (!join_ok)
+       /*
+        * If the advice indicates both that this join order is permissible and
+        * also that it isn't, then mark advice related to the join order as
+        * conflicting.
+        */
+       if (jo_permit_indexes != NULL && jo_deny_indexes != NULL)
+       {
+               pgpa_set_entry_flags(pjs->entries, jo_permit_indexes,
+                                                        PGPA_TE_CONFLICTING);
+               pgpa_set_entry_flags(pjs->entries, jo_deny_indexes,
+                                                        PGPA_TE_CONFLICTING);
+       }
+
+       /*
+        * If more than one join method specification is relevant here and they
+        * differ, mark them all as conflicting.
+        */
+       if (jm_conflict)
+               pgpa_set_entry_flags(pjs->entries, jm_indexes,
+                                                        PGPA_TE_CONFLICTING);
+
+       /*
+        * If we were advised to deny this join order, then do so. However, if we
+        * were also advised to permit it, then do nothing, since the advice
+        * conflicts.
+        */
+       if (jo_deny_indexes != NULL && jo_permit_indexes == NULL)
                *pgs_mask_p = 0;
-       else if (pgs_mask != 0)
+
+       /*
+        * If we were advised to restrict the join method, then do so. However,
+        * if we got conflicting join method advice or were also advised to reject
+        * this join order completely, then instead do nothing.
+        */
+       if (pgs_mask != 0 && !jm_conflict && jo_deny_indexes == NULL)
                *pgs_mask_p = pgs_mask;
 }
 
@@ -864,14 +932,18 @@ static bool
 pgpa_join_order_permits_join(int outer_count, int inner_count,
                                                         pgpa_trove_key *tkeys,
                                                         char *plan_name,
-                                                        pgpa_advice_target *target)
+                                                        pgpa_trove_entry *entry)
 {
        bool            loop = true;
        bool            sublist = false;
        int                     length;
        int                     outer_length;
+       pgpa_advice_target *target = entry->target;
        pgpa_advice_target *prefix_target;
 
+       /* We definitely have at least a partial match for this trove entry. */
+       entry->flags |= PGPA_TE_MATCH_PARTIAL;
+
        /*
         * Find the innermost sublist that contains all keys; if no sublist does,
         * then continue processing with the toplevel list.
@@ -971,6 +1043,16 @@ pgpa_join_order_permits_join(int outer_count, int inner_count,
 
                tkm = pgpa_trove_keys_match_target(inner_count, tkeys + outer_count,
                                                                                   plan_name, inner_target);
+
+               /*
+                * Before returning, consider whether we need to mark this entry as
+                * fully matched. If we found every item but one on the lefthand side
+                * of the join and the last item on the righthand side of the join,
+                * then the answer is yes.
+                */
+               if (outer_length + 1 == length && tkm == PGPA_TKM_EQUAL)
+                       entry->flags |= PGPA_TE_MATCH_FULL;
+
                return (tkm == PGPA_TKM_EQUAL);
        }
 
@@ -1016,13 +1098,17 @@ static bool
 pgpa_join_method_permits_join(int outer_count, int inner_count,
                                                          pgpa_trove_key *tkeys,
                                                          char *plan_name,
-                                                         pgpa_advice_target *target,
+                                                         pgpa_trove_entry *entry,
                                                          bool *restrict_method)
 {
+       pgpa_advice_target *target = entry->target;
        pgpa_tkm_type inner_tkm;
        pgpa_tkm_type outer_tkm;
        pgpa_tkm_type join_tkm;
 
+       /* We definitely have at least a partial match for this trove entry. */
+       entry->flags |= PGPA_TE_MATCH_PARTIAL;
+
        *restrict_method = false;
 
        /*
@@ -1039,6 +1125,7 @@ pgpa_join_method_permits_join(int outer_count, int inner_count,
                                                                                         target);
        if (inner_tkm == PGPA_TKM_EQUAL)
        {
+               entry->flags |= PGPA_TE_MATCH_FULL;
                *restrict_method = true;
                return true;
        }
@@ -1320,6 +1407,22 @@ pgpa_ri_checker_hash_key(pgpa_ri_checker_key key)
 
 #endif
 
+/*
+ * Set PGPA_TE_* flags on a set of trove entries.
+ */
+static void
+pgpa_set_entry_flags(pgpa_trove_entry *entries, Bitmapset *indexes, int flags)
+{
+       int                     i = -1;
+
+       while ((i = bms_next_member(indexes, i)) >= 0)
+       {
+               pgpa_trove_entry *entry = &entries[i];
+
+               entry->flags |= flags;
+       }
+}
+
 /*
  * Save the range table identifier for one relation for future cross-checking.
  */
index f177adc1706f3011244d7dcabd3b09017293b78f..da2a1a1b1ce7a7fbbf48597adc41d7e5181e403e 100644 (file)
@@ -288,6 +288,27 @@ pgpa_trove_lookup(pgpa_trove *trove, pgpa_trove_lookup_type type,
        result->indexes = indexes;
 }
 
+/*
+ * Return all entries in a trove slice to the caller.
+ *
+ * The first two arguments are input arguments, and the remainder are output
+ * arguments.
+ */
+void
+pgpa_trove_lookup_all(pgpa_trove *trove, pgpa_trove_lookup_type type,
+                                         pgpa_trove_entry **entries, int *nentries)
+{
+       pgpa_trove_slice *tslice;
+
+       if (type == PGPA_TROVE_LOOKUP_SCAN)
+               tslice = &trove->scan;
+       else
+               tslice = &trove->join;
+
+       *entries = tslice->entries;
+       *nentries = tslice->nused;
+}
+
 /*
  * Match trove keys to advice targets and return an enum value indicating
  * the relationship between the set of keys and the set of targets.
@@ -342,9 +363,18 @@ pgpa_cstring_trove_entry(pgpa_trove_entry *entry)
        StringInfoData buf;
 
        initStringInfo(&buf);
-       appendStringInfo(&buf, "%s(", pgpa_cstring_advice_tag(entry->tag));
+       appendStringInfo(&buf, "%s", pgpa_cstring_advice_tag(entry->tag));
+
+       /* JOIN_ORDER tags are transformed by pgpa_build_trove; undo that here */
+       if (entry->tag != PGPA_TAG_JOIN_ORDER)
+               appendStringInfoChar(&buf, '(');
+       else
+               Assert(entry->target->ttype == PGPA_TARGET_ORDERED_LIST);
+
        pgpa_format_advice_target(&buf, entry->target);
-       appendStringInfoChar(&buf, ')');
+
+       if (entry->tag != PGPA_TAG_JOIN_ORDER)
+               appendStringInfoChar(&buf, ')');
 
        return buf.data;
 }
@@ -372,6 +402,7 @@ pgpa_trove_add_to_slice(pgpa_trove_slice *tslice,
        entry = &tslice->entries[tslice->nused];
        entry->tag = tag;
        entry->target = target;
+       entry->flags = 0;
 
        pgpa_trove_add_to_hash(tslice->hash, target, tslice->nused);
 
index 0324f18e969d72128b649159b6d36630c30d6068..f85cb23973618cb1abe04a696a772833750d7296 100644 (file)
 
 typedef struct pgpa_trove pgpa_trove;
 
+/*
+ * Flags that can be set on a pgpa_trove_entry to indicate what happened when
+ * trying to plan using advice.
+ *
+ * PGPA_TE_MATCH_PARTIAL means that we found some part of the query that at
+ * least partially matched the target; e.g. given JOIN_ORDER(a b), this would
+ * be set if we ever saw any joinrel including either "a" or "b".
+ *
+ * PGPA_TE_MATCH_FULL means that we found an exact match for the target; e.g.
+ * given JOIN_ORDER(a b), this would be set if we saw a joinrel containing
+ * exactly "a" and "b" and nothing else.
+ *
+ * PGPA_TE_INAPPLICABLE means that the advice doesn't properly apply to the
+ * target; e.g. INDEX_SCAN(foo bar_idx) would be so marked if bar_idx does not
+ * exist on foo. The fact that this bit has been set does not mean that the
+ * advice had no effect.
+ *
+ * PGPA_TE_CONFLICTING means that a conflict was detected between what this
+ * advice wants and what some other plan advice wants; e.g. JOIN_ORDER(a b)
+ * would conflict with HASH_JOIN(a), because the former requires "a" to be the
+ * outer table while the latter requires it to be the inner table.
+ */
+#define PGPA_TE_MATCH_PARTIAL          0x0001
+#define PGPA_TE_MATCH_FULL                     0x0002
+#define PGPA_TE_INAPPLICABLE           0x0004
+#define PGPA_TE_CONFLICTING                    0x0008
+
 /*
  * Each entry in a trove of advice represents the application of a tag to
  * a single target.
@@ -27,6 +54,7 @@ typedef struct pgpa_trove_entry
 {
        pgpa_advice_tag_type tag;
        pgpa_advice_target *target;
+       int                     flags;
 } pgpa_trove_entry;
 
 /*
@@ -108,6 +136,10 @@ extern void pgpa_trove_lookup(pgpa_trove *trove,
                                                          pgpa_trove_key *tkeys,
                                                          char *plan_name,
                                                          pgpa_trove_result *result);
+extern void pgpa_trove_lookup_all(pgpa_trove *trove,
+                                                                 pgpa_trove_lookup_type type,
+                                                                 pgpa_trove_entry **entries,
+                                                                 int *nentries);
 extern pgpa_tkm_type pgpa_trove_keys_match_target(int nkeys,
                                                                                                  pgpa_trove_key *tkeys,
                                                                                                  char *plan_name,