v20251226-0003-SQL-PGQ-Support-multi-pattern-path-matching-in-GRAPH.patch
application/octet-stream
Filename: v20251226-0003-SQL-PGQ-Support-multi-pattern-path-matching-in-GRAPH.patch
Type: application/octet-stream
Part: 1
From af6d9cd07c140ffed2c1672b66f0b76e28a2d48c Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Fri, 26 Dec 2025 20:17:29 +0900
Subject: [PATCH 3/3] SQL/PGQ: Support multi-pattern path matching in
GRAPH_TABLE
Add support for comma-separated path patterns in MATCH clause, e.g.:
MATCH (a)->(b), (b)->(c)
Path patterns with shared variables are merged into a single join.
Disconnected patterns (no shared variables) produce a Cartesian product,
consistent with SQL/PGQ standard and Neo4j Cypher semantics.
The parser already supported this syntax; this commit enables execution
by merging multiple path patterns instead of rejecting them.
---
src/backend/rewrite/rewriteGraphTable.c | 34 ++++-
src/test/regress/expected/graph_table.out | 167 ++++++++++++++++++++++
src/test/regress/sql/graph_table.sql | 85 +++++++++++
3 files changed, 281 insertions(+), 5 deletions(-)
diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
index 44bdc8a1f20..1420786fbb4 100644
--- a/src/backend/rewrite/rewriteGraphTable.c
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -121,16 +121,40 @@ rewriteGraphTable(Query *parsetree, int rt_index)
{
RangeTblEntry *rte;
Query *graph_table_query;
- List *path_pattern;
List *pathqueries = NIL;
+ int num_patterns;
rte = rt_fetch(rt_index, parsetree->rtable);
+ num_patterns = list_length(rte->graph_pattern->path_pattern_list);
- if (list_length(rte->graph_pattern->path_pattern_list) != 1)
- elog(ERROR, "unsupported path pattern list length");
+ if (num_patterns == 1)
+ {
+ /* Single path pattern - original behavior */
+ List *path_pattern = linitial(rte->graph_pattern->path_pattern_list);
+
+ pathqueries = generate_queries_for_path_pattern(rte, path_pattern);
+ }
+ else
+ {
+ /*
+ * Multiple path patterns - merge all element patterns into a single
+ * combined path pattern. Variables with the same name will be merged
+ * by generate_queries_for_path_pattern().
+ */
+ List *combined_pattern = NIL;
+ ListCell *lc;
+
+ foreach(lc, rte->graph_pattern->path_pattern_list)
+ {
+ List *path_pattern = (List *) lfirst(lc);
+
+ combined_pattern = list_concat(combined_pattern,
+ list_copy(path_pattern));
+ }
+
+ pathqueries = generate_queries_for_path_pattern(rte, combined_pattern);
+ }
- path_pattern = linitial(rte->graph_pattern->path_pattern_list);
- pathqueries = generate_queries_for_path_pattern(rte, path_pattern);
graph_table_query = generate_union_from_pathqueries(&pathqueries);
AcquireRewriteLocks(graph_table_query, true, false);
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index a7377a6d768..1c69104a0d8 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -1382,4 +1382,171 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES
ERROR: PROPERTY_NAMES() argument must be an element variable
LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_N...
^
+-- Multi-pattern path tests (comma-separated path patterns with shared variables)
+-- Basic multi-pattern: (a)->(b), (b)->(c) where b is shared
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[e1 IS el1]->(b IS vl2),
+ (b)-[e2 IS el2]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+ a_name | b_name | c_name
+--------+--------+--------
+ v11 | v22 | v32
+(1 row)
+
+-- Multi-pattern with same start vertex reaching different destinations
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (a)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+ a_name | b_name | c_name
+--------+--------+--------
+ v11 | v22 | v22
+ v11 | v22 | v31
+ v11 | v22 | v33
+ v12 | v21 | v21
+ v13 | v23 | v23
+(5 rows)
+
+-- Multi-pattern with three patterns sharing one variable
+SELECT a_name, b2_name, b3_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b2 IS vl2),
+ (a)-[]->(b3 IS vl3)
+ COLUMNS (a.vname AS a_name, b2.vname AS b2_name, b3.vname AS b3_name)
+) ORDER BY a_name, b2_name, b3_name;
+ a_name | b2_name | b3_name
+--------+---------+---------
+ v11 | v22 | v22
+ v11 | v22 | v31
+ v11 | v22 | v33
+ v12 | v21 | v21
+ v13 | v23 | v23
+(5 rows)
+
+-- Same variable with same label (should work)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (b IS vl2)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | c_name
+--------+--------+--------
+ v11 | v22 | v32
+(1 row)
+
+-- Same variable with different labels (conflict - what happens?)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (b IS vl3)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ERROR: element patterns with same variable name "b" but different label expressions are not supported
+-- Same variable with label OR expression (b IS vl2|vl3)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | c_name
+--------+--------+--------
+ v11 | v22 | v32
+ v11 | v33 | v33
+ v11 | v33 | v33
+(3 rows)
+
+-- Multi-pattern with LABELS() filtering in WHERE clause
+-- Filter shared variable b by label using LABELS() function
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl2' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+);
+ QUERY PLAN
+-------------------------------------------------------------------------------------------
+ Nested Loop
+ -> Nested Loop
+ -> Hash Join
+ Hash Cond: ((e2_3.id_2_1 = v2.id1) AND (e2_3.id_2_2 = v2.id2))
+ -> Seq Scan on e2_3
+ -> Hash
+ -> Merge Join
+ Merge Cond: ((e1_2.id_2_1 = v2.id1) AND (e1_2.id_2_2 = v2.id2))
+ -> Sort
+ Sort Key: e1_2.id_2_1, e1_2.id_2_2
+ -> Seq Scan on e1_2
+ -> Sort
+ Sort Key: v2.id1, v2.id2
+ -> Seq Scan on v2
+ -> Index Scan using v1_pkey on v1
+ Index Cond: (id = e1_2.id_1)
+ -> Index Scan using v3_pkey on v3
+ Index Cond: (id = e2_3.id_3)
+(18 rows)
+
+SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl2' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | b_labels | c_name
+--------+--------+--------------+--------
+ v11 | v22 | {l1,vl2,vl3} | v32
+(1 row)
+
+-- Multi-pattern with LABELS() filtering - no common labels (should prune all)
+-- b IS vl2|vl3 but WHERE filters by 'vl1' which has no overlap
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl1' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+);
+ QUERY PLAN
+---------------------------------
+ Result
+ Replaces: Scan on graph_table
+ One-Time Filter: false
+(3 rows)
+
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl1' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | c_name
+--------+--------+--------
+(0 rows)
+
+-- Disconnected patterns (no shared variables) - should produce cross product
+-- (a)->(b) and (c)->(d) are independent, result is Cartesian product
+-- vl1->vl2 has 3 paths, vl1->vl3 has 3 paths, so cross product = 3 x 3 = 9 rows
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (c IS vl1)-[]->(d IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name,
+ c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+ a_name | b_name | c_name | d_name
+--------+--------+--------+--------
+ v11 | v22 | v11 | v22
+ v11 | v22 | v11 | v31
+ v11 | v22 | v11 | v33
+ v11 | v22 | v12 | v21
+ v11 | v22 | v13 | v23
+ v12 | v21 | v11 | v22
+ v12 | v21 | v11 | v31
+ v12 | v21 | v11 | v33
+ v12 | v21 | v12 | v21
+ v12 | v21 | v13 | v23
+ v13 | v23 | v11 | v22
+ v13 | v23 | v11 | v31
+ v13 | v23 | v11 | v33
+ v13 | v23 | v12 | v21
+ v13 | v23 | v13 | v23
+(15 rows)
+
-- leave the objects behind for pg_upgrade/pg_dump tests
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
index 3e0b10e39d2..521fb42a616 100644
--- a/src/test/regress/sql/graph_table.sql
+++ b/src/test/regress/sql/graph_table.sql
@@ -767,4 +767,89 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES
-- PROPERTY_NAMES() error: non-variable argument
SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(123)));
+-- Multi-pattern path tests (comma-separated path patterns with shared variables)
+-- Basic multi-pattern: (a)->(b), (b)->(c) where b is shared
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[e1 IS el1]->(b IS vl2),
+ (b)-[e2 IS el2]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+
+-- Multi-pattern with same start vertex reaching different destinations
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (a)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+
+-- Multi-pattern with three patterns sharing one variable
+SELECT a_name, b2_name, b3_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b2 IS vl2),
+ (a)-[]->(b3 IS vl3)
+ COLUMNS (a.vname AS a_name, b2.vname AS b2_name, b3.vname AS b3_name)
+) ORDER BY a_name, b2_name, b3_name;
+
+-- Same variable with same label (should work)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (b IS vl2)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Same variable with different labels (conflict - what happens?)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (b IS vl3)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Same variable with label OR expression (b IS vl2|vl3)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Multi-pattern with LABELS() filtering in WHERE clause
+-- Filter shared variable b by label using LABELS() function
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl2' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+);
+
+SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl2' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Multi-pattern with LABELS() filtering - no common labels (should prune all)
+-- b IS vl2|vl3 but WHERE filters by 'vl1' which has no overlap
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl1' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+);
+
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+ (b)-[]->(c IS vl3)
+ WHERE 'vl1' = ANY(LABELS(b))
+ COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Disconnected patterns (no shared variables) - should produce cross product
+-- (a)->(b) and (c)->(d) are independent, result is Cartesian product
+-- vl1->vl2 has 3 paths, vl1->vl3 has 3 paths, so cross product = 3 x 3 = 9 rows
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+ MATCH (a IS vl1)-[]->(b IS vl2),
+ (c IS vl1)-[]->(d IS vl3)
+ COLUMNS (a.vname AS a_name, b.vname AS b_name,
+ c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+
-- leave the objects behind for pg_upgrade/pg_dump tests
--
2.50.1 (Apple Git-155)