@@ -508,6 +508,92 @@ TEST(cypher_exec_where_eq) {
508508 * call outright ("unexpected operator"); RETURN-side coalesce already
509509 * worked, so only the WHERE leaf needs it. Semantics: when the property is
510510 * missing/empty, the literal default is compared instead. */
511+ /* #797: variable-length / repeated-variable path semantics. Fixture:
512+ * loopy has a SELF-LOOP as one of its outbound CALLS edges plus a real
513+ * 2-chain loopy->mid->leaf. Correct openCypher semantics:
514+ * - a repeated node variable must unify: (a)-[:CALLS]->(a) matches ONLY
515+ * the self-loop, not every edge;
516+ * - relationship uniqueness within a path: the self-loop cannot be
517+ * traversed repeatedly, so no *k..k path exists beyond the real chain;
518+ * - the engine hop cap must not fabricate or silently truncate results. */
519+ TEST (cypher_exec_varlength_path_semantics_issue797 ) {
520+ cbm_store_t * s = cbm_store_open_memory ();
521+ cbm_store_upsert_project (s , "test" , "/tmp/test" );
522+ cbm_node_t loopy = {.project = "test" ,
523+ .label = "Function" ,
524+ .name = "loopy" ,
525+ .qualified_name = "test.mod.loopy" ,
526+ .file_path = "mod.go" ,
527+ .start_line = 1 ,
528+ .end_line = 2 };
529+ cbm_node_t mid = {.project = "test" ,
530+ .label = "Function" ,
531+ .name = "mid" ,
532+ .qualified_name = "test.mod.mid" ,
533+ .file_path = "mod.go" ,
534+ .start_line = 3 ,
535+ .end_line = 4 };
536+ cbm_node_t leaf = {.project = "test" ,
537+ .label = "Function" ,
538+ .name = "leaf" ,
539+ .qualified_name = "test.mod.leaf" ,
540+ .file_path = "mod.go" ,
541+ .start_line = 5 ,
542+ .end_line = 6 };
543+ int64_t id_loopy = cbm_store_upsert_node (s , & loopy );
544+ int64_t id_mid = cbm_store_upsert_node (s , & mid );
545+ int64_t id_leaf = cbm_store_upsert_node (s , & leaf );
546+ ASSERT_GT (id_loopy , 0 );
547+ cbm_edge_t self_loop = {
548+ .project = "test" , .source_id = id_loopy , .target_id = id_loopy , .type = "CALLS" };
549+ cbm_edge_t e1 = {
550+ .project = "test" , .source_id = id_loopy , .target_id = id_mid , .type = "CALLS" };
551+ cbm_edge_t e2 = {.project = "test" , .source_id = id_mid , .target_id = id_leaf , .type = "CALLS" };
552+ cbm_store_insert_edge (s , & self_loop );
553+ cbm_store_insert_edge (s , & e1 );
554+ cbm_store_insert_edge (s , & e2 );
555+
556+ /* Bug 1: repeated variable must unify — only the self-loop matches. */
557+ cbm_cypher_result_t r1 = {0 };
558+ ASSERT_EQ (cbm_cypher_execute (s , "MATCH (a)-[:CALLS]->(a) RETURN a.name" , "test" , 0 , & r1 ), 0 );
559+ ASSERT_EQ (r1 .row_count , 1 );
560+ cbm_cypher_result_free (& r1 );
561+
562+ /* Bug 2: *2..2 from loopy — only the REAL 2-chain (leaf); the self-loop
563+ * must not be reused to pad paths (relationship uniqueness). */
564+ cbm_cypher_result_t r2 = {0 };
565+ ASSERT_EQ (cbm_cypher_execute (s ,
566+ "MATCH (a {name: \"loopy\"})-[:CALLS*2..2]->(b) "
567+ "RETURN DISTINCT b.name" ,
568+ "test" , 0 , & r2 ),
569+ 0 );
570+ ASSERT_EQ (r2 .row_count , 1 ); /* leaf only */
571+ cbm_cypher_result_free (& r2 );
572+
573+ /* Bug 2 amplifier: no directed path of length 5 exists at all. */
574+ cbm_cypher_result_t r3 = {0 };
575+ ASSERT_EQ (cbm_cypher_execute (s ,
576+ "MATCH (a {name: \"loopy\"})-[:CALLS*5..5]->(b) "
577+ "RETURN b.name" ,
578+ "test" , 0 , & r3 ),
579+ 0 );
580+ ASSERT_EQ (r3 .row_count , 0 );
581+ cbm_cypher_result_free (& r3 );
582+
583+ /* Bug 3: a hop range beyond the engine ceiling must be an ADVERTISED
584+ * clamp, not silently indistinguishable from "no such path". */
585+ cbm_cypher_result_t r4 = {0 };
586+ ASSERT_EQ (
587+ cbm_cypher_execute (s , "MATCH (a)-[:CALLS*150..150]->(b) RETURN b.name" , "test" , 0 , & r4 ), 0 );
588+ ASSERT_EQ (r4 .row_count , 0 );
589+ ASSERT_NOT_NULL (r4 .warning );
590+ ASSERT_NOT_NULL (strstr (r4 .warning , "clamped" ));
591+ cbm_cypher_result_free (& r4 );
592+
593+ cbm_store_close (s );
594+ PASS ();
595+ }
596+
511597TEST (cypher_exec_where_coalesce_issue874 ) {
512598 cbm_store_t * s = cbm_store_open_memory ();
513599 cbm_store_upsert_project (s , "test" , "/tmp/test" );
@@ -2782,6 +2868,7 @@ SUITE(cypher) {
27822868 RUN_TEST (cypher_issue252_tointeger );
27832869 RUN_TEST (cypher_issue305_count_star_alias );
27842870 RUN_TEST (cypher_exec_where_eq );
2871+ RUN_TEST (cypher_exec_varlength_path_semantics_issue797 );
27852872 RUN_TEST (cypher_exec_where_coalesce_issue874 );
27862873 RUN_TEST (cypher_exec_where_regex );
27872874 RUN_TEST (cypher_exec_where_contains );
0 commit comments