diff --git a/.gitignore b/.gitignore index dfefea08..cc490c22 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # User IDE Files .idea +.zed # Local integration test datasets -integration/testdata/local/ \ No newline at end of file +integration/testdata/local/ diff --git a/cypher/models/pgsql/format/format.go b/cypher/models/pgsql/format/format.go index f3c54b55..9606d02f 100644 --- a/cypher/models/pgsql/format/format.go +++ b/cypher/models/pgsql/format/format.go @@ -279,6 +279,13 @@ func formatNode(builder *OutputBuilder, rootExpr pgsql.SyntaxNode) error { exprStack = append(exprStack, typedNextExpr.Name) + case pgsql.LateralSubquery: + if typedNextExpr.Binding.Set { + exprStack = append(exprStack, typedNextExpr.Binding.Value, pgsql.FormattingLiteral(" ")) + } + + exprStack = append(exprStack, pgsql.FormattingLiteral(")"), typedNextExpr.Query, pgsql.FormattingLiteral("lateral (")) + case pgsql.Assignment: exprStack = append(exprStack, typedNextExpr, @@ -532,6 +539,10 @@ func Expression(expression pgsql.SyntaxNode, builder *OutputBuilder) (string, er func formatSelect(builder *OutputBuilder, selectStmt pgsql.Select) error { builder.Write("select ") + if selectStmt.Distinct { + builder.Write("distinct ") + } + for idx, projection := range selectStmt.Projection { if idx > 0 { builder.Write(", ") @@ -783,6 +794,9 @@ func formatSetExpression(builder *OutputBuilder, expression pgsql.SetExpression) case pgsql.Values: return formatNode(builder, typedSetExpression) + case pgsql.Insert: + return formatInsertStatement(builder, typedSetExpression) + case pgsql.Update: return formatUpdateStatement(builder, typedSetExpression) @@ -909,7 +923,7 @@ func formatInsertStatement(builder *OutputBuilder, insert pgsql.Insert) error { return err } - if len(insert.Shape.Columns) > 0 { + if insert.Shape != nil && len(insert.Shape.Columns) > 0 { builder.Write(" (") for idx, column := range insert.Shape.Columns { diff --git a/cypher/models/pgsql/format/format_test.go b/cypher/models/pgsql/format/format_test.go index 0089124a..3c63ccf9 100644 --- a/cypher/models/pgsql/format/format_test.go +++ b/cypher/models/pgsql/format/format_test.go @@ -26,6 +26,73 @@ func TestFormat_TypeCastedParenthetical(t *testing.T) { require.Equal(t, "('str')::text", formattedQuery) } +func TestFormat_SelectDistinct(t *testing.T) { + formattedQuery, err := format.Statement(pgsql.Query{ + Body: pgsql.Select{ + Distinct: true, + Projection: []pgsql.SelectItem{ + pgsql.Identifier("id"), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{"node"}, + }, + }}, + }, + }, format.NewOutputBuilder()) + + require.Nil(t, err) + require.Equal(t, "select distinct id from node;", formattedQuery) +} + +func TestFormat_LateralSubqueryJoin(t *testing.T) { + formattedQuery, err := format.Statement(pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{"n", "id"}, + pgsql.CompoundIdentifier{"e", "id"}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{"node"}, + Binding: pgsql.AsOptionalIdentifier("n"), + }, + Joins: []pgsql.Join{{ + Table: pgsql.LateralSubquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{"e", "id"}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{"edge"}, + Binding: pgsql.AsOptionalIdentifier("e"), + }, + }}, + Where: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{"e", "start_id"}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{"n", "id"}, + ), + }, + Offset: pgsql.NewLiteral(0, pgsql.Int), + }, + Binding: pgsql.AsOptionalIdentifier("e"), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewLiteral(true, pgsql.Boolean), + }, + }}, + }}, + }, + }, format.NewOutputBuilder()) + + require.Nil(t, err) + require.Equal(t, "select n.id, e.id from node n join lateral (select e.id from edge e where e.start_id = n.id offset 0) e on true;", formattedQuery) +} + func TestFormat_Delete(t *testing.T) { formattedQuery, err := format.Statement(pgsql.Delete{ From: []pgsql.TableReference{{ diff --git a/cypher/models/pgsql/functions.go b/cypher/models/pgsql/functions.go index 3e88a96e..aed7174b 100644 --- a/cypher/models/pgsql/functions.go +++ b/cypher/models/pgsql/functions.go @@ -4,6 +4,7 @@ const ( FunctionUnidirectionalASPHarness Identifier = "unidirectional_asp_harness" FunctionUnidirectionalSPHarness Identifier = "unidirectional_sp_harness" FunctionBidirectionalASPHarness Identifier = "bidirectional_asp_harness" + FunctionBidirectionalSPHarness Identifier = "bidirectional_sp_harness" FunctionIntArrayUnique Identifier = "uniq" FunctionIntArraySort Identifier = "sort" FunctionJSONBToTextArray Identifier = "jsonb_to_text_array" @@ -26,10 +27,13 @@ const ( FunctionToUpper Identifier = "upper" FunctionCoalesce Identifier = "coalesce" FunctionUnnest Identifier = "unnest" + FunctionNextValue Identifier = "nextval" + FunctionPGGetSerialSequence Identifier = "pg_get_serial_sequence" FunctionJSONBSet Identifier = "jsonb_set" FunctionCount Identifier = "count" FunctionStringToArray Identifier = "string_to_array" FunctionEdgesToPath Identifier = "edges_to_path" + FunctionOrderedEdgesToPath Identifier = "ordered_edges_to_path" FunctionNodesToPath Identifier = "nodes_to_path" FunctionExtract Identifier = "extract" ) diff --git a/cypher/models/pgsql/model.go b/cypher/models/pgsql/model.go index e2e5b308..1d21be17 100644 --- a/cypher/models/pgsql/model.go +++ b/cypher/models/pgsql/model.go @@ -564,7 +564,7 @@ func (s FunctionCall) TypeHint() DataType { } type Join struct { - Table TableReference + Table Expression JoinOperator JoinOperator } @@ -724,6 +724,19 @@ func (s TableReference) NodeType() string { return "table_reference" } +type LateralSubquery struct { + Query Query + Binding models.Optional[Identifier] +} + +func (s LateralSubquery) AsExpression() Expression { + return s +} + +func (s LateralSubquery) NodeType() string { + return "lateral_subquery" +} + type FromClause struct { Source Expression Joins []Join @@ -956,6 +969,14 @@ type Insert struct { Returning []SelectItem } +func (s Insert) AsExpression() Expression { + return s +} + +func (s Insert) AsSetExpression() SetExpression { + return s +} + func (s Insert) AsStatement() Statement { return s } diff --git a/cypher/models/pgsql/test/query_test.go b/cypher/models/pgsql/test/query_test.go index f43979bc..38ec85ab 100644 --- a/cypher/models/pgsql/test/query_test.go +++ b/cypher/models/pgsql/test/query_test.go @@ -36,7 +36,7 @@ func TestQuery_KindGeneratesInclusiveKindMatcher(t *testing.T) { t.Errorf("could not build query: %v", err) } - translatedQuery, err := translate.Translate(context.Background(), builtQuery, mapper, nil) + translatedQuery, err := translate.Translate(context.Background(), builtQuery, mapper, nil, translate.DefaultGraphID) if err != nil { t.Errorf("could not translate query: %#v: %v", builtQuery, err) } diff --git a/cypher/models/pgsql/test/testcase.go b/cypher/models/pgsql/test/testcase.go index ecb26281..65dcf571 100644 --- a/cypher/models/pgsql/test/testcase.go +++ b/cypher/models/pgsql/test/testcase.go @@ -121,7 +121,7 @@ func (s *TranslationTestCase) WriteTo(output io.Writer, kindMapper pgsql.KindMap } } - if translation, err := translate.Translate(context.Background(), regularQuery, kindMapper, nil); err != nil { + if translation, err := translate.Translate(context.Background(), regularQuery, kindMapper, nil, translate.DefaultGraphID); err != nil { return err } else if formattedQuery, err := translate.Translated(translation); err != nil { return err @@ -164,7 +164,7 @@ func (s *TranslationTestCase) Assert(t *testing.T, expectedSQL string, kindMappe } } - if translation, err := translate.Translate(context.Background(), regularQuery, kindMapper, nil); err != nil { + if translation, err := translate.Translate(context.Background(), regularQuery, kindMapper, nil, translate.DefaultGraphID); err != nil { t.Fatalf("Failed to translate cypher query: %s - %v", s.Cypher, err) } else if formattedQuery, err := translate.Translated(translation); err != nil { t.Fatalf("Failed to format SQL translatedQuery: %v", err) @@ -200,7 +200,12 @@ func (s *TranslationTestCase) AssertLive(ctx context.Context, t *testing.T, driv } } - if translation, err := translate.Translate(context.Background(), regularQuery, driver.KindMapper(), s.CypherParams); err != nil { + defaultGraph, hasDefaultGraph := driver.DefaultGraph() + if !hasDefaultGraph { + t.Fatalf("Driver has no default graph set") + } + + if translation, err := translate.Translate(context.Background(), regularQuery, driver.KindMapper(), s.CypherParams, defaultGraph.ID); err != nil { t.Fatalf("Failed to translate cypher query: %s - %v", s.Cypher, err) } else if formattedQuery, err := translate.Translated(translation); err != nil { t.Fatalf("Failed to format SQL translatedQuery: %v", err) diff --git a/cypher/models/pgsql/test/translation_cases/create.sql b/cypher/models/pgsql/test/translation_cases/create.sql new file mode 100644 index 00000000..b3fc65ce --- /dev/null +++ b/cypher/models/pgsql/test/translation_cases/create.sql @@ -0,0 +1,75 @@ +-- Copyright 2026 Specter Ops, Inc. +-- +-- Licensed under the Apache License, Version 2.0 +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- case: create (n:NodeKind1 {name: 'Bob'}) return n +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'Bob')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id) select s2.n0 as n from s2; + +-- case: create (n:NodeKind1) +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object()::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id) select 1; + +-- case: create (n) +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array []::int2[], jsonb_build_object()::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id) select 1; + +-- case: create (n:NodeKind1:NodeKind2 {name: 'Bob', value: 1}) return n +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1, 2]::int2[], jsonb_build_object('name', 'Bob', 'value', 1)::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id) select s2.n0 as n from s2; + +-- case: create (n) return n +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array []::int2[], jsonb_build_object()::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id) select s2.n0 as n from s2; + +-- case: create (n:NodeKind1 {name: 'Alice', value: 42}) return n +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'Alice', 'value', 42)::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id) select s2.n0 as n from s2; + +-- case: match (n:NodeKind1) with n create (m:NodeKind2) return m +with s0 as (with s1 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[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id) select s4.n1 as m from s4; + +-- case: match (n:NodeKind1) with n create (m:NodeKind2 {name: 'Bob'}) return m +with s0 as (with s1 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[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object('name', 'Bob')::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id) select s4.n1 as m from s4; + +-- case: match (n:NodeKind1) with n create (m:NodeKind2) +with s0 as (with s1 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[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id) select 1; + +-- case: create (a:NodeKind1)-[:EdgeKind1]->(b:NodeKind2) +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object()::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object()::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select 1; + +-- case: create (a:NodeKind1)-[r:EdgeKind1]->(b:NodeKind2) return r +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object()::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object()::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select s8.e0 as r from s8; + +-- case: create (a:NodeKind1)-[:EdgeKind1 {name: 'rel'}]->(b:NodeKind2) +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object()::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object('name', 'rel')::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select 1; + +-- case: create (a:NodeKind1)<-[:EdgeKind1]-(b:NodeKind2) +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object()::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n1).id, (s6.n0).id, 3, jsonb_build_object()::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select 1; + +-- case: match (a:NodeKind1) with a create (a)-[:EdgeKind1]->(b:NodeKind2) +with s0 as (with s1 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[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id), s5 as (select s4.n0 as n0, s4.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s4), s6 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s5.e0_id, (s5.n0).id, (s5.n1).id, 3, jsonb_build_object()::jsonb from s5 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s7 as (select s5.n0 as n0, s5.n1 as n1, s6.e0 as e0 from s5, s6 where s6.e0_id = s5.e0_id) select 1; + +-- case: match (a:NodeKind1) with a create (a)-[r:EdgeKind1]->(b:NodeKind2) return r +with s0 as (with s1 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[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object()::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id), s5 as (select s4.n0 as n0, s4.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s4), s6 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s5.e0_id, (s5.n0).id, (s5.n1).id, 3, jsonb_build_object()::jsonb from s5 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s7 as (select s5.n0 as n0, s5.n1 as n1, s6.e0 as e0 from s5, s6 where s6.e0_id = s5.e0_id) select s7.e0 as r from s7; + +-- case: create (a:NodeKind1 {name: 'abc'})-[:EdgeKind1 {prop: 123}]->(:NodeKind2 {name: 'test'}) return a +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'abc')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object('name', 'test')::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object('prop', 123)::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select s8.n0 as a from s8; + +-- case: create (:NodeKind1 {name: 'abc'})-[:EdgeKind1 {prop: 123}]->(c:NodeKind2 {name: 'test'}) return c +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'abc')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object('name', 'test')::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object('prop', 123)::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select s8.n1 as c from s8; + +-- case: create (:NodeKind1 {name: 'abc'})-[:EdgeKind1 {prop: 123}]->(c:NodeKind2 {name: 'test'})<-[:EdgeKind2]-(:NodeKind1 {name: 'other'}) return c +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'abc')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object('name', 'test')::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n2_id from s5), s7 as (insert into node (graph_id, id, kind_ids, properties) select 0, s6.n2_id, array [1]::int2[], jsonb_build_object('name', 'other')::jsonb from s6 returning id as n2_id, (id, kind_ids, properties)::nodecomposite as n2), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.n2 as n2 from s6, s7 where s7.n2_id = s6.n2_id), s9 as (select s8.n0 as n0, s8.n1 as n1, s8.n2 as n2, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s8), s10 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s9.e0_id, (s9.n0).id, (s9.n1).id, 3, jsonb_build_object('prop', 123)::jsonb from s9 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s11 as (select s9.n0 as n0, s9.n1 as n1, s9.n2 as n2, s10.e0 as e0 from s9, s10 where s10.e0_id = s9.e0_id), s12 as (select s11.e0 as e0, s11.n0 as n0, s11.n1 as n1, s11.n2 as n2, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e1_id from s11), s13 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s12.e1_id, (s12.n2).id, (s12.n1).id, 4, jsonb_build_object()::jsonb from s12 returning id as e1_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e1), s14 as (select s12.e0 as e0, s12.n0 as n0, s12.n1 as n1, s12.n2 as n2, s13.e1 as e1 from s12, s13 where s13.e1_id = s12.e1_id) select s14.n1 as c from s14; + +-- case: create p = (:NodeKind1 {name: 'abc'})-[:EdgeKind1 {prop: 123}]->(:NodeKind2 {name: 'test'}) return p +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'abc')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object('name', 'test')::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object('prop', 123)::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select (array [s8.n0, s8.n1]::nodecomposite[], array [s8.e0]::edgecomposite[])::pathcomposite as p from s8; + +-- case: match (a:NodeKind1) with a create (b:NodeKind2 {source: a.name}) return a, b +with s0 as (with s1 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[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object('source', ((s2.n0).properties ->> 'name'))::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id) select s4.n0 as a, s4.n1 as b from s4; diff --git a/cypher/models/pgsql/test/translation_cases/delete.sql b/cypher/models/pgsql/test/translation_cases/delete.sql index 386769e7..540765f8 100644 --- a/cypher/models/pgsql/test/translation_cases/delete.sql +++ b/cypher/models/pgsql/test/translation_cases/delete.sql @@ -18,8 +18,8 @@ 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[]), s1 as (delete from node n1 using s0 where (s0.n0).id = n1.id) select 1; -- case: match ()-[r:EdgeKind1]->() delete r -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 e0.kind_id = any (array [3]::int2[])), s1 as (delete from edge e1 using s0 where (s0.e0).id = e1.id) select 1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (delete from edge e1 using s0 where (s0.e0).id = e1.id) select 1; -- case: match ()-[]->()-[r:EdgeKind1]->() delete r -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 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 [3]::int2[])), s2 as (delete from edge e2 using s1 where (s1.e1).id = e2.id) select 1; +with s0 as (select (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), s1 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 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 [3]::int2[])), s2 as (delete from edge e2 using s1 where (s1.e1).id = e2.id) select 1; diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index 4bfe3c50..7238eaad 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -24,22 +24,22 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'value'))::int8 = 1) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') = 'me')) select s3.n1 as n1 from s3), s4 as (select s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where (n2.id = (s2.n1).id)) select s4.n2 as b from s4; -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, collect(distinct(n)) as p where size(p) >= 10 return m -with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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]::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 n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (array_length(s0.i0, 1)::int >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (array_length(s0.i0, 1)::int >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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]::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 n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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]::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 n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: with 365 as max_days match (n:NodeKind1) where n.pwdlastset < (datetime().epochseconds - (max_days * 86400)) and not n.pwdlastset IN [-1.0, 0.0] return n limit 100 with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (not ((n0.properties ->> 'pwdlastset'))::float8 = any (array [- 1, 0]::float8[]) and ((n0.properties ->> 'pwdlastset'))::numeric < (extract(epoch from now()::timestamp with time zone)::numeric - (s0.i0 * 86400))) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1 limit 100; -- case: match (n:NodeKind1) where n.hasspn = true and n.enabled = true and not n.objectid ends with '-502' and not coalesce(n.gmsa, false) = true and not coalesce(n.msa, false) = true match (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) with distinct n, count(c) as adminCount return n order by adminCount desc limit 100 -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'hasspn'))::bool = true and ((n0.properties ->> 'enabled'))::bool = true and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1 join edge e0 on e0.start_id = (s1.n0).id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s3.path), s3.path || e0.id from s3 join edge e0 on e0.start_id = s3.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) and s3.depth < 15 and not s3.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 (s3.path)) as e0, s3.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join node n0 on n0.id = s3.root_id join node n1 on n1.id = s3.next_id where s3.satisfied) select s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'hasspn'))::bool = true and ((n0.properties ->> 'enabled'))::bool = true and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s1.n0).id as root_id from s1), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s3.path || e0.id from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s3.depth < 15 and not s3.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s3.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.next_id offset 0) n1 on true where s3.satisfied and (s1.n0).id = s3.root_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; -- case: match (n:NodeKind1) where n.objectid = 'S-1-5-21-1260426776-3623580948-1897206385-23225' match p = (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'objectid') = 'S-1-5-21-1260426776-3623580948-1897206385-23225') 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, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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 < 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 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; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'objectid') = 'S-1-5-21-1260426776-3623580948-1897206385-23225') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, s1.e0, array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1; -- case: match (g1:NodeKind1) where g1.name starts with 'test' with collect (g1.domain) as excludes match (d:NodeKind2) where d.name starts with 'other' and not d.name in excludes return d with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like 'test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'domain'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (not (n1.properties ->> 'name') = any (s0.i0) and (n1.properties ->> 'name') like 'other%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]) select s2.n1 as d from s2; @@ -48,47 +48,46 @@ 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 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 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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n3.id = e1.end_id where e1.kind_id = any (array [4]::int2[])), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, ((n4.properties ->> 'objectid') like '%-512') and n4.kind_ids operator (pg_catalog.@>) array [1]::int2[], 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 where s5.satisfied and (s3.n3).id = s5.root_id) select edges_to_path(variadic array [(s4.e1).id]::int8[] || s4.ep1)::pathcomposite as p from s4 where (not (s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i0) as _unnest_elem))) limit 100; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties ->> 'objectid') like '%-516') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), 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 s2_seed join edge e0 on e0.end_id = s2_seed.root_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, false, false, e0.id || s2.path from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.next_id offset 0) n0 on true) 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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n3.id = e1.end_id where e1.kind_id = any (array [4]::int2[])), s4 as (with recursive s5_seed(root_id) as not materialized (select distinct (s3.n3).id as root_id from s3), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, ((n4.properties ->> 'objectid') like '%-512') and n4.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.start_id = e2.end_id, array [e2.id] from s5_seed join edge e2 on e2.start_id = s5_seed.root_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [4]::int2[]) union all 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[], false, s5.path || e2.id from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [4]::int2[]) offset 0) e2 on true join node n4 on n4.id = e2.end_id where s5.depth < 15 and not s5.is_cycle) select s3.e1 as e1, (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s5.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) 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 lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s5.root_id offset 0) n3 on true join lateral (select n4.id, n4.kind_ids, n4.properties from node n4 where n4.id = s5.next_id offset 0) n4 on true where s5.satisfied and (s3.n3).id = s5.root_id) select ordered_edges_to_path(s4.n2, array [s4.e1]::edgecomposite[] || s4.e2, array [s4.n2, s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4 where (not (s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i0) as _unnest_elem))) 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.properties ->> 'objectid') like '%-516') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::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); +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on ((n0.properties ->> 'objectid') like '%-516') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::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); -- case: match (n:NodeKind1)-[:EdgeKind1]->(m:NodeKind2) where n.enabled = true with n, collect(distinct(n)) as p where size(p) >= 100 match p = (n)-[:EdgeKind1]->(m) return p limit 10 -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.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n0 as n0, array_remove(coalesce(array_agg(distinct (s1.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1 group by n0), s2 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.i0 as i0, s0.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (array_length(s0.i0, 1)::int >= 100) and (s0.n0).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s2.e1).id]::int8[])::pathcomposite as p from s2 limit 10; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (((n0.properties ->> 'enabled'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n0 as n0, array_remove(coalesce(array_agg(distinct (s1.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1 group by n0), s2 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.i0 as i0, s0.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (array_length(s0.i0, 1)::int >= 100) and (s0.n0).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) limit 10) select (array [s2.n0, s2.n2]::nodecomposite[], array [s2.e1]::edgecomposite[])::pathcomposite as p from s2 limit 10; -- case: with "a" as check, "b" as ref match p = (u)-[:EdgeKind1]->(g:NodeKind1) where u.name starts with check and u.domain = ref with collect(tolower(g.samaccountname)) as refmembership, tolower(u.samaccountname) as samname return refmembership, samname -with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and (n0.properties ->> 'name') like s0.i0 || '%')) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::anyarray, array []::text[])::anyarray, null)::anyarray as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by s2.n0) select s1.i2 as refmembership, s1.i3 as samname from s1; +with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and (n0.properties ->> 'name') like s0.i0 || '%')) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::anyarray, array []::text[])::anyarray, null)::anyarray as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by s2.n0) select s1.i2 as refmembership, s1.i3 as samname from s1; -- case: with "a" as check, "b" as ref match p = (u)-[:EdgeKind1]->(g:NodeKind1) where u.name starts with check and u.domain = ref with collect(tolower(g.samaccountname)) as refmembership, tolower(u.samaccountname) as samname match (u)-[:EdgeKind2]-(g:NodeKind1) where tolower(u.samaccountname) = samname and not tolower(g.samaccountname) IN refmembership return g -with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and (n0.properties ->> 'name') like s0.i0 || '%')) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::anyarray, array []::text[])::anyarray, null)::anyarray as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by s2.n0), s3 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on (n2.id = e1.end_id or n2.id = e1.start_id) join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n3.id = e1.end_id or n3.id = e1.start_id) where (n2.id <> n3.id) and (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; +with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and (n0.properties ->> 'name') like s0.i0 || '%')) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::anyarray, array []::text[])::anyarray, null)::anyarray as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by s2.n0), s3 as (select s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on (n2.id = e1.end_id or n2.id = e1.start_id) join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n3.id = e1.end_id or n3.id = e1.start_id) where (n2.id <> n3.id) and (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; -- case: with "a" as check, "b" as ref match p = (u)-[:EdgeKind1]->(g:NodeKind1) where u.name starts with check and u.domain = ref with collect(tolower(g.samaccountname)) as refmembership, tolower(u.samaccountname) as samname match (u)-[:EdgeKind2]->(g:NodeKind1) where tolower(u.samaccountname) = samname and not tolower(g.samaccountname) IN refmembership return g -with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and (n0.properties ->> 'name') like s0.i0 || '%')) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::anyarray, array []::text[])::anyarray, null)::anyarray as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by s2.n0), s3 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n3.id = e1.end_id where (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; +with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and (n0.properties ->> 'name') like s0.i0 || '%')) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::anyarray, array []::text[])::anyarray, null)::anyarray as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by s2.n0), s3 as (select s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n3.id = e1.end_id where (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; -- case: match p =(n:NodeKind1)<-[r:EdgeKind1|EdgeKind2*..3]-(u:NodeKind1) where n.domain = 'test' with n, count(r) as incomingCount where incomingCount > 90 with collect(n) as lotsOfAdmins match p =(n:NodeKind1)<-[:EdgeKind1]-() where n in lotsOfAdmins return p -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, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where ((n0.properties ->> 'domain') = 'test') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3, 4]::int2[]) union select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.end_id = s2.next_id join node n1 on n1.id = e0.start_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 s2 join node n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s4.e1).id]::int8[])::pathcomposite as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'domain') = 'test') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select (array [s4.n2, s4.n3]::nodecomposite[], array [s4.e1]::edgecomposite[])::pathcomposite as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); -- case: match (u:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) with g match (g)<-[:EdgeKind1]-(u:NodeKind1) return g -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n1 as n1 from s1), s2 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select s2.n1 as g from s2; +with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n1 as n1 from s1), s2 as (select s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select s2.n1 as g from s2; -- 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 n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (not (n2.properties ->> 'email') = any (s0.i0) and (n2.properties ->> 'name') like 'blah%')) select s2.n1 as o from s2; +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 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 n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (not (n2.properties ->> 'email') = any (s0.i0) and (n2.properties ->> 'name') like 'blah%')) select s2.n1 as o from s2; -- case: match (e) match p = ()-[]->(e) return p limit 1 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), 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.end_id join node n1 on n1.id = e0.start_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p from s1 limit 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), 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.end_id join node n1 on n1.id = e0.start_id) select (array [s1.n1, s1.n0]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p from s1 limit 1; -- case: match p = (a)-[]->() match q = ()-[]->(a) return p, q -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p, edges_to_path(variadic array [(s1.e1).id]::int8[])::pathcomposite as q from s1; +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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p, (array [s1.n2, s1.n0]::nodecomposite[], array [s1.e1]::edgecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] union select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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 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 n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s0, edge e2 join node n4 on n4.id = e2.end_id join node n3 on n3.id = e2.start_id where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0)) union select s5.root_id, e2.start_id, s5.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], 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 n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (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, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id where s5.satisfied) select edges_to_path(variadic ep1)::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id union select s5.root_id, e2.start_id, s5.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s5.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join lateral (select n4.id, n4.kind_ids, n4.properties from node n4 where n4.id = s5.root_id offset 0) n4 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s5.next_id offset 0) n3 on true where s5.satisfied) select ordered_edges_to_path(s4.n3, s4.e2, array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; -- case: match (m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)-[:EdgeKind2]->(c3:NodeKind1) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and not m.samaccountname contains "DEX" and not g.name IN ["D"] and not m.samaccountname =~ "^.*$" with collect(g.name) as admingroups match p=(m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and g.name in admingroups and not m.samaccountname =~ "^.*$" return p -with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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]::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 n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s0, edge e2 join node n4 on n4.id = e2.end_id join node n3 on n3.id = e2.start_id where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0)) and e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], 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 n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) and s5.depth < 15 and not s5.is_cycle) select (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, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id where s5.satisfied) select edges_to_path(variadic ep1)::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s5.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join lateral (select n4.id, n4.kind_ids, n4.properties from node n4 where n4.id = s5.root_id offset 0) n4 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s5.next_id offset 0) n3 on true where s5.satisfied) select ordered_edges_to_path(s4.n3, s4.e2, array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; -- case: match (a:NodeKind2)-[:EdgeKind1]->(g:NodeKind1)-[:EdgeKind2]->(s:NodeKind2) with count(a) as uc where uc > 5 match p = (a)-[:EdgeKind1]->(g)-[:EdgeKind2]->(s) return p -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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, (e3.id, e3.start_id, e3.end_id, e3.kind_id, e3.properties)::edgecomposite as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select edges_to_path(variadic array [(s4.e2).id, (s4.e3).id]::int8[])::pathcomposite as p from s4; - --- case: optional match (g:NodeKind1)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); +with s0 as (with s1 as (select (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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, (e3.id, e3.start_id, e3.end_id, e3.kind_id, e3.properties)::edgecomposite as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select (array [s4.n3, s4.n4, s4.n5]::nodecomposite[], array [s4.e2, s4.e3]::edgecomposite[])::pathcomposite as p from s4; +-- case: match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g +with s0 as (with s1 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[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0 from s1 join edge e0 on (s1.n0).id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s3 as (select s1.n0 as n0, s2.e0 as e0 from s1 left outer join s2 on (s1.n0 = s2.n0)) select s3.n0 as n0, count(s3.e0)::int8 as i0 from s3 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); diff --git a/cypher/models/pgsql/test/translation_cases/nodes.sql b/cypher/models/pgsql/test/translation_cases/nodes.sql index c5e3621c..967b7b7b 100644 --- a/cypher/models/pgsql/test/translation_cases/nodes.sql +++ b/cypher/models/pgsql/test/translation_cases/nodes.sql @@ -73,7 +73,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') = any (array ['option 1', 'option 2']::text[]))) select s0.n0 as s from s0; -- case: match (s) where toLower(s.name) = '1234' return distinct s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (lower((n0.properties ->> 'name'))::text = '1234')) select s0.n0 as s from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (lower((n0.properties ->> 'name'))::text = '1234')) select distinct s0.n0 as s from s0; -- case: match (s:NodeKind1), (e:NodeKind2) where s.name = e.name return s, e 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[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((s0.n0).properties -> 'name') = (n1.properties -> 'name')) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]) select s1.n0 as s, s1.n1 as e from s1; @@ -184,13 +184,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id)); -- case: match (s) where not (s)-[]->()-[]->() return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with 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), s2 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select count(*) > 0 from s2)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select 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), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select count(*) > 0 from s2)); -- case: match (s) where not (s)-[{prop: 'a'}]-({name: 'n3'}) return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with 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.end_id or (s0.n0).id = e0.start_id) join node n1 on (n1.properties ->> 'name') = 'n3' and (n1.id = e0.end_id or n1.id = e0.start_id) where ((s0.n0).id <> n1.id) and (e0.properties ->> 'prop') = 'a') select count(*) > 0 from s1)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on ((s0.n0).id = e0.end_id or (s0.n0).id = e0.start_id) join node n1 on (n1.properties ->> 'name') = 'n3' and (n1.id = e0.end_id or n1.id = e0.start_id) where ((s0.n0).id <> n1.id) and (e0.properties ->> 'prop') = 'a') select count(*) > 0 from s1)); -- case: match (s) where not (s)<-[{prop: 'a'}]-({name: 'n3'}) return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with 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.end_id join node n1 on (n1.properties ->> 'name') = 'n3' and n1.id = e0.start_id where (e0.properties ->> 'prop') = 'a') select count(*) > 0 from s1)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on (n1.properties ->> 'name') = 'n3' and n1.id = e0.start_id where (e0.properties ->> 'prop') = 'a') select count(*) > 0 from s1)); -- case: match (n:NodeKind1) where n.distinguishedname = toUpper('admin') return n with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'distinguishedname') = upper('admin')::text) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; @@ -205,7 +205,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'distinguishedname') like '%' || upper('admin')::text) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; -- case: match (s) where not (s)-[{prop: 'a'}]->({name: 'n3'}) return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with 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.properties ->> 'name') = 'n3' and n1.id = e0.end_id where (e0.properties ->> 'prop') = 'a') select count(*) > 0 from s1)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on (n1.properties ->> 'name') = 'n3' and n1.id = e0.end_id where (e0.properties ->> 'prop') = 'a') select count(*) > 0 from s1)); -- case: match (s) where not (s)-[]-() return id(s) with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (s0.n0).id from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id)); diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index 4f95084b..45a43cf0 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -15,65 +15,64 @@ -- SPDX-License-Identifier: Apache-2.0 -- case: match p = (:NodeKind1) 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[]) select nodes_to_path(variadic array [(s0.n0).id]::int8[])::pathcomposite as p from s0; +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[]) select (array [s0.n0]::nodecomposite[], array []::edgecomposite[])::pathcomposite as p from s0; -- case: match p = (n:NodeKind1) where n.name contains 'test' return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like '%test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select nodes_to_path(variadic array [(s0.n0).id]::int8[])::pathcomposite as p from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like '%test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select (array [s0.n0]::nodecomposite[], array []::edgecomposite[])::pathcomposite as p from s0; -- case: match p = ()-[]->() 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.start_id join node n1 on n1.id = e0.end_id) select edges_to_path(variadic array [(s0.e0).id]::int8[])::pathcomposite as p from s0; +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) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0; -- case: match p=(:NodeKind1)-[r]->(:NodeKind1) where r.isacl return p limit 100 -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where (((e0.properties ->> 'isacl'))::bool)) select edges_to_path(variadic array [(s0.e0).id]::int8[])::pathcomposite as p from s0 limit 100; +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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where (((e0.properties ->> 'isacl'))::bool) limit 100) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 100; -- case: match p = ()-[r1]->()-[r2]->(e) return e -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s1.n2 as e from s1; +with s0 as (select (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), s1 as (select s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s1.n2 as e from s1; -- case: match ()-[r1]->()-[r2]->()-[]->() where r1.name = 'a' and r2.name = 'b' return r1 -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 ((e0.properties ->> 'name') = 'a')), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where ((e1.properties ->> 'name') = 'b')), s2 as (select s1.e0 as e0, s1.e1 as e1, (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e2, s1.n0 as n0, s1.n1 as n1, s1.n2 as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id) select s2.e0 as r1 from s2; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (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 ((e0.properties ->> 'name') = 'a')), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where ((e1.properties ->> 'name') = 'b')), s2 as (select s1.e0 as e0, s1.e1 as e1, s1.n1 as n1, s1.n2 as n2 from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id) select s2.e0 as r1 from s2; -- case: match p = (a)-[]->()<-[]-(f) where a.name = 'value' and f.is_target 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.properties ->> 'name') = 'value') and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id) select edges_to_path(variadic array [(s1.e0).id, (s1.e1).id]::int8[])::pathcomposite as p from s1; +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.properties ->> 'name') = 'value') and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id) select (array [s1.n0, s1.n1, s1.n2]::nodecomposite[], array [s1.e0, s1.e1]::edgecomposite[])::pathcomposite as p from s1; -- case: match p = ()-[*..]->() return p limit 1 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union select s1.root_id, e0.end_id, s1.depth + 1, false, e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 limit 1; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1; -- case: match p = (s)-[*..]->(i)-[]->() where id(s) = 1 and i.name = 'n3' return p limit 1 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'name') = 'n3'), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (n0.id = 1) union select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'name') = 'n3'), e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied), s2 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select edges_to_path(variadic s2.ep0 || array [(s2.e1).id]::int8[])::pathcomposite as p from s2 limit 1; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'name') = 'n3'), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'name') = 'n3'), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, s2.e0 || array [s2.e1]::edgecomposite[], array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; -- case: match p = ()-[e:EdgeKind1]->()-[:EdgeKind1*..]->() return e, 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.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::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 [3]::int2[]) union select s2.root_id, e1.end_id, s2.depth + 1, false, e1.id = any (s2.path), s2.path || e1.id from s2 join edge e1 on e1.start_id = s2.next_id where e1.kind_id = any (array [3]::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 where (s0.n1).id = s2.root_id) select s1.e0 as e, edges_to_path(variadic array [(s1.e0).id]::int8[] || s1.ep0)::pathcomposite as p from s1; +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 e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), 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 s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) 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 lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || s1.e1, array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; -- case: match p = (m:NodeKind1)-[:EdgeKind1]->(c:NodeKind2) where m.objectid ends with "-513" and not toUpper(c.operatingsystem) contains "SERVER" 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.properties ->> 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on (not upper((n1.properties ->> 'operatingsystem'))::text like '%SERVER%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s0.e0).id]::int8[])::pathcomposite as p from s0 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.properties ->> 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on (not upper((n1.properties ->> 'operatingsystem'))::text like '%SERVER%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) limit 1000) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 1000; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2]->(e:NodeKind2)-[:EdgeKind2]->(:NodeKind1) where 'a' in e.values or 'b' in e.values or size(e.values) = 0 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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('a' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or 'b' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or jsonb_array_length((n1.properties -> 'values'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select edges_to_path(variadic array [(s1.e0).id, (s1.e1).id]::int8[])::pathcomposite as p from s1; +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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('a' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or 'b' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or jsonb_array_length((n1.properties -> 'values'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select (array [s1.n0, s1.n1, s1.n2]::nodecomposite[], array [s1.e0, s1.e1]::edgecomposite[])::pathcomposite as p from s1; -- 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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.id = e0.end_id or n0.id = e0.start_id) join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n1.id = e0.end_id or n1.id = e0.start_id) where (n0.id <> n1.id)) select edges_to_path(variadic array [(s0.e0).id]::int8[])::pathcomposite as p from s0; +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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.id = e0.end_id or n0.id = e0.start_id) join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n1.id = e0.end_id or n1.id = e0.start_id) where (n0.id <> n1.id)) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), 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 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 where s2.satisfied and (s0.n1).id = s2.root_id) select edges_to_path(variadic array [(s1.e0).id]::int8[] || s1.ep0)::pathcomposite as p from s1 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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), 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 s2_seed join edge e1 on e1.start_id = s2_seed.root_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union all 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[], false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.end_id where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) 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 lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.satisfied and (s0.n1).id = s2.root_id limit 1000) select ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || s1.e1, array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1 limit 1000; -- 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; +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_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), 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 s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, s1.e0, array [s1.n0, s1.n1]::nodecomposite[])::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; +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 (array [s2.n0, s2.n1]::nodecomposite[], array [s2.e0]::edgecomposite[])::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; +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 (array [s2.n0, s2.n1]::nodecomposite[], array [s2.e0]::edgecomposite[])::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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and (n1.properties ->> 'name') = 'bar' and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p from s1; +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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and (n1.properties ->> 'name') = 'bar' and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p from s1; -- case: match (e) match p = ()-[]->(e) return p limit 1 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), 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.end_id join node n1 on n1.id = e0.start_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p from s1 limit 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), 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.end_id join node n1 on n1.id = e0.start_id) select (array [s1.n1, s1.n0]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p from s1 limit 1; -- case: match p = (a)-[]->() match q = ()-[]->(a) return p, q -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p, edges_to_path(variadic array [(s1.e1).id]::int8[])::pathcomposite as q from s1; +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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p, (array [s1.n2, s1.n0]::nodecomposite[], array [s1.e1]::edgecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] union select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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 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 n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s0, edge e2 join node n4 on n4.id = e2.end_id join node n3 on n3.id = e2.start_id where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0)) union select s5.root_id, e2.start_id, s5.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], 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 n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (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, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id where s5.satisfied) select edges_to_path(variadic ep1)::pathcomposite as p from s4; - +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id union select s5.root_id, e2.start_id, s5.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s5.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join lateral (select n4.id, n4.kind_ids, n4.properties from node n4 where n4.id = s5.root_id offset 0) n4 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s5.next_id offset 0) n3 on true where s5.satisfied) select ordered_edges_to_path(s4.n3, s4.e2, array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql index 085e8fc0..aa678338 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -15,68 +15,68 @@ -- SPDX-License-Identifier: Apache-2.0 -- case: match (n)-[*..]->(e) return n, e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union select s1.root_id, e0.end_id, s1.depth + 1, false, e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true) select s0.n0 as n, s0.n1 as e from s0; -- case: match (n)-[*1..2]->(e) return n, e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union select s1.root_id, e0.end_id, s1.depth + 1, false, e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id where s1.depth < 2 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 2 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true) select s0.n0 as n, s0.n1 as e from s0; -- case: match (n)-[*3..5]->(e) return n, e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union select s1.root_id, e0.end_id, s1.depth + 1, false, e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id where s1.depth < 5 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.depth >= 3) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 5 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 3) select s0.n0 as n, s0.n1 as e from s0; -- case: match (n)<-[*2..5]-(e) return n, e -with s0 as (with recursive s1(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 union select s1.root_id, e0.start_id, s1.depth + 1, false, e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id where s1.depth < 5 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.depth >= 2) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(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 union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 5 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2) select s0.n0 as n, s0.n1 as e from s0; -- case: match p = (n)-[*..]->(e:NodeKind1) return p -with s0 as (with recursive s1(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 where n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] union select s1.root_id, e0.start_id, s1.depth + 1, false, e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(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 s1_seed join edge e0 on e0.end_id = s1_seed.root_id union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match (n)-[*..]->(e:NodeKind1) where n.name = 'n1' return e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'name') = 'n1') union select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied) select s0.n1 as e from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'name') = 'n1')), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n1 as e from s0; -- case: match (n)-[r*..]->(e:NodeKind1) where n.name = 'n1' and r.prop = 'a' return e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'name') = 'n1') and ((e0.properties ->> 'prop') = 'a') union select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where ((e0.properties ->> 'prop') = 'a') and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied) select s0.n1 as e from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'name') = 'n1')), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where ((e0.properties ->> 'prop') = 'a') union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and ((e0.properties ->> 'prop') = 'a') offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n1 as e from s0; -- case: match (n)-[*..]->(e:NodeKind1) where n.name = 'n2' return n -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'name') = 'n2') union select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied) select s0.n0 as n from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'name') = 'n2')), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n0 as n from s0; -- case: match (n)-[*..]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'name') = 'n1') union select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied), s2 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'name') = 'n1')), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.e0 as e0, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; -- case: match (n)-[*2..3]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'name') = 'n1') union select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.depth >= 2 and s1.satisfied), s2 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'name') = 'n1')), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.e0 as e0, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; -- case: match (n)-[]->(e:NodeKind1)-[*2..3]->(l) where n.name = 'n1' return l -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.properties ->> 'name') = 'n1') and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), 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 union select s2.root_id, e1.end_id, s2.depth + 1, false, e1.id = any (s2.path), s2.path || e1.id from s2 join edge e1 on e1.start_id = s2.next_id where s2.depth < 3 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 where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; +with s0 as (select (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.properties ->> 'name') = 'n1') and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), 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 s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) 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 lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; -- case: match (n)-[*..]->(e)-[:EdgeKind1|EdgeKind2]->()-[*..]->(l) where n.name = 'n1' and e.name = 'n2' return l -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'name') = 'n2'), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'name') = 'n1') union select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'name') = 'n2'), e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied), s2 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 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 [3, 4]::int2[])), s3 as (with recursive s4(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 s2 join edge e2 on (s2.n2).id = e2.start_id join node n3 on n3.id = e2.end_id union select s4.root_id, e2.end_id, s4.depth + 1, false, e2.id = any (s4.path), s4.path || e2.id from s4 join edge e2 on e2.start_id = s4.next_id where s4.depth < 15 and not s4.is_cycle) select s2.e0 as e0, s2.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 (s4.path)) as e2, s2.ep0 as ep0, s4.path as ep1, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join node n2 on n2.id = s4.root_id join node n3 on n3.id = s4.next_id where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'name') = 'n1')), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'name') = 'n2'), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'name') = 'n2'), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.e0 as e0, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 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 [3, 4]::int2[])), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(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 s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.e0 as e0, (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) as e2, s2.ep0 as ep0, s4.path as ep1, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; -- case: match p = (:NodeKind1)-[:EdgeKind1*1..]->(n:NodeKind2) where 'admin_tier_0' in split(n.system_tags, ' ') return p limit 1000 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], 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 ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id where s1.satisfied) select edges_to_path(variadic ep0)::pathcomposite as p from s0 limit 1000; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; -- case: match p = (s:NodeKind1)-[*..]->(e:NodeKind2) where s <> e return p -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] union select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied and (n0.id <> n1.id)) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and (n0.id <> n1.id)) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = (g:NodeKind1)-[:EdgeKind1|EdgeKind2*]->(target:NodeKind1) where g.objectid ends with '1234' and target.objectid ends with '4567' return p -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'objectid') like '%1234') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3, 4]::int2[]) union select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%1234') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = (m:NodeKind2)-[:EdgeKind1*1..]->(n:NodeKind1) where n.objectid = '1234' return p limit 10 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], 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') = '1234') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id where s1.satisfied) select edges_to_path(variadic ep0)::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties ->> 'objectid') = '1234') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-() return p limit 10 -with s0 as (with recursive s1(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 n0 on n0.id = e0.end_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3, 4]::int2[]) union select s1.root_id, e0.start_id, s1.depth + 1, false, e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3, 4]::int2[]) and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(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 s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 10) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind2)<-[:EdgeKind1|EdgeKind2*2..]-(:NodeKind1) return p limit 10 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3, 4]::int2[]) union select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied), s2 as (with recursive s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.id = any (s3.path), s3.path || e1.id from s3 join edge e1 on e1.end_id = s3.next_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) and s3.depth < 15 and not s3.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 (s3.path)) as e1, s0.ep0 as ep0, s3.path as ep1, 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, s3 join node n1 on n1.id = s3.root_id join node n2 on n2.id = s3.next_id where s3.depth >= 2 and s3.satisfied and (s0.n1).id = s3.root_id) select edges_to_path(variadic s2.ep1 || s2.ep0)::pathcomposite as p from s2 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) as e1, s0.ep0 as ep0, s3.path as ep1, 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, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.depth >= 2 and s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, s2.e0 || s2.e1, array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind2)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind1) return p limit 10 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3, 4]::int2[]) union select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied), s2 as (with recursive s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.id = any (s3.path), s3.path || e1.id from s3 join edge e1 on e1.end_id = s3.next_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) and s3.depth < 15 and not s3.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 (s3.path)) as e1, s0.ep0 as ep0, s3.path as ep1, 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, s3 join node n1 on n1.id = s3.root_id join node n2 on n2.id = s3.next_id where s3.satisfied and (s0.n1).id = s3.root_id) select edges_to_path(variadic s2.ep1 || s2.ep0)::pathcomposite as p from s2 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) as e1, s0.ep0 as ep0, s3.path as ep1, 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, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, s2.e0 || s2.e1, array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; -- case: match p = (n:NodeKind1)-[:EdgeKind1|EdgeKind2*1..2]->(r:NodeKind2) where r.name =~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*' return p limit 10 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], 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 ->> 'name') ~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e0.kind_id = any (array [3, 4]::int2[]) union select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) and s1.depth < 2 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id where s1.satisfied) select edges_to_path(variadic ep0)::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties ->> 'name') ~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 2 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; -- case: match p = (t:NodeKind2)<-[:EdgeKind1*1..]-(a) where (a:NodeKind1 or a:NodeKind2) and t.objectid ends with '-512' return p limit 1000 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.end_id = e0.start_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where ((n0.properties ->> 'objectid') like '%-512') and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s1.root_id, e0.start_id, s1.depth + 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.id = any (s1.path), s1.path || e0.id from s1 join edge e0 on e0.end_id = s1.next_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and s1.depth < 15 and not s1.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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id where s1.satisfied) select edges_to_path(variadic ep0)::pathcomposite as p from s0 limit 1000; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%-512') and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; -- case: match p=(n:NodeKind1)-[:EdgeKind1|EdgeKind2]->(g:NodeKind1)-[:EdgeKind2]->(:NodeKind2)-[:EdgeKind1*1..]->(m:NodeKind1) where n.objectid = m.objectid return p limit 100 -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])), s2 as (with recursive s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, (((s1.n0).properties -> 'objectid') = (n3.properties -> 'objectid')) and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.start_id = e2.end_id, array [e2.id] from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) union select s3.root_id, e2.end_id, s3.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.id = any (s3.path), s3.path || e2.id from s3 join edge e2 on e2.start_id = s3.next_id join node n3 on n3.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and s3.depth < 15 and not s3.is_cycle) select s1.e0 as e0, s1.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 (s3.path)) as e2, s3.path as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, s3 join node n2 on n2.id = s3.root_id join node n3 on n3.id = s3.next_id where s3.satisfied and (s1.n2).id = s3.root_id and (((s1.n0).properties -> 'objectid') = (n3.properties -> 'objectid'))) select edges_to_path(variadic array [(s2.e0).id, (s2.e1).id]::int8[] || s2.ep0)::pathcomposite as p from s2 limit 100; +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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s1.n2).id as root_id from s1), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.start_id = e2.end_id, array [e2.id] from s3_seed join edge e2 on e2.start_id = s3_seed.root_id join node n3 on n3.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) union all select s3.root_id, e2.end_id, s3.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e2.id from s3 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s3.next_id and e2.id != all (s3.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.end_id where s3.depth < 15 and not s3.is_cycle) select s1.e0 as e0, s1.e1 as e1, (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) as e2, s3.path as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, s3 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s3.next_id offset 0) n3 on true where s3.satisfied and (s1.n2).id = s3.root_id and (((s1.n0).properties -> 'objectid') = (n3.properties -> 'objectid')) limit 100) select ordered_edges_to_path(s2.n0, array [s2.e0]::edgecomposite[] || array [s2.e1]::edgecomposite[] || s2.e2, array [s2.n0, s2.n1, s2.n2, s2.n3]::nodecomposite[])::pathcomposite as p from s2 limit 100; diff --git a/cypher/models/pgsql/test/translation_cases/quantifiers.sql b/cypher/models/pgsql/test/translation_cases/quantifiers.sql index 7b3ff637..84db16a8 100644 --- a/cypher/models/pgsql/test/translation_cases/quantifiers.sql +++ b/cypher/models/pgsql/test/translation_cases/quantifiers.sql @@ -30,17 +30,17 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'usedeskeyonly'))::bool or ((select count(*)::int from unnest(jsonb_to_text_array((n0.properties -> 'supportedencryptiontypes'))) as i0 where (i0 like '%DES%')) >= 1)::bool or ((select count(*)::int from unnest(jsonb_to_text_array((n0.properties -> 'serviceprincipalnames'))) as i1 where (lower(i1)::text like '%mssqlservercluster%' or lower(i1)::text like '%mssqlserverclustermgmtapi%' or lower(i1)::text like '%msclustervirtualserver%')) >= 1)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0 limit 100; -- case: MATCH (m:NodeKind1) WHERE m.unconstraineddelegation = true WITH m MATCH (n:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) WHERE g.objectid ENDS WITH '-516' WITH m, COLLECT(n) AS matchingNs WHERE NONE(n IN matchingNs WHERE n.objectid = m.objectid) RETURN m -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'unconstraineddelegation'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 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, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-516') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i0) as i1 where ((i1.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = 0)::bool); +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'unconstraineddelegation'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 as (select 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, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-516') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i0) as i1 where ((i1.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = 0)::bool); -- case: MATCH (m:NodeKind1) WHERE m.unconstraineddelegation = true WITH m MATCH (n:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) WHERE g.objectid ENDS WITH '-516' WITH m, COLLECT(n) AS matchingNs WHERE ALL(n IN matchingNs WHERE n.objectid = m.objectid) RETURN m -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'unconstraineddelegation'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 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, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-516') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i0) as i1 where ((i1.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = array_length(s2.i0, 1))::bool); +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'unconstraineddelegation'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 as (select 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, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-516') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i0) as i1 where ((i1.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = array_length(s2.i0, 1))::bool); -- case: MATCH (m:NodeKind1) WHERE ANY(name in m.serviceprincipalnames WHERE name CONTAINS "PHANTOM") WITH m MATCH (n:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) WHERE g.objectid ENDS WITH '-525' WITH m, COLLECT(n) AS matchingNs WHERE NONE(t IN matchingNs WHERE t.objectid = m.objectid) RETURN m -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((select count(*)::int from unnest(jsonb_to_text_array((n0.properties -> 'serviceprincipalnames'))) as i0 where (i0 like '%PHANTOM%')) >= 1)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 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, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-525') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i1) as i2 where ((i2.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = 0)::bool); +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((select count(*)::int from unnest(jsonb_to_text_array((n0.properties -> 'serviceprincipalnames'))) as i0 where (i0 like '%PHANTOM%')) >= 1)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 as (select 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, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-525') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i1) as i2 where ((i2.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = 0)::bool); -- case: MATCH (m:NodeKind1) WHERE m.unconstraineddelegation = true WITH m MATCH (n:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) WHERE g.objectid ENDS WITH '-516' WITH m, COLLECT(n) AS matchingNs WHERE ALL(n IN matchingNs WHERE n.objectid = m.objectid) RETURN m -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'unconstraineddelegation'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 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, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-516') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i0) as i1 where ((i1.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = array_length(s2.i0, 1))::bool); +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'unconstraineddelegation'))::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 as (select 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, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-516') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i0) as i1 where ((i1.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = array_length(s2.i0, 1))::bool); -- case: MATCH (m:NodeKind1) WHERE ANY(name in m.serviceprincipalnames WHERE name CONTAINS "PHANTOM") WITH m MATCH (n:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) WHERE g.objectid ENDS WITH '-525' WITH m, COLLECT(n) AS matchingNs WHERE NONE(t IN matchingNs WHERE t.objectid = m.objectid) RETURN m -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((select count(*)::int from unnest(jsonb_to_text_array((n0.properties -> 'serviceprincipalnames'))) as i0 where (i0 like '%PHANTOM%')) >= 1)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 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, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-525') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i1) as i2 where ((i2.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = 0)::bool); +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((select count(*)::int from unnest(jsonb_to_text_array((n0.properties -> 'serviceprincipalnames'))) as i0 where (i0 like '%PHANTOM%')) >= 1)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (with s3 as (select 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, edge e0 join node n2 on ((n2.properties ->> 'objectid') like '%-525') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select s3.n0 as n0, array_remove(coalesce(array_agg(s3.n1)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s3 group by n0) select s2.n0 as m from s2 where (((select count(*)::int from unnest(s2.i1) as i2 where ((i2.properties -> 'objectid') = ((s2.n0).properties -> 'objectid'))) = 0)::bool); diff --git a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql index 15c91fd4..b808f2e9 100644 --- a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql +++ b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql @@ -15,54 +15,69 @@ -- SPDX-License-Identifier: Apache-2.0 -- case: match p = allShortestPaths((s:NodeKind1)-[*..]->()) return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, exists (select 1 from edge where end_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id where n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[];","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.start_id), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id;"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_asp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from edge where end_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id;","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.id != all (s1.path);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_asp_harness(@pi0::text, @pi1::text, 15)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = allShortestPaths((s:NodeKind1)-[*..]->({name: "123"})) return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.start_id = e0.end_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 -\u003e\u003e 'name') = '123';","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id join node n0 on n0.id = e0.start_id;","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, (n1.properties -\u003e\u003e 'name') = '123', e0.start_id = e0.end_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 n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[];","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, (n1.properties -\u003e\u003e 'name') = '123', e0.id = any (s1.path), e0.id || s1.path from backward_front s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id;"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (n1.properties -\u003e\u003e 'name') = '123') select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id;","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), false, e0.id || s1.path from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.id != all (s1.path);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_asp_harness(@pi0::text, @pi1::text, 15, ('')::text, ('insert into traversal_terminal_filter (id) select distinct n0.id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = allShortestPaths((s:NodeKind1)-[*..]->(e)) where e.name = '123' return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.start_id = e0.end_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 -\u003e\u003e 'name') = '123');","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id join node n0 on n0.id = e0.start_id;","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, ((n1.properties -\u003e\u003e 'name') = '123'), e0.start_id = e0.end_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 n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[];","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties -\u003e\u003e 'name') = '123'), e0.id = any (s1.path), e0.id || s1.path from backward_front s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id;"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties -\u003e\u003e 'name') = '123')) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id;","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), false, e0.id || s1.path from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.id != all (s1.path);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_asp_harness(@pi0::text, @pi1::text, 15, ('')::text, ('insert into traversal_terminal_filter (id) select distinct n0.id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p=shortestPath((n:NodeKind1)-[:EdgeKind1*1..]->(m)) where 'admin_tier_0' in split(m.system_tags, ' ') and n.objectid ends with '-513' and n<>m return p limit 1000 --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[])), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties -\u003e\u003e 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[])), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id) limit 1000; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties -\u003e\u003e 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s1.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[]))) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s1.root_id and backward_visited.id = e0.start_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id) limit 1000; -- case: match p=shortestPath((n:NodeKind1)-[:EdgeKind1*1..]->(m)) where 'admin_tier_0' in split(m.system_tags, ' ') and n.objectid ends with '-513' and m<>n return p limit 1000 --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[])), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties -\u003e\u003e 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[])), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties -\u003e\u003e 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s1.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[]))) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s1.root_id and backward_visited.id = e0.start_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; -- case: match p=shortestPath((t:NodeKind1)<-[:EdgeKind1|EdgeKind2*1..]-(s:NodeKind2)) where coalesce(t.system_tags, '') contains 'admin_tier_0' and t.name =~ 'name.*' and s<>t return p limit 1000 --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%' and (n0.properties -\u003e\u003e 'name') ~ 'name.*') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[] and e0.kind_id = any (array [3, 4]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[], e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%' and (n0.properties -\u003e\u003e 'name') ~ 'name.*') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3, 4]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.start_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15, ('')::text, ('insert into traversal_terminal_filter (id) select distinct n1.id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id is not null;')::text, (1000)::int8)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; -- case: match p=shortestPath((a)-[:EdgeKind1*]->(b)) where id(a) = 1 and id(b) = 2 return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, (n1.id = 2), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (n0.id = 1) and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, (n1.id = 2), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s1.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (n1.id = 2)) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s1.root_id and backward_visited.id = e0.start_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where (n0.id = 1) and (n1.id = 2) and n0.id is not null and n1.id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p=shortestPath((a)-[:EdgeKind1*]->(b:NodeKind1)) where a <> b return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, exists (select 1 from edge where end_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.end_id where n1.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.end_id), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from edge where end_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.end_id), false, e0.id || s1.path from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.start_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); -- case: match p=shortestPath((a:NodeKind2)-[:EdgeKind1*]->(b)) where a <> b return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, exists (select 1 from edge where end_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id where n0.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[] and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.start_id), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from edge where end_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.end_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); -- case: match p=shortestPath((b)<-[:EdgeKind1*]-(a)) where id(a) = 1 and id(b) = 2 return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, (n1.id = 1), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where (n0.id = 2) and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, (n1.id = 1), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 2)) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.end_id and traversal_pair_filter.terminal_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s1.root_id and forward_visited.id = e0.start_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (n1.id = 1)) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.end_id and traversal_pair_filter.terminal_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.end_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s1.root_id and backward_visited.id = e0.end_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where (n0.id = 2) and (n1.id = 1) and n0.id is not null and n1.id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = allShortestPaths((m:NodeKind1)<-[:EdgeKind1*..]-(n)) where coalesce(m.system_tags, '') contains 'admin_tier_0' and n.name = '123' and n <> m return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.start_id join node n0 on n0.id = e0.end_id where ((n1.properties -\u003e\u003e 'name') = '123') and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id join node n0 on n0.id = e0.end_id where e0.kind_id = any (array [3]::int2[]);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, ((n1.properties -\u003e\u003e 'name') = '123'), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.start_id join node n0 on n0.id = e0.end_id where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, ((n1.properties -\u003e\u003e 'name') = '123'), e0.id = any (s1.path), e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id); +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties -\u003e\u003e 'name') = '123')) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n1.id, n0.id from node n1, node n0 where ((n1.properties ->> ''name'') = ''123'') and (coalesce((n0.properties ->> ''system_tags''), '''')::text like ''%admin_tier_0%'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id is not null and n0.id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id); -- case: match p=shortestPath((a)-[:EdgeKind1*]->(b:NodeKind1)) where a <> b return p --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, exists (select 1 from edge where end_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.end_id where n1.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.end_id), e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from edge where end_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.end_id), false, e0.id || s1.path from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.start_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); -- case: match p=(c:NodeKind1)-[]->(u:NodeKind2) match p2=shortestPath((u:NodeKind2)-[*1..]->(d:NodeKind1)) return p, p2 limit 500 --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e1.start_id, e1.end_id, 1, n2.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e1.start_id = e1.end_id, array [e1.id] from edge e1 join node n1 on n1.id = e1.start_id join node n2 on n2.id = e1.end_id where n1.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[];","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e1.end_id, s2.depth + 1, n2.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e1.id = any (s2.path), s2.path || e1.id from forward_front s2 join edge e1 on e1.start_id = s2.next_id join node n2 on n2.id = e1.end_id;"} -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id), s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) 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 where (s0.n1).id = s2.root_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p, edges_to_path(variadic ep0)::pathcomposite as p2 from s1 limit 500; +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s2_seed(root_id) as not materialized (select distinct n1.id as root_id from traversal_root_filter s2_seed_filter join node n1 on n1.id = s2_seed_filter.id where n1.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[]) select e1.start_id, e1.end_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e1.end_id), e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id;","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e1.end_id, s2.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e1.end_id), false, s2.path || e1.id from forward_front s2 join edge e1 on e1.start_id = s2.next_id where e1.id != all (s2.path) and not exists (select 1 from visited where visited.root_id = s2.root_id and visited.id = e1.end_id);"} +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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id), s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15, ('insert into traversal_root_filter (id) select distinct (s0.n1).id from s0 where (s0.n1).id is not null;')::text, ('insert into traversal_terminal_filter (id) select distinct n2.id from node n2 where n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id is not null;')::text)) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) 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 where (s0.n1).id = s2.root_id) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n1, s1.e1, array [s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p2 from s1 limit 500; +-- case: match p = allShortestPaths((a)-[:EdgeKind1*..]->()) return p +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, exists (select 1 from edge where end_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from edge e0 where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_asp_harness(@pi0::text, @pi1::text, 15)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; + +-- case: match p=shortestPath((n:NodeKind1)-[:EdgeKind1*1..]->(m:NodeKind2)) return p limit 10 +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.end_id);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15, ('')::text, ('insert into traversal_terminal_filter (id) select distinct n1.id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id is not null;')::text, (10)::int8)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; + +-- case: match (a:NodeKind1), (b:NodeKind2) match p=shortestPath((a)-[:EdgeKind1*]->(b)) return p +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s3_seed(root_id) as not materialized (select s3_seed_filter.id as root_id from traversal_root_filter s3_seed_filter) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s3.root_id, e0.end_id, s3.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s3.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s3.path || e0.id from forward_front s3 join edge e0 on e0.start_id = s3.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s3.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s3.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s3_seed(root_id) as not materialized (select s3_seed_filter.id as root_id from traversal_terminal_filter s3_seed_filter) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.end_id = s3_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s3.root_id, e0.start_id, s3.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s3.root_id), false, e0.id || s3.path from backward_front s3 join edge e0 on e0.end_id = s3.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s3.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s3.root_id and backward_visited.id = e0.start_id);"} +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[]), 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[]), s2 as (with s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct (s1.n0).id, (s1.n1).id from s1 where (s1.n0).id is not null and (s1.n1).id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s3.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join node n0 on n0.id = s3.root_id join node n1 on n1.id = s3.next_id where (s1.n0).id = s3.root_id and (s1.n1).id = s3.next_id) select ordered_edges_to_path(s2.n0, s2.e0, array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; + +-- case: match (a:NodeKind1), (b:NodeKind2) match p=allShortestPaths((a)-[:EdgeKind1*..]->(b)) return p +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s3_seed(root_id) as not materialized (select s3_seed_filter.id as root_id from traversal_root_filter s3_seed_filter) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s3.root_id, e0.end_id, s3.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s3.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s3.path || e0.id from forward_front s3 join edge e0 on e0.start_id = s3.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s3.path);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s3_seed(root_id) as not materialized (select s3_seed_filter.id as root_id from traversal_terminal_filter s3_seed_filter) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.end_id = s3_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s3.root_id, e0.start_id, s3.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s3.root_id), false, e0.id || s3.path from backward_front s3 join edge e0 on e0.end_id = s3.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s3.path);"} +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[]), 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[]), s2 as (with s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct (s1.n0).id, (s1.n1).id from s1 where (s1.n0).id is not null and (s1.n1).id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s3.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join node n0 on n0.id = s3.root_id join node n1 on n1.id = s3.next_id where (s1.n0).id = s3.root_id and (s1.n1).id = s3.next_id) select ordered_edges_to_path(s2.n0, s2.e0, array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; diff --git a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql index ec41768d..127f3476 100644 --- a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql +++ b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql @@ -15,48 +15,51 @@ -- SPDX-License-Identifier: Apache-2.0 -- case: match ()-[r]->() return r -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) select s0.e0 as r from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.e0 as r from s0; -- case: match ()-[r]->() where type(r) = 'EdgeKind1' return r -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 (e0.kind_id = 3)) select s0.e0 as r from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (e0.kind_id = 3)) select s0.e0 as r from s0; -- case: match ()-[r]->() where 'EdgeKind1' = type(r) return r -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 (3 = e0.kind_id)) select s0.e0 as r from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (3 = e0.kind_id)) select s0.e0 as r from s0; -- case: match (n), ()-[r]->() return n, r -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), 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, (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) select s1.n0 as n, s1.e0 as r from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0 from s0, edge e0 join node n1 on n1.id = e0.start_id join node n2 on n2.id = e0.end_id) select s1.n0 as n, s1.e0 as r from s1; -- case: match ()-[r]->(), ()-[e]->() return r, e -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (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) select s1.e0 as r, s1.e1 as e from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1 from s0, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.id = e1.end_id) select s1.e0 as r, s1.e1 as e from s1; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2]->(c:NodeKind2) where '123' in c.prop2 or '243' in c.prop2 or size(c.prop2) = 0 return p limit 10 -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select edges_to_path(variadic array [(s0.e0).id]::int8[])::pathcomposite as p from s0 limit 10; +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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) limit 10) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 10; -- case: match ()-[r:EdgeKind1]->() return count(r) as the_count -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 e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; + +-- case: match ()-[r:EdgeKind1]->() return count(r) as the_count limit 1 +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0 limit 1; -- case: match ()-[r:EdgeKind1]->({name: "123"}) return count(r) as the_count -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 n1 on (n1.properties ->> 'name') = '123' and n1.id = e0.end_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n1 on (n1.properties ->> 'name') = '123' and n1.id = e0.end_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; -- case: match (s)-[r]->(e) where id(e) = $a and not (id(s) = $b) and (r:EdgeKind1 or r:EdgeKind2) and not (s.objectid ends with $c or e.objectid ends with $d) return distinct id(s), id(r), id(e) -- cypher_params: {"a":1,"b":2,"c":"123","d":"456"} -- pgsql_params:{"pi0":1,"pi1":2,"pi2":"123","pi3":"456"} -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 n1 on n1.id = e0.end_id join node n0 on (not (n0.id = @pi1::float8)) and n0.id = e0.start_id where ((e0.kind_id = any (array [3]::int2[]) or e0.kind_id = any (array [4]::int2[]))) and (not ((n0.properties ->> 'objectid') like '%' || @pi2::text or (n1.properties ->> 'objectid') like '%' || @pi3::text) and n1.id = @pi0::float8)) select (s0.n0).id, (s0.e0).id, (s0.n1).id from s0; +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 n1 on n1.id = e0.end_id join node n0 on (not (n0.id = @pi1::float8)) and n0.id = e0.start_id where ((e0.kind_id = any (array [3]::int2[]) or e0.kind_id = any (array [4]::int2[]))) and (not ((n0.properties ->> 'objectid') like '%' || @pi2::text or (n1.properties ->> 'objectid') like '%' || @pi3::text) and n1.id = @pi0::float8)) select distinct (s0.n0).id, (s0.e0).id, (s0.n1).id from s0; -- case: match (s)-[r]->(e) where s.name = '123' and e:NodeKind1 and not r.property return s, r, e 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.properties ->> 'name') = '123') and n0.id = e0.start_id join node n1 on (n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]) and n1.id = e0.end_id where (not ((e0.properties ->> 'property'))::bool)) select s0.n0 as s, s0.e0 as r, s0.n1 as e from s0; -- case: match ()-[r]->() where r.value = 42 return r -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 (((e0.properties ->> 'value'))::int8 = 42)) select s0.e0 as r from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties ->> 'value'))::int8 = 42)) select s0.e0 as r from s0; -- case: match ()-[r]->() where r.bool_prop return r -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 (((e0.properties ->> 'bool_prop'))::bool)) select s0.e0 as r from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties ->> 'bool_prop'))::bool)) select s0.e0 as r from s0; -- case: match (n)-[r]->() where n.name = '123' return n, r -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.properties ->> 'name') = '123') and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; +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 from edge e0 join node n0 on ((n0.properties ->> 'name') = '123') and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; -- case: match (n:NodeKind1)-[r]->() where n.name = '123' or n.name = '321' or n.name = '222' or n.name = '333' return n, r -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.properties ->> 'name') = '123' or (n0.properties ->> 'name') = '321' or (n0.properties ->> 'name') = '222' or (n0.properties ->> 'name') = '333') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; +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 from edge e0 join node n0 on ((n0.properties ->> 'name') = '123' or (n0.properties ->> 'name') = '321' or (n0.properties ->> 'name') = '222' or (n0.properties ->> 'name') = '333') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; -- case: match (s)-[r]->(e) where s.name = '123' and e.name = '321' return s, r, e 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.properties ->> 'name') = '123') and n0.id = e0.start_id join node n1 on ((n1.properties ->> 'name') = '321') and n1.id = e0.end_id) select s0.n0 as s, s0.e0 as r, s0.n1 as e from s0; @@ -65,41 +68,41 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (not ((n0.properties ->> 'bool_field'))::bool)), 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, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n1 on ((n1.properties ->> 'name') = '123') and n1.id = e0.start_id join node n2 on ((n2.properties ->> 'name') = '321') and n2.id = e0.end_id) select s1.n0 as f, s1.n1 as s, s1.e0 as r, s1.n2 as e from s1; -- case: match ()-[e0]->(n)<-[e1]-() return e0, n, e1 -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; -- case: match ()-[e0]->(n)-[e1]->() return e0, n, e1 -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; -- case: match ()<-[e0]-(n)<-[e1]-() return e0, n, e1 -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 join node n1 on n1.id = e0.start_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; -- case: match (s)<-[r:EdgeKind1|EdgeKind2]-(e) return s.name, e.name -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 join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; +with s0 as (select (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 e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; -- case: match (s)-[:EdgeKind1|EdgeKind2]->(e)-[:EdgeKind1]->() return s.name as s_name, e.name as e_name -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 e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 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 [3]::int2[])) select ((s1.n0).properties -> 'name') as s_name, ((s1.n1).properties -> 'name') as e_name from s1; +with s0 as (select (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 e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.n0 as n0, s0.n1 as n1 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 [3]::int2[])) select ((s1.n0).properties -> 'name') as s_name, ((s1.n1).properties -> 'name') as e_name from s1; -- case: match (s:NodeKind1)-[r:EdgeKind1|EdgeKind2]->(e:NodeKind2) return s.name, e.name -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; +with s0 as (select (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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; -- case: match (s)-[r:EdgeKind1]->() where (s)-[r {prop: 'a'}]->() return s -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 (e0.properties ->> 'prop') = 'a' and e0.kind_id = any (array [3]::int2[])) select s0.n0 as s from s0 where ((with s1 as (select s0.e0 as e0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e0 on (s0.n0).id = (s0.e0).start_id join node n2 on n2.id = (s0.e0).end_id) select count(*) > 0 from s1)); +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 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (e0.properties ->> 'prop') = 'a' and e0.kind_id = any (array [3]::int2[])) select s0.n0 as s from s0 where ((with s1 as (select s0.e0 as e0, s0.n0 as n0 from s0 join edge e0 on (s0.n0).id = (s0.e0).start_id join node n2 on n2.id = (s0.e0).end_id) select count(*) > 0 from s1)); -- case: match (s)-[r:EdgeKind1]->(e) where not (s.system_tags contains 'admin_tier_0') and id(e) = 1 return id(s), labels(s), id(r), type(r) 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 n1 on (n1.id = 1) and n1.id = e0.end_id join node n0 on (not (coalesce((n0.properties ->> 'system_tags'), '')::text like '%admin\_tier\_0%')) and n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select (s0.n0).id, (s0.n0).kind_ids, (s0.e0).id, (s0.e0).kind_id from s0; -- case: match (s)-[r]->(e) where s:NodeKind1 and toLower(s.name) starts with 'test' and r:EdgeKind1 and id(e) in [1, 2] return r limit 1 -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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and lower((n0.properties ->> 'name'))::text like 'test%') and n0.id = e0.start_id join node n1 on (n1.id = any (array [1, 2]::int8[])) and n1.id = e0.end_id where (e0.kind_id = any (array [3]::int2[]))) select s0.e0 as r from s0 limit 1; +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.kind_ids operator (pg_catalog.@>) array [1]::int2[] and lower((n0.properties ->> 'name'))::text like 'test%') and n0.id = e0.start_id join node n1 on (n1.id = any (array [1, 2]::int8[])) and n1.id = e0.end_id where (e0.kind_id = any (array [3]::int2[])) limit 1) select s0.e0 as r from s0 limit 1; -- case: match (n1)-[]->(n2) where n1 <> n2 return n2 -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 (n0.id <> n1.id)) select s0.n1 as n2 from s0; +with s0 as (select (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 (n0.id <> n1.id)) select s0.n1 as n2 from s0; -- case: match (n1)-[]->(n2) where n2 <> n1 return n2 -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.id <> n0.id)) select s0.n1 as n2 from s0; +with s0 as (select (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.id <> n0.id)) select s0.n1 as n2 from s0; -- case: match ()-[r]->()-[e]->(n) where r <> e return n -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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where ((s0.e0).id <> e1.id)) select s1.n2 as n from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (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), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where ((s0.e0).id <> e1.id)) select s1.n2 as n from s1; -- case: match (s:NodeKind1:NodeKind2)-[r:EdgeKind1|EdgeKind2]->(e:NodeKind2:NodeKind1) return s.name, e.name -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.kind_ids operator (pg_catalog.@>) array [1, 2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2, 1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; +with s0 as (select (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.kind_ids operator (pg_catalog.@>) array [1, 2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2, 1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; diff --git a/cypher/models/pgsql/test/translation_cases/unwind.sql b/cypher/models/pgsql/test/translation_cases/unwind.sql new file mode 100644 index 00000000..631cf966 --- /dev/null +++ b/cypher/models/pgsql/test/translation_cases/unwind.sql @@ -0,0 +1,51 @@ +-- Copyright 2026 Specter Ops, Inc. +-- +-- Licensed under the Apache License, Version 2.0 +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- case: with [1, 2, 3] as ids unwind ids as x return x +with s0 as (select array [1, 2, 3]::int8[] as i0) select i1 as x from s0, unnest(i0) as i1; + +-- case: match (n:NodeKind1) with collect(n.name) as names unwind names as name return name +with s0 as (with s1 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[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1) select i1 as name from s0, unnest(i0) as i1; + +-- case: match (n:NodeKind1) with collect(n.name) as names unwind names as name with name where name starts with 'test' return name +with s0 as (with s1 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[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select i1 as i1 from s0, unnest(i0) as i1 where (i1 like 'test%')) select s2.i1 as name from s2; + +-- case: with ['a', 'b', 'c'] as names unwind names as name return name +with s0 as (select array ['a', 'b', 'c']::text[] as i0) select i1 as name from s0, unnest(i0) as i1; + +-- case: with [1, 2, 3] as ids unwind ids as x return x order by x desc +with s0 as (select array [1, 2, 3]::int8[] as i0) select i1 as x from s0, unnest(i0) as i1 order by i1 desc; + +-- case: with [1, 2, 3, 1, 2] as ids unwind ids as x return distinct x +with s0 as (select array [1, 2, 3, 1, 2]::int8[] as i0) select distinct i1 as x from s0, unnest(i0) as i1; + +-- case: with [1, 2, 3] as ids unwind ids as x return count(x) +with s0 as (select array [1, 2, 3]::int8[] as i0) select count(i1)::int8 from s0, unnest(i0) as i1; + +-- case: match (n:NodeKind1) with collect(n.name) as names unwind names as name match (m:NodeKind2) where m.name = name return m +with s0 as (with s1 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[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, i1 as i1, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, unnest(i0) as i1, node n1 where ((n1.properties ->> 'name') = i1) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]) select s2.n1 as m from s2; + +-- case: with [1, 2, 3] as ids unwind ids as x with x where x > 1 return x +with s0 as (select array [1, 2, 3]::int8[] as i0), s1 as (select i1 as i1 from s0, unnest(i0) as i1 where (i1 > 1)) select s1.i1 as x from s1; + +-- case: with [1, 2, 3] as ids unwind ids as x return x limit 2 +with s0 as (select array [1, 2, 3]::int8[] as i0) select i1 as x from s0, unnest(i0) as i1 limit 2; + +-- case: unwind [1, 2, 3] as x return x +select i0 as x from unnest(array [1, 2, 3]::int8[]) as i0; + +-- case: with [1, 2, 3] as x unwind x as x return x +with s0 as (select array [1, 2, 3]::int8[] as i0) select i1 as x from s0, unnest(i0) as i1; diff --git a/cypher/models/pgsql/test/translation_cases/update.sql b/cypher/models/pgsql/test/translation_cases/update.sql index 85de1f00..40e9a3ba 100644 --- a/cypher/models/pgsql/test/translation_cases/update.sql +++ b/cypher/models/pgsql/test/translation_cases/update.sql @@ -45,13 +45,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1), s2 as (update node n2 set properties = n2.properties || jsonb_build_object('target', true)::jsonb from s1 where (s1.n0).id = n2.id returning (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0, s1.n1 as n1), s3 as (update node n3 set properties = n3.properties || jsonb_build_object('target', true)::jsonb from s2 where (s2.n1).id = n3.id returning s2.n0 as n0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n1) select s3.n0 as n1, s3.n1 as n3 from s3; -- case: match ()-[r]->(:NodeKind1) set r.is_special_outbound = true -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 n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id join node n0 on n0.id = e0.start_id), s1 as (update edge e1 set properties = e1.properties || jsonb_build_object('is_special_outbound', true)::jsonb from s0 where (s0.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s0.n0 as n0, s0.n1 as n1) select 1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id join node n0 on n0.id = e0.start_id), s1 as (update edge e1 set properties = e1.properties || jsonb_build_object('is_special_outbound', true)::jsonb from s0 where (s0.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0) select 1; -- case: match (a)-[r]->(:NodeKind1) set a.name = '123', r.is_special_outbound = true -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 n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id join node n0 on n0.id = e0.start_id), s1 as (update node n2 set properties = n2.properties || jsonb_build_object('name', '123')::jsonb from s0 where (s0.n0).id = n2.id returning s0.e0 as e0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0, s0.n1 as n1), s2 as (update edge e1 set properties = e1.properties || jsonb_build_object('is_special_outbound', true)::jsonb from s1 where (s1.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s1.n0 as n0, s1.n1 as n1) select 1; +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 from edge e0 join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id join node n0 on n0.id = e0.start_id), s1 as (update node n2 set properties = n2.properties || jsonb_build_object('name', '123')::jsonb from s0 where (s0.n0).id = n2.id returning s0.e0 as e0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0), s2 as (update edge e1 set properties = e1.properties || jsonb_build_object('is_special_outbound', true)::jsonb from s1 where (s1.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s1.n0 as n0) select 1; -- case: match (a)-[r]->(:NodeKind1) set a.name = '123', r.is_special_outbound = true -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 n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id join node n0 on n0.id = e0.start_id), s1 as (update node n2 set properties = n2.properties || jsonb_build_object('name', '123')::jsonb from s0 where (s0.n0).id = n2.id returning s0.e0 as e0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0, s0.n1 as n1), s2 as (update edge e1 set properties = e1.properties || jsonb_build_object('is_special_outbound', true)::jsonb from s1 where (s1.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s1.n0 as n0, s1.n1 as n1) select 1; +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 from edge e0 join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id join node n0 on n0.id = e0.start_id), s1 as (update node n2 set properties = n2.properties || jsonb_build_object('name', '123')::jsonb from s0 where (s0.n0).id = n2.id returning s0.e0 as e0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0), s2 as (update edge e1 set properties = e1.properties || jsonb_build_object('is_special_outbound', true)::jsonb from s1 where (s1.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s1.n0 as n0) select 1; -- case: match (s) remove s.name with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (update node n1 set properties = n1.properties - array ['name']::text[] from s0 where (s0.n0).id = n1.id returning (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n0) select 1; @@ -66,8 +66,8 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') = 'n1')), 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') = 'n4')), s2 as (update node n2 set properties = n2.properties || jsonb_build_object('name', ((s1.n1).properties -> 'name'))::jsonb from s1 where (s1.n0).id = n2.id returning (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0, s1.n1 as n1), s3 as (update node n3 set properties = n3.properties || jsonb_build_object('name', 'RENAMED')::jsonb from s2 where (s2.n1).id = n3.id returning s2.n0 as n0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n1) select 1; -- case: match (n)-[r:EdgeKind1]->() where n:NodeKind1 set r.visited = true return r -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.kind_ids operator (pg_catalog.@>) array [1]::int2[]) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (update edge e1 set properties = e1.properties || jsonb_build_object('visited', true)::jsonb from s0 where (s0.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s0.n0 as n0, s0.n1 as n1) select s1.e0 as r from s1; +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 from edge e0 join node n0 on (n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (update edge e1 set properties = e1.properties || jsonb_build_object('visited', true)::jsonb from s0 where (s0.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s0.n0 as n0) select s1.e0 as r from s1; -- case: match (n)-[]->()-[r]->() where n.name = 'n1' set r.visited = true return r.name -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.properties ->> 'name') = 'n1') and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id), s2 as (update edge e2 set properties = e2.properties || jsonb_build_object('visited', true)::jsonb from s1 where (s1.e1).id = e2.id returning s1.e0 as e0, (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1, s1.n2 as n2) select ((s2.e1).properties -> 'name') from s2; +with s0 as (select (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.properties ->> 'name') = 'n1') and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id), s2 as (update edge e2 set properties = e2.properties || jsonb_build_object('visited', true)::jsonb from s1 where (s1.e1).id = e2.id returning (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1) select ((s2.e1).properties -> 'name') from s2; diff --git a/cypher/models/pgsql/test/validation_integration_test.go b/cypher/models/pgsql/test/validation_integration_test.go index 1c0fe399..f106a36e 100644 --- a/cypher/models/pgsql/test/validation_integration_test.go +++ b/cypher/models/pgsql/test/validation_integration_test.go @@ -7,9 +7,11 @@ import ( "fmt" "os" "runtime/debug" + "strings" "testing" "github.com/specterops/dawgs" + "github.com/specterops/dawgs/drivers" "github.com/specterops/dawgs/drivers/pg" "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/util/size" @@ -18,8 +20,26 @@ import ( const ( PGConnectionStringEV = "PG_CONNECTION_STRING" + + emptyFrontier = `insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) +select 1::int8, 1::int8, 1::int4, false, false, array []::int8[] +where false;` ) +func nextFrontValues(rows ...string) string { + return fmt.Sprintf( + "insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) values %s;", + strings.Join(rows, ", "), + ) +} + +func pairFilterValues(rows ...string) string { + return fmt.Sprintf( + "insert into traversal_pair_filter (root_id, terminal_id) values %s;", + strings.Join(rows, ", "), + ) +} + func TestTranslationTestCases(t *testing.T) { var ( testCtx, done = context.WithCancel(context.Background()) @@ -35,7 +55,7 @@ func TestTranslationTestCases(t *testing.T) { // pool connection. Using pgxpool.New directly omits these hooks; after // AssertSchema calls pool.Reset(), new connections would return composite // values as raw []uint8 instead of map[string]any, causing scan failures. - if pgxPool, err := pg.NewPool(pgConnectionStr); err != nil { + if pgxPool, err := pg.NewPool(drivers.DatabaseConfiguration{Connection: pgConnectionStr}); err != nil { t.Fatalf("Failed opening database connection: %v", err) } else if connection, err := dawgs.Open(context.TODO(), pg.DriverName, dawgs.Config{ GraphQueryMemoryLimit: size.Gibibyte, @@ -99,3 +119,270 @@ func TestTranslationTestCases(t *testing.T) { fmt.Printf("Validated %d test cases with %d passing\n", casesRun, cassesPassed) } } + +func TestBidirectionalASPHarnessOverloads(t *testing.T) { + var ( + testCtx, done = context.WithCancel(context.Background()) + pgConnectionStr = os.Getenv(PGConnectionStringEV) + ) + + defer done() + + require.NotEmpty(t, pgConnectionStr) + + pgxPool, err := pg.NewPool(drivers.DatabaseConfiguration{Connection: pgConnectionStr}) + require.NoError(t, err) + defer pgxPool.Close() + + connection, err := dawgs.Open(context.TODO(), pg.DriverName, dawgs.Config{ + GraphQueryMemoryLimit: size.Gibibyte, + Pool: pgxPool, + }) + require.NoError(t, err) + defer connection.Close(testCtx) + + graphSchema := graph.Schema{ + Graphs: []graph.Graph{{ + Name: "test", + Nodes: graph.Kinds{ + graph.StringKind("NodeKind1"), + graph.StringKind("NodeKind2"), + }, + Edges: graph.Kinds{ + graph.StringKind("EdgeKind1"), + graph.StringKind("EdgeKind2"), + }, + }}, + DefaultGraph: graph.Graph{ + Name: "test", + }, + } + + require.NoError(t, connection.AssertSchema(testCtx, graphSchema)) + + t.Run("array overload does not require pair filter", func(t *testing.T) { + tx, err := pgxPool.Begin(testCtx) + require.NoError(t, err) + defer tx.Rollback(testCtx) + + var count int + require.NoError(t, tx.QueryRow(testCtx, + "select count(*) from bidirectional_asp_harness($1::text, $2::text, $3::text, $4::text, 1, array []::int8[], array []::int8[])", + emptyFrontier, + emptyFrontier, + emptyFrontier, + emptyFrontier, + ).Scan(&count)) + require.Equal(t, 0, count) + }) + + t.Run("pair filter constrains midpoint meets", func(t *testing.T) { + tx, err := pgxPool.Begin(testCtx) + require.NoError(t, err) + defer tx.Rollback(testCtx) + + var ( + graphID int64 + kindID int16 + ) + + require.NoError(t, tx.QueryRow(testCtx, "select id from graph where name = 'test'").Scan(&graphID)) + require.NoError(t, tx.QueryRow(testCtx, "select id from kind where name = 'EdgeKind1'").Scan(&kindID)) + + _, err = tx.Exec(testCtx, + "insert into edge (graph_id, start_id, end_id, kind_id, properties) values ($1, 10, 10, $2, '{}'::jsonb) on conflict do nothing", + graphID, + kindID, + ) + require.NoError(t, err) + + forwardPrimer := nextFrontValues( + "(1::int8, 10::int8, 1::int4, false, false, array [101]::int8[])", + "(3::int8, 10::int8, 1::int4, false, false, array [103]::int8[])", + ) + backwardPrimer := nextFrontValues( + "(2::int8, 10::int8, 1::int4, false, false, array [202]::int8[])", + "(4::int8, 10::int8, 1::int4, false, false, array [204]::int8[])", + ) + pairFilter := pairFilterValues("(1::int8, 2::int8)") + + rows, err := tx.Query(testCtx, + "select root_id, next_id from bidirectional_asp_harness($1::text, $2::text, $3::text, $4::text, 4, ''::text, ''::text, $5::text) order by root_id, next_id", + forwardPrimer, + emptyFrontier, + backwardPrimer, + emptyFrontier, + pairFilter, + ) + require.NoError(t, err) + defer rows.Close() + + var results [][2]int64 + for rows.Next() { + var result [2]int64 + require.NoError(t, rows.Scan(&result[0], &result[1])) + results = append(results, result) + } + require.NoError(t, rows.Err()) + require.Equal(t, [][2]int64{{1, 2}}, results) + }) + + t.Run("pair-aware all shortest paths harness resolves all explicit pairs", func(t *testing.T) { + tx, err := pgxPool.Begin(testCtx) + require.NoError(t, err) + defer tx.Rollback(testCtx) + + var ( + graphID int64 + kindID int16 + ) + + require.NoError(t, tx.QueryRow(testCtx, "select id from graph where name = 'test'").Scan(&graphID)) + require.NoError(t, tx.QueryRow(testCtx, "select id from kind where name = 'EdgeKind1'").Scan(&kindID)) + + _, err = tx.Exec(testCtx, + "insert into edge (graph_id, start_id, end_id, kind_id, properties) values ($1, 30, 30, $2, '{}'::jsonb) on conflict do nothing", + graphID, + kindID, + ) + require.NoError(t, err) + + forwardPrimer := nextFrontValues( + "(1::int8, 2::int8, 1::int4, true, false, array [102]::int8[])", + "(1::int8, 2::int8, 1::int4, true, false, array [103]::int8[])", + "(3::int8, 30::int8, 1::int4, false, false, array [330]::int8[])", + ) + backwardPrimer := nextFrontValues( + "(4::int8, 30::int8, 1::int4, false, false, array [304]::int8[])", + "(4::int8, 30::int8, 1::int4, false, false, array [305]::int8[])", + ) + pairFilter := pairFilterValues( + "(1::int8, 2::int8)", + "(3::int8, 4::int8)", + ) + + rows, err := tx.Query(testCtx, + "select root_id, next_id, depth, path from bidirectional_asp_harness($1::text, $2::text, $3::text, $4::text, 4, ''::text, ''::text, $5::text) order by root_id, next_id, path", + forwardPrimer, + emptyFrontier, + backwardPrimer, + emptyFrontier, + pairFilter, + ) + require.NoError(t, err) + defer rows.Close() + + type result struct { + rootID int64 + nextID int64 + depth int32 + path []int64 + } + + var results []result + for rows.Next() { + var next result + require.NoError(t, rows.Scan(&next.rootID, &next.nextID, &next.depth, &next.path)) + results = append(results, next) + } + require.NoError(t, rows.Err()) + + require.Equal(t, []result{{ + rootID: 1, + nextID: 2, + depth: 1, + path: []int64{102}, + }, { + rootID: 1, + nextID: 2, + depth: 1, + path: []int64{103}, + }, { + rootID: 3, + nextID: 4, + depth: 2, + path: []int64{330, 304}, + }, { + rootID: 3, + nextID: 4, + depth: 2, + path: []int64{330, 305}, + }}, results) + }) + + t.Run("shortest path harnesses avoid output column ambiguity", func(t *testing.T) { + frontier := nextFrontValues("(1::int8, 2::int8, 1::int4, false, false, array [101]::int8[])") + + var unidirectionalCount int + require.NoError(t, pgxPool.QueryRow(testCtx, + "select count(*) from unidirectional_sp_harness($1::text, $2::text, 1, array []::int8[], array []::int8[])", + frontier, + emptyFrontier, + ).Scan(&unidirectionalCount)) + require.Equal(t, 0, unidirectionalCount) + + var bidirectionalCount int + require.NoError(t, pgxPool.QueryRow(testCtx, + "select count(*) from bidirectional_sp_harness($1::text, $2::text, $3::text, $4::text, 1, array []::int8[], array []::int8[])", + frontier, + emptyFrontier, + emptyFrontier, + emptyFrontier, + ).Scan(&bidirectionalCount)) + require.Equal(t, 0, bidirectionalCount) + }) + + t.Run("pair-aware shortest path harness resolves all explicit pairs", func(t *testing.T) { + tx, err := pgxPool.Begin(testCtx) + require.NoError(t, err) + defer tx.Rollback(testCtx) + + forwardPrimer := nextFrontValues( + "(1::int8, 2::int8, 1::int4, true, false, array [102]::int8[])", + "(3::int8, 30::int8, 1::int4, false, false, array [330]::int8[])", + ) + backwardPrimer := nextFrontValues("(4::int8, 30::int8, 1::int4, false, false, array [304]::int8[])") + pairFilter := pairFilterValues( + "(1::int8, 2::int8)", + "(3::int8, 4::int8)", + ) + + rows, err := tx.Query(testCtx, + "select root_id, next_id, depth, path from bidirectional_sp_harness($1::text, $2::text, $3::text, $4::text, 4, ''::text, ''::text, $5::text) order by root_id, next_id", + forwardPrimer, + emptyFrontier, + backwardPrimer, + emptyFrontier, + pairFilter, + ) + require.NoError(t, err) + defer rows.Close() + + type result struct { + rootID int64 + nextID int64 + depth int32 + path []int64 + } + + var results []result + for rows.Next() { + var next result + require.NoError(t, rows.Scan(&next.rootID, &next.nextID, &next.depth, &next.path)) + results = append(results, next) + } + require.NoError(t, rows.Err()) + + require.Equal(t, []result{{ + rootID: 1, + nextID: 2, + depth: 1, + path: []int64{102}, + }, { + rootID: 3, + nextID: 4, + depth: 2, + path: []int64{330, 304}, + }}, results) + }) +} diff --git a/cypher/models/pgsql/translate/constraints_test.go b/cypher/models/pgsql/translate/constraints_test.go index 32b9d19a..94f41619 100644 --- a/cypher/models/pgsql/translate/constraints_test.go +++ b/cypher/models/pgsql/translate/constraints_test.go @@ -17,3 +17,309 @@ func TestMeasureSelectivity(t *testing.T) { require.Nil(t, err) require.Equal(t, 30, selectivity) } + +func TestCanExecuteSelectiveBidirectionalSearch(t *testing.T) { + lowSelectivity := pgd.Equals( + pgsql.Identifier("123"), + pgsql.Identifier("456"), + ) + idLookup := func(identifier pgsql.Identifier, id int64) pgsql.Expression { + return pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnID}, + pgd.IntLiteral(id), + ) + } + + t.Run("rejects low selectivity endpoints", func(t *testing.T) { + step := &TraversalStep{ + Expansion: &Expansion{ + PrimerNodeConstraints: lowSelectivity, + TerminalNodeConstraints: lowSelectivity, + }, + } + + canExecute, err := step.CanExecuteSelectiveBidirectionalSearch(NewScope()) + + require.NoError(t, err) + require.False(t, canExecute) + }) + + t.Run("accepts singleton endpoint pairs", func(t *testing.T) { + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n0"), + }, + RightNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n1"), + }, + Expansion: &Expansion{ + PrimerNodeConstraints: idLookup(pgsql.Identifier("n0"), 1), + TerminalNodeConstraints: idLookup(pgsql.Identifier("n1"), 2), + }, + } + + canExecute, err := step.CanExecuteSelectiveBidirectionalSearch(NewScope()) + + require.NoError(t, err) + require.True(t, canExecute) + }) + + t.Run("rejects non-static id equality endpoints", func(t *testing.T) { + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n0"), + }, + RightNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n1"), + }, + Expansion: &Expansion{ + PrimerNodeConstraints: pgd.Equals( + pgsql.CompoundIdentifier{pgsql.Identifier("n0"), pgsql.ColumnID}, + pgsql.CompoundIdentifier{pgsql.Identifier("n0"), pgsql.ColumnProperties}, + ), + TerminalNodeConstraints: idLookup(pgsql.Identifier("n1"), 2), + }, + } + + canExecute, err := step.CanExecuteSelectiveBidirectionalSearch(NewScope()) + + require.NoError(t, err) + require.False(t, canExecute) + }) + + t.Run("rejects singleton endpoints with external constraints", func(t *testing.T) { + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n0"), + }, + RightNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n1"), + }, + Expansion: &Expansion{ + PrimerNodeConstraints: pgd.And( + idLookup(pgsql.Identifier("n0"), 1), + pgd.Equals( + pgsql.CompoundIdentifier{pgsql.Identifier("n0"), pgsql.ColumnProperties}, + pgsql.CompoundIdentifier{pgsql.Identifier("n1"), pgsql.ColumnProperties}, + ), + ), + TerminalNodeConstraints: idLookup(pgsql.Identifier("n1"), 2), + }, + } + + canExecute, err := step.CanExecuteSelectiveBidirectionalSearch(NewScope()) + + require.NoError(t, err) + require.False(t, canExecute) + }) + + t.Run("accepts materialized endpoint pairs", func(t *testing.T) { + scope := NewScope() + _, err := scope.PushFrame() + require.NoError(t, err) + + frame, err := scope.PushFrame() + require.NoError(t, err) + + step := &TraversalStep{ + Frame: frame, + LeftNodeBound: true, + RightNodeBound: true, + Expansion: &Expansion{}, + } + + canExecute, err := step.CanExecuteSelectiveBidirectionalSearch(scope) + + require.NoError(t, err) + require.True(t, canExecute) + }) +} + +func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { + scopeWithNodeBindings := func(identifiers ...pgsql.Identifier) *Scope { + scope := NewScope() + for _, identifier := range identifiers { + scope.Define(identifier, pgsql.NodeComposite) + } + + return scope + } + localSelectivePropertyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgd.PropertyLookup(identifier, "name"), + pgd.TextLiteral("123"), + ) + } + localBroadPropertyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnProperties}, + pgd.IntLiteral(1), + ) + } + localKindConstraint := func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.And( + pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, + pgd.IntLiteral(1), + ), + pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, + pgd.IntLiteral(2), + ), + ) + } + + t.Run("accepts selective property-backed local endpoint constraints for shortest path", func(t *testing.T) { + leftIdentifier := pgsql.Identifier("n0") + rightIdentifier := pgsql.Identifier("n1") + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, + }, + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), + }, + } + + canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) + + require.NoError(t, err) + require.True(t, canExecute) + }) + + t.Run("rejects broad non-kind local endpoint constraints for shortest path", func(t *testing.T) { + leftIdentifier := pgsql.Identifier("n0") + rightIdentifier := pgsql.Identifier("n1") + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, + }, + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: localBroadPropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localBroadPropertyConstraint(rightIdentifier), + }, + } + + canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) + + require.NoError(t, err) + require.False(t, canExecute) + }) + + t.Run("rejects pair-aware search when only one endpoint is selective", func(t *testing.T) { + leftIdentifier := pgsql.Identifier("n0") + rightIdentifier := pgsql.Identifier("n1") + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, + }, + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localBroadPropertyConstraint(rightIdentifier), + }, + } + + canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) + + require.NoError(t, err) + require.False(t, canExecute) + }) + + t.Run("rejects label-only local endpoint constraints for shortest path", func(t *testing.T) { + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n0"), + }, + RightNode: &BoundIdentifier{ + Identifier: pgsql.Identifier("n1"), + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: localKindConstraint(pgsql.Identifier("n0")), + TerminalNodeConstraints: localKindConstraint(pgsql.Identifier("n1")), + }, + } + + canExecute, err := step.CanExecutePairAwareBidirectionalSearch(NewScope()) + + require.NoError(t, err) + require.False(t, canExecute) + }) + + t.Run("accepts selective property-backed local endpoint constraints for all shortest paths", func(t *testing.T) { + leftIdentifier := pgsql.Identifier("n0") + rightIdentifier := pgsql.Identifier("n1") + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, + }, + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindAllShortestPaths: true, + }, + PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), + }, + } + + canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) + + require.NoError(t, err) + require.True(t, canExecute) + }) + + t.Run("rejects endpoint constraints that reference the other endpoint", func(t *testing.T) { + leftIdentifier := pgsql.Identifier("n0") + rightIdentifier := pgsql.Identifier("n1") + step := &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, + }, + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: pgd.And( + localSelectivePropertyConstraint(leftIdentifier), + pgd.Equals( + pgsql.CompoundIdentifier{leftIdentifier, pgsql.ColumnKindIDs}, + pgsql.CompoundIdentifier{rightIdentifier, pgsql.ColumnKindIDs}, + ), + ), + TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), + }, + } + + canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) + + require.NoError(t, err) + require.False(t, canExecute) + }) +} diff --git a/cypher/models/pgsql/translate/create.go b/cypher/models/pgsql/translate/create.go new file mode 100644 index 00000000..ddb463cf --- /dev/null +++ b/cypher/models/pgsql/translate/create.go @@ -0,0 +1,449 @@ +package translate + +import ( + "fmt" + "sort" + + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/graph" +) + +func createIDIdentifier(binding *BoundIdentifier) pgsql.Identifier { + return pgsql.Identifier(binding.Identifier.String() + "_id") +} + +func sequenceValue(table pgsql.Identifier) pgsql.Expression { + return pgsql.FunctionCall{ + Function: pgsql.FunctionNextValue, + Parameters: []pgsql.Expression{ + pgsql.FunctionCall{ + Function: pgsql.FunctionPGGetSerialSequence, + Parameters: []pgsql.Expression{ + pgsql.NewLiteral(table.String(), pgsql.Text), + pgsql.NewLiteral(pgsql.ColumnID.String(), pgsql.Text), + }, + }, + }, + CastType: pgsql.Int8, + } +} + +func frameReference(frame *Frame) pgsql.FromClause { + return pgsql.FromClause{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{frame.Binding.Identifier}, + }, + } +} + +func (s *Translator) createSourceFromClauses(previousFrame *Frame) []pgsql.FromClause { + var fromClauses []pgsql.FromClause + + if previousFrame != nil && !previousFrame.Synthetic { + fromClauses = append(fromClauses, frameReference(previousFrame)) + } + + return append(fromClauses, unwindFromClauses(s.query.CurrentPart().ConsumeUnwindClauses())...) +} + +func bindingProjectionExpression(binding *BoundIdentifier, referenceFrame *Frame) pgsql.Expression { + if referenceFrame != nil && !referenceFrame.Synthetic { + return pgsql.CompoundIdentifier{referenceFrame.Binding.Identifier, binding.Identifier} + } + + if binding.LastProjection != nil { + return pgsql.CompoundIdentifier{binding.LastProjection.Binding.Identifier, binding.Identifier} + } + + return binding.Identifier +} + +func buildCarryProjection(scope *Scope, identifiers *pgsql.IdentifierSet, referenceFrame *Frame) (pgsql.Projection, error) { + var projection pgsql.Projection + + for _, identifier := range identifiers.Slice() { + binding, bound := scope.Lookup(identifier) + if !bound { + return nil, fmt.Errorf("missing bound identifier: %s", identifier) + } + + projection = append(projection, &pgsql.AliasedExpression{ + Expression: bindingProjectionExpression(binding, referenceFrame), + Alias: pgsql.AsOptionalIdentifier(identifier), + }) + } + + return projection, nil +} + +func (s *Translator) addCTE(frame *Frame, body pgsql.SetExpression) { + s.query.CurrentPart().Model.AddCTE(pgsql.CommonTableExpression{ + Alias: pgsql.TableAlias{ + Name: frame.Binding.Identifier, + }, + Query: pgsql.Query{ + Body: body, + }, + }) +} + +func (s *Translator) buildCreateSourceFrame(idAlias pgsql.Identifier, sequenceTable pgsql.Identifier) (*Frame, *pgsql.IdentifierSet, error) { + var ( + currentFrame = s.scope.CurrentFrame() + carried = pgsql.NewIdentifierSet() + ) + + if currentFrame != nil { + carried = currentFrame.Known() + } + + // Reserve the database ID before the INSERT CTE runs. This lets later edge + // creations in the same Cypher CREATE clause refer to newly-created nodes. + sourceProjection, err := buildCarryProjection(s.scope, carried, currentFrame) + if err != nil { + return nil, nil, err + } + + sourceProjection = append(sourceProjection, &pgsql.AliasedExpression{ + Expression: sequenceValue(sequenceTable), + Alias: pgsql.AsOptionalIdentifier(idAlias), + }) + + sourceFrame, err := s.scope.PushFrame() + if err != nil { + return nil, nil, err + } + + sourceFrame.Visible = carried.Copy() + sourceFrame.Exported = carried.Copy() + + s.addCTE(sourceFrame, pgsql.Select{ + Projection: sourceProjection, + From: s.createSourceFromClauses(currentFrame), + }) + + for _, identifier := range carried.Slice() { + if binding, bound := s.scope.Lookup(identifier); bound { + binding.MaterializedBy(sourceFrame) + } + } + + return sourceFrame, carried, nil +} + +func (s *Translator) buildCreateProjectionFrame(sourceFrame, insertFrame *Frame, carried *pgsql.IdentifierSet, createdBinding *BoundIdentifier, idAlias pgsql.Identifier) (*Frame, error) { + projection, err := buildCarryProjection(s.scope, carried, sourceFrame) + if err != nil { + return nil, err + } + + projection = append(projection, &pgsql.AliasedExpression{ + Expression: pgsql.CompoundIdentifier{insertFrame.Binding.Identifier, createdBinding.Identifier}, + Alias: pgsql.AsOptionalIdentifier(createdBinding.Identifier), + }) + + projectFrame, err := s.scope.PushFrame() + if err != nil { + return nil, err + } + + // Join the source and insert CTEs by the preallocated ID so row cardinality + // from the incoming pipeline is preserved after RETURNING materializes the composite. + carried = carried.Copy().Add(createdBinding.Identifier) + projectFrame.Visible = carried.Copy() + projectFrame.Exported = carried.Copy() + + s.addCTE(projectFrame, pgsql.Select{ + Projection: projection, + From: []pgsql.FromClause{ + frameReference(sourceFrame), + frameReference(insertFrame), + }, + Where: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{insertFrame.Binding.Identifier, idAlias}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{sourceFrame.Binding.Identifier, idAlias}, + ), + }) + + for _, identifier := range carried.Slice() { + if binding, bound := s.scope.Lookup(identifier); bound { + binding.MaterializedBy(projectFrame) + } + } + + return projectFrame, nil +} + +func (s *Translator) translateCreate(create *cypher.Create) error { + s.query.CurrentPart().isCreating = false + + if err := s.buildNodeCreations(); err != nil { + return err + } + + return s.buildEdgeCreations() +} + +func (s *Translator) buildNodeCreations() error { + currentQueryPart := s.query.CurrentPart() + + for _, nodeCreate := range currentQueryPart.mutations.Creations.Values() { + idAlias := createIDIdentifier(nodeCreate.Binding) + + // Each CREATE item becomes a source CTE, an INSERT CTE, and a projection + // CTE. Keeping them separate preserves the existing frame pipeline while + // making created values visible to later CREATE items. + sourceFrame, carried, err := s.buildCreateSourceFrame(idAlias, pgsql.TableNode) + if err != nil { + return err + } + + kindIDsExpr, err := s.buildKindIDsArray(nodeCreate) + if err != nil { + return err + } + + propsExpr, err := s.buildPropertiesObject(nodeCreate.Properties) + if err != nil { + return err + } + + insertFrame, err := s.scope.PushFrame() + if err != nil { + return err + } + + s.addCTE(insertFrame, pgsql.Insert{ + Table: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{pgsql.TableNode}, + }, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{ + pgsql.ColumnGraphID, + pgsql.ColumnID, + pgsql.ColumnKindIDs, + pgsql.ColumnProperties, + }), + Source: &pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.NewLiteral(s.graphID, pgsql.Int4), + pgsql.CompoundIdentifier{sourceFrame.Binding.Identifier, idAlias}, + kindIDsExpr, + propsExpr, + }, + From: []pgsql.FromClause{frameReference(sourceFrame)}, + }, + }, + Returning: []pgsql.SelectItem{ + &pgsql.AliasedExpression{ + Expression: pgsql.ColumnID, + Alias: pgsql.AsOptionalIdentifier(idAlias), + }, + &pgsql.AliasedExpression{ + Expression: pgsql.CompositeValue{ + DataType: pgsql.NodeComposite, + Values: []pgsql.Expression{ + pgsql.ColumnID, + pgsql.ColumnKindIDs, + pgsql.ColumnProperties, + }, + }, + Alias: pgsql.AsOptionalIdentifier(nodeCreate.Binding.Identifier), + }, + }, + }) + + if _, err := s.buildCreateProjectionFrame(sourceFrame, insertFrame, carried, nodeCreate.Binding, idAlias); err != nil { + return err + } + } + + return nil +} + +func (s *Translator) buildKindIDsArray(nodeCreate *NodeCreate) (pgsql.SelectItem, error) { + if len(nodeCreate.Kinds) == 0 { + return pgsql.ArrayLiteral{ + Values: []pgsql.Expression{}, + CastType: pgsql.Int2Array, + }, nil + } + + kindIDs, err := s.kindMapper.AssertKinds(nodeCreate.Kinds) + if err != nil { + return nil, fmt.Errorf("failed to translate kinds: %w", err) + } + + arrayLiteral := pgsql.ArrayLiteral{ + Values: make([]pgsql.Expression, len(kindIDs)), + CastType: pgsql.Int2Array, + } + + for idx, kindID := range kindIDs { + arrayLiteral.Values[idx] = pgsql.NewLiteral(kindID, pgsql.Int2) + } + + return arrayLiteral, nil +} + +func (s *Translator) buildPropertiesObject(properties map[string]pgsql.Expression) (pgsql.SelectItem, error) { + jsonObjectFunction := pgsql.FunctionCall{ + Function: pgsql.FunctionJSONBBuildObject, + CastType: pgsql.JSONB, + } + + keys := make([]string, 0, len(properties)) + for key := range properties { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + value := properties[key] + if err := RewriteFrameBindings(s.scope, value); err != nil { + return nil, err + } + + jsonObjectFunction.Parameters = append(jsonObjectFunction.Parameters, + pgsql.NewLiteral(key, pgsql.Text), + value, + ) + } + + return jsonObjectFunction, nil +} + +func (s *Translator) buildEdgeCreations() error { + currentQueryPart := s.query.CurrentPart() + + for _, edgeCreate := range currentQueryPart.mutations.EdgeCreations.Values() { + if edgeCreate.LeftNode == nil || edgeCreate.RightNode == nil { + return fmt.Errorf("edge creation %s is missing endpoint nodes", edgeCreate.Binding.Identifier) + } + + idAlias := createIDIdentifier(edgeCreate.Binding) + + // Edge creation runs after node creation so endpoint references can resolve + // either carried-in nodes or nodes inserted earlier in this CREATE clause. + sourceFrame, carried, err := s.buildCreateSourceFrame(idAlias, pgsql.TableEdge) + if err != nil { + return err + } + + startNode, endNode, err := resolveEdgeEndpoints(edgeCreate) + if err != nil { + return err + } + + kindIDExpr, err := s.buildEdgeKindIDExpression(edgeCreate) + if err != nil { + return err + } + + startIDRef := createdNodeIDRef(sourceFrame, startNode) + endIDRef := createdNodeIDRef(sourceFrame, endNode) + + propsExpr, err := s.buildPropertiesObject(edgeCreate.Properties) + if err != nil { + return err + } + + insertFrame, err := s.scope.PushFrame() + if err != nil { + return err + } + + s.addCTE(insertFrame, pgsql.Insert{ + Table: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, + }, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{ + pgsql.ColumnGraphID, + pgsql.ColumnID, + pgsql.ColumnStartID, + pgsql.ColumnEndID, + pgsql.ColumnKindID, + pgsql.ColumnProperties, + }), + Source: &pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.NewLiteral(s.graphID, pgsql.Int4), + pgsql.CompoundIdentifier{sourceFrame.Binding.Identifier, idAlias}, + startIDRef, + endIDRef, + kindIDExpr, + propsExpr, + }, + From: []pgsql.FromClause{frameReference(sourceFrame)}, + }, + }, + Returning: []pgsql.SelectItem{ + &pgsql.AliasedExpression{ + Expression: pgsql.ColumnID, + Alias: pgsql.AsOptionalIdentifier(idAlias), + }, + &pgsql.AliasedExpression{ + Expression: pgsql.CompositeValue{ + DataType: pgsql.EdgeComposite, + Values: []pgsql.Expression{ + pgsql.ColumnID, + pgsql.ColumnStartID, + pgsql.ColumnEndID, + pgsql.ColumnKindID, + pgsql.ColumnProperties, + }, + }, + Alias: pgsql.AsOptionalIdentifier(edgeCreate.Binding.Identifier), + }, + }, + }) + + if _, err := s.buildCreateProjectionFrame(sourceFrame, insertFrame, carried, edgeCreate.Binding, idAlias); err != nil { + return err + } + } + + return nil +} + +func resolveEdgeEndpoints(edgeCreate *EdgeCreate) (*BoundIdentifier, *BoundIdentifier, error) { + switch edgeCreate.Direction { + case graph.DirectionOutbound: + return edgeCreate.LeftNode, edgeCreate.RightNode, nil + + case graph.DirectionInbound: + return edgeCreate.RightNode, edgeCreate.LeftNode, nil + + default: + return nil, nil, fmt.Errorf("unsupported direction for edge creation: %v", edgeCreate.Direction) + } +} + +func (s *Translator) buildEdgeKindIDExpression(edgeCreate *EdgeCreate) (pgsql.SelectItem, error) { + if len(edgeCreate.Kinds) == 0 { + return nil, fmt.Errorf("edge creation requires exactly one kind but none were specified") + } + + if len(edgeCreate.Kinds) > 1 { + return nil, fmt.Errorf("edge creation supports only one kind but %d were specified", len(edgeCreate.Kinds)) + } + + if kindIDs, err := s.kindMapper.AssertKinds(edgeCreate.Kinds); err != nil { + return nil, fmt.Errorf("failed to translate edge kind: %w", err) + } else { + return pgsql.NewLiteral(kindIDs[0], pgsql.Int2), nil + } +} + +func createdNodeIDRef(sourceFrame *Frame, nodeBinding *BoundIdentifier) pgsql.SelectItem { + return pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{ + sourceFrame.Binding.Identifier, + nodeBinding.Identifier, + }, + Column: pgsql.ColumnID, + } +} diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 03c63966..5f3cb172 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -11,6 +11,16 @@ import ( const translateDefaultMaxTraversalDepth int64 = 15 +var ( + expansionRootFilter = pgsql.Identifier("traversal_root_filter") + expansionTerminalFilter = pgsql.Identifier("traversal_terminal_filter") + expansionPairFilter = pgsql.Identifier("traversal_pair_filter") + expansionTerminalID = pgsql.Identifier("terminal_id") + expansionVisited = pgsql.Identifier("visited") + expansionForwardVisited = pgsql.Identifier("forward_visited") + expansionBackwardVisited = pgsql.Identifier("backward_visited") +) + func expansionEdgeJoinCondition(traversalStep *TraversalStep) (pgsql.Expression, error) { return pgd.Equals( pgd.EntityID(traversalStep.LeftNode.Identifier), @@ -40,6 +50,7 @@ type ExpansionBuilder struct { PrimerStatement pgsql.Select RecursiveStatement pgsql.Select ProjectionStatement pgsql.Select + UseUnionAll bool queryParameters map[string]any traversalStep *TraversalStep @@ -70,30 +81,423 @@ func nextFrontInsert(body pgsql.SetExpression) pgsql.Insert { } } -func deframeExpression(expression pgsql.Expression) pgsql.Expression { +func expansionNodeTableReference(binding pgsql.Identifier) pgsql.TableReference { + return pgsql.TableReference{ + Name: pgsql.TableNode.AsCompoundIdentifier(), + Binding: models.OptionalValue(binding), + } +} + +func expansionEdgeTableReference(binding pgsql.Identifier) pgsql.TableReference { + return pgsql.TableReference{ + Name: pgsql.TableEdge.AsCompoundIdentifier(), + Binding: models.OptionalValue(binding), + } +} + +type expansionSeed struct { + identifier pgsql.Identifier + query pgsql.Select +} + +func expansionSeedIdentifier(expansionIdentifier pgsql.Identifier) pgsql.Identifier { + return pgsql.Identifier(string(expansionIdentifier) + "_seed") +} + +func expansionSeedColumns() *pgsql.RecordShape { + return pgsql.NewRecordShape([]pgsql.Identifier{ + expansionRootID, + }) +} + +func newExpansionSeed(identifier pgsql.Identifier, rootExpression pgsql.Expression, from []pgsql.FromClause, where pgsql.Expression) expansionSeed { + return expansionSeed{ + identifier: identifier, + query: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.AliasedExpression{ + Expression: rootExpression, + Alias: models.OptionalValue(expansionRootID), + }, + }, + From: from, + Where: where, + }, + } +} + +func newExpansionNodeSeed(identifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { + return newExpansionSeed(identifier, pgd.EntityID(nodeIdentifier), []pgsql.FromClause{{ + Source: expansionNodeTableReference(nodeIdentifier), + }}, constraints) +} + +func newExpansionNodeFilterSeed(identifier, filterIdentifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { + filterAlias := pgsql.Identifier(string(identifier) + "_filter") + filterID := pgsql.CompoundIdentifier{filterAlias, pgsql.ColumnID} + + if constraints == nil { + return newExpansionSeed(identifier, filterID, []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{filterIdentifier}, + Binding: models.OptionalValue(filterAlias), + }, + }}, nil) + } + + seed := newExpansionSeed(identifier, pgd.EntityID(nodeIdentifier), []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{filterIdentifier}, + Binding: models.OptionalValue(filterAlias), + }, + Joins: []pgsql.Join{{ + Table: expansionNodeTableReference(nodeIdentifier), + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgd.Equals( + pgd.EntityID(nodeIdentifier), + filterID, + ), + }, + }}, + }}, constraints) + seed.query.Distinct = true + + return seed +} + +func newExpansionBoundNodeSeed(identifier pgsql.Identifier, previousFrame *Frame, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { + seed := newExpansionSeed(identifier, pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{ + previousFrame.Binding.Identifier, + nodeIdentifier, + }, + Column: pgsql.ColumnID, + }, []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{previousFrame.Binding.Identifier}, + }, + }}, constraints) + + seed.query.Distinct = true + return seed +} + +func newExpansionRootIDsParameterSeed(identifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { + return newExpansionNodeFilterSeed(identifier, expansionRootFilter, nodeIdentifier, constraints) +} + +func newExpansionTerminalIDsParameterSeed(identifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { + return newExpansionNodeFilterSeed(identifier, expansionTerminalFilter, nodeIdentifier, constraints) +} + +func (s expansionSeed) CTE() pgsql.CommonTableExpression { + return pgsql.CommonTableExpression{ + Alias: pgsql.TableAlias{ + Name: s.identifier, + Shape: expansionSeedColumns(), + }, + Materialized: &pgsql.Materialized{Materialized: false}, + Query: pgsql.Query{ + Body: s.query, + }, + } +} + +func (s expansionSeed) rootID() pgsql.CompoundIdentifier { + return pgsql.CompoundIdentifier{s.identifier, expansionRootID} +} + +func (s expansionSeed) fromClause(joins ...pgsql.Join) pgsql.FromClause { + return pgsql.FromClause{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{s.identifier}, + }, + Joins: joins, + } +} + +func (s expansionSeed) edgeJoin(edgeIdentifier pgsql.Identifier, edgeStartColumn pgsql.CompoundIdentifier) pgsql.Join { + return pgsql.Join{ + Table: expansionEdgeTableReference(edgeIdentifier), + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgd.Equals(edgeStartColumn, s.rootID()), + }, + } +} + +func expansionEdgeFromClause(edgeIdentifier pgsql.Identifier, joins ...pgsql.Join) pgsql.FromClause { + return pgsql.FromClause{ + Source: expansionEdgeTableReference(edgeIdentifier), + Joins: joins, + } +} + +func recursiveExpansionEdgeProjection(edgeIdentifier pgsql.Identifier) pgsql.Projection { + projection := make(pgsql.Projection, len(pgsql.EdgeTableColumns)) + + for idx, column := range pgsql.EdgeTableColumns { + projection[idx] = pgsql.CompoundIdentifier{edgeIdentifier, column} + } + + return projection +} + +func expansionEdgeNotInPath(edgeIdentifier, frameIdentifier pgsql.Identifier) *pgsql.BinaryExpression { + return pgsql.NewBinaryExpression( + pgd.EntityID(edgeIdentifier), + pgsql.OperatorNotEquals, + pgsql.NewAllExpression( + pgsql.CompoundIdentifier{frameIdentifier, expansionPath}, + ), + ) +} + +func recursiveExpansionEdgeLookupJoin(traversalStep *TraversalStep) pgsql.Join { + var ( + expansionModel = traversalStep.Expansion + edgeIdentifier = traversalStep.Edge.Identifier + edgeLookup = pgsql.Select{ + Projection: recursiveExpansionEdgeProjection(edgeIdentifier), + From: []pgsql.FromClause{{ + Source: expansionEdgeTableReference(edgeIdentifier), + }}, + Where: pgsql.OptionalAnd( + pgsql.OptionalAnd( + pgd.Equals( + expansionModel.EdgeStartColumn, + pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionNextID}, + ), + expansionEdgeNotInPath(edgeIdentifier, expansionModel.Frame.Binding.Identifier), + ), + expansionModel.EdgeConstraints, + ), + } + ) + + return pgsql.Join{ + Table: pgsql.LateralSubquery{ + Query: pgsql.Query{ + Body: edgeLookup, + // OFFSET 0 keeps PostgreSQL from flattening this correlated lookup into a merge over the full edge index. + Offset: pgsql.NewLiteral(0, pgsql.Int), + }, + Binding: models.OptionalValue(edgeIdentifier), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewLiteral(true, pgsql.Boolean), + }, + } +} + +func expansionNodeProjection(nodeIdentifier pgsql.Identifier) pgsql.Projection { + projection := make(pgsql.Projection, len(pgsql.NodeTableColumns)) + + for idx, column := range pgsql.NodeTableColumns { + projection[idx] = pgsql.CompoundIdentifier{nodeIdentifier, column} + } + + return projection +} + +func expansionNodeLookupJoin(nodeIdentifier pgsql.Identifier, nodeID pgsql.Expression) pgsql.Join { + nodeLookup := pgsql.Select{ + Projection: expansionNodeProjection(nodeIdentifier), + From: []pgsql.FromClause{{ + Source: expansionNodeTableReference(nodeIdentifier), + }}, + Where: pgd.Equals( + pgsql.CompoundIdentifier{nodeIdentifier, pgsql.ColumnID}, + nodeID, + ), + } + + return pgsql.Join{ + Table: pgsql.LateralSubquery{ + Query: pgsql.Query{ + Body: nodeLookup, + // OFFSET 0 keeps PostgreSQL from flattening this correlated lookup into a full-table hash join. + Offset: pgsql.NewLiteral(0, pgsql.Int), + }, + Binding: models.OptionalValue(nodeIdentifier), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewLiteral(true, pgsql.Boolean), + }, + } +} + +// rewriteBoundEndpointSeedReference converts references to a bound endpoint in +// the previous frame into references that are local to the seed query. Anything +// that still references outside scope is left for seedEndpointConstraintSplit to +// keep out of the seed. +func rewriteBoundEndpointSeedReference(expression pgsql.Expression, previousFrameIdentifier, nodeIdentifier pgsql.Identifier) pgsql.Expression { if expression == nil { return nil } switch typedExpression := expression.(type) { - case pgsql.RowColumnReference: - if compound, ok := typedExpression.Identifier.(pgsql.CompoundIdentifier); ok && len(compound) >= 2 { - // Drop the frame prefix and keep only the leaf identifier + column. - return pgsql.CompoundIdentifier{compound[len(compound)-1], typedExpression.Column} + case pgsql.CompoundIdentifier: + if previousFrameIdentifier != "" && len(typedExpression) == 2 && typedExpression[0] == previousFrameIdentifier && typedExpression[1] == nodeIdentifier { + return nodeIdentifier } return expression + case pgsql.RowColumnReference: + if previousFrameIdentifier != "" { + if compound, ok := typedExpression.Identifier.(pgsql.CompoundIdentifier); ok && + len(compound) == 2 && + compound[0] == previousFrameIdentifier && + compound[1] == nodeIdentifier { + return pgsql.CompoundIdentifier{nodeIdentifier, typedExpression.Column} + } + } + + return pgsql.RowColumnReference{ + Identifier: rewriteBoundEndpointSeedReference(typedExpression.Identifier, previousFrameIdentifier, nodeIdentifier), + Column: typedExpression.Column, + } + + case pgsql.TypeCast: + return pgsql.TypeCast{ + Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), + CastType: typedExpression.CastType, + } + + case pgsql.Variadic: + return pgsql.Variadic{ + Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), + } + + case pgsql.CompositeValue: + values := make([]pgsql.Expression, len(typedExpression.Values)) + for idx, value := range typedExpression.Values { + values[idx] = rewriteBoundEndpointSeedReference(value, previousFrameIdentifier, nodeIdentifier) + } + + return pgsql.CompositeValue{ + Values: values, + DataType: typedExpression.DataType, + } + + case pgsql.FunctionCall: + parameters := make([]pgsql.Expression, len(typedExpression.Parameters)) + for idx, parameter := range typedExpression.Parameters { + parameters[idx] = rewriteBoundEndpointSeedReference(parameter, previousFrameIdentifier, nodeIdentifier) + } + + return pgsql.FunctionCall{ + Bare: typedExpression.Bare, + Distinct: typedExpression.Distinct, + Function: typedExpression.Function, + Parameters: parameters, + Over: typedExpression.Over, + CastType: typedExpression.CastType, + } + + case *pgsql.FunctionCall: + if typedExpression == nil { + return nil + } + + rewritten := rewriteBoundEndpointSeedReference(*typedExpression, previousFrameIdentifier, nodeIdentifier).(pgsql.FunctionCall) + return &rewritten + + case pgsql.ArrayExpression: + return pgsql.ArrayExpression{ + Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), + } + + case pgsql.ArrayLiteral: + values := make([]pgsql.Expression, len(typedExpression.Values)) + for idx, value := range typedExpression.Values { + values[idx] = rewriteBoundEndpointSeedReference(value, previousFrameIdentifier, nodeIdentifier) + } + + return pgsql.ArrayLiteral{ + Values: values, + CastType: typedExpression.CastType, + } + + case pgsql.ArrayIndex: + indexes := make([]pgsql.Expression, len(typedExpression.Indexes)) + for idx, index := range typedExpression.Indexes { + indexes[idx] = rewriteBoundEndpointSeedReference(index, previousFrameIdentifier, nodeIdentifier) + } + + return pgsql.ArrayIndex{ + Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), + Indexes: indexes, + } + + case *pgsql.ArrayIndex: + if typedExpression == nil { + return nil + } + + rewritten := rewriteBoundEndpointSeedReference(*typedExpression, previousFrameIdentifier, nodeIdentifier).(pgsql.ArrayIndex) + return &rewritten + + case pgsql.AnyExpression: + return pgsql.AnyExpression{ + Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), + CastType: typedExpression.CastType, + } + + case *pgsql.AnyExpression: + if typedExpression == nil { + return nil + } + + rewritten := rewriteBoundEndpointSeedReference(*typedExpression, previousFrameIdentifier, nodeIdentifier).(pgsql.AnyExpression) + return &rewritten + + case pgsql.AllExpression: + return pgsql.NewAllExpression(rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier)) + + case pgsql.UnaryExpression: + return pgsql.UnaryExpression{ + Operator: typedExpression.Operator, + Operand: rewriteBoundEndpointSeedReference(typedExpression.Operand, previousFrameIdentifier, nodeIdentifier), + } + + case *pgsql.UnaryExpression: + if typedExpression == nil { + return nil + } + + rewritten := rewriteBoundEndpointSeedReference(*typedExpression, previousFrameIdentifier, nodeIdentifier).(pgsql.UnaryExpression) + return &rewritten + + case pgsql.BinaryExpression: + return pgsql.BinaryExpression{ + Operator: typedExpression.Operator, + LOperand: rewriteBoundEndpointSeedReference(typedExpression.LOperand, previousFrameIdentifier, nodeIdentifier), + ROperand: rewriteBoundEndpointSeedReference(typedExpression.ROperand, previousFrameIdentifier, nodeIdentifier), + } + case *pgsql.BinaryExpression: + if typedExpression == nil { + return nil + } + return &pgsql.BinaryExpression{ Operator: typedExpression.Operator, - LOperand: deframeExpression(typedExpression.LOperand), - ROperand: deframeExpression(typedExpression.ROperand), + LOperand: rewriteBoundEndpointSeedReference(typedExpression.LOperand, previousFrameIdentifier, nodeIdentifier), + ROperand: rewriteBoundEndpointSeedReference(typedExpression.ROperand, previousFrameIdentifier, nodeIdentifier), } case *pgsql.Parenthetical: + if typedExpression == nil { + return nil + } + return &pgsql.Parenthetical{ - Expression: deframeExpression(typedExpression.Expression), + Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), } default: @@ -101,53 +505,465 @@ func deframeExpression(expression pgsql.Expression) pgsql.Expression { } } -func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expansion) pgsql.Select { +func seedEndpointConstraintSplit(expression pgsql.Expression, nodeIdentifier pgsql.Identifier, previousFrameIdentifier pgsql.Identifier) (pgsql.Expression, pgsql.Expression) { + // Harness seed fragments only range over the endpoint node alias and an optional ID filter. + // Reframe safe endpoint references first, then leave anything still non-local for the outer projection. + seedExpression := rewriteBoundEndpointSeedReference(expression, previousFrameIdentifier, nodeIdentifier) + return partitionConstraintByLocality(seedExpression, pgsql.AsIdentifierSet(nodeIdentifier)) +} + +func seededFrontPrimerQuery(seed expansionSeed, primer pgsql.Select) pgsql.Query { + return pgsql.Query{ + CommonTableExpressions: &pgsql.With{ + Expressions: []pgsql.CommonTableExpression{seed.CTE()}, + }, + Body: primer, + } +} + +func frontPrimerQuery(seed *expansionSeed, primer pgsql.Select) pgsql.Query { + if seed == nil { + return pgsql.Query{Body: primer} + } + + return seededFrontPrimerQuery(*seed, primer) +} + +func (s *ExpansionBuilder) usesBoundRootIDs() bool { + return s.traversalStep.LeftNodeBound && s.traversalStep.Frame != nil && s.traversalStep.Frame.Previous != nil +} + +func (s *ExpansionBuilder) usesBoundTerminalIDs() bool { + return s.traversalStep.RightNodeBound && s.traversalStep.Frame != nil && s.traversalStep.Frame.Previous != nil +} + +func (s *ExpansionBuilder) usesBoundEndpointPairs() bool { + return s.usesBoundRootIDs() && s.usesBoundTerminalIDs() +} + +func (s *ExpansionBuilder) boundNodeIDsFilterStatement(filterIdentifier pgsql.Identifier, nodeIdentifier pgsql.Identifier) pgsql.Insert { + previousFrameIdentifier := s.traversalStep.Frame.Previous.Binding.Identifier + nodeIDExpression := pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, nodeIdentifier}, + Column: pgsql.ColumnID, + } + + return pgsql.Insert{ + Table: pgsql.TableReference{ + Name: filterIdentifier.AsCompoundIdentifier(), + }, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{pgsql.ColumnID}), + Source: &pgsql.Query{ + Body: pgsql.Select{ + Distinct: true, + Projection: []pgsql.SelectItem{ + nodeIDExpression, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{previousFrameIdentifier}, + }, + }}, + Where: pgsql.NewBinaryExpression( + nodeIDExpression, + pgsql.OperatorIsNot, + pgsql.NullLiteral(), + ), + }, + }, + } +} + +func (s *ExpansionBuilder) boundRootIDsFilterStatement() (pgsql.Insert, bool) { + if !s.usesBoundRootIDs() { + return pgsql.Insert{}, false + } + + return s.boundNodeIDsFilterStatement(expansionRootFilter, s.traversalStep.LeftNode.Identifier), true +} + +func (s *ExpansionBuilder) boundTerminalIDsFilterStatement() (pgsql.Insert, bool) { + if !s.usesBoundTerminalIDs() { + return pgsql.Insert{}, false + } + + return s.boundNodeIDsFilterStatement(expansionTerminalFilter, s.traversalStep.RightNode.Identifier), true +} + +func (s *ExpansionBuilder) unboundTerminalIDsFilterStatement() (pgsql.Insert, bool) { + expansionModel := s.traversalStep.Expansion + if !expansionModel.UseMaterializedTerminalFilter { + return pgsql.Insert{}, false + } + + return s.nodeIDsFilterStatement(expansionTerminalFilter, s.traversalStep.RightNode.Identifier, expansionModel.TerminalNodeConstraints), true +} + +func (s *ExpansionBuilder) nodeIDsFilterStatement(filterIdentifier pgsql.Identifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) pgsql.Insert { + nodeIDExpression := pgsql.CompoundIdentifier{nodeIdentifier, pgsql.ColumnID} + + return pgsql.Insert{ + Table: pgsql.TableReference{ + Name: filterIdentifier.AsCompoundIdentifier(), + }, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{pgsql.ColumnID}), + Source: &pgsql.Query{ + Body: pgsql.Select{ + Distinct: true, + Projection: []pgsql.SelectItem{ + nodeIDExpression, + }, + From: []pgsql.FromClause{{ + Source: expansionNodeTableReference(nodeIdentifier), + }}, + Where: pgsql.OptionalAnd( + constraints, + pgsql.NewBinaryExpression( + nodeIDExpression, + pgsql.OperatorIsNot, + pgsql.NullLiteral(), + ), + ), + }, + }, + } +} + +func (s *ExpansionBuilder) boundEndpointPairFilterStatement() (pgsql.Insert, bool) { + if !s.usesBoundEndpointPairs() { + return pgsql.Insert{}, false + } + + previousFrameIdentifier := s.traversalStep.Frame.Previous.Binding.Identifier + rootIDExpression := pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, s.traversalStep.LeftNode.Identifier}, + Column: pgsql.ColumnID, + } + terminalIDExpression := pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, s.traversalStep.RightNode.Identifier}, + Column: pgsql.ColumnID, + } + + return pgsql.Insert{ + Table: pgsql.TableReference{ + Name: expansionPairFilter.AsCompoundIdentifier(), + }, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{expansionRootID, expansionTerminalID}), + Source: &pgsql.Query{ + Body: pgsql.Select{ + Distinct: true, + Projection: []pgsql.SelectItem{ + rootIDExpression, + terminalIDExpression, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{previousFrameIdentifier}, + }, + }}, + Where: pgd.And( + pgsql.NewBinaryExpression( + rootIDExpression, + pgsql.OperatorIsNot, + pgsql.NullLiteral(), + ), + pgsql.NewBinaryExpression( + terminalIDExpression, + pgsql.OperatorIsNot, + pgsql.NullLiteral(), + ), + ), + }, + }, + }, true +} + +func (s *ExpansionBuilder) materializedEndpointPairFilterStatement() (pgsql.Insert, bool) { + expansionModel := s.traversalStep.Expansion + if !expansionModel.UseMaterializedEndpointPairFilter { + return pgsql.Insert{}, false + } + + rootIDExpression := pgsql.CompoundIdentifier{s.traversalStep.LeftNode.Identifier, pgsql.ColumnID} + terminalIDExpression := pgsql.CompoundIdentifier{s.traversalStep.RightNode.Identifier, pgsql.ColumnID} + pairConstraints := pgsql.OptionalAnd(expansionModel.PrimerNodeConstraints, expansionModel.TerminalNodeConstraints) + pairConstraints = pgsql.OptionalAnd(pairConstraints, pgsql.NewBinaryExpression( + rootIDExpression, + pgsql.OperatorIsNot, + pgsql.NullLiteral(), + )) + pairConstraints = pgsql.OptionalAnd(pairConstraints, pgsql.NewBinaryExpression( + terminalIDExpression, + pgsql.OperatorIsNot, + pgsql.NullLiteral(), + )) + + return pgsql.Insert{ + Table: pgsql.TableReference{ + Name: expansionPairFilter.AsCompoundIdentifier(), + }, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{expansionRootID, expansionTerminalID}), + Source: &pgsql.Query{ + Body: pgsql.Select{ + Distinct: true, + Projection: []pgsql.SelectItem{ + rootIDExpression, + terminalIDExpression, + }, + From: []pgsql.FromClause{{ + Source: expansionNodeTableReference(s.traversalStep.LeftNode.Identifier), + }, { + Source: expansionNodeTableReference(s.traversalStep.RightNode.Identifier), + }}, + Where: pairConstraints, + }, + }, + }, true +} + +func boundTerminalFilterSatisfaction(expansionModel *Expansion) pgsql.Expression { + return pgsql.ExistsExpression{ + Subquery: pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgd.IntLiteral(1), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{expansionTerminalFilter}, + }, + }}, + Where: pgd.Equals( + pgsql.CompoundIdentifier{expansionTerminalFilter, pgsql.ColumnID}, + expansionModel.EdgeEndColumn, + ), + }, + }, + }, + Negated: false, + } +} + +func boundTerminalPairFilterSatisfaction(rootIDExpression pgsql.Expression, terminalIDExpression pgsql.Expression) pgsql.Expression { + return pgsql.ExistsExpression{ + Subquery: pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgd.IntLiteral(1), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{expansionPairFilter}, + }, + }}, + Where: pgd.And( + pgd.Equals( + pgsql.CompoundIdentifier{expansionPairFilter, expansionRootID}, + rootIDExpression, + ), + pgd.Equals( + pgsql.CompoundIdentifier{expansionPairFilter, expansionTerminalID}, + terminalIDExpression, + ), + ), + }, + }, + }, + Negated: false, + } +} + +func boundRootFilterSatisfaction(expansionModel *Expansion) pgsql.Expression { + return pgsql.ExistsExpression{ + Subquery: pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgd.IntLiteral(1), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{expansionRootFilter}, + }, + }}, + Where: pgd.Equals( + pgsql.CompoundIdentifier{expansionRootFilter, pgsql.ColumnID}, + expansionModel.EdgeStartColumn, + ), + }, + }, + }, + Negated: false, + } +} + +func shortestPathVisitedPruningCondition(visitedTable pgsql.Identifier, rootIDExpression pgsql.Expression, nextIDExpression pgsql.Expression) pgsql.Expression { + return pgsql.ExistsExpression{ + Subquery: pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgd.IntLiteral(1), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{visitedTable}, + }, + }}, + Where: pgd.And( + pgd.Equals( + pgsql.CompoundIdentifier{visitedTable, expansionRootID}, + rootIDExpression, + ), + pgd.Equals( + pgsql.CompoundIdentifier{visitedTable, pgsql.ColumnID}, + nextIDExpression, + ), + ), + }, + }, + }, + Negated: true, + } +} + +func forwardContinuationSatisfaction(expansionModel *Expansion) pgsql.Expression { + return pgsql.ExistsExpression{ + Subquery: pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgd.IntLiteral(1), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.TableEdge.AsCompoundIdentifier(), + }, + }}, + Where: pgd.Equals( + expansionModel.EdgeEndIdentifier, + expansionModel.EdgeStartColumn, + ), + }, + }, + }, + Negated: false, + } +} + +func (s *ExpansionBuilder) forwardTerminalSatisfaction(expansionModel *Expansion, rootIDExpression pgsql.Expression) pgsql.SelectItem { + var satisfied pgsql.Expression + + if s.usesBoundEndpointPairs() || expansionModel.UseMaterializedEndpointPairFilter { + satisfied = boundTerminalPairFilterSatisfaction(rootIDExpression, expansionModel.EdgeEndColumn) + } else if s.usesBoundTerminalIDs() || expansionModel.UseMaterializedTerminalFilter { + satisfied = boundTerminalFilterSatisfaction(expansionModel) + } + + if expansionModel.TerminalNodeSatisfactionProjection != nil && + !expansionModel.UseMaterializedTerminalFilter && + !expansionModel.UseMaterializedEndpointPairFilter { + satisfied = pgsql.OptionalAnd(satisfied, expansionModel.TerminalNodeSatisfactionProjection) + } else if satisfied == nil { + satisfied = forwardContinuationSatisfaction(expansionModel) + } + + satisfiedSelectItem, _ := pgsql.As[pgsql.SelectItem](satisfied) + return satisfiedSelectItem +} + +func backwardContinuationSatisfaction(expansionModel *Expansion) pgsql.Expression { + return pgsql.ExistsExpression{ + Subquery: pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgd.IntLiteral(1), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.TableEdge.AsCompoundIdentifier(), + }, + }}, + Where: pgd.Equals( + expansionModel.EdgeStartIdentifier, + expansionModel.EdgeEndColumn, + ), + }, + }, + }, + Negated: false, + } +} + +func (s *ExpansionBuilder) backwardTerminalSatisfaction(expansionModel *Expansion, terminalIDExpression pgsql.Expression) pgsql.SelectItem { + var satisfied pgsql.Expression + + if s.usesBoundEndpointPairs() || expansionModel.UseMaterializedEndpointPairFilter { + satisfied = boundTerminalPairFilterSatisfaction(expansionModel.EdgeStartColumn, terminalIDExpression) + } else if s.usesBoundRootIDs() { + satisfied = boundRootFilterSatisfaction(expansionModel) + } + + if expansionModel.PrimerNodeSatisfactionProjection != nil && !expansionModel.UseMaterializedEndpointPairFilter { + satisfied = pgsql.OptionalAnd(satisfied, expansionModel.PrimerNodeSatisfactionProjection) + } else if satisfied == nil { + satisfied = backwardContinuationSatisfaction(expansionModel) + } + + satisfiedSelectItem, _ := pgsql.As[pgsql.SelectItem](satisfied) + return satisfiedSelectItem +} + +func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expansion) (pgsql.Query, pgsql.Expression) { var ( - primerNodeConstraints = expansionModel.PrimerNodeConstraints - primerNodeJoinCondition = expansionModel.PrimerNodeJoinCondition - nextQuery = pgsql.Select{ + primerSeedConstraints pgsql.Expression + primerProjectionPredicate pgsql.Expression + previousFrameIdentifier pgsql.Identifier + seed *expansionSeed + nextQuery = pgsql.Select{ Where: expansionModel.EdgeConstraints, } ) - if s.traversalStep.LeftNodeBound { - primerNodeConstraints = deframeExpression(primerNodeConstraints) - primerNodeJoinCondition = pgd.Equals(pgd.Column(s.traversalStep.LeftNode.Identifier, pgsql.ColumnID), pgd.StartID(s.traversalStep.Edge.Identifier)) + if s.traversalStep.LeftNodeBound && s.traversalStep.Frame != nil && s.traversalStep.Frame.Previous != nil { + previousFrameIdentifier = s.traversalStep.Frame.Previous.Binding.Identifier } - nextQuery.Where = pgsql.OptionalAnd(primerNodeConstraints, nextQuery.Where) + primerSeedConstraints, primerProjectionPredicate = seedEndpointConstraintSplit( + expansionModel.PrimerNodeConstraints, + s.traversalStep.LeftNode.Identifier, + previousFrameIdentifier, + ) + + if s.usesBoundRootIDs() { + rootIDsSeed := newExpansionRootIDsParameterSeed( + expansionSeedIdentifier(expansionModel.Frame.Binding.Identifier), + s.traversalStep.LeftNode.Identifier, + primerSeedConstraints, + ) + seed = &rootIDsSeed + } else if primerSeedConstraints != nil { + nodeSeed := newExpansionNodeSeed( + expansionSeedIdentifier(expansionModel.Frame.Binding.Identifier), + s.traversalStep.LeftNode.Identifier, + primerSeedConstraints, + ) + seed = &nodeSeed + } + // The returned projection predicate is the part of the endpoint predicate + // that cannot be evaluated in the seed CTE because it still references an + // outer frame. nextQuery.Projection = []pgsql.SelectItem{ s.model.EdgeStartColumn, s.model.EdgeEndColumn, pgd.IntLiteral(1), } - if expansionModel.TerminalNodeSatisfactionProjection != nil { - nextQuery.Projection = append(nextQuery.Projection, expansionModel.TerminalNodeSatisfactionProjection) - } else { - nextQuery.Projection = append(nextQuery.Projection, pgsql.ExistsExpression{ - Subquery: pgsql.Subquery{ - Query: pgsql.Query{ - Body: pgsql.Select{ - Projection: []pgsql.SelectItem{ - pgd.IntLiteral(1), - }, - From: []pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.TableEdge.AsCompoundIdentifier(), - }, - }}, - Where: pgd.Equals( - expansionModel.EdgeEndIdentifier, - expansionModel.EdgeStartColumn, - ), - }, - }, - }, - Negated: false, - }) - } + nextQuery.Projection = append(nextQuery.Projection, s.forwardTerminalSatisfaction(expansionModel, expansionModel.EdgeStartColumn)) nextQuery.Projection = append(nextQuery.Projection, pgd.Equals( @@ -159,32 +975,19 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans ), ) - nextQueryFrom := pgsql.FromClause{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(s.traversalStep.Edge.Identifier), - }, - } + var nextQueryFrom pgsql.FromClause - if primerNodeConstraints != nil { - nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.LeftNode.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: deframeExpression(primerNodeJoinCondition), - }, - }) + if seed != nil { + nextQueryFrom = seed.fromClause(seed.edgeJoin(s.traversalStep.Edge.Identifier, expansionModel.EdgeStartColumn)) + } else { + nextQueryFrom = expansionEdgeFromClause(s.traversalStep.Edge.Identifier) } - if expansionModel.TerminalNodeConstraints != nil { + if expansionModel.TerminalNodeConstraints != nil && + !expansionModel.UseMaterializedTerminalFilter && + !expansionModel.UseMaterializedEndpointPairFilter { nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: s.traversalStep.Expansion.ExpansionNodeJoinCondition, @@ -193,7 +996,7 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - return nextQuery + return frontPrimerQuery(seed, nextQuery), primerProjectionPredicate } func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Expansion) pgsql.Select { @@ -209,41 +1012,22 @@ func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Exp pgd.IntLiteral(1)), } - if expansionModel.TerminalNodeSatisfactionProjection != nil { - nextQuery.Projection = append(nextQuery.Projection, expansionModel.TerminalNodeSatisfactionProjection) - } else { - nextQuery.Projection = append(nextQuery.Projection, pgsql.ExistsExpression{ - Subquery: pgsql.Subquery{ - Query: pgsql.Query{ - Body: pgsql.Select{ - Projection: []pgsql.SelectItem{ - pgd.IntLiteral(1), - }, - From: []pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.TableEdge.AsCompoundIdentifier(), - }, - }}, - Where: pgd.Equals( - expansionModel.EdgeEndIdentifier, - expansionModel.EdgeStartColumn, - ), - }, - }, - }, - Negated: false, - }) - } + nextQuery.Projection = append(nextQuery.Projection, s.forwardTerminalSatisfaction(expansionModel, pgd.Column(expansionModel.Frame.Binding.Identifier, expansionRootID))) - nextQuery.Projection = append(nextQuery.Projection, pgd.Equals( - pgd.EntityID(s.traversalStep.Edge.Identifier), - pgd.Any(pgd.Column(expansionModel.Frame.Binding.Identifier, expansionPath), pgsql.ExpansionPath), - )) + nextQuery.Projection = append(nextQuery.Projection, pgsql.NewLiteral(false, pgsql.Boolean)) - nextQuery.Projection = append(nextQuery.Projection, pgd.Concatenate( + pathProjection := pgd.Concatenate( pgd.Column(expansionModel.Frame.Binding.Identifier, expansionPath), pgd.EntityID(s.traversalStep.Edge.Identifier), - )) + ) + if s.traversalStep.PathReversed && !expansionModel.UseBidirectionalSearch { + pathProjection = pgd.Concatenate( + pgd.EntityID(s.traversalStep.Edge.Identifier), + pgd.Column(expansionModel.Frame.Binding.Identifier, expansionPath), + ) + } + + nextQuery.Projection = append(nextQuery.Projection, pathProjection) nextQueryFrom := pgsql.FromClause{ Source: pgsql.TableReference{ @@ -252,10 +1036,7 @@ func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Exp }, Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(s.traversalStep.Edge.Identifier), - }, + Table: expansionEdgeTableReference(s.traversalStep.Edge.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: pgsql.NewBinaryExpression( @@ -267,12 +1048,11 @@ func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Exp }}, } - if expansionModel.TerminalNodeConstraints != nil { + if expansionModel.TerminalNodeConstraints != nil && + !expansionModel.UseMaterializedTerminalFilter && + !expansionModel.UseMaterializedEndpointPairFilter { nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: s.traversalStep.Expansion.ExpansionNodeJoinCondition, @@ -280,46 +1060,74 @@ func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Exp }) } + nextQuery.Where = pgsql.OptionalAnd(nextQuery.Where, expansionEdgeNotInPath( + s.traversalStep.Edge.Identifier, + expansionModel.Frame.Binding.Identifier, + )) + + if expansionModel.Options.FindShortestPath { + visitedTable := expansionVisited + if expansionModel.UseBidirectionalSearch { + visitedTable = expansionForwardVisited + } + + nextQuery.Where = pgsql.OptionalAnd(nextQuery.Where, shortestPathVisitedPruningCondition( + visitedTable, + pgd.Column(expansionModel.Frame.Binding.Identifier, expansionRootID), + s.model.EdgeEndColumn, + )) + } + nextQuery.From = []pgsql.FromClause{nextQueryFrom} return nextQuery } -func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expansion) pgsql.Select { - nextQuery := pgsql.Select{ - Where: pgsql.OptionalAnd(expansionModel.TerminalNodeConstraints, expansionModel.EdgeConstraints), +func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expansion) (pgsql.Query, pgsql.Expression) { + var ( + terminalSeedConstraints pgsql.Expression + terminalProjectionPredicate pgsql.Expression + previousFrameIdentifier pgsql.Identifier + seed *expansionSeed + nextQuery = pgsql.Select{ + Where: expansionModel.EdgeConstraints, + } + ) + + if s.traversalStep.RightNodeBound && s.traversalStep.Frame != nil && s.traversalStep.Frame.Previous != nil { + previousFrameIdentifier = s.traversalStep.Frame.Previous.Binding.Identifier } + terminalSeedConstraints, terminalProjectionPredicate = seedEndpointConstraintSplit( + expansionModel.TerminalNodeConstraints, + s.traversalStep.RightNode.Identifier, + previousFrameIdentifier, + ) + + if s.usesBoundTerminalIDs() { + terminalIDsSeed := newExpansionTerminalIDsParameterSeed( + expansionSeedIdentifier(expansionModel.Frame.Binding.Identifier), + s.traversalStep.RightNode.Identifier, + terminalSeedConstraints, + ) + seed = &terminalIDsSeed + } else if terminalSeedConstraints != nil { + nodeSeed := newExpansionNodeSeed( + expansionSeedIdentifier(expansionModel.Frame.Binding.Identifier), + s.traversalStep.RightNode.Identifier, + terminalSeedConstraints, + ) + seed = &nodeSeed + } + + // The returned projection predicate is applied after the harness materializes + // endpoints, where any outer-frame references are back in scope. nextQuery.Projection = []pgsql.SelectItem{ s.model.EdgeEndColumn, s.model.EdgeStartColumn, pgd.IntLiteral(1), } - if expansionModel.PrimerNodeSatisfactionProjection != nil { - nextQuery.Projection = append(nextQuery.Projection, expansionModel.PrimerNodeSatisfactionProjection) - } else { - nextQuery.Projection = append(nextQuery.Projection, pgsql.ExistsExpression{ - Subquery: pgsql.Subquery{ - Query: pgsql.Query{ - Body: pgsql.Select{ - Projection: []pgsql.SelectItem{ - pgd.IntLiteral(1), - }, - From: []pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.TableEdge.AsCompoundIdentifier(), - }, - }}, - Where: pgd.Equals( - expansionModel.EdgeStartIdentifier, - expansionModel.EdgeEndColumn, - ), - }, - }, - }, - Negated: false, - }) - } + nextQuery.Projection = append(nextQuery.Projection, s.backwardTerminalSatisfaction(expansionModel, expansionModel.EdgeEndColumn)) nextQuery.Projection = append(nextQuery.Projection, pgd.Equals( @@ -331,41 +1139,26 @@ func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expan ), ) - nextQueryFrom := pgsql.FromClause{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(s.traversalStep.Edge.Identifier), - }, - } + var nextQueryFrom pgsql.FromClause - if expansionModel.PrimerNodeConstraints != nil { - nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.LeftNode.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: s.traversalStep.Expansion.PrimerNodeJoinCondition, - }, - }) + if seed != nil { + nextQueryFrom = seed.fromClause(seed.edgeJoin(s.traversalStep.Edge.Identifier, expansionModel.EdgeEndColumn)) + } else { + nextQueryFrom = expansionEdgeFromClause(s.traversalStep.Edge.Identifier) } - if expansionModel.TerminalNodeConstraints != nil { + if expansionModel.PrimerNodeConstraints != nil && !expansionModel.UseMaterializedEndpointPairFilter { nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.LeftNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, - Constraint: s.traversalStep.Expansion.ExpansionNodeJoinCondition, + Constraint: s.traversalStep.Expansion.PrimerNodeJoinCondition, }, }) } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - return nextQuery + return frontPrimerQuery(seed, nextQuery), terminalProjectionPredicate } func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Expansion) pgsql.Select { @@ -381,36 +1174,9 @@ func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Ex pgd.IntLiteral(1)), } - if expansionModel.PrimerNodeSatisfactionProjection != nil { - nextQuery.Projection = append(nextQuery.Projection, expansionModel.PrimerNodeSatisfactionProjection) - } else { - nextQuery.Projection = append(nextQuery.Projection, pgsql.ExistsExpression{ - Subquery: pgsql.Subquery{ - Query: pgsql.Query{ - Body: pgsql.Select{ - Projection: []pgsql.SelectItem{ - pgd.IntLiteral(1), - }, - From: []pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.TableEdge.AsCompoundIdentifier(), - }, - }}, - Where: pgd.Equals( - expansionModel.EdgeStartIdentifier, - expansionModel.EdgeEndColumn, - ), - }, - }, - }, - Negated: false, - }) - } + nextQuery.Projection = append(nextQuery.Projection, s.backwardTerminalSatisfaction(expansionModel, pgd.Column(expansionModel.Frame.Binding.Identifier, expansionRootID))) - nextQuery.Projection = append(nextQuery.Projection, pgd.Equals( - pgd.EntityID(s.traversalStep.Edge.Identifier), - pgd.Any(pgd.Column(expansionModel.Frame.Binding.Identifier, expansionPath), pgsql.ExpansionPath), - )) + nextQuery.Projection = append(nextQuery.Projection, pgsql.NewLiteral(false, pgsql.Boolean)) nextQuery.Projection = append(nextQuery.Projection, pgd.Concatenate( pgd.EntityID(s.traversalStep.Edge.Identifier), @@ -424,10 +1190,7 @@ func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Ex }, Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(s.traversalStep.Edge.Identifier), - }, + Table: expansionEdgeTableReference(s.traversalStep.Edge.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: pgsql.NewBinaryExpression( @@ -439,12 +1202,9 @@ func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Ex }}, } - if expansionModel.PrimerNodeConstraints != nil { + if expansionModel.PrimerNodeConstraints != nil && !expansionModel.UseMaterializedEndpointPairFilter { nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.LeftNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.LeftNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: s.traversalStep.Expansion.PrimerNodeJoinCondition, @@ -452,6 +1212,19 @@ func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Ex }) } + nextQuery.Where = pgsql.OptionalAnd(nextQuery.Where, expansionEdgeNotInPath( + s.traversalStep.Edge.Identifier, + expansionModel.Frame.Binding.Identifier, + )) + + if expansionModel.Options.FindShortestPath { + nextQuery.Where = pgsql.OptionalAnd(nextQuery.Where, shortestPathVisitedPruningCondition( + expansionBackwardVisited, + pgd.Column(expansionModel.Frame.Binding.Identifier, expansionRootID), + s.model.EdgeStartColumn, + )) + } + nextQuery.From = []pgsql.FromClause{nextQueryFrom} return nextQuery } @@ -482,12 +1255,93 @@ func shortestPathSearchCTE(functionName pgsql.Identifier, expansionModel *Expans } } +func boundEndpointProjectionConstraint(prevFrameID, nodeIdentifier, expansionFrameID, expansionColumn pgsql.Identifier) pgsql.Expression { + return pgsql.NewBinaryExpression( + pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{prevFrameID, nodeIdentifier}, + Column: pgsql.ColumnID, + }, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{expansionFrameID, expansionColumn}, + ) +} + +func (s *ExpansionBuilder) applyBoundEndpointProjectionConstraints(projectionQuery *pgsql.Select, expansionModel *Expansion) { + if s.traversalStep.Frame == nil || s.traversalStep.Frame.Previous == nil { + return + } + + if !s.traversalStep.LeftNodeBound && !s.traversalStep.RightNodeBound { + return + } + + prevFrameID := s.traversalStep.Frame.Previous.Binding.Identifier + + ensureProjectionFrameSource(projectionQuery, prevFrameID) + + if s.traversalStep.LeftNodeBound { + projectionQuery.Where = pgsql.OptionalAnd(projectionQuery.Where, + boundEndpointProjectionConstraint( + prevFrameID, + s.traversalStep.LeftNode.Identifier, + expansionModel.Frame.Binding.Identifier, + expansionRootID, + ), + ) + } + + if s.traversalStep.RightNodeBound { + projectionQuery.Where = pgsql.OptionalAnd(projectionQuery.Where, + boundEndpointProjectionConstraint( + prevFrameID, + s.traversalStep.RightNode.Identifier, + expansionModel.Frame.Binding.Identifier, + expansionNextID, + ), + ) + } +} + +func ensureProjectionFrameSource(projectionQuery *pgsql.Select, frameIdentifier pgsql.Identifier) { + for _, from := range projectionQuery.From { + if tableReference, ok := from.Source.(pgsql.TableReference); ok && len(tableReference.Name) == 1 && tableReference.Name[0] == frameIdentifier { + return + } + } + + projectionQuery.From = append([]pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{frameIdentifier}, + }, + }}, projectionQuery.From...) +} + +func (s *ExpansionBuilder) applyShortestPathSeedProjectionConstraints(projectionQuery *pgsql.Select, projectionConstraints pgsql.Expression) { + if projectionConstraints == nil { + return + } + + if s.traversalStep.Frame != nil && s.traversalStep.Frame.Previous != nil { + prevFrameID := s.traversalStep.Frame.Previous.Binding.Identifier + if referencesIdentifier(projectionConstraints, prevFrameID) { + ensureProjectionFrameSource(projectionQuery, prevFrameID) + } + } + + projectionQuery.Where = pgsql.OptionalAnd(projectionQuery.Where, projectionConstraints) +} + func (s *ExpansionBuilder) buildShortestPathsHarnessCall(harnessFunctionName pgsql.Identifier) (pgsql.Query, error) { var ( - expansionModel = s.traversalStep.Expansion - forwardFrontPrimerQuery = s.prepareForwardFrontPrimerQuery(expansionModel) - forwardFrontRecursiveQuery = s.prepareForwardFrontRecursiveQuery(expansionModel) - projectionQuery pgsql.Select + expansionModel = s.traversalStep.Expansion + projectionQuery pgsql.Select + ) + + expansionModel.UseMaterializedTerminalFilter = s.canMaterializeTerminalFilter(expansionModel) + + var ( + forwardFrontPrimerQuery, forwardSeedProjectionConstraints = s.prepareForwardFrontPrimerQuery(expansionModel) + forwardFrontRecursiveQuery = s.prepareForwardFrontRecursiveQuery(expansionModel) ) projectionQuery.Projection = expansionModel.Projection @@ -499,10 +1353,7 @@ func (s *ExpansionBuilder) buildShortestPathsHarnessCall(harnessFunctionName pgs Binding: models.EmptyOptional[pgsql.Identifier](), }, Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.LeftNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.LeftNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: pgsql.NewBinaryExpression( @@ -512,10 +1363,7 @@ func (s *ExpansionBuilder) buildShortestPathsHarnessCall(harnessFunctionName pgs ), }, }, { - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: pgsql.NewBinaryExpression( @@ -527,28 +1375,8 @@ func (s *ExpansionBuilder) buildShortestPathsHarnessCall(harnessFunctionName pgs }}, }} - // If the traversal's left node was already bound in a prior frame, that frame must appear in the - // projection's FROM clause so that columns like s0.e0 and s0.n0 are in scope. - if s.traversalStep.LeftNodeBound && s.traversalStep.Frame.Previous != nil { - prevFrameID := s.traversalStep.Frame.Previous.Binding.Identifier - - // Prepend as a comma-join so it does not interfere with the explicit JOIN chain. - projectionQuery.From = append([]pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{prevFrameID}, - }, - }}, projectionQuery.From...) - - // (s0.n1).id = s2.root_id - projectionQuery.Where = pgsql.NewBinaryExpression( - pgsql.RowColumnReference{ - Identifier: pgsql.CompoundIdentifier{prevFrameID, s.traversalStep.LeftNode.Identifier}, - Column: pgsql.ColumnID, - }, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionRootID}, - ) - } + s.applyBoundEndpointProjectionConstraints(&projectionQuery, expansionModel) + s.applyShortestPathSeedProjectionConstraints(&projectionQuery, forwardSeedProjectionConstraints) if harnessParameters, err := s.shortestPathsParameters(expansionModel, forwardFrontPrimerQuery, forwardFrontRecursiveQuery); err != nil { return pgsql.Query{}, err @@ -567,18 +1395,53 @@ func (s *ExpansionBuilder) BuildShortestPathsRoot() (pgsql.Query, error) { return s.buildShortestPathsHarnessCall(pgsql.FunctionUnidirectionalSPHarness) } -func (s *ExpansionBuilder) BuildAllShortestPathsRoot() (pgsql.Query, error) { - return s.buildShortestPathsHarnessCall(pgsql.FunctionUnidirectionalASPHarness) +func (s *ExpansionBuilder) BuildAllShortestPathsRoot() (pgsql.Query, error) { + return s.buildShortestPathsHarnessCall(pgsql.FunctionUnidirectionalASPHarness) +} + +func (s *ExpansionBuilder) canMaterializeTerminalFilter(expansionModel *Expansion) bool { + if expansionModel.TerminalNodeConstraints == nil || s.usesBoundEndpointPairs() || s.usesBoundTerminalIDs() { + return false + } + + // Terminal filters are only useful as standalone SQL when they depend solely + // on the terminal node; external references must stay in the main query. + _, externalConstraints := partitionConstraintByLocality( + expansionModel.TerminalNodeConstraints, + pgsql.AsIdentifierSet(s.traversalStep.RightNode.Identifier), + ) + + return externalConstraints == nil +} + +func (s *ExpansionBuilder) canMaterializeEndpointPairFilter(expansionModel *Expansion) bool { + // Pair filters enumerate the exact root/terminal combinations the + // bidirectional harness must resolve. Kind-only endpoint predicates are not + // enough because they do not constrain the search columns used by the harness. + if s.usesBoundEndpointPairs() || + expansionModel.PrimerNodeConstraints == nil || + expansionModel.TerminalNodeConstraints == nil || + !hasLocalEndpointConstraint(expansionModel.PrimerNodeConstraints, s.traversalStep.LeftNode.Identifier) || + !hasLocalEndpointConstraint(expansionModel.TerminalNodeConstraints, s.traversalStep.RightNode.Identifier) { + return false + } + + return true } -func (s *ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot() (pgsql.Query, error) { +func (s *ExpansionBuilder) buildBiDirectionalShortestPathsHarnessCall(harnessFunctionName pgsql.Identifier) (pgsql.Query, error) { + var ( + expansionModel = s.traversalStep.Expansion + projectionQuery pgsql.Select + ) + + expansionModel.UseMaterializedEndpointPairFilter = s.canMaterializeEndpointPairFilter(expansionModel) + var ( - expansionModel = s.traversalStep.Expansion - forwardFrontPrimerQuery = s.prepareForwardFrontPrimerQuery(expansionModel) - forwardFrontRecursiveQuery = s.prepareForwardFrontRecursiveQuery(expansionModel) - backwardFrontPrimerQuery = s.prepareBackwardFrontPrimerQuery(expansionModel) - backwardFrontRecursiveQuery = s.prepareBackwardFrontRecursiveQuery(expansionModel) - projectionQuery pgsql.Select + forwardFrontPrimerQuery, forwardSeedProjectionConstraints = s.prepareForwardFrontPrimerQuery(expansionModel) + forwardFrontRecursiveQuery = s.prepareForwardFrontRecursiveQuery(expansionModel) + backwardFrontPrimerQuery, backwardSeedProjectionConstraints = s.prepareBackwardFrontPrimerQuery(expansionModel) + backwardFrontRecursiveQuery = s.prepareBackwardFrontRecursiveQuery(expansionModel) ) projectionQuery.Projection = expansionModel.Projection @@ -590,10 +1453,7 @@ func (s *ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot() (pgsql.Query Binding: models.EmptyOptional[pgsql.Identifier](), }, Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.LeftNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.LeftNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: pgsql.NewBinaryExpression( @@ -603,10 +1463,7 @@ func (s *ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot() (pgsql.Query ), }, }, { - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(s.traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(s.traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: pgsql.NewBinaryExpression( @@ -618,28 +1475,8 @@ func (s *ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot() (pgsql.Query }}, }} - // If the traversal's left node was already bound in a prior frame, that frame must appear in the - // projection's FROM clause so that columns like s0.e0 and s0.n0 are in scope. - if s.traversalStep.LeftNodeBound && s.traversalStep.Frame.Previous != nil { - prevFrameID := s.traversalStep.Frame.Previous.Binding.Identifier - - // Prepend as a comma-join so it does not interfere with the explicit JOIN chain. - projectionQuery.From = append([]pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{prevFrameID}, - }, - }}, projectionQuery.From...) - - // (s0.n1).id = s2.root_id - projectionQuery.Where = pgsql.NewBinaryExpression( - pgsql.RowColumnReference{ - Identifier: pgsql.CompoundIdentifier{prevFrameID, s.traversalStep.LeftNode.Identifier}, - Column: pgsql.ColumnID, - }, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionRootID}, - ) - } + s.applyBoundEndpointProjectionConstraints(&projectionQuery, expansionModel) + s.applyShortestPathSeedProjectionConstraints(&projectionQuery, pgsql.OptionalAnd(forwardSeedProjectionConstraints, backwardSeedProjectionConstraints)) if harnessParameters, err := s.bidirectionalAllShortestPathsParameters(expansionModel, forwardFrontPrimerQuery, forwardFrontRecursiveQuery, backwardFrontPrimerQuery, backwardFrontRecursiveQuery); err != nil { return pgsql.Query{}, err @@ -649,15 +1486,84 @@ func (s *ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot() (pgsql.Query Body: projectionQuery, } - query.AddCTE(shortestPathSearchCTE(pgsql.FunctionBidirectionalASPHarness, expansionModel, harnessParameters)) + query.AddCTE(shortestPathSearchCTE(harnessFunctionName, expansionModel, harnessParameters)) return query, nil } } -func (s *ExpansionBuilder) shortestPathsParameters(expansionModel *Expansion, forwardFrontPrimerQuery pgsql.Select, forwardFrontRecursiveQuery pgsql.Select) ([]pgsql.Expression, error) { +func (s *ExpansionBuilder) BuildBiDirectionalShortestPathsRoot() (pgsql.Query, error) { + return s.buildBiDirectionalShortestPathsHarnessCall(pgsql.FunctionBidirectionalSPHarness) +} + +func (s *ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot() (pgsql.Query, error) { + return s.buildBiDirectionalShortestPathsHarnessCall(pgsql.FunctionBidirectionalASPHarness) +} + +func (s *ExpansionBuilder) boundEndpointFilterParameters() ([]pgsql.Expression, error) { + var ( + rootFilterStatement, hasRootFilter = s.boundRootIDsFilterStatement() + terminalFilterStatement, hasTerminalFilter = s.boundTerminalIDsFilterStatement() + pairFilterStatement, hasPairFilter = s.boundEndpointPairFilterStatement() + ) + + if !hasPairFilter { + pairFilterStatement, hasPairFilter = s.materializedEndpointPairFilterStatement() + } + + if !hasTerminalFilter { + terminalFilterStatement, hasTerminalFilter = s.unboundTerminalIDsFilterStatement() + } + + if !hasRootFilter && !hasTerminalFilter && !hasPairFilter { + return nil, nil + } + + // Pair filters supersede separate root/terminal filters because they encode + // the allowed combinations, not just independent endpoint sets. + var ( + rootFilter string + terminalFilter string + pairFilter string + ) + + if hasPairFilter { + if formattedFilter, err := format.Statement(pairFilterStatement, format.NewOutputBuilder().WithMaterializedParameters(s.queryParameters)); err != nil { + return nil, err + } else { + pairFilter = formattedFilter + } + } else if hasRootFilter { + if formattedFilter, err := format.Statement(rootFilterStatement, format.NewOutputBuilder().WithMaterializedParameters(s.queryParameters)); err != nil { + return nil, err + } else { + rootFilter = formattedFilter + } + } + + if !hasPairFilter && hasTerminalFilter { + if formattedFilter, err := format.Statement(terminalFilterStatement, format.NewOutputBuilder().WithMaterializedParameters(s.queryParameters)); err != nil { + return nil, err + } else { + terminalFilter = formattedFilter + } + } + + filterParameters := []pgsql.Expression{ + pgsql.NewTypeCast(pgsql.NewLiteral(rootFilter, pgsql.Text), pgsql.Text), + pgsql.NewTypeCast(pgsql.NewLiteral(terminalFilter, pgsql.Text), pgsql.Text), + } + + if hasPairFilter { + filterParameters = append(filterParameters, pgsql.NewTypeCast(pgsql.NewLiteral(pairFilter, pgsql.Text), pgsql.Text)) + } + + return filterParameters, nil +} + +func (s *ExpansionBuilder) shortestPathsParameters(expansionModel *Expansion, forwardFrontPrimerQuery pgsql.SetExpression, forwardFrontRecursiveQuery pgsql.SetExpression) ([]pgsql.Expression, error) { var ( harnessParameters []pgsql.Expression - formatFragment = func(query pgsql.Select) (string, error) { + formatFragment = func(query pgsql.SetExpression) (string, error) { return format.Statement( nextFrontInsert(query), format.NewOutputBuilder().WithMaterializedParameters(s.queryParameters)) @@ -687,13 +1593,21 @@ func (s *ExpansionBuilder) shortestPathsParameters(expansionModel *Expansion, fo }) } - return append(harnessParameters, pgsql.NewLiteral(expansionModel.Options.MaxDepth.GetOr(translateDefaultMaxTraversalDepth), pgsql.Int)), nil + harnessParameters = append(harnessParameters, pgsql.NewLiteral(expansionModel.Options.MaxDepth.GetOr(translateDefaultMaxTraversalDepth), pgsql.Int)) + + if filterParameters, err := s.boundEndpointFilterParameters(); err != nil { + return nil, err + } else { + harnessParameters = append(harnessParameters, filterParameters...) + } + + return harnessParameters, nil } -func (s *ExpansionBuilder) bidirectionalAllShortestPathsParameters(expansionModel *Expansion, forwardFrontPrimerQuery pgsql.Select, forwardFrontRecursiveQuery pgsql.Select, backwardFrontPrimerQuery pgsql.Select, backwardFrontRecursiveQuery pgsql.Select) ([]pgsql.Expression, error) { +func (s *ExpansionBuilder) bidirectionalAllShortestPathsParameters(expansionModel *Expansion, forwardFrontPrimerQuery pgsql.SetExpression, forwardFrontRecursiveQuery pgsql.SetExpression, backwardFrontPrimerQuery pgsql.SetExpression, backwardFrontRecursiveQuery pgsql.SetExpression) ([]pgsql.Expression, error) { var ( harnessParameters []pgsql.Expression - formatFragment = func(query pgsql.Select) (string, error) { + formatFragment = func(query pgsql.SetExpression) (string, error) { return format.Statement( nextFrontInsert(query), format.NewOutputBuilder().WithMaterializedParameters(s.queryParameters)) @@ -743,10 +1657,18 @@ func (s *ExpansionBuilder) bidirectionalAllShortestPathsParameters(expansionMode }) } - return append(harnessParameters, pgsql.NewLiteral(expansionModel.Options.MaxDepth.GetOr(translateDefaultMaxTraversalDepth), pgsql.Int)), nil + harnessParameters = append(harnessParameters, pgsql.NewLiteral(expansionModel.Options.MaxDepth.GetOr(translateDefaultMaxTraversalDepth), pgsql.Int)) + + if filterParameters, err := s.boundEndpointFilterParameters(); err != nil { + return nil, err + } else { + harnessParameters = append(harnessParameters, filterParameters...) + } + + return harnessParameters, nil } -func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier) pgsql.Query { +func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier, commonTableExpressions ...pgsql.CommonTableExpression) pgsql.Query { query := pgsql.Query{ CommonTableExpressions: &pgsql.With{ Recursive: true, @@ -754,6 +1676,10 @@ func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier) pgsql.Que Body: s.ProjectionStatement, } + for _, commonTableExpression := range commonTableExpressions { + query.AddCTE(commonTableExpression) + } + query.AddCTE(pgsql.CommonTableExpression{ Alias: pgsql.TableAlias{ Name: expansionIdentifier, @@ -764,6 +1690,7 @@ func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier) pgsql.Que LOperand: s.PrimerStatement, ROperand: s.RecursiveStatement, Operator: pgsql.OperatorUnion, + All: s.UseUnionAll, }, }, }) @@ -775,6 +1702,7 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte var ( traversalStep = traversalStepContext.CurrentStep expansionModel = traversalStep.Expansion + seedIdentifier = expansionSeedIdentifier(expansionModel.Frame.Binding.Identifier) ) // Determine local scope of the primer: the edge and both nodes. @@ -787,24 +1715,41 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte ), ) - // External terms reference a prior CTE (e.g. s0.i0). Cross-join it into the - // primer so it is in scope for the base case WHERE clause. - if primerExternal != nil && traversalStep.Frame.Previous != nil { - expansion.PrimerStatement.From = append([]pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{traversalStep.Frame.Previous.Binding.Identifier}, - }, - }}, expansion.PrimerStatement.From...) + seedConstraints := pgsql.OptionalAnd(primerLocal, primerExternal) + var seed *expansionSeed + + if traversalStep.LeftNodeBound { + boundSeed := newExpansionBoundNodeSeed(seedIdentifier, traversalStep.Frame.Previous, traversalStep.LeftNode.Identifier, seedConstraints) + seed = &boundSeed + expansion.UseUnionAll = true + } else if seedConstraints != nil { + nodeSeed := newExpansionNodeSeed(seedIdentifier, traversalStep.LeftNode.Identifier, seedConstraints) + seed = &nodeSeed + expansion.UseUnionAll = primerExternal == nil + + // External terms reference a prior CTE (e.g. s0.i0). Cross-join it into the + // seed so it is in scope before the traversal primer joins edges. + if primerExternal != nil && traversalStep.Frame.Previous != nil { + nodeSeed.query.From = append([]pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{traversalStep.Frame.Previous.Binding.Identifier}, + }, + }}, nodeSeed.query.From...) + seed = &nodeSeed + } + } else { + expansion.UseUnionAll = true } - expansion.PrimerStatement.Where = pgsql.OptionalAnd( - pgsql.OptionalAnd(primerLocal, primerExternal), - expansionModel.EdgeConstraints, - ) + expansion.PrimerStatement.Where = expansionModel.EdgeConstraints expansion.ProjectionStatement.Projection = expansionModel.Projection - expansion.RecursiveStatement.Where = pgsql.OptionalAnd(expansionModel.EdgeConstraints, expansionModel.RecursiveConstraints) - expansion.PrimerStatement.Projection = s.buildExpansionPrimerProjection(traversalStep) + expansion.RecursiveStatement.Where = expansionModel.RecursiveConstraints + if projection, err := s.buildExpansionPrimerProjection(traversalStep); err != nil { + return pgsql.Query{}, err + } else { + expansion.PrimerStatement.Projection = projection + } if projection, err := s.buildExpansionRecursiveProjection(traversalStep); err != nil { return pgsql.Query{}, err @@ -812,65 +1757,18 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte expansion.RecursiveStatement.Projection = projection } - // Craft the from clause - nextQueryFrom := pgsql.FromClause{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(traversalStep.Edge.Identifier), - }, - } + var nextQueryFrom pgsql.FromClause - // If the left node was already bound at time of translation connect this expansion to the - // previously materialized node - if traversalStep.LeftNodeBound { - nextQueryFrom = pgsql.FromClause{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{traversalStep.Frame.Previous.Binding.Identifier}, - }, - Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(traversalStep.Edge.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, - pgsql.OperatorEquals, - rewriteCompositeTypeFieldReference( - traversalStep.Frame.Previous.Binding.Identifier, - pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}, - )), - }, - }}, - } - } else if expansionModel.PrimerNodeConstraints != nil { - // Primer node constraints require a join of of the left node - nextQueryFrom = pgsql.FromClause{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(traversalStep.Edge.Identifier), - }, - Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.LeftNode.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: traversalStep.Expansion.PrimerNodeJoinCondition, - }, - }}, - } + if seed != nil { + nextQueryFrom = seed.fromClause(seed.edgeJoin(traversalStep.Edge.Identifier, expansionModel.EdgeStartColumn)) + } else { + nextQueryFrom = expansionEdgeFromClause(traversalStep.Edge.Identifier) } // If there are terminal node constraints then the right node must be joined if expansionModel.TerminalNodeSatisfactionProjection != nil { nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: traversalStep.Expansion.ExpansionNodeJoinCondition, @@ -883,27 +1781,11 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte // Build recursive step joins. The terminal node join is only added when the // expansion carries terminal-node constraints, which are the only cases where // node columns appear in the recursive body. - recursiveJoins := []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(traversalStep.Edge.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.NewBinaryExpression( - expansionModel.EdgeStartColumn, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionNextID}, - ), - }, - }} + recursiveJoins := []pgsql.Join{recursiveExpansionEdgeLookupJoin(traversalStep)} if expansionModel.TerminalNodeConstraints != nil { recursiveJoins = append(recursiveJoins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: expansionModel.ExpansionNodeJoinCondition, @@ -918,11 +1800,14 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte Joins: recursiveJoins, }) + var previousProjectionFrameID pgsql.Identifier + // The current query part may not have a frame associated with it if is a single part query component if traversalStep.Frame.Previous != nil && (s.query.CurrentPart().Frame == nil || traversalStep.Frame.Previous.Binding.Identifier != s.query.CurrentPart().Frame.Binding.Identifier) { + previousProjectionFrameID = traversalStep.Frame.Previous.Binding.Identifier expansion.ProjectionStatement.From = append(expansion.ProjectionStatement.From, pgsql.FromClause{ Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{traversalStep.Frame.Previous.Binding.Identifier}, + Name: pgsql.CompoundIdentifier{previousProjectionFrameID}, Binding: models.EmptyOptional[pgsql.Identifier](), }, }) @@ -934,41 +1819,40 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte Name: pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier}, Binding: models.EmptyOptional[pgsql.Identifier](), }, - Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.LeftNode.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionRootID}, - ), - }, - }, { - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.RightNode.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionNextID}, - ), - }, - }}, + Joins: []pgsql.Join{ + expansionNodeLookupJoin( + traversalStep.LeftNode.Identifier, + pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionRootID}, + ), + expansionNodeLookupJoin( + traversalStep.RightNode.Identifier, + pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionNextID}, + ), + }, }) if projectionConstraints, err := s.buildExpansionProjectionConstraints(traversalStepContext); err != nil { return pgsql.Query{}, err } else { + if previousProjectionFrameID != "" && traversalStep.LeftNodeBound { + projectionConstraints = pgsql.OptionalAnd( + projectionConstraints, + boundEndpointProjectionConstraint( + previousProjectionFrameID, + traversalStep.LeftNode.Identifier, + expansionModel.Frame.Binding.Identifier, + expansionRootID, + ), + ) + } + expansion.ProjectionStatement.Where = projectionConstraints } + if seed != nil { + return expansion.Build(expansionModel.Frame.Binding.Identifier, seed.CTE()), nil + } + return expansion.Build(expansionModel.Frame.Binding.Identifier), nil } @@ -976,12 +1860,23 @@ func (s *Translator) buildExpansionPatternStep(traversalStepContext TraversalSte var ( traversalStep = traversalStepContext.CurrentStep expansionModel = traversalStep.Expansion + seed = newExpansionBoundNodeSeed( + expansionSeedIdentifier(expansionModel.Frame.Binding.Identifier), + traversalStep.Frame.Previous, + traversalStep.LeftNode.Identifier, + expansionModel.PrimerNodeConstraints, + ) ) expansion.ProjectionStatement.Projection = expansionModel.Projection - expansion.PrimerStatement.Where = pgsql.OptionalAnd(expansionModel.PrimerNodeConstraints, expansionModel.EdgeConstraints) - expansion.RecursiveStatement.Where = pgsql.OptionalAnd(expansionModel.EdgeConstraints, expansionModel.RecursiveConstraints) - expansion.PrimerStatement.Projection = s.buildExpansionPrimerProjection(traversalStep) + expansion.UseUnionAll = true + expansion.PrimerStatement.Where = expansionModel.EdgeConstraints + expansion.RecursiveStatement.Where = expansionModel.RecursiveConstraints + if projection, err := s.buildExpansionPrimerProjection(traversalStep); err != nil { + return pgsql.Query{}, err + } else { + expansion.PrimerStatement.Projection = projection + } if projection, err := s.buildExpansionRecursiveProjection(traversalStep); err != nil { return pgsql.Query{}, err @@ -989,56 +1884,31 @@ func (s *Translator) buildExpansionPatternStep(traversalStepContext TraversalSte expansion.RecursiveStatement.Projection = projection } - expansion.PrimerStatement.From = append(expansion.PrimerStatement.From, pgsql.FromClause{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{traversalStep.Frame.Previous.Binding.Identifier}, - }, - Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(traversalStep.Edge.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: expansionModel.EdgeJoinCondition, - }, - }, { - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.RightNode.Identifier), - }, + primerJoins := []pgsql.Join{ + seed.edgeJoin(traversalStep.Edge.Identifier, expansionModel.EdgeStartColumn), + } + + if expansionModel.TerminalNodeSatisfactionProjection != nil { + primerJoins = append(primerJoins, pgsql.Join{ + Table: expansionNodeTableReference(traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: expansionModel.ExpansionNodeJoinCondition, }, - }}, - }) + }) + } + + expansion.PrimerStatement.From = append(expansion.PrimerStatement.From, seed.fromClause(primerJoins...)) // Build recursive step joins. The terminal node join is only added when the // expansion carries terminal-node constraints, which are the only cases where // node columns appear in the recursive body. - recursiveJoins := []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(traversalStep.Edge.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.NewBinaryExpression( - expansionModel.EdgeStartColumn, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionNextID}, - ), - }, - }} + recursiveJoins := []pgsql.Join{recursiveExpansionEdgeLookupJoin(traversalStep)} // If there are terminal node constraints then the right node must be joined if expansionModel.TerminalNodeSatisfactionProjection != nil { recursiveJoins = append(recursiveJoins, pgsql.Join{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.RightNode.Identifier), - }, + Table: expansionNodeTableReference(traversalStep.RightNode.Identifier), JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, Constraint: expansionModel.ExpansionNodeJoinCondition, @@ -1066,33 +1936,16 @@ func (s *Translator) buildExpansionPatternStep(traversalStepContext TraversalSte Name: pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier}, Binding: models.EmptyOptional[pgsql.Identifier](), }, - Joins: []pgsql.Join{{ - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.LeftNode.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionRootID}, - ), - }, - }, { - Table: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(traversalStep.RightNode.Identifier), - }, - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionNextID}, - ), - }, - }}, + Joins: []pgsql.Join{ + expansionNodeLookupJoin( + traversalStep.LeftNode.Identifier, + pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionRootID}, + ), + expansionNodeLookupJoin( + traversalStep.RightNode.Identifier, + pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionNextID}, + ), + }, }) if projectionConstraints, err := s.buildExpansionProjectionConstraints(traversalStepContext); err != nil { @@ -1101,18 +1954,44 @@ func (s *Translator) buildExpansionPatternStep(traversalStepContext TraversalSte expansion.ProjectionStatement.Where = projectionConstraints } - return expansion.Build(expansionModel.Frame.Binding.Identifier), nil + return expansion.Build(expansionModel.Frame.Binding.Identifier, seed.CTE()), nil +} + +func expansionTerminalSatisfactionLocality(traversalStep *TraversalStep) (pgsql.Expression, pgsql.Expression) { + return partitionConstraintByLocality( + pgsql.Expression(traversalStep.Expansion.TerminalNodeSatisfactionProjection), + pgsql.AsIdentifierSet( + traversalStep.LeftNode.Identifier, + traversalStep.Edge.Identifier, + traversalStep.RightNode.Identifier, + ), + ) +} + +func expansionLocalTerminalSatisfactionProjection(traversalStep *TraversalStep) (pgsql.SelectItem, error) { + localSatisfiedConstraint, _ := expansionTerminalSatisfactionLocality(traversalStep) + + if localSatisfiedConstraint == nil { + return pgsql.NewLiteral(true, pgsql.Boolean), nil + } + + return pgsql.As[pgsql.SelectItem](localSatisfiedConstraint) } -func (s *Translator) buildExpansionPrimerProjection(traversalStep *TraversalStep) []pgsql.SelectItem { +func (s *Translator) buildExpansionPrimerProjection(traversalStep *TraversalStep) ([]pgsql.SelectItem, error) { expansionModel := traversalStep.Expansion if expansionModel.TerminalNodeSatisfactionProjection != nil { + satisfiedProjection, err := expansionLocalTerminalSatisfactionProjection(traversalStep) + if err != nil { + return nil, err + } + return []pgsql.SelectItem{ expansionModel.EdgeStartColumn, expansionModel.EdgeEndColumn, pgsql.NewLiteral(1, pgsql.Int), - expansionModel.TerminalNodeSatisfactionProjection, + satisfiedProjection, pgsql.NewBinaryExpression( expansionModel.EdgeStartColumn, pgsql.OperatorEquals, @@ -1123,7 +2002,7 @@ func (s *Translator) buildExpansionPrimerProjection(traversalStep *TraversalStep pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID}, }, }, - } + }, nil } else { return []pgsql.SelectItem{ expansionModel.EdgeStartColumn, @@ -1140,8 +2019,22 @@ func (s *Translator) buildExpansionPrimerProjection(traversalStep *TraversalStep pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID}, }, }, - } + }, nil + } +} + +func expansionRecursivePathExpression(traversalStep *TraversalStep) *pgsql.BinaryExpression { + var ( + expansionModel = traversalStep.Expansion + path = pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionPath} + edgeID = pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID} + ) + + if traversalStep.PathReversed { + return pgsql.NewBinaryExpression(edgeID, pgsql.OperatorConcatenate, path) } + + return pgsql.NewBinaryExpression(path, pgsql.OperatorConcatenate, edgeID) } func (s *Translator) buildExpansionRecursiveProjection(traversalStep *TraversalStep) ([]pgsql.SelectItem, error) { @@ -1150,18 +2043,15 @@ func (s *Translator) buildExpansionRecursiveProjection(traversalStep *TraversalS if expansionModel.TerminalNodeSatisfactionProjection != nil { // Split up constraints that can not be satisfied by the local scope of the expansion. This is done to ensure // that cross-entity references and other extra-scope comparisons are added external to the expansion frame. - localSatisfiedConstraint, externalSatisfiedConstraint := partitionConstraintByLocality( - pgsql.Expression(expansionModel.TerminalNodeSatisfactionProjection), - pgsql.AsIdentifierSet( - traversalStep.LeftNode.Identifier, - traversalStep.Edge.Identifier, - traversalStep.RightNode.Identifier, - ), - ) + localSatisfiedConstraint, externalSatisfiedConstraint := expansionTerminalSatisfactionLocality(traversalStep) // Store the external constraints to be inserted during the final projection and where clause expansionModel.DeferredNodeSatisfactionConstraint = externalSatisfiedConstraint + if localSatisfiedConstraint == nil { + localSatisfiedConstraint = pgsql.NewLiteral(true, pgsql.Boolean) + } + if satisfiedSelectItem, err := pgsql.As[pgsql.SelectItem](localSatisfiedConstraint); err != nil { return nil, err } else { @@ -1174,16 +2064,8 @@ func (s *Translator) buildExpansionRecursiveProjection(traversalStep *TraversalS pgsql.NewLiteral(1, pgsql.Int), ), satisfiedSelectItem, - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.NewAnyExpression(pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionPath}, pgsql.ExpansionPath), - ), - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionPath}, - pgsql.OperatorConcatenate, - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID}, - ), + pgsql.NewLiteral(false, pgsql.Boolean), + expansionRecursivePathExpression(traversalStep), }, nil } } else { @@ -1196,16 +2078,8 @@ func (s *Translator) buildExpansionRecursiveProjection(traversalStep *TraversalS pgsql.NewLiteral(1, pgsql.Int), ), pgsql.NewLiteral(false, pgsql.Boolean), - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.NewAnyExpression(pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionPath}, pgsql.ExpansionPath), - ), - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{expansionModel.Frame.Binding.Identifier, expansionPath}, - pgsql.OperatorConcatenate, - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID}, - ), + pgsql.NewLiteral(false, pgsql.Boolean), + expansionRecursivePathExpression(traversalStep), }, nil } } @@ -1338,7 +2212,7 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(isFirstTraversal } if expansionModel.Options.FindShortestPath || expansionModel.Options.FindAllShortestPaths { - if err := s.translateShortestPathTraversal(expansionModel); err != nil { + if err := s.translateShortestPathTraversal(traversalStep, expansionModel); err != nil { return err } } @@ -1419,7 +2293,20 @@ func (s *Translator) translateExpansionConstraints(isFirstTraversalStep bool, st return nil } -func (s *Translator) translateShortestPathTraversal(expansionModel *Expansion) error { +func (s *Translator) translateShortestPathTraversal(traversalStep *TraversalStep, expansionModel *Expansion) error { + var ( + useBidirectionalSearch bool + err error + ) + + useBidirectionalSearch, err = traversalStep.CanExecutePairAwareBidirectionalSearch(s.scope) + + if err != nil { + return err + } + + expansionModel.UseBidirectionalSearch = useBidirectionalSearch + // If this query is a shortest-path look up, the translator will have to use a function harness for // traversal. As such, query fragments for the traversal harness will have to be passed by the parameters // defined below. @@ -1437,7 +2324,7 @@ func (s *Translator) translateShortestPathTraversal(expansionModel *Expansion) e // Bidirectional BFS searches require an additional set of query fragments to represent the backward traversal // front of the search. - if expansionModel.CanExecuteBidirectionalSearch() { + if expansionModel.UseBidirectionalSearch { if reversePrimerQueryParameter, err := s.scope.DefineNew(pgsql.ParameterIdentifier); err != nil { return err } else { diff --git a/cypher/models/pgsql/translate/expansion_test.go b/cypher/models/pgsql/translate/expansion_test.go new file mode 100644 index 00000000..603468bc --- /dev/null +++ b/cypher/models/pgsql/translate/expansion_test.go @@ -0,0 +1,140 @@ +package translate + +import ( + "testing" + + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/format" + "github.com/specterops/dawgs/cypher/models/pgsql/pgd" + "github.com/stretchr/testify/require" +) + +const ( + shortestPathSeedTestPreviousFrame pgsql.Identifier = "s0" + shortestPathSeedTestFrame pgsql.Identifier = "s1" + shortestPathSeedTestRoot pgsql.Identifier = "n0" + shortestPathSeedTestTerminal pgsql.Identifier = "n1" + shortestPathSeedTestOther pgsql.Identifier = "x" + shortestPathSeedTestEdge pgsql.Identifier = "e0" +) + +func shortestPathSeedTestBoundColumn(nodeIdentifier pgsql.Identifier, column pgsql.Identifier) pgsql.RowColumnReference { + return pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{shortestPathSeedTestPreviousFrame, nodeIdentifier}, + Column: column, + } +} + +func shortestPathSeedTestLocalFunctionPredicate(nodeIdentifier pgsql.Identifier, value string) pgsql.Expression { + return pgsql.NewBinaryExpression( + pgsql.FunctionCall{ + Function: pgsql.FunctionToLower, + Parameters: []pgsql.Expression{ + shortestPathSeedTestBoundColumn(nodeIdentifier, pgsql.ColumnID), + }, + CastType: pgsql.Text, + }, + pgsql.OperatorEquals, + pgsql.NewLiteral(value, pgsql.Text), + ) +} + +func shortestPathSeedTestExternalPredicate(nodeIdentifier pgsql.Identifier) pgsql.Expression { + return pgsql.NewBinaryExpression( + shortestPathSeedTestBoundColumn(nodeIdentifier, pgsql.ColumnID), + pgsql.OperatorEquals, + shortestPathSeedTestBoundColumn(shortestPathSeedTestOther, pgsql.ColumnID), + ) +} + +func newShortestPathSeedTestBuilder(leftBound, rightBound bool) (*ExpansionBuilder, *Expansion) { + previousFrame := &Frame{ + Binding: &BoundIdentifier{Identifier: shortestPathSeedTestPreviousFrame}, + } + expansionFrame := &Frame{ + Previous: previousFrame, + Binding: &BoundIdentifier{Identifier: shortestPathSeedTestFrame}, + } + expansionModel := &Expansion{ + Frame: expansionFrame, + PrimerQueryParameter: &BoundIdentifier{Identifier: "pi0"}, + RecursiveQueryParameter: &BoundIdentifier{Identifier: "pi1"}, + EdgeStartIdentifier: pgsql.ColumnStartID, + EdgeStartColumn: pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnStartID}, + EdgeEndIdentifier: pgsql.ColumnEndID, + EdgeEndColumn: pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnEndID}, + PrimerNodeJoinCondition: pgd.Equals(pgd.EntityID(shortestPathSeedTestRoot), pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnStartID}), + ExpansionNodeJoinCondition: pgd.Equals( + pgd.EntityID(shortestPathSeedTestTerminal), + pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnEndID}, + ), + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{shortestPathSeedTestRoot, pgsql.ColumnID}, + pgsql.CompoundIdentifier{shortestPathSeedTestTerminal, pgsql.ColumnID}, + }, + } + + traversalStep := &TraversalStep{ + Frame: expansionFrame, + Expansion: expansionModel, + LeftNode: &BoundIdentifier{Identifier: shortestPathSeedTestRoot}, + LeftNodeBound: leftBound, + Edge: &BoundIdentifier{Identifier: shortestPathSeedTestEdge}, + RightNode: &BoundIdentifier{Identifier: shortestPathSeedTestTerminal}, + RightNodeBound: rightBound, + } + + return &ExpansionBuilder{ + queryParameters: map[string]any{}, + traversalStep: traversalStep, + model: expansionModel, + }, expansionModel +} + +func TestBoundRootShortestPathPrimerKeepsOnlySeedLocalConstraints(t *testing.T) { + builder, expansionModel := newShortestPathSeedTestBuilder(true, false) + expansionModel.PrimerNodeConstraints = pgsql.NewBinaryExpression( + shortestPathSeedTestLocalFunctionPredicate(shortestPathSeedTestRoot, "1"), + pgsql.OperatorAnd, + shortestPathSeedTestExternalPredicate(shortestPathSeedTestRoot), + ) + + query, err := builder.buildShortestPathsHarnessCall(pgsql.FunctionUnidirectionalSPHarness) + require.NoError(t, err) + + primerQuery, hasPrimerQuery := builder.queryParameters[expansionModel.PrimerQueryParameter.Identifier.String()].(string) + require.True(t, hasPrimerQuery) + require.Contains(t, primerQuery, "lower(n0.id)::text = '1'") + require.NotContains(t, primerQuery, "s0") + require.NotContains(t, primerQuery, "(s0.x).id") + + formattedQuery, err := format.Statement(query, format.NewOutputBuilder()) + require.NoError(t, err) + require.Contains(t, formattedQuery, "n0.id = (s0.x).id") + require.Contains(t, formattedQuery, "(s0.n0).id = s1.root_id") +} + +func TestBoundTerminalShortestPathPrimerKeepsOnlySeedLocalConstraints(t *testing.T) { + builder, expansionModel := newShortestPathSeedTestBuilder(false, true) + expansionModel.BackwardPrimerQueryParameter = &BoundIdentifier{Identifier: "pi2"} + expansionModel.BackwardRecursiveQueryParameter = &BoundIdentifier{Identifier: "pi3"} + expansionModel.TerminalNodeConstraints = pgsql.NewBinaryExpression( + shortestPathSeedTestLocalFunctionPredicate(shortestPathSeedTestTerminal, "2"), + pgsql.OperatorAnd, + shortestPathSeedTestExternalPredicate(shortestPathSeedTestTerminal), + ) + + query, err := builder.buildBiDirectionalShortestPathsHarnessCall(pgsql.FunctionBidirectionalSPHarness) + require.NoError(t, err) + + backwardPrimerQuery, hasBackwardPrimerQuery := builder.queryParameters[expansionModel.BackwardPrimerQueryParameter.Identifier.String()].(string) + require.True(t, hasBackwardPrimerQuery) + require.Contains(t, backwardPrimerQuery, "lower(n1.id)::text = '2'") + require.NotContains(t, backwardPrimerQuery, "s0") + require.NotContains(t, backwardPrimerQuery, "(s0.x).id") + + formattedQuery, err := format.Statement(query, format.NewOutputBuilder()) + require.NoError(t, err) + require.Contains(t, formattedQuery, "n1.id = (s0.x).id") + require.Contains(t, formattedQuery, "(s0.n1).id = s1.next_id") +} diff --git a/cypher/models/pgsql/translate/format.go b/cypher/models/pgsql/translate/format.go index 01824ad8..1bd0e696 100644 --- a/cypher/models/pgsql/translate/format.go +++ b/cypher/models/pgsql/translate/format.go @@ -14,7 +14,7 @@ func Translated(translation Result) (string, error) { return format.Statement(translation.Statement, format.NewOutputBuilder()) } -func FromCypher(ctx context.Context, regularQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, stripLiterals bool) (format.Formatted, error) { +func FromCypher(ctx context.Context, regularQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, stripLiterals bool, graphID int32) (format.Formatted, error) { var ( output = &bytes.Buffer{} emitter = cypherFormat.NewCypherEmitter(stripLiterals) @@ -28,7 +28,7 @@ func FromCypher(ctx context.Context, regularQuery *cypher.RegularQuery, kindMapp output.WriteString("\n") - if translation, err := Translate(ctx, regularQuery, kindMapper, nil); err != nil { + if translation, err := Translate(ctx, regularQuery, kindMapper, nil, graphID); err != nil { return format.Formatted{}, err } else if sqlQuery, err := format.Statement(translation.Statement, format.NewOutputBuilder()); err != nil { return format.Formatted{}, err diff --git a/cypher/models/pgsql/translate/hinting.go b/cypher/models/pgsql/translate/hinting.go index 74f8bb77..821cd68a 100644 --- a/cypher/models/pgsql/translate/hinting.go +++ b/cypher/models/pgsql/translate/hinting.go @@ -169,6 +169,10 @@ func (s *contextAwareKindMapper) MapKinds(kinds graph.Kinds) ([]int16, error) { return s.kindMapper.MapKinds(s.ctx, kinds) } +func (s *contextAwareKindMapper) AssertKinds(kinds graph.Kinds) ([]int16, error) { + return s.kindMapper.AssertKinds(s.ctx, kinds) +} + func applyTypeFunctionLikeTypeHints(kindMapper *contextAwareKindMapper, expression *pgsql.BinaryExpression) error { switch typedLOperand := expression.LOperand.(type) { case pgsql.CompoundIdentifier: diff --git a/cypher/models/pgsql/translate/limit_pushdown_test.go b/cypher/models/pgsql/translate/limit_pushdown_test.go new file mode 100644 index 00000000..7334b2fb --- /dev/null +++ b/cypher/models/pgsql/translate/limit_pushdown_test.go @@ -0,0 +1,296 @@ +package translate + +import ( + "testing" + + "github.com/specterops/dawgs/cypher/models" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/stretchr/testify/require" +) + +const ( + limitPushdownTestSourceFrame pgsql.Identifier = "s0" + limitPushdownTestHarnessFrame pgsql.Identifier = "s1" + limitPushdownTestPreviousFrame pgsql.Identifier = "s2" + limitPushdownTestRootAlias pgsql.Identifier = "n0" + limitPushdownTestTerminalAlias pgsql.Identifier = "n1" +) + +func limitPushdownTestEndpointRef(alias pgsql.Identifier) pgsql.RowColumnReference { + return pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{limitPushdownTestSourceFrame, alias}, + Column: pgsql.ColumnID, + } +} + +func limitPushdownTestEndpointInequality(leftAlias, rightAlias pgsql.Identifier) pgsql.Expression { + return pgsql.NewBinaryExpression( + limitPushdownTestEndpointRef(leftAlias), + pgsql.OperatorCypherNotEquals, + limitPushdownTestEndpointRef(rightAlias), + ) +} + +func limitPushdownTestBoundEndpointConstraint(endpointAlias, expansionColumn pgsql.Identifier) pgsql.Expression { + return pgsql.NewBinaryExpression( + pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{limitPushdownTestPreviousFrame, endpointAlias}, + Column: pgsql.ColumnID, + }, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{limitPushdownTestHarnessFrame, expansionColumn}, + ) +} + +func limitPushdownTestSourceWhere(t *testing.T, part *QueryPart, where pgsql.Expression) { + t.Helper() + + sourceCTE := findCTE(part.Model, limitPushdownTestSourceFrame) + require.NotNil(t, sourceCTE) + + selectBody, isSelect := sourceCTE.Query.Body.(pgsql.Select) + require.True(t, isSelect) + + selectBody.Where = where + sourceCTE.Query.Body = selectBody +} + +func limitPushdownTestJoin(nodeAlias, expansionColumn pgsql.Identifier) pgsql.Join { + return pgsql.Join{ + Table: pgsql.TableReference{ + Name: pgsql.TableNode.AsCompoundIdentifier(), + Binding: models.OptionalValue(nodeAlias), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{nodeAlias, pgsql.ColumnID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{limitPushdownTestHarnessFrame, expansionColumn}, + ), + }, + } +} + +func limitPushdownTestPart(harnessFunction pgsql.Identifier) *QueryPart { + part := NewQueryPart(1, 0) + part.Limit = pgsql.NewLiteral(10, pgsql.Int) + part.Model.AddCTE(pgsql.CommonTableExpression{ + Alias: pgsql.TableAlias{Name: limitPushdownTestSourceFrame}, + Query: pgsql.Query{ + CommonTableExpressions: &pgsql.With{Expressions: []pgsql.CommonTableExpression{{ + Alias: pgsql.TableAlias{Name: limitPushdownTestHarnessFrame}, + Query: pgsql.Query{ + Body: pgsql.Select{From: []pgsql.FromClause{{ + Source: pgsql.FunctionCall{Function: harnessFunction}, + }}}, + }, + }}}, + Body: pgsql.Select{From: []pgsql.FromClause{{ + Source: pgsql.TableReference{Name: pgsql.CompoundIdentifier{limitPushdownTestHarnessFrame}}, + Joins: []pgsql.Join{ + limitPushdownTestJoin(limitPushdownTestRootAlias, expansionRootID), + limitPushdownTestJoin(limitPushdownTestTerminalAlias, expansionNextID), + }, + }}}, + }, + }) + + return part +} + +func limitPushdownTestTail(where pgsql.Expression) pgsql.Select { + return pgsql.Select{ + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{Name: pgsql.CompoundIdentifier{limitPushdownTestSourceFrame}}, + }}, + Where: where, + } +} + +func TestLimitPushdownTailSourceAllowsUnidirectionalShortestPathEndpointInequality(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + + sourceFrame, canPushDown := limitPushdownTailSource(part, tailSelect) + require.True(t, canPushDown) + require.Equal(t, limitPushdownTestSourceFrame, sourceFrame) +} + +func TestLimitPushdownTailSourceAllowsReversedUnidirectionalShortestPathEndpointInequality(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestTerminalAlias, + limitPushdownTestRootAlias, + )) + + sourceFrame, canPushDown := limitPushdownTailSource(part, tailSelect) + require.True(t, canPushDown) + require.Equal(t, limitPushdownTestSourceFrame, sourceFrame) +} + +func TestLimitPushdownTailSourceBlocksMixedShortestPathWherePredicate(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect := limitPushdownTestTail(pgsql.NewBinaryExpression( + limitPushdownTestEndpointInequality(limitPushdownTestRootAlias, limitPushdownTestTerminalAlias), + pgsql.OperatorAnd, + pgsql.NewBinaryExpression( + limitPushdownTestEndpointRef(limitPushdownTestTerminalAlias), + pgsql.OperatorGreaterThan, + pgsql.NewLiteral(0, pgsql.Int), + ), + )) + + _, canPushDown := limitPushdownTailSource(part, tailSelect) + require.False(t, canPushDown) +} + +func TestLimitPushdownTailSourceBlocksFilteredShortestPathSource(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + limitPushdownTestSourceWhere(t, part, pgsql.NewLiteral(true, pgsql.Boolean)) + + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + + _, canPushDown := limitPushdownTailSource(part, tailSelect) + require.False(t, canPushDown) +} + +func TestLimitPushdownTailSourceAllowsBoundEndpointShortestPathSource(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + limitPushdownTestSourceWhere(t, part, pgsql.NewBinaryExpression( + limitPushdownTestBoundEndpointConstraint(limitPushdownTestRootAlias, expansionRootID), + pgsql.OperatorAnd, + limitPushdownTestBoundEndpointConstraint(limitPushdownTestTerminalAlias, expansionNextID), + )) + + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + + sourceFrame, canPushDown := limitPushdownTailSource(part, tailSelect) + require.True(t, canPushDown) + require.Equal(t, limitPushdownTestSourceFrame, sourceFrame) +} + +func TestLimitPushdownTailSourceBlocksUnrelatedSourceEndpointConstraint(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + limitPushdownTestSourceWhere(t, part, limitPushdownTestBoundEndpointConstraint( + pgsql.Identifier("n2"), + expansionRootID, + )) + + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + + _, canPushDown := limitPushdownTailSource(part, tailSelect) + require.False(t, canPushDown) +} + +func TestLimitPushdownTailSourceAllowsBoundEndpointShortestPathSourceWithoutTailWhere(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + limitPushdownTestSourceWhere(t, part, limitPushdownTestBoundEndpointConstraint( + limitPushdownTestRootAlias, + expansionRootID, + )) + + sourceFrame, canPushDown := limitPushdownTailSource(part, limitPushdownTestTail(nil)) + require.True(t, canPushDown) + require.Equal(t, limitPushdownTestSourceFrame, sourceFrame) +} + +func TestLimitPushdownTailSourceAllowsBidirectionalShortestPathEndpointInequality(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + + sourceFrame, canPushDown := limitPushdownTailSource(part, tailSelect) + require.True(t, canPushDown) + require.Equal(t, limitPushdownTestSourceFrame, sourceFrame) +} + +func TestPushDownShortestPathLimitAppendsHarnessLimitWithEndpointInequality(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + + pushDownShortestPathLimit(part, tailSelect) + + sourceCTE := findCTE(part.Model, limitPushdownTestSourceFrame) + require.NotNil(t, sourceCTE) + require.NotNil(t, sourceCTE.Query.CommonTableExpressions) + require.Len(t, sourceCTE.Query.CommonTableExpressions.Expressions, 1) + + harnessCTE := sourceCTE.Query.CommonTableExpressions.Expressions[0] + selectBody, isSelect := harnessCTE.Query.Body.(pgsql.Select) + require.True(t, isSelect) + require.Len(t, selectBody.From, 1) + + functionCall, isFunctionCall := selectBody.From[0].Source.(pgsql.FunctionCall) + require.True(t, isFunctionCall) + require.Len(t, functionCall.Parameters, 1) + require.Equal(t, pgsql.NewTypeCast(part.Limit, pgsql.Int8), functionCall.Parameters[0]) +} + +func TestPushDownBidirectionalShortestPathLimitAppendsHarnessLimitWithEndpointInequality(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + + pushDownShortestPathLimit(part, tailSelect) + + sourceCTE := findCTE(part.Model, limitPushdownTestSourceFrame) + require.NotNil(t, sourceCTE) + require.NotNil(t, sourceCTE.Query.CommonTableExpressions) + require.Len(t, sourceCTE.Query.CommonTableExpressions.Expressions, 1) + + harnessCTE := sourceCTE.Query.CommonTableExpressions.Expressions[0] + selectBody, isSelect := harnessCTE.Query.Body.(pgsql.Select) + require.True(t, isSelect) + require.Len(t, selectBody.From, 1) + + functionCall, isFunctionCall := selectBody.From[0].Source.(pgsql.FunctionCall) + require.True(t, isFunctionCall) + require.Len(t, functionCall.Parameters, 1) + require.Equal(t, pgsql.NewTypeCast(part.Limit, pgsql.Int8), functionCall.Parameters[0]) +} + +func TestPushDownBidirectionalShortestPathLimitAllowsBoundEndpointSourceWhere(t *testing.T) { + part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + limitPushdownTestSourceWhere(t, part, pgsql.NewBinaryExpression( + limitPushdownTestBoundEndpointConstraint(limitPushdownTestRootAlias, expansionRootID), + pgsql.OperatorAnd, + limitPushdownTestBoundEndpointConstraint(limitPushdownTestTerminalAlias, expansionNextID), + )) + + pushDownShortestPathLimit(part, limitPushdownTestTail(nil)) + + sourceCTE := findCTE(part.Model, limitPushdownTestSourceFrame) + require.NotNil(t, sourceCTE) + require.NotNil(t, sourceCTE.Query.CommonTableExpressions) + require.Len(t, sourceCTE.Query.CommonTableExpressions.Expressions, 1) + + harnessCTE := sourceCTE.Query.CommonTableExpressions.Expressions[0] + selectBody, isSelect := harnessCTE.Query.Body.(pgsql.Select) + require.True(t, isSelect) + require.Len(t, selectBody.From, 1) + + functionCall, isFunctionCall := selectBody.From[0].Source.(pgsql.FunctionCall) + require.True(t, isFunctionCall) + require.Len(t, functionCall.Parameters, 1) + require.Equal(t, pgsql.NewTypeCast(part.Limit, pgsql.Int8), functionCall.Parameters[0]) +} diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index 07b46c24..a110b43a 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -71,12 +71,16 @@ type Expansion struct { TerminalNodeConstraints pgsql.Expression TerminalNodeSatisfactionProjection pgsql.SelectItem DeferredNodeSatisfactionConstraint pgsql.Expression + UseMaterializedTerminalFilter bool + UseMaterializedEndpointPairFilter bool PrimerQueryParameter *BoundIdentifier BackwardPrimerQueryParameter *BoundIdentifier RecursiveQueryParameter *BoundIdentifier BackwardRecursiveQueryParameter *BoundIdentifier + UseBidirectionalSearch bool + EdgeStartIdentifier pgsql.Identifier EdgeStartColumn pgsql.CompoundIdentifier EdgeEndIdentifier pgsql.Identifier @@ -122,6 +126,205 @@ func (s *Expansion) CanExecuteBidirectionalSearch() bool { return s.PrimerNodeConstraints != nil && s.TerminalNodeConstraints != nil } +func (s *TraversalStep) CanExecuteBidirectionalSearch() bool { + if s.Expansion == nil { + return false + } + + return s.Expansion.CanExecuteBidirectionalSearch() || + (s.LeftNodeBound && s.RightNodeBound && s.Frame != nil && s.Frame.Previous != nil) +} + +func (s *TraversalStep) hasPreviousFrameBinding() bool { + return s.Frame != nil && s.Frame.Previous != nil +} + +func (s *TraversalStep) usesBoundEndpointPairs() bool { + return s.LeftNodeBound && s.RightNodeBound && s.hasPreviousFrameBinding() +} + +func (s *TraversalStep) endpointSelectivity(scope *Scope, expression pgsql.Expression, bound bool) (int, error) { + selectivity, err := MeasureSelectivity(scope, expression) + if err != nil { + return 0, err + } + + if bound && s.hasPreviousFrameBinding() { + selectivity += selectivityWeightBoundIdentifier + } + + return selectivity, nil +} + +func isBidirectionalSearchAnchor(selectivity int) bool { + return selectivity >= selectivityBidirectionalAnchorThreshold +} + +func hasIDEqualityConstraint(expression pgsql.Expression, identifier pgsql.Identifier) bool { + for _, term := range flattenConjunction(expression) { + binaryExpression, isBinaryExpression := unwrapParenthetical(term).(*pgsql.BinaryExpression) + if !isBinaryExpression || binaryExpression.Operator != pgsql.OperatorEquals { + continue + } + + leftIsID := isIdentifierIDReference(binaryExpression.LOperand, identifier) + rightIsID := isIdentifierIDReference(binaryExpression.ROperand, identifier) + + if leftIsID && isStaticIDEqualityOperand(binaryExpression.ROperand) { + return true + } + + if rightIsID && isStaticIDEqualityOperand(binaryExpression.LOperand) { + return true + } + } + + return false +} + +func hasLocalIDEqualityConstraint(expression pgsql.Expression, identifier pgsql.Identifier) bool { + if !hasIDEqualityConstraint(expression, identifier) { + return false + } + + return hasLocalEndpointConstraint(expression, identifier) +} + +func hasLocalEndpointConstraint(expression pgsql.Expression, identifier pgsql.Identifier) bool { + if expression == nil || !referencesIdentifier(expression, identifier) { + return false + } + + _, externalConstraints := partitionConstraintByLocality(expression, pgsql.AsIdentifierSet(identifier)) + return externalConstraints == nil +} + +func referencesIdentifier(expression pgsql.Expression, identifier pgsql.Identifier) bool { + references := false + + _ = walk.PgSQL(expression, walk.NewSimpleVisitor[pgsql.SyntaxNode]( + func(node pgsql.SyntaxNode, handler walk.VisitorHandler) { + if compoundIdentifier, isCompoundIdentifier := node.(pgsql.CompoundIdentifier); isCompoundIdentifier && + len(compoundIdentifier) > 0 && + compoundIdentifier[0] == identifier { + references = true + handler.SetDone() + } + }, + )) + + return references +} + +func hasPairAwareEndpointConstraint(expression pgsql.Expression, identifier pgsql.Identifier) bool { + return hasLocalEndpointConstraint(expression, identifier) && + referencesEndpointSearchColumn(expression, identifier) +} + +func referencesEndpointSearchColumn(expression pgsql.Expression, identifier pgsql.Identifier) bool { + references := false + + _ = walk.PgSQL(expression, walk.NewSimpleVisitor[pgsql.SyntaxNode]( + func(node pgsql.SyntaxNode, handler walk.VisitorHandler) { + // kind_ids constrains labels but not the endpoint ID space the + // bidirectional harness uses to enumerate root/terminal pairs. + if compoundIdentifier, isCompoundIdentifier := node.(pgsql.CompoundIdentifier); isCompoundIdentifier && + len(compoundIdentifier) > 1 && + compoundIdentifier[0] == identifier && + compoundIdentifier[1] != pgsql.ColumnKindIDs { + references = true + handler.SetDone() + } + }, + )) + + return references +} + +func isStaticIDEqualityOperand(expression pgsql.Expression) bool { + if expression == nil { + return false + } + + isStatic := true + + _ = walk.PgSQL(unwrapParenthetical(expression), walk.NewSimpleVisitor[pgsql.SyntaxNode]( + func(node pgsql.SyntaxNode, handler walk.VisitorHandler) { + switch node.(type) { + case pgsql.Identifier, pgsql.CompoundIdentifier, pgsql.RowColumnReference: + isStatic = false + handler.SetDone() + } + }, + )) + + return isStatic +} + +func isIdentifierIDReference(expression pgsql.Expression, identifier pgsql.Identifier) bool { + compoundIdentifier, isCompoundIdentifier := unwrapParenthetical(expression).(pgsql.CompoundIdentifier) + return isCompoundIdentifier && len(compoundIdentifier) == 2 && + compoundIdentifier[0] == identifier && + compoundIdentifier[1] == pgsql.ColumnID +} + +func (s *TraversalStep) CanExecuteSelectiveBidirectionalSearch(scope *Scope) (bool, error) { + if s.Expansion == nil { + return false, nil + } + + if s.usesBoundEndpointPairs() { + return true, nil + } + + if !s.Expansion.CanExecuteBidirectionalSearch() { + return false, nil + } + + if s.LeftNode == nil || s.RightNode == nil { + return false, nil + } + + if hasLocalIDEqualityConstraint(s.Expansion.PrimerNodeConstraints, s.LeftNode.Identifier) && + hasLocalIDEqualityConstraint(s.Expansion.TerminalNodeConstraints, s.RightNode.Identifier) { + return true, nil + } + + // Bidirectional shortest-path search is only correct for multi-endpoint + // queries when the harness knows the complete pair universe. Unbound + // endpoint predicates can be selective, but they do not by themselves + // define which (root, terminal) pairs must be completed. + return false, nil +} + +func (s *TraversalStep) CanExecutePairAwareBidirectionalSearch(scope *Scope) (bool, error) { + if canExecute, err := s.CanExecuteSelectiveBidirectionalSearch(scope); canExecute || err != nil { + return canExecute, err + } + + if s.Expansion == nil || + (!s.Expansion.Options.FindShortestPath && !s.Expansion.Options.FindAllShortestPaths) || + !s.Expansion.CanExecuteBidirectionalSearch() || + s.LeftNode == nil || + s.RightNode == nil || + !hasPairAwareEndpointConstraint(s.Expansion.PrimerNodeConstraints, s.LeftNode.Identifier) || + !hasPairAwareEndpointConstraint(s.Expansion.TerminalNodeConstraints, s.RightNode.Identifier) { + return false, nil + } + + if primerSelectivity, err := s.endpointSelectivity(scope, s.Expansion.PrimerNodeConstraints, s.LeftNodeBound); err != nil { + return false, err + } else if !isBidirectionalSearchAnchor(primerSelectivity) { + return false, nil + } + + if terminalSelectivity, err := s.endpointSelectivity(scope, s.Expansion.TerminalNodeConstraints, s.RightNodeBound); err != nil { + return false, err + } else { + return isBidirectionalSearchAnchor(terminalSelectivity), nil + } +} + // flattenConjunction collects the leaf operands of a left-recursive AND chain. func flattenConjunction(expr pgsql.Expression) []pgsql.Expression { if bin, typeOK := expr.(*pgsql.BinaryExpression); !typeOK || bin.Operator != pgsql.OperatorAnd { @@ -179,6 +382,7 @@ type TraversalStep struct { Frame *Frame Direction graph.Direction Expansion *Expansion + PathReversed bool LeftNode *BoundIdentifier LeftNodeBound bool LeftNodeConstraints pgsql.Expression @@ -239,6 +443,8 @@ func (s *TraversalStep) FlipNodes() { case graph.DirectionInbound: s.Direction = graph.DirectionOutbound } + + s.PathReversed = !s.PathReversed } type PatternPart struct { @@ -323,9 +529,18 @@ type QueryPart struct { projections *Projections mutations *Mutations fromClauses []pgsql.FromClause + limitPushdownFrames *pgsql.IdentifierSet + referencedIdentifiers *pgsql.IdentifierSet stashedExpressionTreeTranslator *ExpressionTreeTranslator stashedQuantifierArray []pgsql.Expression quantifierIdentifiers *pgsql.IdentifierSet + unwindClauses []UnwindClause + isCreating bool +} + +type UnwindClause struct { + Expression pgsql.Expression + Binding *BoundIdentifier } func NewQueryPart(numReadingClauses, numUpdatingClauses int) *QueryPart { @@ -338,6 +553,8 @@ func NewQueryPart(numReadingClauses, numUpdatingClauses int) *QueryPart { numUpdatingClauses: numUpdatingClauses, mutations: NewMutations(), properties: map[string]pgsql.Expression{}, + limitPushdownFrames: pgsql.NewIdentifierSet(), + referencedIdentifiers: pgsql.NewIdentifierSet(), quantifierIdentifiers: pgsql.NewIdentifierSet(), } } @@ -353,6 +570,24 @@ func (s *QueryPart) ConsumeFromClauses() []pgsql.FromClause { return fromClauses } +func (s *QueryPart) AllowLimitPushdown(frameIdentifier pgsql.Identifier) { + s.limitPushdownFrames.Add(frameIdentifier) +} + +func (s *QueryPart) CanPushDownLimitTo(frameIdentifier pgsql.Identifier) bool { + return s.limitPushdownFrames.Contains(frameIdentifier) +} + +func (s *QueryPart) AddUnwindClause(clause UnwindClause) { + s.unwindClauses = append(s.unwindClauses, clause) +} + +func (s *QueryPart) ConsumeUnwindClauses() []UnwindClause { + clauses := s.unwindClauses + s.unwindClauses = nil + return clauses +} + func (s *QueryPart) RestoreStashedPattern() { s.currentPattern = s.stashedPattern s.stashedPattern = nil @@ -469,15 +704,34 @@ type Delete struct { UpdateBinding *BoundIdentifier } +type NodeCreate struct { + Binding *BoundIdentifier + Properties map[string]pgsql.Expression + Kinds graph.Kinds +} + +type EdgeCreate struct { + Binding *BoundIdentifier + Properties map[string]pgsql.Expression + Kinds graph.Kinds + LeftNode *BoundIdentifier + RightNode *BoundIdentifier + Direction graph.Direction +} + type Mutations struct { - Deletions *graph.IndexedSlice[pgsql.Identifier, *Delete] - Updates *graph.IndexedSlice[pgsql.Identifier, *Update] + Deletions *graph.IndexedSlice[pgsql.Identifier, *Delete] + Updates *graph.IndexedSlice[pgsql.Identifier, *Update] + Creations *graph.IndexedSlice[pgsql.Identifier, *NodeCreate] + EdgeCreations *graph.IndexedSlice[pgsql.Identifier, *EdgeCreate] } func NewMutations() *Mutations { return &Mutations{ - Deletions: graph.NewIndexedSlice[pgsql.Identifier, *Delete](), - Updates: graph.NewIndexedSlice[pgsql.Identifier, *Update](), + Deletions: graph.NewIndexedSlice[pgsql.Identifier, *Delete](), + Updates: graph.NewIndexedSlice[pgsql.Identifier, *Update](), + Creations: graph.NewIndexedSlice[pgsql.Identifier, *NodeCreate](), + EdgeCreations: graph.NewIndexedSlice[pgsql.Identifier, *EdgeCreate](), } } @@ -626,3 +880,39 @@ func extractIdentifierFromCypherExpression(expression cypher.Expression) (pgsql. return pgsql.Identifier(variableExpression.Symbol), true, nil } + +type FromClauseBuilder struct { + seen map[pgsql.Identifier]struct{} + fromClauses []pgsql.FromClause +} + +func NewFromClauseBuilder() *FromClauseBuilder { + return &FromClauseBuilder{ + seen: make(map[pgsql.Identifier]struct{}), + } +} + +func (s *FromClauseBuilder) AddIdentifier(frameID pgsql.Identifier) { + if frameID == "" { + return + } + + if _, already := s.seen[frameID]; !already { + s.seen[frameID] = struct{}{} + s.fromClauses = append(s.fromClauses, pgsql.FromClause{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{frameID}, + }, + }) + } +} + +func (s *FromClauseBuilder) AddBinding(binding *BoundIdentifier) { + if binding != nil && binding.LastProjection != nil { + s.AddIdentifier(binding.LastProjection.Binding.Identifier) + } +} + +func (s *FromClauseBuilder) Clauses() []pgsql.FromClause { + return s.fromClauses +} diff --git a/cypher/models/pgsql/translate/node.go b/cypher/models/pgsql/translate/node.go index fcf3a203..22cba658 100644 --- a/cypher/models/pgsql/translate/node.go +++ b/cypher/models/pgsql/translate/node.go @@ -16,6 +16,8 @@ func (s *Translator) translateNodePattern(nodePattern *cypher.NodePattern) error if bindingResult, err := s.bindPatternExpression(nodePattern, pgsql.NodeComposite); err != nil { return err + } else if queryPart.isCreating { + return s.collectCreateNodePattern(nodePattern, patternPart, bindingResult) } else if err := s.translateNodePatternToStep(nodePattern, patternPart, bindingResult); err != nil { return err } @@ -23,6 +25,51 @@ func (s *Translator) translateNodePattern(nodePattern *cypher.NodePattern) error return nil } +func (s *Translator) collectCreateNodePattern(nodePattern *cypher.NodePattern, part *PatternPart, bindingResult BindingResult) error { + queryPart := s.query.CurrentPart() + + if !bindingResult.AlreadyBound { + queryPart.mutations.Creations.Put(bindingResult.Binding.Identifier, &NodeCreate{ + Binding: bindingResult.Binding, + Properties: queryPart.ConsumeProperties(), + Kinds: nodePattern.Kinds, + }) + } else { + queryPart.ConsumeProperties() + } + + if part.IsTraversal { + if numSteps := len(part.TraversalSteps); numSteps == 0 { + part.TraversalSteps = append(part.TraversalSteps, &TraversalStep{ + LeftNode: bindingResult.Binding, + LeftNodeBound: bindingResult.AlreadyBound, + }) + } else { + currentStep := part.TraversalSteps[numSteps-1] + if currentStep.RightNode == nil { + currentStep.RightNode = bindingResult.Binding + currentStep.RightNodeBound = bindingResult.AlreadyBound + + if currentStep.Edge != nil { + // CREATE sees the relationship before the right node. Once the + // node arrives, complete the pending edge endpoint. + if pendingEdge := queryPart.mutations.EdgeCreations.Get(currentStep.Edge.Identifier); pendingEdge != nil { + pendingEdge.RightNode = bindingResult.Binding + } + } + } + } + } else { + part.NodeSelect.Binding = bindingResult.Binding + } + + if part.PatternBinding != nil { + part.PatternBinding.DependOn(bindingResult.Binding) + } + + return nil +} + func (s *Translator) translateNodePatternToStep(nodePattern *cypher.NodePattern, part *PatternPart, bindingResult BindingResult) error { currentQueryPart := s.query.CurrentPart() @@ -119,6 +166,10 @@ func (s *Translator) buildNodePatternPart(part *PatternPart) error { }) } + nextSelect.From = append(nextSelect.From, unwindFromClauses(s.query.CurrentPart().ConsumeUnwindClauses())...) + + // UNWIND clauses collected before the first concrete node lookup become row + // sources for the pattern CTE that starts the MATCH pipeline. nextSelect.From = append(nextSelect.From, pgsql.FromClause{ Source: pgsql.TableReference{ Name: pgsql.CompoundIdentifier{pgsql.TableNode}, diff --git a/cypher/models/pgsql/translate/pattern.go b/cypher/models/pgsql/translate/pattern.go index f8fc6791..57eff0d2 100644 --- a/cypher/models/pgsql/translate/pattern.go +++ b/cypher/models/pgsql/translate/pattern.go @@ -69,12 +69,18 @@ func (s *Translator) buildTraversalPattern(traversalStep *TraversalStep, isRootS if traversalStepQuery, err := s.buildTraversalPatternRoot(traversalStep.Frame, traversalStep); err != nil { return err } else { + if selectBody, ok := traversalStepQuery.Body.(pgsql.Select); ok { + selectBody.From = append(selectBody.From, unwindFromClauses(s.query.CurrentPart().ConsumeUnwindClauses())...) + traversalStepQuery.Body = selectBody + } + s.query.CurrentPart().Model.AddCTE(pgsql.CommonTableExpression{ Alias: pgsql.TableAlias{ Name: traversalStep.Frame.Binding.Identifier, }, Query: traversalStepQuery, }) + s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } else { if traversalStepQuery, err := s.buildTraversalPatternStep(traversalStep.Frame, traversalStep); err != nil { @@ -86,6 +92,7 @@ func (s *Translator) buildTraversalPattern(traversalStep *TraversalStep, isRootS }, Query: traversalStepQuery, }) + s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } @@ -105,6 +112,7 @@ func (s *Translator) buildExpansionPattern(traversalStepContext TraversalStepCon }, Query: traversalStepQuery, }) + s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } else { if traversalStepQuery, err := s.buildExpansionPatternStep(traversalStepContext, expansion); err != nil { @@ -116,6 +124,7 @@ func (s *Translator) buildExpansionPattern(traversalStepContext TraversalStepCon }, Query: traversalStepQuery, }) + s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } @@ -127,7 +136,7 @@ func (s *Translator) buildShortestPathsExpansionPattern(traversalStepContext Tra if traversalStepContext.IsRootStep { if allPaths { - if traversalStep.Expansion.CanExecuteBidirectionalSearch() { + if traversalStep.Expansion.UseBidirectionalSearch { if traversalStepQuery, err := expansion.BuildBiDirectionalAllShortestPathsRoot(); err != nil { return err } else { @@ -148,9 +157,22 @@ func (s *Translator) buildShortestPathsExpansionPattern(traversalStepContext Tra Query: traversalStepQuery, }) } - } else if traversalStepQuery, err := expansion.BuildShortestPathsRoot(); err != nil { - return err } else { + var ( + traversalStepQuery pgsql.Query + err error + ) + + if traversalStep.Expansion.UseBidirectionalSearch { + traversalStepQuery, err = expansion.BuildBiDirectionalShortestPathsRoot() + } else { + traversalStepQuery, err = expansion.BuildShortestPathsRoot() + } + + if err != nil { + return err + } + s.query.CurrentPart().Model.AddCTE(pgsql.CommonTableExpression{ Alias: pgsql.TableAlias{ Name: traversalStep.Frame.Binding.Identifier, diff --git a/cypher/models/pgsql/translate/projection.go b/cypher/models/pgsql/translate/projection.go index 9a5712c3..ba499e95 100644 --- a/cypher/models/pgsql/translate/projection.go +++ b/cypher/models/pgsql/translate/projection.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/walk" "github.com/specterops/dawgs/cypher/models" "github.com/specterops/dawgs/cypher/models/pgsql" @@ -97,6 +98,10 @@ func buildInternalProjection(scope *Scope, projectedBindings []*BoundIdentifier) } } + if len(boundProjections.Items) == 0 { + boundProjections.Items = append(boundProjections.Items, pgsql.NewLiteral(1, pgsql.Int)) + } + // Lastly, return the projections while rewriting the given constraints return boundProjections, nil } @@ -129,14 +134,52 @@ func buildProjectionForExpansionPath(alias pgsql.Identifier, projected *BoundIde }, nil } +func concatenatePathCompositeParts(parts []pgsql.Expression) pgsql.Expression { + if len(parts) == 0 { + return nil + } + + joined := parts[0] + for idx := 1; idx < len(parts); idx++ { + joined = pgsql.NewBinaryExpression(joined, pgsql.OperatorConcatenate, parts[idx]) + } + + return joined +} + +func bindingFrameReference(scope *Scope, binding *BoundIdentifier) pgsql.CompoundIdentifier { + frameIdentifier := scope.CurrentFrameBinding().Identifier + if binding.LastProjection != nil { + frameIdentifier = binding.LastProjection.Binding.Identifier + } + + return pgsql.CompoundIdentifier{frameIdentifier, binding.Identifier} +} + +func expansionPathEdgeArrayReference(scope *Scope, expansionPath *BoundIdentifier) (pgsql.Expression, error) { + for _, dependency := range expansionPath.Dependencies { + return bindingFrameReference(scope, dependency), nil + } + + return nil, fmt.Errorf("expansion path %s does not reference an expansion edge binding", expansionPath.Identifier) +} + func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIdentifier, scope *Scope) ([]pgsql.SelectItem, error) { + if projected.LastProjection != nil { + return []pgsql.SelectItem{ + &pgsql.AliasedExpression{ + Expression: pgsql.CompoundIdentifier{projected.LastProjection.Binding.Identifier, projected.Identifier}, + Alias: pgsql.AsOptionalIdentifier(alias), + }, + }, nil + } + var ( - parameterExpression pgsql.Expression - preExpansionEdgeRefs []pgsql.Expression - postExpansionEdgeRefs []pgsql.Expression - nodeReferences []pgsql.Expression - seenExpansionPath = false - useEdgesToPathFunction = false + edgeArrayReferences []pgsql.Expression + nodeReferences []pgsql.Expression + directNodeReferences []pgsql.Expression + directEdgeReferences []pgsql.Expression + seenExpansionPath = false ) // Path composite components are encoded as dependencies on the bound identifier representing the @@ -146,27 +189,30 @@ func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIde switch dependency.DataType { case pgsql.ExpansionPath: seenExpansionPath = true - useEdgesToPathFunction = true - - parameterExpression = pgsql.OptionalBinaryExpressionJoin( - parameterExpression, pgsql.OperatorConcatenate, dependency.Identifier, - ) + if edgeArrayReference, err := expansionPathEdgeArrayReference(scope, dependency); err != nil { + return nil, err + } else { + edgeArrayReferences = append(edgeArrayReferences, edgeArrayReference) + } case pgsql.EdgeComposite: - useEdgesToPathFunction = true - - ref := rewriteCompositeTypeFieldReference( + directEdgeReference := pgsql.CompoundIdentifier{ scope.CurrentFrameBinding().Identifier, - pgsql.CompoundIdentifier{dependency.Identifier, pgsql.ColumnID}, - ) - - if seenExpansionPath { - postExpansionEdgeRefs = append(postExpansionEdgeRefs, ref) - } else { - preExpansionEdgeRefs = append(preExpansionEdgeRefs, ref) + dependency.Identifier, } + directEdgeReferences = append(directEdgeReferences, directEdgeReference) + edgeArrayReferences = append(edgeArrayReferences, pgsql.ArrayLiteral{ + Values: []pgsql.Expression{directEdgeReference}, + CastType: pgsql.EdgeCompositeArray, + }) + case pgsql.NodeComposite: + directNodeReferences = append(directNodeReferences, pgsql.CompoundIdentifier{ + scope.CurrentFrameBinding().Identifier, + dependency.Identifier, + }) + nodeReferences = append(nodeReferences, rewriteCompositeTypeFieldReference( scope.CurrentFrameBinding().Identifier, pgsql.CompoundIdentifier{dependency.Identifier, pgsql.ColumnID}, @@ -177,41 +223,51 @@ func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIde } } - // The code below is covering a strange edge-case of cypher where a query may contain the following - // form: match p = (n) return p - // - // In this case it is not appropriate to call the edges_to_path(...) function and instead a call to - // the corresponding nodes_to_path(...) function must be authored. - if useEdgesToPathFunction { - // Pre-expansion edges go LEFT of the expansion (existing prepend semantics). - if len(preExpansionEdgeRefs) > 0 { - parameterExpression = pgsql.OptionalBinaryExpressionJoin( - parameterExpression, - pgsql.OperatorConcatenate, - pgsql.ArrayLiteral{Values: preExpansionEdgeRefs, CastType: pgsql.Int8Array}, - ) - } + // Direct, non-expansion path bindings already have their node and edge composites in scope. Keep + // those explicit components instead of reconstructing the path from edge IDs: this preserves path + // order and duplicate nodes, and it also works for rows produced by data-modifying CTEs where + // re-reading node/edge tables in the same statement may not see the RETURNING values. + if !seenExpansionPath && len(directNodeReferences) > 0 { + return []pgsql.SelectItem{ + &pgsql.AliasedExpression{ + Expression: pgsql.CompositeValue{ + DataType: pgsql.PathComposite, + Values: []pgsql.Expression{ + pgsql.ArrayLiteral{ + Values: directNodeReferences, + CastType: pgsql.NodeCompositeArray, + }, + pgsql.ArrayLiteral{ + Values: directEdgeReferences, + CastType: pgsql.EdgeCompositeArray, + }, + }, + }, + Alias: pgsql.AsOptionalIdentifier(alias), + }, + }, nil + } - // Post-expansion edges go RIGHT of the expansion — use NewBinaryExpression directly. - if len(postExpansionEdgeRefs) > 0 { - postArray := pgsql.ArrayLiteral{Values: postExpansionEdgeRefs, CastType: pgsql.Int8Array} + if seenExpansionPath { + if len(directNodeReferences) == 0 { + return nil, fmt.Errorf("expansion path %s does not contain a root node reference", projected.Identifier) + } - if parameterExpression == nil { - parameterExpression = postArray - } else { - parameterExpression = pgsql.NewBinaryExpression( - parameterExpression, pgsql.OperatorConcatenate, postArray, - ) - } + edgeArrayExpression := concatenatePathCompositeParts(edgeArrayReferences) + if edgeArrayExpression == nil { + edgeArrayExpression = pgsql.ArrayLiteral{CastType: pgsql.EdgeCompositeArray} } return []pgsql.SelectItem{ &pgsql.AliasedExpression{ Expression: pgsql.FunctionCall{ - Function: pgsql.FunctionEdgesToPath, + Function: pgsql.FunctionOrderedEdgesToPath, Parameters: []pgsql.Expression{ - pgsql.Variadic{ - Expression: parameterExpression, + directNodeReferences[0], + edgeArrayExpression, + pgsql.ArrayLiteral{ + Values: directNodeReferences, + CastType: pgsql.NodeCompositeArray, }, }, CastType: pgsql.PathComposite, @@ -300,49 +356,18 @@ func buildProjectionForNodeComposite(alias pgsql.Identifier, projected *BoundIde } func buildProjectionForExpansionEdge(alias pgsql.Identifier, projected *BoundIdentifier, scope *Scope) ([]pgsql.SelectItem, error) { - var ( - edgeAggregateParameter = pgsql.CompositeValue{ - DataType: pgsql.EdgeComposite, - } - - edgeWhereClause = pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{projected.Identifier, pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.NewAnyExpression( - pgsql.CompoundIdentifier{scope.CurrentFrame().Binding.Identifier, pgsql.ColumnPath}, - pgsql.ExpansionPath, - ), - ) - ) - - // Reference all of the edge table columns to create the edge composites - for _, edgeTableColumn := range pgsql.EdgeTableColumns { - edgeAggregateParameter.Values = append(edgeAggregateParameter.Values, pgsql.CompoundIdentifier{projected.Identifier, edgeTableColumn}) - } - - // Change the type to the node composite now that this is projected + // Change the type to the edge composite now that this is projected projected.DataType = pgsql.EdgeComposite // Create a new final projection that's aliased to the visible binding's identifier return []pgsql.SelectItem{ &pgsql.AliasedExpression{ Expression: &pgsql.Parenthetical{ - Expression: pgsql.Select{ - Projection: []pgsql.SelectItem{ - pgsql.FunctionCall{ - Function: pgsql.FunctionArrayAggregate, - Parameters: []pgsql.Expression{edgeAggregateParameter}, - }, - }, - From: []pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, - Binding: models.OptionalValue(projected.Identifier), - }, - Joins: nil, - }}, - Where: edgeWhereClause, - }, + Expression: pgsql.FormattingLiteral(fmt.Sprintf( + "select coalesce(array_agg((%[1]s.id, %[1]s.start_id, %[1]s.end_id, %[1]s.kind_id, %[1]s.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(%[2]s.path) with ordinality as _path(id, ordinality) join edge %[1]s on %[1]s.id = _path.id", + projected.Identifier, + scope.CurrentFrame().Binding.Identifier, + )), }, Alias: pgsql.AsOptionalIdentifier(alias), }, @@ -398,9 +423,16 @@ func buildProjection(alias pgsql.Identifier, projected *BoundIdentifier, scope * default: // If this isn't a type that requires a unique projection, reflect the identifier as-is with its alias + var expression pgsql.Expression + if referenceFrame != nil { + expression = pgsql.CompoundIdentifier{referenceFrame.Binding.Identifier, projected.Identifier} + } else { + expression = projected.Identifier + } + return []pgsql.SelectItem{ &pgsql.AliasedExpression{ - Expression: pgsql.CompoundIdentifier{referenceFrame.Binding.Identifier, projected.Identifier}, + Expression: expression, Alias: pgsql.AsOptionalIdentifier(alias), }, }, nil @@ -409,13 +441,14 @@ func buildProjection(alias pgsql.Identifier, projected *BoundIdentifier, scope * func (s *Translator) buildInlineProjection(part *QueryPart) (pgsql.Select, error) { sqlSelect := pgsql.Select{ - Where: part.projections.Constraints, + Distinct: part.projections.Distinct, + Where: part.projections.Constraints, } // If there's a projection frame set, some additional negotiation is required to identify which frame the // from-statement should be written to. Some of this would be better figured out during the translation // of the projection where query scope and other components are not yet fully translated. - if part.projections.Frame != nil { + if part.projections.Frame != nil && !part.projections.Frame.Synthetic { // Look up to see if there are CTE expressions registered. If there are then it is likely // there was a projection between this CTE and the previous multipart query part hasCTEs := part.Model.CommonTableExpressions != nil && len(part.Model.CommonTableExpressions.Expressions) > 0 @@ -431,6 +464,8 @@ func (s *Translator) buildInlineProjection(part *QueryPart) (pgsql.Select, error } } + sqlSelect.From = append(sqlSelect.From, unwindFromClauses(part.ConsumeUnwindClauses())...) + for _, projection := range part.projections.Items { builtProjection := projection.SelectItem @@ -453,21 +488,452 @@ func (s *Translator) buildInlineProjection(part *QueryPart) (pgsql.Select, error return sqlSelect, nil } +func (s *Translator) collectProjectionFromFrames(projections []*Projection) []pgsql.FromClause { + fromClauseBuilder := NewFromClauseBuilder() + + for _, projection := range projections { + identExpr, ok := projection.SelectItem.(pgsql.Identifier) + if !ok { + continue + } + + binding, bound := s.scope.Lookup(identExpr) + if !bound { + continue + } + + if binding.LastProjection != nil { + fromClauseBuilder.AddBinding(binding) + } else if binding.DataType == pgsql.PathComposite { + for _, dep := range binding.Dependencies { + fromClauseBuilder.AddBinding(dep) + } + } + } + + if len(fromClauseBuilder.Clauses()) == 0 { + if currentFrame := s.scope.CurrentFrame(); currentFrame != nil && !currentFrame.Synthetic { + fromClauseBuilder.AddIdentifier(currentFrame.Binding.Identifier) + } + } + + return fromClauseBuilder.Clauses() +} + +func countLimitPushdownShortestPathHarnessCalls(query pgsql.Query) int { + var count int + + if query.CommonTableExpressions != nil { + for _, cte := range query.CommonTableExpressions.Expressions { + count += countLimitPushdownShortestPathHarnessCalls(cte.Query) + } + } + + if selectBody, isSelect := query.Body.(pgsql.Select); isSelect { + for _, fromClause := range selectBody.From { + if functionCall, isFunctionCall := fromClause.Source.(pgsql.FunctionCall); isFunctionCall && + isLimitPushdownShortestPathHarness(functionCall.Function) { + count += 1 + } + } + } + + return count +} + +func isLimitPushdownShortestPathHarness(function pgsql.Identifier) bool { + return function == pgsql.FunctionUnidirectionalSPHarness || function == pgsql.FunctionBidirectionalSPHarness +} + +func appendLimitToShortestPathHarness(query *pgsql.Query, limit pgsql.Expression) { + if query.CommonTableExpressions != nil { + for idx := range query.CommonTableExpressions.Expressions { + appendLimitToShortestPathHarness(&query.CommonTableExpressions.Expressions[idx].Query, limit) + } + } + + if selectBody, isSelect := query.Body.(pgsql.Select); isSelect { + for idx := range selectBody.From { + if functionCall, isFunctionCall := selectBody.From[idx].Source.(pgsql.FunctionCall); isFunctionCall && + isLimitPushdownShortestPathHarness(functionCall.Function) { + // The shortest-path harness accepts an optional path_limit. Passing + // the query LIMIT here lets the BFS stop before producing rows the + // outer query will discard. + functionCall.Parameters = append(functionCall.Parameters, pgsql.NewTypeCast(limit, pgsql.Int8)) + selectBody.From[idx].Source = functionCall + } + } + + query.Body = selectBody + } +} + +func selectContainsAggregate(selectBody pgsql.Select) bool { + containsAggregate := false + + if err := walk.PgSQL(selectBody, walk.NewSimpleVisitor[pgsql.SyntaxNode](func(node pgsql.SyntaxNode, errorHandler walk.VisitorHandler) { + if functionCall, isFunctionCall := node.(pgsql.FunctionCall); isFunctionCall && pgsql.IsAggregateFunction(functionCall.Function) { + containsAggregate = true + } + })); err != nil { + return true + } + + return containsAggregate +} + +func compoundIdentifierEqual(left, right pgsql.CompoundIdentifier) bool { + if len(left) != len(right) { + return false + } + + for idx := range left { + if left[idx] != right[idx] { + return false + } + } + + return true +} + +func directShortestPathHarnessFrame(query pgsql.Query) (pgsql.Identifier, bool) { + if query.CommonTableExpressions == nil { + return "", false + } + + var harnessFrame pgsql.Identifier + + for _, cte := range query.CommonTableExpressions.Expressions { + selectBody, isSelect := cte.Query.Body.(pgsql.Select) + if !isSelect { + continue + } + + for _, fromClause := range selectBody.From { + if functionCall, isFunctionCall := fromClause.Source.(pgsql.FunctionCall); isFunctionCall && + isLimitPushdownShortestPathHarness(functionCall.Function) { + if harnessFrame != "" { + return "", false + } + + harnessFrame = cte.Alias.Name + } + } + } + + return harnessFrame, harnessFrame != "" +} + +func isCompoundIdentifierOperand(expression pgsql.Expression, identifier pgsql.CompoundIdentifier) bool { + compoundIdentifier, isCompoundIdentifier := unwrapParenthetical(expression).(pgsql.CompoundIdentifier) + return isCompoundIdentifier && compoundIdentifierEqual(compoundIdentifier, identifier) +} + +func isEqualityBetweenCompoundIdentifiers(expression pgsql.Expression, left, right pgsql.CompoundIdentifier) bool { + binaryExpression, isBinaryExpression := unwrapParenthetical(expression).(*pgsql.BinaryExpression) + if !isBinaryExpression || binaryExpression.Operator != pgsql.OperatorEquals { + return false + } + + return (isCompoundIdentifierOperand(binaryExpression.LOperand, left) && isCompoundIdentifierOperand(binaryExpression.ROperand, right)) || + (isCompoundIdentifierOperand(binaryExpression.LOperand, right) && isCompoundIdentifierOperand(binaryExpression.ROperand, left)) +} + +func expansionEndpointJoin(join pgsql.Join, harnessFrame pgsql.Identifier) (pgsql.Identifier, pgsql.Identifier, bool) { + tableReference, isTableReference := join.Table.(pgsql.TableReference) + if !isTableReference || + !tableReference.Binding.Set || + !compoundIdentifierEqual(tableReference.Name, pgsql.TableNode.AsCompoundIdentifier()) { + return "", "", false + } + + var ( + nodeAlias = tableReference.Binding.Value + nodeID = pgsql.CompoundIdentifier{nodeAlias, pgsql.ColumnID} + rootID = pgsql.CompoundIdentifier{harnessFrame, expansionRootID} + nextID = pgsql.CompoundIdentifier{harnessFrame, expansionNextID} + ) + + if isEqualityBetweenCompoundIdentifiers(join.JoinOperator.Constraint, nodeID, rootID) { + return nodeAlias, expansionRootID, true + } + + if isEqualityBetweenCompoundIdentifiers(join.JoinOperator.Constraint, nodeID, nextID) { + return nodeAlias, expansionNextID, true + } + + return "", "", false +} + +func shortestPathEndpointAliases(query pgsql.Query) (pgsql.Identifier, pgsql.Identifier, bool) { + harnessFrame, hasHarnessFrame := directShortestPathHarnessFrame(query) + if !hasHarnessFrame { + return "", "", false + } + + selectBody, isSelect := query.Body.(pgsql.Select) + if !isSelect { + return "", "", false + } + + var rootAlias, terminalAlias pgsql.Identifier + + for _, fromClause := range selectBody.From { + for _, join := range fromClause.Joins { + if nodeAlias, expansionColumn, isEndpointJoin := expansionEndpointJoin(join, harnessFrame); isEndpointJoin { + switch expansionColumn { + case expansionRootID: + rootAlias = nodeAlias + case expansionNextID: + terminalAlias = nodeAlias + } + } + } + } + + return rootAlias, terminalAlias, rootAlias != "" && terminalAlias != "" && rootAlias != terminalAlias +} + +func harnessEndpointColumn(expression pgsql.Expression, harnessFrame pgsql.Identifier) (pgsql.Identifier, bool) { + compoundIdentifier, isCompoundIdentifier := unwrapParenthetical(expression).(pgsql.CompoundIdentifier) + if !isCompoundIdentifier || + len(compoundIdentifier) != 2 || + compoundIdentifier[0] != harnessFrame || + (compoundIdentifier[1] != expansionRootID && compoundIdentifier[1] != expansionNextID) { + return "", false + } + + return compoundIdentifier[1], true +} + +func rowIDReferenceAlias(expression pgsql.Expression) (pgsql.Identifier, bool) { + rowColumnReference, isRowColumnReference := unwrapParenthetical(expression).(pgsql.RowColumnReference) + if !isRowColumnReference || rowColumnReference.Column != pgsql.ColumnID { + return "", false + } + + compoundIdentifier, isCompoundIdentifier := unwrapParenthetical(rowColumnReference.Identifier).(pgsql.CompoundIdentifier) + if !isCompoundIdentifier || len(compoundIdentifier) != 2 { + return "", false + } + + return compoundIdentifier[1], true +} + +func sourceAliasMatchesEndpointColumn(sourceAlias, endpointColumn, rootAlias, terminalAlias pgsql.Identifier) bool { + switch endpointColumn { + case expansionRootID: + return sourceAlias == rootAlias + case expansionNextID: + return sourceAlias == terminalAlias + default: + return false + } +} + +func isBoundEndpointProjectionConstraint(expression pgsql.Expression, harnessFrame, rootAlias, terminalAlias pgsql.Identifier) bool { + binaryExpression, isBinaryExpression := unwrapParenthetical(expression).(*pgsql.BinaryExpression) + if !isBinaryExpression || binaryExpression.Operator != pgsql.OperatorEquals { + return false + } + + leftEndpointColumn, leftIsEndpoint := harnessEndpointColumn(binaryExpression.LOperand, harnessFrame) + rightEndpointColumn, rightIsEndpoint := harnessEndpointColumn(binaryExpression.ROperand, harnessFrame) + leftSourceAlias, leftIsRowIDReference := rowIDReferenceAlias(binaryExpression.LOperand) + rightSourceAlias, rightIsRowIDReference := rowIDReferenceAlias(binaryExpression.ROperand) + + return (leftIsEndpoint && rightIsRowIDReference && sourceAliasMatchesEndpointColumn(rightSourceAlias, leftEndpointColumn, rootAlias, terminalAlias)) || + (rightIsEndpoint && leftIsRowIDReference && sourceAliasMatchesEndpointColumn(leftSourceAlias, rightEndpointColumn, rootAlias, terminalAlias)) +} + +func shortestPathSourceWhereTransparent(query pgsql.Query, rootAlias, terminalAlias pgsql.Identifier) bool { + harnessFrame, hasHarnessFrame := directShortestPathHarnessFrame(query) + if !hasHarnessFrame { + return false + } + + selectBody, isSelect := query.Body.(pgsql.Select) + if !isSelect { + return false + } + + if selectBody.Where == nil { + return true + } + + // The source CTE may add equality predicates that tie endpoint node aliases + // back to the harness frame. Those are shape-preserving and do not affect + // whether LIMIT may be pushed into the harness. + for _, term := range flattenConjunction(unwrapParenthetical(selectBody.Where)) { + if !isBoundEndpointProjectionConstraint(term, harnessFrame, rootAlias, terminalAlias) { + return false + } + } + + return true +} + +func endpointIDReference(expression pgsql.Expression, sourceFrame pgsql.Identifier) (pgsql.Identifier, bool) { + rowColumnReference, isRowColumnReference := unwrapParenthetical(expression).(pgsql.RowColumnReference) + compoundIdentifier, isCompoundIdentifier := unwrapParenthetical(rowColumnReference.Identifier).(pgsql.CompoundIdentifier) + if !isRowColumnReference || + !isCompoundIdentifier || + rowColumnReference.Column != pgsql.ColumnID || + len(compoundIdentifier) != 2 || + compoundIdentifier[0] != sourceFrame { + return "", false + } + + return compoundIdentifier[1], true +} + +func isEndpointAliasPair(leftAlias, rightAlias, rootAlias, terminalAlias pgsql.Identifier) bool { + return (leftAlias == rootAlias && rightAlias == terminalAlias) || + (leftAlias == terminalAlias && rightAlias == rootAlias) +} + +func isEndpointInequality(expression pgsql.Expression, sourceFrame, rootAlias, terminalAlias pgsql.Identifier) bool { + binaryExpression, isBinaryExpression := unwrapParenthetical(expression).(*pgsql.BinaryExpression) + if !isBinaryExpression || + !binaryExpression.Operator.IsIn(pgsql.OperatorNotEquals, pgsql.OperatorCypherNotEquals) { + return false + } + + leftAlias, hasLeftAlias := endpointIDReference(binaryExpression.LOperand, sourceFrame) + rightAlias, hasRightAlias := endpointIDReference(binaryExpression.ROperand, sourceFrame) + + return hasLeftAlias && hasRightAlias && isEndpointAliasPair(leftAlias, rightAlias, rootAlias, terminalAlias) +} + +func shortestPathLimitPushdownTransparentWhere(currentPart *QueryPart, sourceFrame pgsql.Identifier, where pgsql.Expression) bool { + if where == nil { + return true + } + + sourceCTE := findCTE(currentPart.Model, sourceFrame) + if sourceCTE == nil { + return false + } + + rootAlias, terminalAlias, hasEndpointAliases := shortestPathEndpointAliases(sourceCTE.Query) + if !hasEndpointAliases || !shortestPathSourceWhereTransparent(sourceCTE.Query, rootAlias, terminalAlias) { + return false + } + + // After the source CTE is known to be transparent, the final tail WHERE may + // only contain the endpoint anti-reflexive predicate. Pushing LIMIT through + // arbitrary filters could drop shorter paths that would have survived later. + for _, term := range flattenConjunction(unwrapParenthetical(where)) { + if !isEndpointInequality(term, sourceFrame, rootAlias, terminalAlias) { + return false + } + } + + return true +} + +func limitPushdownTailSource(currentPart *QueryPart, tailSelect pgsql.Select) (pgsql.Identifier, bool) { + // Keep this intentionally narrow: LIMIT can move into the harness only when + // the tail SELECT is a simple pass-through over one shortest-path CTE. Sorts, + // grouping, aggregation, DISTINCT, SKIP, and writes all change which rows the + // outer LIMIT would observe. + if currentPart.Limit == nil || + currentPart.Skip != nil || + len(currentPart.SortItems) > 0 || + currentPart.numReadingClauses != 1 || + currentPart.numUpdatingClauses > 0 || + currentPart.HasMutations() || + currentPart.HasDeletions() { + return "", false + } + + if currentPart.projections != nil && currentPart.projections.Distinct { + return "", false + } + + if len(tailSelect.GroupBy) > 0 || tailSelect.Having != nil { + return "", false + } + + if selectContainsAggregate(tailSelect) { + return "", false + } + + if len(tailSelect.From) != 1 || len(tailSelect.From[0].Joins) > 0 { + return "", false + } + + tableReference, finalSourceIsCTE := tailSelect.From[0].Source.(pgsql.TableReference) + if !finalSourceIsCTE || len(tableReference.Name) != 1 { + return "", false + } + + sourceFrame := tableReference.Name.Root() + if !shortestPathLimitPushdownTransparentWhere(currentPart, sourceFrame, tailSelect.Where) { + return "", false + } + + return sourceFrame, true +} + +func pushDownShortestPathLimit(currentPart *QueryPart, tailSelect pgsql.Select) { + sourceFrame, canPushDown := limitPushdownTailSource(currentPart, tailSelect) + if !canPushDown { + return + } + + if sourceCTE := findCTE(currentPart.Model, sourceFrame); sourceCTE != nil && + countLimitPushdownShortestPathHarnessCalls(sourceCTE.Query) == 1 { + // Multiple harness calls in one source CTE would make one outer LIMIT + // ambiguous, so only the single-harness case is rewritten. + appendLimitToShortestPathHarness(&sourceCTE.Query, currentPart.Limit) + } +} + +func findCTE(query *pgsql.Query, cteName pgsql.Identifier) *pgsql.CommonTableExpression { + if query.CommonTableExpressions == nil { + return nil + } + + for idx := range query.CommonTableExpressions.Expressions { + nextCTE := &query.CommonTableExpressions.Expressions[idx] + + if nextCTE.Alias.Name == cteName { + return nextCTE + } + } + + return nil +} + +func applyLimitToCTE(query *pgsql.Query, cteName pgsql.Identifier, limit pgsql.Expression) bool { + if cte := findCTE(query, cteName); cte != nil { + cte.Query.Limit = limit + return true + } + + return false +} + +func pushDownTraversalLimit(currentPart *QueryPart, tailSelect pgsql.Select) { + sourceFrame, canPushDown := limitPushdownTailSource(currentPart, tailSelect) + if !canPushDown || !currentPart.CanPushDownLimitTo(sourceFrame) { + return + } + + applyLimitToCTE(currentPart.Model, sourceFrame, currentPart.Limit) +} + func (s *Translator) buildTailProjection() error { var ( currentPart = s.query.CurrentPart() - currentFrame = s.scope.CurrentFrame() - singlePartQuerySelect = pgsql.Select{} + singlePartQuerySelect = pgsql.Select{ + Distinct: currentPart.projections.Distinct, + } ) - // Only add FROM clause if we have a current frame (i.e. there was a MATCH clause) - if currentFrame != nil && currentFrame.Binding.Identifier != "" { - singlePartQuerySelect.From = []pgsql.FromClause{{ - Source: pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{currentFrame.Binding.Identifier}, - }, - }} - } + singlePartQuerySelect.From = s.collectProjectionFromFrames(currentPart.projections.Items) + singlePartQuerySelect.From = append(singlePartQuerySelect.From, unwindFromClauses(currentPart.ConsumeUnwindClauses())...) if projectionConstraint, err := s.treeTranslator.ConsumeAllConstraints(); err != nil { return err @@ -527,6 +993,8 @@ func (s *Translator) buildTailProjection() error { } currentPart.Model.Body = singlePartQuerySelect + pushDownShortestPathLimit(currentPart, singlePartQuerySelect) + pushDownTraversalLimit(currentPart, singlePartQuerySelect) if currentPart.Skip != nil { currentPart.Model.Offset = currentPart.Skip diff --git a/cypher/models/pgsql/translate/query.go b/cypher/models/pgsql/translate/query.go index 8f8ba1e7..6e551b32 100644 --- a/cypher/models/pgsql/translate/query.go +++ b/cypher/models/pgsql/translate/query.go @@ -21,7 +21,8 @@ func (s *Translator) previousValidFrame(partFrame *Frame) (*Frame, bool) { } func (s *Translator) buildMultiPartSinglePartQuery(singlePartQuery *cypher.SinglePartQuery, cteChain []pgsql.CommonTableExpression) error { - // Prepend the CTE chain to the model's + // Earlier multipart sections are rendered as CTEs that the final single-part + // query can read from. currentPart := s.query.CurrentPart() currentPart.Model.CommonTableExpressions.Expressions = append(cteChain, currentPart.Model.CommonTableExpressions.Expressions...) @@ -54,14 +55,15 @@ func (s *Translator) buildSinglePartQuery(singlePartQuery *cypher.SinglePartQuer func (s *Translator) buildMultiPartQuery(singlePartQuery *cypher.SinglePartQuery) error { var multipartCTEChain []pgsql.CommonTableExpression - // In order to author the multipart query part we first have to wrap it in a for _, part := range s.query.Parts[:len(s.query.Parts)-1] { // If the part has an empty inner CTE, make sure to remove it otherwise the keyword will still render if len(part.Model.CommonTableExpressions.Expressions) == 0 { part.Model.CommonTableExpressions = nil } - // Author the part as a nested CTE + // Each non-final query part is a pipeline boundary. Wrap its inline + // projection as a nested CTE so later parts can reference only exported + // bindings. nextCTE := pgsql.CommonTableExpression{ Query: *part.Model, } @@ -97,13 +99,27 @@ func (s *Translator) translateMultiPartQueryPart() error { } func (s *Translator) prepareSinglePartQueryPart(singlePartQuery *cypher.SinglePartQuery) error { - s.query.AddPart(NewQueryPart(len(singlePartQuery.ReadingClauses), len(singlePartQuery.UpdatingClauses))) + newQueryPart := NewQueryPart(len(singlePartQuery.ReadingClauses), len(singlePartQuery.UpdatingClauses)) + + if referencedIdentifiers, err := collectReferencedIdentifiers(singlePartQuery); err != nil { + return err + } else { + newQueryPart.SetReferencedIdentifiers(referencedIdentifiers) + } + + s.query.AddPart(newQueryPart) return nil } func (s *Translator) prepareMultiPartQueryPart(multiPartQueryPart *cypher.MultiPartQueryPart) error { newQueryPart := NewQueryPart(len(multiPartQueryPart.ReadingClauses), len(multiPartQueryPart.UpdatingClauses)) + if referencedIdentifiers, err := collectReferencedIdentifiers(multiPartQueryPart); err != nil { + return err + } else { + newQueryPart.SetReferencedIdentifiers(referencedIdentifiers) + } + // All multipart query parts must be wrapped in a nested CTE if mpFrame, err := s.scope.PushFrame(); err != nil { return err diff --git a/cypher/models/pgsql/translate/references.go b/cypher/models/pgsql/translate/references.go new file mode 100644 index 00000000..a310017a --- /dev/null +++ b/cypher/models/pgsql/translate/references.go @@ -0,0 +1,156 @@ +package translate + +import ( + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/walk" +) + +// referencedIdentifierCollector gathers a conservative set of source-level +// identifiers that may be observed after MATCH translation. Traversal pruning +// uses this set to decide whether a pattern binding, node alias, or edge alias +// must remain projected out of a traversal CTE. +// +// MATCH pattern variables are declarations, not reads. Treating every +// declaration as a read forces path and edge composites to stay alive even when +// the query never references them. The collector therefore ignores variables +// while it is inside a MATCH pattern declaration and records only uses outside +// those declarations. +// +// There are two deliberate conservative cases: +// - pattern predicates such as `WHERE (s)-[r]->()` are expressions, so their +// variables are counted as reads; +// - a variable declared more than once inside MATCH patterns is marked live, +// because repeated declarations commonly represent reuse across pattern +// parts or MATCH clauses and may be needed for joins. +type referencedIdentifierCollector struct { + walk.VisitorHandler + + referencedIdentifiers *pgsql.IdentifierSet + matchPatternDeclarationRefs map[pgsql.Identifier]int + matchPatternDeclarations map[*cypher.PatternPart]struct{} + matchPatternDeclarationDepth int +} + +func newReferencedIdentifierCollector() *referencedIdentifierCollector { + return &referencedIdentifierCollector{ + VisitorHandler: walk.NewCancelableErrorHandler(), + referencedIdentifiers: pgsql.NewIdentifierSet(), + matchPatternDeclarationRefs: map[pgsql.Identifier]int{}, + matchPatternDeclarations: map[*cypher.PatternPart]struct{}{}, + } +} + +func (s *referencedIdentifierCollector) addVariable(variable *cypher.Variable) { + if variable != nil { + s.referencedIdentifiers.Add(pgsql.Identifier(variable.Symbol)) + } +} + +func (s *referencedIdentifierCollector) addMatchPatternDeclaration(variable *cypher.Variable) { + if variable != nil { + s.matchPatternDeclarationRefs[pgsql.Identifier(variable.Symbol)] += 1 + } +} + +func (s *referencedIdentifierCollector) collectRepeatedMatchPatternDeclarations() { + for identifier, numDeclarations := range s.matchPatternDeclarationRefs { + if numDeclarations > 1 { + s.referencedIdentifiers.Add(identifier) + } + } +} + +func (s *referencedIdentifierCollector) isMatchPatternDeclaration(patternPart *cypher.PatternPart) bool { + _, isDeclaration := s.matchPatternDeclarations[patternPart] + return isDeclaration +} + +func (s *referencedIdentifierCollector) Enter(node cypher.SyntaxNode) { + switch typedNode := node.(type) { + case *cypher.Match: + for _, patternPart := range typedNode.Pattern { + s.matchPatternDeclarations[patternPart] = struct{}{} + } + + case *cypher.PatternPart: + if s.isMatchPatternDeclaration(typedNode) { + s.addMatchPatternDeclaration(typedNode.Variable) + s.matchPatternDeclarationDepth += 1 + } else { + s.addVariable(typedNode.Variable) + } + + case *cypher.NodePattern: + if s.matchPatternDeclarationDepth == 0 { + s.addVariable(typedNode.Variable) + } else { + s.addMatchPatternDeclaration(typedNode.Variable) + } + + case *cypher.RelationshipPattern: + if s.matchPatternDeclarationDepth == 0 { + s.addVariable(typedNode.Variable) + } else { + s.addMatchPatternDeclaration(typedNode.Variable) + } + + case *cypher.Variable: + s.addVariable(typedNode) + } +} + +func (s *referencedIdentifierCollector) Visit(cypher.SyntaxNode) {} + +func (s *referencedIdentifierCollector) Exit(node cypher.SyntaxNode) { + if patternPart, isPatternPart := node.(*cypher.PatternPart); isPatternPart && s.isMatchPatternDeclaration(patternPart) { + s.matchPatternDeclarationDepth -= 1 + } +} + +// collectReferencedIdentifiers walks a query part before it is translated and +// returns the source identifiers that should be considered live for traversal +// projection pruning. It is intentionally not a full data-flow analysis; it is +// a cheap, conservative guard against pruning values that later query clauses +// can observe. +func collectReferencedIdentifiers(root cypher.SyntaxNode) (*pgsql.IdentifierSet, error) { + if root == nil { + return pgsql.NewIdentifierSet(), nil + } + + collector := newReferencedIdentifierCollector() + if err := walk.Cypher(root, collector); err != nil { + return collector.referencedIdentifiers, err + } + + collector.collectRepeatedMatchPatternDeclarations() + return collector.referencedIdentifiers, nil +} + +func (s *QueryPart) SetReferencedIdentifiers(referencedIdentifiers *pgsql.IdentifierSet) { + if referencedIdentifiers == nil { + s.referencedIdentifiers = pgsql.NewIdentifierSet() + } else { + s.referencedIdentifiers = referencedIdentifiers + } +} + +func (s *QueryPart) ReferencesSourceIdentifier(identifier pgsql.Identifier) bool { + if s == nil || s.referencedIdentifiers == nil { + return false + } + + return s.referencedIdentifiers.Contains(pgsql.Identifier(cypher.TokenLiteralAsterisk)) || s.referencedIdentifiers.Contains(identifier) +} + +func (s *QueryPart) ReferencesBinding(binding *BoundIdentifier) bool { + if binding == nil { + return false + } + + if binding.Alias.Set { + return s.ReferencesSourceIdentifier(binding.Alias.Value) + } + + return s.ReferencesSourceIdentifier(binding.Identifier) +} diff --git a/cypher/models/pgsql/translate/references_test.go b/cypher/models/pgsql/translate/references_test.go new file mode 100644 index 00000000..b9a71583 --- /dev/null +++ b/cypher/models/pgsql/translate/references_test.go @@ -0,0 +1,68 @@ +package translate + +import ( + "testing" + + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models" + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/stretchr/testify/require" +) + +func referencedIdentifiersForQuery(t *testing.T, cypherQuery string) *pgsql.IdentifierSet { + t.Helper() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) + require.NoError(t, err) + require.NotNil(t, regularQuery.SingleQuery) + require.NotNil(t, regularQuery.SingleQuery.SinglePartQuery) + + referencedIdentifiers, err := collectReferencedIdentifiers(regularQuery.SingleQuery.SinglePartQuery) + require.NoError(t, err) + + return referencedIdentifiers +} + +func TestCollectReferencedIdentifiersIgnoresPatternDeclarations(t *testing.T) { + referencedIdentifiers := referencedIdentifiersForQuery(t, "match p = ()-[r1]->()-[r2]->(e) return e") + + require.False(t, referencedIdentifiers.Contains("p")) + require.False(t, referencedIdentifiers.Contains("r1")) + require.False(t, referencedIdentifiers.Contains("r2")) + require.True(t, referencedIdentifiers.Contains("e")) +} + +func TestCollectReferencedIdentifiersIncludesUsedPathBinding(t *testing.T) { + referencedIdentifiers := referencedIdentifiersForQuery(t, "match p = ()-[r]->(e) return p") + + require.True(t, referencedIdentifiers.Contains("p")) + require.False(t, referencedIdentifiers.Contains("r")) + require.False(t, referencedIdentifiers.Contains("e")) +} + +func TestCollectReferencedIdentifiersIncludesPatternPredicateReferences(t *testing.T) { + referencedIdentifiers := referencedIdentifiersForQuery(t, "match (s)-[r]->() where (s)-[r {prop: 'a'}]->() return s") + + require.True(t, referencedIdentifiers.Contains("s")) + require.True(t, referencedIdentifiers.Contains("r")) +} + +func TestCollectReferencedIdentifiersIncludesRepeatedMatchPatternDeclarations(t *testing.T) { + referencedIdentifiers := referencedIdentifiersForQuery(t, "match (a)-->(b) match (b)-->(c) return c") + + require.False(t, referencedIdentifiers.Contains("a")) + require.True(t, referencedIdentifiers.Contains("b")) + require.True(t, referencedIdentifiers.Contains("c")) +} + +func TestQueryPartReferencesBindingForGreedyProjection(t *testing.T) { + queryPart := NewQueryPart(1, 0) + queryPart.SetReferencedIdentifiers(pgsql.AsIdentifierSet(pgsql.Identifier(cypher.TokenLiteralAsterisk))) + + binding := &BoundIdentifier{ + Alias: models.OptionalValue(pgsql.Identifier("r")), + } + + require.True(t, queryPart.ReferencesBinding(binding)) +} diff --git a/cypher/models/pgsql/translate/relationship.go b/cypher/models/pgsql/translate/relationship.go index eb6aee5d..bf7171e6 100644 --- a/cypher/models/pgsql/translate/relationship.go +++ b/cypher/models/pgsql/translate/relationship.go @@ -15,6 +15,8 @@ func (s *Translator) translateRelationshipPattern(relationshipPattern *cypher.Re if bindingResult, err := s.bindPatternExpression(relationshipPattern, pgsql.EdgeComposite); err != nil { return err + } else if currentQueryPart.isCreating { + return s.collectCreateEdgePattern(relationshipPattern, patternPart, bindingResult) } else { if err := s.translateRelationshipPatternToStep(bindingResult, patternPart, relationshipPattern); err != nil { return err @@ -56,6 +58,49 @@ func (s *Translator) translateRelationshipPattern(relationshipPattern *cypher.Re return nil } +func (s *Translator) collectCreateEdgePattern(relationshipPattern *cypher.RelationshipPattern, part *PatternPart, bindingResult BindingResult) error { + var ( + queryPart = s.query.CurrentPart() + numSteps = len(part.TraversalSteps) + ) + + if numSteps == 0 { + return fmt.Errorf("relationship pattern encountered before any left node in CREATE pattern") + } + + currentStep := part.TraversalSteps[numSteps-1] + if currentStep.Edge != nil { + // Multiple relationships in one CREATE pattern share the prior right node + // as the next left node, so start a continuation step before collecting + // the new edge. + part.TraversalSteps = append(part.TraversalSteps, &TraversalStep{ + LeftNode: currentStep.RightNode, + LeftNodeBound: currentStep.RightNodeBound, + }) + + currentStep = part.TraversalSteps[len(part.TraversalSteps)-1] + } + + currentStep.Edge = bindingResult.Binding + currentStep.Direction = relationshipPattern.Direction + + if part.PatternBinding != nil { + // Pattern bindings are materialized later from their dependencies; record + // the created edge even though the INSERT has not been built yet. + part.PatternBinding.DependOn(bindingResult.Binding) + } + + queryPart.mutations.EdgeCreations.Put(bindingResult.Binding.Identifier, &EdgeCreate{ + Binding: bindingResult.Binding, + Properties: queryPart.ConsumeProperties(), + Kinds: relationshipPattern.Kinds, + LeftNode: currentStep.LeftNode, + Direction: relationshipPattern.Direction, + }) + + return nil +} + func (s *Translator) translateRelationshipPatternToStep(bindingResult BindingResult, part *PatternPart, relationshipPattern *cypher.RelationshipPattern) error { var ( expansion *Expansion @@ -106,6 +151,7 @@ func (s *Translator) translateRelationshipPatternToStep(bindingResult BindingRes // Set the path binding in the expansion struct for easier referencing upstream expansion.PathBinding = expansionPathBinding + expansionPathBinding.DependOn(bindingResult.Binding) if part.PatternBinding != nil { // If there's a bound pattern track this expansion's path as a dependency of the diff --git a/cypher/models/pgsql/translate/selectivity.go b/cypher/models/pgsql/translate/selectivity.go index d7582602..c75b0a9f 100644 --- a/cypher/models/pgsql/translate/selectivity.go +++ b/cypher/models/pgsql/translate/selectivity.go @@ -51,6 +51,12 @@ const ( // sufficient to clear this bar; a bare AND connector (weight 5) or a range comparison on // an unindexed property (weight 10) is not. selectivityFlipThreshold = selectivityWeightNarrowSearch + + // selectivityBidirectionalAnchorThreshold is the minimum score each endpoint must carry + // before shortest-path translation starts a bidirectional search from both sides. This + // keeps broad label-only endpoints out of bidirectional BFS; a single kind predicate + // scores below this threshold, while a materially narrower property predicate can clear it. + selectivityBidirectionalAnchorThreshold = selectivityWeightNarrowSearch * 2 ) // knownNodePropertySelectivity is a hack to enable the selectivity measurement to take advantage of known property indexes diff --git a/cypher/models/pgsql/translate/tracking.go b/cypher/models/pgsql/translate/tracking.go index 9a946ca6..b775e753 100644 --- a/cypher/models/pgsql/translate/tracking.go +++ b/cypher/models/pgsql/translate/tracking.go @@ -60,6 +60,7 @@ type Frame struct { stashedVisible *pgsql.IdentifierSet Exported *pgsql.IdentifierSet stashedExported *pgsql.IdentifierSet + Synthetic bool } func (s *Frame) RestoreStashed() { diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 3b528cad..07199152 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -9,18 +9,25 @@ import ( "github.com/specterops/dawgs/graph" ) +// DefaultGraphID is the graph_id used by callers that do not have a specific +// graph target available (tests, tooling, and visualization passes that only +// exercise translation output). +const DefaultGraphID int32 = 0 + type Translator struct { walk.Visitor[cypher.SyntaxNode] ctx context.Context kindMapper *contextAwareKindMapper + graphID int32 translation Result treeTranslator *ExpressionTreeTranslator query *Query scope *Scope + unwindTargets map[*cypher.Variable]struct{} } -func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any) *Translator { +func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) *Translator { if parameters == nil { parameters = map[string]any{} } @@ -34,9 +41,11 @@ func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters }, ctx: ctx, kindMapper: ctxAwareKindMapper, + graphID: graphID, treeTranslator: NewExpressionTreeTranslator(ctxAwareKindMapper), query: &Query{}, scope: NewScope(), + unwindTargets: map[*cypher.Variable]struct{}{}, } } @@ -46,12 +55,27 @@ func (s *Translator) Enter(expression cypher.SyntaxNode) { *cypher.Comparison, *cypher.Skip, *cypher.Limit, cypher.Operator, *cypher.ArithmeticExpression, *cypher.NodePattern, *cypher.RelationshipPattern, *cypher.Remove, *cypher.Set, *cypher.ReadingClause, *cypher.UnaryAddOrSubtractExpression, *cypher.PropertyLookup, - *cypher.Negation, *cypher.Create, *cypher.Where, *cypher.ListLiteral, + *cypher.Negation, *cypher.Where, *cypher.ListLiteral, *cypher.FunctionInvocation, *cypher.Order, *cypher.RemoveItem, *cypher.SetItem, *cypher.MapItem, *cypher.UpdatingClause, *cypher.Delete, *cypher.With, *cypher.Return, *cypher.MultiPartQuery, *cypher.Properties, *cypher.KindMatcher, *cypher.Quantifier, *cypher.IDInCollection: + case *cypher.Unwind: + if typedExpression.Variable != nil { + // The UNWIND target is declared by the UNWIND clause itself, so later + // variable visits for the same syntax node must not resolve through + // the normal outer-scope lookup path. + s.unwindTargets[typedExpression.Variable] = struct{}{} + } + + case *cypher.Create: + // CREATE pattern nodes and relationships are collected first, then + // translated into mutation CTEs after the full pattern is known. + currentQueryPart := s.query.CurrentPart() + currentQueryPart.currentPattern = &Pattern{} + currentQueryPart.isCreating = true + case *cypher.MultiPartQueryPart: if err := s.prepareMultiPartQueryPart(typedExpression); err != nil { s.SetError(err) @@ -104,12 +128,18 @@ func (s *Translator) Enter(expression cypher.SyntaxNode) { s.treeTranslator.PushOperand(binding.Parameter) case *cypher.Variable: - identifier := pgsql.Identifier(typedExpression.Symbol) - - if binding, resolved := s.scope.AliasedLookup(identifier); !resolved { - s.SetErrorf("unable to resolve or otherwise lookup identifer %s", identifier) - } else { + if binding, isUnwindTarget, err := s.prepareUnwindTarget(typedExpression); err != nil { + s.SetError(err) + } else if isUnwindTarget { s.treeTranslator.PushOperand(binding.Identifier) + } else { + identifier := pgsql.Identifier(typedExpression.Symbol) + + if binding, resolved := s.scope.AliasedLookup(identifier); !resolved { + s.SetErrorf("unable to resolve or otherwise lookup identifer %s", identifier) + } else { + s.treeTranslator.PushOperand(binding.Identifier) + } } case *cypher.Literal: @@ -227,6 +257,11 @@ func (s *Translator) Exit(expression cypher.SyntaxNode) { s.SetError(err) } + case *cypher.Create: + if err := s.translateCreate(typedExpression); err != nil { + s.SetError(err) + } + case *cypher.SetItem: if err := s.translateSetItem(typedExpression); err != nil { s.SetError(err) @@ -414,6 +449,11 @@ func (s *Translator) Exit(expression cypher.SyntaxNode) { s.SetError(err) } + case *cypher.Unwind: + if err := s.translateUnwind(typedExpression); err != nil { + s.SetError(err) + } + case *cypher.With: if err := s.translateWith(); err != nil { s.SetError(err) @@ -443,8 +483,8 @@ type Result struct { Parameters map[string]any } -func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any) (Result, error) { - translator := NewTranslator(ctx, kindMapper, parameters) +func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) (Result, error) { + translator := NewTranslator(ctx, kindMapper, parameters, graphID) if err := walk.Cypher(cypherQuery, translator); err != nil { return Result{}, err diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 2776c79c..b46289bc 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -366,7 +366,7 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } } else if part.AllShortestPaths || part.ShortestPath { return fmt.Errorf("expected shortest path search to utilize variable expansion: ()-[*..]->()") - } else if err := s.translateTraversalPatternPartWithoutExpansion(idx == 0, traversalStep); err != nil { + } else if err := s.translateTraversalPatternPartWithoutExpansion(part, idx, traversalStep); err != nil { return err } } @@ -378,7 +378,64 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr return nil } -func (s *Translator) translateTraversalPatternPartWithoutExpansion(isFirstTraversalStep bool, traversalStep *TraversalStep) error { +func patternBindingDependsOn(queryPart *QueryPart, part *PatternPart, binding *BoundIdentifier) bool { + if queryPart == nil || part == nil || part.PatternBinding == nil || binding == nil { + return false + } + + if !queryPart.ReferencesBinding(part.PatternBinding) { + return false + } + + for _, dependency := range part.PatternBinding.Dependencies { + if dependency.Identifier == binding.Identifier { + return true + } + } + + return false +} + +func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepIndex int, binding *BoundIdentifier) bool { + if binding == nil { + return false + } + + // Keep aliases referenced by later clauses and bindings needed to materialize + // a referenced path pattern. Everything else can stay internal to this step. + if (binding.Alias.Set && queryPart.ReferencesBinding(binding)) || patternBindingDependsOn(queryPart, part, binding) { + return true + } + + if stepIndex+1 < len(part.TraversalSteps) { + // A multi-hop pattern needs the right node from this step as the next + // step's left node even when the user never projects it. + nextStep := part.TraversalSteps[stepIndex+1] + return nextStep.LeftNode != nil && nextStep.LeftNode.Identifier == binding.Identifier + } + + return false +} + +func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep) { + // Bound endpoints already exist in an outer frame. Only unexport unbound + // values that later clauses and continuation steps cannot observe. + if !traversalStep.LeftNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.LeftNode) { + traversalStep.Frame.Unexport(traversalStep.LeftNode.Identifier) + } + + if !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.Edge) { + traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + } + + if !traversalStep.RightNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.RightNode) { + traversalStep.Frame.Unexport(traversalStep.RightNode.Identifier) + } +} + +func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *PatternPart, stepIndex int, traversalStep *TraversalStep) error { + isFirstTraversalStep := stepIndex == 0 + if constraints, err := consumePatternConstraints(isFirstTraversalStep, nonRecursivePattern, traversalStep, s.treeTranslator); err != nil { return err } else { @@ -453,6 +510,8 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(isFirstTraver } } + pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep) + if boundProjections, err := buildVisibleProjections(s.scope); err != nil { return err } else { diff --git a/cypher/models/pgsql/translate/unwind.go b/cypher/models/pgsql/translate/unwind.go new file mode 100644 index 00000000..ef4cde5a --- /dev/null +++ b/cypher/models/pgsql/translate/unwind.go @@ -0,0 +1,78 @@ +package translate + +import ( + "fmt" + + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" +) + +func (s *Translator) prepareUnwindTarget(variable *cypher.Variable) (*BoundIdentifier, bool, error) { + if _, isUnwindTarget := s.unwindTargets[variable]; !isUnwindTarget { + return nil, false, nil + } + + delete(s.unwindTargets, variable) + + binding, err := s.scope.DefineNew(pgsql.UnsetDataType) + if err != nil { + return nil, true, err + } + + s.scope.Alias(pgsql.Identifier(variable.Symbol), binding) + return binding, true, nil +} + +func unwindFromClauses(clauses []UnwindClause) []pgsql.FromClause { + fromClauses := make([]pgsql.FromClause, 0, len(clauses)) + + for _, clause := range clauses { + fromClauses = append(fromClauses, pgsql.FromClause{ + Source: pgsql.AliasedExpression{ + Expression: pgsql.FunctionCall{ + Function: pgsql.FunctionUnnest, + Parameters: []pgsql.Expression{clause.Expression}, + }, + Alias: pgsql.AsOptionalIdentifier(clause.Binding.Identifier), + }, + }) + } + + return fromClauses +} + +func (s *Translator) translateUnwind(unwind *cypher.Unwind) error { + if variableIdentifier, err := s.treeTranslator.PopOperand(); err != nil { + return err + } else if arrayExpression, err := s.treeTranslator.PopOperand(); err != nil { + return err + } else if err := RewriteFrameBindings(s.scope, arrayExpression); err != nil { + return err + } else if identifier, isIdentifier := variableIdentifier.(pgsql.Identifier); !isIdentifier { + return fmt.Errorf("expected identifier for unwind variable but got %T", variableIdentifier) + } else if binding, isBound := s.scope.Lookup(identifier); !isBound { + return fmt.Errorf("unable to lookup unwind variable binding %s", identifier) + } else { + if s.scope.CurrentFrame() == nil { + // A leading UNWIND has no MATCH/CREATE frame to attach to, but + // later clauses still need a frame that can export the unwind binding. + frame, err := s.scope.PushFrame() + if err != nil { + return err + } + + frame.Synthetic = true + s.query.CurrentPart().Frame = frame + } + + s.scope.Declare(binding.Identifier) + s.scope.CurrentFrame().Export(binding.Identifier) + + s.query.CurrentPart().AddUnwindClause(UnwindClause{ + Expression: arrayExpression, + Binding: binding, + }) + } + + return nil +} diff --git a/cypher/models/pgsql/translate/with.go b/cypher/models/pgsql/translate/with.go index aacc6670..daaa0ca8 100644 --- a/cypher/models/pgsql/translate/with.go +++ b/cypher/models/pgsql/translate/with.go @@ -72,15 +72,24 @@ func (s *Translator) translateWith() error { if binding, isBound := s.scope.Lookup(typedSelectItem); !isBound { return fmt.Errorf("unable to lookup identifer %s for with statement", typedSelectItem) } else { + var selectItem pgsql.SelectItem + if binding.LastProjection != nil { + selectItem = pgsql.CompoundIdentifier{ + binding.LastProjection.Binding.Identifier, typedSelectItem, + } + } else { + // A WITH can project bindings introduced in the same query + // part before they have a materialized frame back-reference. + selectItem = typedSelectItem + } + // Track this projected item for scope pruning projectedItems.Add(binding.Identifier) // Create a new projection that maps the identifier currentPart.projections.Items[idx] = &Projection{ - SelectItem: pgsql.CompoundIdentifier{ - binding.LastProjection.Binding.Identifier, typedSelectItem, - }, - Alias: pgsql.AsOptionalIdentifier(binding.Identifier), + SelectItem: selectItem, + Alias: pgsql.AsOptionalIdentifier(binding.Identifier), } // Assign the frame to the binding's last projection backref diff --git a/cypher/models/pgsql/visualization/visualizer_test.go b/cypher/models/pgsql/visualization/visualizer_test.go index 08dc604a..ad2a8783 100644 --- a/cypher/models/pgsql/visualization/visualizer_test.go +++ b/cypher/models/pgsql/visualization/visualizer_test.go @@ -18,7 +18,7 @@ func TestGraphToPUMLDigraph(t *testing.T) { regularQuery, err := frontend.ParseCypher(frontend.NewContext(), "match (s), (e) where s.name = s.other + 1 / s.last return s") require.Nil(t, err) - translation, err := translate.Translate(context.Background(), regularQuery, kindMapper, nil) + translation, err := translate.Translate(context.Background(), regularQuery, kindMapper, nil, translate.DefaultGraphID) require.Nil(t, err) graph, err := SQLToDigraph(translation.Statement) diff --git a/cypher/models/walk/walk_pgsql.go b/cypher/models/walk/walk_pgsql.go index e37e19d1..3bfe2330 100644 --- a/cypher/models/walk/walk_pgsql.go +++ b/cypher/models/walk/walk_pgsql.go @@ -255,9 +255,21 @@ func newSQLWalkCursor(node pgsql.SyntaxNode) (*Cursor[pgsql.SyntaxNode], error) case pgsql.Join: return &Cursor[pgsql.SyntaxNode]{ Node: node, - Branches: []pgsql.SyntaxNode{typedNode.JoinOperator}, + Branches: []pgsql.SyntaxNode{typedNode.Table, typedNode.JoinOperator}, }, nil + case pgsql.LateralSubquery: + nextCursor := &Cursor[pgsql.SyntaxNode]{ + Node: node, + Branches: []pgsql.SyntaxNode{typedNode.Query}, + } + + if typedNode.Binding.Set { + nextCursor.AddBranches(typedNode.Binding.Value) + } + + return nextCursor, nil + case pgsql.JoinOperator: return &Cursor[pgsql.SyntaxNode]{ Node: node, diff --git a/cypher/models/walk/walk_test.go b/cypher/models/walk/walk_test.go index f55ebd31..4ccd6b2f 100644 --- a/cypher/models/walk/walk_test.go +++ b/cypher/models/walk/walk_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/specterops/dawgs/cypher/models/walk" "github.com/specterops/dawgs/cypher/frontend" @@ -30,3 +31,69 @@ func TestWalk(t *testing.T) { } } } + +func TestPgSQLWalkVisitsJoinTable(t *testing.T) { + query := pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{"outer_table", "id"}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{"outer_table"}, + Binding: pgsql.AsOptionalIdentifier("outer_table"), + }, + Joins: []pgsql.Join{{ + Table: pgsql.LateralSubquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{"inner_table", "id"}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{"inner_table"}, + Binding: pgsql.AsOptionalIdentifier("inner_table"), + }, + }}, + }, + }, + Binding: pgsql.AsOptionalIdentifier("inner_table"), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewLiteral(true, pgsql.Boolean), + }, + }}, + }}, + }, + } + + var ( + visitedLateralSubquery bool + visitedInnerProjection bool + visitedJoinConstraint bool + ) + + visitor := walk.NewSimpleVisitor[pgsql.SyntaxNode](func(node pgsql.SyntaxNode, _ walk.VisitorHandler) { + switch typedNode := node.(type) { + case pgsql.LateralSubquery: + visitedLateralSubquery = true + + case pgsql.CompoundIdentifier: + if typedNode.String() == "inner_table.id" { + visitedInnerProjection = true + } + + case pgsql.Literal: + if typedNode.Value == true { + visitedJoinConstraint = true + } + } + }) + + require.NoError(t, walk.PgSQL(query, visitor)) + require.True(t, visitedLateralSubquery) + require.True(t, visitedInnerProjection) + require.True(t, visitedJoinConstraint) +} diff --git a/drivers/pg/query.go b/drivers/pg/query.go index afa5559c..c06a8eed 100644 --- a/drivers/pg/query.go +++ b/drivers/pg/query.go @@ -9,25 +9,29 @@ import ( ) type liveQuery struct { - ctx context.Context - tx graph.Transaction - kindMapper KindMapper - queryBuilder *query.Builder + ctx context.Context + tx graph.Transaction + kindMapper KindMapper + graphIDResolver func() (int32, error) + queryBuilder *query.Builder } -func newLiveQuery(ctx context.Context, tx graph.Transaction, kindMapper KindMapper) liveQuery { +func newLiveQuery(ctx context.Context, tx graph.Transaction, kindMapper KindMapper, graphIDResolver func() (int32, error)) liveQuery { return liveQuery{ - ctx: ctx, - tx: tx, - kindMapper: kindMapper, - queryBuilder: query.NewBuilder(nil), + ctx: ctx, + tx: tx, + kindMapper: kindMapper, + graphIDResolver: graphIDResolver, + queryBuilder: query.NewBuilder(nil), } } func (s *liveQuery) runRegularQuery(allShortestPaths bool) graph.Result { if regularQuery, err := s.queryBuilder.Build(allShortestPaths); err != nil { return graph.NewErrorResult(err) - } else if translation, err := translate.FromCypher(s.ctx, regularQuery, s.kindMapper, false); err != nil { + } else if graphID, err := s.graphIDResolver(); err != nil { + return graph.NewErrorResult(err) + } else if translation, err := translate.FromCypher(s.ctx, regularQuery, s.kindMapper, false, graphID); err != nil { return graph.NewErrorResult(err) } else { return s.tx.Raw(translation.Statement, translation.Parameters) diff --git a/drivers/pg/query/sql/schema_down.sql b/drivers/pg/query/sql/schema_down.sql index d9919391..cc5180e2 100644 --- a/drivers/pg/query/sql/schema_down.sql +++ b/drivers/pg/query/sql/schema_down.sql @@ -16,6 +16,42 @@ drop function if exists index_utilization; drop function if exists _format_asp_where_clause; drop function if exists _format_asp_query; drop function if exists asp_harness; +drop function if exists create_traversal_filter_tables; +drop function if exists create_traversal_filter_tables(text, text, text); +drop function if exists create_traversal_filter_tables(text, text); +drop function if exists create_traversal_filter_tables(int8[], int8[]); +drop function if exists unidirectional_sp_harness(text, text, int4); +drop function if exists unidirectional_sp_harness(text, text, int4, int8); +drop function if exists unidirectional_sp_harness(text, text, int4, text, text); +drop function if exists unidirectional_sp_harness(text, text, int4, text, text, int8); +drop function if exists unidirectional_sp_harness(text, text, int4, int8[]); +drop function if exists unidirectional_sp_harness(text, text, int4, int8[], int8); +drop function if exists unidirectional_sp_harness(text, text, int4, int8[], int8[]); +drop function if exists unidirectional_sp_harness(text, text, int4, int8[], int8[], int8); +drop function if exists unidirectional_asp_harness(text, text, int4); +drop function if exists unidirectional_asp_harness(text, text, int4, text, text); +drop function if exists unidirectional_asp_harness(text, text, int4, int8[]); +drop function if exists unidirectional_asp_harness(text, text, int4, int8[], int8[]); +drop function if exists bidirectional_asp_harness(text, text, text, text, int4); +drop function if exists bidirectional_asp_harness(text, text, text, text, int4, text, text, text); +drop function if exists bidirectional_asp_harness(text, text, text, text, int4, text, text); +drop function if exists bidirectional_asp_harness(text, text, text, text, int4, int8[]); +drop function if exists bidirectional_asp_harness(text, text, text, text, int4, int8[], int8[]); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, int8); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, text, text, text, int8); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, text, text, text); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, text, text, int8); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, text, text); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, int8[], int8); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, int8[]); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, int8[], int8[], int8); +drop function if exists bidirectional_sp_harness(text, text, text, text, int4, int8[], int8[]); +drop function if exists _bidirectional_sp_harness(text, text, text, text, int4, text, text, text, int8[], int8[], int8, bool); +drop function if exists _bidirectional_sp_harness(text, text, text, text, int4, text, text, text, int8[], int8[], bool); +drop function if exists _bidirectional_sp_harness(text, text, text, text, int4, text, text, int8[], int8[], bool); +drop function if exists create_bidirectional_shortest_path_tables; +drop function if exists create_bidirectional_pair_pathspace_indexes; drop function if exists _all_shortest_paths; drop function if exists all_shortest_paths; drop function if exists traversal_step; @@ -24,6 +60,7 @@ drop function if exists _format_traversal_query; drop function if exists _format_traversal_initial_query; drop function if exists expand_traversal_step; drop function if exists traverse; +drop function if exists ordered_edges_to_path(nodeComposite, edgeComposite[], nodeComposite[]); drop function if exists edges_to_path; drop function if exists traverse_paths; diff --git a/drivers/pg/query/sql/schema_up.sql b/drivers/pg/query/sql/schema_up.sql index 1cf8a38d..1bc887d8 100644 --- a/drivers/pg/query/sql/schema_up.sql +++ b/drivers/pg/query/sql/schema_up.sql @@ -359,6 +359,100 @@ $$ parallel safe strict; +create or replace function public.ordered_edges_to_path(root nodeComposite, edges edgeComposite[], known_nodes nodeComposite[]) returns pathComposite as +$$ +with recursive edge_bounds(edge_count) as +( + select coalesce(array_length(edges, 1), 0) +), +path_walk(idx, current_node_id, node_ids, edge_ordinals, last_ordinal, direction) as +( + select 1::int4, + (root).id, + array [(root).id]::int8[], + array []::int8[], + case + when edge_bounds.edge_count > 0 and ((root).id = (edges[1]).start_id or (root).id = (edges[1]).end_id) then 0::int8 + when edge_bounds.edge_count > 0 and ((root).id = (edges[edge_bounds.edge_count]).start_id or (root).id = (edges[edge_bounds.edge_count]).end_id) then edge_bounds.edge_count::int8 + 1 + else 0::int8 + end as last_ordinal, + case + when edge_bounds.edge_count > 0 and + not ((root).id = (edges[1]).start_id or (root).id = (edges[1]).end_id) and + ((root).id = (edges[edge_bounds.edge_count]).start_id or (root).id = (edges[edge_bounds.edge_count]).end_id) then -1::int8 + else 1::int8 + end as direction + from edge_bounds + union all + select path_walk.idx + 1, + next_step.next_node_id, + path_walk.node_ids || next_step.next_node_id, + path_walk.edge_ordinals || next_step.ordinality, + next_step.ordinality, + path_walk.direction + from path_walk + cross join edge_bounds + cross join lateral + ( + select edge_item.input_ordinality as ordinality, + case + when path_walk.current_node_id = edge_item.start_id then edge_item.end_id + when path_walk.current_node_id = edge_item.end_id then edge_item.start_id + end as next_node_id + from unnest(edges) with ordinality as edge_item(id, start_id, end_id, kind_id, properties, input_ordinality) + where edge_item.input_ordinality != all (path_walk.edge_ordinals) + and ( + path_walk.current_node_id = edge_item.start_id or + path_walk.current_node_id = edge_item.end_id + ) + order by + case when edge_item.input_ordinality = path_walk.last_ordinal + path_walk.direction then 0 else 1 end, + case when path_walk.direction < 0 then -edge_item.input_ordinality else edge_item.input_ordinality end + limit 1 + ) next_step + where path_walk.idx <= edge_bounds.edge_count +), +final_walk as +( + select path_walk.node_ids, path_walk.edge_ordinals + from path_walk + order by path_walk.idx desc + limit 1 +) +select row ( + ( + select coalesce( + array_agg(coalesce(known_node.node, (n.id, n.kind_ids, n.properties)::nodeComposite) order by ordered_node.ordinality)::nodeComposite[], + array []::nodeComposite[] + ) + from final_walk + cross join lateral unnest(final_walk.node_ids) with ordinality as ordered_node(id, ordinality) + left join lateral + ( + select (candidate.id, candidate.kind_ids, candidate.properties)::nodeComposite as node + from unnest(known_nodes) as candidate(id, kind_ids, properties) + where candidate.id = ordered_node.id + limit 1 + ) known_node on true + left join node n on n.id = ordered_node.id and known_node.node is null + ), + ( + select coalesce( + array_agg((ordered_edge.id, ordered_edge.start_id, ordered_edge.end_id, ordered_edge.kind_id, ordered_edge.properties)::edgeComposite order by selected_edge.path_ordinality)::edgeComposite[], + array []::edgeComposite[] + ) + from final_walk + cross join lateral unnest(final_walk.edge_ordinals) with ordinality as selected_edge(edge_ordinality, path_ordinality) + join lateral unnest(edges) with ordinality as ordered_edge(id, start_id, end_id, kind_id, properties, input_ordinality) + on ordered_edge.input_ordinality = selected_edge.edge_ordinality + ) +)::pathComposite; +$$ + language sql + stable + parallel safe + strict; + create or replace function public.create_unidirectional_pathspace_tables() returns void as $$ @@ -387,12 +481,12 @@ begin ) on commit drop; create index forward_front_next_id_index on forward_front using btree (next_id); - create index forward_front_satisfied_index on forward_front using btree (satisfied); - create index forward_front_is_cycle_index on forward_front using btree (is_cycle); + create index forward_front_satisfied_index on forward_front using btree (root_id, next_id, depth) where satisfied; + create index forward_front_is_cycle_index on forward_front using btree (root_id, next_id) where is_cycle; create index next_front_next_id_index on next_front using btree (next_id); - create index next_front_satisfied_index on next_front using btree (satisfied); - create index next_front_is_cycle_index on next_front using btree (is_cycle); + create index next_front_satisfied_index on next_front using btree (root_id, next_id, depth) where satisfied; + create index next_front_is_cycle_index on next_front using btree (root_id, next_id) where is_cycle; end; $$ language plpgsql @@ -406,8 +500,9 @@ $$ begin create temporary table visited ( - id int8 not null, - primary key (id) + root_id int8 not null, + id int8 not null, + primary key (root_id, id) ) on commit drop; create temporary table paths @@ -417,17 +512,141 @@ begin depth int4 not null, satisfied bool, is_cycle bool not null, - path int8[] not null, - primary key (path) + path int8[] not null + ) on commit drop; + + create temporary table resolved_roots + ( + root_id int8 not null, + primary key (root_id) ) on commit drop; perform create_unidirectional_pathspace_tables(); + + create index forward_front_root_id_next_id_index on forward_front using btree (root_id, next_id); + create index next_front_root_id_next_id_index on next_front using btree (root_id, next_id); + create index paths_root_id_next_id_index on paths using btree (root_id, next_id); +end; +$$ + language plpgsql + volatile + strict; + +-- create_traversal_filter_tables materializes the root, terminal and pair filter sets into temporary tables that the +-- harness functions join against. The tables use `on commit drop`, so a single transaction can only host one harness +-- invocation that depends on these tables; concurrent or sequential expansions in the same transaction will conflict +-- on the temporary table names. +create or replace function public.create_traversal_filter_tables() + returns void as +$$ +begin + create temporary table if not exists traversal_root_filter + ( + id int8 not null, + primary key (id) + ) on commit drop; + + create temporary table if not exists traversal_terminal_filter + ( + id int8 not null, + primary key (id) + ) on commit drop; + + create temporary table if not exists traversal_pair_filter + ( + root_id int8 not null, + terminal_id int8 not null, + primary key (root_id, terminal_id) + ) on commit drop; + + create index if not exists traversal_pair_filter_terminal_id_root_id_index on traversal_pair_filter using btree (terminal_id, root_id); + + truncate table traversal_root_filter; + truncate table traversal_terminal_filter; + truncate table traversal_pair_filter; + + return; +end; +$$ + language plpgsql + volatile; + +create or replace function public.create_traversal_filter_tables(root_ids int8[], terminal_ids int8[]) + returns void as +$$ +begin + perform create_traversal_filter_tables(); + + insert into traversal_root_filter + select distinct root_id + from unnest(root_ids) as root_ids(root_id) + where root_id is not null + on conflict (id) do nothing; + + insert into traversal_terminal_filter + select distinct terminal_id + from unnest(terminal_ids) as terminal_ids(terminal_id) + where terminal_id is not null + on conflict (id) do nothing; + + analyze traversal_root_filter; + analyze traversal_terminal_filter; + + return; +end; +$$ + language plpgsql + volatile + strict; + +create or replace function public.create_traversal_filter_tables(root_filter text, terminal_filter text, pair_filter text) + returns void as +$$ +begin + perform create_traversal_filter_tables(); + + if length(pair_filter) > 0 then + execute pair_filter; + end if; + + if length(root_filter) > 0 then + execute root_filter; + elsif length(pair_filter) > 0 then + insert into traversal_root_filter + select distinct root_id + from traversal_pair_filter + on conflict (id) do nothing; + end if; + + if length(terminal_filter) > 0 then + execute terminal_filter; + elsif length(pair_filter) > 0 then + insert into traversal_terminal_filter + select distinct terminal_id + from traversal_pair_filter + on conflict (id) do nothing; + end if; + + analyze traversal_root_filter; + analyze traversal_terminal_filter; + analyze traversal_pair_filter; + + return; end; $$ language plpgsql volatile strict; +create or replace function public.create_traversal_filter_tables(root_filter text, terminal_filter text) + returns void as +$$ +select public.create_traversal_filter_tables(root_filter, terminal_filter, ''::text); +$$ + language sql + volatile + strict; + create or replace function public.create_bidirectional_pathspace_tables() returns void as $$ @@ -441,13 +660,51 @@ begin depth int4 not null, satisfied bool, is_cycle bool not null, - path int8[] not null, - primary key (path) + path int8[] not null ) on commit drop; create index backward_front_next_id_index on backward_front using btree (next_id); - create index backward_front_satisfied_index on backward_front using btree (satisfied); - create index backward_front_is_cycle_index on backward_front using btree (is_cycle); + create index backward_front_satisfied_index on backward_front using btree (root_id, next_id, depth) where satisfied; + create index backward_front_is_cycle_index on backward_front using btree (root_id, next_id) where is_cycle; +end; +$$ + language plpgsql + volatile + strict; + +create or replace function public.create_bidirectional_pair_pathspace_indexes() + returns void as +$$ +begin + create index forward_front_root_id_next_id_index on forward_front using btree (root_id, next_id); + create index backward_front_root_id_next_id_index on backward_front using btree (root_id, next_id); + create index next_front_root_id_next_id_index on next_front using btree (root_id, next_id); +end; +$$ + language plpgsql + volatile + strict; + +create or replace function public.create_bidirectional_shortest_path_tables() + returns void as +$$ +begin + create temporary table forward_visited + ( + root_id int8 not null, + id int8 not null, + primary key (root_id, id) + ) on commit drop; + + create temporary table backward_visited + ( + root_id int8 not null, + id int8 not null, + primary key (root_id, id) + ) on commit drop; + + perform create_bidirectional_pathspace_tables(); + perform create_bidirectional_pair_pathspace_indexes(); end; $$ language plpgsql @@ -467,11 +724,9 @@ begin truncate table next_front; - delete - from forward_front r - where r.is_cycle - or r.satisfied is null - or not r.satisfied and not exists(select 1 from edge e where e.end_id = r.next_id); + delete from forward_front r where r.is_cycle; + delete from forward_front r where r.satisfied is null; + delete from forward_front r where not r.satisfied and not exists(select 1 from edge e where e.start_id = r.next_id); return; end; @@ -493,11 +748,9 @@ begin truncate table next_front; - delete - from backward_front r - where r.is_cycle - or r.satisfied is null - or not r.satisfied and not exists(select 1 from edge e where e.start_id = r.next_id); + delete from backward_front r where r.is_cycle; + delete from backward_front r where r.satisfied is null; + delete from backward_front r where not r.satisfied and not exists(select 1 from edge e where e.end_id = r.next_id); return; end; @@ -506,7 +759,8 @@ $$ volatile strict; -create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4) +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_ids int8[], terminal_ids int8[], path_limit int8) returns table ( root_id int8, @@ -518,54 +772,90 @@ create or replace function public.unidirectional_sp_harness(forward_primer text, ) as $$ +#variable_conflict use_column declare - forward_front_depth int4 := 0; + forward_front_depth int4 := 0; + terminal_filter_count int8 := 0; + inserted_path_count int8 := 0; + paths_count int8 := 0; begin raise debug 'Shortest Path Harness starting'; -- Create all tables necessary to drive traversal perform create_unidirectional_shortest_path_tables(); + perform create_traversal_filter_tables(root_ids, terminal_ids); + select count(*) into terminal_filter_count from traversal_terminal_filter; - while forward_front_depth < max_depth and (forward_front_depth = 0 or exists(select 1 from forward_front)) + while forward_front_depth < max_depth and + (path_limit <= 0 or paths_count < path_limit) and + (forward_front_depth = 0 or exists(select 1 from forward_front)) loop if forward_front_depth = 0 then - execute forward_primer; + execute forward_primer using root_ids, terminal_ids; -- Insert all root nodes as visited - insert into visited (id) select distinct f.root_id from next_front f on conflict (id) do nothing; + insert into visited (root_id, id) select distinct f.root_id, f.root_id from next_front f on conflict on constraint visited_pkey do nothing; else - execute forward_recursive; + execute forward_recursive using root_ids, terminal_ids; end if; forward_front_depth = forward_front_depth + 1; -- Swap the next_front table into the forward_front -- Remove cycles and non-conformant satisfaction checks - delete from next_front f using visited v where f.is_cycle or f.satisfied is null or f.next_id = v.id; + delete from next_front f where f.is_cycle; + delete from next_front f where f.satisfied is null; + delete from next_front f using visited v where f.root_id = v.root_id and f.next_id = v.id; - raise debug 'Expansion step % - Available Root Paths % - Num satisfied: %', forward_front_depth, (select count(*) from next_front), (select count(*) from next_front p where p.satisfied); + raise debug 'Expansion step %', forward_front_depth; -- Insert new newly visited nodes into the visited table - insert into visited (id) select distinct on (f.next_id) f.next_id from next_front f on conflict (id) do nothing; + insert into visited (root_id, id) select distinct f.root_id, f.next_id from next_front f on conflict on constraint visited_pkey do nothing; -- Copy pathspace over into the next front truncate table forward_front; insert into forward_front - select distinct on (f.next_id) f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path - from next_front f; + select distinct on (f.root_id, f.next_id) f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from next_front f + order by f.root_id, f.next_id, f.depth; -- Copy newly satisfied paths into the path table - insert into paths - select f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path - from forward_front f - where f.satisfied; + if path_limit > 0 then + insert into paths + select f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from forward_front f + where f.satisfied + limit path_limit - paths_count; + else + insert into paths + select f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from forward_front f + where f.satisfied; + end if; + get diagnostics inserted_path_count = row_count; + paths_count = paths_count + inserted_path_count; + + if terminal_filter_count > 0 then + insert into resolved_roots (root_id) + select p.root_id + from paths p + group by p.root_id + having count(distinct p.next_id) >= terminal_filter_count + on conflict on constraint resolved_roots_pkey do nothing; + + delete from forward_front f using resolved_roots r where f.root_id = r.root_id; + end if; -- Empty the next front last to capture the next expansion truncate table next_front; end loop; - return query select * from paths; + if path_limit > 0 then + return query select * from paths limit path_limit; + else + return query select * from paths; + end if; -- This bare return is not an error. This closes this function's resultset, and the return above will -- be treated as a yield and continue execution once the result cursor is exhausted. @@ -576,7 +866,48 @@ $$ volatile strict; -create or replace function public.unidirectional_asp_harness(forward_primer text, forward_recursive text, max_depth int4) +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_ids int8[], terminal_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.unidirectional_sp_harness(forward_primer, forward_recursive, max_depth, root_ids, terminal_ids, 0::int8); +$$ + language sql + volatile + strict; + +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_ids int8[], path_limit int8) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.unidirectional_sp_harness(forward_primer, forward_recursive, max_depth, root_ids, array []::int8[], path_limit); +$$ + language sql + volatile + strict; + +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_filter text, terminal_filter text, path_limit int8) returns table ( root_id int8, @@ -588,50 +919,102 @@ create or replace function public.unidirectional_asp_harness(forward_primer text ) as $$ +#variable_conflict use_column declare - forward_front_depth int4 := 0; + forward_front_depth int4 := 0; + terminal_filter_count int8 := 0; + inserted_path_count int8 := 0; + paths_count int8 := 0; begin - raise debug 'unidirectional_asp_harness start'; + raise debug 'Shortest Path Harness starting'; - -- Defines two tables to represent pathspace of the recursive expansion - perform create_unidirectional_pathspace_tables(); + -- Create all tables necessary to drive traversal + perform create_unidirectional_shortest_path_tables(); + perform create_traversal_filter_tables(root_filter, terminal_filter); + select count(*) into terminal_filter_count from traversal_terminal_filter; - while forward_front_depth < max_depth and (forward_front_depth = 0 or exists(select 1 from forward_front)) + while forward_front_depth < max_depth and + (path_limit <= 0 or paths_count < path_limit) and + (forward_front_depth = 0 or exists(select 1 from forward_front)) loop - -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the - -- recursive expansion query if forward_front_depth = 0 then execute forward_primer; + + -- Insert all root nodes as visited + insert into visited (root_id, id) select distinct f.root_id, f.root_id from next_front f on conflict on constraint visited_pkey do nothing; else execute forward_recursive; end if; forward_front_depth = forward_front_depth + 1; - raise debug 'Expansion step % - Available Root Paths % - Num satisfied: %', forward_front_depth, (select count(*) from next_front), (select count(*) from next_front p where p.satisfied); + -- Swap the next_front table into the forward_front + -- Remove cycles and non-conformant satisfaction checks + delete from next_front f where f.is_cycle; + delete from next_front f where f.satisfied is null; + delete from next_front f using visited v where f.root_id = v.root_id and f.next_id = v.id; + + raise debug 'Expansion step %', forward_front_depth; - -- Check to see if the root front is satisfied - if exists(select 1 from next_front r where r.satisfied) then - -- Return all satisfied paths from the next front - return query select * from next_front r where r.satisfied; - exit; + -- Insert new newly visited nodes into the visited table + insert into visited (root_id, id) select distinct f.root_id, f.next_id from next_front f on conflict on constraint visited_pkey do nothing; + + -- Copy pathspace over into the next front + truncate table forward_front; + + insert into forward_front + select distinct on (f.root_id, f.next_id) f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from next_front f + order by f.root_id, f.next_id, f.depth; + + -- Copy newly satisfied paths into the path table + if path_limit > 0 then + insert into paths + select f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from forward_front f + where f.satisfied + limit path_limit - paths_count; + else + insert into paths + select f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from forward_front f + where f.satisfied; + end if; + get diagnostics inserted_path_count = row_count; + paths_count = paths_count + inserted_path_count; + + if terminal_filter_count > 0 then + insert into resolved_roots (root_id) + select p.root_id + from paths p + group by p.root_id + having count(distinct p.next_id) >= terminal_filter_count + on conflict on constraint resolved_roots_pkey do nothing; + + delete from forward_front f using resolved_roots r where f.root_id = r.root_id; end if; - -- Swap the next_front table into the forward_front - perform swap_forward_front(); + -- Empty the next front last to capture the next expansion + truncate table next_front; end loop; + if path_limit > 0 then + return query select * from paths limit path_limit; + else + return query select * from paths; + end if; + -- This bare return is not an error. This closes this function's resultset, and the return above will -- be treated as a yield and continue execution once the result cursor is exhausted. return; end; $$ - language plpgsql volatile - strict; + language plpgsql + volatile + strict; -create or replace function public.bidirectional_asp_harness(forward_primer text, forward_recursive text, - backward_primer text, - backward_recursive text, max_depth int4) +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_filter text, terminal_filter text) returns table ( root_id int8, @@ -643,76 +1026,322 @@ create or replace function public.bidirectional_asp_harness(forward_primer text, ) as $$ -declare - forward_front_depth int4 := 0; - backward_front_depth int4 := 0; -begin - raise debug 'bidirectional_asp_harness start'; - - -- Defines three tables to represent pathspace of the recursive expansion - perform create_bidirectional_pathspace_tables(); - - while forward_front_depth + backward_front_depth < max_depth and - (forward_front_depth = 0 or exists(select 1 from forward_front)) and - (backward_front_depth = 0 or exists(select 1 from backward_front)) - loop - -- Check to expand the smaller of the two frontiers, or if both are the same size prefer the forward frontier - if (select count(*) from forward_front) <= (select count(*) from backward_front) then - -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the - -- recursive expansion query - if forward_front_depth = 0 then - execute forward_primer; - else - execute forward_recursive; - end if; +select * +from public.unidirectional_sp_harness(forward_primer, forward_recursive, max_depth, root_filter, terminal_filter, 0::int8); +$$ + language sql + volatile + strict; - forward_front_depth = forward_front_depth + 1; +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.unidirectional_sp_harness(forward_primer, forward_recursive, max_depth, root_ids, array []::int8[]); +$$ + language sql + volatile + strict; - raise debug 'Forward expansion as step % - Available Root Paths % - Num satisfied: %', forward_front_depth + backward_front_depth, (select count(*) from next_front), (select count(*) from next_front p where p.satisfied); +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4, + path_limit int8) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.unidirectional_sp_harness(forward_primer, forward_recursive, max_depth, array []::int8[], array []::int8[], path_limit); +$$ + language sql + volatile + strict; - -- Check to see if the next frontier is satisfied - if exists(select 1 from next_front r where r.satisfied) then +create or replace function public.unidirectional_sp_harness(forward_primer text, forward_recursive text, max_depth int4) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.unidirectional_sp_harness(forward_primer, forward_recursive, max_depth, array []::int8[], array []::int8[]); +$$ + language sql + volatile + strict; + +create or replace function public.unidirectional_asp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_ids int8[], terminal_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +#variable_conflict use_column +declare + forward_front_depth int4 := 0; +begin + raise debug 'unidirectional_asp_harness start'; + + -- Defines two tables to represent pathspace of the recursive expansion + perform create_unidirectional_pathspace_tables(); + perform create_traversal_filter_tables(root_ids, terminal_ids); + + while forward_front_depth < max_depth and (forward_front_depth = 0 or exists(select 1 from forward_front)) + loop + -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the + -- recursive expansion query + if forward_front_depth = 0 then + execute forward_primer using root_ids, terminal_ids; + else + execute forward_recursive using root_ids, terminal_ids; + end if; + + forward_front_depth = forward_front_depth + 1; + + raise debug 'Expansion step %', forward_front_depth; + + -- Check to see if the root front is satisfied + if exists(select 1 from next_front r where r.satisfied) then + -- Return all satisfied paths from the next front + return query select * from next_front r where r.satisfied; + exit; + end if; + + -- Swap the next_front table into the forward_front + perform swap_forward_front(); + end loop; + + -- This bare return is not an error. This closes this function's resultset, and the return above will + -- be treated as a yield and continue execution once the result cursor is exhausted. + return; +end; +$$ + language plpgsql volatile + strict; + +create or replace function public.unidirectional_asp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_filter text, terminal_filter text) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +#variable_conflict use_column +declare + forward_front_depth int4 := 0; +begin + raise debug 'unidirectional_asp_harness start'; + + -- Defines two tables to represent pathspace of the recursive expansion + perform create_unidirectional_pathspace_tables(); + perform create_traversal_filter_tables(root_filter, terminal_filter); + + while forward_front_depth < max_depth and (forward_front_depth = 0 or exists(select 1 from forward_front)) + loop + -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the + -- recursive expansion query + if forward_front_depth = 0 then + execute forward_primer; + else + execute forward_recursive; + end if; + + forward_front_depth = forward_front_depth + 1; + + raise debug 'Expansion step %', forward_front_depth; + + -- Check to see if the root front is satisfied + if exists(select 1 from next_front r where r.satisfied) then + -- Return all satisfied paths from the next front + return query select * from next_front r where r.satisfied; + exit; + end if; + + -- Swap the next_front table into the forward_front + perform swap_forward_front(); + end loop; + + -- This bare return is not an error. This closes this function's resultset, and the return above will + -- be treated as a yield and continue execution once the result cursor is exhausted. + return; +end; +$$ + language plpgsql volatile + strict; + +create or replace function public.unidirectional_asp_harness(forward_primer text, forward_recursive text, max_depth int4, + root_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.unidirectional_asp_harness(forward_primer, forward_recursive, max_depth, root_ids, array []::int8[]); +$$ + language sql volatile + strict; + +create or replace function public.unidirectional_asp_harness(forward_primer text, forward_recursive text, max_depth int4) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.unidirectional_asp_harness(forward_primer, forward_recursive, max_depth, array []::int8[], array []::int8[]); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_asp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_ids int8[], terminal_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +#variable_conflict use_column +declare + forward_front_depth int4 := 0; + backward_front_depth int4 := 0; + forward_front_count int8 := 0; + backward_front_count int8 := 0; + next_front_count int8 := 0; + matched_count int8 := 0; +begin + raise debug 'bidirectional_asp_harness start'; + + -- Defines three tables to represent pathspace of the recursive expansion + perform create_bidirectional_pathspace_tables(); + perform create_traversal_filter_tables(root_ids, terminal_ids); + + while forward_front_depth + backward_front_depth < max_depth and + (forward_front_depth = 0 or forward_front_count > 0) and + (backward_front_depth = 0 or backward_front_count > 0) + loop + -- Check to expand the smaller of the two frontiers, or if both are the same size prefer the forward frontier + if forward_front_depth = 0 or (backward_front_depth > 0 and forward_front_count <= backward_front_count) then + -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the + -- recursive expansion query + if forward_front_depth = 0 then + execute forward_primer using root_ids, terminal_ids; + else + execute forward_recursive using root_ids, terminal_ids; + end if; + + get diagnostics next_front_count = row_count; + forward_front_depth = forward_front_depth + 1; + + raise debug 'Forward expansion as step % - Available Root Paths %', forward_front_depth + backward_front_depth, next_front_count; + + -- Check to see if the next frontier is satisfied + if exists(select 1 from next_front r where r.satisfied) then return query select * from next_front r where r.satisfied; exit; end if; -- Swap the next_front table into the forward_front perform swap_forward_front(); + forward_front_count = next_front_count; else -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the -- recursive expansion query if backward_front_depth = 0 then - execute backward_primer; + execute backward_primer using root_ids, terminal_ids; else - execute backward_recursive; + execute backward_recursive using root_ids, terminal_ids; end if; + get diagnostics next_front_count = row_count; backward_front_depth = backward_front_depth + 1; - raise debug 'Backward expansion as step % - Available Terminal Paths % - Num satisfied: %', forward_front_depth + backward_front_depth, (select count(*) from next_front), (select count(*) from next_front p where p.satisfied); + raise debug 'Backward expansion as step % - Available Terminal Paths %', forward_front_depth + backward_front_depth, next_front_count; -- Check to see if the next frontier is satisfied if exists(select 1 from next_front r where r.satisfied) then - return query select * from next_front r where r.satisfied; + return query select r.next_id, + r.root_id, + r.depth, + r.satisfied, + r.is_cycle, + r.path + from next_front r + where r.satisfied; exit; end if; -- Swap the next_front table into the backward_front perform swap_backward_front(); + backward_front_count = next_front_count; end if; - -- Check to see if the two frontiers meet somewhere in the middle - if exists(select 1 - from forward_front f - join backward_front b on f.next_id = b.next_id) then - -- Zip the path arrays together treating the matches as satisfied - return query select f.root_id, - b.root_id, - f.depth + b.depth, - true, - false, - f.path || b.path - from forward_front f - join backward_front b on f.next_id = b.next_id; + -- Zip the path arrays together treating midpoint matches as satisfied + return query select f.root_id, + b.root_id, + f.depth + b.depth, + true, + false, + f.path || b.path + from forward_front f + join backward_front b on f.next_id = b.next_id; + get diagnostics matched_count = row_count; + + if matched_count > 0 then exit; end if; end loop; @@ -724,3 +1353,937 @@ end; $$ language plpgsql volatile strict; + +create or replace function public.bidirectional_asp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_filter text, terminal_filter text, pair_filter text) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +#variable_conflict use_column +declare + forward_front_depth int4 := 0; + backward_front_depth int4 := 0; + forward_front_count int8 := 0; + backward_front_count int8 := 0; + next_front_count int8 := 0; + deleted_count int8 := 0; + use_pair_filter bool := length(pair_filter) > 0; + matched_count int8 := 0; +begin + raise debug 'bidirectional_asp_harness start'; + + -- Defines three tables to represent pathspace of the recursive expansion + perform create_bidirectional_pathspace_tables(); + perform create_traversal_filter_tables(root_filter, terminal_filter, pair_filter); + + if use_pair_filter then + perform create_bidirectional_pair_pathspace_indexes(); + end if; + + create temporary table unresolved_pairs + ( + root_id int8 not null, + terminal_id int8 not null, + primary key (root_id, terminal_id) + ) on commit drop; + + create index unresolved_pairs_terminal_id_root_id_index on unresolved_pairs using btree (terminal_id, root_id); + + create temporary table resolved_pair_depths + ( + root_id int8 not null, + terminal_id int8 not null, + depth int4 not null, + primary key (root_id, terminal_id) + ) on commit drop; + + create temporary table resolved_paths + ( + root_id int8 not null, + next_id int8 not null, + depth int4 not null, + satisfied bool, + is_cycle bool not null, + path int8[] not null + ) on commit drop; + + if use_pair_filter then + insert into unresolved_pairs (root_id, terminal_id) + select distinct root_id, terminal_id + from traversal_pair_filter + on conflict on constraint unresolved_pairs_pkey do nothing; + end if; + + while forward_front_depth + backward_front_depth < max_depth and + (not use_pair_filter or exists(select 1 from unresolved_pairs)) and + (forward_front_depth = 0 or forward_front_count > 0) and + (backward_front_depth = 0 or backward_front_count > 0) + loop + -- Check to expand the smaller of the two frontiers, or if both are the same size prefer the forward frontier + if forward_front_depth = 0 or (backward_front_depth > 0 and forward_front_count <= backward_front_count) then + -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the + -- recursive expansion query + if forward_front_depth = 0 then + execute forward_primer; + else + execute forward_recursive; + end if; + + get diagnostics next_front_count = row_count; + forward_front_depth = forward_front_depth + 1; + + raise debug 'Forward expansion as step % - Available Root Paths %', forward_front_depth + backward_front_depth, next_front_count; + + -- Check to see if the next frontier is satisfied + if exists(select 1 from next_front r where r.satisfied) then + if use_pair_filter then + with inserted_depths as ( + insert into resolved_pair_depths (root_id, terminal_id, depth) + select distinct r.root_id, r.next_id, r.depth + from next_front r + join unresolved_pairs p on p.root_id = r.root_id and p.terminal_id = r.next_id + where r.satisfied + on conflict on constraint resolved_pair_depths_pkey do nothing + returning root_id, terminal_id, depth + ) + insert + into resolved_paths (root_id, next_id, depth, satisfied, is_cycle, path) + select r.root_id, r.next_id, r.depth, r.satisfied, r.is_cycle, r.path + from next_front r + join inserted_depths p on p.root_id = r.root_id and + p.terminal_id = r.next_id and + p.depth = r.depth + where r.satisfied; + get diagnostics matched_count = row_count; + + if matched_count > 0 then + delete + from unresolved_pairs p + using resolved_pair_depths r + where p.root_id = r.root_id + and p.terminal_id = r.terminal_id; + + delete from next_front f where not exists(select 1 from unresolved_pairs p where p.root_id = f.root_id); + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + delete from backward_front b where not exists(select 1 from unresolved_pairs p where p.terminal_id = b.root_id); + get diagnostics deleted_count = row_count; + backward_front_count = backward_front_count - deleted_count; + end if; + else + return query select * from next_front r where r.satisfied; + exit; + end if; + end if; + + -- Swap the next_front table into the forward_front + perform swap_forward_front(); + forward_front_count = next_front_count; + else + -- If this is the first expansion of this frontier, perform the primer query - otherwise perform the + -- recursive expansion query + if backward_front_depth = 0 then + execute backward_primer; + else + execute backward_recursive; + end if; + + get diagnostics next_front_count = row_count; + backward_front_depth = backward_front_depth + 1; + raise debug 'Backward expansion as step % - Available Terminal Paths %', forward_front_depth + backward_front_depth, next_front_count; + + -- Check to see if the next frontier is satisfied + if exists(select 1 from next_front r where r.satisfied) then + if use_pair_filter then + with inserted_depths as ( + insert into resolved_pair_depths (root_id, terminal_id, depth) + select distinct r.next_id, r.root_id, r.depth + from next_front r + join unresolved_pairs p on p.root_id = r.next_id and p.terminal_id = r.root_id + where r.satisfied + on conflict on constraint resolved_pair_depths_pkey do nothing + returning root_id, terminal_id, depth + ) + insert + into resolved_paths (root_id, next_id, depth, satisfied, is_cycle, path) + select r.next_id, r.root_id, r.depth, r.satisfied, r.is_cycle, r.path + from next_front r + join inserted_depths p on p.root_id = r.next_id and + p.terminal_id = r.root_id and + p.depth = r.depth + where r.satisfied; + get diagnostics matched_count = row_count; + + if matched_count > 0 then + delete + from unresolved_pairs p + using resolved_pair_depths r + where p.root_id = r.root_id + and p.terminal_id = r.terminal_id; + + delete from next_front f where not exists(select 1 from unresolved_pairs p where p.terminal_id = f.root_id); + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + delete from forward_front f where not exists(select 1 from unresolved_pairs p where p.root_id = f.root_id); + get diagnostics deleted_count = row_count; + forward_front_count = forward_front_count - deleted_count; + end if; + else + return query select r.next_id, + r.root_id, + r.depth, + r.satisfied, + r.is_cycle, + r.path + from next_front r + where r.satisfied; + exit; + end if; + end if; + + -- Swap the next_front table into the backward_front + perform swap_backward_front(); + backward_front_count = next_front_count; + end if; + + -- Check to see if the two frontiers meet somewhere in the middle + if use_pair_filter then + -- Zip the path arrays together treating the matches as satisfied + with inserted_depths as ( + insert into resolved_pair_depths (root_id, terminal_id, depth) + select p.root_id, p.terminal_id, midpoint.depth + from unresolved_pairs p + join lateral ( + select f.depth + b.depth as depth + from forward_front f + join backward_front b on b.root_id = p.terminal_id and b.next_id = f.next_id + where f.root_id = p.root_id + order by f.depth + b.depth + limit 1 + ) midpoint on true + on conflict on constraint resolved_pair_depths_pkey do nothing + returning root_id, terminal_id, depth + ) + insert + into resolved_paths (root_id, next_id, depth, satisfied, is_cycle, path) + select p.root_id, + p.terminal_id, + p.depth, + true, + false, + midpoint.path + from inserted_depths p + join lateral ( + select f.path || b.path as path + from forward_front f + join backward_front b on b.root_id = p.terminal_id and b.next_id = f.next_id + where f.root_id = p.root_id + and f.depth + b.depth = p.depth + ) midpoint on true; + get diagnostics matched_count = row_count; + + if matched_count > 0 then + delete + from unresolved_pairs p + using resolved_pair_depths r + where p.root_id = r.root_id + and p.terminal_id = r.terminal_id; + + delete from forward_front f where not exists(select 1 from unresolved_pairs p where p.root_id = f.root_id); + get diagnostics deleted_count = row_count; + forward_front_count = forward_front_count - deleted_count; + + delete from backward_front b where not exists(select 1 from unresolved_pairs p where p.terminal_id = b.root_id); + get diagnostics deleted_count = row_count; + backward_front_count = backward_front_count - deleted_count; + end if; + else + -- Zip the path arrays together treating the matches as satisfied + return query select f.root_id, + b.root_id, + f.depth + b.depth, + true, + false, + f.path || b.path + from forward_front f + join backward_front b on f.next_id = b.next_id; + get diagnostics matched_count = row_count; + + if matched_count > 0 then + exit; + end if; + end if; + end loop; + + if use_pair_filter then + return query select * + from resolved_paths + order by root_id, next_id, depth; + end if; + + -- This bare return is not an error. This closes this function's result set and the return above will + -- be treated as a yield and continue execution once the results cursor is exhausted. + return; +end; +$$ + language plpgsql volatile + strict; + +create or replace function public.bidirectional_asp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_filter text, terminal_filter text) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_asp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_filter, terminal_filter, ''::text); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_asp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_asp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_ids, array []::int8[]); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_asp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_asp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, array []::int8[], array []::int8[]); +$$ + language sql volatile + strict; + +drop function if exists public._bidirectional_sp_harness(text, text, text, text, int4, text, text, int8[], int8[], bool); +drop function if exists public._bidirectional_sp_harness(text, text, text, text, int4, text, text, text, int8[], int8[], bool); +drop function if exists public._bidirectional_sp_harness(text, text, text, text, int4, text, text, text, int8[], int8[], int8, bool); + +-- _bidirectional_sp_harness implements the shortest-path bidirectional BFS in two control paths selected by +-- `use_array_parameters`: +-- * `use_array_parameters = true`: the primer/recursive queries reference $1/$2 parameter placeholders bound to +-- `root_ids` and `terminal_ids`; `root_filter`, `terminal_filter` and `pair_filter` are ignored. +-- * `use_array_parameters = false`: the primer/recursive queries are self-contained and `root_filter`, +-- `terminal_filter`, and `pair_filter` are executed (when non-empty) to materialize traversal filter tables that +-- the primer/recursive queries join against. +-- When `pair_filter` is non-empty (and `use_array_parameters` is false) the harness runs in pair-filter mode: each +-- (root, terminal) pair tracks its own resolution and pruning, allowing per-pair early termination. Otherwise it runs +-- in batch mode and emits the first satisfied frontier. +create or replace function public._bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_filter text, terminal_filter text, pair_filter text, + root_ids int8[], terminal_ids int8[], + path_limit int8, + use_array_parameters bool) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +#variable_conflict use_column +declare + forward_front_depth int4 := 0; + backward_front_depth int4 := 0; + forward_front_count int8 := 0; + backward_front_count int8 := 0; + next_front_count int8 := 0; + deleted_count int8 := 0; + use_pair_filter bool := not use_array_parameters and length(pair_filter) > 0; + matched_count int8 := 0; + resolved_pairs_count int8 := 0; +begin + raise debug 'bidirectional_sp_harness start'; + + perform create_bidirectional_shortest_path_tables(); + + if use_array_parameters then + perform create_traversal_filter_tables(root_ids, terminal_ids); + else + perform create_traversal_filter_tables(root_filter, terminal_filter, pair_filter); + end if; + + create temporary table unresolved_pairs + ( + root_id int8 not null, + terminal_id int8 not null, + primary key (root_id, terminal_id) + ) on commit drop; + + create index unresolved_pairs_terminal_id_root_id_index on unresolved_pairs using btree (terminal_id, root_id); + + create temporary table resolved_pairs + ( + root_id int8 not null, + next_id int8 not null, + depth int4 not null, + satisfied bool, + is_cycle bool not null, + path int8[] not null, + primary key (root_id, next_id) + ) on commit drop; + + if use_pair_filter then + insert into unresolved_pairs (root_id, terminal_id) + select distinct root_id, terminal_id + from traversal_pair_filter + on conflict on constraint unresolved_pairs_pkey do nothing; + end if; + + -- Pair-filter mode keeps expanding until each requested pair is resolved or + -- the limit is met. Batch mode returns from inside the loop as soon as the + -- current BFS depth produces results. + while forward_front_depth + backward_front_depth < max_depth and + (path_limit <= 0 or resolved_pairs_count < path_limit) and + (not use_pair_filter or exists(select 1 from unresolved_pairs)) and + (forward_front_depth = 0 or forward_front_count > 0) and + (backward_front_depth = 0 or backward_front_count > 0) + loop + if forward_front_depth = 0 or (backward_front_depth > 0 and forward_front_count <= backward_front_count) then + if forward_front_depth = 0 then + if use_array_parameters then + execute forward_primer using root_ids, terminal_ids; + else + execute forward_primer; + end if; + + get diagnostics next_front_count = row_count; + + insert into forward_visited (root_id, id) + select distinct f.root_id, f.root_id + from next_front f + on conflict on constraint forward_visited_pkey do nothing; + else + if use_array_parameters then + execute forward_recursive using root_ids, terminal_ids; + else + execute forward_recursive; + end if; + + get diagnostics next_front_count = row_count; + end if; + + forward_front_depth = forward_front_depth + 1; + + delete from next_front f where f.is_cycle; + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + delete from next_front f where f.satisfied is null; + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + delete from next_front f using forward_visited v where f.root_id = v.root_id and f.next_id = v.id; + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + raise debug 'Forward shortest expansion as step % - Available Root Paths %', forward_front_depth + backward_front_depth, next_front_count; + + truncate table forward_front; + + insert into forward_front + select distinct on (f.root_id, f.next_id) f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from next_front f + order by f.root_id, f.next_id, f.depth; + get diagnostics forward_front_count = row_count; + + truncate table next_front; + + insert into forward_visited (root_id, id) + select f.root_id, f.next_id + from forward_front f + on conflict on constraint forward_visited_pkey do nothing; + + if exists(select 1 from forward_front r where r.satisfied) then + if use_pair_filter then + -- A direct forward hit resolves only the requested pairs it satisfies. + -- Frontiers for completed roots/terminals are pruned below. + insert into resolved_pairs (root_id, next_id, depth, satisfied, is_cycle, path) + select distinct on (r.root_id, r.next_id) r.root_id, + r.next_id, + r.depth, + r.satisfied, + r.is_cycle, + r.path + from forward_front r + join unresolved_pairs p on p.root_id = r.root_id and p.terminal_id = r.next_id + where r.satisfied + order by r.root_id, r.next_id, r.depth + on conflict on constraint resolved_pairs_pkey do nothing; + get diagnostics matched_count = row_count; + resolved_pairs_count = resolved_pairs_count + matched_count; + + delete + from unresolved_pairs p + using resolved_pairs r + where p.root_id = r.root_id + and p.terminal_id = r.next_id; + + delete from forward_front f where not exists(select 1 from unresolved_pairs p where p.root_id = f.root_id); + get diagnostics deleted_count = row_count; + forward_front_count = forward_front_count - deleted_count; + + delete from backward_front b where not exists(select 1 from unresolved_pairs p where p.terminal_id = b.root_id); + get diagnostics deleted_count = row_count; + backward_front_count = backward_front_count - deleted_count; + else + -- Without pair tracking, the first satisfied frontier is the shortest + -- frontier for this batch, so return it immediately. + return query select distinct on (r.root_id, r.next_id) r.root_id, + r.next_id, + r.depth, + r.satisfied, + r.is_cycle, + r.path + from forward_front r + where r.satisfied + order by r.root_id, r.next_id, r.depth + limit case when path_limit > 0 then path_limit else null end; + exit; + end if; + end if; + else + if backward_front_depth = 0 then + if use_array_parameters then + execute backward_primer using root_ids, terminal_ids; + else + execute backward_primer; + end if; + + get diagnostics next_front_count = row_count; + + insert into backward_visited (root_id, id) + select distinct f.root_id, f.root_id + from next_front f + on conflict on constraint backward_visited_pkey do nothing; + else + if use_array_parameters then + execute backward_recursive using root_ids, terminal_ids; + else + execute backward_recursive; + end if; + + get diagnostics next_front_count = row_count; + end if; + + backward_front_depth = backward_front_depth + 1; + + delete from next_front f where f.is_cycle; + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + delete from next_front f where f.satisfied is null; + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + delete from next_front f using backward_visited v where f.root_id = v.root_id and f.next_id = v.id; + get diagnostics deleted_count = row_count; + next_front_count = next_front_count - deleted_count; + + raise debug 'Backward shortest expansion as step % - Available Terminal Paths %', forward_front_depth + backward_front_depth, next_front_count; + + truncate table backward_front; + + insert into backward_front + select distinct on (f.root_id, f.next_id) f.root_id, f.next_id, f.depth, f.satisfied, f.is_cycle, f.path + from next_front f + order by f.root_id, f.next_id, f.depth; + get diagnostics backward_front_count = row_count; + + truncate table next_front; + + insert into backward_visited (root_id, id) + select f.root_id, f.next_id + from backward_front f + on conflict on constraint backward_visited_pkey do nothing; + + if exists(select 1 from backward_front r where r.satisfied) then + if use_pair_filter then + -- Symmetric direct hit from the terminal side; swap root/terminal + -- columns back into the function's result shape. + insert into resolved_pairs (root_id, next_id, depth, satisfied, is_cycle, path) + select distinct on (r.next_id, r.root_id) r.next_id, + r.root_id, + r.depth, + r.satisfied, + r.is_cycle, + r.path + from backward_front r + join unresolved_pairs p on p.root_id = r.next_id and p.terminal_id = r.root_id + where r.satisfied + order by r.next_id, r.root_id, r.depth + on conflict on constraint resolved_pairs_pkey do nothing; + get diagnostics matched_count = row_count; + resolved_pairs_count = resolved_pairs_count + matched_count; + + delete + from unresolved_pairs p + using resolved_pairs r + where p.root_id = r.root_id + and p.terminal_id = r.next_id; + + delete from backward_front f where not exists(select 1 from unresolved_pairs p where p.terminal_id = f.root_id); + get diagnostics deleted_count = row_count; + backward_front_count = backward_front_count - deleted_count; + + delete from forward_front f where not exists(select 1 from unresolved_pairs p where p.root_id = f.root_id); + get diagnostics deleted_count = row_count; + forward_front_count = forward_front_count - deleted_count; + else + return query select distinct on (r.next_id, r.root_id) r.next_id, + r.root_id, + r.depth, + r.satisfied, + r.is_cycle, + r.path + from backward_front r + where r.satisfied + order by r.next_id, r.root_id, r.depth + limit case when path_limit > 0 then path_limit else null end; + exit; + end if; + end if; + end if; + + if use_pair_filter then + -- For unresolved pairs that meet in the middle, keep one shortest + -- stitched path per pair and leave already-resolved pairs untouched. + insert into resolved_pairs (root_id, next_id, depth, satisfied, is_cycle, path) + select p.root_id, + p.terminal_id, + midpoint.depth, + true, + false, + midpoint.path + from unresolved_pairs p + join lateral ( + select f.depth + b.depth as depth, + f.path || b.path as path + from forward_front f + join backward_front b on b.root_id = p.terminal_id and b.next_id = f.next_id + where f.root_id = p.root_id + order by f.depth + b.depth + limit 1 + ) midpoint on true + on conflict on constraint resolved_pairs_pkey do nothing; + get diagnostics matched_count = row_count; + resolved_pairs_count = resolved_pairs_count + matched_count; + + if matched_count > 0 then + delete + from unresolved_pairs p + using resolved_pairs r + where p.root_id = r.root_id + and p.terminal_id = r.next_id; + + delete from forward_front f where not exists(select 1 from unresolved_pairs p where p.root_id = f.root_id); + get diagnostics deleted_count = row_count; + forward_front_count = forward_front_count - deleted_count; + + delete from backward_front b where not exists(select 1 from unresolved_pairs p where p.terminal_id = b.root_id); + get diagnostics deleted_count = row_count; + backward_front_count = backward_front_count - deleted_count; + end if; + else + return query select distinct on (f.root_id, b.root_id) f.root_id, + b.root_id, + f.depth + b.depth, + true, + false, + f.path || b.path + from forward_front f + join backward_front b on f.next_id = b.next_id + order by f.root_id, b.root_id, f.depth + b.depth + limit case when path_limit > 0 then path_limit else null end; + get diagnostics matched_count = row_count; + + if matched_count > 0 then + exit; + end if; + end if; + end loop; + + if use_pair_filter then + -- Pair mode accumulates results during expansion so it can keep searching + -- for unresolved pairs after the first frontier-level success. + if path_limit > 0 then + return query select * + from resolved_pairs + order by root_id, next_id, depth + limit path_limit; + else + return query select * + from resolved_pairs + order by root_id, next_id, depth; + end if; + end if; + + return; +end; +$$ + language plpgsql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_ids int8[], terminal_ids int8[], path_limit int8) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public._bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, ''::text, ''::text, ''::text, root_ids, terminal_ids, path_limit, true); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_ids int8[], terminal_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_ids, terminal_ids, 0::int8); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_filter text, terminal_filter text, path_limit int8) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public._bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_filter, terminal_filter, ''::text, array []::int8[], array []::int8[], path_limit, false); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_filter text, terminal_filter text) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_filter, terminal_filter, 0::int8); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_filter text, terminal_filter text, pair_filter text, + path_limit int8) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public._bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_filter, terminal_filter, pair_filter, array []::int8[], array []::int8[], path_limit, false); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_filter text, terminal_filter text, pair_filter text) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_filter, terminal_filter, pair_filter, 0::int8); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_ids int8[], path_limit int8) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_ids, array []::int8[], path_limit); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, array []::int8[], array []::int8[]); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + path_limit int8) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, array []::int8[], array []::int8[], path_limit); +$$ + language sql volatile + strict; + +create or replace function public.bidirectional_sp_harness(forward_primer text, forward_recursive text, + backward_primer text, + backward_recursive text, max_depth int4, + root_ids int8[]) + returns table + ( + root_id int8, + next_id int8, + depth int4, + satisfied bool, + is_cycle bool, + path int8[] + ) +as +$$ +select * +from public.bidirectional_sp_harness(forward_primer, forward_recursive, backward_primer, backward_recursive, max_depth, root_ids, 0::int8); +$$ + language sql volatile + strict; diff --git a/drivers/pg/transaction.go b/drivers/pg/transaction.go index 03d45ce1..7bf4bbd7 100644 --- a/drivers/pg/transaction.go +++ b/drivers/pg/transaction.go @@ -117,6 +117,14 @@ func (s *transaction) getTargetGraph() (model.Graph, error) { return s.schemaManager.AssertGraph(s, s.targetSchema) } +func (s *transaction) targetGraphID() (int32, error) { + if graphTarget, err := s.getTargetGraph(); err != nil { + return 0, err + } else { + return graphTarget.ID, nil + } +} + func (s *transaction) CreateNode(properties *graph.Properties, kinds ...graph.Kind) (*graph.Node, error) { if graphTarget, err := s.getTargetGraph(); err != nil { return nil, err @@ -174,7 +182,7 @@ func (s *transaction) UpdateNode(node *graph.Node) error { func (s *transaction) Nodes() graph.NodeQuery { return &nodeQuery{ - liveQuery: newLiveQuery(s.ctx, s, s.schemaManager), + liveQuery: newLiveQuery(s.ctx, s, s.schemaManager, s.targetGraphID), } } @@ -252,7 +260,7 @@ func (s *transaction) UpdateRelationship(relationship *graph.Relationship) error func (s *transaction) Relationships() graph.RelationshipQuery { return &relationshipQuery{ - liveQuery: newLiveQuery(s.ctx, s, s.schemaManager), + liveQuery: newLiveQuery(s.ctx, s, s.schemaManager, s.targetGraphID), } } @@ -269,7 +277,9 @@ func (s *transaction) query(query string, parameters map[string]any) (pgx.Rows, func (s *transaction) Query(query string, parameters map[string]any) graph.Result { if parsedQuery, err := frontend.ParseCypher(frontend.NewContext(), query); err != nil { return graph.NewErrorResult(err) - } else if translated, err := translate.Translate(s.ctx, parsedQuery, s.schemaManager, parameters); err != nil { + } else if graphTarget, err := s.getTargetGraph(); err != nil { + return graph.NewErrorResult(err) + } else if translated, err := translate.Translate(s.ctx, parsedQuery, s.schemaManager, parameters, graphTarget.ID); err != nil { return graph.NewErrorResult(err) } else if sqlQuery, err := translate.Translated(translated); err != nil { return graph.NewErrorResult(err) diff --git a/integration/testdata/cases/create_inline.json b/integration/testdata/cases/create_inline.json new file mode 100644 index 00000000..7d336b02 --- /dev/null +++ b/integration/testdata/cases/create_inline.json @@ -0,0 +1,82 @@ +{ + "cases": [ + { + "name": "create a node with a kind label and string property", + "cypher": "CREATE (n:NodeKind1 {name: 'Bob'}) RETURN n", + "fixture": {"nodes": [], "edges": []}, + "assert": {"contains_node_with_prop": ["name", "Bob"]} + }, + { + "name": "create a node with a kind label and no return", + "cypher": "CREATE (n:NodeKind1)", + "fixture": {"nodes": [], "edges": []}, + "assert": "no_error" + }, + { + "name": "create an unlabeled node", + "cypher": "CREATE (n) RETURN n", + "fixture": {"nodes": [], "edges": []}, + "assert": "non_empty" + }, + { + "name": "create two typed nodes connected by a directed edge", + "cypher": "CREATE (a:NodeKind1)-[r:EdgeKind1]->(b:NodeKind2) RETURN r", + "fixture": {"nodes": [], "edges": []}, + "assert": "non_empty" + }, + { + "name": "create two typed nodes connected by a reverse edge", + "cypher": "CREATE (a:NodeKind1)<-[r:EdgeKind1]-(b:NodeKind2) RETURN r", + "fixture": {"nodes": [], "edges": []}, + "assert": "non_empty" + }, + { + "name": "match two rows then create one node per row", + "cypher": "MATCH (a:NodeKind1) WHERE a.tag = 'create_row_test' WITH a CREATE (b:NodeKind2 {source: a.name}) RETURN b", + "fixture": { + "nodes": [ + {"id": "a", "kinds": ["NodeKind1"], "properties": {"name": "source-a", "tag": "create_row_test"}}, + {"id": "b", "kinds": ["NodeKind1"], "properties": {"name": "source-b", "tag": "create_row_test"}} + ], + "edges": [] + }, + "assert": {"row_count": 2} + }, + { + "name": "carry a matched property into a created node", + "cypher": "MATCH (a:NodeKind1) WHERE a.name = 'source-a' WITH a CREATE (b:NodeKind2 {source: a.name}) RETURN b", + "fixture": { + "nodes": [ + {"id": "a", "kinds": ["NodeKind1"], "properties": {"name": "source-a"}}, + {"id": "b", "kinds": ["NodeKind1"], "properties": {"name": "source-b"}} + ], + "edges": [] + }, + "assert": {"contains_node_with_prop": ["source", "source-a"]} + }, + { + "name": "match two rows then create one edge per row", + "cypher": "MATCH (a:NodeKind1) WHERE a.tag = 'create_edge_row_test' WITH a CREATE (a)-[r:EdgeKind1]->(b:NodeKind2) RETURN r", + "fixture": { + "nodes": [ + {"id": "a", "kinds": ["NodeKind1"], "properties": {"name": "source-a", "tag": "create_edge_row_test"}}, + {"id": "b", "kinds": ["NodeKind1"], "properties": {"name": "source-b", "tag": "create_edge_row_test"}} + ], + "edges": [] + }, + "assert": {"row_count": 2} + }, + { + "name": "unwind values then create one node per value", + "cypher": "UNWIND ['a', 'b', 'c'] AS name CREATE (n:NodeKind1 {name: name}) RETURN n", + "fixture": {"nodes": [], "edges": []}, + "assert": {"row_count": 3} + }, + { + "name": "create a named path and return it", + "cypher": "CREATE p = (:NodeKind1 {name: 'abc'})-[:EdgeKind1 {prop: 123}]->(:NodeKind2 {name: 'test'}) RETURN p", + "fixture": {"nodes": [], "edges": []}, + "assert": "non_empty" + } + ] +} diff --git a/integration/testdata/cases/multipart_inline.json b/integration/testdata/cases/multipart_inline.json index 658fe292..ca93fa18 100644 --- a/integration/testdata/cases/multipart_inline.json +++ b/integration/testdata/cases/multipart_inline.json @@ -48,6 +48,19 @@ }, "assert": "non_empty" }, + { + "name": "preserve anchored optional-match rows when counting absent relationships", + "cypher": "match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g", + "fixture": { + "nodes": [ + {"id": "empty", "kinds": ["NodeKind1"], "properties": {"name": "no-members"}}, + {"id": "linked", "kinds": ["NodeKind1"], "properties": {"name": "has-member"}}, + {"id": "member", "kinds": ["NodeKind2"]} + ], + "edges": [{"start_id": "member", "end_id": "linked", "kind": "EdgeKind1"}] + }, + "assert": {"contains_node_with_prop": ["name", "no-members"]} + }, { "name": "filter typed nodes by a regular expression and carry collected results to the next stage", "cypher": "match (cg:NodeKind1) where cg.name =~ \".*TT\" with collect(cg.name) as names return names", @@ -75,6 +88,23 @@ ] }, "assert": "non_empty" + }, + { + "name": "correlate bound expansion results before counting reachable targets", + "cypher": "match (n:NodeKind1) where n.correlationcase = 'bound-expansion-count' match (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) with distinct n, count(c) as adminCount where n.name = 'alpha' return adminCount", + "fixture": { + "nodes": [ + {"id": "alpha", "kinds": ["NodeKind1"], "properties": {"name": "alpha", "correlationcase": "bound-expansion-count"}}, + {"id": "beta", "kinds": ["NodeKind1"], "properties": {"name": "beta", "correlationcase": "bound-expansion-count"}}, + {"id": "alpha_target", "kinds": ["NodeKind2"]}, + {"id": "beta_target", "kinds": ["NodeKind2"]} + ], + "edges": [ + {"start_id": "alpha", "end_id": "alpha_target", "kind": "EdgeKind1"}, + {"start_id": "beta", "end_id": "beta_target", "kind": "EdgeKind1"} + ] + }, + "assert": {"exact_int": 1} } ] } diff --git a/integration/testdata/cases/unwind_inline.json b/integration/testdata/cases/unwind_inline.json new file mode 100644 index 00000000..08644e55 --- /dev/null +++ b/integration/testdata/cases/unwind_inline.json @@ -0,0 +1,62 @@ +{ + "cases": [ + { + "name": "unwind integer array and return elements", + "cypher": "WITH [1, 2, 3] AS ids UNWIND ids AS x RETURN x", + "assert": {"row_count": 3} + }, + { + "name": "unwind with distinct removes duplicates", + "cypher": "WITH [1, 2, 3, 1, 2] AS ids UNWIND ids AS x RETURN DISTINCT x", + "assert": {"row_count": 3} + }, + { + "name": "unwind with count aggregation", + "cypher": "WITH [1, 2, 3] AS ids UNWIND ids AS x RETURN count(x)", + "assert": {"exact_int": 3} + }, + { + "name": "unwind with where filter", + "cypher": "WITH [1, 2, 3] AS ids UNWIND ids AS x WITH x WHERE x > 1 RETURN x", + "assert": {"row_count": 2} + }, + { + "name": "standalone unwind without preceding clause", + "cypher": "UNWIND [1, 2, 3] AS x RETURN x", + "assert": {"row_count": 3} + }, + { + "name": "unwind target can shadow source name", + "cypher": "WITH [1, 2, 3] AS x UNWIND x AS x RETURN x", + "skip_drivers": ["neo4j"], + "assert": {"row_count": 3} + }, + { + "name": "unwind collected node names and return them", + "cypher": "MATCH (n:NodeKind1) WHERE n.tag = 'unwind_test' WITH collect(n.name) AS names UNWIND names AS name RETURN name", + "fixture": { + "nodes": [ + {"id": "a", "kinds": ["NodeKind1"], "properties": {"name": "alpha", "tag": "unwind_test"}}, + {"id": "b", "kinds": ["NodeKind1"], "properties": {"name": "beta", "tag": "unwind_test"}}, + {"id": "c", "kinds": ["NodeKind1"], "properties": {"name": "gamma", "tag": "unwind_test"}} + ], + "edges": [] + }, + "assert": {"row_count": 3} + }, + { + "name": "unwind collected names then match against another kind", + "cypher": "MATCH (n:NodeKind1) WHERE n.tag = 'unwind_xk' WITH collect(n.name) AS names UNWIND names AS name MATCH (m:NodeKind2) WHERE m.name = name RETURN m", + "fixture": { + "nodes": [ + {"id": "a", "kinds": ["NodeKind1"], "properties": {"name": "shared", "tag": "unwind_xk"}}, + {"id": "b", "kinds": ["NodeKind1"], "properties": {"name": "only_kind1", "tag": "unwind_xk"}}, + {"id": "c", "kinds": ["NodeKind2"], "properties": {"name": "shared"}}, + {"id": "d", "kinds": ["NodeKind2"], "properties": {"name": "only_kind2"}} + ], + "edges": [] + }, + "assert": {"contains_node_with_prop": ["name", "shared"]} + } + ] +} diff --git a/opengraph/opengraph_integration_test.go b/opengraph/opengraph_integration_test.go index ada6f169..4790a42e 100644 --- a/opengraph/opengraph_integration_test.go +++ b/opengraph/opengraph_integration_test.go @@ -28,6 +28,7 @@ import ( "testing" "github.com/specterops/dawgs" + "github.com/specterops/dawgs/drivers" "github.com/specterops/dawgs/drivers/pg" "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/util/size" @@ -43,7 +44,7 @@ func setupTestDB(t *testing.T) (graph.Database, context.Context) { t.Skip("PG_CONNECTION_STRING not set") } - pool, err := pg.NewPool(connStr) + pool, err := pg.NewPool(drivers.DatabaseConfiguration{Connection: connStr}) if err != nil { t.Fatalf("Failed to create pool: %v", err) } diff --git a/tools/dawgrun/pkg/commands/cypher.go b/tools/dawgrun/pkg/commands/cypher.go index 0560f97b..6c47f0dd 100644 --- a/tools/dawgrun/pkg/commands/cypher.go +++ b/tools/dawgrun/pkg/commands/cypher.go @@ -13,6 +13,7 @@ import ( "github.com/kanmu/go-sqlfmt/sqlfmt" "github.com/specterops/dawgs/cypher/models/pgsql/format" "github.com/specterops/dawgs/cypher/models/pgsql/translate" + "github.com/specterops/dawgs/drivers/pg" "github.com/specterops/dawgs/graph" "golang.org/x/term" @@ -83,7 +84,7 @@ func translateToPsqlCmd() CommandDesc { kindMapper = stubs.MapperFromKindMap(kindMap) } - result, err := translate.Translate(ctx, query, kindMapper, nil) + result, err := translate.Translate(ctx, query, kindMapper, nil, defaultGraphID(ctx, kindMapperConnRef)) if err != nil { return fmt.Errorf("could not translate cypher query to pgsql: %w", err) } @@ -149,7 +150,7 @@ func explainAsPsqlCmd() CommandDesc { // Populate a DumbKindMapper from the database's kinds table kindMapper := stubs.MapperFromKindMap(kindMap) - result, err := translate.Translate(ctx, query, kindMapper, nil) + result, err := translate.Translate(ctx, query, kindMapper, nil, defaultGraphID(ctx, connName)) if err != nil { return fmt.Errorf("could not translate cypher query to pgsql: %w", err) } @@ -203,6 +204,28 @@ func explainAsPsqlCmd() CommandDesc { } } +func defaultGraphID(ctx *CommandContext, connName string) int32 { + if connName == "" { + return translate.DefaultGraphID + } + + conn, ok := ctx.scope.connections[connName] + if !ok { + return translate.DefaultGraphID + } + + driver, ok := conn.(*pg.Driver) + if !ok { + return translate.DefaultGraphID + } + + if defaultGraph, hasDefaultGraph := driver.DefaultGraph(); hasDefaultGraph { + return defaultGraph.ID + } + + return translate.DefaultGraphID +} + func queryCypherCmd() CommandDesc { flagSet := flag.NewFlagSet("query-cypher", flag.ContinueOnError)