From d46fbc50d866e7f6b864a7431a1a62f04770cf92 Mon Sep 17 00:00:00 2001 From: Henri Gasc Date: Thu, 10 Sep 2026 12:29:55 +0200 Subject: [PATCH 6/8] Correct bug (const with label disjunction) and show plan --- src/backend/commands/explain.c | 2 - src/backend/executor/nodeGraphScan.c | 34 ++++++-- src/backend/optimizer/path/allpaths.c | 10 +++ src/backend/optimizer/plan/createplan.c | 69 ++++++++++----- src/backend/rewrite/rewriteGraphTable.c | 109 ++++++++++++++++++++++++ src/include/nodes/execnodes.h | 5 +- src/include/nodes/plannodes.h | 11 ++- src/include/rewrite/rewriteGraphTable.h | 9 ++ 8 files changed, 213 insertions(+), 36 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 285ed052136..6f4ac6390ba 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -2576,7 +2576,6 @@ show_qual(List *qual, const char *qlabel, static void show_graphscan_info(GraphScan * plan, ExplainState *es) { - ExplainOpenGroup("Graph Scan", "Graph Scan", false, es); ExplainPropertyInteger("min_depth", NULL, plan->min_depth, es); ExplainPropertyInteger("max_depth", NULL, plan->max_depth, es); switch (plan->direction) @@ -2591,7 +2590,6 @@ show_graphscan_info(GraphScan * plan, ExplainState *es) ExplainPropertyText("direction", "undirected", es); break; } - ExplainCloseGroup("Graph Scan", "Graph Scan", false, es); } static void diff --git a/src/backend/executor/nodeGraphScan.c b/src/backend/executor/nodeGraphScan.c index e8a2faf9a5e..e0f605d3e4d 100644 --- a/src/backend/executor/nodeGraphScan.c +++ b/src/backend/executor/nodeGraphScan.c @@ -6,7 +6,7 @@ * A GraphScan evaluates one quantified (variable-length) hop of a graph * pattern with a depth-first search. The scan is a parameterized inner of * its enclosing join: each outer row provides a seed vertex (as nestloop - * params, see GraphScan.seed_param_ids). From that seed the executor walks + * params, see GraphScan.seed_params). From that seed the executor walks * the edge elements of the hop, one depth level at a time, by driving * per-depth copies of the planned 1-hop expansion (the inner plan, a UNION * ALL of the matching edge element tables). @@ -169,11 +169,31 @@ graph_fetch_seed(GraphScanState * node, GraphScan * plan) fr->vid_nkeys = list_length(node->seed_params); foreach(lc, node->seed_params) { - ParamExecData *prm = &estate->es_param_exec_vals[lfirst_int(lc)]; + Node *item = (Node *) lfirst(lc); + Datum value; + bool isnull; - fr->vid[k] = prm->value; - fr->vidnull[k] = prm->isnull; - if (prm->isnull) + if (IsA(item, Param)) + { + /* bound by the enclosing nestloop (see GraphScan.seed_params) */ + ParamExecData *prm = + &estate->es_param_exec_vals[((Param *) item)->paramid]; + + value = prm->value; + isnull = prm->isnull; + } + else + { + /* constant-bound seed column (see GraphScan.seed_params) */ + Const *con = castNode(Const, item); + + value = con->constvalue; + isnull = con->constisnull; + } + + fr->vid[k] = value; + fr->vidnull[k] = isnull; + if (isnull) hasnull = true; k++; } @@ -665,7 +685,7 @@ ExecInitGraphScan(GraphScan * node, EState *estate, int eflags) scanstate->nprops = list_length(node->edge_list_cols); scanstate->max_nsrc = node->max_nsrc; scanstate->max_ndst = node->max_ndst; - scanstate->seed_params = node->seed_param_ids; + scanstate->seed_params = node->seed_params; scanstate->narms = list_length(node->edge_element_oids); scanstate->cur_depth = -1; scanstate->need_seed = true; @@ -699,7 +719,7 @@ ExecInitGraphScan(GraphScan * node, EState *estate, int eflags) * Build the depth frames: every frame owns a copy of the inner (1-hop) * expansion plan so that each frame's scan cursor is independent. */ - maxwidth = Max(list_length(node->seed_param_ids), + maxwidth = Max(list_length(node->seed_params), Max(node->max_nsrc, node->max_ndst)); scanstate->tmp_vid = palloc(sizeof(Datum) * Max(maxwidth, 1)); scanstate->tmp_vidnull = palloc(sizeof(bool) * Max(maxwidth, 1)); diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 6e7d42a863d..7e4c72af537 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -3229,6 +3229,13 @@ set_graph_pathlist(PlannerInfo *root, RelOptInfo *rel, else subquery = copyObject(decomposeGraphTable(rte)); + /* + * The decomposed query is planned without passing through the rewriter, + * which is where RLS policies are normally attached; apply them here so + * row-level security on the backing element tables is enforced. + */ + native_apply_rls_to_query(subquery); + /* * If the pattern or its COLUMNS reference outer relations (lateral), the * graph table must be treated as parameterized even though it is not @@ -3650,6 +3657,9 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, (Node *) makeBoolExpr(AND_EXPR, quals, -1); } + /* Edge element tables can carry RLS policies. */ + native_apply_rls_to_query(arm); + arm_queries = lappend(arm_queries, arm); ai++; } diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 6286507a188..1d13fe6c8a8 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -3656,47 +3656,56 @@ create_graphscan_plan(PlannerInfo *root, GraphPath * best_path, } /* - * Identify the nestloop params that supply the current seed key values. - * The ghost seed element's WHERE clause is a conjunction of equalities + * Identify the values that supply the current seed key columns. The + * ghost seed element's WHERE clause is a conjunction of equalities * "gs_seed_attr = seed_key"; after replace_nestloop_params() the seed key - * side is a PARAM_EXEC that the enclosing nestloop fills from the outer - * row. Record those param ids, in key column order, so the executor can - * read the seed vertex for every outer row. + * side is usually a PARAM_EXEC that the enclosing nestloop fills from the + * outer row. When the planner can prove the seed relation is a single + * row (e.g. a constant equality on its primary key), it instead derives + * the constant directly on the scan, so the equality is "gs_seed_attr = + * ". Record, in key column order, one Param or Const node per + * seed key column so the executor can seed the traversal. */ { ListCell *lc2; List *seed_params = NIL; for (int i = 0; i < list_length(best_path->seed_key_cols); i++) - seed_params = lappend_int(seed_params, -1); + seed_params = lappend(seed_params, NULL); foreach(lc2, scan_clauses) { OpExpr *op = (OpExpr *) lfirst(lc2); Var *var = NULL; - Param *param = NULL; + Node *other = NULL; + bool is_param; + bool is_const; int pos = -1; int amp = 0; if (!IsA(op, OpExpr) || list_length(op->args) != 2) continue; - if (IsA(linitial(op->args), Var) && - IsA(lsecond(op->args), Param)) + if (IsA(linitial(op->args), Var)) { var = linitial_node(Var, op->args); - param = lsecond_node(Param, op->args); + other = lsecond(op->args); } - else if (IsA(linitial(op->args), Param) && - IsA(lsecond(op->args), Var)) + else if (IsA(lsecond(op->args), Var)) { - param = linitial_node(Param, op->args); var = lsecond_node(Var, op->args); + other = linitial(op->args); } else continue; - if (var->varno != scan_relid || var->varlevelsup != 0 || - param->paramkind != PARAM_EXEC) + if (var->varno != scan_relid || var->varlevelsup != 0) + continue; + + is_param = (IsA(other, Param) && + ((Param *) other)->paramkind == PARAM_EXEC); + is_const = IsA(other, Const); + + if (!is_param && !is_const) continue; foreach_int(att, best_path->seed_key_cols) @@ -3708,15 +3717,31 @@ create_graphscan_plan(PlannerInfo *root, GraphPath * best_path, } amp++; } - if (pos >= 0) - lfirst_int(list_nth_cell(seed_params, pos)) = param->paramid; + if (pos < 0) + continue; + + /* + * A parameter for a column overrides any previously seen clause + * for it; a constant only fills a column not yet supplied. + */ + if (is_param) + lfirst(list_nth_cell(seed_params, pos)) = other; + else if (lfirst(list_nth_cell(seed_params, pos)) == NULL) + lfirst(list_nth_cell(seed_params, pos)) = copyObject(other); } - if (list_length(seed_params) != - list_length(best_path->seed_key_cols) || - list_member_int(seed_params, -1)) - elog(ERROR, "could not identify graph scan seed parameters"); - scan_plan->seed_param_ids = seed_params; + /* + * Every seed key column must be supplied either by a nestloop param + * or by a constant; a column with neither is a planner/rewriter + * disagreement we must not ignore. + */ + foreach(lc2, seed_params) + { + if (lfirst(lc2) == NULL) + elog(ERROR, + "could not identify graph scan seed parameters"); + } + scan_plan->seed_params = seed_params; } scan_plan->scan.plan.qual = scan_clauses; diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c index 025058943a1..e503f5e7ba5 100644 --- a/src/backend/rewrite/rewriteGraphTable.c +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -40,6 +40,8 @@ #include "rewrite/rewriteGraphTable.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" +#include "rewrite/rowsecurity.h" +#include "storage/lmgr.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -1545,6 +1547,110 @@ get_vle_array_props(RangeTblEntry *rte, const char *edge_var) return result; } +/* + * Walker acquiring locks on the relations referenced by sublinks found in + * RLS policy quals. Mirrors acquireLocksOnSubLinks() in rewriteHandler.c: + * policy quals are added post-parsing, so the relations they reference must + * be locked here rather than by the parser. + */ +static bool +native_rls_lock_sublinks(Node *node, void *context) +{ + if (node == NULL) + return false; + + if (IsA(node, Query)) + { + Query *subquery = (Query *) node; + ListCell *lc; + + foreach(lc, subquery->rtable) + { + RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); + + if (rte->rtekind == RTE_RELATION) + LockRelationOid(rte->relid, AccessShareLock); + } + + return query_tree_walker(subquery, native_rls_lock_sublinks, context, + QTW_IGNORE_RC_SUBQUERIES); + } + + return expression_tree_walker(node, native_rls_lock_sublinks, context); +} + +/* + * Apply row-level security policies to the backing relation RTEs of an + * internally built query. + * + * The rewriter's fireRIRrules() performs this step for parsed queries, but + * the decomposed internal queries are built inside the planner and never + * pass through the rewriter, so without this their RTEs would carry no + * securityQuals and RLS would be silently bypassed. Mirrors the RLS loop + * of fireRIRrules(), recursing into join subqueries (branch queries are + * wrapped as subquery RTEs of the UNION). + */ +void +native_apply_rls_to_query(Query *query) +{ + int rt_index = 0; + ListCell *lc; + + foreach(lc, query->rtable) + { + RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); + List *securityQuals = NIL; + List *withCheckOptions = NIL; + bool hasRowSecurity = false; + bool hasSubLinks = false; + + rt_index++; + + /* Recurse into wrapped branch (or other subquery) queries. */ + if (rte->rtekind == RTE_SUBQUERY) + { + native_apply_rls_to_query(rte->subquery); + continue; + } + + /* Only plain relations can have RLS policies. */ + if (rte->rtekind != RTE_RELATION || + (rte->relkind != RELKIND_RELATION && + rte->relkind != RELKIND_PARTITIONED_TABLE)) + continue; + + get_row_security_policies(query, rte, rt_index, + &securityQuals, &withCheckOptions, + &hasRowSecurity, &hasSubLinks); + + if (securityQuals != NIL) + { + if (hasSubLinks) + { + /* Lock relations referenced by the policy quals. */ + (void) native_rls_lock_sublinks((Node *) securityQuals, NULL); + } + + /* + * Add the new security barrier quals ahead of any pre-existing + * security quals, exactly as fireRIRrules() does. + */ + rte->securityQuals = list_concat(securityQuals, + rte->securityQuals); + } + + /* + * The decomposed queries are SELECT-only, so no WITH CHECK + * OPTIONS can apply; hasRowSecurity still matters for the + * plancache (dependsOnRLS). + */ + if (hasRowSecurity) + query->hasRowSecurity = true; + if (hasSubLinks) + query->hasSubLinks = true; + } +} + /* * Build, for one branch, the internal RTE_GRAPH_TABLE representing the * quantified (variable-length) hop described by vf, with concrete ghost @@ -2042,6 +2148,9 @@ native_query_for_branch(native_decomp * dc, List *elems, List *vles) var->varattno - FirstLowInvalidHeapAttributeNumber); } + /* Backing element tables can carry RLS policies: handled by the + * native planner in set_graph_pathlist(), before subquery_planner. */ + return path_query; } diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index b6f6e342ed2..4f972a62f87 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1987,7 +1987,10 @@ typedef struct GraphScanState int narms; struct GraphScanArmData *arms; - /* PARAM_EXEC ids of the seed key columns (List of int), in key order. */ + /* + * Seed key values of the current seed column (List of Param or Const, + * one per seed key column, in key order); see GraphScan.seed_params. + */ List *seed_params; /* diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index ed2c6726813..90be4a9c47a 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -625,11 +625,14 @@ typedef struct GraphScan Oid seed_elem_oid; /* - * PARAM_EXEC ids (List of int) of the seed key columns, in key order. The - * enclosing nestloop fills them from the outer row; the executor reads - * them to obtain the seed vertex for the traversal. + * Seed key values (List, one entry per seed key column, in key order). + * Each entry is either a Param (PARAM_EXEC) that the enclosing nestloop + * fills from the outer row, or -- when the planner can prove the seed + * relation is a single row (e.g. a constant equality on its primary key) + * -- a Const substituted directly on the scan. The executor seeds the + * traversal from these values. */ - List *seed_param_ids; + List *seed_params; /* Hop-wide max src/dest key widths over the edge element arms. */ int max_nsrc; diff --git a/src/include/rewrite/rewriteGraphTable.h b/src/include/rewrite/rewriteGraphTable.h index bef9271a927..345dcb68118 100644 --- a/src/include/rewrite/rewriteGraphTable.h +++ b/src/include/rewrite/rewriteGraphTable.h @@ -85,4 +85,13 @@ extern Oid key_equality_operator(Oid typid); */ extern List *get_vle_array_props(RangeTblEntry *rte, const char *edge_var); +/* + * Apply row-level security policies to the backing relation RTEs of an + * internally built query. The rewriter's fireRIRrules() does this for + * parsed queries; the decomposed internal query never passes through the + * rewriter, so its RTEs would otherwise carry no securityQuals and RLS + * would be silently bypassed. + */ +extern void native_apply_rls_to_query(Query *query); + #endif /* REWRITEGRAPHTABLE_H */ -- 2.39.2