Skip to content

[Cypher] OR EXISTS + AND NOT (EXISTS … OR EXISTS …) returns 0 rows under two-outer-MATCH binding #4126

@Inodix

Description

@Inodix

Summary

A WHERE clause combining <bool> = true OR EXISTS { … } with a grouped AND NOT (EXISTS { … } OR EXISTS { … }) returns 0 rows when both outer entities are bound by separate MATCH statements
(cross-product) — even when the boolean branch alone matches. The same logic works correctly when the second entity is matched inside an outer EXISTS { MATCH … }.

Possibly related to but distinct from #4097 (which addressed EXISTS { RETURN n } over outer-variable expressions, fixed in 26.5.1).

Environment

  • ArcadeDB 26.5.1-SNAPSHOT (build 1dc68da159f12ef9c8c436e5b5998a7ab483714b)
  • HTTP API /api/v1/command/<db> with language=cypher
  • Reproduced fresh — no application-side wrappers

Reproduction

Schema + data:

CREATE VERTEX TYPE Owner;
CREATE PROPERTY Owner.id STRING;
CREATE VERTEX TYPE Item;
CREATE PROPERTY Item.id STRING;                                                                                                                                                                                
CREATE PROPERTY Item.flag BOOLEAN;
CREATE EDGE TYPE rel;                                                                                                                                                                                          
CREATE EDGE TYPE excl;                                    
CREATE (:Owner {id: "u"});
CREATE (:Item  {id: "c", flag: true});                                                                                                                                                                         

(No edges of any kind.)

Probe B1 — control: 2-MATCH + boolean only ✓

MATCH (u:Owner {id: "u"})                                 
MATCH (c:Item)
WHERE c.flag = true
RETURN c.id AS id

Returns [{"id":"c"}]

Probe B2 — 2-MATCH + bool OR EXISTS{…}

MATCH (u:Owner {id: "u"})                                 
MATCH (c:Item)
WHERE c.flag = true
   OR EXISTS { MATCH (u)-[:rel]->(c) }                                                                                                                                                                         
RETURN c.id AS id

Returns [{"id":"c"}] ✓ — boolean branch matches.

Probe B3 — adding AND NOT EXISTS { … }

MATCH (u:Owner {id: "u"})                                 
WHERE (c.flag = true OR EXISTS { MATCH (u)-[:rel]->(c) })
  AND NOT EXISTS { MATCH (u)-[:excl]->(c) }              
RETURN c.id AS id                                                                                                                                                                                              

Returns [] — expected 1 row (no excl edge means NOT EXISTS is true).

Probe B4 — full grouped exclusion ❌

MATCH (u:Owner {id: "u"})                                                                                                                                                                                      
MATCH (c:Item)                                            
WHERE (c.flag = true OR EXISTS { MATCH (u)-[:rel]->(c) })
  AND NOT (                                                                                                                                                                                                    
    EXISTS { MATCH (u)-[:excl]->(c) }
    OR EXISTS { MATCH (u)-[:excl*1..3]->(c) }                                                                                                                                                                  
  )                                                                                                                                                                                                            
RETURN c.id AS id                                                                                                                                                                                              

Returns [] — expected 1 row.

Probe B5 — control: same WHERE wrapped under single outer MATCH ❌

MATCH (c:Item)                                                                                                                                                                                                 
WHERE EXISTS {                                            
  MATCH (u:Owner {id: "u"})                                                                                                                                                                                    
  WHERE (c.flag = true OR EXISTS { MATCH (u)-[:rel]->(c) })
    AND NOT (                                                                                                                                                                                                  
      EXISTS { MATCH (u)-[:excl]->(c) }                                                                                                                                                                        
      OR EXISTS { MATCH (u)-[:excl*1..3]->(c) }
    )                                                                                                                                                                                                          
}                                                         
RETURN c.id AS id                                                                                                                                                                                              

Also returns [] — same bug surfaces when the same WHERE shape is nested.

Expected behavior

All four probes B2-B5 should return [{"id":"c"}] because:

  • The boolean branch (c.flag = true) matches — inclusion is satisfied.
  • There are no :excl edges from the owner — exclusion is empty.
  • Therefore (inclusion) AND NOT (exclusion) should be true.

Impact

This blocks a single-query implementation of any include/exclude resolver where multiple inclusion vectors (boolean class flag + EXISTS over edges) must combine with multiple exclusion vectors. The current
workaround is two separate UNION queries + application-side dedup (one inclusion query per branch + one exclusion query, with subtraction in the consumer), roughly 110 lines of extra code per resolver.

Possibly related to #4097 (fixed 2026-05-06, addressed EXISTS { RETURN n } shapes). The current case appears separate — inclusion EXISTS works (probe B2 ✓), exclusion-only NOT EXISTS works in isolation,
but the combination collapses every row.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No fields configured for Bug.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions