Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions age--1.7.0--y.y.y.sql
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,50 @@ BEGIN
END LOOP;
END;
$$;


--
-- S4: VLE SRF signature change
--
-- The age_vle SRF now emits start_id and end_id as scalar graphid columns
-- alongside the existing `edges` column. This allows the cypher transformer
-- to rewrite terminal-edge match quals as plain integer equalities,
-- removing the per-row age_match_vle_terminal_edge and age_match_two_vle_edges
-- function calls from VLE query plans. Both qual functions are dropped.
--
-- BREAKING CHANGE for any external SQL that called age_vle(...) directly
-- and relied on `RETURNS SETOF agtype`, or called age_match_vle_terminal_edge
-- / age_match_two_vle_edges directly. Internal AGE callers (the cypher
-- transformer) are not affected.
--
DROP FUNCTION IF EXISTS ag_catalog.age_match_vle_terminal_edge(variadic "any");
DROP FUNCTION IF EXISTS ag_catalog.age_match_two_vle_edges(agtype, agtype);

DROP FUNCTION IF EXISTS ag_catalog.age_vle(agtype, agtype, agtype, agtype,
agtype, agtype, agtype);
DROP FUNCTION IF EXISTS ag_catalog.age_vle(agtype, agtype, agtype, agtype,
agtype, agtype, agtype, agtype);

CREATE FUNCTION ag_catalog.age_vle(IN agtype, IN agtype, IN agtype, IN agtype,
IN agtype, IN agtype, IN agtype,
OUT edges agtype,
OUT start_id graphid,
OUT end_id graphid)
RETURNS SETOF record
LANGUAGE C
STABLE
CALLED ON NULL INPUT
PARALLEL UNSAFE
AS 'MODULE_PATHNAME';

CREATE FUNCTION ag_catalog.age_vle(IN agtype, IN agtype, IN agtype, IN agtype,
IN agtype, IN agtype, IN agtype, IN agtype,
OUT edges agtype,
OUT start_id graphid,
OUT end_id graphid)
RETURNS SETOF record
LANGUAGE C
STABLE
CALLED ON NULL INPUT
PARALLEL UNSAFE
AS 'MODULE_PATHNAME';
6 changes: 3 additions & 3 deletions regress/expected/cypher_match.out
Original file line number Diff line number Diff line change
Expand Up @@ -2784,13 +2784,13 @@ SELECT * FROM cypher('cypher_match', $$ MATCH p=()-[*]->() RETURN length(p) $$)
length
--------
1
2
1
2
1
1
2
1
2
1
(8 rows)

SELECT * FROM cypher('cypher_match', $$ MATCH p=()-[*]->() WHERE length(p) > 1 RETURN length(p) $$) as (length agtype);
Expand All @@ -2812,8 +2812,8 @@ SELECT * FROM cypher('cypher_match', $$ MATCH p=()-[*]->() WHERE size(nodes(p))
SELECT * FROM cypher('cypher_match', $$ MATCH (n {name:'Dave'}) MATCH p=()-[*]->() WHERE nodes(p)[0] = n RETURN length(p) $$) as (length agtype);
length
--------
1
2
1
(2 rows)

SELECT * FROM cypher('cypher_match', $$ MATCH p1=(n {name:'Dave'})-[]->() MATCH p2=()-[*]->() WHERE p2=p1 RETURN p2=p1 $$) as (path agtype);
Expand Down
28 changes: 14 additions & 14 deletions regress/expected/cypher_vle.out

Large diffs are not rendered by default.

64 changes: 32 additions & 32 deletions regress/expected/expr.out

Large diffs are not rendered by default.

31 changes: 10 additions & 21 deletions sql/agtype_typecast.sql
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@ PARALLEL SAFE
AS 'MODULE_PATHNAME';

-- original VLE function definition
-- S4: emit start_id/end_id as scalar columns to enable transformer rewrite
-- of terminal-edge quals as integer equalities (see PERF_VLE_TERMINAL_QUAL_PLAN).
CREATE FUNCTION ag_catalog.age_vle(IN agtype, IN agtype, IN agtype, IN agtype,
IN agtype, IN agtype, IN agtype,
OUT edges agtype)
RETURNS SETOF agtype
OUT edges agtype,
OUT start_id graphid,
OUT end_id graphid)
RETURNS SETOF record
LANGUAGE C
STABLE
CALLED ON NULL INPUT
Expand All @@ -84,8 +88,10 @@ AS 'MODULE_PATHNAME';
-- caching mechanism to coexist with the previous VLE version.
CREATE FUNCTION ag_catalog.age_vle(IN agtype, IN agtype, IN agtype, IN agtype,
IN agtype, IN agtype, IN agtype, IN agtype,
OUT edges agtype)
RETURNS SETOF agtype
OUT edges agtype,
OUT start_id graphid,
OUT end_id graphid)
RETURNS SETOF record
LANGUAGE C
STABLE
CALLED ON NULL INPUT
Expand All @@ -100,15 +106,6 @@ CREATE FUNCTION ag_catalog.age_build_vle_match_edge(agtype, agtype)
PARALLEL SAFE
AS 'MODULE_PATHNAME';

-- function to match a terminal vle edge
CREATE FUNCTION ag_catalog.age_match_vle_terminal_edge(variadic "any")
RETURNS boolean
LANGUAGE C
STABLE
CALLED ON NULL INPUT
PARALLEL SAFE
AS 'MODULE_PATHNAME';

-- function to create an AGTV_PATH from a VLE_path_container
CREATE FUNCTION ag_catalog.age_materialize_vle_path(agtype)
RETURNS agtype
Expand All @@ -135,14 +132,6 @@ RETURNS NULL ON NULL INPUT
PARALLEL SAFE
AS 'MODULE_PATHNAME';

CREATE FUNCTION ag_catalog.age_match_two_vle_edges(agtype, agtype)
RETURNS boolean
LANGUAGE C
STABLE
RETURNS NULL ON NULL INPUT
PARALLEL SAFE
AS 'MODULE_PATHNAME';

-- list functions
CREATE FUNCTION ag_catalog.age_keys(agtype)
RETURNS agtype
Expand Down
100 changes: 66 additions & 34 deletions src/backend/parser/cypher_clause.c
Original file line number Diff line number Diff line change
Expand Up @@ -3982,10 +3982,6 @@ static List *make_join_condition_for_edge(cypher_parsestate *cpstate,
{
Node *left_id = NULL;
Node *right_id = NULL;
String *ag_catalog = makeString("ag_catalog");
String *func_name;
List *qualified_func_name;
List *args = NIL;
List *quals = NIL;

/*
Expand All @@ -3998,56 +3994,86 @@ static List *make_join_condition_for_edge(cypher_parsestate *cpstate,
}

/*
* If the previous node and the next node are in the join tree, we need
* to create the age_match_vle_terminal_edge to compare the vle returned
* results against the two nodes.
* S5: if the previous and next nodes are both in the join tree,
* emit two graphid equality A_Exprs:
* <vle_alias>.start_id = prev_node.id
* <vle_alias>.end_id = next_node.id
* This replaces the historical per-row
* age_match_vle_terminal_edge(prev.id, next.id, edges)
* function call with plain integer (int8) equality quals on the
* SRF's S4 output columns. The planner can now drive the join
* directly on these keys (HashJoin hash keys, NestLoop index
* conditions where indexed).
*/
if (prev_node->in_join_tree)
{
func_name = makeString("age_match_vle_terminal_edge");
qualified_func_name = list_make2(ag_catalog, func_name);
ColumnRef *cr_start;
ColumnRef *cr_end;
A_Expr *eq_start;
A_Expr *eq_end;

/*
* Get the vertex's id and pass to the function. Pass in NULL
* otherwise.
*/
left_id = (Node *)make_qual(cpstate, prev_node, "id");
Assert(entity->vle_alias != NULL);

cr_start = makeNode(ColumnRef);
cr_start->fields = list_make2(makeString(entity->vle_alias),
makeString("start_id"));
cr_start->location = -1;

cr_end = makeNode(ColumnRef);
cr_end->fields = list_make2(makeString(entity->vle_alias),
makeString("end_id"));
cr_end->location = -1;
Comment on lines +4015 to +4025
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entity->vle_alias is only protected by Assert(). In production builds where asserts are disabled, a missing alias would lead to NULL being passed into makeString() and likely a crash while building the ColumnRef. Please replace the Assert with a runtime check (ereport(ERROR, ...) with a useful message) before constructing cr_start/cr_end.

Copilot uses AI. Check for mistakes.

left_id = (Node *)make_qual(cpstate, prev_node, "id");
right_id = (Node *)make_qual(cpstate, next_node, "id");

/* create the argument list */
args = list_make3(left_id, right_id, entity->expr);
eq_start = makeSimpleA_Expr(AEXPR_OP, "=",
(Node *)cr_start, left_id, -1);
eq_end = makeSimpleA_Expr(AEXPR_OP, "=",
(Node *)cr_end, right_id, -1);

/* add to quals */
quals = lappend(quals, makeFuncCall(qualified_func_name, args,
COERCE_EXPLICIT_CALL, -1));
quals = lappend(quals, eq_start);
quals = lappend(quals, eq_end);
}

/*
* When the previous node is not in the join tree, but there is a vle
* edge before that join, then we need to compare this vle's start node
* against the previous vle's end node. No need to check the next edge,
* because that would be redundant.
* S6: when the previous node is not in the join tree but there is
* a vle edge before that join, emit a single graphid equality
* connecting the two VLE SRFs:
*
* prev_vle.end_id = this_vle.start_id
*
* This replaces the per-row age_match_two_vle_edges(prev, this)
* function call with a plain int8 equality on the S4 scalar
* output columns of both age_vle SRFs. No detoasting of either
* VLE_path_container is needed.
*/
if (!prev_node->in_join_tree &&
prev_edge != NULL &&
prev_edge->type == ENT_VLE_EDGE)
{
List *qualified_name;
String *match_qual;
FuncCall *fc;
ColumnRef *cr_prev_end;
ColumnRef *cr_this_start;
A_Expr *eq_chain;

match_qual = makeString("age_match_two_vle_edges");
Assert(prev_edge->vle_alias != NULL);
Assert(entity->vle_alias != NULL);

/* make the qualified function name */
qualified_name = list_make2(ag_catalog, match_qual);
cr_prev_end = makeNode(ColumnRef);
cr_prev_end->fields = list_make2(makeString(prev_edge->vle_alias),
makeString("end_id"));
cr_prev_end->location = -1;

/* make the args */
args = list_make2(prev_edge->expr, entity->expr);
cr_this_start = makeNode(ColumnRef);
cr_this_start->fields = list_make2(makeString(entity->vle_alias),
makeString("start_id"));
Comment on lines +4059 to +4069
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prev_edge->vle_alias / entity->vle_alias are validated only via Assert(). If asserts are compiled out, a NULL alias will cause invalid ColumnRef construction and can crash the backend. Please add a non-asserting runtime check (ereport(ERROR, ...)) before using these pointers.

Copilot uses AI. Check for mistakes.
cr_this_start->location = -1;

/* create the function call */
fc = makeFuncCall(qualified_name, args, COERCE_EXPLICIT_CALL, -1);
eq_chain = makeSimpleA_Expr(AEXPR_OP, "=",
(Node *)cr_prev_end,
(Node *)cr_this_start, -1);

quals = lappend(quals, fc);
quals = lappend(quals, eq_chain);
}

return quals;
Expand Down Expand Up @@ -4898,6 +4924,12 @@ static transform_entity *transform_VLE_edge_entity(cypher_parsestate *cpstate,
vle_entity = make_transform_entity(cpstate, ENT_VLE_EDGE, (Node *)rel,
(Expr *)var);

/*
* S5: stash the auto-generated alias name so make_join_condition_for_edge
* can build ColumnRefs for the SRF's start_id/end_id output columns.
*/
vle_entity->vle_alias = alias->aliasname;

/* return the vle entity */
return vle_entity;
}
Expand Down
1 change: 1 addition & 0 deletions src/backend/parser/cypher_transform_entity.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ transform_entity *make_transform_entity(cypher_parsestate *cpstate,
entity->declared_in_current_clause = true;
entity->expr = expr;
entity->in_join_tree = expr != NULL;
entity->vle_alias = NULL;

return entity;
}
Expand Down
Loading
Loading