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
3 changes: 1 addition & 2 deletions cypher/models/pgsql/test/translation_cases/multipart.sql
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit
with s0 as (select 'a' as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where ((n0.properties ->> 'domain') = ' ' and (n0.properties ->> 'name') like s0.i0 || '%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as o from s1;

-- case: match (dc)-[r:EdgeKind1*0..]->(g:NodeKind1) where g.objectid ends with '-516' with collect(dc) as exclude match p = (c:NodeKind2)-[n:EdgeKind2]->(u:NodeKind2)-[:EdgeKind2*1..]->(g:NodeKind1) where g.objectid ends with '-512' and not c in exclude return p limit 100
with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.end_id join node n0 on n0.id = e0.start_id where ((n1.properties ->> 'objectid') like '%-516') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.start_id, s2.depth + 1, false, e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.end_id = s2.next_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and s2.depth <= 15 and not s2.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s2.path)) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join node n1 on n1.id = s2.root_id join node n0 on n0.id = s2.next_id) select array_remove(coalesce(array_agg(s1.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1), s3 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.i0 as i0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s0, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.id = e1.end_id where n3.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e1.kind_id = any (array [4]::int2[]) and (not n2.id = any ((s0.i0).id)) and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s3 join edge e2 on n4.id = e2.end_id join node n3 on (s3.n3).id = e2.start_id where ((n4.properties ->> 'objectid') like '%-512') and n4.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e2.kind_id = any (array [4]::int2[]) union select s5.root_id, e2.end_id, s5.depth + 1, false, e2.id = any (s5.path), s5.path || e2.id from s5 join edge e2 on e2.end_id = s5.next_id join node n3 on (s3.n3).id = e2.start_id where e2.kind_id = any (array [4]::int2[]) and s5.depth <= 15 and not s5.is_cycle) select s3.e1 as e1, (select array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite) from edge e2 where e2.id = any (s5.path)) as e2, s5.path as ep1, s3.i0 as i0, s3.n2 as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s3, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id) select edges_to_path(variadic array [(s4.e1).id]::int8[] || s4.ep1)::pathcomposite as p from s4 limit 100;
with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.end_id join node n0 on n0.id = e0.start_id where ((n1.properties ->> 'objectid') like '%-516') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.start_id, s2.depth + 1, false, e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.end_id = s2.next_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and s2.depth <= 15 and not s2.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s2.path)) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join node n1 on n1.id = s2.root_id join node n0 on n0.id = s2.next_id) select array_remove(coalesce(array_agg(s1.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1), s3 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.i0 as i0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s0, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.id = e1.end_id where n3.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e1.kind_id = any (array [4]::int2[]) and (not n2.id = any ((s0.i0).id)) and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s3 join edge e2 on (s3.n3).id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [4]::int2[]) union select s5.root_id, e2.end_id, s5.depth + 1, ((n4.properties ->> 'objectid') like '%-512') and n4.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.id = any (s5.path), s5.path || e2.id from s5 join edge e2 on e2.start_id = s5.next_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [4]::int2[]) and s5.depth <= 15 and not s5.is_cycle) select s3.e1 as e1, (select array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite) from edge e2 where e2.id = any (s5.path)) as e2, s5.path as ep1, s3.i0 as i0, s3.n2 as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s3, s5 join node n3 on n3.id = s5.root_id join node n4 on n4.id = s5.next_id) select edges_to_path(variadic array [(s4.e1).id]::int8[] || s4.ep1)::pathcomposite as p from s4 limit 100;

-- case: match (n:NodeKind1)<-[:EdgeKind1]-(:NodeKind2) where n.objectid ends with '-516' with n, count(n) as dc_count where dc_count = 1 return n
with s0 as (with s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'objectid') like '%-516') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0, count(s1.n0)::int8 as i0 from s1 group by n0) select s0.n0 as n from s0 where (s0.i0 = 1);
Expand All @@ -70,4 +70,3 @@ with s0 as (with s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.pr

-- case: match (cg:NodeKind1) where cg.name =~ ".*TT" and cg.domain = "MY DOMAIN" with collect (cg.email) as emails match (o:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) where g.name starts with "blah" and not g.email in emails return o
with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') ~ '.*TT' and (n0.properties ->> 'domain') = 'MY DOMAIN') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'email'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n1 on n1.id = e0.start_id join node n2 on n2.id = e0.end_id where (not (n2.properties ->> 'email') = any (s0.i0) and (n2.properties ->> 'name') like 'blah%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e0.kind_id = any (array [3]::int2[]) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s2.n1 as o from s2;

14 changes: 14 additions & 0 deletions cypher/models/pgsql/test/translation_cases/pattern_binding.sql
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,17 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e
-- case: match p = (n:NodeKind1)-[r]-(m:NodeKind1) return p
with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.end_id or n0.id = e0.start_id join node n1 on n1.id = e0.end_id or n1.id = e0.start_id where (n0.id <> n1.id) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select edges_to_path(variadic array [(s0.e0).id]::int8[])::pathcomposite as p from s0;

-- case: match p = (:NodeKind1)-[:EdgeKind1]->(:NodeKind2)-[:EdgeKind2*1..]->(t:NodeKind2) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000
with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e0.kind_id = any (array [3]::int2[]) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union select s2.root_id, e1.end_id, s2.depth + 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], e1.id = any (s2.path), s2.path || e1.id from s2 join edge e1 on e1.start_id = s2.next_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and s2.depth <= 15 and not s2.is_cycle) select s0.e0 as e0, (select array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite) from edge e1 where e1.id = any (s2.path)) as e1, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join node n1 on n1.id = s2.root_id join node n2 on n2.id = s2.next_id) select edges_to_path(variadic array [(s1.e0).id]::int8[] || s1.ep0)::pathcomposite as p from s1 limit 1000;
Comment on lines +56 to +57
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file size and read lines around 56-57
wc -l cypher/models/pgsql/test/translation_cases/pattern_binding.sql

Repository: SpecterOps/DAWGS

Length of output: 125


🏁 Script executed:

# Read the file to see the context around lines 56-57
sed -n '50,70p' cypher/models/pgsql/test/translation_cases/pattern_binding.sql | cat -n

Repository: SpecterOps/DAWGS

Length of output: 8025


🏁 Script executed:

# Search for all satisfied-related patterns in the file
rg -n "satisfied" cypher/models/pgsql/test/translation_cases/pattern_binding.sql -A 2 -B 2

Repository: SpecterOps/DAWGS

Length of output: 7644


Missing terminal-node filter in recursive path.
Line 56–57 computes satisfied but never filters on it, so the t.system_tags constraint from the Cypher predicate is not enforced in the SQL. The base case should also compute satisfied correctly rather than hardcoding false. Please add the filter and update the base case to match the pattern used in similar cases (lines 10, 42).

🐛 Suggested fix
- with s0 as (select ...), s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (
-   select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id]
+ with s0 as (select ...), s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (
+   select e1.start_id, e1.end_id, 1,
+     (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%')
+     and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[],
+     e1.start_id = e1.end_id, array [e1.id]
    from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id
    where e1.kind_id = any (array [4]::int2[])
    union
    select s2.root_id, e1.end_id, s2.depth + 1,
      (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%')
      and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[],
      e1.id = any (s2.path), s2.path || e1.id
    from s2 join edge e1 on e1.start_id = s2.next_id join node n2 on n2.id = e1.end_id
    where e1.kind_id = any (array [4]::int2[]) and s2.depth <= 15 and not s2.is_cycle
- ) select ... from s0, s2 join node n1 on n1.id = s2.root_id join node n2 on n2.id = s2.next_id) select ...
+ ) select ... from s0, s2 join node n1 on n1.id = s2.root_id join node n2 on n2.id = s2.next_id
+   where s2.satisfied
+ ) select ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypher/models/pgsql/test/translation_cases/pattern_binding.sql` around lines
56 - 57, The recursive CTE never applies the terminal-node predicate because the
s2 column "satisfied" is computed but not used and the base case sets satisfied
to false; update the s2 recursive definition in the CTE (symbols: s1, s2,
satisfied, path, ep0) so the base case computes satisfied using the same
expression as the recursive step (e.g. check coalesce((n2.properties ->>
'system_tags'),'')::text like '%admin_tier_0%' and n2.kind_ids @>
array[2]::int2[]) and then add a WHERE filter in the outer select from s2 (or
inside s1) to only emit rows where satisfied = true before constructing p;
ensure the modified logic mirrors the patterns used around lines 10 and 42
(compute satisfied in both base and recursive branches and filter by satisfied
when selecting paths).


-- case: match (u:NodeKind1) where u.samaccountname in ["foo", "bar"] match p = (u)-[:EdgeKind1|EdgeKind2*1..3]->(t) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s0 join edge e0 on e0.start_id = (s0.n0).id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.start_id = s2.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) and s2.depth <= 3 and not s2.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s2.path)) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join node n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied) select edges_to_path(variadic ep0)::pathcomposite as p from s1 limit 1000;

-- case: match (x:NodeKind1) where x.name = 'foo' match (y:NodeKind2) where y.name = 'bar' match p=(x)-[:EdgeKind1]->(y) return p
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') = 'foo') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') = 'bar') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s2.e0).id]::int8[])::pathcomposite as p from s2;

-- case: match (x:NodeKind1{name:'foo'}) match (y:NodeKind2{name:'bar'}) match p=(x)-[:EdgeKind1]->(y) return p
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.properties ->> 'name') = 'foo'), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and (n1.properties ->> 'name') = 'bar'), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s2.e0).id]::int8[])::pathcomposite as p from s2;

-- case: match (x:NodeKind1{name:'foo'}) match p=(x)-[:EdgeKind1]->(y:NodeKind2{name:'bar'}) return p
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.properties ->> 'name') = 'foo'), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.id = e0.end_id where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and (n1.properties ->> 'name') = 'bar' and e0.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p from s1;
5 changes: 4 additions & 1 deletion cypher/models/pgsql/translate/selectivity.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const (
// Unique node properties are both covered by a compatible index and unique, making them highly selective
selectivityWeightUniqueNodeProperty = 100

// Bound identifiers are heavily weighted for preserving join order integrity
selectivityWeightBoundIdentifier = 700

// Operators that narrow the search space are given a higher selectivity
selectivityWeightNarrowSearch = 30

Expand Down Expand Up @@ -189,7 +192,7 @@ func MeasureSelectivity(scope *Scope, owningIdentifierBound bool, expression pgs

// If the identifier is reified at this stage in the query then it's already selected
if owningIdentifierBound {
visitor.addSelectivity(selectivityWeightNarrowSearch)
visitor.addSelectivity(selectivityWeightBoundIdentifier)
}

if expression != nil {
Expand Down