Procedural BFS Examples¶
This page shows queries with variable-length paths (VLP) transpiled using the procedural BFS rendering mode.
Procedural BFS uses SQL scripting (BEGIN...END, WHILE) instead of WITH RECURSIVE CTEs. This enables a global visited set that prevents re-visiting nodes (shortest-path semantics).
Two materialization strategies are shown:
- Databricks (
temp_tables): UsesCREATE TEMPORARY TABLE+INSERT INTO. Fixed table names, O(1) visited reads per level. - PySpark 4.2 (
numbered_views): UsesEXECUTE IMMEDIATE+ numbered views. Dynamic names, UNION chain for visited.
Quick Start with GraphContext¶
The simplest way to use procedural BFS is via GraphContext:
PySpark: enable SQL scripting
Procedural BFS generates BEGIN...END blocks with DECLARE, WHILE, etc. PySpark requires SQL scripting to be enabled:
Databricks has SQL scripting enabled by default.
from gsql2rsql import GraphContext
graph = GraphContext(
spark=spark,
nodes_table="catalog.schema.nodes",
edges_table="catalog.schema.edges",
)
# Procedural BFS for Databricks (default)
sql = graph.transpile(
"""
MATCH (root {node_id: 'Alice'})
MATCH p = (root)-[*1..4]-()
UNWIND relationships(p) AS r
RETURN r
""",
vlp_rendering_mode="procedural",
materialization_strategy="temp_tables",
)
# Procedural BFS for PySpark 4.2+
sql_pyspark = graph.transpile(
"""
MATCH (root {node_id: 'Alice'})
MATCH p = (root)-[*1..4]-()
UNWIND relationships(p) AS r
RETURN r
""",
vlp_rendering_mode="procedural",
materialization_strategy="numbered_views",
)
Edge collection support
Procedural BFS supports UNWIND relationships(path) AS r to access edge properties. Each BFS result row is one edge, so UNWIND produces one row per traversed edge. nodes(path) is not supported (requires full path reconstruction). Use vlp_rendering_mode='cte' if you need it.
1. Variable-length path traversal (1 to 3 hops)¶
Source: Features
OpenCypher Query
Procedural BFS — Databricks (temp_tables)
BEGIN
DECLARE current_depth_1 INT DEFAULT 0;
DECLARE rows_in_frontier_1 BIGINT DEFAULT 1;
DROP TEMPORARY TABLE IF EXISTS bfs_visited_1;
DROP TEMPORARY TABLE IF EXISTS bfs_frontier_1;
DROP TEMPORARY TABLE IF EXISTS bfs_result_1;
DROP TEMPORARY TABLE IF EXISTS bfs_frontier_1_init;
DROP TEMPORARY TABLE IF EXISTS bfs_edges_keyed_1;
CREATE TEMPORARY TABLE bfs_visited_1 (node STRING);
CREATE TEMPORARY TABLE bfs_frontier_1 AS
SELECT n.id AS node
FROM catalog.demo.Person n
WHERE (n.name) = ('Alice');
INSERT INTO bfs_visited_1
SELECT node FROM bfs_frontier_1;
CREATE TEMPORARY TABLE bfs_result_1 (_row_id BIGINT, _next_node STRING, _bfs_depth INT);
CREATE TEMPORARY TABLE bfs_frontier_1_init AS
SELECT node FROM bfs_frontier_1;
SELECT IF(COUNT(*) > 1, RAISE_ERROR('gsql2rsql procedural BFS is single-source but the start filter matched multiple nodes. Use vlp_rendering_mode="cte" for multi-source traversals.'), TRUE) FROM bfs_frontier_1_init;
CREATE TEMPORARY TABLE bfs_edges_keyed_1 AS
SELECT e.*, MONOTONICALLY_INCREASING_ID() AS _row_id
FROM (
SELECT e.person_id, e.friend_id, e.since, e.strength
FROM catalog.demo.Knows e
) e;
WHILE rows_in_frontier_1 > 0 AND current_depth_1 < 3 DO
SET current_depth_1 = current_depth_1 + 1;
DROP TEMPORARY TABLE IF EXISTS bfs_edges_1;
CREATE TEMPORARY TABLE bfs_edges_1 AS
SELECT e._row_id, e.friend_id AS _next_node
FROM bfs_edges_keyed_1 e
INNER JOIN bfs_frontier_1 f ON e.person_id = f.node
WHERE e.friend_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM bfs_visited_1 v WHERE v.node = e.friend_id);
SET rows_in_frontier_1 = (SELECT COUNT(1) FROM bfs_edges_1);
IF rows_in_frontier_1 > 0 THEN
INSERT INTO bfs_visited_1
SELECT DISTINCT _next_node FROM bfs_edges_1;
DROP TEMPORARY TABLE bfs_frontier_1;
CREATE TEMPORARY TABLE bfs_frontier_1 AS
SELECT DISTINCT _next_node AS node FROM bfs_edges_1;
INSERT INTO bfs_result_1
SELECT *, current_depth_1 AS _bfs_depth FROM bfs_edges_1;
END IF;
END WHILE;
CREATE OR REPLACE TEMPORARY VIEW paths_1 AS
SELECT f0.node AS start_node, r._next_node AS end_node, r._bfs_depth AS depth,
e.person_id, e.friend_id, e.since, e.strength
FROM bfs_result_1 r
JOIN bfs_edges_keyed_1 e ON e._row_id = r._row_id
CROSS JOIN bfs_frontier_1_init f0;
SELECT
FIRST(_gsql2rsql_f_name) AS reachable
FROM (
SELECT
sink.id AS _gsql2rsql_f_id
,sink.name AS _gsql2rsql_f_name
,sink.age AS _gsql2rsql_f_age
,sink.nickname AS _gsql2rsql_f_nickname
,sink.salary AS _gsql2rsql_f_salary
,sink.active AS _gsql2rsql_f_active
,p.start_node
,p.end_node
,p.depth
FROM paths_1 p
JOIN catalog.demo.Person sink
ON sink.id = p.end_node
WHERE p.depth >= 1 AND p.depth <= 3
) AS _proj
GROUP BY TO_JSON(NAMED_STRUCT('_', _gsql2rsql_f_name));
END
Procedural BFS — PySpark 4.2 (numbered_views)
BEGIN
DECLARE bfs_depth_1 INT DEFAULT 0;
DECLARE bfs_frontier_count_1 BIGINT DEFAULT 1;
DECLARE bfs_union_sql_1 STRING DEFAULT '';
CREATE OR REPLACE TEMPORARY VIEW bfs_frontier_1_0 AS
SELECT n.id AS node
FROM catalog.demo.Person n
WHERE (n.name) = ('Alice');
SELECT IF(COUNT(*) > 1, RAISE_ERROR('gsql2rsql procedural BFS is single-source but the start filter matched multiple nodes. Use vlp_rendering_mode="cte" for multi-source traversals.'), TRUE) FROM bfs_frontier_1_0;
CREATE OR REPLACE TEMPORARY VIEW bfs_visited_1_0 AS
SELECT node FROM bfs_frontier_1_0;
WHILE bfs_frontier_count_1 > 0 AND bfs_depth_1 < 3 DO
SET bfs_depth_1 = bfs_depth_1 + 1;
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW bfs_edges_1_' || CAST(bfs_depth_1 AS STRING) || ' AS
SELECT e.person_id, e.friend_id, e.since, e.strength, e.friend_id AS _next_node, ' || CAST(bfs_depth_1 AS STRING) || ' AS _bfs_depth FROM catalog.demo.Knows e INNER JOIN bfs_frontier_1_' || CAST(bfs_depth_1 - 1 AS STRING) || ' f ON e.person_id = f.node WHERE e.friend_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM bfs_visited_1_' || CAST(bfs_depth_1 - 1 AS STRING) || ' v WHERE v.node = e.friend_id)';
EXECUTE IMMEDIATE
'SELECT COUNT(1) FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING) INTO bfs_frontier_count_1;
IF bfs_frontier_count_1 > 0 THEN
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW bfs_visited_1_' || CAST(bfs_depth_1 AS STRING) || ' AS
SELECT node FROM bfs_visited_1_' || CAST(bfs_depth_1 - 1 AS STRING) || '
UNION
SELECT DISTINCT _next_node AS node
FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW bfs_frontier_1_' || CAST(bfs_depth_1 AS STRING) || ' AS
SELECT DISTINCT _next_node AS node
FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
IF bfs_depth_1 >= 1 THEN
IF bfs_union_sql_1 = '' THEN
SET bfs_union_sql_1 =
'SELECT person_id, friend_id, since, strength, _next_node AS end_node, _bfs_depth AS depth FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
ELSE
SET bfs_union_sql_1 = bfs_union_sql_1
|| ' UNION ALL SELECT person_id, friend_id, since, strength, _next_node AS end_node, _bfs_depth AS depth FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
END IF;
END IF;
END IF;
END WHILE;
IF bfs_union_sql_1 != '' THEN
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW paths_1 AS
SELECT f0.node AS start_node, r.end_node, r.depth,
r.person_id, r.friend_id, r.since, r.strength
FROM (' || bfs_union_sql_1 || ') r
CROSS JOIN bfs_frontier_1_0 f0';
ELSE
CREATE OR REPLACE TEMPORARY VIEW paths_1 AS
SELECT
CAST(NULL AS STRING) AS start_node,
CAST(NULL AS STRING) AS end_node,
CAST(NULL AS INT) AS depth,
CAST(NULL AS STRING) AS person_id,
CAST(NULL AS STRING) AS friend_id,
CAST(NULL AS STRING) AS since,
CAST(NULL AS STRING) AS strength
WHERE 1 = 0;
END IF;
SELECT
FIRST(_gsql2rsql_f_name) AS reachable
FROM (
SELECT
sink.id AS _gsql2rsql_f_id
,sink.name AS _gsql2rsql_f_name
,sink.age AS _gsql2rsql_f_age
,sink.nickname AS _gsql2rsql_f_nickname
,sink.salary AS _gsql2rsql_f_salary
,sink.active AS _gsql2rsql_f_active
,p.start_node
,p.end_node
,p.depth
FROM paths_1 p
JOIN catalog.demo.Person sink
ON sink.id = p.end_node
WHERE p.depth >= 1 AND p.depth <= 3
) AS _proj
GROUP BY TO_JSON(NAMED_STRUCT('_', _gsql2rsql_f_name));
END
2. Variable-length with source AND sink filter pushdown¶
Source: Features
OpenCypher Query
Procedural BFS — Databricks (temp_tables)
BEGIN
DECLARE current_depth_1 INT DEFAULT 0;
DECLARE rows_in_frontier_1 BIGINT DEFAULT 1;
DROP TEMPORARY TABLE IF EXISTS bfs_visited_1;
DROP TEMPORARY TABLE IF EXISTS bfs_frontier_1;
DROP TEMPORARY TABLE IF EXISTS bfs_result_1;
DROP TEMPORARY TABLE IF EXISTS bfs_frontier_1_init;
DROP TEMPORARY TABLE IF EXISTS bfs_edges_keyed_1;
CREATE TEMPORARY TABLE bfs_visited_1 (node STRING);
CREATE TEMPORARY TABLE bfs_frontier_1 AS
SELECT n.id AS node
FROM catalog.demo.Person n
WHERE (n.age) > (30);
INSERT INTO bfs_visited_1
SELECT node FROM bfs_frontier_1;
CREATE TEMPORARY TABLE bfs_result_1 (_row_id BIGINT, _next_node STRING, _bfs_depth INT);
CREATE TEMPORARY TABLE bfs_frontier_1_init AS
SELECT node FROM bfs_frontier_1;
SELECT IF(COUNT(*) > 1, RAISE_ERROR('gsql2rsql procedural BFS is single-source but the start filter matched multiple nodes. Use vlp_rendering_mode="cte" for multi-source traversals.'), TRUE) FROM bfs_frontier_1_init;
CREATE TEMPORARY TABLE bfs_edges_keyed_1 AS
SELECT e.*, MONOTONICALLY_INCREASING_ID() AS _row_id
FROM (
SELECT e.person_id, e.friend_id, e.since, e.strength
FROM catalog.demo.Knows e
) e;
WHILE rows_in_frontier_1 > 0 AND current_depth_1 < 4 DO
SET current_depth_1 = current_depth_1 + 1;
DROP TEMPORARY TABLE IF EXISTS bfs_edges_1;
CREATE TEMPORARY TABLE bfs_edges_1 AS
SELECT e._row_id, e.friend_id AS _next_node
FROM bfs_edges_keyed_1 e
INNER JOIN bfs_frontier_1 f ON e.person_id = f.node
WHERE e.friend_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM bfs_visited_1 v WHERE v.node = e.friend_id);
SET rows_in_frontier_1 = (SELECT COUNT(1) FROM bfs_edges_1);
IF rows_in_frontier_1 > 0 THEN
INSERT INTO bfs_visited_1
SELECT DISTINCT _next_node FROM bfs_edges_1;
DROP TEMPORARY TABLE bfs_frontier_1;
CREATE TEMPORARY TABLE bfs_frontier_1 AS
SELECT DISTINCT _next_node AS node FROM bfs_edges_1;
IF current_depth_1 >= 2 THEN
INSERT INTO bfs_result_1
SELECT *, current_depth_1 AS _bfs_depth FROM bfs_edges_1;
END IF;
END IF;
END WHILE;
CREATE OR REPLACE TEMPORARY VIEW paths_1 AS
SELECT f0.node AS start_node, r._next_node AS end_node, r._bfs_depth AS depth,
e.person_id, e.friend_id, e.since, e.strength,
ARRAY(NAMED_STRUCT('person_id', e.person_id, 'friend_id', e.friend_id, 'since', e.since, 'strength', e.strength)) AS path_edges
FROM bfs_result_1 r
JOIN bfs_edges_keyed_1 e ON e._row_id = r._row_id
CROSS JOIN bfs_frontier_1_init f0;
SELECT
_gsql2rsql_a_id AS a_id
,_gsql2rsql_b_id AS b_id
FROM (
SELECT
sink.id AS _gsql2rsql_b_id
,sink.name AS _gsql2rsql_b_name
,sink.age AS _gsql2rsql_b_age
,sink.nickname AS _gsql2rsql_b_nickname
,sink.salary AS _gsql2rsql_b_salary
,sink.active AS _gsql2rsql_b_active
,source.id AS _gsql2rsql_a_id
,source.name AS _gsql2rsql_a_name
,source.age AS _gsql2rsql_a_age
,source.nickname AS _gsql2rsql_a_nickname
,source.salary AS _gsql2rsql_a_salary
,source.active AS _gsql2rsql_a_active
,p.start_node
,p.end_node
,p.depth
,p.path_edges AS _gsql2rsql_path_edges
FROM paths_1 p
JOIN catalog.demo.Person sink
ON sink.id = p.end_node
JOIN catalog.demo.Person source
ON source.id = p.start_node
WHERE p.depth >= 2 AND p.depth <= 4 AND (sink.age) > (50)
) AS _proj;
END
Procedural BFS — PySpark 4.2 (numbered_views)
BEGIN
DECLARE bfs_depth_1 INT DEFAULT 0;
DECLARE bfs_frontier_count_1 BIGINT DEFAULT 1;
DECLARE bfs_union_sql_1 STRING DEFAULT '';
CREATE OR REPLACE TEMPORARY VIEW bfs_frontier_1_0 AS
SELECT n.id AS node
FROM catalog.demo.Person n
WHERE (n.age) > (30);
SELECT IF(COUNT(*) > 1, RAISE_ERROR('gsql2rsql procedural BFS is single-source but the start filter matched multiple nodes. Use vlp_rendering_mode="cte" for multi-source traversals.'), TRUE) FROM bfs_frontier_1_0;
CREATE OR REPLACE TEMPORARY VIEW bfs_visited_1_0 AS
SELECT node FROM bfs_frontier_1_0;
WHILE bfs_frontier_count_1 > 0 AND bfs_depth_1 < 4 DO
SET bfs_depth_1 = bfs_depth_1 + 1;
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW bfs_edges_1_' || CAST(bfs_depth_1 AS STRING) || ' AS
SELECT e.person_id, e.friend_id, e.since, e.strength, e.friend_id AS _next_node, ' || CAST(bfs_depth_1 AS STRING) || ' AS _bfs_depth FROM catalog.demo.Knows e INNER JOIN bfs_frontier_1_' || CAST(bfs_depth_1 - 1 AS STRING) || ' f ON e.person_id = f.node WHERE e.friend_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM bfs_visited_1_' || CAST(bfs_depth_1 - 1 AS STRING) || ' v WHERE v.node = e.friend_id)';
EXECUTE IMMEDIATE
'SELECT COUNT(1) FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING) INTO bfs_frontier_count_1;
IF bfs_frontier_count_1 > 0 THEN
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW bfs_visited_1_' || CAST(bfs_depth_1 AS STRING) || ' AS
SELECT node FROM bfs_visited_1_' || CAST(bfs_depth_1 - 1 AS STRING) || '
UNION
SELECT DISTINCT _next_node AS node
FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW bfs_frontier_1_' || CAST(bfs_depth_1 AS STRING) || ' AS
SELECT DISTINCT _next_node AS node
FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
IF bfs_depth_1 >= 2 THEN
IF bfs_union_sql_1 = '' THEN
SET bfs_union_sql_1 =
'SELECT person_id, friend_id, since, strength, _next_node AS end_node, _bfs_depth AS depth FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
ELSE
SET bfs_union_sql_1 = bfs_union_sql_1
|| ' UNION ALL SELECT person_id, friend_id, since, strength, _next_node AS end_node, _bfs_depth AS depth FROM bfs_edges_1_' || CAST(bfs_depth_1 AS STRING);
END IF;
END IF;
END IF;
END WHILE;
IF bfs_union_sql_1 != '' THEN
EXECUTE IMMEDIATE
'CREATE OR REPLACE TEMPORARY VIEW paths_1 AS
SELECT f0.node AS start_node, r.end_node, r.depth,
r.person_id, r.friend_id, r.since, r.strength,
ARRAY(NAMED_STRUCT(''person_id'', r.person_id, ''friend_id'', r.friend_id, ''since'', r.since, ''strength'', r.strength)) AS path_edges
FROM (' || bfs_union_sql_1 || ') r
CROSS JOIN bfs_frontier_1_0 f0';
ELSE
CREATE OR REPLACE TEMPORARY VIEW paths_1 AS
SELECT
CAST(NULL AS STRING) AS start_node,
CAST(NULL AS STRING) AS end_node,
CAST(NULL AS INT) AS depth,
CAST(NULL AS STRING) AS person_id,
CAST(NULL AS STRING) AS friend_id,
CAST(NULL AS STRING) AS since,
CAST(NULL AS STRING) AS strength,
CAST(NULL AS ARRAY<STRUCT<person_id: STRING, friend_id: STRING, since: STRING, strength: STRING>>) AS path_edges
WHERE 1 = 0;
END IF;
SELECT
_gsql2rsql_a_id AS a_id
,_gsql2rsql_b_id AS b_id
FROM (
SELECT
sink.id AS _gsql2rsql_b_id
,sink.name AS _gsql2rsql_b_name
,sink.age AS _gsql2rsql_b_age
,sink.nickname AS _gsql2rsql_b_nickname
,sink.salary AS _gsql2rsql_b_salary
,sink.active AS _gsql2rsql_b_active
,source.id AS _gsql2rsql_a_id
,source.name AS _gsql2rsql_a_name
,source.age AS _gsql2rsql_a_age
,source.nickname AS _gsql2rsql_a_nickname
,source.salary AS _gsql2rsql_a_salary
,source.active AS _gsql2rsql_a_active
,p.start_node
,p.end_node
,p.depth
,p.path_edges AS _gsql2rsql_path_edges
FROM paths_1 p
JOIN catalog.demo.Person sink
ON sink.id = p.end_node
JOIN catalog.demo.Person source
ON source.id = p.start_node
WHERE p.depth >= 2 AND p.depth <= 4 AND (sink.age) > (50)
) AS _proj;
END