From f9856225304e8a42334fbff6931ee6e3e0c74075 Mon Sep 17 00:00:00 2001 From: Henri Gasc Date: Fri, 11 Sep 2026 09:01:58 +0200 Subject: [PATCH 7/8] Add rows estimate to planner and use IndexScan if possible --- src/backend/executor/nodeGraphScan.c | 104 +++- src/backend/optimizer/path/allpaths.c | 677 ++++++++++++++++++++---- src/backend/optimizer/plan/createplan.c | 1 + src/backend/optimizer/plan/subselect.c | 16 + src/backend/rewrite/rewriteGraphTable.c | 46 +- src/backend/utils/adt/ruleutils.c | 5 +- src/include/executor/nodeGraphScan.h | 10 + src/include/nodes/execnodes.h | 19 +- src/include/nodes/pathnodes.h | 6 + src/include/nodes/plannodes.h | 10 + src/include/rewrite/rewriteGraphTable.h | 4 +- 11 files changed, 772 insertions(+), 126 deletions(-) diff --git a/src/backend/executor/nodeGraphScan.c b/src/backend/executor/nodeGraphScan.c index e0f605d3e4d..79da8915e91 100644 --- a/src/backend/executor/nodeGraphScan.c +++ b/src/backend/executor/nodeGraphScan.c @@ -53,6 +53,10 @@ static TupleTableSlot *ExecGraphScan(PlanState *pstate); static void build_arms(GraphScanState * node, GraphScan * plan); static void build_arm_keys(List *keys, int *nkeys, FmgrInfo **eq, Oid **colls); static bool graph_fetch_seed(GraphScanState * node, GraphScan * plan); +static void graph_bind_side(GraphScanState * node, GraphDepthFrameData * fr, + bool active, int first_slot, int nslots); +static void graph_bind_vertex_params(GraphScanState * node, + GraphDepthFrameData * fr); static bool graph_next(GraphScanState * node, GraphScan * plan); static bool graph_step(GraphScanState * node, GraphScan * plan, GraphDepthFrameData * fr); @@ -70,7 +74,7 @@ static void graph_push(GraphScanState * node, Oid newelem, int newnkeys, static void graph_backtrack(GraphScanState * node); static void graph_reset(GraphScanState * node); static void graph_build_row(GraphScanState * node, TupleTableSlot *slot); -static TupleTableSlot *graph_emit_row(GraphScanState *node, TupleTableSlot *slot); +static TupleTableSlot *graph_emit_row(GraphScanState * node, TupleTableSlot *slot); static Datum graph_build_edge_array(GraphScanState * node, TupleTableSlot *slot, int pi); @@ -160,10 +164,14 @@ graph_fetch_seed(GraphScanState * node, GraphScan * plan) graph_reset(node); - /* restart every depth's inner scan for the new seed */ + /* + * Every depth frame must (re)start its inner scan for the current vertex + * of the new traversal (see GraphDepthFrameData.need_init); the (re)scan + * happens lazily in graph_step, when the current-vertex parameters are + * bound. + */ for (int d = 0; d < node->ndepths; d++) - if (node->frames[d].inner_state != NULL) - ExecReScan(node->frames[d].inner_state); + node->frames[d].need_init = true; fr->vid_elem = node->seed_elem; fr->vid_nkeys = list_length(node->seed_params); @@ -207,6 +215,57 @@ graph_fetch_seed(GraphScanState * node, GraphScan * plan) return true; } +/* + * Bind one side -- source (forward) or destination (reverse) -- of the + * current (innermost frame's) vertex key values into the PARAM_EXEC slots + * that parameterize the inner 1-hop arm scans. Slots beyond the current + * vertex's key width, and the whole slot range of an inactive direction, + * are bound to NULL: "key = NULL" matches no rows, so the corresponding arm + * variants produce nothing. + */ +static void +graph_bind_side(GraphScanState * node, GraphDepthFrameData * fr, + bool active, int first_slot, int nslots) +{ + EState *estate = node->ss.ps.state; + + for (int k = 0; k < nslots; k++) + { + ParamExecData *prm = + &estate->es_param_exec_vals[lfirst_int(list_nth_cell(node->vertex_params, + first_slot + k))]; + + if (active && k < fr->vid_nkeys && !fr->vidnull[k]) + { + prm->value = fr->vid[k]; + prm->isnull = false; + } + else + { + prm->value = (Datum) 0; + prm->isnull = true; + } + } +} + +/* + * Bind the current (innermost frame's) vertex key values into the PARAM_EXEC + * slots that parameterize the inner 1-hop arm scans. The forward (source + * key) parameters are filled when the scan traverses out of the vertex's + * source side (outgoing/undirected); the reverse (destination key) + * parameters when it traverses in (incoming/undirected). + */ +static void +graph_bind_vertex_params(GraphScanState * node, GraphDepthFrameData * fr) +{ + if (node->vertex_params == NIL) + return; + + /* forward (source key) slots come first, then reverse (dest key) slots */ + graph_bind_side(node, fr, node->fwd_active, 0, node->max_nsrc); + graph_bind_side(node, fr, node->rev_active, node->max_nsrc, node->max_ndst); +} + /* * Try to advance the traversal one edge from the current (innermost) frame, * backtracking when a frame is exhausted. Returns false when the current @@ -255,7 +314,7 @@ graph_step(GraphScanState * node, GraphScan * plan, CHECK_FOR_INTERRUPTS(); - if (fr->inner_state == NULL) + if (plan->inner_plan == NULL) return false; /* @@ -266,6 +325,26 @@ graph_step(GraphScanState * node, GraphScan * plan, if (node->cur_depth >= node->max_depth) return false; + /* + * The inner arm scans are parameterized on the current vertex; bind it + * (the frame's vertex) before pulling any rows. Parameterized index + * scans only re-evaluate their scan keys when (re)started, so a frame + * whose vertex was (re)set (a fresh push or a new seed) must have its + * inner scan (re)initialized or rescanned now, first and only time it is + * stepped for that vertex. + */ + graph_bind_vertex_params(node, fr); + if (fr->need_init) + { + if (fr->inner_state == NULL) + fr->inner_state = + ExecInitNode(copyObject(plan->inner_plan), + node->ss.ps.state, node->eflags); + else + ExecReScan(fr->inner_state); + fr->need_init = false; + } + for (;;) { int res; @@ -477,6 +556,8 @@ graph_push(GraphScanState * node, Oid newelem, int newnkeys, memcpy(nfr->edge_props, eprops, sizeof(Datum) * node->nprops); memcpy(nfr->edge_propsnull, epropsnull, sizeof(bool) * node->nprops); } + /* the new vertex's inner scan must (re)start (see graph_step) */ + nfr->need_init = true; node->cur_depth = d; } @@ -604,7 +685,7 @@ graph_build_row(GraphScanState * node, TupleTableSlot *slot) * the row failed the qual (the caller must keep traversing). */ static TupleTableSlot * -graph_emit_row(GraphScanState *node, TupleTableSlot *slot) +graph_emit_row(GraphScanState * node, TupleTableSlot *slot) { graph_build_row(node, slot); node->ss.ps.ps_ExprContext->ecxt_scantuple = slot; @@ -690,6 +771,10 @@ ExecInitGraphScan(GraphScan * node, EState *estate, int eflags) scanstate->cur_depth = -1; scanstate->need_seed = true; scanstate->seed_emitted = false; + scanstate->vertex_params = node->vertex_param_ids; + scanstate->fwd_active = (node->direction != GRAPH_DIR_INCOMING); + scanstate->rev_active = (node->direction != GRAPH_DIR_OUTGOING); + scanstate->eflags = eflags; /* * Effective maximum depth. Explicit bounds are honored; unbounded (or @@ -735,11 +820,18 @@ ExecInitGraphScan(GraphScan * node, EState *estate, int eflags) fr->edge_props = palloc(sizeof(Datum) * Max(scanstate->nprops, 1)); fr->edge_propsnull = palloc(sizeof(bool) * Max(scanstate->nprops, 1)); + /* + * Initialize the inner (1-hop) expansion eagerly (so EXPLAIN can + * display it); mark the frame for a re-started scan (need_init) so + * the inner index scans pick up the current-vertex parameters, which + * are bound later, at the frame's first step. + */ if (node->inner_plan != NULL) fr->inner_state = ExecInitNode(copyObject(node->inner_plan), estate, eflags); else fr->inner_state = NULL; + fr->need_init = true; } /* diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 7e4c72af537..c81fee8da7b 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -27,6 +27,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_propgraph_element.h" +#include "catalog/pg_statistic.h" #include "catalog/pg_type.h" #include "foreign/fdwapi.h" #include "miscadmin.h" @@ -41,6 +42,7 @@ #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "optimizer/paramassign.h" #include "optimizer/plancat.h" #include "optimizer/planner.h" #include "optimizer/prep.h" @@ -55,8 +57,10 @@ #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" #include "utils/array.h" +#include "utils/builtins.h" #include "utils/selfuncs.h" #include "utils/syscache.h" +#include "utils/typcache.h" /* Bitmask flags for pushdown_safety_info.unsafeFlags */ @@ -3381,15 +3385,79 @@ resolve_edge_where_mutator(Node *node, Oid *elemoid) typedef struct GraphHopArmKeys { Oid arm_relid; /* edge element table */ - int nsrc; /* source key width */ - AttrNumber *srckey; /* edge attnums (pgesrckey) */ - int ndst; /* destination key width */ - AttrNumber *dstkey; /* edge attnums (pgedestkey) */ + Oid srcvertex; /* source vertex element */ + Oid dstvertex; /* destination vertex element */ + List *srckeys; /* source key columns (GraphElementKeyCol) */ + List *dstkeys; /* destination key columns + * (GraphElementKeyCol) */ } GraphHopArmKeys; +/* + * Build the equality clause "rel1.attno = ", where the + * right-hand side is the given PARAM_EXEC. The operator is the datatype's + * default equality, the same one the GraphScan executor uses to validate + * candidate edges, so the pushed-down filter has exactly the executor's + * semantics. Used to parameterize the inner (1-hop) arm scans on the + * current vertex, letting the planner choose index scans on the src/dst key + * columns. + */ +static Node * +make_graph_key_param_clause(AttrNumber attno, Oid atttype, int32 atttypmod, + Oid attcoll, int paramid) +{ + Oid eqop; + Var *var; + Param *param; + + eqop = key_equality_operator(atttype); + + var = makeVar(1, attno, atttype, atttypmod, attcoll, 0); + param = makeNode(Param); + param->paramkind = PARAM_EXEC; + param->paramid = paramid; + param->paramtype = atttype; + param->paramtypmod = atttypmod; + param->paramcollid = attcoll; + param->location = -1; + + return (Node *) make_opclause(eqop, BOOLOID, false, + (Expr *) var, (Expr *) param, + InvalidOid, attcoll); +} + +/* + * Build the equality clause "rel1.attno1 = rel1.attno2" for two key columns + * of the same datatype (the datatype's default equality operator), used to + * detect self-loop edges in the reverse variant of an undirected hop. + */ +static Node * +make_graph_key_eq_clause(AttrNumber attno1, AttrNumber attno2, + Oid atttype, int32 atttypmod, Oid attcoll) +{ + Oid eqop; + Var *var1; + Var *var2; + + eqop = key_equality_operator(atttype); + + var1 = makeVar(1, attno1, atttype, atttypmod, attcoll, 0); + var2 = makeVar(1, attno2, atttype, atttypmod, attcoll, 0); + + return (Node *) make_opclause(eqop, BOOLOID, false, + (Expr *) var1, (Expr *) var2, + InvalidOid, attcoll); +} + /* * Build the Query for the GraphScan's inner 1-hop expansion over the given - * edge element tables: a UNION ALL of per-table SELECTs. Each arm outputs, + * edge element tables: a UNION ALL of per-table SELECTs. Each edge element + * contributes two SELECTs, a "forward" variant filtered on the element's + * source key columns and a "reverse" variant filtered on its destination key + * columns; both filter against PARAM_EXEC parameters bound, at execution, + * to the current vertex's key values (the executor leaves the inactive + * direction's parameters NULL, making its variants produce no rows). Pushing + * these filters down lets the planner choose index scans on the src/dst key + * columns instead of always scanning whole edge tables. Each arm outputs, * for every traversed edge: * * one column per VLE edge-list property (the edge's property value, @@ -3404,17 +3472,22 @@ typedef struct GraphHopArmKeys * interpret the padded layout. All arms must agree on the datatype of each * (padded) key slot, since the UNION result has one type per column. * + * The PARAM_EXEC nodes allocated for the current-vertex key values are + * returned in *vertex_params, ordered [forward src-key slots (max_nsrc), + * reverse dst-key slots (max_ndst)]. + * * The returned Query is what we plan through a nested subquery_planner() to * obtain the parameterized 1-hop subplan (righttree). */ static Query * -build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, +build_graphscan_inner_query(PlannerInfo *root, Oid graphid, + GraphElementPattern *edge_gep, List *edge_element_oids, List *array_props, - int *max_nsrc, int *max_ndst) + int *max_nsrc, int *max_ndst, + List **vertex_params) { List *arm_queries = NIL; List *arm_keys = NIL; - int nprops = list_length(array_props); ListCell *lc; *max_nsrc = 0; @@ -3430,9 +3503,6 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, HeapTuple etup; Form_pg_propgraph_element pge; GraphHopArmKeys *ak; - Datum datum; - Datum *d; - int n; etup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(elemoid)); if (!HeapTupleIsValid(etup)) @@ -3442,29 +3512,18 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, ak = palloc_object(GraphHopArmKeys); ak->arm_relid = pge->pgerelid; - - datum = SysCacheGetAttrNotNull(PROPGRAPHELOID, etup, - Anum_pg_propgraph_element_pgesrckey); - deconstruct_array_builtin(DatumGetArrayTypeP(datum), INT2OID, - &d, NULL, &n); - ak->nsrc = n; - ak->srckey = palloc_array(AttrNumber, Max(n, 1)); - for (int i = 0; i < n; i++) - ak->srckey[i] = DatumGetInt16(d[i]); - - datum = SysCacheGetAttrNotNull(PROPGRAPHELOID, etup, - Anum_pg_propgraph_element_pgedestkey); - deconstruct_array_builtin(DatumGetArrayTypeP(datum), INT2OID, - &d, NULL, &n); - ak->ndst = n; - ak->dstkey = palloc_array(AttrNumber, Max(n, 1)); - for (int i = 0; i < n; i++) - ak->dstkey[i] = DatumGetInt16(d[i]); + ak->srcvertex = pge->pgesrcvertexid; + ak->dstvertex = pge->pgedestvertexid; ReleaseSysCache(etup); - *max_nsrc = Max(*max_nsrc, ak->nsrc); - *max_ndst = Max(*max_ndst, ak->ndst); + ak->srckeys = get_graph_element_key_columns(elemoid, + Anum_pg_propgraph_element_pgesrckey); + ak->dstkeys = get_graph_element_key_columns(elemoid, + Anum_pg_propgraph_element_pgedestkey); + + *max_nsrc = Max(*max_nsrc, list_length(ak->srckeys)); + *max_ndst = Max(*max_ndst, list_length(ak->dstkeys)); arm_keys = lappend(arm_keys, ak); } @@ -3486,90 +3545,117 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, foreach_ptr(GraphHopArmKeys, ak, arm_keys) { - for (int k = 0; k < ak->nsrc; k++) - { - Oid typid; - int32 typmod; - Oid coll; + int k = 0; - get_atttypetypmodcoll(ak->arm_relid, ak->srckey[k], - &typid, &typmod, &coll); + foreach_ptr(GraphElementKeyCol, kc, ak->srckeys) + { if (src_types[k] == InvalidOid) { - src_types[k] = typid; - src_typmods[k] = typmod; - src_colls[k] = coll; + src_types[k] = kc->typid; + src_typmods[k] = kc->typmod; + src_colls[k] = kc->collation; } - else if (src_types[k] != typid) + else if (src_types[k] != kc->typid) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("graph hop source key column %d has different datatypes across edge elements", k + 1))); + k++; } - for (int k = 0; k < ak->ndst; k++) - { - Oid typid; - int32 typmod; - Oid coll; - get_atttypetypmodcoll(ak->arm_relid, ak->dstkey[k], - &typid, &typmod, &coll); + k = 0; + foreach_ptr(GraphElementKeyCol, kc, ak->dstkeys) + { if (dst_types[k] == InvalidOid) { - dst_types[k] = typid; - dst_typmods[k] = typmod; - dst_colls[k] = coll; + dst_types[k] = kc->typid; + dst_typmods[k] = kc->typmod; + dst_colls[k] = kc->collation; } - else if (dst_types[k] != typid) + else if (dst_types[k] != kc->typid) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("graph hop destination key column %d has different datatypes across edge elements", k + 1))); + k++; } - ai++; } - /* Build one Query per edge element (arm). */ + /* + * Allocate the PARAM_EXEC params the forward/reverse variants filter + * against: one per source key slot, then one per destination key + * slot. They are registered before the inner query is planned, so + * the planner sees them as ordinary (indexable) parameters; the + * executor binds them to the current vertex before each fetch. + */ + *vertex_params = NIL; + for (int k = 0; k < *max_nsrc; k++) + *vertex_params = + lappend(*vertex_params, + generate_new_exec_param(root, src_types[k], + src_typmods[k], + src_colls[k])); + for (int k = 0; k < *max_ndst; k++) + *vertex_params = + lappend(*vertex_params, + generate_new_exec_param(root, dst_types[k], + dst_typmods[k], + dst_colls[k])); + + /* + * Build one Query per edge element (arm): a "forward" variant whose + * source key columns must equal the current vertex, and a "reverse" + * variant whose destination key columns must. Both are UNIONed into + * the single inner expansion; the executor keeps only the variants + * matching its direction active (the inactive direction's parameters + * are NULL). + */ ai = 0; foreach_ptr(GraphHopArmKeys, ak, arm_keys) { - Query *arm = makeNode(Query); + Query *skeleton = makeNode(Query); Relation rel; ParseNamespaceItem *pni; List *tlist = NIL; - List *quals = NIL; + List *edgequals = NIL; int resno = 0; + Query *fwd; + Query *rev; - arm->commandType = CMD_SELECT; + skeleton->commandType = CMD_SELECT; rel = table_open(ak->arm_relid, AccessShareLock); pni = addRangeTableEntryForRelation(make_parsestate(NULL), rel, AccessShareLock, NULL, true, false); table_close(rel, NoLock); - arm->rtable = lappend(arm->rtable, pni->p_rte); - arm->rteperminfos = lappend(arm->rteperminfos, pni->p_perminfo); - pni->p_rte->perminfoindex = list_length(arm->rteperminfos); + skeleton->rtable = lappend(skeleton->rtable, pni->p_rte); + skeleton->rteperminfos = lappend(skeleton->rteperminfos, + pni->p_perminfo); + pni->p_rte->perminfoindex = list_length(skeleton->rteperminfos); { RangeTblRef *rtr = makeNode(RangeTblRef); rtr->rtindex = 1; - arm->jointree = makeFromExpr(list_make1(rtr), NULL); + skeleton->jointree = makeFromExpr(list_make1(rtr), NULL); } /* Property value columns for the VLE edge-list refs. */ - foreach_ptr(GraphPropertyRef, gpr, array_props) { Oid elemoid = lfirst_oid(list_nth_cell(edge_element_oids, ai)); - Node *n; - resno++; - n = get_element_property_expr(elemoid, gpr->propid, 1); - if (!n) - n = (Node *) makeNullConst(gpr->typeId, - gpr->typmod, - gpr->collation); - tlist = lappend(tlist, - makeTargetEntry((Expr *) n, resno, - psprintf("gep%d", resno), false)); + foreach_ptr(GraphPropertyRef, gpr, array_props) + { + Node *n; + + resno++; + n = get_element_property_expr(elemoid, gpr->propid, 1); + if (!n) + n = (Node *) makeNullConst(gpr->typeId, + gpr->typmod, + gpr->collation); + tlist = lappend(tlist, + makeTargetEntry((Expr *) n, resno, + psprintf("gep%d", resno), false)); + } } /* Edge row identity: ctid. */ @@ -3596,16 +3682,13 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, Node *n; resno++; - if (k < ak->nsrc) + if (k < list_length(ak->srckeys)) { - Oid typid; - int32 typmod; - Oid coll; - - get_atttypetypmodcoll(ak->arm_relid, ak->srckey[k], - &typid, &typmod, &coll); - n = (Node *) makeVar(1, ak->srckey[k], - typid, typmod, coll, 0); + GraphElementKeyCol *kc = list_nth(ak->srckeys, k); + + n = (Node *) makeVar(1, kc->attnum, + kc->typid, kc->typmod, + kc->collation, 0); } else n = (Node *) makeNullConst(src_types[k], src_typmods[k], @@ -3622,16 +3705,13 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, Node *n; resno++; - if (k < ak->ndst) + if (k < list_length(ak->dstkeys)) { - Oid typid; - int32 typmod; - Oid coll; - - get_atttypetypmodcoll(ak->arm_relid, ak->dstkey[k], - &typid, &typmod, &coll); - n = (Node *) makeVar(1, ak->dstkey[k], - typid, typmod, coll, 0); + GraphElementKeyCol *kc = list_nth(ak->dstkeys, k); + + n = (Node *) makeVar(1, kc->attnum, + kc->typid, kc->typmod, + kc->collation, 0); } else n = (Node *) makeNullConst(dst_types[k], dst_typmods[k], @@ -3642,7 +3722,7 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, false)); } - arm->targetList = tlist; + skeleton->targetList = tlist; /* The edge's own WHERE, resolved against this edge element. */ if (edge_gep->whereClause) @@ -3651,16 +3731,101 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, Node *w = copyObject(edge_gep->whereClause); IncrementVarSublevelsUp(w, 1, 1); - quals = lappend(quals, - resolve_edge_where_mutator(w, &elemoid)); - ((FromExpr *) arm->jointree)->quals = - (Node *) makeBoolExpr(AND_EXPR, quals, -1); + edgequals = lappend(edgequals, + resolve_edge_where_mutator(w, &elemoid)); + } + + /* + * Forward variant: the edge's source keys must equal the current + * vertex's (PARAM_EXEC slots 0..*max_nsrc-1). + */ + fwd = copyObject(skeleton); + { + List *fquals = list_copy(edgequals); + int k = 0; + + foreach_ptr(GraphElementKeyCol, kc, ak->srckeys) + { + int paramid = + ((Param *) list_nth(*vertex_params, k))->paramid; + + fquals = lappend(fquals, + make_graph_key_param_clause(kc->attnum, + kc->typid, + kc->typmod, + kc->collation, + paramid)); + k++; + } + ((FromExpr *) fwd->jointree)->quals = + (Node *) makeBoolExpr(AND_EXPR, fquals, -1); } + native_apply_rls_to_query(fwd); + arm_queries = lappend(arm_queries, fwd); + + /* + * Reverse variant: the edge's destination keys must equal the + * current vertex's (PARAM_EXEC slots *max_nsrc..). For an + * undirected hop, skip self-loops here (the forward variant + * already yields them), else they would be traversed twice; and + * skip the deduplication for directed hops, where the reverse + * variant must still traverse self-loops. + */ + rev = copyObject(skeleton); + { + List *rquals = list_copy(edgequals); + int k = 0; + + foreach_ptr(GraphElementKeyCol, kc, ak->dstkeys) + { + int paramid = + ((Param *) list_nth(*vertex_params, + *max_nsrc + k))->paramid; + + rquals = lappend(rquals, + make_graph_key_param_clause(kc->attnum, + kc->typid, + kc->typmod, + kc->collation, + paramid)); + k++; + } - /* Edge element tables can carry RLS policies. */ - native_apply_rls_to_query(arm); + if (edge_gep->kind == EDGE_PATTERN_ANY && + ak->srcvertex == ak->dstvertex) + { + List *selfeq = NIL; + ListCell *lc2s, + *lc2d; + + forboth(lc2s, ak->srckeys, lc2d, ak->dstkeys) + { + GraphElementKeyCol *kc = + (GraphElementKeyCol *) lfirst(lc2s); + GraphElementKeyCol *dc = + (GraphElementKeyCol *) lfirst(lc2d); + + selfeq = lappend(selfeq, + make_graph_key_eq_clause(kc->attnum, + dc->attnum, + kc->typid, + kc->typmod, + kc->collation)); + } + rquals = lappend(rquals, + makeBoolExpr(NOT_EXPR, + list_make1(makeBoolExpr(AND_EXPR, + selfeq, + -1)), + -1)); + } + + ((FromExpr *) rev->jointree)->quals = + (Node *) makeBoolExpr(AND_EXPR, rquals, -1); + } + native_apply_rls_to_query(rev); + arm_queries = lappend(arm_queries, rev); - arm_queries = lappend(arm_queries, arm); ai++; } } @@ -3757,6 +3922,14 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, /* * Build the GraphScan's inner (1-hop) expansion plan (the righttree). * Returns the Plan and stores its PlannerInfo into *inner_rootp. + * + * The current-vertex PARAM_EXEC nodes used by the inner (arm) plans are + * returned in *vertex_params. Because the arm scans live in the set-operation + * branch subqueries of the inner query, they only see parameters whose IDs + * were registered in the plan_params of an ancestor query level: register + * them on the caller's root before planning, so the branch subroots' outer + * params (and hence finalize_plan's acceptance of them) include them. They + * are also marked as outer params of the inner subroot itself. */ static Plan * build_graphscan_inner_plan(PlannerInfo *root, Oid graphid, @@ -3764,26 +3937,69 @@ build_graphscan_inner_plan(PlannerInfo *root, Oid graphid, List *edge_element_oids, List *array_props, PlannerInfo **inner_rootp, - int *max_nsrc, int *max_ndst) + int *max_nsrc, int *max_ndst, + List **vertex_params) { Query *qr; PlannerInfo *subroot; RelOptInfo *sub_final_rel; Plan *plan; + Bitmapset *vp; char *plan_name; + int orig_plen; - qr = build_graphscan_inner_query(graphid, edge_gep, edge_element_oids, - array_props, max_nsrc, max_ndst); + qr = build_graphscan_inner_query(root, graphid, edge_gep, edge_element_oids, + array_props, max_nsrc, max_ndst, + vertex_params); if (qr == NULL) { *inner_rootp = NULL; return NULL; } + /* + * Make the vertex params available to every descendant of the inner query + * (including the set-op branch subroots), so finalize_plan accepts them + * as externally supplied. The GraphScan node itself binds them at + * execution and the T_GraphScan finalize case subtracts them again, so + * only this plan's descendants see them. + * + * The registration is temporary: the inner (and branch) subroots copy + * these ids into their outer_params when SS_identify_outer_params() runs + * during subquery_planner(), after which the entries must not remain in + * root->plan_params (create_plan asserts that list is empty at every + * query level). + */ + + vp = NULL; + orig_plen = list_length(root->plan_params); + + if (*vertex_params != NIL) + { + foreach_ptr(Param, prm, *vertex_params) + { + PlannerParamItem *pitem = makeNode(PlannerParamItem); + + pitem->item = (Node *) prm; + pitem->paramId = prm->paramid; + root->plan_params = lappend(root->plan_params, pitem); + vp = bms_add_member(vp, prm->paramid); + } + } + plan_name = choose_plan_name(root->glob, "graph_hop", false); subroot = subquery_planner(root->glob, qr, plan_name, root, NULL, false, 0.0, NULL); + if (vp != NULL) + { + /* Ensure the inner subroot itself also sees them as outer params. */ + subroot->outer_params = bms_add_members(subroot->outer_params, vp); + + /* Drop the temporary plan_params entries. */ + root->plan_params = list_truncate(root->plan_params, orig_plen); + } + sub_final_rel = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL); if (IS_DUMMY_REL(sub_final_rel)) { @@ -3797,6 +4013,222 @@ build_graphscan_inner_plan(PlannerInfo *root, Oid graphid, return plan; } +/* + * Estimated number of distinct values of a key column, from pg_statistic + * (stadistinct < 0 means a fraction of the rows). With no stats (unanalyzed + * table) we fall back to "all values distinct", which is the conservative + * choice here: it makes the estimated degree (and hence the walk count) as + * small as possible rather than letting a stale/fake cardinality blow it up. + */ +static double +graph_est_key_distinct(Oid relid, AttrNumber attnum, double reltuples) +{ + HeapTuple statup; + Form_pg_statistic stat; + double ndistinct; + + statup = SearchSysCache3(STATRELATTINH, + ObjectIdGetDatum(relid), + Int16GetDatum(attnum), + BoolGetDatum(false)); + if (!HeapTupleIsValid(statup)) + return Max(reltuples, 1.0); + + stat = (Form_pg_statistic) GETSTRUCT(statup); + ndistinct = stat->stadistinct; + ReleaseSysCache(statup); + + if (ndistinct < 0) + ndistinct = -ndistinct * reltuples; + if (ndistinct <= 0) + ndistinct = Max(reltuples, 1.0); + return ndistinct; +} + +/* + * Estimated number of rows that a GraphScan returns for a single seed + * vertex: the number of walks of length [min_depth, max_depth] from a seed + * to a terminal-eligible vertex, computed from catalog statistics: + * + * rows(seed) ~= walkcount * terminal_frac + * walkcount = sum_{k=min..maxeff} term_k + * term_1 = fanout + * term_k = fanout * (fanout * DAMPING)^(k-1) (k >= 2) + * + * where fanout is the hop's average degree (source-side for outgoing, + * destination-side for incoming, both for undirected) over its own edge + * element tables, and terminal_frac is the share of the hop's endpoint + * vertex tables that carry the terminal's labels. This is a heuristic (like + * the estimates for recursive queries); its purpose is to keep the outer + * planner from treating the hop as a one-row relation. + * + * The per-step damping models the fact that walks re-converge on shared + * vertices, so the number of distinct walks grows far more slowly than the + * branching tree; without it (and without the hard cap), a {0,5} hop on a + * moderately connected graph estimates tens of thousands of rows per seed + * where only a handful exist, which pushed the planner into pathological + * choices such as a full-table hash on the terminal side, rebuilt once per + * LATERAL iteration. + * + * We could use better estimate if we collected statistics on each table involved. + * Currently, those are "fine-tuned" based on the data I have available in my environment. + */ +#define GRAPH_EST_PER_STEP_DAMPING 0.5 +#define GRAPH_EST_MAX_ROWS 1000.0 + +/* + * Look up a graph element, returning the OID of its backing table and its + * estimated row total (via estimate_rel_size), plus -- when the caller asks + * -- the vertex element ids the element connects (edges) or belongs to + * (vertices). Shared by the three row-count loops of graph_estimate_rows(). + */ +static void +graph_est_element_rows(Oid elemoid, Oid *relid, Oid *srcvertex, + Oid *dstvertex, double *tuples) +{ + HeapTuple etup; + Form_pg_propgraph_element pge; + Relation rel; + BlockNumber pages; + double allvisfrac; + + etup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(elemoid)); + if (!HeapTupleIsValid(etup)) + elog(ERROR, "cache lookup failed for property graph element %u", + elemoid); + pge = (Form_pg_propgraph_element) GETSTRUCT(etup); + + *relid = pge->pgerelid; + if (srcvertex) + *srcvertex = pge->pgesrcvertexid; + if (dstvertex) + *dstvertex = pge->pgedestvertexid; + + rel = table_open(*relid, AccessShareLock); + estimate_rel_size(rel, NULL, &pages, tuples, &allvisfrac); + table_close(rel, NoLock); + + ReleaseSysCache(etup); +} + +static double +graph_estimate_rows(Oid graphid, GraphElementPattern *edge_gep, + GraphElementPattern *term_gep, List *edge_element_oids, + int min_depth, int max_depth) +{ + double fanout = 0.0; + double endpoint_rows = 0.0; + double terminal_rows = 0.0; + List *endpoint_elems = NIL; + List *terminal_elems = NIL; + ListCell *lc; + int maxeff; + double walkcount = 0.0; + + foreach(lc, edge_element_oids) + { + Oid elemoid = lfirst_oid(lc); + Oid relid; + Oid srcvertex; + Oid dstvertex; + double tuples; + double src_nd = 1.0; + double dst_nd = 1.0; + + graph_est_element_rows(elemoid, &relid, &srcvertex, &dstvertex, + &tuples); + + if (srcvertex != InvalidOid) + endpoint_elems = lappend_oid(endpoint_elems, srcvertex); + if (dstvertex != InvalidOid) + endpoint_elems = lappend_oid(endpoint_elems, dstvertex); + + if (tuples > 0) + { + foreach_ptr(GraphElementKeyCol, kc, + get_graph_element_key_columns(elemoid, + Anum_pg_propgraph_element_pgesrckey)) + src_nd *= graph_est_key_distinct(relid, kc->attnum, tuples); + + foreach_ptr(GraphElementKeyCol, kc, + get_graph_element_key_columns(elemoid, + Anum_pg_propgraph_element_pgedestkey)) + dst_nd *= graph_est_key_distinct(relid, kc->attnum, tuples); + + if (edge_gep->kind == EDGE_PATTERN_LEFT) + fanout += tuples / dst_nd; /* incoming: matched on dst keys */ + else if (edge_gep->kind == EDGE_PATTERN_ANY) + fanout += tuples / src_nd + tuples / dst_nd; /* undirected */ + else + fanout += tuples / src_nd; /* outgoing */ + } + } + + /* Row total of the vertex tables that can be reached as endpoints. */ + foreach_oid(endpoint_elem, endpoint_elems) + { + Oid relid; + double tuples; + + graph_est_element_rows(endpoint_elem, &relid, NULL, NULL, &tuples); + if (tuples > 0) + endpoint_rows += tuples; + } + + /* Row total of the vertex tables carrying the terminal's labels. */ + if (term_gep != NULL) + terminal_elems = get_graph_vertex_element_oids(graphid, term_gep); + foreach_oid(term_elem, terminal_elems) + { + Oid relid; + double tuples; + + graph_est_element_rows(term_elem, &relid, NULL, NULL, &tuples); + if (tuples > 0) + terminal_rows += tuples; + } + + /* + * Average-degree sum over k steps, capped: unbounded or very deep hops + * make walk counts explode on any cyclic component, so never let the + * estimate grow without bound. + */ + maxeff = (max_depth < 0) ? 8 : max_depth; + maxeff = Min(maxeff, 8); + if (maxeff < min_depth) + maxeff = min_depth; + for (int k = min_depth; k <= maxeff; k++) + { + double term; + + if (k == 0) + term = 1.0; /* the seed vertex itself */ + else if (k == 1) + term = fanout; + else + { + double rgrowth = Max(fanout * GRAPH_EST_PER_STEP_DAMPING, 1.0); + + term = fanout * pow(rgrowth, (double) (k - 1)); + } + + walkcount += term; + if (walkcount > GRAPH_EST_MAX_ROWS) + { + walkcount = GRAPH_EST_MAX_ROWS; + break; + } + } + + { + double term_frac = 1.0; + + if (endpoint_rows > 0) + term_frac = Min(terminal_rows / endpoint_rows, 1.0); + return Max(walkcount * term_frac, 1.0); + } +} + /* * set_graphscan_pathlist * Build the (single) access path for an internal RTE_GRAPH_TABLE @@ -3814,6 +4246,7 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, Relids required_outer; List *edge_element_oids; List *array_props; + List *vertex_params = NIL; PlannerInfo *inner_root; Plan *inner_plan; List *seed_key_cols = NIL; @@ -3866,23 +4299,37 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, array_props = rte->graph_vle_props; /* - * The internal single-hop query has no easy rowcount estimate; use a - * minimal nonzero rowcount so the relation is not treated as dummy. + * Estimate the row count; see graph_estimate_rows(). The estimate is for + * one seed vertex (the scan is (re)started once per encountered seed), so + * it is per-execution for parameterized uses as well. A nonzero minimum + * keeps the relation from being treated as dummy. */ - rel->rows = 1; + { + GraphElementPattern *term_gep = + (list_length(path_term) >= 3) ? + lthird_node(GraphElementPattern, path_term) : NULL; + + rel->rows = + graph_estimate_rows(rte->relid, edge_gep, term_gep, + edge_element_oids, + linitial_int(edge_gep->quantifier), + lsecond_int(edge_gep->quantifier)); + rel->tuples = rel->rows; + } /* Build the parameterized 1-hop inner expansion (righttree). */ inner_plan = build_graphscan_inner_plan(root, rte->relid, edge_gep, edge_element_oids, array_props, - &inner_root, &max_nsrc, &max_ndst); + &inner_root, &max_nsrc, &max_ndst, + &vertex_params); gpath = makeNode(GraphPath); gpath->path.pathtype = T_GraphScan; gpath->path.parent = rel; gpath->path.pathtarget = rel->reltarget; gpath->path.rows = rel->rows; - gpath->path.startup_cost = 0; - gpath->path.total_cost = rel->rows * cpu_tuple_cost; + gpath->path.startup_cost = cpu_operator_cost * 10; + gpath->path.total_cost = rel->rows * cpu_tuple_cost + cpu_operator_cost * 100; gpath->path.pathkeys = NIL; gpath->min_depth = linitial_int(edge_gep->quantifier); gpath->max_depth = lsecond_int(edge_gep->quantifier); @@ -3907,6 +4354,10 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, gpath->graph_columns = rte->graph_table_columns; gpath->inner_plan = inner_plan; gpath->subplan_params = (inner_root != NULL) ? inner_root->plan_params : NIL; + gpath->vertex_param_ids = NIL; + foreach_ptr(Param, vprm, vertex_params) + gpath->vertex_param_ids = + lappend_int(gpath->vertex_param_ids, vprm->paramid); gpath->seed_elem_oid = rte->graph_seed_elem_oid; gpath->seed_param_ids = NIL; gpath->max_nsrc = max_nsrc; @@ -3928,7 +4379,15 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, required_outer = bms_del_member(required_outer, rti); param_info = get_baserel_parampathinfo(root, rel, required_outer); gpath->path.param_info = param_info; - gpath->path.rows = param_info->ppi_rows; + + /* + * get_baserel_parampathinfo() would scale rel->rows by the estimated + * selectivity of the seed equality (v1.id = gs_seed), but that + * equation is the scan's dispatch mechanism, not a filter: the scan + * emits one seed vertex's worth of rows per execution. Ship our + * per-seed estimate directly. + */ + gpath->path.rows = rel->rows; } add_path(rel, (Path *) gpath); diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 1d13fe6c8a8..470237032d1 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -3755,6 +3755,7 @@ create_graphscan_plan(PlannerInfo *root, GraphPath * best_path, scan_plan->edge_element_oids = best_path->edge_element_oids; scan_plan->graph_columns = best_path->graph_columns; scan_plan->inner_plan = best_path->inner_plan; + scan_plan->vertex_param_ids = best_path->vertex_param_ids; scan_plan->seed_elem_oid = best_path->seed_elem_oid; scan_plan->max_nsrc = best_path->max_nsrc; scan_plan->max_ndst = best_path->max_ndst; diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index 44507d2a8c6..4f33e8d54c6 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -2776,6 +2776,22 @@ finalize_plan(PlannerInfo *root, Plan *plan, /* Now we can add its extParams to the parent's params */ context.paramids = bms_add_members(context.paramids, gs->inner_plan->extParam); + + /* + * The current-vertex PARAM_EXEC ids referenced by the + * inner (arm) plans are supplied by the GraphScan node + * itself (as a NestLoop supplies its nestParams), so they + * must not count as external params of this level. + */ + if (gs->vertex_param_ids != NIL) + { + Bitmapset *vp = NULL; + + foreach_int(pid, gs->vertex_param_ids) + vp = bms_add_member(vp, pid); + context.paramids = + bms_del_members(context.paramids, vp); + } } context.paramids = bms_add_members(context.paramids, diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c index e503f5e7ba5..9877604d170 100644 --- a/src/backend/rewrite/rewriteGraphTable.c +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -1640,9 +1640,9 @@ native_apply_rls_to_query(Query *query) } /* - * The decomposed queries are SELECT-only, so no WITH CHECK - * OPTIONS can apply; hasRowSecurity still matters for the - * plancache (dependsOnRLS). + * 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; @@ -1998,9 +1998,9 @@ native_query_for_branch(native_decomp * dc, List *elems, List *vles) vb->gs_rti = gs_rti; vb->array_first = 1 + list_length(get_graph_element_key_columns(srcpe->elemoid, - Anum_pg_propgraph_element_pgekey)) + Anum_pg_propgraph_element_pgekey)) + list_length(get_graph_element_key_columns(termpe->elemoid, - Anum_pg_propgraph_element_pgekey)); + Anum_pg_propgraph_element_pgekey)); vb->array_props = vf->array_props; { RangeTblEntry *gs_rte = @@ -2148,8 +2148,10 @@ 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. */ + /* + * Backing element tables can carry RLS policies: handled by the native + * planner in set_graph_pathlist(), before subquery_planner. + */ return path_query; } @@ -2241,6 +2243,36 @@ get_graph_edge_element_oids(Oid propgraphid, GraphElementPattern *gep) return result; } +/* + * Return the OIDs of the vertex elements matching the given vertex element + * pattern in the given property graph. Used by the native planner to + * estimate the row count of a GraphScan (the terminal pattern's label set + * determines which vertex tables may end a walk). + */ +List * +get_graph_vertex_element_oids(Oid propgraphid, GraphElementPattern *gep) +{ + struct path_factor *pf; + List *pes; + List *result = NIL; + ListCell *lc; + + Assert(gep->kind == VERTEX_PATTERN); + + pf = palloc0_object(struct path_factor); + pf->factorpos = 0; + pf->kind = VERTEX_PATTERN; + pf->labelexpr = gep->labelexpr; + pf->variable = gep->variable; + pf->whereClause = gep->whereClause; + + pes = get_path_elements_for_path_factor(propgraphid, pf); + foreach_ptr(struct path_element, pe, pes) + result = lappend_oid(result, pe->elemoid); + + return result; +} + /* * Decompose a GRAPH_TABLE clause into a Query for native execution. * diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 6506bf12e3c..86fae6fa74f 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -9373,8 +9373,11 @@ get_parameter(Param *param, deparse_context *context) * * It's a bug if we get here for anything except PARAM_EXTERN Params, but * in production builds printing $N seems more useful than failing. + * (GraphScan also hands PARAM_EXEC slots to executor-bound current-vertex + * key values, which have no deparse referent; print those as $N too.) */ - Assert(param->paramkind == PARAM_EXTERN); + Assert(param->paramkind == PARAM_EXTERN || + param->paramkind == PARAM_EXEC); appendStringInfo(context->buf, "$%d", param->paramid); } diff --git a/src/include/executor/nodeGraphScan.h b/src/include/executor/nodeGraphScan.h index 3bf5cbe2cba..ef9a310a399 100644 --- a/src/include/executor/nodeGraphScan.h +++ b/src/include/executor/nodeGraphScan.h @@ -72,6 +72,16 @@ typedef struct GraphDepthFrameData { PlanState *inner_state; /* own copy of the inner 1-hop expansion */ + /* + * True until the frame's inner scan has been (re)started for the current + * vertex: the executor (re)initializes or rescans it the first time the + * frame is stepped after a push or a new seed, when the current-vertex + * PARAM_EXEC parameters are bound. Parameterized index scans only + * re-evaluate their scan keys on (re)scan, so restarting like this is + * what keeps them in sync with the vertex. + */ + bool need_init; + Oid vid_elem; /* vertex element of the current vertex */ int vid_nkeys; /* key width of the current vertex */ Datum *vid; /* current vertex key values */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 4f972a62f87..d9dd23fb6b3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1987,12 +1987,27 @@ typedef struct GraphScanState int narms; struct GraphScanArmData *arms; + /* Flags passed to ExecInitGraphScan (needed for lazy inner init). */ + int eflags; + /* - * 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. + * 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; + /* + * PARAM_EXEC ids of the current-vertex key values used to parameterize + * the inner (1-hop) arm scans (see GraphScan.vertex_param_ids). The + * executor binds the active direction's parameters from the current + * vertex before each depth frame's fetch. + */ + List *vertex_params; + + /* Which direction's parameter set is active (from plan->direction). */ + bool fwd_active; + bool rev_active; + /* * Scratch buffers for graph_step(): resized to the max key width and * number of VLE properties at init. They must NOT live in the per-tuple diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 9b2bbe87a66..5baacce3aad 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -2244,6 +2244,12 @@ typedef struct GraphPath /* PARAM_EXEC ids of the seed key columns (filled at create_plan time). */ List *seed_param_ids; + /* + * PARAM_EXEC ids of the current-vertex key values used to parameterize + * the inner (1-hop) arm scans; see GraphScan.vertex_param_ids. + */ + List *vertex_param_ids; + /* Hop-wide max src/dest key widths over the edge element arms. */ int max_nsrc; int max_ndst; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 90be4a9c47a..55dba0ff525 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -634,6 +634,16 @@ typedef struct GraphScan */ List *seed_params; + /* + * PARAM_EXEC ids of the current-vertex key values used to parameterize + * the inner (1-hop) arm scans, ordered [forward (source key) slots, + * reverse (destination key) slots]; see build_graphscan_inner_query(). + * The executor binds the active direction's parameters from the current + * vertex before each depth frame's fetch. finalize_plan treats these ids + * as supplied by this node (see the T_GraphScan case in subselect.c). + */ + List *vertex_param_ids; + /* Hop-wide max src/dest key widths over the edge element arms. */ int max_nsrc; int max_ndst; diff --git a/src/include/rewrite/rewriteGraphTable.h b/src/include/rewrite/rewriteGraphTable.h index 345dcb68118..3a6ffab37a6 100644 --- a/src/include/rewrite/rewriteGraphTable.h +++ b/src/include/rewrite/rewriteGraphTable.h @@ -48,6 +48,8 @@ extern Node *get_element_property_expr(Oid elemoid, Oid propoid, int rtindex); */ extern List *get_graph_edge_element_oids(Oid propgraphid, GraphElementPattern *gep); +extern List *get_graph_vertex_element_oids(Oid propgraphid, + GraphElementPattern *gep); /* * One key column of a graph element: the element table's column (attnum) @@ -76,7 +78,7 @@ extern List *get_graph_element_key_columns(Oid elemoid, int key_attnum); * both by the filters pushed into the GraphScan's inner (1-hop) expansion * and by the GraphScan executor itself. */ -extern Oid key_equality_operator(Oid typid); +extern Oid key_equality_operator(Oid typid); /* * Return the VLE edge-list (array) property references (GraphPropertyRef -- 2.39.2