From 79ac460fc4f82741c1cbf89a8aa1ebd8fe0e07aa Mon Sep 17 00:00:00 2001 From: Henri Gasc Date: Thu, 10 Sep 2026 09:51:36 +0200 Subject: [PATCH 5/8] Implement the executor --- src/backend/commands/explain.c | 7 +- src/backend/executor/nodeGraphScan.c | 707 ++++++++++++++++++++-- src/backend/optimizer/path/allpaths.c | 330 +++++++--- src/backend/optimizer/plan/createplan.c | 69 ++- src/backend/rewrite/rewriteGraphTable.c | 35 +- src/include/executor/nodeGraphScan.h | 63 ++ src/include/nodes/execnodes.h | 54 +- src/include/nodes/parsenodes.h | 8 + src/include/nodes/pathnodes.h | 14 +- src/include/nodes/plannodes.h | 22 +- src/test/regress/expected/graph_table.out | 431 +++++-------- src/test/regress/sql/graph_table.sql | 35 +- 12 files changed, 1349 insertions(+), 426 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index ac039099b53..285ed052136 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -24,6 +24,7 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "foreign/fdwapi.h" +#include "executor/nodeGraphScan.h" #include "jit/jit.h" #include "libpq/pqformat.h" #include "libpq/protocol.h" @@ -2434,8 +2435,10 @@ ExplainNode(PlanState *planstate, List *ancestors, "Subquery", NULL, es); break; case T_GraphScan: - ExplainNode(((GraphScanState *) planstate)->inner_plan, ancestors, - "Inner", NULL, es); + /* the inner 1-hop expansion lives in the first depth frame */ + if (((GraphScanState *) planstate)->frames[0].inner_state != NULL) + ExplainNode(((GraphScanState *) planstate)->frames[0].inner_state, + ancestors, "Inner", NULL, es); break; case T_CustomScan: ExplainCustomChildren((CustomScanState *) planstate, diff --git a/src/backend/executor/nodeGraphScan.c b/src/backend/executor/nodeGraphScan.c index b563405d338..e8a2faf9a5e 100644 --- a/src/backend/executor/nodeGraphScan.c +++ b/src/backend/executor/nodeGraphScan.c @@ -3,10 +3,28 @@ * nodeGraphScan.c * Routines to handle graph scan nodes. * - * The full graph scan executor (variable-length hop traversal) will be - * implemented together with the hop machinery; for now this only supports - * EXPLAIN: the plan is initialized so it can be displayed, but actually - * executing it raises a "not yet implemented" error. + * 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 + * 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). + * + * Each depth frame owns its own PlanState copy of the inner plan, so a + * frame's scan cursor is independent and backtracking simply resumes the + * parent frame's cursor (no bookkeeping needed). + * + * An edge is traversable from the current vertex iff the edge element's + * source vertex element matches the current vertex's element and each source + * key column equals the current vertex's key value (compared with the + * default equality operator of the source key column datatype). No src-key + * filter is pushed into the inner plan, so heterogeneous source key widths + * and element sets are handled uniformly (see build_graphscan_inner_query). + * + * Identified rows are emitted as (seed keys, terminal keys, VLE edge-list + * arrays); the arrays contain the property values accumulated over the + * path's edges, in traversal order (empty for a zero-length path). * * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California @@ -19,26 +37,608 @@ */ #include "postgres.h" +#include "access/htup_details.h" +#include "catalog/pg_propgraph_element.h" #include "executor/executor.h" #include "executor/nodeGraphScan.h" #include "miscadmin.h" -#include "parser/parse_target.h" +#include "optimizer/cost.h" +#include "rewrite/rewriteGraphTable.h" +#include "utils/array.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" 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 bool graph_next(GraphScanState * node, GraphScan * plan); +static bool graph_step(GraphScanState * node, GraphScan * plan, + GraphDepthFrameData * fr); +static int graph_try_edge(GraphScanState * node, GraphScan * plan, + GraphDepthFrameData * fr, TupleTableSlot *eslot, + Oid *newelem, int *newnkeys, Datum *newvid, + bool *newnull, Datum *eprops, bool *epropsnull); +static bool edge_key_matches(GraphScanState * node, GraphDepthFrameData * fr, + TupleTableSlot *eslot, GraphScanArmData * arm, + bool issrc); +static int graph_find_arm(GraphScanState * node, Oid relid); +static void graph_push(GraphScanState * node, Oid newelem, int newnkeys, + Datum *newvid, bool *newnull, Datum *eprops, + bool *epropsnull); +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 Datum graph_build_edge_array(GraphScanState * node, TupleTableSlot *slot, + int pi); + +/* Result values of graph_try_edge */ +#define GRAPH_EDGE_NONE 0 /* not traversable */ +#define GRAPH_EDGE_FORWARD 1 /* traversable in the forward direction */ +#define GRAPH_EDGE_FORWARD 1 /* forward traversal */ + +/* + * Compile, per edge element arm, the metadata needed to match edges against + * the current vertex: element ids, source/destination key column positions + * within the arm's output row, and default equality functions. + */ +static void +build_arms(GraphScanState * node, GraphScan * plan) +{ + int nprops = node->nprops; + + node->arms = palloc0(sizeof(GraphScanArmData) * node->narms); + + for (int a = 0; a < node->narms; a++) + { + Oid elemoid = list_nth_oid(plan->edge_element_oids, a); + HeapTuple etup; + Form_pg_propgraph_element pge; + GraphScanArmData *arm = &node->arms[a]; + + 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); + + arm->arm_relid = pge->pgerelid; + arm->arm_srcvertex = pge->pgesrcvertexid; + arm->arm_dstvertex = pge->pgedestvertexid; + arm->arm_src_first = nprops + 2; + arm->arm_dst_first = nprops + 2 + node->max_nsrc; + + build_arm_keys(get_graph_element_key_columns(elemoid, + Anum_pg_propgraph_element_pgesrckey), + &arm->arm_nsrc, &arm->arm_srceq, &arm->arm_srccoll); + build_arm_keys(get_graph_element_key_columns(elemoid, + Anum_pg_propgraph_element_pgedestkey), + &arm->arm_ndst, &arm->arm_dsteq, &arm->arm_dstcoll); + + ReleaseSysCache(etup); + } +} + +/* + * Compile the default equality functions and collations for one side + * (source or destination) of an edge element arm, from the key columns read + * by get_graph_element_key_columns(). + */ +static void +build_arm_keys(List *keys, int *nkeys, FmgrInfo **eq, Oid **colls) +{ + int i = 0; + + *nkeys = list_length(keys); + *eq = palloc(sizeof(FmgrInfo) * Max(list_length(keys), 1)); + *colls = palloc(sizeof(Oid) * Max(list_length(keys), 1)); + + foreach_ptr(GraphElementKeyCol, kc, keys) + { + Oid eqop = key_equality_operator(kc->typid); + + fmgr_info(get_opcode(eqop), &(*eq)[i]); + (*colls)[i] = kc->collation; + i++; + } +} + +/* + * Fetch the next seed vertex from the enclosing nestloop (via the seed + * PARAM_EXEC slots) and start a new depth-0 frame. Returns false when there + * is no (usable) seed; the scan is then exhausted. + */ +static bool +graph_fetch_seed(GraphScanState * node, GraphScan * plan) +{ + EState *estate = node->ss.ps.state; + GraphDepthFrameData *fr = &node->frames[0]; + ListCell *lc; + int k = 0; + bool hasnull = false; + + graph_reset(node); + + /* restart every depth's inner scan for the new seed */ + for (int d = 0; d < node->ndepths; d++) + if (node->frames[d].inner_state != NULL) + ExecReScan(node->frames[d].inner_state); + + fr->vid_elem = node->seed_elem; + fr->vid_nkeys = list_length(node->seed_params); + foreach(lc, node->seed_params) + { + ParamExecData *prm = &estate->es_param_exec_vals[lfirst_int(lc)]; + + fr->vid[k] = prm->value; + fr->vidnull[k] = prm->isnull; + if (prm->isnull) + hasnull = true; + k++; + } + + /* a NULL seed matches nothing (SQL NULL semantics) */ + if (hasnull) + return false; + + node->cur_depth = 0; + node->seed_emitted = false; + return true; +} + +/* + * Try to advance the traversal one edge from the current (innermost) frame, + * backtracking when a frame is exhausted. Returns false when the current + * seed is exhausted (caller must fetch a new seed). + */ +static bool +graph_next(GraphScanState * node, GraphScan * plan) +{ + for (;;) + { + GraphDepthFrameData *fr = &node->frames[node->cur_depth]; + + if (graph_step(node, plan, fr)) + { + /* descended one edge; emit whenever the new depth is deep enough */ + if (node->cur_depth >= node->min_depth) + return true; + continue; /* not deep enough yet; descend further */ + } + + /* this frame is exhausted: backtrack */ + if (node->cur_depth <= 0) + { + node->cur_depth = -1; /* need a new seed */ + return false; + } + graph_backtrack(node); + } +} + +/* + * Pull the next traversable edge from the given frame's inner scan. Returns + * true if a new depth was pushed onto the stack. + */ +static bool +graph_step(GraphScanState * node, GraphScan * plan, + GraphDepthFrameData * fr) +{ + TupleTableSlot *eslot; + Oid newelem; + int newnkeys; + Datum *newvid = node->tmp_vid; + bool *newnull = node->tmp_vidnull; + Datum *newprops = node->tmp_props; + bool *newpropsnull = node->tmp_propsnull; + + CHECK_FOR_INTERRUPTS(); + + if (fr->inner_state == NULL) + return false; + + /* + * The current frame already sits at (or beyond) the effective maximum + * depth: no further descent is allowed. Its single row was emitted when + * this depth was pushed; further calls just exhaust the frame. + */ + if (node->cur_depth >= node->max_depth) + return false; + + for (;;) + { + int res; + + eslot = ExecProcNode(fr->inner_state); + if (TupIsNull(eslot)) + return false; + + res = graph_try_edge(node, plan, fr, eslot, &newelem, &newnkeys, newvid, + newnull, newprops, newpropsnull); + if (res != GRAPH_EDGE_NONE) + { + graph_push(node, newelem, newnkeys, newvid, newnull, newprops, + newpropsnull); + return true; + } + } +} + +/* + * Check whether a candidate edge (one row of the inner plan) is traversable + * from the current vertex, according to the hop's direction, and if so fill + * the next vertex plus the edge's VLE property values. + * + * Returns GRAPH_EDGE_NONE / _FORWARD / _BOTH (the latter for an undirected + * non-loop edge, whose reverse traversal is also valid and is deferred). + */ +static int +graph_try_edge(GraphScanState * node, GraphScan * plan, + GraphDepthFrameData * fr, TupleTableSlot *eslot, + Oid *newelem, int *newnkeys, Datum *newvid, + bool *newnull, Datum *eprops, bool *epropsnull) +{ + Oid tbl; + bool isnull; + int armno; + GraphScanArmData *arm; + int nprops = node->nprops; + + /* identify the edge element by its table OID */ + tbl = DatumGetObjectId(slot_getattr(eslot, nprops + 2, &isnull)); + armno = graph_find_arm(node, tbl); + if (armno < 0) + elog(ERROR, "graph scan encountered unknown edge element table %u", tbl); + arm = &node->arms[armno]; + + /* VLE property values of the edge (may be filtered out below) */ + for (int i = 0; i < nprops; i++) + { + eprops[i] = slot_getattr(eslot, i + 1, &epropsnull[i]); + } + + switch (plan->direction) + { + case GRAPH_DIR_INCOMING: + if (edge_key_matches(node, fr, eslot, arm, false)) + { + *newelem = arm->arm_srcvertex; + *newnkeys = arm->arm_nsrc; + for (int i = 0; i < arm->arm_nsrc; i++) + newvid[i] = + slot_getattr(eslot, arm->arm_src_first + i + 1, &newnull[i]); + return GRAPH_EDGE_FORWARD; + } + break; + + case GRAPH_DIR_UNDIRECTED: + + /* + * An undirected edge is traversable from the current vertex when + * it matches either endpoint: as the source it yields the + * destination as next vertex, as the destination it yields the + * source (the reverse traversal, generated here rather than with + * a deferred mechanism). + */ + if (edge_key_matches(node, fr, eslot, arm, true)) + { + *newelem = arm->arm_dstvertex; + *newnkeys = arm->arm_ndst; + for (int i = 0; i < arm->arm_ndst; i++) + newvid[i] = + slot_getattr(eslot, arm->arm_dst_first + i + 1, &newnull[i]); + return GRAPH_EDGE_FORWARD; + } + if (edge_key_matches(node, fr, eslot, arm, false)) + { + *newelem = arm->arm_srcvertex; + *newnkeys = arm->arm_nsrc; + for (int i = 0; i < arm->arm_nsrc; i++) + newvid[i] = + slot_getattr(eslot, arm->arm_src_first + i + 1, &newnull[i]); + return GRAPH_EDGE_FORWARD; + } + break; + + default: /* GRAPH_DIR_OUTGOING */ + if (edge_key_matches(node, fr, eslot, arm, true)) + { + *newelem = arm->arm_dstvertex; + *newnkeys = arm->arm_ndst; + for (int i = 0; i < arm->arm_ndst; i++) + newvid[i] = + slot_getattr(eslot, arm->arm_dst_first + i + 1, &newnull[i]); + return GRAPH_EDGE_FORWARD; + } + break; + } + + return GRAPH_EDGE_NONE; +} + +/* + * Compare the current vertex key values against the given side (source or + * destination) key columns of a candidate edge. The arm's source (resp. + * destination) vertex element must equal the current vertex's element. + */ +static bool +edge_key_matches(GraphScanState * node, GraphDepthFrameData * fr, + TupleTableSlot *eslot, GraphScanArmData * arm, + bool issrc) +{ + int n; + int first; + Oid elem; + int i; + + if (issrc) + { + n = arm->arm_nsrc; + first = arm->arm_src_first; + elem = arm->arm_srcvertex; + } + else + { + n = arm->arm_ndst; + first = arm->arm_dst_first; + elem = arm->arm_dstvertex; + } + + if (fr->vid_elem != elem) + return false; + if (fr->vid_nkeys != n) + return false; + + for (i = 0; i < n; i++) + { + Datum edatum; + bool eisnull; + FmgrInfo *eq; + Oid eqcoll; + + edatum = slot_getattr(eslot, first + i + 1, &eisnull); + if (eisnull || fr->vidnull[i]) + return false; + + if (issrc) + { + eq = &arm->arm_srceq[i]; + eqcoll = arm->arm_srccoll[i]; + } + else + { + eq = &arm->arm_dsteq[i]; + eqcoll = arm->arm_dstcoll[i]; + } + + if (!DatumGetBool(FunctionCall2Coll(eq, eqcoll, edatum, fr->vid[i]))) + return false; + } + return true; +} + +/* Arm index whose edge element table matches relid, or -1. */ +static int +graph_find_arm(GraphScanState * node, Oid relid) +{ + for (int a = 0; a < node->narms; a++) + if (node->arms[a].arm_relid == relid) + return a; + return -1; +} + +/* + * Push a new depth frame for a traversed edge. Enforces max_graph_stack_depth + * via a shared counter on the EState (summed over all active GraphScans). + */ +static void +graph_push(GraphScanState * node, Oid newelem, int newnkeys, + Datum *newvid, bool *newnull, Datum *eprops, + bool *epropsnull) +{ + int d = node->cur_depth + 1; + GraphDepthFrameData *nfr = &node->frames[d]; + EState *estate = node->ss.ps.state; + + estate->es_graph_stack_depth++; + if (estate->es_graph_stack_depth > max_graph_stack_depth) + ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("exceeded maximum graph traversal depth"), + errhint("Increase max_graph_stack_depth and retry, or try " + "to remove the infinite loop"))); + + nfr->vid_elem = newelem; + nfr->vid_nkeys = newnkeys; + memcpy(nfr->vid, newvid, sizeof(Datum) * newnkeys); + memcpy(nfr->vidnull, newnull, sizeof(bool) * newnkeys); + if (eprops != NULL) + { + memcpy(nfr->edge_props, eprops, sizeof(Datum) * node->nprops); + memcpy(nfr->edge_propsnull, epropsnull, sizeof(bool) * node->nprops); + } + node->cur_depth = d; +} + +/* Pop the innermost depth frame (called only for depth > 0). */ +static void +graph_backtrack(GraphScanState * node) +{ + node->ss.ps.state->es_graph_stack_depth--; + Assert(node->ss.ps.state->es_graph_stack_depth >= 0); + node->cur_depth--; +} + +/* Pop all active frames; the scan becomes ready for a new seed. */ +static void +graph_reset(GraphScanState * node) +{ + for (; node->cur_depth > 0; node->cur_depth--) + { + node->ss.ps.state->es_graph_stack_depth--; + (void) 0; + } + Assert(node->ss.ps.state->es_graph_stack_depth >= 0); + node->cur_depth = -1; + node->seed_emitted = false; +} static TupleTableSlot * ExecGraphScan(PlanState *pstate) { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("graph scan execution is not yet implemented"))); - return NULL; /* keep compiler quiet */ + GraphScanState *node = castNode(GraphScanState, pstate); + GraphScan *plan = castNode(GraphScan, pstate->plan); + TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; + + for (;;) + { + CHECK_FOR_INTERRUPTS(); + + if (node->cur_depth < 0) + { + /* + * Fetch a seed vertex from the enclosing nestloop (via the seed + * PARAM_EXEC slots), but only once per (re)scan: after the + * current seed has been fully traversed, keep returning NULL + * until ExecReScan() re-arms us for the next outer row. + */ + if (!node->need_seed) + return NULL; + node->need_seed = false; + if (!graph_fetch_seed(node, plan)) + return NULL; + } + + /* emit the zero-hop seed row first, when applicable */ + if (!node->seed_emitted && node->min_depth == 0) + { + TupleTableSlot *res; + + node->seed_emitted = true; + res = graph_emit_row(node, slot); + if (res != NULL) + return res; + /* else drop and keep traversing */ + } + + /* try to descend along another edge (or backtrack) */ + if (graph_next(node, plan)) + { + TupleTableSlot *res = graph_emit_row(node, slot); + + if (res != NULL) + return res; + } + } +} + +/* + * Fill the scan's output slot for the path currently on the stack: seed + * keys (frame 0), terminal keys (innermost frame), and VLE edge-list arrays. + */ +static void +graph_build_row(GraphScanState * node, TupleTableSlot *slot) +{ + GraphScan *plan = castNode(GraphScan, node->ss.ps.plan); + GraphDepthFrameData *seedfr = &node->frames[0]; + GraphDepthFrameData *endfr = &node->frames[node->cur_depth]; + int amp; + MemoryContext oldcxt; + + ExecClearTuple(slot); + + amp = 0; + foreach_int(attno, plan->seed_key_cols) + { + slot->tts_values[attno - 1] = seedfr->vid[amp]; + slot->tts_isnull[attno - 1] = seedfr->vidnull[amp]; + amp++; + } + + amp = 0; + foreach_int(attno, plan->terminal_key_cols) + { + slot->tts_values[attno - 1] = endfr->vid[amp]; + slot->tts_isnull[attno - 1] = endfr->vidnull[amp]; + amp++; + } + + /* Build the VLE edge-list arrays in the per-tuple context. */ + oldcxt = + MemoryContextSwitchTo(node->ss.ps.ps_ExprContext->ecxt_per_tuple_memory); + amp = 0; + foreach_int(attno, plan->edge_list_cols) + { + slot->tts_values[attno - 1] = graph_build_edge_array(node, slot, amp); + slot->tts_isnull[attno - 1] = false; + amp++; + } + MemoryContextSwitchTo(oldcxt); + + ExecStoreVirtualTuple(slot); +} + +/* + * Build and emit the current path's row: fill the scan slot, then apply the + * scan qual and projection. Returns the resulting table slot, or NULL when + * the row failed the qual (the caller must keep traversing). + */ +static TupleTableSlot * +graph_emit_row(GraphScanState *node, TupleTableSlot *slot) +{ + graph_build_row(node, slot); + node->ss.ps.ps_ExprContext->ecxt_scantuple = slot; + if (node->ss.ps.qual == NULL || + ExecQual(node->ss.ps.qual, node->ss.ps.ps_ExprContext)) + { + if (node->ss.ps.ps_ProjInfo != NULL) + return ExecProject(node->ss.ps.ps_ProjInfo); + return slot; + } + return NULL; +} + +/* + * Build the VLE edge-list array for property pi: the property's value over + * every traversed edge of the current path, in traversal order; an empty + * array when the path has no edges. + */ +static Datum +graph_build_edge_array(GraphScanState * node, TupleTableSlot *slot, + int pi) +{ + GraphScan *plan = castNode(GraphScan, node->ss.ps.plan); + Oid arrtype; + Oid elemtype; + ArrayBuildState *astate = NULL; + + arrtype = TupleDescAttr(slot->tts_tupleDescriptor, + list_nth_int(plan->edge_list_cols, pi) - 1) + ->atttypid; + elemtype = get_element_type(arrtype); + if (elemtype == InvalidOid) + elog(ERROR, "graph scan edge-list column %d is not an array", pi + 1); + + for (int d = 1; d <= node->cur_depth; d++) + { + GraphDepthFrameData *fr = &node->frames[d]; + + astate = + accumArrayResult(astate, fr->edge_props[pi], fr->edge_propsnull[pi], + elemtype, CurrentMemoryContext); + } + + if (astate == NULL) + return PointerGetDatum(construct_empty_array(elemtype)); + + return makeArrayResult(astate, CurrentMemoryContext); } GraphScanState * ExecInitGraphScan(GraphScan * node, EState *estate, int eflags) { GraphScanState *scanstate; + int maxwidth; /* check for unsupported flags */ Assert(!(eflags & EXEC_FLAG_MARK)); @@ -57,34 +657,70 @@ ExecInitGraphScan(GraphScan * node, EState *estate, int eflags) scanstate->ss.ps.state = estate; scanstate->ss.ps.ExecProcNode = ExecGraphScan; - /* - * Miscellaneous initialization - * - * create expression context for node - */ ExecAssignExprContext(estate, &scanstate->ss.ps); + scanstate->min_depth = node->min_depth; + scanstate->max_depth = node->max_depth; + scanstate->seed_elem = node->seed_elem_oid; + 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->narms = list_length(node->edge_element_oids); + scanstate->cur_depth = -1; + scanstate->need_seed = true; + scanstate->seed_emitted = false; + /* - * initialize inner (single quantified hop) plan as a nested child + * Effective maximum depth. Explicit bounds are honored; unbounded (or + * absurdly large) ones are clamped to max_graph_stack_depth + 1 so that + * the traversal-depth guard below fires instead of looping forever. */ - if (node->inner_plan != NULL) - scanstate->inner_plan = ExecInitNode(node->inner_plan, estate, eflags); + if (node->max_depth < 0 || node->max_depth > max_graph_stack_depth) + scanstate->max_depth = max_graph_stack_depth + 1; + scanstate->ndepths = scanstate->max_depth + 1; + scanstate->frames = palloc0(sizeof(GraphDepthFrameData) * scanstate->ndepths); + + /* Compile the per-arm edge element metadata. */ + build_arms(scanstate, node); /* - * Initialize scan slot. There is no heap relation to describe it, so we + * Initialize the scan slot. There is no heap relation to describe it, so * build a virtual slot whose descriptor comes from the scan's own * targetlist; the result slot is built from the targetlist by * ExecInitResultTypeTL. */ ExecInitScanTupleSlot(estate, &scanstate->ss, - ExecTypeFromTL(node->scan.plan.targetlist), - &TTSOpsVirtual, 0); + ExecTypeFromTL(node->graph_columns), &TTSOpsVirtual, 0); + ExecInitResultTypeTL(&scanstate->ss.ps); + ExecAssignScanProjectionInfo(&scanstate->ss); /* - * Initialize result type and projection. + * 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. */ - ExecInitResultTypeTL(&scanstate->ss.ps); - ExecAssignScanProjectionInfo(&scanstate->ss); + maxwidth = Max(list_length(node->seed_param_ids), + 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)); + scanstate->tmp_props = palloc(sizeof(Datum) * Max(scanstate->nprops, 1)); + scanstate->tmp_propsnull = palloc(sizeof(bool) * Max(scanstate->nprops, 1)); + + for (int d = 0; d < scanstate->ndepths; d++) + { + GraphDepthFrameData *fr = &scanstate->frames[d]; + + fr->vid = palloc(sizeof(Datum) * Max(maxwidth, 1)); + fr->vidnull = palloc(sizeof(bool) * Max(maxwidth, 1)); + fr->edge_props = palloc(sizeof(Datum) * Max(scanstate->nprops, 1)); + fr->edge_propsnull = palloc(sizeof(bool) * Max(scanstate->nprops, 1)); + + if (node->inner_plan != NULL) + fr->inner_state = + ExecInitNode(copyObject(node->inner_plan), estate, eflags); + else + fr->inner_state = NULL; + } /* * initialize child expressions @@ -98,27 +734,24 @@ ExecInitGraphScan(GraphScan * node, EState *estate, int eflags) void ExecEndGraphScan(GraphScanState * node) { - if (node->inner_plan) - ExecEndNode(node->inner_plan); + graph_reset(node); - /* - * XXX: ExecFreeExprContext is not called here on purpose; the expr - * context is freed by the parent scan code. The scan slot and result - * slot are cleaned up by ExecEndPlan. - */ + for (int d = 0; d < node->ndepths; d++) + if (node->frames[d].inner_state != NULL) + ExecEndNode(node->frames[d].inner_state); } void ExecReScanGraphScan(GraphScanState * node) { - ExecScanReScan(&node->ss); + graph_reset(node); + node->need_seed = true; - if (node->inner_plan) + if (node->ss.ps.chgParam != NULL) { - if (node->ss.ps.chgParam != NULL) - UpdateChangedParamSet(node->inner_plan, node->ss.ps.chgParam); - - if (node->inner_plan->chgParam == NULL) - ExecReScan(node->inner_plan); + for (int d = 0; d < node->ndepths; d++) + if (node->frames[d].inner_state != NULL) + UpdateChangedParamSet(node->frames[d].inner_state, + node->ss.ps.chgParam); } } diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index a622e955d1a..6e7d42a863d 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -54,6 +54,7 @@ #include "rewrite/rewriteGraphTable.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" +#include "utils/array.h" #include "utils/selfuncs.h" #include "utils/syscache.h" @@ -3366,38 +3367,65 @@ resolve_edge_where_mutator(Node *node, Oid *elemoid) (void *) elemoid); } +/* + * Key-column mapping of one edge element of a hop (catatog pgesrckey / + * pgedestkey entry for the element). + */ +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) */ +} GraphHopArmKeys; + /* * 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, - * for every traversed edge: one column per VLE edge-list property (the edge's - * property value, accumulated into arrays by the executor), the edge's ctid, - * and the edge element table OID (so the executor can identify the source - * table of each row). + * for every traversed edge: + * + * one column per VLE edge-list property (the edge's property value, + * accumulated into arrays by the executor), + * the edge's ctid, + * the edge element table OID (so the executor can identify the source + * table of each row), + * the edge's source key columns (padded with NULLs to *max_nsrc), and + * the edge's destination key columns (padded with NULLs to *max_ndst). + * + * The hop-wide maxima *max_nsrc / *max_ndst are returned so the executor can + * 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 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, - List *edge_element_oids, List *array_props) + List *edge_element_oids, List *array_props, + int *max_nsrc, int *max_ndst) { List *arm_queries = NIL; + List *arm_keys = NIL; + int nprops = list_length(array_props); ListCell *lc; + *max_nsrc = 0; + *max_ndst = 0; + if (edge_element_oids == NIL) return NULL; + /* Read each edge element's src/dst key column mapping. */ foreach(lc, edge_element_oids) { Oid elemoid = lfirst_oid(lc); HeapTuple etup; Form_pg_propgraph_element pge; - Query *arm = makeNode(Query); - Relation rel; - ParseNamespaceItem *pni; - List *tlist = NIL; - List *quals = NIL; - int resno = 0; + GraphHopArmKeys *ak; + Datum datum; + Datum *d; + int n; etup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(elemoid)); if (!HeapTupleIsValid(etup)) @@ -3405,74 +3433,226 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, elemoid); pge = (Form_pg_propgraph_element) GETSTRUCT(etup); - arm->commandType = CMD_SELECT; + 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]); - rel = table_open(pge->pgerelid, 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); + ReleaseSysCache(etup); + + *max_nsrc = Max(*max_nsrc, ak->nsrc); + *max_ndst = Max(*max_ndst, ak->ndst); + arm_keys = lappend(arm_keys, ak); + } + + /* + * Type of each (padded) src/dst key slot: taken from the first arm that + * defines the slot; every other arm must use the same datatype. + */ + { + Oid *src_types = palloc_array(Oid, Max(*max_nsrc, 1)); + int32 *src_typmods = palloc_array(int32, Max(*max_nsrc, 1)); + Oid *src_colls = palloc_array(Oid, Max(*max_nsrc, 1)); + Oid *dst_types = palloc_array(Oid, Max(*max_ndst, 1)); + int32 *dst_typmods = palloc_array(int32, Max(*max_ndst, 1)); + Oid *dst_colls = palloc_array(Oid, Max(*max_ndst, 1)); + int ai = 0; + + memset(src_types, 0, sizeof(Oid) * (Size) Max(*max_nsrc, 1)); + memset(dst_types, 0, sizeof(Oid) * (Size) Max(*max_ndst, 1)); + + foreach_ptr(GraphHopArmKeys, ak, arm_keys) { - RangeTblRef *rtr = makeNode(RangeTblRef); + for (int k = 0; k < ak->nsrc; k++) + { + Oid typid; + int32 typmod; + Oid coll; + + get_atttypetypmodcoll(ak->arm_relid, ak->srckey[k], + &typid, &typmod, &coll); + if (src_types[k] == InvalidOid) + { + src_types[k] = typid; + src_typmods[k] = typmod; + src_colls[k] = coll; + } + else if (src_types[k] != typid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("graph hop source key column %d has different datatypes across edge elements", k + 1))); + } + for (int k = 0; k < ak->ndst; k++) + { + Oid typid; + int32 typmod; + Oid coll; - rtr->rtindex = 1; - arm->jointree = makeFromExpr(list_make1(rtr), NULL); + get_atttypetypmodcoll(ak->arm_relid, ak->dstkey[k], + &typid, &typmod, &coll); + if (dst_types[k] == InvalidOid) + { + dst_types[k] = typid; + dst_typmods[k] = typmod; + dst_colls[k] = coll; + } + else if (dst_types[k] != typid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("graph hop destination key column %d has different datatypes across edge elements", k + 1))); + } + ai++; } - /* Property value columns for the VLE edge-list refs. */ - foreach_ptr(GraphPropertyRef, gpr, array_props) + /* Build one Query per edge element (arm). */ + ai = 0; + foreach_ptr(GraphHopArmKeys, ak, arm_keys) { - Node *n; + Query *arm = makeNode(Query); + Relation rel; + ParseNamespaceItem *pni; + List *tlist = NIL; + List *quals = NIL; + int resno = 0; + + arm->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); + { + RangeTblRef *rtr = makeNode(RangeTblRef); + rtr->rtindex = 1; + arm->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)); + } + + /* Edge row identity: ctid. */ 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)); - } + makeTargetEntry((Expr *) makeVar(1, + SelfItemPointerAttributeNumber, + TIDOID, -1, + InvalidOid, 0), + resno, pstrdup("gs_ctid"), false)); + /* Source edge element table OID. */ + resno++; + tlist = lappend(tlist, + makeTargetEntry((Expr *) makeConst(OIDOID, -1, + InvalidOid, + sizeof(Oid), + ObjectIdGetDatum(ak->arm_relid), + false, true), + resno, pstrdup("gs_tbl"), false)); + + /* Source key columns (padded to *max_nsrc). */ + for (int k = 0; k < *max_nsrc; k++) + { + Node *n; - /* Edge row identity: ctid. */ - resno++; - tlist = lappend(tlist, - makeTargetEntry((Expr *) makeVar(1, - SelfItemPointerAttributeNumber, - TIDOID, -1, - InvalidOid, 0), - resno, pstrdup("gs_ctid"), false)); - /* Source edge element table OID. */ - resno++; - tlist = lappend(tlist, - makeTargetEntry((Expr *) makeConst(OIDOID, -1, - InvalidOid, - sizeof(Oid), - ObjectIdGetDatum(pge->pgerelid), - false, true), - resno, pstrdup("gs_tbl"), false)); - - arm->targetList = tlist; - - /* The edge's own WHERE, resolved against this edge element. */ - if (edge_gep->whereClause) - { - Node *w = copyObject(edge_gep->whereClause); + resno++; + if (k < ak->nsrc) + { + 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); + } + else + n = (Node *) makeNullConst(src_types[k], src_typmods[k], + src_colls[k]); + tlist = lappend(tlist, + makeTargetEntry((Expr *) n, resno, + psprintf("gs_src%d", k + 1), + false)); + } - IncrementVarSublevelsUp(w, 1, 1); - quals = lappend(quals, - resolve_edge_where_mutator(w, &elemoid)); - ((FromExpr *) arm->jointree)->quals = - (Node *) makeBoolExpr(AND_EXPR, quals, -1); - } + /* Destination key columns (padded to *max_ndst). */ + for (int k = 0; k < *max_ndst; k++) + { + Node *n; - ReleaseSysCache(etup); + resno++; + if (k < ak->ndst) + { + 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); + } + else + n = (Node *) makeNullConst(dst_types[k], dst_typmods[k], + dst_colls[k]); + tlist = lappend(tlist, + makeTargetEntry((Expr *) n, resno, + psprintf("gs_dst%d", k + 1), + false)); + } + + arm->targetList = tlist; + + /* The edge's own WHERE, resolved against this edge element. */ + if (edge_gep->whereClause) + { + Oid elemoid = lfirst_oid(list_nth_cell(edge_element_oids, ai)); + 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); + } - arm_queries = lappend(arm_queries, arm); + arm_queries = lappend(arm_queries, arm); + ai++; + } } if (list_length(arm_queries) == 1) @@ -3484,7 +3664,6 @@ build_graphscan_inner_query(Oid graphid, GraphElementPattern *edge_gep, List *rtable = NIL; Query *union_query; Query *sample_query = linitial_node(Query, arm_queries); - List *arms = arm_queries; Node *larg = NULL; int resno = 1; ListCell *lct, @@ -3574,7 +3753,8 @@ build_graphscan_inner_plan(PlannerInfo *root, Oid graphid, GraphElementPattern *edge_gep, List *edge_element_oids, List *array_props, - PlannerInfo **inner_rootp) + PlannerInfo **inner_rootp, + int *max_nsrc, int *max_ndst) { Query *qr; PlannerInfo *subroot; @@ -3583,7 +3763,7 @@ build_graphscan_inner_plan(PlannerInfo *root, Oid graphid, char *plan_name; qr = build_graphscan_inner_query(graphid, edge_gep, edge_element_oids, - array_props); + array_props, max_nsrc, max_ndst); if (qr == NULL) { *inner_rootp = NULL; @@ -3629,6 +3809,8 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, List *seed_key_cols = NIL; List *terminal_key_cols = NIL; List *edge_list_cols = NIL; + int max_nsrc = 0; + int max_ndst = 0; int col; ListCell *lc; @@ -3671,7 +3853,7 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, /* Edge element tables backing the hop's edge pattern. */ edge_element_oids = get_graph_edge_element_oids(rte->relid, edge_gep); - array_props = get_vle_array_props(rte, edge_gep->variable); + array_props = rte->graph_vle_props; /* * The internal single-hop query has no easy rowcount estimate; use a @@ -3682,7 +3864,7 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, /* 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); + &inner_root, &max_nsrc, &max_ndst); gpath = makeNode(GraphPath); gpath->path.pathtype = T_GraphScan; @@ -3712,9 +3894,13 @@ set_graphscan_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, gpath->terminal_key_cols = terminal_key_cols; gpath->edge_list_cols = edge_list_cols; gpath->edge_element_oids = edge_element_oids; + gpath->graph_columns = rte->graph_table_columns; gpath->inner_plan = inner_plan; gpath->subplan_params = (inner_root != NULL) ? inner_root->plan_params : NIL; - gpath->vid_param = -1; + gpath->seed_elem_oid = rte->graph_seed_elem_oid; + gpath->seed_param_ids = NIL; + gpath->max_nsrc = max_nsrc; + gpath->max_ndst = max_ndst; /* * Remember the inner (1-hop) planner root on the RelOptInfo, like diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index d9bef238fe6..6286507a188 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -3655,6 +3655,70 @@ create_graphscan_plan(PlannerInfo *root, GraphPath * best_path, replace_nestloop_params(root, (Node *) scan_clauses); } + /* + * Identify the nestloop params that supply the current seed key values. + * 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. + */ + { + 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); + + foreach(lc2, scan_clauses) + { + OpExpr *op = (OpExpr *) lfirst(lc2); + Var *var = NULL; + Param *param = NULL; + 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)) + { + var = linitial_node(Var, op->args); + param = lsecond_node(Param, op->args); + } + else if (IsA(linitial(op->args), Param) && + IsA(lsecond(op->args), Var)) + { + param = linitial_node(Param, op->args); + var = lsecond_node(Var, op->args); + } + else + continue; + + if (var->varno != scan_relid || var->varlevelsup != 0 || + param->paramkind != PARAM_EXEC) + continue; + + foreach_int(att, best_path->seed_key_cols) + { + if (att == var->varattno) + { + pos = amp; + break; + } + amp++; + } + if (pos >= 0) + lfirst_int(list_nth_cell(seed_params, pos)) = param->paramid; + } + + 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; + } + scan_plan->scan.plan.qual = scan_clauses; scan_plan->min_depth = best_path->min_depth; @@ -3664,8 +3728,11 @@ create_graphscan_plan(PlannerInfo *root, GraphPath * best_path, scan_plan->terminal_key_cols = best_path->terminal_key_cols; scan_plan->edge_list_cols = best_path->edge_list_cols; 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->vid_param = best_path->vid_param; + 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; copy_generic_path_info(&scan_plan->scan.plan, &best_path->path); diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c index c114577277a..025058943a1 100644 --- a/src/backend/rewrite/rewriteGraphTable.c +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -1357,6 +1357,8 @@ typedef struct native_vle_bind int gs_rti; /* RT index of the internal graph RTE */ int array_first; /* first array output attno on the graph RTE */ List *array_props; /* the factor's GraphPropertyRef* list */ + Node *seed_quals; /* ghost seed element's WHERE clause (the seed + * key equality with the previous segment) */ } native_vle_bind; /* Binding of a concrete element variable in a branch. */ @@ -1726,6 +1728,8 @@ native_build_vle_rte(RangeTblEntry *rte, native_vle_factor * vf, gs_rte->rellockmode = AccessShareLock; gs_rte->lateral = true; gs_rte->is_internal_graph = true; + gs_rte->graph_seed_elem_oid = srcpe->elemoid; + gs_rte->graph_vle_props = vf->array_props; perminfo = addRTEPermissionInfo(&branch->rteperminfos, gs_rte); perminfo->requiredPerms = ACL_SELECT; @@ -1892,6 +1896,15 @@ native_query_for_branch(native_decomp * dc, List *elems, List *vles) + list_length(get_graph_element_key_columns(termpe->elemoid, Anum_pg_propgraph_element_pgekey)); vb->array_props = vf->array_props; + { + RangeTblEntry *gs_rte = + list_nth(path_query->rtable, gs_rti - 1); + GraphElementPattern *pd; + + pd = linitial_node(GraphElementPattern, + linitial(gs_rte->graph_pattern->path_pattern_list)); + vb->seed_quals = copyObject(pd->whereClause); + } vle_binds = lappend(vle_binds, vb); } else @@ -1932,7 +1945,19 @@ native_query_for_branch(native_decomp * dc, List *elems, List *vles) if (pe == NULL) { - /* VLE factor: no branch-level qual here. */ + /* + * VLE factor: keep the ghost seed's key equality with the + * previous segment so the scan is parameterized by it. + */ + foreach_ptr(native_vle_bind, vb, vle_binds) + { + if (vb->gs_rti == i + 1) + { + if (vb->seed_quals) + qual_exprs = lappend(qual_exprs, vb->seed_quals); + break; + } + } } else if (IS_EDGE_PATTERN(pe->path_factor->kind)) { @@ -2045,8 +2070,8 @@ native_queries_recurse(native_decomp * dc, int facpos, List *elems, List *vles) native_vle_factor *vf = list_nth(dc->vle_factors, facpos); native_queries_recurse(dc, facpos + 1, - lappend(elems, NULL), - lappend(vles, vf)); + lappend(list_copy(elems), NULL), + lappend(list_copy(vles), vf)); } else { @@ -2055,8 +2080,8 @@ native_queries_recurse(native_decomp * dc, int facpos, List *elems, List *vles) struct path_element *pe = lfirst(lc); native_queries_recurse(dc, facpos + 1, - lappend(elems, pe), - lappend(vles, NULL)); + lappend(list_copy(elems), pe), + lappend(list_copy(vles), NULL)); } } } diff --git a/src/include/executor/nodeGraphScan.h b/src/include/executor/nodeGraphScan.h index fbe4d989f3e..3bf5cbe2cba 100644 --- a/src/include/executor/nodeGraphScan.h +++ b/src/include/executor/nodeGraphScan.h @@ -16,8 +16,71 @@ #include "nodes/execnodes.h" +typedef struct FmgrInfo FmgrInfo; + extern GraphScanState * ExecInitGraphScan(GraphScan * node, EState *estate, int eflags); extern void ExecEndGraphScan(GraphScanState * node); extern void ExecReScanGraphScan(GraphScanState * node); +/* + * One compiled edge element arm of the GraphScan's inner 1-hop expansion. + * The inner plan is a UNION ALL (or a single relation) over the arms; each + * row carries, in this order: + * + * 0 .. nprops-1 VLE edge-list property values + * nprops the edge's ctid (TIDOID) + * nprops+1 the edge element table OID + * nprops+2 .. +max_nsrc-1 the edge's source key columns (padded) + * nprops+2+max_nsrc .. +max_ndst-1 the edge's destination key columns + * + * An edge is traversable from the current vertex when the arm's source + * vertex element equals the current vertex's element and, given that, each + * source key column equals the current vertex's key value (compared with + * the default equality operator of the source key column's datatype). + * arm_src_first / arm_dst_first are 0-based row offsets of the groups. + */ +typedef struct GraphScanArmData +{ + Oid arm_relid; /* edge element table */ + Oid arm_srcvertex; /* source vertex element */ + Oid arm_dstvertex; /* destination vertex element */ + + int arm_nsrc; /* source key width */ + int arm_ndst; /* destination key width */ + + int arm_src_first; /* 0-based row offset of the src key group */ + int arm_dst_first; /* 0-based row offset of the dst key group */ + + FmgrInfo *arm_srceq; /* [arm_nsrc] default equality fmgr */ + Oid *arm_srccoll; /* [arm_nsrc] column collations */ + FmgrInfo *arm_dsteq; /* [arm_ndst] default equality fmgr */ + Oid *arm_dstcoll; /* [arm_ndst] column collations */ +} GraphScanArmData; + +/* + * One depth frame of the DFS: a copy of the inner 1-hop expansion plan, + * plus the vertex reached at this depth and the VLE property values of the + * edge that led here. + * + * frames[0] holds the seed vertex (no edge); frames[d] (d >= 1) holds the + * vertex reached after traversing the d-th edge of the current path. The + * inner scan state of a frame acts as a cursor: it is only started (rescan) + * when the frame is first pushed, and never again, so backtracking resumes + * the parent's scan exactly where it left off. + */ +typedef struct GraphDepthFrameData +{ + PlanState *inner_state; /* own copy of the inner 1-hop expansion */ + + Oid vid_elem; /* vertex element of the current vertex */ + int vid_nkeys; /* key width of the current vertex */ + Datum *vid; /* current vertex key values */ + bool *vidnull; + + Datum *edge_props; /* [nprops] VLE property values of the edge + * into this frame (frame 0: unused) */ + bool *edge_propsnull; + +} GraphDepthFrameData; + #endif /* NODEGRAPHSCAN_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 4cc53a03aec..b6f6e342ed2 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -752,6 +752,12 @@ typedef struct EState uint64 es_total_processed; /* total # of tuples aggregated across all * ExecutorRun() calls. */ + /* + * Depth of the currently active graph traversal (sum over all active + * GraphScans in the query), used to enforce max_graph_stack_depth. + */ + int es_graph_stack_depth; + int es_top_eflags; /* eflags passed to ExecutorStart */ int es_instrument; /* OR of InstrumentOption flags */ bool es_finished; /* true when ExecutorFinish is done */ @@ -1944,15 +1950,55 @@ typedef struct SubqueryScanState * GraphScanState information * * GraphScanState is used for scanning a graph pattern seek (a single - * quantified hop) in the range table. It keeps the inner (1-hop) plan - * as a nested child so that it can be displayed and (eventually) - * executed. + * quantified hop) in the range table. The variable-length hop is + * traversed with a depth-first search over per-depth copies of the + * inner 1-hop expansion plan (struct GraphDepthFrameData, defined in + * executor/nodeGraphScan.h). * ---------------- */ typedef struct GraphScanState { ScanState ss; /* its first field is NodeTag */ - PlanState *inner_plan; /* the inner (single quantified hop) plan */ + + /* Effective (clamped) depth bounds. */ + int min_depth; + int max_depth; + + /* Depth frames: one per active path level, [0..ndepths-1]. */ + int ndepths; + struct GraphDepthFrameData *frames; + int cur_depth; /* innermost active frame; -1 = need a new + * seed */ + + bool need_seed; /* params may hold a new seed (set on rescan) */ + bool seed_emitted; /* zero-hop seed row already emitted */ + + /* Vertex element the (ghost) seed belongs to. */ + Oid seed_elem; + + /* Number of VLE edge-list (array) output columns. */ + int nprops; + + /* Hop-wide max src/dest key widths over the edge element arms. */ + int max_nsrc; + int max_ndst; + + /* Per-arm edge element info (struct GraphScanArmData). */ + int narms; + struct GraphScanArmData *arms; + + /* PARAM_EXEC ids of the seed key columns (List of int), in key order. */ + List *seed_params; + + /* + * 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 + * context, which the (inner) child plans reset. + */ + Datum *tmp_vid; + bool *tmp_vidnull; + Datum *tmp_props; + bool *tmp_propsnull; } GraphScanState; /* ---------------- diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 6860442409c..0b106f14f7f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1284,6 +1284,14 @@ typedef struct RangeTblEntry */ bool is_internal_graph pg_node_attr(query_jumble_ignore); + /* + * For internal graph RTEs: the graph element (vertex) the ghost seed + * belongs to, and the VLE edge-list property references of the hidden + * quantified edge (as GraphPropertyRef nodes). Planner-internal only. + */ + Oid graph_seed_elem_oid pg_node_attr(query_jumble_ignore); + List *graph_vle_props pg_node_attr(query_jumble_ignore); + /* * Fields valid for a values RTE (else NIL): */ diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 0c56b2589f1..9b2bbe87a66 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -2226,6 +2226,9 @@ typedef struct GraphPath /* Edge element OIDs behind the inner expansion. */ List *edge_element_oids; + /* The internal RTE's output columns (see GraphScan). */ + List *graph_columns; + /* The parameterized 1-hop expansion plan. */ struct Plan *inner_plan; @@ -2235,8 +2238,15 @@ typedef struct GraphPath */ List *subplan_params; - /* PARAM_EXEC id of the current-vertex parameter. */ - int vid_param; + /* Vertex element the (ghost) seed belongs to. */ + Oid seed_elem_oid; + + /* PARAM_EXEC ids of the seed key columns (filled at create_plan time). */ + List *seed_param_ids; + + /* Hop-wide max src/dest key widths over the edge element arms. */ + int max_nsrc; + int max_ndst; } GraphPath; /* diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 87964cef733..ed2c6726813 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -606,6 +606,13 @@ typedef struct GraphScan */ List *edge_element_oids; /* List of Oid */ + /* + * The internal RTE's output columns (List of TargetEntry), in RTE column + * order (seed keys, terminal keys, VLE edge-list columns). Used by the + * executor to build the scan's (positional) tuple descriptor. + */ + List *graph_columns; + /* * The parameterized 1-hop expansion plan (the righttree). It was planned * out-of-band with rel->subroot (see allpaths.c), whose rtable is spliced @@ -614,8 +621,19 @@ typedef struct GraphScan */ Plan *inner_plan; - /* PARAM_EXEC id of the current-vertex parameter for inner rescans. */ - int vid_param; + /* Vertex element the (ghost) seed belongs to. */ + 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. + */ + List *seed_param_ids; + + /* Hop-wide max src/dest key widths over the edge element arms. */ + int max_nsrc; + int max_ndst; } GraphScan; /* ---------------- diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out index 01996a0da0a..9c676aff3e1 100644 --- a/src/test/regress/expected/graph_table.out +++ b/src/test/regress/expected/graph_table.out @@ -1070,305 +1070,160 @@ SELECT src.vname, count(*) FROM v1 AS src -- --------------------------------------------------------------------- -- Quantified (variable-length) hops are planned as GraphScan nodes by the --- native planner. Hop *execution* is not implemented yet, so the --- queries below use EXPLAIN; they will become plain SELECTs once the --- GraphScan executor lands. +-- native planner and executed by the executor. -- --------------------------------------------------------------------- -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); - QUERY PLAN -------------------------------------------------------------------------------------------------------------- - Append - -> Merge Join - Merge Cond: ((graph_scan.gs_term = v2.id1) AND (graph_scan.gs_term_1 = v2.id2)) - -> Sort - Sort Key: graph_scan.gs_term, graph_scan.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 - -> Graph Scan on graph_scan - min_depth: 1 - max_depth: 3 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2.id1, v2.id2 - -> Seq Scan on v2 - -> Nested Loop - -> Seq Scan on v3 - -> Materialize - -> Merge Join - Merge Cond: ((graph_scan_1.gs_term = v2_1.id1) AND (graph_scan_1.gs_term_1 = v2_1.id2)) - -> Sort - Sort Key: graph_scan_1.gs_term, graph_scan_1.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 v1_1 - -> Graph Scan on graph_scan_1 - min_depth: 1 - max_depth: 3 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2_1.id1, v2_1.id2 - -> Seq Scan on v2 v2_1 -(38 rows) - -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0}(c IS vl1) COLUMNS (a.vname AS src, c.vname AS dst)); - QUERY PLAN ---------------------------------------------- - Hash Join - Hash Cond: (graph_scan.gs_term = v1_1.id) - -> Nested Loop - -> Seq Scan on v1 - -> Graph Scan on graph_scan - min_depth: 0 - max_depth: 0 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Hash - -> Seq Scan on v1 v1_1 -(14 rows) - -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{2,}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); - QUERY PLAN -------------------------------------------------------------------------------------------------------------- - Append - -> Merge Join - Merge Cond: ((graph_scan.gs_term = v2.id1) AND (graph_scan.gs_term_1 = v2.id2)) - -> Sort - Sort Key: graph_scan.gs_term, graph_scan.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 - -> Graph Scan on graph_scan - min_depth: 2 - max_depth: -1 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2.id1, v2.id2 - -> Seq Scan on v2 - -> Nested Loop - -> Seq Scan on v3 - -> Materialize - -> Merge Join - Merge Cond: ((graph_scan_1.gs_term = v2_1.id1) AND (graph_scan_1.gs_term_1 = v2_1.id2)) - -> Sort - Sort Key: graph_scan_1.gs_term, graph_scan_1.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 v1_1 - -> Graph Scan on graph_scan_1 - min_depth: 2 - max_depth: -1 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2_1.id1, v2_1.id2 - -> Seq Scan on v2 v2_1 -(38 rows) - -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); - QUERY PLAN -------------------------------------------------------------------------------------------------------------- - Append - -> Merge Join - Merge Cond: ((graph_scan.gs_term = v2.id1) AND (graph_scan.gs_term_1 = v2.id2)) - -> Sort - Sort Key: graph_scan.gs_term, graph_scan.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 - -> Graph Scan on graph_scan - min_depth: 1 - max_depth: 2 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2.id1, v2.id2 - -> Seq Scan on v2 - -> Nested Loop - -> Seq Scan on v3 - -> Materialize - -> Merge Join - Merge Cond: ((graph_scan_1.gs_term = v2_1.id1) AND (graph_scan_1.gs_term_1 = v2_1.id2)) - -> Sort - Sort Key: graph_scan_1.gs_term, graph_scan_1.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 v1_1 - -> Graph Scan on graph_scan_1 - min_depth: 1 - max_depth: 2 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2_1.id1, v2_1.id2 - -> Seq Scan on v2 v2_1 -(38 rows) +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v22 + v11 | v31 + v11 | v33 + v12 | v21 + v12 | v21 + v13 | v23 + v13 | v23 +(7 rows) + +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0}(c IS vl1) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v11 + v12 | v12 + v13 | v13 +(3 rows) + +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{2,}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +ERROR: exceeded maximum graph traversal depth +HINT: Increase max_graph_stack_depth and retry, or try to remove the infinite loop +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v22 + v11 | v31 + v11 | v33 + v12 | v21 + v13 | v23 +(5 rows) -- a quantified hop followed by a fixed one -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{,2}(c IS vl3)->(d IS vl3) COLUMNS (a.vname AS src, d.vname AS dst)); - QUERY PLAN --------------------------- - Result - One-Time Filter: false -(2 rows) +SELECT src, dst, el1, el2 FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e1 IS el1]->{,2}(c IS vl3)-[e2]->(d IS vl3) COLUMNS (a.vname AS src, d.vname AS dst, e1.ename AS el1, e2.ename AS el2)) ORDER BY src, dst, el1, el2; + src | dst | el1 | el2 +-----+-----+--------+------ + v11 | v32 | {e121} | e231 + v11 | v33 | {e131} | e331 + v11 | v33 | {e131} | E331 +(3 rows) -- label disjunction inside the hop: the inner 1-hop expansion is a UNION ALL -- of the matching edge element tables -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl2 | vl3) COLUMNS (a.vname AS src, c.vname AS dst)); - QUERY PLAN -------------------------------------------------------------------------------------------------------------- - Append - -> Merge Join - Merge Cond: ((graph_scan.gs_term = v2.id1) AND (graph_scan.gs_term_1 = v2.id2)) - -> Sort - Sort Key: graph_scan.gs_term, graph_scan.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 - -> Graph Scan on graph_scan - min_depth: 1 - max_depth: 2 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2.id1, v2.id2 - -> Seq Scan on v2 - -> Nested Loop - -> Seq Scan on v3 - -> Materialize - -> Merge Join - Merge Cond: ((graph_scan_1.gs_term = v2_1.id1) AND (graph_scan_1.gs_term_1 = v2_1.id2)) - -> Sort - Sort Key: graph_scan_1.gs_term, graph_scan_1.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 v1_1 - -> Graph Scan on graph_scan_1 - min_depth: 1 - max_depth: 2 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2_1.id1, v2_1.id2 - -> Seq Scan on v2 v2_1 -(38 rows) +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl2 | vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v22 + v11 | v31 + v11 | v33 + v12 | v21 + v13 | v23 +(5 rows) -- VLE edge variable referenced outside the edge element: its value is the -- array of the property's value over every traversed edge -EXPLAIN (COSTS OFF) SELECT src, dst, el FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.ename AS el)); - QUERY PLAN -------------------------------------------------------------------------------------------------------------- - Append - -> Merge Join - Merge Cond: ((graph_scan.gs_term = v2.id1) AND (graph_scan.gs_term_1 = v2.id2)) - -> Sort - Sort Key: graph_scan.gs_term, graph_scan.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 - -> Graph Scan on graph_scan - min_depth: 1 - max_depth: 2 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2.id1, v2.id2 - -> Seq Scan on v2 - -> Nested Loop - -> Seq Scan on v3 - -> Materialize - -> Merge Join - Merge Cond: ((graph_scan_1.gs_term = v2_1.id1) AND (graph_scan_1.gs_term_1 = v2_1.id2)) - -> Sort - Sort Key: graph_scan_1.gs_term, graph_scan_1.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 v1_1 - -> Graph Scan on graph_scan_1 - min_depth: 1 - max_depth: 2 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2_1.id1, v2_1.id2 - -> Seq Scan on v2 v2_1 -(38 rows) +SELECT src, dst, el FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.ename AS el)) ORDER BY src, dst, el; + src | dst | el +-----+-----+-------- + v11 | v22 | {e121} + v11 | v31 | {e132} + v11 | v33 | {e131} + v12 | v21 | {e122} + v13 | v23 | {e123} +(5 rows) + +-- wrapped list expression +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.ename[1] AS first_e, cardinality(e.ename) AS n)) ORDER BY src, dst; + src | dst | first_e | n +-----+-----+---------+--- + v11 | v22 | e121 | 1 + v11 | v31 | e132 | 1 + v11 | v33 | e131 | 1 + v12 | v21 | e122 | 1 + v13 | v23 | e123 | 1 +(5 rows) + +-- zero-hop path yields an empty list +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0,2}(b IS vl1 | vl3) COLUMNS (a.vname AS src, b.vname AS dst, e.ename AS es)) ORDER BY src, dst, es; + src | dst | es +-----+-----+------------- + v11 | v11 | {} + v11 | v22 | {e121} + v11 | v31 | {e132} + v11 | v33 | {e131} + v12 | v12 | {} + v12 | v12 | {e122,e211} + v12 | v21 | {e122} + v13 | v13 | {} + v13 | v13 | {e123,e212} + v13 | v23 | {e123} +(10 rows) -- graph-level WHERE on the VLE list becomes a filter on the GraphScan -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) WHERE cardinality(e.ename) = 2 COLUMNS (a.vname AS src, c.vname AS dst)); - QUERY PLAN -------------------------------------------------------------------------------------------------------------- - Append - -> Merge Join - Merge Cond: ((graph_scan.gs_term = v2.id1) AND (graph_scan.gs_term_1 = v2.id2)) - -> Sort - Sort Key: graph_scan.gs_term, graph_scan.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 - -> Graph Scan on graph_scan - Filter: (cardinality(graph_scan.gs_arr) = 2) - min_depth: 1 - max_depth: 3 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2.id1, v2.id2 - -> Seq Scan on v2 - -> Nested Loop - -> Seq Scan on v3 - -> Materialize - -> Merge Join - Merge Cond: ((graph_scan_1.gs_term = v2_1.id1) AND (graph_scan_1.gs_term_1 = v2_1.id2)) - -> Sort - Sort Key: graph_scan_1.gs_term, graph_scan_1.gs_term_1 - -> Nested Loop - -> Seq Scan on v1 v1_1 - -> Graph Scan on graph_scan_1 - Filter: (cardinality(graph_scan_1.gs_arr) = 2) - min_depth: 1 - max_depth: 3 - direction: outgoing - -> Append - -> Seq Scan on e1_2 - -> Seq Scan on e1_3 - -> Seq Scan on e2_1 - -> Sort - Sort Key: v2_1.id1, v2_1.id2 - -> Seq Scan on v2 v2_1 -(40 rows) - --- executing a GraphScan is not yet implemented (phase D) -EXPLAIN ANALYZE SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); -ERROR: graph scan execution is not yet implemented +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) WHERE cardinality(e.ename) = 2 COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- +(0 rows) + +-- undirected quantified hop: each undirected edge yields a walk per orientation +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]-{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v22 + v11 | v31 + v11 | v33 + v12 | v21 + v12 | v21 + v13 | v23 + v13 | v23 +(7 rows) + +-- int-typed VLE edge list, empty for a zero-hop path +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0,2}(c IS vl1 | vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.eprop1 AS ep)) ORDER BY src, dst, ep; + src | dst | ep +-----+-----+--------------- + v11 | v11 | {} + v11 | v22 | {10001} + v11 | v31 | {10004} + v11 | v33 | {10003} + v12 | v12 | {} + v12 | v12 | {10002,10006} + v12 | v21 | {10002} + v13 | v13 | {} + v13 | v13 | {10007,10008} + v13 | v23 | {10007} +(10 rows) + +-- graph-level WHERE with relational references only (no VLE list) +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) WHERE a.vname = 'v11' COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v22 + v11 | v31 + v11 | v33 +(3 rows) + +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) WHERE c.vname = 'v33' COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v33 +(1 row) + +-- graph-level WHERE mixing a relational and a VLE-list reference +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) WHERE cardinality(e.ename) = 1 AND a.vname = 'v11' COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; + src | dst +-----+----- + v11 | v22 + v11 | v31 + v11 | v33 +(3 rows) + -- Locking clause on GRAPH_TABLE SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname)) gt FOR UPDATE OF gt; -- not supported ERROR: FOR UPDATE cannot be applied to GRAPH_TABLE diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql index 84a81434886..db1b95cd93a 100644 --- a/src/test/regress/sql/graph_table.sql +++ b/src/test/regress/sql/graph_table.sql @@ -649,27 +649,36 @@ SELECT src.vname, count(*) FROM v1 AS src -- --------------------------------------------------------------------- -- Quantified (variable-length) hops are planned as GraphScan nodes by the --- native planner. Hop *execution* is not implemented yet, so the --- queries below use EXPLAIN; they will become plain SELECTs once the --- GraphScan executor lands. +-- native planner and executed by the executor. -- --------------------------------------------------------------------- -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0}(c IS vl1) COLUMNS (a.vname AS src, c.vname AS dst)); -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{2,}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0}(c IS vl1) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{2,}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; -- a quantified hop followed by a fixed one -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{,2}(c IS vl3)->(d IS vl3) COLUMNS (a.vname AS src, d.vname AS dst)); +SELECT src, dst, el1, el2 FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e1 IS el1]->{,2}(c IS vl3)-[e2]->(d IS vl3) COLUMNS (a.vname AS src, d.vname AS dst, e1.ename AS el1, e2.ename AS el2)) ORDER BY src, dst, el1, el2; -- label disjunction inside the hop: the inner 1-hop expansion is a UNION ALL -- of the matching edge element tables -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl2 | vl3) COLUMNS (a.vname AS src, c.vname AS dst)); +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl2 | vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; -- VLE edge variable referenced outside the edge element: its value is the -- array of the property's value over every traversed edge -EXPLAIN (COSTS OFF) SELECT src, dst, el FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.ename AS el)); +SELECT src, dst, el FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.ename AS el)) ORDER BY src, dst, el; +-- wrapped list expression +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.ename[1] AS first_e, cardinality(e.ename) AS n)) ORDER BY src, dst; +-- zero-hop path yields an empty list +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0,2}(b IS vl1 | vl3) COLUMNS (a.vname AS src, b.vname AS dst, e.ename AS es)) ORDER BY src, dst, es; -- graph-level WHERE on the VLE list becomes a filter on the GraphScan -EXPLAIN (COSTS OFF) SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) WHERE cardinality(e.ename) = 2 COLUMNS (a.vname AS src, c.vname AS dst)); --- executing a GraphScan is not yet implemented (phase D) -EXPLAIN ANALYZE SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)); +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) WHERE cardinality(e.ename) = 2 COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +-- undirected quantified hop: each undirected edge yields a walk per orientation +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]-{1,2}(c IS vl3) COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +-- int-typed VLE edge list, empty for a zero-hop path +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{0,2}(c IS vl1 | vl3) COLUMNS (a.vname AS src, c.vname AS dst, e.eprop1 AS ep)) ORDER BY src, dst, ep; +-- graph-level WHERE with relational references only (no VLE list) +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) WHERE a.vname = 'v11' COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,2}(c IS vl3) WHERE c.vname = 'v33' COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; +-- graph-level WHERE mixing a relational and a VLE-list reference +SELECT src, dst FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[e IS el1]->{1,3}(c IS vl3) WHERE cardinality(e.ename) = 1 AND a.vname = 'v11' COLUMNS (a.vname AS src, c.vname AS dst)) ORDER BY src, dst; -- Locking clause on GRAPH_TABLE SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname)) gt FOR UPDATE OF gt; -- not supported -- 2.39.2