diff --git a/cypher/models/cypher/functions.go b/cypher/models/cypher/functions.go index 704b372a..20daeade 100644 --- a/cypher/models/cypher/functions.go +++ b/cypher/models/cypher/functions.go @@ -18,6 +18,10 @@ const ( ToIntegerFunction = "toint" ToIntegerAliasFunction = "tointeger" ListSizeFunction = "size" + HeadFunction = "head" + TailFunction = "tail" + NodesFunction = "nodes" + RelationshipsFunction = "relationships" CoalesceFunction = "coalesce" CollectFunction = "collect" SumFunction = "sum" diff --git a/cypher/models/pgsql/format/format.go b/cypher/models/pgsql/format/format.go index 9606d02f..e44e399c 100644 --- a/cypher/models/pgsql/format/format.go +++ b/cypher/models/pgsql/format/format.go @@ -148,6 +148,48 @@ func formatLiteral(builder *OutputBuilder, literal pgsql.Literal) error { return formatValue(builder, literal.Value) } +func formatCase(builder *OutputBuilder, caseExpr pgsql.Case) error { + if len(caseExpr.Conditions) != len(caseExpr.Then) { + return fmt.Errorf("case expression has %d conditions and %d then expressions", len(caseExpr.Conditions), len(caseExpr.Then)) + } + + builder.Write("case") + + if caseExpr.Operand != nil { + builder.Write(" ") + + if err := formatNode(builder, caseExpr.Operand); err != nil { + return err + } + } + + for idx, condition := range caseExpr.Conditions { + builder.Write(" when ") + + if err := formatNode(builder, condition); err != nil { + return err + } + + builder.Write(" then ") + + if err := formatNode(builder, caseExpr.Then[idx]); err != nil { + return err + } + } + + if caseExpr.Else != nil { + builder.Write(" else ") + + if err := formatNode(builder, caseExpr.Else); err != nil { + return err + } + } + + builder.Write(" end") + + return nil +} + func formatNode(builder *OutputBuilder, rootExpr pgsql.SyntaxNode) error { exprStack := []pgsql.SyntaxNode{ rootExpr, @@ -184,6 +226,16 @@ func formatNode(builder *OutputBuilder, rootExpr pgsql.SyntaxNode) error { return err } + case *pgsql.Case: + if err := formatCase(builder, *typedNextExpr); err != nil { + return err + } + + case pgsql.Case: + if err := formatCase(builder, typedNextExpr); err != nil { + return err + } + case *pgsql.Materialized: if typedNextExpr.Materialized { exprStack = append(exprStack, pgsql.FormattingLiteral("materialized")) @@ -434,6 +486,25 @@ func formatNode(builder *OutputBuilder, rootExpr pgsql.SyntaxNode) error { case *pgsql.ArrayIndex: exprStack = append(exprStack, *typedNextExpr) + case pgsql.ArraySlice: + exprStack = append(exprStack, pgsql.FormattingLiteral("]")) + + if typedNextExpr.Upper != nil { + exprStack = append(exprStack, typedNextExpr.Upper) + } + + exprStack = append(exprStack, pgsql.FormattingLiteral(":")) + + if typedNextExpr.Lower != nil { + exprStack = append(exprStack, typedNextExpr.Lower) + } + + exprStack = append(exprStack, pgsql.FormattingLiteral("[")) + exprStack = append(exprStack, typedNextExpr.Expression) + + case *pgsql.ArraySlice: + exprStack = append(exprStack, *typedNextExpr) + case pgsql.TypeCast: switch typedCastedExpr := typedNextExpr.Expression.(type) { case *pgsql.BinaryExpression: diff --git a/cypher/models/pgsql/format/format_test.go b/cypher/models/pgsql/format/format_test.go index 3c63ccf9..86b9629d 100644 --- a/cypher/models/pgsql/format/format_test.go +++ b/cypher/models/pgsql/format/format_test.go @@ -26,6 +26,31 @@ func TestFormat_TypeCastedParenthetical(t *testing.T) { require.Equal(t, "('str')::text", formattedQuery) } +func TestFormat_Case(t *testing.T) { + formattedQuery, err := format.Expression(pgsql.Case{ + Conditions: []pgsql.Expression{ + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{"s0", "root_id"}, + pgsql.OperatorNotEquals, + pgsql.CompoundIdentifier{"s0", "next_id"}, + ), + }, + Then: []pgsql.Expression{ + pgsql.NewLiteral(true, pgsql.Boolean), + }, + Else: pgsql.FunctionCall{ + Function: "shortest_path_self_endpoint_error", + Parameters: []pgsql.Expression{ + pgsql.CompoundIdentifier{"s0", "root_id"}, + pgsql.CompoundIdentifier{"s0", "next_id"}, + }, + }, + }, format.NewOutputBuilder()) + + require.NoError(t, err) + require.Equal(t, "case when s0.root_id != s0.next_id then true else shortest_path_self_endpoint_error(s0.root_id, s0.next_id) end", formattedQuery) +} + func TestFormat_SelectDistinct(t *testing.T) { formattedQuery, err := format.Statement(pgsql.Query{ Body: pgsql.Select{ diff --git a/cypher/models/pgsql/functions.go b/cypher/models/pgsql/functions.go index f95dc2a9..89edb0f0 100644 --- a/cypher/models/pgsql/functions.go +++ b/cypher/models/pgsql/functions.go @@ -1,47 +1,48 @@ package pgsql 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" - FunctionJSONBArrayElementsText Identifier = "jsonb_array_elements_text" - FunctionJSONBBuildObject Identifier = "jsonb_build_object" - FunctionJSONBArrayLength Identifier = "jsonb_array_length" - FunctionToJSONB Identifier = "to_jsonb" - FunctionCypherContains Identifier = "cypher_contains" - FunctionCypherStartsWith Identifier = "cypher_starts_with" - FunctionCypherEndsWith Identifier = "cypher_ends_with" - FunctionArrayLength Identifier = "array_length" - FunctionCardinality Identifier = "cardinality" - FunctionArrayAggregate Identifier = "array_agg" - FunctionArrayRemove Identifier = "array_remove" - FunctionMin Identifier = "min" - FunctionMax Identifier = "max" - FunctionSum Identifier = "sum" - FunctionAvg Identifier = "avg" - FunctionLocalTimestamp Identifier = "localtimestamp" - FunctionLocalTime Identifier = "localtime" - FunctionCurrentTime Identifier = "current_time" - FunctionCurrentDate Identifier = "current_date" - FunctionNow Identifier = "now" - FunctionToLower Identifier = "lower" - FunctionToUpper Identifier = "upper" - FunctionCoalesce Identifier = "coalesce" - FunctionReplace Identifier = "replace" - 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" + FunctionUnidirectionalASPHarness Identifier = "unidirectional_asp_harness" + FunctionUnidirectionalSPHarness Identifier = "unidirectional_sp_harness" + FunctionBidirectionalASPHarness Identifier = "bidirectional_asp_harness" + FunctionBidirectionalSPHarness Identifier = "bidirectional_sp_harness" + FunctionShortestPathSelfEndpointError Identifier = "shortest_path_self_endpoint_error" + FunctionIntArrayUnique Identifier = "uniq" + FunctionIntArraySort Identifier = "sort" + FunctionJSONBToTextArray Identifier = "jsonb_to_text_array" + FunctionJSONBArrayElementsText Identifier = "jsonb_array_elements_text" + FunctionJSONBBuildObject Identifier = "jsonb_build_object" + FunctionJSONBArrayLength Identifier = "jsonb_array_length" + FunctionToJSONB Identifier = "to_jsonb" + FunctionCypherContains Identifier = "cypher_contains" + FunctionCypherStartsWith Identifier = "cypher_starts_with" + FunctionCypherEndsWith Identifier = "cypher_ends_with" + FunctionArrayLength Identifier = "array_length" + FunctionCardinality Identifier = "cardinality" + FunctionArrayAggregate Identifier = "array_agg" + FunctionArrayRemove Identifier = "array_remove" + FunctionMin Identifier = "min" + FunctionMax Identifier = "max" + FunctionSum Identifier = "sum" + FunctionAvg Identifier = "avg" + FunctionLocalTimestamp Identifier = "localtimestamp" + FunctionLocalTime Identifier = "localtime" + FunctionCurrentTime Identifier = "current_time" + FunctionCurrentDate Identifier = "current_date" + FunctionNow Identifier = "now" + FunctionToLower Identifier = "lower" + FunctionToUpper Identifier = "upper" + FunctionCoalesce Identifier = "coalesce" + FunctionReplace Identifier = "replace" + 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" ) func IsAggregateFunction(function Identifier) bool { diff --git a/cypher/models/pgsql/model.go b/cypher/models/pgsql/model.go index 1d21be17..268131c6 100644 --- a/cypher/models/pgsql/model.go +++ b/cypher/models/pgsql/model.go @@ -93,6 +93,18 @@ type Case struct { Else Expression } +func (s Case) NodeType() string { + return "case" +} + +func (s Case) AsExpression() Expression { + return s +} + +func (s Case) AsSelectItem() SelectItem { + return s +} + // InExpression represents a contains operation against a list of evaluated expressions: // m.identifier in (val1, val2, ...) type InExpression struct { @@ -145,6 +157,10 @@ func (s TypeCast) AsExpression() Expression { return s } +func (s TypeCast) AsSelectItem() SelectItem { + return s +} + func (s TypeCast) TypeHint() DataType { return s.CastType } @@ -617,6 +633,7 @@ func (s Identifier) Matches(others ...Identifier) bool { type ArrayIndex struct { Expression Expression Indexes []Expression + CastType DataType } func (s ArrayIndex) NodeType() string { @@ -627,6 +644,41 @@ func (s ArrayIndex) AsExpression() Expression { return s } +func (s ArrayIndex) TypeHint() DataType { + if s.CastType == UnsetDataType { + return UnknownDataType + } + + return s.CastType +} + +type ArraySlice struct { + Expression Expression + Lower Expression + Upper Expression + CastType DataType +} + +func (s ArraySlice) NodeType() string { + return "array_slice" +} + +func (s ArraySlice) AsExpression() Expression { + return s +} + +func (s ArraySlice) AsSelectItem() SelectItem { + return s +} + +func (s ArraySlice) TypeHint() DataType { + if s.CastType == UnsetDataType { + return UnknownDataType + } + + return s.CastType +} + type RowColumnReference struct { Identifier Expression Column Identifier diff --git a/cypher/models/pgsql/pgtypes.go b/cypher/models/pgsql/pgtypes.go index e0f8400f..70bad2a1 100644 --- a/cypher/models/pgsql/pgtypes.go +++ b/cypher/models/pgsql/pgtypes.go @@ -27,6 +27,8 @@ const ( ColumnGraphID Identifier = "graph_id" ColumnStartID Identifier = "start_id" ColumnEndID Identifier = "end_id" + ColumnNodes Identifier = "nodes" + ColumnEdges Identifier = "edges" ) var ( @@ -114,6 +116,24 @@ func (s DataType) IsKnown() bool { } } +func (s DataType) IsTemporalType() bool { + switch s { + case Date, TimeWithTimeZone, TimeWithoutTimeZone, TimestampWithTimeZone, TimestampWithoutTimeZone: + return true + + default: + return false + } +} + +func (s DataType) TemporalIntervalArithmeticResultType() DataType { + if s == Date { + return TimestampWithoutTimeZone + } + + return s +} + func (s DataType) IsComparable(other DataType, operator Operator) bool { switch operator { case OperatorPGArrayOverlap, OperatorArrayOverlap, OperatorPGArrayLHSContainsRHS: @@ -263,6 +283,22 @@ func (s DataType) OperatorResultType(other DataType, operator Operator) (DataTyp } // Other special cases for arithmetic + switch operator { + case OperatorAdd: + if s.IsTemporalType() && other == Interval { + return s.TemporalIntervalArithmeticResultType(), true + } + + if s == Interval && other.IsTemporalType() { + return other.TemporalIntervalArithmeticResultType(), true + } + + case OperatorSubtract: + if s.IsTemporalType() && other == Interval { + return s.TemporalIntervalArithmeticResultType(), true + } + } + switch s { case Date: switch other { diff --git a/cypher/models/pgsql/pytypes_test.go b/cypher/models/pgsql/pytypes_test.go index 0223d83d..1ef8d3df 100644 --- a/cypher/models/pgsql/pytypes_test.go +++ b/cypher/models/pgsql/pytypes_test.go @@ -236,6 +236,96 @@ func TestDataType_Comparable(t *testing.T) { } } +func TestDataType_OperatorResultTypeTemporalArithmetic(t *testing.T) { + testCases := []struct { + Name string + Left DataType + Operator Operator + Right DataType + Expected DataType + Valid bool + }{{ + Name: "timestamp with time zone minus interval", + Left: TimestampWithTimeZone, + Operator: OperatorSubtract, + Right: Interval, + Expected: TimestampWithTimeZone, + Valid: true, + }, { + Name: "timestamp with time zone plus interval", + Left: TimestampWithTimeZone, + Operator: OperatorAdd, + Right: Interval, + Expected: TimestampWithTimeZone, + Valid: true, + }, { + Name: "interval plus timestamp with time zone", + Left: Interval, + Operator: OperatorAdd, + Right: TimestampWithTimeZone, + Expected: TimestampWithTimeZone, + Valid: true, + }, { + Name: "timestamp without time zone minus interval", + Left: TimestampWithoutTimeZone, + Operator: OperatorSubtract, + Right: Interval, + Expected: TimestampWithoutTimeZone, + Valid: true, + }, { + Name: "time without time zone plus interval", + Left: TimeWithoutTimeZone, + Operator: OperatorAdd, + Right: Interval, + Expected: TimeWithoutTimeZone, + Valid: true, + }, { + Name: "date minus interval", + Left: Date, + Operator: OperatorSubtract, + Right: Interval, + Expected: TimestampWithoutTimeZone, + Valid: true, + }, { + Name: "date plus interval", + Left: Date, + Operator: OperatorAdd, + Right: Interval, + Expected: TimestampWithoutTimeZone, + Valid: true, + }, { + Name: "interval plus date", + Left: Interval, + Operator: OperatorAdd, + Right: Date, + Expected: TimestampWithoutTimeZone, + Valid: true, + }, { + Name: "timestamp with time zone multiplied by interval", + Left: TimestampWithTimeZone, + Operator: OperatorMultiply, + Right: Interval, + Valid: false, + }, { + Name: "interval minus timestamp with time zone", + Left: Interval, + Operator: OperatorSubtract, + Right: TimestampWithTimeZone, + Valid: false, + }} + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + resultType, valid := testCase.Left.OperatorResultType(testCase.Right, testCase.Operator) + require.Equal(t, testCase.Valid, valid) + + if testCase.Valid { + require.Equal(t, testCase.Expected, resultType) + } + }) + } +} + func TestValueToDataType(t *testing.T) { testCases := []struct { Value any diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index c3a2f7b3..cbe8c8e5 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -48,7 +48,7 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit with s0 as (select 'a' as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb((' ')::text)::jsonb and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool) 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_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; +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 s2_seed.root_id, s2_seed.root_id, 0, false, false, array []::int8[] from s2_seed union all 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 and s2.depth > 0) 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 (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); @@ -91,4 +91,3 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit -- 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 b1f040dc..308130f0 100644 --- a/cypher/models/pgsql/test/translation_cases/nodes.sql +++ b/cypher/models/pgsql/test/translation_cases/nodes.sql @@ -109,10 +109,10 @@ 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 ->> 'created_at'))::date = current_date::date)) select s0.n0 as s from s0; -- case: match (s) where s.created_at = date() - duration('P1D') return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'created_at'))::date = current_date::date - interval 'P1D')) 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 (((n0.properties ->> 'created_at'))::timestamp without time zone = current_date::date - interval 'P1D')) select s0.n0 as s from s0; -- case: match (s) where s.created_at = date() + duration('PT4H') return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'created_at'))::date = current_date::date + interval 'PT4H')) 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 (((n0.properties ->> 'created_at'))::timestamp without time zone = current_date::date + interval 'PT4H')) select s0.n0 as s from s0; -- case: match (s) where s.created_at = date('2023-4-4') return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'created_at'))::date = ('2023-4-4')::date)) select s0.n0 as s from s0; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index 53c54931..afa9861b 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -76,3 +76,10 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e -- 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_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 p=(:Computer)-[r:HasSession]->(:User) WHERE r.lastseen >= datetime() - duration('P3D') 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 [5]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [6]::int2[] and n1.id = e0.end_id where (((e0.properties ->> 'lastseen'))::timestamp with time zone >= now()::timestamp with time zone - interval 'P3D') and e0.kind_id = any (array [7]::int2[]) limit 100) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 100; + +-- case: MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE HEAD(r).enforced OR NONE(n in TAIL(TAIL(NODES(p))) WHERE (n:OU AND n.blocksinheritance)) RETURN p +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 [8]::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 [10]::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 [11, 12]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [10]::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 [11, 12]::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 where (((((s0.e0)[1]).properties ->> 'enforced'))::bool or ((select count(*)::int from unnest(coalesce((coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[]) as i0 where ((i0.kind_ids operator (pg_catalog.@>) array [9]::int2[] and ((i0.properties ->> 'blocksinheritance'))::bool))) = 0 and coalesce((coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[] is not null)::bool); + diff --git a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql index 847a93de..06878fc5 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -76,3 +76,9 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as -- 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_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; + +-- case: match (a:NodeKind1)-[:EdgeKind1*0..]->(b:NodeKind1) where a.name = 'solo' and b.name = 'solo' return a.name, b.name +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'))::jsonb = to_jsonb(('solo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select s1_seed.root_id, s1_seed.root_id, 0, (((n1.properties -> 'name'))::jsonb = to_jsonb(('solo')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, array []::int8[] from s1_seed join node n1 on n1.id = s1_seed.root_id union all select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('solo')::text)::jsonb) 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]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('solo')::text)::jsonb) 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]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle and s1.depth > 0) 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).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; + +-- case: match (a:NodeKind1)-[:EdgeKind1*0..]->(b:NodeKind1) where a.name = 'zero-source' and b.name = 'zero-target' return count(b) +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'))::jsonb = to_jsonb(('zero-source')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select s1_seed.root_id, s1_seed.root_id, 0, (((n1.properties -> 'name'))::jsonb = to_jsonb(('zero-target')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, array []::int8[] from s1_seed join node n1 on n1.id = s1_seed.root_id union all select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('zero-target')::text)::jsonb) 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]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('zero-target')::text)::jsonb) 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]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle and s1.depth > 0) 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 count(s0.n1)::int8 from s0; diff --git a/cypher/models/pgsql/test/translation_cases/quantifiers.sql b/cypher/models/pgsql/test/translation_cases/quantifiers.sql index 35f2bfa8..198db68f 100644 --- a/cypher/models/pgsql/test/translation_cases/quantifiers.sql +++ b/cypher/models/pgsql/test/translation_cases/quantifiers.sql @@ -37,6 +37,8 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit -- 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 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 and s2.i1 is not null)::bool); +-- case: WITH [1, 2] AS nums MATCH (n:NodeKind1) WHERE ANY(num IN nums + [3] WHERE num = 3) RETURN n +with s0 as (select array [1, 2]::int8[] as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (((select count(*)::int from unnest(s0.i0 || array [3]::int8[]) as i1 where (i1 = 3)) >= 1)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1; -- 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'))::jsonb = to_jsonb((true)::bool)::jsonb) 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'))) = cardinality(s2.i0))::bool); diff --git a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql index 772e20f2..e120b0ee 100644 --- a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql +++ b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql @@ -15,16 +15,16 @@ -- 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) 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 where 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id))::int8) = 1;","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 where (s1.root_id - s1.next_id) / (s1.root_id - s1.next_id) = 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::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 where case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end;","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 where case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end) 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) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties -\u003e 'name'))::jsonb = to_jsonb(('123')::text)::jsonb) 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 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id))::int8) = 1;","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 where (s1.root_id - s1.next_id) / (s1.root_id - s1.next_id) = 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::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 'name'))::jsonb = to_jsonb(('123')::text)::jsonb) 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 case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id) = 0 then true else shortest_path_self_endpoint_error(e0.end_id, e0.end_id) end;","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 where case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end) 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) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -\u003e 'name'))::jsonb = to_jsonb(('123')::text)::jsonb)) 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 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id))::int8) = 1;","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 where (s1.root_id - s1.next_id) / (s1.root_id - s1.next_id) = 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::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 'name'))::jsonb = to_jsonb(('123')::text)::jsonb)) 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 case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id) = 0 then true else shortest_path_self_endpoint_error(e0.end_id, e0.end_id) end;","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 where case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end) 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) 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);"} @@ -39,8 +39,8 @@ with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (sele 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) 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[]) and 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id))::int8) = 1;","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 where (s1.root_id - s1.next_id) / (s1.root_id - s1.next_id) = 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::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[]) and case when (select count(*)::int8 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end;","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 where case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end) 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) 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);"} @@ -51,8 +51,8 @@ with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (sele 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) 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[]) and 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id))::int8) = 1;","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 where (s1.root_id - s1.next_id) / (s1.root_id - s1.next_id) = 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::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[]) and case when (select count(*)::int8 from traversal_pair_filter where traversal_pair_filter.root_id = e0.end_id and traversal_pair_filter.terminal_id = e0.end_id) = 0 then true else shortest_path_self_endpoint_error(e0.end_id, e0.end_id) end;","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 where case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end) 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) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -\u003e 'name'))::jsonb = to_jsonb(('123')::text)::jsonb)) 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);"} @@ -63,21 +63,25 @@ with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (sele 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) 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 where 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e1.start_id))::int8) = 1;","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 and (s2.root_id - s2.next_id) / (s2.root_id - s2.next_id) = 1) 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; +-- 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 where case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e1.start_id) = 0 then true else shortest_path_self_endpoint_error(e1.start_id, e1.start_id) end;","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 and case when s2.root_id != s2.next_id then true else shortest_path_self_endpoint_error(s2.root_id, s2.next_id) end) 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[]) and 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id))::int8) = 1;","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 where (s1.root_id - s1.next_id) / (s1.root_id - s1.next_id) = 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +-- 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[]) and case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end;","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 where case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end) 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[]) and 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id))::int8) = 1;","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 where (s1.root_id - s1.next_id) / (s1.root_id - s1.next_id) = 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 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[]) and case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end;","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 where case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end) 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[]) and 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id))::int8) = 1;","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 and (s3.root_id - s3.next_id) / (s3.root_id - s3.next_id) = 1) select ordered_edges_to_path(s2.n0, s2.e0, array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +-- 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[]) and case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end;","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 and case when s3.root_id != s3.next_id then true else shortest_path_self_endpoint_error(s3.root_id, s3.next_id) end) 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[]) and 1 / (1 - least(1, (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id))::int8) = 1;","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 and (s3.root_id - s3.next_id) / (s3.root_id - s3.next_id) = 1) select ordered_edges_to_path(s2.n0, s2.e0, array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +-- 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[]) and case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end;","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 and case when s3.root_id != s3.next_id then true else shortest_path_self_endpoint_error(s3.root_id, s3.next_id) end) select ordered_edges_to_path(s2.n0, s2.e0, array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; + +-- case: match p=shortestPath((u:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)) with distinct g as Group, count(u) as UserCount return Group.name, UserCount order by UserCount desc limit 5 +-- 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 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 s2_seed join edge e0 on e0.start_id = s2_seed.root_id where e0.kind_id = any (array [3]::int2[]) and case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end;","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e0.end_id, s2.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.end_id), false, s2.path || e0.id from forward_front s2 join edge e0 on e0.start_id = s2.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from visited where visited.root_id = s2.root_id and visited.id = e0.end_id);"} +with s0 as (with s1 as (with s2(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)) 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 node n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where case when s2.root_id != s2.next_id then true else shortest_path_self_endpoint_error(s2.root_id, s2.next_id) end) select distinct s1.n1 as n2, count(s1.n0)::int8 as i0 from s1 group by n1) select ((s0.n2).properties -> 'name'), s0.i0 as UserCount from s0 order by s0.i0 desc limit 5; diff --git a/cypher/models/pgsql/test/translation_test.go b/cypher/models/pgsql/test/translation_test.go index 07c93d67..c9fb43d4 100644 --- a/cypher/models/pgsql/test/translation_test.go +++ b/cypher/models/pgsql/test/translation_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/specterops/dawgs/drivers/pg/pgutil" + "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/cypher/models/pgsql" ) @@ -20,6 +21,14 @@ func newKindMapper() pgsql.KindMapper { mapper.Put(NodeKind2) mapper.Put(EdgeKind1) mapper.Put(EdgeKind2) + mapper.Put(graph.StringKind("Computer")) + mapper.Put(graph.StringKind("User")) + mapper.Put(graph.StringKind("HasSession")) + mapper.Put(graph.StringKind("GPO")) + mapper.Put(graph.StringKind("OU")) + mapper.Put(graph.StringKind("Base")) + mapper.Put(graph.StringKind("GPLink")) + mapper.Put(graph.StringKind("Contains")) return mapper } diff --git a/cypher/models/pgsql/test/validation_integration_test.go b/cypher/models/pgsql/test/validation_integration_test.go index 5e9fd760..527734ff 100644 --- a/cypher/models/pgsql/test/validation_integration_test.go +++ b/cypher/models/pgsql/test/validation_integration_test.go @@ -342,6 +342,13 @@ func TestBidirectionalASPHarnessOverloads(t *testing.T) { require.Equal(t, 0, bidirectionalCount) }) + t.Run("shortest path self endpoint helper reports clear error", func(t *testing.T) { + var ok bool + err := pgxPool.QueryRow(testCtx, "select shortest_path_self_endpoint_error(1::int8, 1::int8)").Scan(&ok) + require.Error(t, err) + require.Contains(t, err.Error(), "shortest path endpoints must not resolve to the same node") + }) + t.Run("pair-aware shortest path harness resolves all explicit pairs", func(t *testing.T) { tx, err := pgxPool.Begin(testCtx) require.NoError(t, err) diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 3e878238..3acd4612 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -50,6 +50,7 @@ type ExpansionBuilder struct { PrimerStatement pgsql.Select RecursiveStatement pgsql.Select ProjectionStatement pgsql.Select + ZeroDepthStatement *pgsql.Select UseUnionAll bool queryParameters map[string]any @@ -432,6 +433,7 @@ func rewriteBoundEndpointSeedReference(expression pgsql.Expression, previousFram return pgsql.ArrayIndex{ Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), Indexes: indexes, + CastType: typedExpression.CastType, } case *pgsql.ArrayIndex: @@ -442,6 +444,30 @@ func rewriteBoundEndpointSeedReference(expression pgsql.Expression, previousFram rewritten := rewriteBoundEndpointSeedReference(*typedExpression, previousFrameIdentifier, nodeIdentifier).(pgsql.ArrayIndex) return &rewritten + case pgsql.ArraySlice: + var lower, upper pgsql.Expression + if typedExpression.Lower != nil { + lower = rewriteBoundEndpointSeedReference(typedExpression.Lower, previousFrameIdentifier, nodeIdentifier) + } + if typedExpression.Upper != nil { + upper = rewriteBoundEndpointSeedReference(typedExpression.Upper, previousFrameIdentifier, nodeIdentifier) + } + + return pgsql.ArraySlice{ + Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), + Lower: lower, + Upper: upper, + CastType: typedExpression.CastType, + } + + case *pgsql.ArraySlice: + if typedExpression == nil { + return nil + } + + rewritten := rewriteBoundEndpointSeedReference(*typedExpression, previousFrameIdentifier, nodeIdentifier).(pgsql.ArraySlice) + return &rewritten + case pgsql.AnyExpression: return pgsql.AnyExpression{ Expression: rewriteBoundEndpointSeedReference(typedExpression.Expression, previousFrameIdentifier, nodeIdentifier), @@ -529,6 +555,89 @@ func frontPrimerQuery(seed *expansionSeed, primer pgsql.Select) pgsql.Query { return seededFrontPrimerQuery(*seed, primer) } +func expansionAllowsZeroDepth(expansionModel *Expansion) bool { + return expansionModel.Options.MinDepth.Set && expansionModel.Options.MinDepth.Value == 0 +} + +func zeroDepthNodeJoin(nodeIdentifier pgsql.Identifier, nodeID pgsql.Expression) pgsql.Join { + return pgsql.Join{ + Table: expansionNodeTableReference(nodeIdentifier), + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgd.Equals(pgd.EntityID(nodeIdentifier), nodeID), + }, + } +} + +func zeroDepthTerminalSatisfaction(traversalStep *TraversalStep) pgsql.Expression { + localSatisfaction, _ := expansionTerminalSatisfactionLocality(traversalStep) + if localSatisfaction == nil { + return pgsql.NewLiteral(true, pgsql.Boolean) + } + + // Depth 0 has no relationship row in scope, so edge-dependent terminal + // satisfaction can only be met by a later recursive step. + if referencesIdentifier(localSatisfaction, traversalStep.Edge.Identifier) { + return pgsql.NewLiteral(false, pgsql.Boolean) + } + + return localSatisfaction +} + +func (s *ExpansionBuilder) buildZeroDepthExpansionSelect(seed *expansionSeed) (pgsql.Select, error) { + var ( + expansionModel = s.traversalStep.Expansion + rootIDExpression pgsql.Expression + fromClause pgsql.FromClause + satisfiedExpression pgsql.Expression = pgsql.NewLiteral(false, pgsql.Boolean) + ) + + if seed != nil { + rootIDExpression = seed.rootID() + fromClause = seed.fromClause() + } else { + rootIDExpression = pgd.EntityID(s.traversalStep.LeftNode.Identifier) + fromClause = pgsql.FromClause{ + Source: expansionNodeTableReference(s.traversalStep.LeftNode.Identifier), + } + } + + if expansionModel.TerminalNodeSatisfactionProjection != nil { + satisfiedExpression = zeroDepthTerminalSatisfaction(s.traversalStep) + + if seed != nil && referencesIdentifier(satisfiedExpression, s.traversalStep.LeftNode.Identifier) { + fromClause.Joins = append(fromClause.Joins, zeroDepthNodeJoin(s.traversalStep.LeftNode.Identifier, rootIDExpression)) + } + + if s.traversalStep.RightNode.Identifier != s.traversalStep.LeftNode.Identifier && + referencesIdentifier(satisfiedExpression, s.traversalStep.RightNode.Identifier) { + fromClause.Joins = append(fromClause.Joins, zeroDepthNodeJoin(s.traversalStep.RightNode.Identifier, rootIDExpression)) + } + } + + satisfiedSelectItem, err := pgsql.As[pgsql.SelectItem](satisfiedExpression) + if err != nil { + return pgsql.Select{}, err + } + + rootIDSelectItem, err := pgsql.As[pgsql.SelectItem](rootIDExpression) + if err != nil { + return pgsql.Select{}, err + } + + return pgsql.Select{ + Projection: []pgsql.SelectItem{ + rootIDSelectItem, + rootIDSelectItem, + pgsql.NewLiteral(0, pgsql.Int), + satisfiedSelectItem, + pgsql.NewLiteral(false, pgsql.Boolean), + pgsql.ArrayLiteral{CastType: pgsql.Int8Array}, + }, + From: []pgsql.FromClause{fromClause}, + }, nil +} + func (s *ExpansionBuilder) usesBoundRootIDs() bool { return s.traversalStep.LeftNodeBound && s.traversalStep.Frame != nil && s.traversalStep.Frame.Previous != nil } @@ -1000,7 +1109,7 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans if !expansionModel.HasExplicitEndpointInequality { nextQuery.Where = pgsql.OptionalAnd( nextQuery.Where, - shortestPathTerminalFilterSelfEndpointGuard(s.model.EdgeStartColumn), + shortestPathSeedSelfEndpointGuard(s.model.EdgeStartColumn, expansionModel.UseMaterializedEndpointPairFilter), ) } @@ -1342,27 +1451,40 @@ func (s *ExpansionBuilder) applyShortestPathSeedProjectionConstraints(projection // Match Neo4j's shortest-path behavior by surfacing an error for result rows // where the resolved root and terminal endpoints are the same node. func shortestPathSelfEndpointGuard(expansionFrame pgsql.Identifier) pgsql.Expression { - endpointDifference := func() pgsql.Expression { - return pgsql.NewParenthetical(pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{expansionFrame, expansionRootID}, - pgsql.OperatorSubtract, - pgsql.CompoundIdentifier{expansionFrame, expansionNextID}, - )) - } + rootID := pgsql.CompoundIdentifier{expansionFrame, expansionRootID} + terminalID := pgsql.CompoundIdentifier{expansionFrame, expansionNextID} - return pgsql.NewBinaryExpression( - pgsql.NewBinaryExpression( - endpointDifference(), - pgsql.OperatorDivide, - endpointDifference(), - ), - pgsql.OperatorEquals, - pgsql.NewLiteral(1, pgsql.Int), + return shortestPathSelfEndpointGuardCase(rootID, terminalID) +} + +func shortestPathSelfEndpointGuardCase(rootID, terminalID pgsql.Expression) pgsql.Expression { + return shortestPathSelfEndpointConditionGuard( + pgsql.NewBinaryExpression(rootID, pgsql.OperatorNotEquals, terminalID), + rootID, + terminalID, ) } -// PostgreSQL has no portable expression-level RAISE; the denominator becomes -// zero when the shortest-path root is also present in the terminal filter. +func shortestPathSelfEndpointConditionGuard(condition pgsql.Expression, rootID, terminalID pgsql.Expression) pgsql.Expression { + return &pgsql.Case{ + Conditions: []pgsql.Expression{ + condition, + }, + Then: []pgsql.Expression{ + pgsql.NewLiteral(true, pgsql.Boolean), + }, + Else: pgsql.FunctionCall{ + Function: pgsql.FunctionShortestPathSelfEndpointError, + Parameters: []pgsql.Expression{ + rootID, + terminalID, + }, + }, + } +} + +// PostgreSQL has no portable expression-level RAISE. Keep the normal path +// visible in generated SQL and call the schema helper only for the error path. func shortestPathTerminalFilterSelfEndpointGuard(rootID pgsql.Expression) pgsql.Expression { matchingTerminalCount := pgsql.Subquery{ Query: pgsql.Query{ @@ -1390,30 +1512,80 @@ func shortestPathTerminalFilterSelfEndpointGuard(rootID pgsql.Expression) pgsql. }, } - denominator := pgsql.NewParenthetical(pgsql.NewBinaryExpression( - pgsql.NewLiteral(1, pgsql.Int8), - pgsql.OperatorSubtract, - pgsql.FunctionCall{ - Function: pgsql.Identifier("least"), - Parameters: []pgsql.Expression{ - pgsql.NewLiteral(1, pgsql.Int8), + return &pgsql.Case{ + Conditions: []pgsql.Expression{ + pgsql.NewBinaryExpression( matchingTerminalCount, + pgsql.OperatorEquals, + pgsql.NewLiteral(0, pgsql.Int8), + ), + }, + Then: []pgsql.Expression{ + pgsql.NewLiteral(true, pgsql.Boolean), + }, + Else: pgsql.FunctionCall{ + Function: pgsql.FunctionShortestPathSelfEndpointError, + Parameters: []pgsql.Expression{ + rootID, + rootID, }, - CastType: pgsql.Int8, }, - )) + } +} - return pgsql.NewBinaryExpression( +func shortestPathEndpointPairFilterSelfEndpointGuard(rootID pgsql.Expression) pgsql.Expression { + matchingEndpointPairCount := pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.FunctionCall{ + Function: pgsql.FunctionCount, + Parameters: []pgsql.Expression{ + pgsql.Wildcard{}, + }, + CastType: pgsql.Int8, + }, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{expansionPairFilter}, + }, + }}, + Where: pgsql.OptionalAnd( + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{expansionPairFilter, expansionRootID}, + pgsql.OperatorEquals, + rootID, + ), + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{expansionPairFilter, expansionTerminalID}, + pgsql.OperatorEquals, + rootID, + ), + ), + }, + }, + } + + return shortestPathSelfEndpointConditionGuard( pgsql.NewBinaryExpression( - pgsql.NewLiteral(1, pgsql.Int8), - pgsql.OperatorDivide, - denominator, + matchingEndpointPairCount, + pgsql.OperatorEquals, + pgsql.NewLiteral(0, pgsql.Int8), ), - pgsql.OperatorEquals, - pgsql.NewLiteral(1, pgsql.Int8), + rootID, + rootID, ) } +func shortestPathSeedSelfEndpointGuard(rootID pgsql.Expression, useEndpointPairFilter bool) pgsql.Expression { + if useEndpointPairFilter { + return shortestPathEndpointPairFilterSelfEndpointGuard(rootID) + } + + return shortestPathTerminalFilterSelfEndpointGuard(rootID) +} + func (s *ExpansionBuilder) applyShortestPathSelfEndpointGuard(projectionQuery *pgsql.Select, expansionModel *Expansion) { if expansionModel.HasExplicitEndpointInequality { return @@ -1765,6 +1937,37 @@ func (s *ExpansionBuilder) bidirectionalAllShortestPathsParameters(expansionMode } func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier, commonTableExpressions ...pgsql.CommonTableExpression) pgsql.Query { + expansionBody := pgsql.SetExpression(pgsql.SetOperation{ + LOperand: s.PrimerStatement, + ROperand: s.RecursiveStatement, + Operator: pgsql.OperatorUnion, + All: s.UseUnionAll, + }) + + if s.ZeroDepthStatement != nil { + recursiveStatement := s.RecursiveStatement + recursiveStatement.Where = pgsql.OptionalAnd( + recursiveStatement.Where, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{expansionIdentifier, expansionDepth}, + pgsql.OperatorGreaterThan, + pgsql.NewLiteral(0, pgsql.Int), + ), + ) + + expansionBody = pgsql.SetOperation{ + LOperand: pgsql.SetOperation{ + LOperand: *s.ZeroDepthStatement, + ROperand: s.PrimerStatement, + Operator: pgsql.OperatorUnion, + All: s.UseUnionAll, + }, + ROperand: recursiveStatement, + Operator: pgsql.OperatorUnion, + All: s.UseUnionAll, + } + } + query := pgsql.Query{ CommonTableExpressions: &pgsql.With{ Recursive: true, @@ -1782,12 +1985,7 @@ func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier, commonTab Shape: expansionColumns(), }, Query: pgsql.Query{ - Body: pgsql.SetOperation{ - LOperand: s.PrimerStatement, - ROperand: s.RecursiveStatement, - Operator: pgsql.OperatorUnion, - All: s.UseUnionAll, - }, + Body: expansionBody, }, }) @@ -1874,6 +2072,15 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte expansion.PrimerStatement.From = append(expansion.PrimerStatement.From, nextQueryFrom) + if expansionAllowsZeroDepth(expansionModel) { + zeroDepthStatement, err := expansion.buildZeroDepthExpansionSelect(seed) + if err != nil { + return pgsql.Query{}, err + } + + expansion.ZeroDepthStatement = &zeroDepthStatement + } + // 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. @@ -1996,6 +2203,15 @@ func (s *Translator) buildExpansionPatternStep(traversalStepContext TraversalSte expansion.PrimerStatement.From = append(expansion.PrimerStatement.From, seed.fromClause(primerJoins...)) + if expansionAllowsZeroDepth(expansionModel) { + zeroDepthStatement, err := expansion.buildZeroDepthExpansionSelect(&seed) + if err != nil { + return pgsql.Query{}, err + } + + expansion.ZeroDepthStatement = &zeroDepthStatement + } + // 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. diff --git a/cypher/models/pgsql/translate/expansion_test.go b/cypher/models/pgsql/translate/expansion_test.go index 603468bc..9075d55e 100644 --- a/cypher/models/pgsql/translate/expansion_test.go +++ b/cypher/models/pgsql/translate/expansion_test.go @@ -1,6 +1,7 @@ package translate import ( + "strings" "testing" "github.com/specterops/dawgs/cypher/models/pgsql" @@ -91,6 +92,29 @@ func newShortestPathSeedTestBuilder(leftBound, rightBound bool) (*ExpansionBuild }, expansionModel } +func TestShortestPathSelfEndpointGuardsUseCaseErrorHelper(t *testing.T) { + projectionGuard, err := format.Expression(shortestPathSelfEndpointGuard(shortestPathSeedTestFrame), format.NewOutputBuilder()) + require.NoError(t, err) + require.Equal(t, "case when s1.root_id != s1.next_id then true else shortest_path_self_endpoint_error(s1.root_id, s1.next_id) end", projectionGuard) + require.NotContains(t, projectionGuard, " / ") + + terminalFilterGuard, err := format.Expression( + shortestPathSeedSelfEndpointGuard(pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnStartID}, false), + format.NewOutputBuilder(), + ) + require.NoError(t, err) + require.Contains(t, terminalFilterGuard, "case when (select count(*)::int8 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end") + require.NotContains(t, terminalFilterGuard, " / ") + + endpointPairFilterGuard, err := format.Expression( + shortestPathSeedSelfEndpointGuard(pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnStartID}, true), + format.NewOutputBuilder(), + ) + require.NoError(t, err) + require.Contains(t, endpointPairFilterGuard, "case when (select count(*)::int8 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.start_id) = 0 then true else shortest_path_self_endpoint_error(e0.start_id, e0.start_id) end") + require.NotContains(t, endpointPairFilterGuard, " / ") +} + func TestBoundRootShortestPathPrimerKeepsOnlySeedLocalConstraints(t *testing.T) { builder, expansionModel := newShortestPathSeedTestBuilder(true, false) expansionModel.PrimerNodeConstraints = pgsql.NewBinaryExpression( @@ -138,3 +162,93 @@ func TestBoundTerminalShortestPathPrimerKeepsOnlySeedLocalConstraints(t *testing require.Contains(t, formattedQuery, "n1.id = (s0.x).id") require.Contains(t, formattedQuery, "(s0.n1).id = s1.next_id") } + +func TestZeroDepthExpansionRejectsEdgeDependentTerminalSatisfaction(t *testing.T) { + builder, expansionModel := newShortestPathSeedTestBuilder(false, false) + seed := newExpansionNodeSeed(expansionSeedIdentifier(shortestPathSeedTestFrame), shortestPathSeedTestRoot, nil) + expansionModel.TerminalNodeSatisfactionProjection = pgsql.NewBinaryExpression( + pgsql.RowColumnReference{ + Identifier: &pgsql.ArrayIndex{ + Expression: pgsql.NewParenthetical(shortestPathSeedTestEdge), + Indexes: []pgsql.Expression{ + pgd.IntLiteral(1), + }, + CastType: pgsql.EdgeComposite, + }, + Column: pgsql.ColumnProperties, + }, + pgsql.OperatorJSONTextField, + pgd.TextLiteral("enforced"), + ) + + zeroDepthSelect, err := builder.buildZeroDepthExpansionSelect(&seed) + require.NoError(t, err) + + formattedQuery, err := format.Statement(pgsql.Query{Body: zeroDepthSelect}, format.NewOutputBuilder()) + require.NoError(t, err) + require.Contains(t, formattedQuery, "select s1_seed.root_id, s1_seed.root_id, 0, false, false") + require.NotContains(t, formattedQuery, "e0") +} + +func TestZeroDepthExpansionBuildKeepsPrimerBranch(t *testing.T) { + expansionSelect := func(root, next, depth int64, isCycle pgsql.SelectItem, edgeID int64) pgsql.Select { + return pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.NewLiteral(root, pgsql.Int8), + pgsql.NewLiteral(next, pgsql.Int8), + pgsql.NewLiteral(depth, pgsql.Int), + pgsql.NewLiteral(true, pgsql.Boolean), + isCycle, + pgsql.ArrayLiteral{ + Values: []pgsql.Expression{ + pgsql.NewLiteral(edgeID, pgsql.Int8), + }, + }, + }, + } + } + + zeroDepthStatement := pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.NewLiteral(int64(1), pgsql.Int8), + pgsql.NewLiteral(int64(1), pgsql.Int8), + pgsql.NewLiteral(int64(0), pgsql.Int), + pgsql.NewLiteral(true, pgsql.Boolean), + pgsql.NewLiteral(false, pgsql.Boolean), + pgsql.ArrayLiteral{CastType: pgsql.Int8Array}, + }, + } + + builder := ExpansionBuilder{ + PrimerStatement: expansionSelect( + 1, + 2, + 1, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnStartID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{shortestPathSeedTestEdge, pgsql.ColumnEndID}, + ), + 7, + ), + RecursiveStatement: expansionSelect(1, 3, 2, pgsql.NewLiteral(false, pgsql.Boolean), 8), + ProjectionStatement: pgsql.Select{Projection: []pgsql.SelectItem{pgsql.NewLiteral(1, pgsql.Int)}}, + ZeroDepthStatement: &zeroDepthStatement, + UseUnionAll: true, + } + + query := builder.Build(shortestPathSeedTestFrame) + formattedQuery, err := format.Statement(query, format.NewOutputBuilder()) + require.NoError(t, err) + + zeroDepthBranch := "select 1, 1, 0, true, false, array []::int8[]" + primerBranch := "select 1, 2, 1, true, e0.start_id = e0.end_id, array [7]" + recursiveBranch := "select 1, 3, 2, true, false, array [8]" + + require.Contains(t, formattedQuery, zeroDepthBranch) + require.Contains(t, formattedQuery, primerBranch) + require.Contains(t, formattedQuery, recursiveBranch) + require.Contains(t, formattedQuery, "where s1.depth > 0") + require.Less(t, strings.Index(formattedQuery, zeroDepthBranch), strings.Index(formattedQuery, primerBranch)) + require.Less(t, strings.Index(formattedQuery, primerBranch), strings.Index(formattedQuery, recursiveBranch)) +} diff --git a/cypher/models/pgsql/translate/expression.go b/cypher/models/pgsql/translate/expression.go index ca2bb4d8..cbf737cd 100644 --- a/cypher/models/pgsql/translate/expression.go +++ b/cypher/models/pgsql/translate/expression.go @@ -26,6 +26,16 @@ func unwrapParenthetical(parenthetical pgsql.Expression) pgsql.Expression { return parenthetical } +func expressionHasCompositeProperties(expressionType pgsql.DataType) bool { + switch expressionType { + case pgsql.NodeComposite, pgsql.EdgeComposite, pgsql.ExpansionRootNode, pgsql.ExpansionEdge, pgsql.ExpansionTerminalNode: + return true + + default: + return false + } +} + func (s *Translator) translatePropertyLookup(lookup *cypher.PropertyLookup) error { if translatedAtom, err := s.treeTranslator.PopOperand(); err != nil { return err @@ -43,6 +53,25 @@ func (s *Translator) translatePropertyLookup(lookup *cypher.PropertyLookup) erro } } + default: + if expressionType, err := s.inferExpressionType(translatedAtom); err != nil { + return err + } else if !expressionHasCompositeProperties(expressionType) { + return fmt.Errorf("unsupported property lookup %s on expression type %s", lookup.Symbol, expressionType) + } else if fieldIdentifierLiteral, err := pgsql.AsLiteral(lookup.Symbol); err != nil { + return err + } else { + s.treeTranslator.PushOperand(pgsql.RowColumnReference{ + Identifier: translatedAtom, + Column: pgsql.ColumnProperties, + }) + s.treeTranslator.PushOperand(fieldIdentifierLiteral) + + if err := s.treeTranslator.CompleteBinaryExpression(s.scope, pgsql.OperatorPropertyLookup); err != nil { + return err + } + } + case pgsql.FunctionCall: if fieldIdentifierLiteral, err := pgsql.AsLiteral(lookup.Symbol); err != nil { return err diff --git a/cypher/models/pgsql/translate/function.go b/cypher/models/pgsql/translate/function.go index 0e875250..bf7f9cbb 100644 --- a/cypher/models/pgsql/translate/function.go +++ b/cypher/models/pgsql/translate/function.go @@ -53,6 +53,186 @@ func GetAggregatedFunctionParameterSymbols(call pgsql.FunctionCall) (*pgsql.Symb return symbolTable, nil } +func bindingExpressionType(binding *BoundIdentifier) pgsql.DataType { + switch binding.DataType { + case pgsql.ExpansionEdge: + return pgsql.EdgeCompositeArray + + case pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode: + return pgsql.NodeComposite + + default: + return binding.DataType + } +} + +func inferRowColumnReferenceType(expression pgsql.RowColumnReference) pgsql.DataType { + switch expression.Column { + case pgsql.ColumnGraphID, pgsql.ColumnID, pgsql.ColumnStartID, pgsql.ColumnEndID: + return pgsql.Int8 + + case pgsql.ColumnKindID: + return pgsql.Int2 + + case pgsql.ColumnKindIDs: + return pgsql.Int2Array + + case pgsql.ColumnProperties: + return pgsql.JSONB + + case pgsql.ColumnNodes: + return pgsql.NodeCompositeArray + + case pgsql.ColumnEdges: + return pgsql.EdgeCompositeArray + + default: + return pgsql.UnknownDataType + } +} + +func (s *Translator) inferExpressionType(expression pgsql.Expression) (pgsql.DataType, error) { + switch typedExpression := unwrapParenthetical(expression).(type) { + case pgsql.Identifier: + if binding, bound := s.scope.Lookup(typedExpression); bound { + return bindingExpressionType(binding), nil + } + + if binding, bound := s.scope.AliasedLookup(typedExpression); bound { + return bindingExpressionType(binding), nil + } + + case pgsql.CompoundIdentifier: + if len(typedExpression) == 2 { + return inferRowColumnReferenceType(pgsql.RowColumnReference{ + Identifier: typedExpression[0], + Column: typedExpression[1], + }), nil + } + + case pgsql.RowColumnReference: + return inferRowColumnReferenceType(typedExpression), nil + } + + return InferExpressionType(expression) +} + +func (s *Translator) inferArrayExpressionType(expression pgsql.Expression) (pgsql.DataType, error) { + if expressionType, err := s.inferExpressionType(expression); err != nil { + return pgsql.UnsetDataType, err + } else if expressionType == pgsql.ExpansionEdge { + return pgsql.EdgeCompositeArray, nil + } else if !expressionType.IsArrayType() { + return pgsql.UnsetDataType, fmt.Errorf("expected array expression but received %s", expressionType) + } else { + return expressionType, nil + } +} + +func (s *Translator) expressionForPath(expression pgsql.Expression) (pgsql.Expression, error) { + switch typedExpression := unwrapParenthetical(expression).(type) { + case pgsql.Identifier: + binding, bound := s.scope.Lookup(typedExpression) + if !bound { + binding, bound = s.scope.AliasedLookup(typedExpression) + } + + if !bound { + return nil, fmt.Errorf("unable to resolve path identifier %s", typedExpression) + } else if binding.DataType != pgsql.PathComposite { + return nil, fmt.Errorf("expected path expression but received %s", binding.DataType) + } else { + return expressionForPathComposite(binding, s.scope) + } + + default: + if expressionType, err := s.inferExpressionType(expression); err != nil { + return nil, err + } else if expressionType != pgsql.PathComposite { + return nil, fmt.Errorf("expected path expression but received %s", expressionType) + } + + return expression, nil + } +} + +func (s *Translator) translateHeadFunction(functionInvocation *cypher.FunctionInvocation) error { + if functionInvocation.NumArguments() != 1 { + return fmt.Errorf("expected only one argument for cypher function: %s", functionInvocation.Name) + } + + if argument, err := s.treeTranslator.PopOperand(); err != nil { + return err + } else if arrayType, err := s.inferArrayExpressionType(argument); err != nil { + return err + } else { + s.treeTranslator.PushOperand(&pgsql.ArrayIndex{ + Expression: pgsql.NewParenthetical(argument), + Indexes: []pgsql.Expression{ + pgsql.NewLiteral(1, pgsql.Int), + }, + CastType: arrayType.ArrayBaseType(), + }) + } + + return nil +} + +func (s *Translator) translateTailFunction(functionInvocation *cypher.FunctionInvocation) error { + if functionInvocation.NumArguments() != 1 { + return fmt.Errorf("expected only one argument for cypher function: %s", functionInvocation.Name) + } + + if argument, err := s.treeTranslator.PopOperand(); err != nil { + return err + } else if arrayType, err := s.inferArrayExpressionType(argument); err != nil { + return err + } else { + s.treeTranslator.PushOperand(pgsql.FunctionCall{ + Function: pgsql.FunctionCoalesce, + Parameters: []pgsql.Expression{ + &pgsql.ArraySlice{ + Expression: pgsql.NewParenthetical(argument), + Lower: pgsql.NewLiteral(2, pgsql.Int), + Upper: pgsql.FunctionCall{ + Function: pgsql.FunctionCardinality, + Parameters: []pgsql.Expression{ + argument, + }, + CastType: pgsql.Int, + }, + CastType: arrayType, + }, + pgsql.ArrayLiteral{ + CastType: arrayType, + }, + }, + CastType: arrayType, + }) + } + + return nil +} + +func (s *Translator) translatePathComponentFunction(functionInvocation *cypher.FunctionInvocation, column pgsql.Identifier, castType pgsql.DataType) error { + if functionInvocation.NumArguments() != 1 { + return fmt.Errorf("expected only one argument for cypher function: %s", functionInvocation.Name) + } + + if argument, err := s.treeTranslator.PopOperand(); err != nil { + return err + } else if pathExpression, err := s.expressionForPath(argument); err != nil { + return err + } else { + s.treeTranslator.PushOperand(pgsql.NewTypeCast(pgsql.RowColumnReference{ + Identifier: pathExpression, + Column: column, + }, castType)) + } + + return nil +} + func (s *Translator) translateFunction(typedExpression *cypher.FunctionInvocation) { switch formattedName := strings.ToLower(typedExpression.Name); formattedName { case cypher.DurationFunction: @@ -215,6 +395,26 @@ func (s *Translator) translateFunction(typedExpression *cypher.FunctionInvocatio s.treeTranslator.PushOperand(functionCall) } + case cypher.HeadFunction: + if err := s.translateHeadFunction(typedExpression); err != nil { + s.SetError(err) + } + + case cypher.TailFunction: + if err := s.translateTailFunction(typedExpression); err != nil { + s.SetError(err) + } + + case cypher.NodesFunction: + if err := s.translatePathComponentFunction(typedExpression, pgsql.ColumnNodes, pgsql.NodeCompositeArray); err != nil { + s.SetError(err) + } + + case cypher.RelationshipsFunction: + if err := s.translatePathComponentFunction(typedExpression, pgsql.ColumnEdges, pgsql.EdgeCompositeArray); err != nil { + s.SetError(err) + } + case cypher.ToUpperFunction: if typedExpression.NumArguments() != 1 { s.SetError(fmt.Errorf("expected only one argument for cypher function: %s", typedExpression.Name)) diff --git a/cypher/models/pgsql/translate/function_test.go b/cypher/models/pgsql/translate/function_test.go new file mode 100644 index 00000000..7bd1ff02 --- /dev/null +++ b/cypher/models/pgsql/translate/function_test.go @@ -0,0 +1,25 @@ +package translate + +import ( + "context" + "testing" + + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/drivers/pg/pgutil" + "github.com/stretchr/testify/require" +) + +func TestPathComponentFunctionsResolvePathAliases(t *testing.T) { + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), `MATCH p = (a)-[r]->(b) WITH p AS q RETURN nodes(q), relationships(q)`) + require.NoError(t, err) + + translation, err := Translate(context.Background(), query, kindMapper, nil, DefaultGraphID) + require.NoError(t, err) + + formatted, err := Translated(translation) + require.NoError(t, err) + require.Contains(t, formatted, ".nodes") + require.Contains(t, formatted, ".edges") +} diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index 982bc0bd..271b7bb3 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -205,11 +205,26 @@ func referencesIdentifier(expression pgsql.Expression, identifier pgsql.Identifi _ = 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() + switch typedNode := node.(type) { + case pgsql.CompoundIdentifier: + if len(typedNode) > 0 && typedNode[0] == identifier { + references = true + handler.SetDone() + } + + case pgsql.Identifier: + if typedNode == identifier { + references = true + handler.SetDone() + } + + case pgsql.RowColumnReference: + if referencesIdentifier(typedNode.Identifier, identifier) { + references = true + handler.SetDone() + } else { + handler.Consume() + } } }, )) @@ -335,17 +350,32 @@ func flattenConjunction(expr pgsql.Expression) []pgsql.Expression { } } -// isLocalToScope returns true only when every compound-identifier root found -// in the expression is a member of localScope. -func isLocalToScope(expression pgsql.Expression, localScope *pgsql.IdentifierSet) bool { +// expressionReferencesOnlyLocalIdentifiers returns true only when every binding +// reference found in the expression is a member of localScope. +func expressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression, localScope *pgsql.IdentifierSet) bool { isLocal := true walk.PgSQL(expression, walk.NewSimpleVisitor[pgsql.SyntaxNode]( func(node pgsql.SyntaxNode, handler walk.VisitorHandler) { - if ci, ok := node.(pgsql.CompoundIdentifier); ok && len(ci) > 0 { - if !localScope.Contains(ci[0]) { + switch typedNode := node.(type) { + case pgsql.CompoundIdentifier: + if len(typedNode) > 0 && !localScope.Contains(typedNode[0]) { + isLocal = false + handler.SetDone() + } + + case pgsql.Identifier: + if !localScope.Contains(typedNode) { + isLocal = false + handler.SetDone() + } + + case pgsql.RowColumnReference: + if !expressionReferencesOnlyLocalIdentifiers(typedNode.Identifier, localScope) { isLocal = false handler.SetDone() + } else { + handler.Consume() } } }, @@ -354,10 +384,18 @@ func isLocalToScope(expression pgsql.Expression, localScope *pgsql.IdentifierSet return isLocal } +func isLocalToScope(expression pgsql.Expression, localScope *pgsql.IdentifierSet) bool { + if expression == nil { + return true + } + + return expressionReferencesOnlyLocalIdentifiers(expression, localScope) +} + // partitionConstraintByLocality splits a conjunction (A AND B AND ...) into -// two expressions: one whose every compound-identifier root is contained in -// localScope (safe for JOIN ON), and one whose roots reference outside -// identifiers (must stay in WHERE). +// two expressions: one whose every binding reference is contained in +// localScope (safe for JOIN ON), and one that references outside identifiers +// (must stay in WHERE). // // Only top-level AND operands are split. If an expression is not a // BinaryExpression with OperatorAnd, the whole expression is tested as a unit. diff --git a/cypher/models/pgsql/translate/projection.go b/cypher/models/pgsql/translate/projection.go index ba499e95..69195db9 100644 --- a/cypher/models/pgsql/translate/projection.go +++ b/cypher/models/pgsql/translate/projection.go @@ -164,14 +164,60 @@ func expansionPathEdgeArrayReference(scope *Scope, expansionPath *BoundIdentifie 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) { +func pathBindingReference(scope *Scope, binding *BoundIdentifier) pgsql.Expression { + if binding.LastProjection != nil { + return pgsql.CompoundIdentifier{binding.LastProjection.Binding.Identifier, binding.Identifier} + } + + if frameBinding := scope.CurrentFrameBinding(); frameBinding != nil { + return pgsql.CompoundIdentifier{frameBinding.Identifier, binding.Identifier} + } + + return binding.Identifier +} + +func pathCompositeReference(scope *Scope, binding *BoundIdentifier, columns []pgsql.Identifier) pgsql.Expression { + if binding.LastProjection != nil || scope.CurrentFrameBinding() != nil { + return pathBindingReference(scope, binding) + } + + values := make([]pgsql.Expression, len(columns)) + for idx, column := range columns { + values[idx] = pgsql.CompoundIdentifier{binding.Identifier, column} + } + + return pgsql.CompositeValue{ + Values: values, + DataType: bindingExpressionType(binding), + } +} + +func pathCompositeColumnReference(scope *Scope, binding *BoundIdentifier, column pgsql.Identifier) pgsql.Expression { + if binding.LastProjection != nil || scope.CurrentFrameBinding() != nil { + return pgsql.RowColumnReference{ + Identifier: pathBindingReference(scope, binding), + Column: column, + } + } + + return pgsql.CompoundIdentifier{binding.Identifier, column} +} + +func expansionPathEdgeArrayExpression(scope *Scope, expansionPath *BoundIdentifier) (pgsql.Expression, error) { + if scope.CurrentFrameBinding() != nil || expansionPath.LastProjection != nil { + return expansionPathEdgeArrayReference(scope, expansionPath) + } + + for _, dependency := range expansionPath.Dependencies { + return dependency.Identifier, nil + } + + return nil, fmt.Errorf("expansion path %s does not reference an expansion edge binding", expansionPath.Identifier) +} + +func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql.Expression, error) { if projected.LastProjection != nil { - return []pgsql.SelectItem{ - &pgsql.AliasedExpression{ - Expression: pgsql.CompoundIdentifier{projected.LastProjection.Binding.Identifier, projected.Identifier}, - Alias: pgsql.AsOptionalIdentifier(alias), - }, - }, nil + return pgsql.CompoundIdentifier{projected.LastProjection.Binding.Identifier, projected.Identifier}, nil } var ( @@ -189,17 +235,14 @@ func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIde switch dependency.DataType { case pgsql.ExpansionPath: seenExpansionPath = true - if edgeArrayReference, err := expansionPathEdgeArrayReference(scope, dependency); err != nil { + if edgeArrayReference, err := expansionPathEdgeArrayExpression(scope, dependency); err != nil { return nil, err } else { edgeArrayReferences = append(edgeArrayReferences, edgeArrayReference) } case pgsql.EdgeComposite: - directEdgeReference := pgsql.CompoundIdentifier{ - scope.CurrentFrameBinding().Identifier, - dependency.Identifier, - } + directEdgeReference := pathCompositeReference(scope, dependency, pgsql.EdgeTableColumns) directEdgeReferences = append(directEdgeReferences, directEdgeReference) edgeArrayReferences = append(edgeArrayReferences, pgsql.ArrayLiteral{ @@ -207,16 +250,9 @@ func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIde 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}, - )) + case pgsql.NodeComposite, pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode: + directNodeReferences = append(directNodeReferences, pathCompositeReference(scope, dependency, pgsql.NodeTableColumns)) + nodeReferences = append(nodeReferences, pathCompositeColumnReference(scope, dependency, pgsql.ColumnID)) default: return nil, fmt.Errorf("unsupported type for path rendering: %s", dependency.DataType) @@ -228,22 +264,17 @@ func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIde // 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, - }, - }, + return 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 } @@ -258,46 +289,49 @@ func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIde edgeArrayExpression = pgsql.ArrayLiteral{CastType: pgsql.EdgeCompositeArray} } - return []pgsql.SelectItem{ - &pgsql.AliasedExpression{ - Expression: pgsql.FunctionCall{ - Function: pgsql.FunctionOrderedEdgesToPath, - Parameters: []pgsql.Expression{ - directNodeReferences[0], - edgeArrayExpression, - pgsql.ArrayLiteral{ - Values: directNodeReferences, - CastType: pgsql.NodeCompositeArray, - }, - }, - CastType: pgsql.PathComposite, + return pgsql.FunctionCall{ + Function: pgsql.FunctionOrderedEdgesToPath, + Parameters: []pgsql.Expression{ + directNodeReferences[0], + edgeArrayExpression, + pgsql.ArrayLiteral{ + Values: directNodeReferences, + CastType: pgsql.NodeCompositeArray, }, - Alias: pgsql.AsOptionalIdentifier(alias), }, + CastType: pgsql.PathComposite, }, nil } else if len(nodeReferences) > 0 { - return []pgsql.SelectItem{ - &pgsql.AliasedExpression{ - Expression: pgsql.FunctionCall{ - Function: pgsql.FunctionNodesToPath, - Parameters: []pgsql.Expression{ - pgsql.Variadic{ - Expression: pgsql.ArrayLiteral{ - Values: nodeReferences, - CastType: pgsql.Int8Array, - }, - }, + return pgsql.FunctionCall{ + Function: pgsql.FunctionNodesToPath, + Parameters: []pgsql.Expression{ + pgsql.Variadic{ + Expression: pgsql.ArrayLiteral{ + Values: nodeReferences, + CastType: pgsql.Int8Array, }, - CastType: pgsql.PathComposite, }, - Alias: pgsql.AsOptionalIdentifier(alias), }, + CastType: pgsql.PathComposite, }, nil } return nil, fmt.Errorf("path variable does not contain valid components") } +func buildProjectionForPathComposite(alias pgsql.Identifier, projected *BoundIdentifier, scope *Scope) ([]pgsql.SelectItem, error) { + if expression, err := expressionForPathComposite(projected, scope); err != nil { + return nil, err + } else { + return []pgsql.SelectItem{ + &pgsql.AliasedExpression{ + Expression: expression, + Alias: pgsql.AsOptionalIdentifier(alias), + }, + }, nil + } +} + func buildProjectionForExpansionNode(alias pgsql.Identifier, projected *BoundIdentifier, referenceFrame *Frame) ([]pgsql.SelectItem, error) { if projected.LastProjection != nil { return []pgsql.SelectItem{ diff --git a/cypher/models/pgsql/translate/quantifiers.go b/cypher/models/pgsql/translate/quantifiers.go index afb1ee04..ca4c7178 100644 --- a/cypher/models/pgsql/translate/quantifiers.go +++ b/cypher/models/pgsql/translate/quantifiers.go @@ -74,6 +74,50 @@ func (s *Translator) translateFilterExpression(filterExpression *cypher.FilterEx return nil } +func (s *Translator) translateQuantifierArrayExpression(quantifierArrayExpression pgsql.Expression) ([]pgsql.Expression, pgsql.DataType, error) { + switch typedQuantifierArrayExpression := quantifierArrayExpression.(type) { + case *pgsql.BinaryExpression: + if pgsql.OperatorIsPropertyLookup(typedQuantifierArrayExpression.Operator) { + // Property look-up operators are converted to JSON text field operators during translation. + // Change this back so the JSON array can be converted to a Postgres text array for unnest(...). + typedQuantifierArrayExpression.Operator = pgsql.OperatorJSONField + return []pgsql.Expression{ + pgsql.FunctionCall{ + Function: pgsql.FunctionJSONBToTextArray, + Parameters: []pgsql.Expression{ + typedQuantifierArrayExpression, + }, + }, + }, pgsql.TextArray, nil + } + + case pgsql.BinaryExpression: + if pgsql.OperatorIsPropertyLookup(typedQuantifierArrayExpression.Operator) { + typedQuantifierArrayExpression.Operator = pgsql.OperatorJSONField + return []pgsql.Expression{ + pgsql.FunctionCall{ + Function: pgsql.FunctionJSONBToTextArray, + Parameters: []pgsql.Expression{ + typedQuantifierArrayExpression, + }, + }, + }, pgsql.TextArray, nil + } + + if inferredCollectionType, err := s.inferArrayExpressionType(&typedQuantifierArrayExpression); err != nil { + return nil, pgsql.UnsetDataType, err + } else { + return []pgsql.Expression{typedQuantifierArrayExpression}, inferredCollectionType, nil + } + } + + if inferredCollectionType, err := s.inferArrayExpressionType(quantifierArrayExpression); err != nil { + return nil, pgsql.UnsetDataType, err + } else { + return []pgsql.Expression{quantifierArrayExpression}, inferredCollectionType, nil + } +} + func (s *Translator) translateIDInCollection(idInCol *cypher.IDInCollection) error { if quantifierArray, err := s.treeTranslator.PopOperand(); err != nil { s.SetError(err) @@ -84,28 +128,21 @@ func (s *Translator) translateIDInCollection(idInCol *cypher.IDInCollection) err } else if array, bound := s.scope.AliasedLookup(identifier); !bound { return fmt.Errorf("filter expression variable must be bound") } else { - var functionParameters []pgsql.Expression - switch quantifierArrayExpression := quantifierArray.AsExpression().(type) { - // Property lookup, n.properties -> usedencryptionkey - case pgsql.BinaryExpression: - // All property look-up operators are converted to JSON Text field Operators during translation, - // this needs to be changed back so we can properly convert it to a Postgres text array which can then be used in an unnest function - quantifierArrayExpression.Operator = pgsql.OperatorJSONField - functionParameters = []pgsql.Expression{ - pgsql.FunctionCall{ - Function: pgsql.FunctionJSONBToTextArray, - Parameters: []pgsql.Expression{ - quantifierArrayExpression, - }, - }, - } - // native postgres array eg: collect(x) as quantifier_array ... ANY(y in quantifier_array... - case pgsql.Identifier: - functionParameters = []pgsql.Expression{quantifierArrayExpression} - default: - return fmt.Errorf("unknown cypher array type %s", quantifierArrayExpression) + var ( + functionParameters []pgsql.Expression + collectionType pgsql.DataType + ) + + quantifierArrayExpression := quantifierArray.AsExpression() + if translatedParameters, inferredCollectionType, err := s.translateQuantifierArrayExpression(quantifierArrayExpression); err != nil { + return err + } else { + functionParameters = translatedParameters + collectionType = inferredCollectionType } + array.DataType = collectionType.ArrayBaseType() + fromClause := pgsql.FromClause{ Source: pgsql.AliasedExpression{ Expression: pgsql.FunctionCall{ diff --git a/cypher/models/pgsql/translate/renamer.go b/cypher/models/pgsql/translate/renamer.go index 1b30c00b..c2a49c3c 100644 --- a/cypher/models/pgsql/translate/renamer.go +++ b/cypher/models/pgsql/translate/renamer.go @@ -41,14 +41,122 @@ func rewriteCompoundIdentifierScopeReference(scope *Scope, identifier pgsql.Comp return identifier, nil } +func rewriteExpressionScopeReference(scope *Scope, expression pgsql.Expression) (pgsql.Expression, bool, error) { + switch typedExpression := expression.(type) { + case pgsql.Identifier: + rewritten, err := rewriteIdentifierScopeReference(scope, typedExpression) + return rewritten, true, err + + case pgsql.CompoundIdentifier: + rewritten, err := rewriteCompoundIdentifierScopeReference(scope, typedExpression) + return rewritten, true, err + + default: + return expression, false, nil + } +} + type FrameBindingRewriter struct { walk.Visitor[pgsql.SyntaxNode] scope *Scope } +func (s *FrameBindingRewriter) rewriteArraySlice(slice *pgsql.ArraySlice) error { + if slice == nil { + return nil + } + + if rewritten, didRewrite, err := rewriteExpressionScopeReference(s.scope, slice.Expression); err != nil { + return err + } else if didRewrite { + slice.Expression = rewritten + } + + if slice.Lower != nil { + if rewritten, didRewrite, err := rewriteExpressionScopeReference(s.scope, slice.Lower); err != nil { + return err + } else if didRewrite { + slice.Lower = rewritten + } + } + + if slice.Upper != nil { + if rewritten, didRewrite, err := rewriteExpressionScopeReference(s.scope, slice.Upper); err != nil { + return err + } else if didRewrite { + slice.Upper = rewritten + } + } + + return nil +} + +func (s *FrameBindingRewriter) rewriteExpression(expression *pgsql.Expression) error { + if expression == nil || *expression == nil { + return nil + } + + switch typedExpression := (*expression).(type) { + case pgsql.Identifier: + if rewritten, err := rewriteIdentifierScopeReference(s.scope, typedExpression); err != nil { + return err + } else { + *expression = rewritten + } + + case pgsql.CompoundIdentifier: + if rewritten, err := rewriteCompoundIdentifierScopeReference(s.scope, typedExpression); err != nil { + return err + } else { + *expression = rewritten + } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedExpression); err != nil { + return err + } + *expression = typedExpression + + case *pgsql.ArraySlice: + return s.rewriteArraySlice(typedExpression) + } + + return nil +} + +func (s *FrameBindingRewriter) rewriteCase(caseExpression *pgsql.Case) error { + if caseExpression == nil { + return nil + } + + if err := s.rewriteExpression(&caseExpression.Operand); err != nil { + return err + } + + for idx := range caseExpression.Conditions { + if err := s.rewriteExpression(&caseExpression.Conditions[idx]); err != nil { + return err + } + } + + for idx := range caseExpression.Then { + if err := s.rewriteExpression(&caseExpression.Then[idx]); err != nil { + return err + } + } + + return s.rewriteExpression(&caseExpression.Else) +} + func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { switch typedExpression := node.(type) { + case pgsql.Case: + return s.rewriteCase(&typedExpression) + + case *pgsql.Case: + return s.rewriteCase(typedExpression) + case pgsql.Projection: for idx, projection := range typedExpression { switch typedProjection := projection.(type) { @@ -65,6 +173,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression[idx] = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedProjection); err != nil { + return err + } + typedExpression[idx] = typedProjection + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedProjection); err != nil { + return err + } } } @@ -84,6 +203,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Values[idx] = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedValue); err != nil { + return err + } + typedExpression.Values[idx] = typedValue + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedValue); err != nil { + return err + } } } @@ -124,6 +254,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { return fmt.Errorf("unknown quantifier loperand expression: %v", typedLOperand) } typedExpression.Parameters[idx] = typedParameter + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedParameter); err != nil { + return err + } + typedExpression.Parameters[idx] = typedParameter + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedParameter); err != nil { + return err + } } } @@ -142,6 +283,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Expression = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedOrderByExpression); err != nil { + return err + } + typedExpression.Expression = typedOrderByExpression + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedOrderByExpression); err != nil { + return err + } } case *pgsql.ArrayIndex: @@ -159,6 +311,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Expression = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedArrayIndexInnerExpression); err != nil { + return err + } + typedExpression.Expression = typedArrayIndexInnerExpression + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedArrayIndexInnerExpression); err != nil { + return err + } } for idx, indexExpression := range typedExpression.Indexes { @@ -176,9 +339,26 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Indexes[idx] = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedIndexExpression); err != nil { + return err + } + typedExpression.Indexes[idx] = typedIndexExpression + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedIndexExpression); err != nil { + return err + } } } + case pgsql.ArraySlice: + return s.rewriteArraySlice(&typedExpression) + + case *pgsql.ArraySlice: + return s.rewriteArraySlice(typedExpression) + case *pgsql.Parenthetical: switch typedInnerExpression := typedExpression.Expression.(type) { case pgsql.Identifier: @@ -194,6 +374,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Expression = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedInnerExpression); err != nil { + return err + } + typedExpression.Expression = typedInnerExpression + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedInnerExpression); err != nil { + return err + } } case *pgsql.AliasedExpression: @@ -211,6 +402,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Expression = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedInnerExpression); err != nil { + return err + } + typedExpression.Expression = typedInnerExpression + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedInnerExpression); err != nil { + return err + } } case *pgsql.AnyExpression: @@ -228,6 +430,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Expression = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedInnerExpression); err != nil { + return err + } + typedExpression.Expression = typedInnerExpression + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedInnerExpression); err != nil { + return err + } } case *pgsql.UnaryExpression: @@ -245,6 +458,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.Operand = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedOperand); err != nil { + return err + } + typedExpression.Operand = typedOperand + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedOperand); err != nil { + return err + } } case *pgsql.BinaryExpression: @@ -262,6 +486,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.LOperand = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedLOperand); err != nil { + return err + } + typedExpression.LOperand = typedLOperand + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedLOperand); err != nil { + return err + } } switch typedROperand := typedExpression.ROperand.(type) { @@ -278,6 +513,17 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } else { typedExpression.ROperand = rewritten } + + case pgsql.ArraySlice: + if err := s.rewriteArraySlice(&typedROperand); err != nil { + return err + } + typedExpression.ROperand = typedROperand + + case *pgsql.ArraySlice: + if err := s.rewriteArraySlice(typedROperand); err != nil { + return err + } } } diff --git a/cypher/models/pgsql/translate/renamer_test.go b/cypher/models/pgsql/translate/renamer_test.go index 1f631934..27999e28 100644 --- a/cypher/models/pgsql/translate/renamer_test.go +++ b/cypher/models/pgsql/translate/renamer_test.go @@ -41,6 +41,7 @@ func TestRewriteFrameBindings(t *testing.T) { frame.Export(a.Identifier) a.MaterializedBy(frame) + rewrittenA := pgsql.CompoundIdentifier{frame.Binding.Identifier, a.Identifier} // Cases testCases := []testCase{{ @@ -48,20 +49,60 @@ func TestRewriteFrameBindings(t *testing.T) { Expression: a.Identifier, }, Expected: &pgsql.Parenthetical{ - Expression: pgsql.CompoundIdentifier{frame.Binding.Identifier, a.Identifier}, + Expression: rewrittenA, }, }, { Case: pgsql.NewBinaryExpression(a.Identifier, pgsql.OperatorEquals, a.Identifier), - Expected: pgsql.NewBinaryExpression(pgsql.CompoundIdentifier{frame.Binding.Identifier, a.Identifier}, pgsql.OperatorEquals, pgsql.CompoundIdentifier{frame.Binding.Identifier, a.Identifier}), + Expected: pgsql.NewBinaryExpression(rewrittenA, pgsql.OperatorEquals, rewrittenA), }, { Case: &pgsql.AliasedExpression{ Expression: a.Identifier, Alias: pgsql.AsOptionalIdentifier("name"), }, Expected: &pgsql.AliasedExpression{ - Expression: pgsql.CompoundIdentifier{frame.Binding.Identifier, a.Identifier}, + Expression: rewrittenA, Alias: pgsql.AsOptionalIdentifier("name"), }, + }, { + Case: pgsql.NewBinaryExpression( + pgsql.ArraySlice{ + Expression: a.Identifier, + Lower: a.Identifier, + Upper: a.Identifier, + }, + pgsql.OperatorEquals, + a.Identifier, + ), + Expected: pgsql.NewBinaryExpression( + pgsql.ArraySlice{ + Expression: rewrittenA, + Lower: rewrittenA, + Upper: rewrittenA, + }, + pgsql.OperatorEquals, + rewrittenA, + ), + }, { + Case: &pgsql.Case{ + Operand: a.Identifier, + Conditions: []pgsql.Expression{ + a.Identifier, + }, + Then: []pgsql.Expression{ + a.Identifier, + }, + Else: a.Identifier, + }, + Expected: &pgsql.Case{ + Operand: rewrittenA, + Conditions: []pgsql.Expression{ + rewrittenA, + }, + Then: []pgsql.Expression{ + rewrittenA, + }, + Else: rewrittenA, + }, }} for _, nextTestCase := range testCases { @@ -80,3 +121,86 @@ func TestRewriteFrameBindings(t *testing.T) { }) } } + +func TestRewriteFrameBindings_ArraySliceDirectParents(t *testing.T) { + var ( + scope = translate.NewScope() + frame = mustPushFrame(t, scope) + a = mustDefineNew(t, scope, pgsql.Int) + ) + + frame.Reveal(a.Identifier) + frame.Export(a.Identifier) + + a.MaterializedBy(frame) + rewrittenA := pgsql.CompoundIdentifier{frame.Binding.Identifier, a.Identifier} + + newSlice := func(expression pgsql.Expression) pgsql.ArraySlice { + return pgsql.ArraySlice{ + Expression: expression, + Lower: expression, + Upper: expression, + } + } + + testCases := []struct { + name string + actual pgsql.Expression + expected pgsql.Expression + }{{ + name: "projection value", + actual: pgsql.Projection{ + newSlice(a.Identifier), + }, + expected: pgsql.Projection{ + newSlice(rewrittenA), + }, + }, { + name: "projection pointer", + actual: pgsql.Projection{ + func() *pgsql.ArraySlice { + value := newSlice(a.Identifier) + return &value + }(), + }, + expected: pgsql.Projection{ + func() *pgsql.ArraySlice { + value := newSlice(rewrittenA) + return &value + }(), + }, + }, { + name: "order by value", + actual: &pgsql.OrderBy{ + Expression: newSlice(a.Identifier), + Ascending: true, + }, + expected: &pgsql.OrderBy{ + Expression: newSlice(rewrittenA), + Ascending: true, + }, + }, { + name: "order by pointer", + actual: &pgsql.OrderBy{ + Expression: func() *pgsql.ArraySlice { + value := newSlice(a.Identifier) + return &value + }(), + Ascending: true, + }, + expected: &pgsql.OrderBy{ + Expression: func() *pgsql.ArraySlice { + value := newSlice(rewrittenA) + return &value + }(), + Ascending: true, + }, + }} + + for _, nextTestCase := range testCases { + t.Run(nextTestCase.name, func(t *testing.T) { + require.NoError(t, translate.RewriteFrameBindings(scope, nextTestCase.actual)) + assert.Equal(t, nextTestCase.expected, nextTestCase.actual) + }) + } +} diff --git a/cypher/models/pgsql/translate/semantic_drift_test.go b/cypher/models/pgsql/translate/semantic_drift_test.go index 4165f89c..a20b7d69 100644 --- a/cypher/models/pgsql/translate/semantic_drift_test.go +++ b/cypher/models/pgsql/translate/semantic_drift_test.go @@ -29,3 +29,14 @@ func TestTranslatorRejectsNeo4jAuthoritativeInvalidShapes(t *testing.T) { }) } } + +func TestTranslatorRejectsUnsupportedPropertyLookupSourcesDirectly(t *testing.T) { + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), `RETURN [1, 2, 3].prop`) + require.NoError(t, err) + + _, err = Translate(context.Background(), query, kindMapper, nil, DefaultGraphID) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported property lookup prop on expression type int8[]") +} diff --git a/cypher/models/pgsql/translate/with.go b/cypher/models/pgsql/translate/with.go index daaa0ca8..ed09287f 100644 --- a/cypher/models/pgsql/translate/with.go +++ b/cypher/models/pgsql/translate/with.go @@ -73,6 +73,23 @@ func (s *Translator) translateWith() error { return fmt.Errorf("unable to lookup identifer %s for with statement", typedSelectItem) } else { var selectItem pgsql.SelectItem + projectedBinding := binding + + if projectionItem.Alias.Set { + if aliasBinding, aliasBound := s.scope.AliasedLookup(projectionItem.Alias.Value); !aliasBound || aliasBinding.Identifier != binding.Identifier { + var err error + + if projectedBinding, err = s.scope.DefineNew(binding.DataType); err != nil { + return err + } + + projectedBinding.Dependencies = binding.Dependencies + s.scope.Alias(projectionItem.Alias.Value, projectedBinding) + } else { + projectedBinding = aliasBinding + } + } + if binding.LastProjection != nil { selectItem = pgsql.CompoundIdentifier{ binding.LastProjection.Binding.Identifier, typedSelectItem, @@ -84,20 +101,20 @@ func (s *Translator) translateWith() error { } // Track this projected item for scope pruning - projectedItems.Add(binding.Identifier) + projectedItems.Add(projectedBinding.Identifier) // Create a new projection that maps the identifier currentPart.projections.Items[idx] = &Projection{ SelectItem: selectItem, - Alias: pgsql.AsOptionalIdentifier(binding.Identifier), + Alias: pgsql.AsOptionalIdentifier(projectedBinding.Identifier), } // Assign the frame to the binding's last projection backref - binding.MaterializedBy(currentPart.Frame) + projectedBinding.MaterializedBy(currentPart.Frame) // Reveal and export the identifier in the current multipart query part's frame - currentPart.Frame.Reveal(binding.Identifier) - currentPart.Frame.Export(binding.Identifier) + currentPart.Frame.Reveal(projectedBinding.Identifier) + currentPart.Frame.Export(projectedBinding.Identifier) } default: diff --git a/cypher/models/walk/walk_pgsql.go b/cypher/models/walk/walk_pgsql.go index 3bfe2330..a140fb66 100644 --- a/cypher/models/walk/walk_pgsql.go +++ b/cypher/models/walk/walk_pgsql.go @@ -10,6 +10,30 @@ func pgsqlSyntaxNodeSliceTypeConvert[F any, FS []F](fs FS) ([]pgsql.SyntaxNode, return ConvertSliceType[pgsql.SyntaxNode](fs) } +func newSQLCaseWalkCursor(node pgsql.SyntaxNode, caseExpr pgsql.Case) (*Cursor[pgsql.SyntaxNode], error) { + if len(caseExpr.Conditions) != len(caseExpr.Then) { + return nil, fmt.Errorf("case expression has %d conditions and %d then expressions", len(caseExpr.Conditions), len(caseExpr.Then)) + } + + nextCursor := &Cursor[pgsql.SyntaxNode]{ + Node: node, + } + + if caseExpr.Operand != nil { + nextCursor.AddBranches(caseExpr.Operand) + } + + for idx, condition := range caseExpr.Conditions { + nextCursor.AddBranches(condition, caseExpr.Then[idx]) + } + + if caseExpr.Else != nil { + nextCursor.AddBranches(caseExpr.Else) + } + + return nextCursor, nil +} + func newSQLWalkCursor(node pgsql.SyntaxNode) (*Cursor[pgsql.SyntaxNode], error) { switch typedNode := node.(type) { case pgsql.Query: @@ -46,6 +70,12 @@ func newSQLWalkCursor(node pgsql.SyntaxNode) (*Cursor[pgsql.SyntaxNode], error) Branches: typedNode.AsSyntaxNodes(), }, nil + case pgsql.Case: + return newSQLCaseWalkCursor(node, typedNode) + + case *pgsql.Case: + return newSQLCaseWalkCursor(node, *typedNode) + case *pgsql.OrderBy: return &Cursor[pgsql.SyntaxNode]{ Node: node, @@ -302,6 +332,34 @@ func newSQLWalkCursor(node pgsql.SyntaxNode) (*Cursor[pgsql.SyntaxNode], error) }, nil } + case pgsql.ArraySlice: + branches := []pgsql.SyntaxNode{typedNode.Expression} + if typedNode.Lower != nil { + branches = append(branches, typedNode.Lower) + } + if typedNode.Upper != nil { + branches = append(branches, typedNode.Upper) + } + + return &Cursor[pgsql.SyntaxNode]{ + Node: node, + Branches: branches, + }, nil + + case *pgsql.ArraySlice: + branches := []pgsql.SyntaxNode{typedNode.Expression} + if typedNode.Lower != nil { + branches = append(branches, typedNode.Lower) + } + if typedNode.Upper != nil { + branches = append(branches, typedNode.Upper) + } + + return &Cursor[pgsql.SyntaxNode]{ + Node: node, + Branches: branches, + }, nil + case pgsql.ExistsExpression: return &Cursor[pgsql.SyntaxNode]{ Node: node, diff --git a/drivers/neo4j/query_rewrite.go b/drivers/neo4j/query_rewrite.go index 7a70339f..52d8477f 100644 --- a/drivers/neo4j/query_rewrite.go +++ b/drivers/neo4j/query_rewrite.go @@ -48,6 +48,117 @@ func rewritePatternPropertyParameters(query string, parameters map[string]any) ( return rewrittenQuery, rewriter.rewrittenParameters, nil } +func rewriteQuery(query string, parameters map[string]any) (string, map[string]any, error) { + if !queryMayNeedRewrite(query) { + return query, parameters, nil + } + + parsed, err := frontend.ParseCypher(frontend.NewContext(), query) + if err != nil { + return query, parameters, nil + } + + var ( + rewrittenParameters = parameters + rewritten bool + ) + + if strings.Contains(query, "$") { + parameterRewriter := &patternPropertyParameterRewriter{ + parameters: parameters, + } + + if err := parameterRewriter.rewriteRegularQuery(parsed); err != nil { + return "", nil, err + } + + if parameterRewriter.rewritten { + rewritten = true + rewrittenParameters = parameterRewriter.rewrittenParameters + } + } + + temporalRewriter := &temporalPropertyComparisonRewriter{} + temporalRewriter.rewriteRegularQuery(parsed) + if temporalRewriter.rewritten { + rewritten = true + } + + if !rewritten { + return query, parameters, nil + } + + rewrittenQuery, err := cypherfmt.RegularQuery(parsed, false) + if err != nil { + return "", nil, err + } + + return rewrittenQuery, rewrittenParameters, nil +} + +func queryMayNeedRewrite(query string) bool { + return strings.Contains(query, "$") || queryMayContainTemporalFunctionCall(query) +} + +func queryMayContainTemporalFunctionCall(query string) bool { + query = strings.ToLower(query) + + for _, functionName := range []string{ + cypher.DateFunction, + cypher.TimeFunction, + cypher.LocalTimeFunction, + cypher.DateTimeFunction, + cypher.LocalDateTimeFunction, + } { + if containsFunctionCall(query, functionName) { + return true + } + } + + return false +} + +func containsFunctionCall(query, functionName string) bool { + for searchOffset := 0; searchOffset < len(query); { + functionOffset := strings.Index(query[searchOffset:], functionName) + if functionOffset == -1 { + return false + } + + functionOffset += searchOffset + if functionOffset > 0 && isCypherIdentifierCharacter(query[functionOffset-1]) { + searchOffset = functionOffset + 1 + continue + } + + nextOffset := functionOffset + len(functionName) + for nextOffset < len(query) && isCypherWhitespace(query[nextOffset]) { + nextOffset++ + } + + if nextOffset < len(query) && query[nextOffset] == '(' { + return true + } + + searchOffset = functionOffset + 1 + } + + return false +} + +func isCypherIdentifierCharacter(value byte) bool { + return (value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '_' +} + +func isCypherWhitespace(value byte) bool { + switch value { + case ' ', '\t', '\n', '\r': + return true + default: + return false + } +} + func (s *patternPropertyParameterRewriter) rewriteRegularQuery(query *cypher.RegularQuery) error { if query == nil || query.SingleQuery == nil { return nil @@ -188,3 +299,330 @@ func parameterPropertyMap(value any) (map[string]any, error) { return propertyMap, nil } + +type temporalPropertyComparisonRewriter struct { + rewritten bool +} + +func (s *temporalPropertyComparisonRewriter) rewriteRegularQuery(query *cypher.RegularQuery) { + if query == nil || query.SingleQuery == nil { + return + } + + if singlePartQuery := query.SingleQuery.SinglePartQuery; singlePartQuery != nil { + s.rewriteSinglePartQuery(singlePartQuery) + } + + if multiPartQuery := query.SingleQuery.MultiPartQuery; multiPartQuery != nil { + for _, part := range multiPartQuery.Parts { + s.rewriteMultiPartQueryPart(part) + } + + if multiPartQuery.SinglePartQuery != nil { + s.rewriteSinglePartQuery(multiPartQuery.SinglePartQuery) + } + } +} + +func (s *temporalPropertyComparisonRewriter) rewriteSinglePartQuery(query *cypher.SinglePartQuery) { + if query == nil { + return + } + + s.rewriteReadingClauseExpressions(query.ReadingClauses) + s.rewriteProjection(query.Return) + + for idx, expression := range query.UpdatingClauses { + query.UpdatingClauses[idx] = s.rewriteExpression(expression) + } +} + +func (s *temporalPropertyComparisonRewriter) rewriteMultiPartQueryPart(part *cypher.MultiPartQueryPart) { + if part == nil { + return + } + + s.rewriteReadingClauseExpressions(part.ReadingClauses) + + for _, updatingClause := range part.UpdatingClauses { + if updatingClause != nil { + updatingClause.Clause = s.rewriteExpression(updatingClause.Clause) + } + } + + s.rewriteWith(part.With) +} + +func (s *temporalPropertyComparisonRewriter) rewriteReadingClauseExpressions(readingClauses []*cypher.ReadingClause) { + for _, readingClause := range readingClauses { + if readingClause == nil { + continue + } + + if readingClause.Match != nil { + s.rewriteWhere(readingClause.Match.Where) + } + + if readingClause.Unwind != nil { + readingClause.Unwind.Expression = s.rewriteExpression(readingClause.Unwind.Expression) + } + } +} + +func (s *temporalPropertyComparisonRewriter) rewriteWith(with *cypher.With) { + if with == nil { + return + } + + s.rewriteProjectionItems(with.Projection) + s.rewriteWhere(with.Where) +} + +func (s *temporalPropertyComparisonRewriter) rewriteProjection(returnClause *cypher.Return) { + if returnClause == nil { + return + } + + s.rewriteProjectionItems(returnClause.Projection) +} + +func (s *temporalPropertyComparisonRewriter) rewriteProjectionItems(projection *cypher.Projection) { + if projection == nil { + return + } + + for idx, item := range projection.Items { + projection.Items[idx] = s.rewriteExpression(item) + } + + if projection.Order != nil { + for _, item := range projection.Order.Items { + if item != nil { + item.Expression = s.rewriteExpression(item.Expression) + } + } + } + + if projection.Skip != nil { + projection.Skip.Value = s.rewriteExpression(projection.Skip.Value) + } + + if projection.Limit != nil { + projection.Limit.Value = s.rewriteExpression(projection.Limit.Value) + } +} + +func (s *temporalPropertyComparisonRewriter) rewriteWhere(where *cypher.Where) { + if where == nil { + return + } + + s.rewriteExpressionList(where) +} + +func (s *temporalPropertyComparisonRewriter) rewriteExpressionList(expressions cypher.ExpressionList) { + for idx := 0; idx < expressions.Len(); idx++ { + expressions.Replace(idx, s.rewriteExpression(expressions.Get(idx))) + } +} + +func (s *temporalPropertyComparisonRewriter) rewriteExpression(expression cypher.Expression) cypher.Expression { + switch typedExpression := expression.(type) { + case *cypher.Comparison: + return s.rewriteComparison(typedExpression) + + case *cypher.Parenthetical: + typedExpression.Expression = s.rewriteExpression(typedExpression.Expression) + + case *cypher.Negation: + typedExpression.Expression = s.rewriteExpression(typedExpression.Expression) + + case *cypher.Disjunction: + s.rewriteExpressionList(typedExpression) + + case *cypher.ExclusiveDisjunction: + s.rewriteExpressionList(typedExpression) + + case *cypher.Conjunction: + s.rewriteExpressionList(typedExpression) + + case *cypher.ArithmeticExpression: + typedExpression.Left = s.rewriteExpression(typedExpression.Left) + for _, partial := range typedExpression.Partials { + partial.Right = s.rewriteExpression(partial.Right) + } + + case *cypher.UnaryAddOrSubtractExpression: + typedExpression.Right = s.rewriteExpression(typedExpression.Right) + + case *cypher.FunctionInvocation: + for idx, argument := range typedExpression.Arguments { + typedExpression.Arguments[idx] = s.rewriteExpression(argument) + } + + case *cypher.ProjectionItem: + typedExpression.Expression = s.rewriteExpression(typedExpression.Expression) + + case *cypher.FilterExpression: + if typedExpression.Specifier != nil { + typedExpression.Specifier.Expression = s.rewriteExpression(typedExpression.Specifier.Expression) + } + s.rewriteWhere(typedExpression.Where) + + case *cypher.Quantifier: + if typedExpression.Filter != nil { + typedExpression.Filter = s.rewriteExpression(typedExpression.Filter).(*cypher.FilterExpression) + } + + case *cypher.MapLiteral: + for key, value := range *typedExpression { + (*typedExpression)[key] = s.rewriteExpression(value) + } + + case cypher.MapLiteral: + for key, value := range typedExpression { + typedExpression[key] = s.rewriteExpression(value) + } + + case *cypher.ListLiteral: + for idx, value := range *typedExpression { + (*typedExpression)[idx] = s.rewriteExpression(value) + } + } + + return expression +} + +func (s *temporalPropertyComparisonRewriter) rewriteComparison(comparison *cypher.Comparison) cypher.Expression { + comparison.Left = s.rewriteExpression(comparison.Left) + + for idx, partial := range comparison.Partials { + partial.Right = s.rewriteExpression(partial.Right) + + if !isTemporalComparisonOperator(partial.Operator) { + continue + } + + left := comparisonOperand(comparison, idx) + right := partial.Right + + if temporalFunction, ok := temporalExpressionFunction(right); ok { + if wrapped, didRewrite := temporalPropertyLookup(left, temporalFunction); didRewrite { + setComparisonOperand(comparison, idx, wrapped) + s.rewritten = true + } + } + + left = comparisonOperand(comparison, idx) + if temporalFunction, ok := temporalExpressionFunction(left); ok { + if wrapped, didRewrite := temporalPropertyLookup(right, temporalFunction); didRewrite { + partial.Right = wrapped + s.rewritten = true + } + } + } + + return comparison +} + +func comparisonOperand(comparison *cypher.Comparison, partialIdx int) cypher.Expression { + if partialIdx == 0 { + return comparison.Left + } + + return comparison.Partials[partialIdx-1].Right +} + +func setComparisonOperand(comparison *cypher.Comparison, partialIdx int, expression cypher.Expression) { + if partialIdx == 0 { + comparison.Left = expression + } else { + comparison.Partials[partialIdx-1].Right = expression + } +} + +func temporalPropertyLookup(expression cypher.Expression, temporalFunction string) (cypher.Expression, bool) { + propertyLookup, isPropertyLookup := expression.(*cypher.PropertyLookup) + if !isPropertyLookup { + return expression, false + } + + if _, isEntityVariable := propertyLookup.Atom.(*cypher.Variable); !isEntityVariable { + return expression, false + } + + return cypher.NewSimpleFunctionInvocation(temporalFunction, expression), true +} + +func isTemporalComparisonOperator(operator cypher.Operator) bool { + switch operator { + case cypher.OperatorEquals, + cypher.OperatorNotEquals, + cypher.OperatorGreaterThan, + cypher.OperatorGreaterThanOrEqualTo, + cypher.OperatorLessThan, + cypher.OperatorLessThanOrEqualTo: + return true + default: + return false + } +} + +func temporalExpressionFunction(expression cypher.Expression) (string, bool) { + switch typedExpression := expression.(type) { + case *cypher.Parenthetical: + return temporalExpressionFunction(typedExpression.Expression) + + case *cypher.FunctionInvocation: + functionName := strings.ToLower(typedExpression.Name) + if isTemporalFunction(functionName) { + return functionName, true + } + + case *cypher.ArithmeticExpression: + return temporalArithmeticExpressionFunction(typedExpression) + } + + return "", false +} + +func temporalArithmeticExpressionFunction(expression *cypher.ArithmeticExpression) (string, bool) { + functionName, isTemporal := temporalExpressionFunction(expression.Left) + if !isTemporal { + return "", false + } + + for _, partial := range expression.Partials { + if partial.Operator != cypher.OperatorAdd && partial.Operator != cypher.OperatorSubtract { + return "", false + } + + if !isDurationExpression(partial.Right) { + return "", false + } + } + + return functionName, true +} + +func isDurationExpression(expression cypher.Expression) bool { + function, isFunction := expression.(*cypher.FunctionInvocation) + if !isFunction { + return false + } + + return strings.EqualFold(function.Name, cypher.DurationFunction) +} + +func isTemporalFunction(functionName string) bool { + switch functionName { + case cypher.DateFunction, + cypher.TimeFunction, + cypher.LocalTimeFunction, + cypher.DateTimeFunction, + cypher.LocalDateTimeFunction: + return true + default: + return false + } +} diff --git a/drivers/neo4j/query_rewrite_internal_test.go b/drivers/neo4j/query_rewrite_internal_test.go index 2817b026..d165052a 100644 --- a/drivers/neo4j/query_rewrite_internal_test.go +++ b/drivers/neo4j/query_rewrite_internal_test.go @@ -47,3 +47,89 @@ func TestRewritePatternPropertyParameters_LeavesRawUnsupportedQueryAlone(t *test require.Equal(t, "match (n) return n[$prop]", query) require.Equal(t, map[string]any{"prop": "name"}, rewrittenParams) } + +func TestQueryMayNeedRewrite(t *testing.T) { + testCases := []struct { + name string + query string + want bool + }{{ + name: "plain query", + query: "match (n) return n", + want: false, + }, { + name: "pattern property parameter candidate", + query: "match (n:TemplateNodeKind1 $props) return n", + want: true, + }, { + name: "datetime function candidate", + query: "match (n) where n.lastseen >= datetime() return n", + want: true, + }, { + name: "temporal function is case insensitive and allows whitespace", + query: "match (n) where DateTime \n () <= n.lastseen return n", + want: true, + }, { + name: "property names containing date are not temporal function calls", + query: "match (n) where n.updated_at is not null return n", + want: false, + }, { + name: "identifier suffix is not temporal function call", + query: "match (n) where candidate() return n", + want: false, + }} + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.Equal(t, testCase.want, queryMayNeedRewrite(testCase.query)) + }) + } +} + +func TestRewriteQuery_WrapsTemporalPropertyComparison(t *testing.T) { + query, rewrittenParams, err := rewriteQuery( + "MATCH p=(:Computer)-[r:HasSession]->(:User) WHERE r.lastseen >= datetime() - duration('P3D') RETURN p LIMIT 100", + nil, + ) + require.NoError(t, err) + require.Nil(t, rewrittenParams) + require.Contains(t, query, "where datetime(r.lastseen) >= datetime() - duration('P3D')") + require.Contains(t, query, "return p limit 100") +} + +func TestRewriteQuery_WrapsTemporalPropertyComparisonRightOperand(t *testing.T) { + query, rewrittenParams, err := rewriteQuery( + "match (n) where date() <= n.created_at return n", + nil, + ) + require.NoError(t, err) + require.Nil(t, rewrittenParams) + require.Contains(t, query, "where date() <= date(n.created_at)") +} + +func TestRewriteQuery_DoesNotWrapTemporalComponentLookup(t *testing.T) { + input := "match (n) where datetime().epochseconds >= n.pwdlastset return n" + query, rewrittenParams, err := rewriteQuery(input, nil) + require.NoError(t, err) + require.Equal(t, input, query) + require.Nil(t, rewrittenParams) +} + +func TestRewriteQuery_RewritesPatternPropertiesAndTemporalComparisons(t *testing.T) { + params := map[string]any{ + "props": map[string]any{ + "name": "beta", + }, + } + + query, rewrittenParams, err := rewriteQuery( + "match (n:TemplateNodeKind1 $props) where n.lastseen >= datetime() return n.name", + params, + ) + require.NoError(t, err) + require.Contains(t, query, "match (n:TemplateNodeKind1 {") + require.Contains(t, query, "name: $__dawgs_pattern_property_") + require.Contains(t, query, "where datetime(n.lastseen) >= datetime()") + require.Equal(t, params["props"], rewrittenParams["props"]) + require.Len(t, rewrittenParams, 2) +} diff --git a/drivers/neo4j/transaction.go b/drivers/neo4j/transaction.go index 0dcda0af..7fea63bd 100644 --- a/drivers/neo4j/transaction.go +++ b/drivers/neo4j/transaction.go @@ -50,7 +50,7 @@ func (s *neo4jTransaction) WithGraph(graphSchema graph.Graph) graph.Transaction } func (s *neo4jTransaction) Query(query string, parameters map[string]any) graph.Result { - if rewrittenQuery, rewrittenParameters, err := rewritePatternPropertyParameters(query, parameters); err != nil { + if rewrittenQuery, rewrittenParameters, err := rewriteQuery(query, parameters); err != nil { return graph.NewErrorResult(err) } else { return s.Raw(rewrittenQuery, rewrittenParameters) diff --git a/drivers/pg/query/sql/schema_down.sql b/drivers/pg/query/sql/schema_down.sql index e2a2d049..2fe6923d 100644 --- a/drivers/pg/query/sql/schema_down.sql +++ b/drivers/pg/query/sql/schema_down.sql @@ -23,6 +23,7 @@ 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 shortest_path_self_endpoint_error(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); diff --git a/drivers/pg/query/sql/schema_up.sql b/drivers/pg/query/sql/schema_up.sql index 89b40bc1..c75b4515 100644 --- a/drivers/pg/query/sql/schema_up.sql +++ b/drivers/pg/query/sql/schema_up.sql @@ -677,6 +677,23 @@ $$ volatile strict; +create or replace function public.shortest_path_self_endpoint_error(root_id int8, terminal_id int8) + returns bool as +$$ +begin + raise exception using + errcode = '22023', + message = format('shortest path endpoints must not resolve to the same node: root_id=%s terminal_id=%s', + root_id, + terminal_id); + + return false; +end; +$$ + language plpgsql + volatile + strict; + create or replace function public.create_bidirectional_pathspace_tables() returns void as $$ diff --git a/integration/testdata/cases/expansion_inline.json b/integration/testdata/cases/expansion_inline.json index eb5f991a..28e18cfa 100644 --- a/integration/testdata/cases/expansion_inline.json +++ b/integration/testdata/cases/expansion_inline.json @@ -126,6 +126,44 @@ }, "assert": {"path_node_ids": [["src", "dst"]], "path_edge_kinds": [["EdgeKind1"]]} }, + { + "name": "zero hop expansion pairs unbound endpoints on the same node", + "cypher": "match (a:NodeKind1)-[:EdgeKind1*0..]->(b:NodeKind1) where a.name = 'solo' and b.name = 'solo' return a.name, b.name", + "fixture": { + "nodes": [ + {"id": "solo", "kinds": ["NodeKind1"], "properties": {"name": "solo"}}, + {"id": "other-a", "kinds": ["NodeKind1"], "properties": {"name": "other-a"}}, + {"id": "other-b", "kinds": ["NodeKind1"], "properties": {"name": "other-b"}} + ], + "edges": [{"start_id": "other-a", "end_id": "other-b", "kind": "EdgeKind1"}] + }, + "assert": {"row_values": [["solo", "solo"]]} + }, + { + "name": "zero hop expansion pairs a bound root with itself", + "cypher": "match (a:NodeKind1) where a.name = 'solo' match (a)-[:EdgeKind1*0..]->(b:NodeKind1) return b.name", + "fixture": { + "nodes": [ + {"id": "solo", "kinds": ["NodeKind1"], "properties": {"name": "solo"}}, + {"id": "other-a", "kinds": ["NodeKind1"], "properties": {"name": "other-a"}}, + {"id": "other-b", "kinds": ["NodeKind1"], "properties": {"name": "other-b"}} + ], + "edges": [{"start_id": "other-a", "end_id": "other-b", "kind": "EdgeKind1"}] + }, + "assert": {"ordered_scalar_values": ["solo"]} + }, + { + "name": "zero hop expansion does not duplicate one hop paths", + "cypher": "match (a:NodeKind1)-[:EdgeKind1*0..]->(b:NodeKind1) where a.name = 'zero-source' and b.name = 'zero-target' return count(b)", + "fixture": { + "nodes": [ + {"id": "zero-source", "kinds": ["NodeKind1"], "properties": {"name": "zero-source"}}, + {"id": "zero-target", "kinds": ["NodeKind1"], "properties": {"name": "zero-target"}} + ], + "edges": [{"start_id": "zero-source", "end_id": "zero-target", "kind": "EdgeKind1"}] + }, + "assert": {"exact_int": 1} + }, { "name": "bind expansion path filtering both endpoints using ends-with on objectid", "cypher": "match p = (g:NodeKind1)-[:EdgeKind1|EdgeKind2*]->(target:NodeKind1) where g.objectid ends with '-src' and target.objectid ends with '-tgt' return p", diff --git a/integration/testdata/cases/pattern_binding_inline.json b/integration/testdata/cases/pattern_binding_inline.json index 9f8accce..3a64ee5b 100644 --- a/integration/testdata/cases/pattern_binding_inline.json +++ b/integration/testdata/cases/pattern_binding_inline.json @@ -209,6 +209,42 @@ "edges": [{"start_id": "b", "end_id": "a", "kind": "EdgeKind1"}] }, "assert": {"path_node_ids": [["a", "b"]], "path_edge_kinds": [["EdgeKind1"]]} + }, + { + "name": "filter a bound expansion path using head relationship and tail nodes", + "cypher": "MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE HEAD(r).enforced OR NONE(n IN TAIL(TAIL(NODES(p))) WHERE (n:OU AND n.blocksinheritance)) RETURN p", + "fixture": { + "nodes": [ + {"id": "gpo-enforced", "kinds": ["GPO"]}, + {"id": "base-enforced", "kinds": ["Base"]}, + {"id": "gpo-enforced-blocked", "kinds": ["GPO"]}, + {"id": "ou-enforced-first", "kinds": ["OU"], "properties": {"blocksinheritance": false}}, + {"id": "ou-enforced-blocked", "kinds": ["OU"], "properties": {"blocksinheritance": true}}, + {"id": "base-enforced-blocked", "kinds": ["Base"]}, + {"id": "gpo-open", "kinds": ["GPO"]}, + {"id": "ou-open", "kinds": ["OU"], "properties": {"blocksinheritance": false}}, + {"id": "base-open", "kinds": ["Base"]}, + {"id": "gpo-blocked", "kinds": ["GPO"]}, + {"id": "ou-first", "kinds": ["OU"], "properties": {"blocksinheritance": false}}, + {"id": "ou-blocked", "kinds": ["OU"], "properties": {"blocksinheritance": true}}, + {"id": "base-blocked", "kinds": ["Base"]} + ], + "edges": [ + {"start_id": "gpo-enforced", "end_id": "base-enforced", "kind": "GPLink", "properties": {"enforced": true}}, + {"start_id": "gpo-enforced-blocked", "end_id": "ou-enforced-first", "kind": "GPLink", "properties": {"enforced": true}}, + {"start_id": "ou-enforced-first", "end_id": "ou-enforced-blocked", "kind": "Contains"}, + {"start_id": "ou-enforced-blocked", "end_id": "base-enforced-blocked", "kind": "Contains"}, + {"start_id": "gpo-open", "end_id": "ou-open", "kind": "GPLink", "properties": {"enforced": false}}, + {"start_id": "ou-open", "end_id": "base-open", "kind": "Contains"}, + {"start_id": "gpo-blocked", "end_id": "ou-first", "kind": "GPLink", "properties": {"enforced": false}}, + {"start_id": "ou-first", "end_id": "ou-blocked", "kind": "Contains"}, + {"start_id": "ou-blocked", "end_id": "base-blocked", "kind": "Contains"} + ] + }, + "assert": { + "path_node_ids": [["gpo-enforced", "base-enforced"], ["gpo-enforced-blocked", "ou-enforced-first", "ou-enforced-blocked", "base-enforced-blocked"], ["gpo-open", "ou-open", "base-open"]], + "path_edge_kinds": [["GPLink"], ["GPLink", "Contains", "Contains"], ["GPLink", "Contains"]] + } } ] } diff --git a/integration/testdata/cases/quantifiers_inline.json b/integration/testdata/cases/quantifiers_inline.json index 338e9d63..395ae748 100644 --- a/integration/testdata/cases/quantifiers_inline.json +++ b/integration/testdata/cases/quantifiers_inline.json @@ -38,6 +38,17 @@ "edges": [{"start_id": "n", "end_id": "g", "kind": "EdgeKind1"}] }, "assert": {"node_ids": ["m"]} + }, + { + "name": "ANY quantifier over a concatenated array expression", + "cypher": "WITH [1, 2] AS nums MATCH (n:NodeKind1) WHERE ANY(num IN nums + [3] WHERE num = 3) RETURN n", + "fixture": { + "nodes": [ + {"id": "n", "kinds": ["NodeKind1"], "properties": {"name": "quantifier-array"}} + ], + "edges": [] + }, + "assert": {"node_ids": ["n"]} } ] } diff --git a/integration/testdata/cases/shortest_paths_inline.json b/integration/testdata/cases/shortest_paths_inline.json index 4ef329bb..ddfe5352 100644 --- a/integration/testdata/cases/shortest_paths_inline.json +++ b/integration/testdata/cases/shortest_paths_inline.json @@ -89,6 +89,25 @@ ] }, "assert": {"row_count": 1, "path_node_ids": [["a", "b", "c"]]} + }, + { + "name": "shortest path with aliased group aggregation orders by user count", + "cypher": "MATCH p=shortestPath((u:User)-[:MemberOf*1..]->(g:Group)) WITH DISTINCT g AS Group, COUNT(u) AS UserCount RETURN Group.name, UserCount ORDER BY UserCount DESC LIMIT 5", + "fixture": { + "nodes": [ + {"id": "u1", "kinds": ["User"], "properties": {"name": "u1"}}, + {"id": "u2", "kinds": ["User"], "properties": {"name": "u2"}}, + {"id": "u3", "kinds": ["User"], "properties": {"name": "u3"}}, + {"id": "admins", "kinds": ["Group"], "properties": {"name": "admins"}}, + {"id": "staff", "kinds": ["Group"], "properties": {"name": "staff"}} + ], + "edges": [ + {"start_id": "u1", "end_id": "admins", "kind": "MemberOf"}, + {"start_id": "u2", "end_id": "admins", "kind": "MemberOf"}, + {"start_id": "u3", "end_id": "staff", "kind": "MemberOf"} + ] + }, + "assert": {"ordered_row_values": [["admins", 2], ["staff", 1]]} } ] } diff --git a/integration/testdata/cases/temporal_inline.json b/integration/testdata/cases/temporal_inline.json new file mode 100644 index 00000000..72a4c6ee --- /dev/null +++ b/integration/testdata/cases/temporal_inline.json @@ -0,0 +1,29 @@ +{ + "cases": [ + { + "name": "filter relationship timestamp property against datetime minus duration", + "cypher": "MATCH p=(:Computer)-[r:HasSession]->(:User) WHERE r.lastseen >= datetime() - duration('P3D') RETURN p LIMIT 100", + "fixture": { + "nodes": [ + {"id": "recent-computer", "kinds": ["Computer"], "properties": {"name": "recent-computer"}}, + {"id": "recent-user", "kinds": ["User"], "properties": {"name": "recent-user"}}, + {"id": "stale-computer", "kinds": ["Computer"], "properties": {"name": "stale-computer"}}, + {"id": "stale-user", "kinds": ["User"], "properties": {"name": "stale-user"}} + ], + "edges": [ + {"start_id": "recent-computer", "end_id": "recent-user", "kind": "HasSession", "properties": {"lastseen": "2999-01-01T00:00:00Z"}}, + {"start_id": "stale-computer", "end_id": "stale-user", "kind": "HasSession", "properties": {"lastseen": "2000-01-01T00:00:00Z"}} + ] + }, + "assert": { + "row_count": 1, + "contains_edge": { + "start": "recent-computer", + "end": "recent-user", + "kind": "HasSession", + "props": {"lastseen": "2999-01-01T00:00:00Z"} + } + } + } + ] +}