From 2e2a4166e036193204e561b15a21dc3a1bccf3cd Mon Sep 17 00:00:00 2001 From: John Hopper Date: Tue, 19 May 2026 11:30:31 -0700 Subject: [PATCH 001/114] fix(pgsql): support aggregate operator projections --- .../translation_cases/scalar_aggregation.sql | 6 ++++ .../testdata/cases/aggregation_inline.json | 30 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql b/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql index 755a4eb0..36163e76 100644 --- a/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql +++ b/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql @@ -92,9 +92,15 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from -- case: MATCH (n) RETURN count(n) AS total ORDER BY total DESC with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select count(s0.n0)::int8 as total from s0 order by total desc; +-- case: MATCH (n) RETURN toint(n.value) + count(n) +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (((s0.n0).properties ->> 'value'))::int8 + count(s0.n0)::int8 from s0 group by (((s0.n0).properties ->> 'value'))::int8; + -- case: MATCH (n) WITH toInteger(n.value) AS value, count(n) AS node_count RETURN value + node_count with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (((s1.n0).properties ->> 'value'))::int8 as i0, count(s1.n0)::int8 as i1 from s1 group by (((s1.n0).properties ->> 'value'))::int8) select s0.i0 + s0.i1 from s0; +-- case: MATCH (n) WITH toint(n.value) + count(n) AS score RETURN score +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (((s1.n0).properties ->> 'value'))::int8 + count(s1.n0)::int8 as i0 from s1 group by (((s1.n0).properties ->> 'value'))::int8) select s0.i0 as score from s0; + -- case: MATCH (n) WITH toInteger(n.value) AS value, count(n) AS node_count WITH value + node_count AS score RETURN score with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (((s1.n0).properties ->> 'value'))::int8 as i0, count(s1.n0)::int8 as i1 from s1 group by (((s1.n0).properties ->> 'value'))::int8), s2 as (select s0.i0 + s0.i1 as i2 from s0) select s2.i2 as score from s2; diff --git a/integration/testdata/cases/aggregation_inline.json b/integration/testdata/cases/aggregation_inline.json index e78589b8..c76bc073 100644 --- a/integration/testdata/cases/aggregation_inline.json +++ b/integration/testdata/cases/aggregation_inline.json @@ -202,7 +202,20 @@ } }, { - "name": "add grouped property expression to aggregate count", + "name": "add grouped property expression to aggregate count inline", + "cypher": "MATCH (n) RETURN toint(n.value) + count(n)", + "fixture": { + "nodes": [ + {"id": "a", "kinds": ["NodeKind1"], "properties": {"value": 10}}, + {"id": "b", "kinds": ["NodeKind1"], "properties": {"value": 10}}, + {"id": "c", "kinds": ["NodeKind1"], "properties": {"value": 20}} + ], + "edges": [] + }, + "assert": {"scalar_values": [12, 21]} + }, + { + "name": "add grouped property expression to aggregate count through aliases", "cypher": "MATCH (n) WITH toInteger(n.value) AS value, count(n) AS node_count RETURN value + node_count", "fixture": { "nodes": [ @@ -215,7 +228,20 @@ "assert": {"scalar_values": [12, 21]} }, { - "name": "project grouped property plus aggregate through with", + "name": "project grouped property plus aggregate through with inline expression", + "cypher": "MATCH (n) WITH toint(n.value) + count(n) AS score RETURN score", + "fixture": { + "nodes": [ + {"id": "a", "kinds": ["NodeKind1"], "properties": {"value": 10}}, + {"id": "b", "kinds": ["NodeKind1"], "properties": {"value": 10}}, + {"id": "c", "kinds": ["NodeKind1"], "properties": {"value": 20}} + ], + "edges": [] + }, + "assert": {"scalar_values": [12, 21]} + }, + { + "name": "project grouped property plus aggregate through with aliases", "cypher": "MATCH (n) WITH toInteger(n.value) AS value, count(n) AS node_count WITH value + node_count AS score RETURN score", "fixture": { "nodes": [ From 404d4b69cb3502c03f9288ecdc839cd40c24bfb4 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 08:10:47 -0700 Subject: [PATCH 002/114] fix(cypher): prefer supported integer conversion --- .../pgsql/test/translation_cases/scalar_aggregation.sql | 4 ++-- integration/testdata/cases/aggregation_inline.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql b/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql index 36163e76..d4668bff 100644 --- a/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql +++ b/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql @@ -92,13 +92,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from -- case: MATCH (n) RETURN count(n) AS total ORDER BY total DESC with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select count(s0.n0)::int8 as total from s0 order by total desc; --- case: MATCH (n) RETURN toint(n.value) + count(n) +-- case: MATCH (n) RETURN toInteger(n.value) + count(n) with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (((s0.n0).properties ->> 'value'))::int8 + count(s0.n0)::int8 from s0 group by (((s0.n0).properties ->> 'value'))::int8; -- case: MATCH (n) WITH toInteger(n.value) AS value, count(n) AS node_count RETURN value + node_count with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (((s1.n0).properties ->> 'value'))::int8 as i0, count(s1.n0)::int8 as i1 from s1 group by (((s1.n0).properties ->> 'value'))::int8) select s0.i0 + s0.i1 from s0; --- case: MATCH (n) WITH toint(n.value) + count(n) AS score RETURN score +-- case: MATCH (n) WITH toInteger(n.value) + count(n) AS score RETURN score with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (((s1.n0).properties ->> 'value'))::int8 + count(s1.n0)::int8 as i0 from s1 group by (((s1.n0).properties ->> 'value'))::int8) select s0.i0 as score from s0; -- case: MATCH (n) WITH toInteger(n.value) AS value, count(n) AS node_count WITH value + node_count AS score RETURN score diff --git a/integration/testdata/cases/aggregation_inline.json b/integration/testdata/cases/aggregation_inline.json index c76bc073..a624f197 100644 --- a/integration/testdata/cases/aggregation_inline.json +++ b/integration/testdata/cases/aggregation_inline.json @@ -203,7 +203,7 @@ }, { "name": "add grouped property expression to aggregate count inline", - "cypher": "MATCH (n) RETURN toint(n.value) + count(n)", + "cypher": "MATCH (n) RETURN toInteger(n.value) + count(n)", "fixture": { "nodes": [ {"id": "a", "kinds": ["NodeKind1"], "properties": {"value": 10}}, @@ -229,7 +229,7 @@ }, { "name": "project grouped property plus aggregate through with inline expression", - "cypher": "MATCH (n) WITH toint(n.value) + count(n) AS score RETURN score", + "cypher": "MATCH (n) WITH toInteger(n.value) + count(n) AS score RETURN score", "fixture": { "nodes": [ {"id": "a", "kinds": ["NodeKind1"], "properties": {"value": 10}}, From 3c77e327d07ce9afec0db012f89bee33cbbc27ca Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:03:26 -0700 Subject: [PATCH 003/114] docs(pgsql): capture optimizer pass plan --- docs/optimization-pass-memory.md | 160 +++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/optimization-pass-memory.md diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md new file mode 100644 index 00000000..d53016d0 --- /dev/null +++ b/docs/optimization-pass-memory.md @@ -0,0 +1,160 @@ +# Cypher Optimization Pass Memory + +## Purpose + +The PostgreSQL translator currently lowers Cypher traversal parts mostly in source order. That is simple and predictable, but it can produce expensive SQL for multi-part path queries where a later pattern contains more selective anchors or where returned path payloads are carried through unrelated expansions. + +This note captures a conservative plan for introducing a PostgreSQL-specific pre-translation optimization phase. The goal is not to require users to reauthor valid Cypher to get acceptable runtime behavior. + +## Motivating Query Shape + +```cypher +MATCH (n:Group) +WHERE n.objectid = 'S-1-5-21-2643190041-1319121918-239771340-513' +MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) +MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) +WHERE ct.authenticationenabled = true +AND ct.requiresmanagerapproval = false +AND ct.enrolleesuppliessubject = true +AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) +RETURN p1, p2 +``` + +The current PostgreSQL shape can preserve too much intermediate state. In particular, because `p1` is returned, path-related state from `p1` may be carried through the `p2` expansion before `p2` has been filtered. Neo4j's planner is more flexible: it can reorder pattern evaluation, use endpoint-aware expansion, and materialize path values late. + +## Architectural Decisions + +The first optimizer effort is intentionally PostgreSQL-specific. The optimizer should avoid painting the project into a backend-neutral corner, but PostgreSQL is the only Cypher translation target that currently needs this work. + +- Ship optimizer rules directly once they are covered. Do not add a user-facing feature flag surface for optimizer behavior. +- Optimize only read-only `MATCH` and `WHERE` groups inside a single query part for the first milestone. +- Treat `WITH`, `RETURN`, aggregation, `DISTINCT`, `ORDER BY`, `LIMIT`, `UNWIND`, `OPTIONAL MATCH`, writes, and procedure calls as semantic barriers. +- Allow the optimizer to build a new ordered logical plan inside eligible regions. +- Represent path variables as late-materialized recipes throughout the optimized PostgreSQL logical model. +- Use deterministic heuristics for early reordering. Defer schema statistics and cost-based planning. +- Accept more complex SQL when it materially improves runtime conditions. The database is responsible for executing the improved plan shape. +- Defer broad benchmark and real-world query set definition until after the basic framework and first optimizer rules are in place. + +## Safety Constraints + +Keep the first implementation deliberately conservative. + +- Preserve Cypher row semantics, path relationship uniqueness, variable binding rules, and zero-length expansion behavior. +- Keep each optimization rule individually named and testable. +- If a rule cannot prove a rewrite is safe, keep the original logical order for that part of the plan. +- Require translation-shape tests, PostgreSQL integration equivalence, and Neo4j equivalence coverage for optimizer behavior. + +## Sequenced Plan + +### Phase 1: Define Optimizer Boundaries + +Document the Cypher regions eligible for optimization and the barriers that terminate an optimization region. The initial eligible region should be a read-only sequence of `MATCH` and `WHERE` clauses within one query part. + +Add diagnostics that can print the logical pattern parts, bindings, predicates, path variables, and final projection dependencies before translation. + +### Phase 2: Build The Safety Net + +Add translation-shape coverage for the motivating ADCS query. The first tests should capture the current expensive SQL shape so improvements can be measured. + +Add smaller focused cases for: + +- multiple `MATCH` clauses sharing variables +- returned path variables used only at final projection +- variable-length expansion followed by a fixed suffix +- repeated bound variables such as `(ca)` and `(d)` +- zero-length expansion with `*0..` + +Validate optimizer behavior with all three test classes: + +- translation-shape tests +- PostgreSQL integration equivalence tests +- Neo4j integration equivalence tests + +Neo4j tests should assert result shape and semantics, not exact Neo4j plan shape. + +### Phase 3: Introduce A No-Op Optimizer Skeleton + +Insert a PostgreSQL-specific pre-translation logical optimization pass between parsing/semantic modeling and PostgreSQL rendering. + +The initial pass should return the same logical model it receives. This keeps the integration point small and gives tests a stable hook before behavioral rules are added. + +Suggested rule names: + +- `PredicateAttachment` +- `ProjectionPruning` +- `LatePathMaterialization` +- `ExpandIntoDetection` +- `ConservativePatternReordering` +- `VariableExpansionTerminalPushdown` + +### Phase 4: Attach Predicates To Their Bindings + +Move eligible `WHERE` predicates as close as possible to the bindings they reference. + +For the motivating query, the `ct.*` predicates should be owned by the `ct:CertTemplate` binding. This does not need to reorder pattern matching at first; it makes predicate dependencies explicit so later rules can apply filters earlier. + +### Phase 5: Prune Intermediate Projections And Paths + +Compute a narrower carry set for each operation: + +- bindings needed by the next operation +- bindings needed by predicates +- bindings needed as join keys +- bindings needed later only to construct returned values + +The translator should not carry every visible binding through every later expansion just because it appears in the final `RETURN`. + +This should be the first real runtime-focused optimization rule. It directly addresses the reported query shape, creates the liveness information required by later rules, and is lower risk than traversal reordering or suffix pushdown. + +### Phase 6: Materialize Paths Late + +Represent returned paths internally as recipes over node and relationship bindings rather than as fully materialized values throughout every step. + +For the motivating query, the optimizer should be able to continue from a narrow frame after `p1`, such as distinct `(n, ca, d)`, evaluate and filter `p2`, then join back to the full `p1` rows and materialize `p1` and `p2` at the final projection. + +This is the first high-value optimization target because it reduces row width and delays the `p1 x p2` multiplication without changing the user's Cypher. + +### Phase 7: Detect Expand-Into Opportunities + +When both endpoints of a relationship or variable-length segment are known, lower that segment as a constrained connectivity/path problem instead of an open expansion. + +This mirrors Neo4j's `Expand(Into)` and `VarLengthExpand(Into)` behavior and is especially relevant when separate `MATCH` clauses bind endpoints that are reused later. + +### Phase 8: Add Deterministic Pattern Reordering + +After projection pruning and late materialization are stable, allow limited reordering inside a single read-only optimization region. + +Start with obvious anchors: + +- node label plus equality property +- fixed relationship type scans +- already-bound endpoints +- selective labels or properties only when deterministic local information is available + +Do not begin with a general cost-based planner or schema-statistics dependency. Prefer deterministic rewrites with clear safety checks. + +### Phase 9: Push Terminal Constraints Into Variable Expansions + +For variable-length expansions followed by fixed suffixes, add terminal or suffix constraints as semi-joins or correlated existence checks. + +For the motivating query, this means avoiding emission of `MemberOf*0..` endpoints that cannot reach an eligible `CertTemplate` published to the already-bound `ca`, and avoiding `RootCA` endpoints that cannot connect back to the already-bound `d`. + +### Phase 10: Measure Each Rule Locally + +Every optimization rule should include: + +- unit or translation tests for the logical rewrite +- PostgreSQL integration result-equivalence coverage +- Neo4j integration result-equivalence coverage +- SQL shape assertions for representative queries +- before and after `EXPLAIN` comparison on synthetic fanout data + +The synthetic data should include many `p1` paths to the same `(n, ca, d)`, many membership paths from `n`, and only a small number of eligible certificate template and root CA paths. + +Broader benchmark suites and real-world query collections are deferred until after the basic optimizer framework and first rules are implemented. + +## Recommended First Milestone + +Implement phases 1 through 6 first. + +That milestone establishes the PostgreSQL optimizer framework, test bar, predicate ownership, projection and path pruning, and late path materialization. It should improve the reported query shape without taking on endpoint-aware expansion, suffix semi-joins, schema statistics, or a full cost-based planner. From 25a74884bed312d6fca7b26e431f1902bcf0e9a0 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:06:10 -0700 Subject: [PATCH 004/114] feat(pgsql): analyze optimizer regions --- cypher/models/pgsql/optimize/analysis.go | 588 ++++++++++++++++++ cypher/models/pgsql/optimize/analysis_test.go | 136 ++++ 2 files changed, 724 insertions(+) create mode 100644 cypher/models/pgsql/optimize/analysis.go create mode 100644 cypher/models/pgsql/optimize/analysis_test.go diff --git a/cypher/models/pgsql/optimize/analysis.go b/cypher/models/pgsql/optimize/analysis.go new file mode 100644 index 00000000..887e37a0 --- /dev/null +++ b/cypher/models/pgsql/optimize/analysis.go @@ -0,0 +1,588 @@ +package optimize + +import ( + "fmt" + "sort" + "strings" + + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/walk" +) + +type QueryPartKind string + +const ( + QueryPartKindSingle QueryPartKind = "single" + QueryPartKindMulti QueryPartKind = "multi" +) + +type BarrierKind string + +const ( + BarrierKindReturn BarrierKind = "return" + BarrierKindWith BarrierKind = "with" + BarrierKindUnwind BarrierKind = "unwind" + BarrierKindOptionalMatch BarrierKind = "optional_match" + BarrierKindUpdate BarrierKind = "update" +) + +type BindingKind string + +const ( + BindingKindNode BindingKind = "node" + BindingKindRelationship BindingKind = "relationship" + BindingKindPath BindingKind = "path" +) + +type Analysis struct { + QueryParts []QueryPart +} + +type QueryPart struct { + Index int + Kind QueryPartKind + Regions []Region + Barriers []Barrier + ProjectionDependencies []string +} + +type Region struct { + QueryPartIndex int + StartClause int + EndClause int + Clauses []MatchClause + Bindings []Binding + PathVariables []PathVariable + Predicates []Predicate +} + +type MatchClause struct { + Index int + PatternCount int + WherePredicate int +} + +type Barrier struct { + QueryPartIndex int + ClauseIndex int + Kind BarrierKind + Dependencies []string +} + +type Binding struct { + Symbol string + Kind BindingKind + ClauseIndex int + PatternIndex int +} + +type PathVariable struct { + Symbol string + ClauseIndex int + PatternIndex int + NodeCount int + RelationshipCount int + VariableLength bool + Dependencies []string +} + +type Predicate struct { + ClauseIndex int + ExpressionIndex int + Dependencies []string +} + +func Analyze(query *cypher.RegularQuery) Analysis { + if query == nil || query.SingleQuery == nil { + return Analysis{} + } + + if query.SingleQuery.MultiPartQuery != nil { + return analyzeMultiPartQuery(query.SingleQuery.MultiPartQuery) + } + + if query.SingleQuery.SinglePartQuery != nil { + return Analysis{ + QueryParts: []QueryPart{ + analyzeSinglePartQuery(0, QueryPartKindSingle, query.SingleQuery.SinglePartQuery), + }, + } + } + + return Analysis{} +} + +func (s Analysis) Diagnostics() []string { + var lines []string + + for _, queryPart := range s.QueryParts { + lines = append(lines, fmt.Sprintf( + "query_part[%d] kind=%s projection_deps=%s", + queryPart.Index, + queryPart.Kind, + strings.Join(queryPart.ProjectionDependencies, ","), + )) + + for regionIndex, region := range queryPart.Regions { + lines = append(lines, fmt.Sprintf( + "region[%d] part=%d clauses=%d..%d matches=%d bindings=%s paths=%s predicates=%s", + regionIndex, + region.QueryPartIndex, + region.StartClause, + region.EndClause, + len(region.Clauses), + formatBindings(region.Bindings), + formatPathVariables(region.PathVariables), + formatPredicates(region.Predicates), + )) + } + + for barrierIndex, barrier := range queryPart.Barriers { + lines = append(lines, fmt.Sprintf( + "barrier[%d] part=%d clause=%d kind=%s deps=%s", + barrierIndex, + barrier.QueryPartIndex, + barrier.ClauseIndex, + barrier.Kind, + strings.Join(barrier.Dependencies, ","), + )) + } + } + + return lines +} + +func (s Analysis) String() string { + return strings.Join(s.Diagnostics(), "\n") +} + +func analyzeMultiPartQuery(query *cypher.MultiPartQuery) Analysis { + var analysis Analysis + + for idx, part := range query.Parts { + analysis.QueryParts = append(analysis.QueryParts, analyzeMultiPartQueryPart(idx, part)) + } + + if query.SinglePartQuery != nil { + analysis.QueryParts = append(analysis.QueryParts, analyzeSinglePartQuery(len(query.Parts), QueryPartKindSingle, query.SinglePartQuery)) + } + + return analysis +} + +func analyzeMultiPartQueryPart(index int, part *cypher.MultiPartQueryPart) QueryPart { + queryPart := QueryPart{ + Index: index, + Kind: QueryPartKindMulti, + } + + queryPart.Regions, queryPart.Barriers = analyzeReadingClauses(index, part.ReadingClauses) + + if len(part.UpdatingClauses) > 0 { + queryPart.Barriers = append(queryPart.Barriers, Barrier{ + QueryPartIndex: index, + ClauseIndex: len(part.ReadingClauses), + Kind: BarrierKindUpdate, + }) + } + + if part.With != nil { + queryPart.ProjectionDependencies = projectionDependencies(part.With.Projection) + queryPart.Barriers = append(queryPart.Barriers, Barrier{ + QueryPartIndex: index, + ClauseIndex: len(part.ReadingClauses) + len(part.UpdatingClauses), + Kind: BarrierKindWith, + Dependencies: queryPart.ProjectionDependencies, + }) + } + + return queryPart +} + +func analyzeSinglePartQuery(index int, kind QueryPartKind, part *cypher.SinglePartQuery) QueryPart { + queryPart := QueryPart{ + Index: index, + Kind: kind, + } + + queryPart.Regions, queryPart.Barriers = analyzeReadingClauses(index, part.ReadingClauses) + + if len(part.UpdatingClauses) > 0 { + queryPart.Barriers = append(queryPart.Barriers, Barrier{ + QueryPartIndex: index, + ClauseIndex: len(part.ReadingClauses), + Kind: BarrierKindUpdate, + }) + } + + if part.Return != nil { + queryPart.ProjectionDependencies = projectionDependencies(part.Return.Projection) + queryPart.Barriers = append(queryPart.Barriers, Barrier{ + QueryPartIndex: index, + ClauseIndex: len(part.ReadingClauses) + len(part.UpdatingClauses), + Kind: BarrierKindReturn, + Dependencies: queryPart.ProjectionDependencies, + }) + } + + return queryPart +} + +func analyzeReadingClauses(queryPartIndex int, readingClauses []*cypher.ReadingClause) ([]Region, []Barrier) { + var ( + regions []Region + barriers []Barrier + currentRegion *Region + ) + + closeRegion := func() { + if currentRegion != nil { + regions = append(regions, *currentRegion) + currentRegion = nil + } + } + + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Unwind != nil { + closeRegion() + barriers = append(barriers, Barrier{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + Kind: BarrierKindUnwind, + Dependencies: dependenciesForReadingClause(readingClause), + }) + continue + } + + match := readingClause.Match + if match == nil { + continue + } + + if match.Optional { + closeRegion() + barriers = append(barriers, Barrier{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + Kind: BarrierKindOptionalMatch, + Dependencies: dependenciesForMatch(match), + }) + continue + } + + if currentRegion == nil { + currentRegion = &Region{ + QueryPartIndex: queryPartIndex, + StartClause: clauseIndex, + EndClause: clauseIndex, + } + } + + currentRegion.EndClause = clauseIndex + currentRegion.Clauses = append(currentRegion.Clauses, MatchClause{ + Index: clauseIndex, + PatternCount: len(match.Pattern), + WherePredicate: wherePredicateCount(match.Where), + }) + currentRegion.Bindings = mergeBindings(currentRegion.Bindings, bindingsForMatch(clauseIndex, match)) + currentRegion.PathVariables = mergePathVariables(currentRegion.PathVariables, pathVariablesForMatch(clauseIndex, match)) + currentRegion.Predicates = append(currentRegion.Predicates, predicatesForWhere(clauseIndex, match.Where)...) + } + + closeRegion() + + return regions, barriers +} + +func dependenciesForReadingClause(readingClause *cypher.ReadingClause) []string { + if readingClause == nil { + return nil + } + + if readingClause.Match != nil { + return dependenciesForMatch(readingClause.Match) + } + + if readingClause.Unwind != nil { + return sortedDependencies(readingClause.Unwind.Expression) + } + + return nil +} + +func dependenciesForMatch(match *cypher.Match) []string { + var deps []string + + if match == nil { + return nil + } + + for _, predicate := range predicatesForWhere(0, match.Where) { + deps = append(deps, predicate.Dependencies...) + } + + return sortedUniqueStrings(deps) +} + +func bindingsForMatch(clauseIndex int, match *cypher.Match) []Binding { + var bindings []Binding + + for patternIndex, pattern := range match.Pattern { + if pattern == nil { + continue + } + + if pattern.Variable != nil && pattern.Variable.Symbol != "" { + bindings = append(bindings, Binding{ + Symbol: pattern.Variable.Symbol, + Kind: BindingKindPath, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }) + } + + for _, element := range pattern.PatternElements { + if nodePattern, isNodePattern := element.AsNodePattern(); isNodePattern { + if nodePattern.Variable != nil && nodePattern.Variable.Symbol != "" { + bindings = append(bindings, Binding{ + Symbol: nodePattern.Variable.Symbol, + Kind: BindingKindNode, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }) + } + } else if relationshipPattern, isRelationshipPattern := element.AsRelationshipPattern(); isRelationshipPattern { + if relationshipPattern.Variable != nil && relationshipPattern.Variable.Symbol != "" { + bindings = append(bindings, Binding{ + Symbol: relationshipPattern.Variable.Symbol, + Kind: BindingKindRelationship, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }) + } + } + } + } + + return bindings +} + +func pathVariablesForMatch(clauseIndex int, match *cypher.Match) []PathVariable { + var pathVariables []PathVariable + + for patternIndex, pattern := range match.Pattern { + if pattern == nil || pattern.Variable == nil || pattern.Variable.Symbol == "" { + continue + } + + pathVariable := PathVariable{ + Symbol: pattern.Variable.Symbol, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + Dependencies: patternDependencies(pattern), + } + + for _, element := range pattern.PatternElements { + if element.IsNodePattern() { + pathVariable.NodeCount++ + } else if relationshipPattern, isRelationshipPattern := element.AsRelationshipPattern(); isRelationshipPattern { + pathVariable.RelationshipCount++ + + if relationshipPattern.Range != nil { + pathVariable.VariableLength = true + } + } + } + + pathVariables = append(pathVariables, pathVariable) + } + + return pathVariables +} + +func patternDependencies(pattern *cypher.PatternPart) []string { + var dependencies []string + + for _, element := range pattern.PatternElements { + if nodePattern, isNodePattern := element.AsNodePattern(); isNodePattern { + if nodePattern.Variable != nil && nodePattern.Variable.Symbol != "" { + dependencies = append(dependencies, nodePattern.Variable.Symbol) + } + } else if relationshipPattern, isRelationshipPattern := element.AsRelationshipPattern(); isRelationshipPattern { + if relationshipPattern.Variable != nil && relationshipPattern.Variable.Symbol != "" { + dependencies = append(dependencies, relationshipPattern.Variable.Symbol) + } + } + } + + return sortedUniqueStrings(dependencies) +} + +func predicatesForWhere(clauseIndex int, where *cypher.Where) []Predicate { + if where == nil { + return nil + } + + var predicates []Predicate + for expressionIndex, expression := range where.GetAll() { + predicates = append(predicates, Predicate{ + ClauseIndex: clauseIndex, + ExpressionIndex: expressionIndex, + Dependencies: sortedDependencies(expression), + }) + } + + return predicates +} + +func projectionDependencies(projection *cypher.Projection) []string { + if projection == nil { + return nil + } + + var dependencies []string + for _, item := range projection.Items { + dependencies = append(dependencies, sortedDependencies(item)...) + } + + if projection.Order != nil { + dependencies = append(dependencies, sortedDependencies(projection.Order)...) + } + + if projection.Skip != nil { + dependencies = append(dependencies, sortedDependencies(projection.Skip)...) + } + + if projection.Limit != nil { + dependencies = append(dependencies, sortedDependencies(projection.Limit)...) + } + + return sortedUniqueStrings(dependencies) +} + +func sortedDependencies(node cypher.SyntaxNode) []string { + dependencies := map[string]struct{}{} + + if node == nil { + return nil + } + + _ = walk.Cypher(node, walk.NewSimpleVisitor[cypher.SyntaxNode](func(node cypher.SyntaxNode, _ walk.VisitorHandler) { + if variable, isVariable := node.(*cypher.Variable); isVariable && variable.Symbol != "" && variable.Symbol != cypher.TokenLiteralAsterisk { + dependencies[variable.Symbol] = struct{}{} + } + })) + + return sortedMapKeys(dependencies) +} + +func wherePredicateCount(where *cypher.Where) int { + if where == nil { + return 0 + } + + return where.Len() +} + +func mergeBindings(existing []Binding, next []Binding) []Binding { + seen := map[string]struct{}{} + for _, binding := range existing { + seen[bindingKey(binding)] = struct{}{} + } + + for _, binding := range next { + key := bindingKey(binding) + if _, hasBinding := seen[key]; hasBinding { + continue + } + + existing = append(existing, binding) + seen[key] = struct{}{} + } + + return existing +} + +func mergePathVariables(existing []PathVariable, next []PathVariable) []PathVariable { + seen := map[string]struct{}{} + for _, pathVariable := range existing { + seen[pathVariable.Symbol] = struct{}{} + } + + for _, pathVariable := range next { + if _, hasPathVariable := seen[pathVariable.Symbol]; hasPathVariable { + continue + } + + existing = append(existing, pathVariable) + seen[pathVariable.Symbol] = struct{}{} + } + + return existing +} + +func bindingKey(binding Binding) string { + return string(binding.Kind) + ":" + binding.Symbol +} + +func sortedMapKeys(values map[string]struct{}) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + + sort.Strings(keys) + + return keys +} + +func sortedUniqueStrings(values []string) []string { + seen := map[string]struct{}{} + + for _, value := range values { + if value != "" { + seen[value] = struct{}{} + } + } + + return sortedMapKeys(seen) +} + +func formatBindings(bindings []Binding) string { + if len(bindings) == 0 { + return "" + } + + items := make([]string, 0, len(bindings)) + for _, binding := range bindings { + items = append(items, fmt.Sprintf("%s:%s", binding.Symbol, binding.Kind)) + } + + return strings.Join(items, ",") +} + +func formatPathVariables(pathVariables []PathVariable) string { + if len(pathVariables) == 0 { + return "" + } + + items := make([]string, 0, len(pathVariables)) + for _, pathVariable := range pathVariables { + items = append(items, pathVariable.Symbol) + } + + return strings.Join(items, ",") +} + +func formatPredicates(predicates []Predicate) string { + if len(predicates) == 0 { + return "" + } + + items := make([]string, 0, len(predicates)) + for _, predicate := range predicates { + items = append(items, strings.Join(predicate.Dependencies, "|")) + } + + return strings.Join(items, ",") +} diff --git a/cypher/models/pgsql/optimize/analysis_test.go b/cypher/models/pgsql/optimize/analysis_test.go new file mode 100644 index 00000000..eb41ea0f --- /dev/null +++ b/cypher/models/pgsql/optimize/analysis_test.go @@ -0,0 +1,136 @@ +package optimize + +import ( + "strings" + "testing" + + "github.com/specterops/dawgs/cypher/frontend" + "github.com/stretchr/testify/require" +) + +const adcsQuery = ` +MATCH (n:Group) +WHERE n.objectid = 'S-1-5-21-2643190041-1319121918-239771340-513' +MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) +MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) +WHERE ct.authenticationenabled = true +AND ct.requiresmanagerapproval = false +AND ct.enrolleesuppliessubject = true +AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) +RETURN p1, p2 +` + +func analyzeCypher(t *testing.T, query string) Analysis { + t.Helper() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), query) + require.NoError(t, err) + + return Analyze(regularQuery) +} + +func requireBinding(t *testing.T, bindings []Binding, symbol string, kind BindingKind) { + t.Helper() + + for _, binding := range bindings { + if binding.Symbol == symbol && binding.Kind == kind { + return + } + } + + t.Fatalf("expected binding %s:%s in %#v", symbol, kind, bindings) +} + +func requirePathVariable(t *testing.T, pathVariables []PathVariable, symbol string, relationshipCount int) { + t.Helper() + + for _, pathVariable := range pathVariables { + if pathVariable.Symbol == symbol { + require.Equal(t, relationshipCount, pathVariable.RelationshipCount) + require.True(t, pathVariable.VariableLength) + return + } + } + + t.Fatalf("expected path variable %s in %#v", symbol, pathVariables) +} + +func TestAnalyzeIdentifiesEligibleADCSRegion(t *testing.T) { + t.Parallel() + + analysis := analyzeCypher(t, adcsQuery) + + require.Len(t, analysis.QueryParts, 1) + + queryPart := analysis.QueryParts[0] + require.Equal(t, QueryPartKindSingle, queryPart.Kind) + require.Equal(t, []string{"p1", "p2"}, queryPart.ProjectionDependencies) + require.Len(t, queryPart.Regions, 1) + require.Len(t, queryPart.Barriers, 1) + require.Equal(t, BarrierKindReturn, queryPart.Barriers[0].Kind) + require.Equal(t, []string{"p1", "p2"}, queryPart.Barriers[0].Dependencies) + + region := queryPart.Regions[0] + require.Equal(t, 0, region.StartClause) + require.Equal(t, 2, region.EndClause) + require.Len(t, region.Clauses, 3) + require.Len(t, region.Predicates, 2) + require.Equal(t, []string{"n"}, region.Predicates[0].Dependencies) + require.Equal(t, []string{"ct"}, region.Predicates[1].Dependencies) + + requireBinding(t, region.Bindings, "n", BindingKindNode) + requireBinding(t, region.Bindings, "ca", BindingKindNode) + requireBinding(t, region.Bindings, "ct", BindingKindNode) + requireBinding(t, region.Bindings, "d", BindingKindNode) + requireBinding(t, region.Bindings, "p1", BindingKindPath) + requireBinding(t, region.Bindings, "p2", BindingKindPath) + + requirePathVariable(t, region.PathVariables, "p1", 4) + requirePathVariable(t, region.PathVariables, "p2", 5) +} + +func TestAnalyzeSegmentsRegionsAtSemanticBarriers(t *testing.T) { + t.Parallel() + + analysis := analyzeCypher(t, ` + MATCH (n:Group) + WITH n + MATCH (n)-[:MemberOf]->(m) + OPTIONAL MATCH (m)-[:MemberOf]->(x) + RETURN m + `) + + require.Len(t, analysis.QueryParts, 2) + + firstPart := analysis.QueryParts[0] + require.Equal(t, QueryPartKindMulti, firstPart.Kind) + require.Len(t, firstPart.Regions, 1) + require.Equal(t, []string{"n"}, firstPart.ProjectionDependencies) + require.Len(t, firstPart.Barriers, 1) + require.Equal(t, BarrierKindWith, firstPart.Barriers[0].Kind) + require.Equal(t, []string{"n"}, firstPart.Barriers[0].Dependencies) + + secondPart := analysis.QueryParts[1] + require.Equal(t, QueryPartKindSingle, secondPart.Kind) + require.Len(t, secondPart.Regions, 1) + require.Equal(t, 0, secondPart.Regions[0].StartClause) + require.Equal(t, 0, secondPart.Regions[0].EndClause) + require.Len(t, secondPart.Barriers, 2) + require.Equal(t, BarrierKindOptionalMatch, secondPart.Barriers[0].Kind) + require.Equal(t, BarrierKindReturn, secondPart.Barriers[1].Kind) + require.Equal(t, []string{"m"}, secondPart.ProjectionDependencies) +} + +func TestAnalysisDiagnosticsAreStable(t *testing.T) { + t.Parallel() + + analysis := analyzeCypher(t, adcsQuery) + diagnostics := strings.Join(analysis.Diagnostics(), "\n") + + require.Contains(t, diagnostics, "query_part[0] kind=single projection_deps=p1,p2") + require.Contains(t, diagnostics, "region[0] part=0 clauses=0..2 matches=3") + require.Contains(t, diagnostics, "bindings=n:node,p1:path,ca:node,d:node,p2:path,ct:node") + require.Contains(t, diagnostics, "paths=p1,p2") + require.Contains(t, diagnostics, "predicates=n,ct") + require.Contains(t, diagnostics, "barrier[0] part=0 clause=3 kind=return deps=p1,p2") +} From e57cac352b44f3c2393f70f38f8e7a0251e0ce15 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:07:31 -0700 Subject: [PATCH 005/114] test(pgsql): cover optimizer path safety --- .../pgsql/translate/optimizer_safety_test.go | 71 +++++++++++++++++++ .../testdata/cases/optimizer_inline.json | 41 +++++++++++ 2 files changed, 112 insertions(+) create mode 100644 cypher/models/pgsql/translate/optimizer_safety_test.go create mode 100644 integration/testdata/cases/optimizer_inline.json diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go new file mode 100644 index 00000000..f776318d --- /dev/null +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -0,0 +1,71 @@ +package translate + +import ( + "context" + "strings" + "testing" + + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/drivers/pg/pgutil" + "github.com/specterops/dawgs/graph" + "github.com/stretchr/testify/require" +) + +const optimizerADCSQuery = ` +MATCH (n:Group) +WHERE n.objectid = 'S-1-5-21-2643190041-1319121918-239771340-513' +MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) +MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) +WHERE ct.authenticationenabled = true +AND ct.requiresmanagerapproval = false +AND ct.enrolleesuppliessubject = true +AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) +RETURN p1, p2 +` + +func optimizerSafetyKindMapper() *pgutil.InMemoryKindMapper { + mapper := pgutil.NewInMemoryKindMapper() + + for _, kind := range graph.StringsToKinds([]string{ + "AllExtendedRights", + "CertTemplate", + "Domain", + "Enroll", + "EnterpriseCA", + "EnterpriseCAFor", + "GenericAll", + "Group", + "IssuedSignedBy", + "MemberOf", + "NTAuthStore", + "NTAuthStoreFor", + "PublishedTo", + "RootCA", + "RootCAFor", + "TrustedForNTAuth", + }) { + mapper.Put(kind) + } + + return mapper +} + +func TestOptimizerSafetyADCSQueryDocumentsCurrentCarryShape(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), optimizerADCSQuery) + require.NoError(t, err) + + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + require.NoError(t, err) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "select distinct (s5.n0).id as root_id from s5") + require.Contains(t, normalizedQuery, "s5.ep0 as ep0") + require.Contains(t, normalizedQuery, "s5.e0 as e0") + require.Contains(t, normalizedQuery, "from s5, s7") +} diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json new file mode 100644 index 00000000..a463de49 --- /dev/null +++ b/integration/testdata/cases/optimizer_inline.json @@ -0,0 +1,41 @@ +{ + "cases": [ + { + "name": "return two ADCS-style paths with shared CA and domain endpoints", + "cypher": "MATCH (n:Group) WHERE n.objectid = 'S-1-5-21-2643190041-1319121918-239771340-513' MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) WHERE ct.authenticationenabled = true AND ct.requiresmanagerapproval = false AND ct.enrolleesuppliessubject = true AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) RETURN p1, p2", + "fixture": { + "nodes": [ + {"id": "n", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-2643190041-1319121918-239771340-513"}}, + {"id": "p1-mid", "kinds": ["Group"]}, + {"id": "p2-mid", "kinds": ["Group"]}, + {"id": "ca", "kinds": ["EnterpriseCA"]}, + {"id": "store", "kinds": ["NTAuthStore"]}, + {"id": "domain", "kinds": ["Domain"]}, + {"id": "template", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 1, "authorizedsignatures": 1}}, + {"id": "root", "kinds": ["RootCA"]} + ], + "edges": [ + {"start_id": "n", "end_id": "p1-mid", "kind": "MemberOf"}, + {"start_id": "p1-mid", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "ca", "end_id": "store", "kind": "TrustedForNTAuth"}, + {"start_id": "store", "end_id": "domain", "kind": "NTAuthStoreFor"}, + {"start_id": "n", "end_id": "p2-mid", "kind": "MemberOf"}, + {"start_id": "p2-mid", "end_id": "template", "kind": "GenericAll"}, + {"start_id": "template", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} + ] + }, + "assert": { + "path_node_ids": [ + ["n", "p1-mid", "ca", "store", "domain"], + ["n", "p2-mid", "template", "ca", "root", "domain"] + ], + "path_edge_kinds": [ + ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + ["MemberOf", "GenericAll", "PublishedTo", "IssuedSignedBy", "RootCAFor"] + ] + } + } + ] +} From bd45f894e08bc9c0c94cd075f48cd9abdbf2113f Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:09:33 -0700 Subject: [PATCH 006/114] feat(pgsql): add optimizer pipeline hook --- cypher/models/cypher/copy.go | 12 ++++ cypher/models/cypher/copy_test.go | 48 +++++++++++++++ cypher/models/cypher/model.go | 52 ++++++++++++++++- cypher/models/pgsql/optimize/optimizer.go | 58 +++++++++++++++++++ .../models/pgsql/optimize/optimizer_test.go | 47 +++++++++++++++ cypher/models/pgsql/translate/translator.go | 8 ++- 6 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 cypher/models/pgsql/optimize/optimizer.go create mode 100644 cypher/models/pgsql/optimize/optimizer_test.go diff --git a/cypher/models/cypher/copy.go b/cypher/models/cypher/copy.go index 77802df1..d08b7c6c 100644 --- a/cypher/models/cypher/copy.go +++ b/cypher/models/cypher/copy.go @@ -110,6 +110,18 @@ func Copy[T any](value T, extensions ...CopyExtension[T]) T { case *Literal: return any(typedValue.copy()).(T) + case *Properties: + return any(typedValue.copy()).(T) + + case MapLiteral: + return any(typedValue.copy()).(T) + + case *ListLiteral: + return any(typedValue.copy()).(T) + + case *MapItem: + return any(typedValue.copy()).(T) + case *ReadingClause: return any(typedValue.copy()).(T) diff --git a/cypher/models/cypher/copy_test.go b/cypher/models/cypher/copy_test.go index 390c6949..a1c30d16 100644 --- a/cypher/models/cypher/copy_test.go +++ b/cypher/models/cypher/copy_test.go @@ -101,6 +101,22 @@ func TestCopy(t *testing.T) { validateCopy(t, &model2.Literal{ Null: true, }) + validateCopy(t, &model2.Properties{ + Map: model2.MapLiteral{ + "name": model2.NewStringLiteral("value"), + }, + }) + validateCopy(t, model2.MapLiteral{ + "name": model2.NewStringLiteral("value"), + }) + validateCopy(t, &model2.ListLiteral{ + model2.NewLiteral(1, false), + model2.NewLiteral(2, false), + }) + validateCopy(t, &model2.MapItem{ + Key: "name", + Value: model2.NewStringLiteral("value"), + }) validateCopy(t, &model2.Projection{ Distinct: true, All: true, @@ -150,3 +166,35 @@ func TestCopy(t *testing.T) { validateCopy(t, []string{}) validateCopy(t, graph.Kinds{}) } + +func TestCopyPatternVariablesAreIndependent(t *testing.T) { + original := &model2.PatternPart{ + Variable: model2.NewVariableWithSymbol("p"), + PatternElements: []*model2.PatternElement{ + { + Element: &model2.NodePattern{ + Variable: model2.NewVariableWithSymbol("n"), + }, + }, + { + Element: &model2.RelationshipPattern{ + Variable: model2.NewVariableWithSymbol("r"), + }, + }, + }, + } + + copied := model2.Copy(original) + copied.Variable.Symbol = "copied_path" + copiedNode, _ := copied.PatternElements[0].AsNodePattern() + copiedNode.Variable.Symbol = "copied_node" + copiedRelationship, _ := copied.PatternElements[1].AsRelationshipPattern() + copiedRelationship.Variable.Symbol = "copied_relationship" + + originalNode, _ := original.PatternElements[0].AsNodePattern() + originalRelationship, _ := original.PatternElements[1].AsRelationshipPattern() + + require.Equal(t, "p", original.Variable.Symbol) + require.Equal(t, "n", originalNode.Variable.Symbol) + require.Equal(t, "r", originalRelationship.Variable.Symbol) +} diff --git a/cypher/models/cypher/model.go b/cypher/models/cypher/model.go index 27cdd549..54001553 100644 --- a/cypher/models/cypher/model.go +++ b/cypher/models/cypher/model.go @@ -881,12 +881,36 @@ type MapItem struct { Value Expression } +func (s *MapItem) copy() *MapItem { + if s == nil { + return nil + } + + return &MapItem{ + Key: s.Key, + Value: Copy(s.Value), + } +} + type MapLiteral map[string]Expression func NewMapLiteral() MapLiteral { return MapLiteral{} } +func (s MapLiteral) copy() MapLiteral { + if s == nil { + return nil + } + + mapCopy := NewMapLiteral() + for key, value := range s { + mapCopy[key] = Copy(value) + } + + return mapCopy +} + func (s MapLiteral) Items() []*MapItem { items := make([]*MapItem, 0, len(s)) @@ -924,6 +948,17 @@ func NewListLiteral() *ListLiteral { return &ListLiteral{} } +func (s *ListLiteral) copy() *ListLiteral { + if s == nil { + return nil + } + + listCopy := NewListLiteral() + *listCopy = Copy([]Expression(*s)) + + return listCopy +} + func NewStringListLiteral(values []string) *ListLiteral { literal := NewListLiteral() @@ -1310,6 +1345,17 @@ func NewProperties() *Properties { return &Properties{} } +func (s *Properties) copy() *Properties { + if s == nil { + return nil + } + + return &Properties{ + Map: Copy(s.Map), + Parameter: Copy(s.Parameter), + } +} + // NodePattern Type // // Kinds is a conjunction of types for the given node. @@ -1328,7 +1374,7 @@ func (s *NodePattern) copy() *NodePattern { } return &NodePattern{ - Variable: s.Variable, + Variable: Copy(s.Variable), Kinds: Copy(s.Kinds), Properties: Copy(s.Properties), } @@ -1358,7 +1404,7 @@ func (s *RelationshipPattern) copy() *RelationshipPattern { } return &RelationshipPattern{ - Variable: s.Variable, + Variable: Copy(s.Variable), Kinds: Copy(s.Kinds), Direction: s.Direction, Range: Copy(s.Range), @@ -1492,7 +1538,7 @@ func (s *PatternPart) copy() *PatternPart { } return &PatternPart{ - Variable: s.Variable, + Variable: Copy(s.Variable), ShortestPathPattern: s.ShortestPathPattern, AllShortestPathsPattern: s.AllShortestPathsPattern, PatternElements: Copy(s.PatternElements), diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go new file mode 100644 index 00000000..a7dcbee7 --- /dev/null +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -0,0 +1,58 @@ +package optimize + +import "github.com/specterops/dawgs/cypher/models/cypher" + +type Rule interface { + Name() string + Apply(*Plan) error +} + +type RuleResult struct { + Name string + Applied bool +} + +type Plan struct { + Query *cypher.RegularQuery + Analysis Analysis + Rules []RuleResult +} + +type Optimizer struct { + rules []Rule +} + +func NewOptimizer(rules ...Rule) Optimizer { + return Optimizer{ + rules: rules, + } +} + +func Optimize(query *cypher.RegularQuery) (Plan, error) { + return NewOptimizer().Optimize(query) +} + +func (s Optimizer) Optimize(query *cypher.RegularQuery) (Plan, error) { + if query == nil { + return Plan{}, nil + } + + plan := Plan{ + Query: cypher.Copy(query), + } + plan.Analysis = Analyze(plan.Query) + + for _, rule := range s.rules { + if err := rule.Apply(&plan); err != nil { + return Plan{}, err + } + + plan.Rules = append(plan.Rules, RuleResult{ + Name: rule.Name(), + Applied: true, + }) + plan.Analysis = Analyze(plan.Query) + } + + return plan, nil +} diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go new file mode 100644 index 00000000..ca5846ab --- /dev/null +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -0,0 +1,47 @@ +package optimize + +import ( + "testing" + + "github.com/specterops/dawgs/cypher/frontend" + "github.com/stretchr/testify/require" +) + +type testRule struct { + name string +} + +func (s testRule) Name() string { + return s.name +} + +func (s testRule) Apply(plan *Plan) error { + return nil +} + +func TestOptimizeCopiesAndAnalyzesQuery(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), adcsQuery) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.NotSame(t, regularQuery, plan.Query) + require.Len(t, plan.Analysis.QueryParts, 1) + require.Len(t, plan.Analysis.QueryParts[0].Regions, 1) + require.Equal(t, []string{"p1", "p2"}, plan.Analysis.QueryParts[0].ProjectionDependencies) +} + +func TestOptimizerRunsRulesAndRefreshesAnalysis(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), `MATCH (n) RETURN n`) + require.NoError(t, err) + + plan, err := NewOptimizer(testRule{name: "test"}).Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, []RuleResult{{Name: "test", Applied: true}}, plan.Rules) + require.Len(t, plan.Analysis.QueryParts, 1) + require.Len(t, plan.Analysis.QueryParts[0].Regions, 1) +} diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 18b96334..84e2863f 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -7,6 +7,7 @@ import ( "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/cypher/models/walk" "github.com/specterops/dawgs/graph" ) @@ -524,9 +525,14 @@ type Result struct { } func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) (Result, error) { + optimizedPlan, err := optimize.Optimize(cypherQuery) + if err != nil { + return Result{}, err + } + translator := NewTranslator(ctx, kindMapper, parameters, graphID) - if err := walk.Cypher(cypherQuery, translator); err != nil { + if err := walk.Cypher(optimizedPlan.Query, translator); err != nil { return Result{}, err } From ea09b7ea012126eaff82e7422f0313bfcee31a7c Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:10:56 -0700 Subject: [PATCH 007/114] feat(pgsql): attach optimizer predicates --- cypher/models/pgsql/optimize/optimizer.go | 96 ++++++++++++++++++- .../models/pgsql/optimize/optimizer_test.go | 58 +++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index a7dcbee7..ec30de85 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -12,10 +12,28 @@ type RuleResult struct { Applied bool } +type PredicateAttachmentScope string + +const ( + PredicateAttachmentScopeBinding PredicateAttachmentScope = "binding" + PredicateAttachmentScopeRegion PredicateAttachmentScope = "region" +) + +type PredicateAttachment struct { + QueryPartIndex int + RegionIndex int + ClauseIndex int + ExpressionIndex int + Scope PredicateAttachmentScope + BindingSymbols []string + Dependencies []string +} + type Plan struct { - Query *cypher.RegularQuery - Analysis Analysis - Rules []RuleResult + Query *cypher.RegularQuery + Analysis Analysis + Rules []RuleResult + PredicateAttachments []PredicateAttachment } type Optimizer struct { @@ -28,8 +46,14 @@ func NewOptimizer(rules ...Rule) Optimizer { } } +func DefaultRules() []Rule { + return []Rule{ + PredicateAttachmentRule{}, + } +} + func Optimize(query *cypher.RegularQuery) (Plan, error) { - return NewOptimizer().Optimize(query) + return NewOptimizer(DefaultRules()...).Optimize(query) } func (s Optimizer) Optimize(query *cypher.RegularQuery) (Plan, error) { @@ -56,3 +80,67 @@ func (s Optimizer) Optimize(query *cypher.RegularQuery) (Plan, error) { return plan, nil } + +type PredicateAttachmentRule struct{} + +func (s PredicateAttachmentRule) Name() string { + return "PredicateAttachment" +} + +func (s PredicateAttachmentRule) Apply(plan *Plan) error { + plan.PredicateAttachments = AttachPredicates(plan.Analysis) + return nil +} + +func AttachPredicates(analysis Analysis) []PredicateAttachment { + var attachments []PredicateAttachment + + for _, queryPart := range analysis.QueryParts { + for regionIndex, region := range queryPart.Regions { + regionBindings := regionBindingSymbols(region) + + for _, predicate := range region.Predicates { + bindingSymbols := predicateBindingSymbols(predicate, regionBindings) + scope := PredicateAttachmentScopeRegion + + if len(bindingSymbols) == 1 && len(predicate.Dependencies) == 1 { + scope = PredicateAttachmentScopeBinding + } + + attachments = append(attachments, PredicateAttachment{ + QueryPartIndex: region.QueryPartIndex, + RegionIndex: regionIndex, + ClauseIndex: predicate.ClauseIndex, + ExpressionIndex: predicate.ExpressionIndex, + Scope: scope, + BindingSymbols: bindingSymbols, + Dependencies: predicate.Dependencies, + }) + } + } + } + + return attachments +} + +func regionBindingSymbols(region Region) map[string]struct{} { + bindings := map[string]struct{}{} + + for _, binding := range region.Bindings { + bindings[binding.Symbol] = struct{}{} + } + + return bindings +} + +func predicateBindingSymbols(predicate Predicate, regionBindings map[string]struct{}) []string { + var bindingSymbols []string + + for _, dependency := range predicate.Dependencies { + if _, isRegionBinding := regionBindings[dependency]; isRegionBinding { + bindingSymbols = append(bindingSymbols, dependency) + } + } + + return bindingSymbols +} diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index ca5846ab..49a27f8d 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -31,6 +31,8 @@ func TestOptimizeCopiesAndAnalyzesQuery(t *testing.T) { require.Len(t, plan.Analysis.QueryParts, 1) require.Len(t, plan.Analysis.QueryParts[0].Regions, 1) require.Equal(t, []string{"p1", "p2"}, plan.Analysis.QueryParts[0].ProjectionDependencies) + require.Equal(t, []RuleResult{{Name: "PredicateAttachment", Applied: true}}, plan.Rules) + require.Len(t, plan.PredicateAttachments, 2) } func TestOptimizerRunsRulesAndRefreshesAnalysis(t *testing.T) { @@ -45,3 +47,59 @@ func TestOptimizerRunsRulesAndRefreshesAnalysis(t *testing.T) { require.Len(t, plan.Analysis.QueryParts, 1) require.Len(t, plan.Analysis.QueryParts[0].Regions, 1) } + +func TestPredicateAttachmentRuleAssignsSingleBindingPredicates(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), adcsQuery) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Len(t, plan.PredicateAttachments, 2) + + require.Equal(t, PredicateAttachment{ + QueryPartIndex: 0, + RegionIndex: 0, + ClauseIndex: 0, + ExpressionIndex: 0, + Scope: PredicateAttachmentScopeBinding, + BindingSymbols: []string{"n"}, + Dependencies: []string{"n"}, + }, plan.PredicateAttachments[0]) + + require.Equal(t, PredicateAttachment{ + QueryPartIndex: 0, + RegionIndex: 0, + ClauseIndex: 2, + ExpressionIndex: 0, + Scope: PredicateAttachmentScopeBinding, + BindingSymbols: []string{"ct"}, + Dependencies: []string{"ct"}, + }, plan.PredicateAttachments[1]) +} + +func TestPredicateAttachmentRuleKeepsMultiBindingPredicatesAtRegionScope(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (a)-[:MemberOf]->(b) + WHERE a.objectid = b.objectid + RETURN a + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Len(t, plan.PredicateAttachments, 1) + + require.Equal(t, PredicateAttachment{ + QueryPartIndex: 0, + RegionIndex: 0, + ClauseIndex: 0, + ExpressionIndex: 0, + Scope: PredicateAttachmentScopeRegion, + BindingSymbols: []string{"a", "b"}, + Dependencies: []string{"a", "b"}, + }, plan.PredicateAttachments[0]) +} From 0763c604e6180ed60c54d627d2c1bf2c57b564ce Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:12:40 -0700 Subject: [PATCH 008/114] test(integration): stabilize optimizer fixture coverage --- .../testdata/cases/optimizer_inline.json | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index a463de49..d8567426 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -12,7 +12,9 @@ {"id": "store", "kinds": ["NTAuthStore"]}, {"id": "domain", "kinds": ["Domain"]}, {"id": "template", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 1, "authorizedsignatures": 1}}, - {"id": "root", "kinds": ["RootCA"]} + {"id": "root", "kinds": ["RootCA"]}, + {"id": "unused-root", "kinds": ["RootCA"]}, + {"id": "unused-template", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": false, "requiresmanagerapproval": true, "enrolleesuppliessubject": false, "schemaversion": 2, "authorizedsignatures": 1}} ], "edges": [ {"start_id": "n", "end_id": "p1-mid", "kind": "MemberOf"}, @@ -23,18 +25,15 @@ {"start_id": "p2-mid", "end_id": "template", "kind": "GenericAll"}, {"start_id": "template", "end_id": "ca", "kind": "PublishedTo"}, {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, - {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} + {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"}, + {"start_id": "ca", "end_id": "unused-root", "kind": "EnterpriseCAFor"}, + {"start_id": "p2-mid", "end_id": "unused-template", "kind": "AllExtendedRights"} ] }, "assert": { - "path_node_ids": [ - ["n", "p1-mid", "ca", "store", "domain"], - ["n", "p2-mid", "template", "ca", "root", "domain"] - ], - "path_edge_kinds": [ - ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], - ["MemberOf", "GenericAll", "PublishedTo", "IssuedSignedBy", "RootCAFor"] - ] + "keys": ["p1", "p2"], + "contains_node_with_props": {"objectid": "S-1-5-21-2643190041-1319121918-239771340-513"}, + "contains_edge": {"start": "template", "end": "ca", "kind": "PublishedTo"} } } ] From ea6cfe52f3cd3922f7bee5a872681ad93bc09d42 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:25:10 -0700 Subject: [PATCH 009/114] chore(pgsql): harden optimizer foundation --- cypher/models/cypher/copy_test.go | 13 +++++ cypher/models/cypher/model.go | 10 +++- cypher/models/pgsql/optimize/analysis.go | 52 ++++++++++++++----- cypher/models/pgsql/optimize/analysis_test.go | 1 + cypher/models/pgsql/optimize/optimizer.go | 23 +++++--- .../models/pgsql/optimize/optimizer_test.go | 18 +++++-- 6 files changed, 92 insertions(+), 25 deletions(-) diff --git a/cypher/models/cypher/copy_test.go b/cypher/models/cypher/copy_test.go index a1c30d16..d6d2b7fa 100644 --- a/cypher/models/cypher/copy_test.go +++ b/cypher/models/cypher/copy_test.go @@ -121,6 +121,7 @@ func TestCopy(t *testing.T) { Distinct: true, All: true, }) + validateCopy(t, &model2.Return{}) validateCopy(t, &model2.ProjectionItem{}) validateCopy(t, &model2.PropertyLookup{ Symbol: "a", @@ -198,3 +199,15 @@ func TestCopyPatternVariablesAreIndependent(t *testing.T) { require.Equal(t, "n", originalNode.Variable.Symbol) require.Equal(t, "r", originalRelationship.Variable.Symbol) } + +func TestNilPatternElementHelpers(t *testing.T) { + var element *model2.PatternElement + + nodePattern, isNodePattern := element.AsNodePattern() + require.Nil(t, nodePattern) + require.False(t, isNodePattern) + + relationshipPattern, isRelationshipPattern := element.AsRelationshipPattern() + require.Nil(t, relationshipPattern) + require.False(t, isRelationshipPattern) +} diff --git a/cypher/models/cypher/model.go b/cypher/models/cypher/model.go index 54001553..b6ef80bf 100644 --- a/cypher/models/cypher/model.go +++ b/cypher/models/cypher/model.go @@ -1322,6 +1322,10 @@ func (s *PatternElement) IsNodePattern() bool { } func (s *PatternElement) AsNodePattern() (*NodePattern, bool) { + if s == nil { + return nil, false + } + nodePattern, isNodePattern := s.Element.(*NodePattern) return nodePattern, isNodePattern } @@ -1332,6 +1336,10 @@ func (s *PatternElement) IsRelationshipPattern() bool { } func (s *PatternElement) AsRelationshipPattern() (*RelationshipPattern, bool) { + if s == nil { + return nil, false + } + relationshipPattern, isRelationshipPattern := s.Element.(*RelationshipPattern) return relationshipPattern, isRelationshipPattern } @@ -1517,7 +1525,7 @@ func (s *Return) copy() *Return { } return &Return{ - Projection: s.Projection.copy(), + Projection: Copy(s.Projection), } } diff --git a/cypher/models/pgsql/optimize/analysis.go b/cypher/models/pgsql/optimize/analysis.go index 887e37a0..82292034 100644 --- a/cypher/models/pgsql/optimize/analysis.go +++ b/cypher/models/pgsql/optimize/analysis.go @@ -47,19 +47,20 @@ type QueryPart struct { } type Region struct { - QueryPartIndex int - StartClause int - EndClause int - Clauses []MatchClause - Bindings []Binding - PathVariables []PathVariable - Predicates []Predicate + QueryPartIndex int + StartClause int + EndClause int + Clauses []MatchClause + Bindings []Binding + BindingOccurrences []Binding + PathVariables []PathVariable + Predicates []Predicate } type MatchClause struct { - Index int - PatternCount int - WherePredicate int + Index int + PatternCount int + WherePredicates int } type Barrier struct { @@ -176,6 +177,10 @@ func analyzeMultiPartQueryPart(index int, part *cypher.MultiPartQueryPart) Query Kind: QueryPartKindMulti, } + if part == nil { + return queryPart + } + queryPart.Regions, queryPart.Barriers = analyzeReadingClauses(index, part.ReadingClauses) if len(part.UpdatingClauses) > 0 { @@ -205,6 +210,10 @@ func analyzeSinglePartQuery(index int, kind QueryPartKind, part *cypher.SinglePa Kind: kind, } + if part == nil { + return queryPart + } + queryPart.Regions, queryPart.Barriers = analyzeReadingClauses(index, part.ReadingClauses) if len(part.UpdatingClauses) > 0 { @@ -280,11 +289,14 @@ func analyzeReadingClauses(queryPartIndex int, readingClauses []*cypher.ReadingC currentRegion.EndClause = clauseIndex currentRegion.Clauses = append(currentRegion.Clauses, MatchClause{ - Index: clauseIndex, - PatternCount: len(match.Pattern), - WherePredicate: wherePredicateCount(match.Where), + Index: clauseIndex, + PatternCount: len(match.Pattern), + WherePredicates: wherePredicateCount(match.Where), }) - currentRegion.Bindings = mergeBindings(currentRegion.Bindings, bindingsForMatch(clauseIndex, match)) + + nextBindings := bindingsForMatch(clauseIndex, match) + currentRegion.BindingOccurrences = append(currentRegion.BindingOccurrences, nextBindings...) + currentRegion.Bindings = mergeBindings(currentRegion.Bindings, nextBindings) currentRegion.PathVariables = mergePathVariables(currentRegion.PathVariables, pathVariablesForMatch(clauseIndex, match)) currentRegion.Predicates = append(currentRegion.Predicates, predicatesForWhere(clauseIndex, match.Where)...) } @@ -342,6 +354,10 @@ func bindingsForMatch(clauseIndex int, match *cypher.Match) []Binding { } for _, element := range pattern.PatternElements { + if element == nil { + continue + } + if nodePattern, isNodePattern := element.AsNodePattern(); isNodePattern { if nodePattern.Variable != nil && nodePattern.Variable.Symbol != "" { bindings = append(bindings, Binding{ @@ -383,6 +399,10 @@ func pathVariablesForMatch(clauseIndex int, match *cypher.Match) []PathVariable } for _, element := range pattern.PatternElements { + if element == nil { + continue + } + if element.IsNodePattern() { pathVariable.NodeCount++ } else if relationshipPattern, isRelationshipPattern := element.AsRelationshipPattern(); isRelationshipPattern { @@ -404,6 +424,10 @@ func patternDependencies(pattern *cypher.PatternPart) []string { var dependencies []string for _, element := range pattern.PatternElements { + if element == nil { + continue + } + if nodePattern, isNodePattern := element.AsNodePattern(); isNodePattern { if nodePattern.Variable != nil && nodePattern.Variable.Symbol != "" { dependencies = append(dependencies, nodePattern.Variable.Symbol) diff --git a/cypher/models/pgsql/optimize/analysis_test.go b/cypher/models/pgsql/optimize/analysis_test.go index eb41ea0f..f6441e32 100644 --- a/cypher/models/pgsql/optimize/analysis_test.go +++ b/cypher/models/pgsql/optimize/analysis_test.go @@ -74,6 +74,7 @@ func TestAnalyzeIdentifiesEligibleADCSRegion(t *testing.T) { require.Equal(t, 0, region.StartClause) require.Equal(t, 2, region.EndClause) require.Len(t, region.Clauses, 3) + require.Len(t, region.BindingOccurrences, 10) require.Len(t, region.Predicates, 2) require.Equal(t, []string{"n"}, region.Predicates[0].Dependencies) require.Equal(t, []string{"ct"}, region.Predicates[1].Dependencies) diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index ec30de85..87556176 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -4,7 +4,7 @@ import "github.com/specterops/dawgs/cypher/models/cypher" type Rule interface { Name() string - Apply(*Plan) error + Apply(*Plan) (bool, error) } type RuleResult struct { @@ -67,13 +67,14 @@ func (s Optimizer) Optimize(query *cypher.RegularQuery) (Plan, error) { plan.Analysis = Analyze(plan.Query) for _, rule := range s.rules { - if err := rule.Apply(&plan); err != nil { + applied, err := rule.Apply(&plan) + if err != nil { return Plan{}, err } plan.Rules = append(plan.Rules, RuleResult{ Name: rule.Name(), - Applied: true, + Applied: applied, }) plan.Analysis = Analyze(plan.Query) } @@ -87,9 +88,9 @@ func (s PredicateAttachmentRule) Name() string { return "PredicateAttachment" } -func (s PredicateAttachmentRule) Apply(plan *Plan) error { +func (s PredicateAttachmentRule) Apply(plan *Plan) (bool, error) { plan.PredicateAttachments = AttachPredicates(plan.Analysis) - return nil + return len(plan.PredicateAttachments) > 0, nil } func AttachPredicates(analysis Analysis) []PredicateAttachment { @@ -113,8 +114,8 @@ func AttachPredicates(analysis Analysis) []PredicateAttachment { ClauseIndex: predicate.ClauseIndex, ExpressionIndex: predicate.ExpressionIndex, Scope: scope, - BindingSymbols: bindingSymbols, - Dependencies: predicate.Dependencies, + BindingSymbols: copyStrings(bindingSymbols), + Dependencies: copyStrings(predicate.Dependencies), }) } } @@ -144,3 +145,11 @@ func predicateBindingSymbols(predicate Predicate, regionBindings map[string]stru return bindingSymbols } + +func copyStrings(values []string) []string { + if values == nil { + return nil + } + + return append([]string(nil), values...) +} diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 49a27f8d..b36f8316 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -15,8 +15,8 @@ func (s testRule) Name() string { return s.name } -func (s testRule) Apply(plan *Plan) error { - return nil +func (s testRule) Apply(plan *Plan) (bool, error) { + return false, nil } func TestOptimizeCopiesAndAnalyzesQuery(t *testing.T) { @@ -43,11 +43,23 @@ func TestOptimizerRunsRulesAndRefreshesAnalysis(t *testing.T) { plan, err := NewOptimizer(testRule{name: "test"}).Optimize(regularQuery) require.NoError(t, err) - require.Equal(t, []RuleResult{{Name: "test", Applied: true}}, plan.Rules) + require.Equal(t, []RuleResult{{Name: "test", Applied: false}}, plan.Rules) require.Len(t, plan.Analysis.QueryParts, 1) require.Len(t, plan.Analysis.QueryParts[0].Regions, 1) } +func TestDefaultPredicateAttachmentRuleReportsSkippedWhenNoPredicatesExist(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), `MATCH (n) RETURN n`) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, []RuleResult{{Name: "PredicateAttachment", Applied: false}}, plan.Rules) + require.Empty(t, plan.PredicateAttachments) +} + func TestPredicateAttachmentRuleAssignsSingleBindingPredicates(t *testing.T) { t.Parallel() From ce87c0e6f1e05a5faf379dfb12fb0b0c5b229dee Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 12:44:40 -0700 Subject: [PATCH 010/114] feat(pgsql): prune expansion path projections Late-materialize variable-length path edge arrays from compact expansion path ID arrays. Avoid carrying expansion edge composites through intermediate projections unless the relationship binding itself remains observable. --- cypher/models/pgsql/format/format.go | 12 +++++ cypher/models/pgsql/model.go | 12 +++++ .../test/translation_cases/multipart.sql | 19 ++++---- .../translation_cases/pattern_binding.sql | 18 ++++---- .../translation_cases/pattern_expansion.sql | 46 +++++++++---------- .../test/translation_cases/shortest_paths.sql | 40 ++++++++-------- cypher/models/pgsql/translate/expansion.go | 3 +- .../pgsql/translate/optimizer_safety_test.go | 5 +- .../models/pgsql/translate/path_functions.go | 2 +- cypher/models/pgsql/translate/projection.go | 29 +++--------- cypher/models/pgsql/translate/renamer.go | 3 ++ cypher/models/pgsql/translate/renamer_test.go | 7 +++ cypher/models/pgsql/translate/traversal.go | 21 ++++++++- cypher/models/walk/walk_pgsql.go | 11 +++++ 14 files changed, 139 insertions(+), 89 deletions(-) diff --git a/cypher/models/pgsql/format/format.go b/cypher/models/pgsql/format/format.go index e44e399c..9cbed49c 100644 --- a/cypher/models/pgsql/format/format.go +++ b/cypher/models/pgsql/format/format.go @@ -533,6 +533,18 @@ func formatNode(builder *OutputBuilder, rootExpr pgsql.SyntaxNode) error { exprStack = append(exprStack, typedNextExpr.Expression) exprStack = append(exprStack, pgsql.FormattingLiteral("(")) + case *pgsql.EdgeArrayFromPathIDs: + if typedNextExpr.PathIDs == nil { + return fmt.Errorf("edge array from path IDs has no path expression") + } + + exprStack = append( + exprStack, + pgsql.FormattingLiteral(") with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id)"), + typedNextExpr.PathIDs, + pgsql.FormattingLiteral("(select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest("), + ) + case pgsql.Parameter: if builder.MaterializeParameters { if parameterValue, hasParameter := builder.parameters[typedNextExpr.Identifier.String()]; !hasParameter { diff --git a/cypher/models/pgsql/model.go b/cypher/models/pgsql/model.go index ae3c750f..54854c50 100644 --- a/cypher/models/pgsql/model.go +++ b/cypher/models/pgsql/model.go @@ -406,6 +406,18 @@ func (s *Parenthetical) AsExpression() Expression { return s } +type EdgeArrayFromPathIDs struct { + PathIDs Expression +} + +func (s *EdgeArrayFromPathIDs) NodeType() string { + return "edge_array_from_path_ids" +} + +func (s *EdgeArrayFromPathIDs) AsExpression() Expression { + return s +} + type JoinType int const ( diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index a30ecd12..a354f376 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -24,22 +24,22 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'value'))::jsonb = to_jsonb((1)::int8)::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 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('me')::text)::jsonb)) select s3.n1 as n1 from s3), s4 as (select s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where (n2.id = (s2.n1).id)) select s4.n2 as b from s4; -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, collect(distinct(n)) as p where size(p) >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: with 365 as max_days match (n:NodeKind1) where n.pwdlastset < (datetime().epochseconds - (max_days * 86400)) and not n.pwdlastset IN [-1.0, 0.0] return n limit 100 with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (not ((n0.properties ->> 'pwdlastset'))::float8 = any (array [- 1, 0]::float8[]) and ((n0.properties ->> 'pwdlastset'))::numeric < (extract(epoch from now()::timestamp with time zone)::numeric - (s0.i0 * 86400))) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1 limit 100; -- case: match (n:NodeKind1) where n.hasspn = true and n.enabled = true and not n.objectid ends with '-502' and not coalesce(n.gmsa, false) = true and not coalesce(n.msa, false) = true match (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) with distinct n, count(c) as adminCount return n order by adminCount desc limit 100 -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s1.n0).id as root_id from s1), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s3.path || e0.id from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s3.depth < 15 and not s3.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s3.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.next_id offset 0) n1 on true where s3.satisfied and (s1.n0).id = s3.root_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s1.n0).id as root_id from s1), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s3.path || e0.id from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s3.depth < 15 and not s3.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.next_id offset 0) n1 on true where s3.satisfied and (s1.n0).id = s3.root_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; -- case: match (n:NodeKind1) where n.objectid = 'S-1-5-21-1260426776-3623580948-1897206385-23225' match p = (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'objectid'))::jsonb = to_jsonb(('S-1-5-21-1260426776-3623580948-1897206385-23225')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, s1.e0, array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'objectid'))::jsonb = to_jsonb(('S-1-5-21-1260426776-3623580948-1897206385-23225')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1; -- case: match (g1:NodeKind1) where g1.name starts with 'test' with collect (g1.domain) as excludes match (d:NodeKind2) where d.name starts with 'other' and not d.name in excludes return d with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like 'test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'domain'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (not (n1.properties ->> 'name') = any (s0.i0) and (n1.properties ->> 'name') like 'other%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]) select s2.n1 as d from s2; @@ -48,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 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; +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 (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, 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[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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); @@ -66,7 +66,7 @@ with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool)) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::text[], array []::text[])::text[], null)::text[] as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by lower(((s2.n0).properties ->> 'samaccountname'))::text), s3 as (select s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n3.id = e1.end_id where (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; -- case: match p =(n:NodeKind1)<-[r:EdgeKind1|EdgeKind2*..3]-(u:NodeKind1) where n.domain = 'test' with n, count(r) as incomingCount where incomingCount > 90 with collect(n) as lotsOfAdmins match p =(n:NodeKind1)<-[:EdgeKind1]-() where n in lotsOfAdmins return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb(('test')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select (array [s4.n2, s4.n3]::nodecomposite[], array [s4.e1]::edgecomposite[])::pathcomposite as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb(('test')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select (array [s4.n2, s4.n3]::nodecomposite[], array [s4.e1]::edgecomposite[])::pathcomposite as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); -- case: match (u:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) with g match (g)<-[:EdgeKind1]-(u:NodeKind1) return g with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n1 as n1 from s1), s2 as (select s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select s2.n1 as g from s2; @@ -81,14 +81,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p, (array [s1.n2, s1.n0]::nodecomposite[], array [s1.e1]::edgecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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; +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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; -- case: match (m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)-[:EdgeKind2]->(c3:NodeKind1) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and not m.samaccountname contains "DEX" and not g.name IN ["D"] and not m.samaccountname =~ "^.*$" with collect(g.name) as admingroups match p=(m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and g.name in admingroups and not m.samaccountname =~ "^.*$" return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied), s3 as (select s1.e0 as e0, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s5.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join lateral (select n4.id, n4.kind_ids, n4.properties from node n4 where n4.id = s5.root_id offset 0) n4 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s5.next_id offset 0) n3 on true where s5.satisfied) select ordered_edges_to_path(s4.n3, s4.e2, array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; -- case: match (a:NodeKind2)-[:EdgeKind1]->(g:NodeKind1)-[:EdgeKind2]->(s:NodeKind2) with count(a) as uc where uc > 5 match p = (a)-[:EdgeKind1]->(g)-[:EdgeKind2]->(s) return p with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, (e3.id, e3.start_id, e3.end_id, e3.kind_id, e3.properties)::edgecomposite as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select (array [s4.n3, s4.n4, s4.n5]::nodecomposite[], array [s4.e2, s4.e3]::edgecomposite[])::pathcomposite as p from s4; -- case: match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0 from s1 join edge e0 on (s1.n0).id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s3 as (select s1.n0 as n0, s2.e0 as e0 from s1 left outer join s2 on (s1.n0 = s2.n0)) select s3.n0 as n0, count(s3.e0)::int8 as i0 from s3 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); - diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index 5b06f866..17ea94e4 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -27,7 +27,7 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select (((array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite).nodes)::nodecomposite[] from s0; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2*1..1]->(:NodeKind2) where any(r in relationships(p) where type(r) STARTS WITH 'EdgeKind') 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 [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 1 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 (((select count(*)::int from unnest((s0.e0)::edgecomposite[]) as i0 where (kind_name(i0.kind_id)::text like 'EdgeKind%')) >= 1)::bool); +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 1 and not s1.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where (kind_name(i0.kind_id)::text like 'EdgeKind%')) >= 1)::bool); -- case: match p=(:NodeKind1)-[r]->(:NodeKind1) where r.isacl return p limit 100 with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where (((e0.properties ->> 'isacl'))::bool) limit 100) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 100; @@ -42,13 +42,13 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('value')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id) select (array [s1.n0, s1.n1, s1.n2]::nodecomposite[], array [s1.e0, s1.e1]::edgecomposite[])::pathcomposite as p from s1; -- case: match p = ()-[*..]->() return p limit 1 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1; -- case: match p = (s)-[*..]->(i)-[]->() where id(s) = 1 and i.name = 'n3' return p limit 1 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, s2.e0 || array [s2.e1]::edgecomposite[], array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || array [s2.e1]::edgecomposite[], array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; -- case: match p = ()-[e:EdgeKind1]->()-[:EdgeKind1*..]->() return e, p -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) as e1, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || s1.e1, array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; -- case: match p = (m:NodeKind1)-[:EdgeKind1]->(c:NodeKind2) where m.objectid ends with "-513" and not toUpper(c.operatingsystem) contains "SERVER" return p limit 1000 with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on ((n0.properties ->> 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on (not upper((n1.properties ->> 'operatingsystem'))::text like '%SERVER%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) limit 1000) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 1000; @@ -60,10 +60,10 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.id = e0.end_id or n0.id = e0.start_id) join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n1.id = e0.end_id or n1.id = e0.start_id) where (n0.id <> n1.id)) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0; -- case: match p = (:NodeKind1)-[:EdgeKind1]->(:NodeKind2)-[:EdgeKind2*1..]->(t:NodeKind2) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.end_id where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) as e1, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.satisfied and (s0.n1).id = s2.root_id limit 1000) select ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || s1.e1, array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1 limit 1000; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.end_id where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.satisfied and (s0.n1).id = s2.root_id limit 1000) select ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1 limit 1000; -- case: match (u:NodeKind1) where u.samaccountname in ["foo", "bar"] match p = (u)-[:EdgeKind1|EdgeKind2*1..3]->(t) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, s1.e0, array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1 limit 1000; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1 limit 1000; -- case: match (x:NodeKind1) where x.name = 'foo' match (y:NodeKind2) where y.name = 'bar' match p=(x)-[:EdgeKind1]->(y) return p with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select (array [s2.n0, s2.n1]::nodecomposite[], array [s2.e0]::edgecomposite[])::pathcomposite as p from s2; @@ -81,13 +81,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p, (array [s1.n2, s1.n0]::nodecomposite[], array [s1.e1]::edgecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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; +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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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); +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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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); -- case: MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE NONE(x in TAIL(r) WHERE NOT type(x) = 'Contains') 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 (((select count(*)::int from unnest(coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[] is not null)::bool); +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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where (((select count(*)::int from unnest(coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[] 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 06878fc5..edf476fc 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -15,70 +15,70 @@ -- SPDX-License-Identifier: Apache-2.0 -- case: match (n)-[*..]->(e) return n, e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true) select s0.n0 as n, s0.n1 as e from s0; -- case: match (n)-[*1..2]->(e) return n, e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 2 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 2 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true) select s0.n0 as n, s0.n1 as e from s0; -- case: match (n)-[*3..5]->(e) return n, e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 5 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 3) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 5 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 3) select s0.n0 as n, s0.n1 as e from s0; -- case: match (n)<-[*2..5]-(e) return n, e -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from edge e0 union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 5 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2) select s0.n0 as n, s0.n1 as e from s0; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from edge e0 union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 5 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2) select s0.n0 as n, s0.n1 as e from s0; -- case: match p = (n)-[*..]->(e:NodeKind1) return p -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match (n)-[*..]->(e:NodeKind1) where n.name = 'n1' return e -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n1 as e from s0; +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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n1 as e from s0; -- case: match (n)-[*..]->(e:NodeKind1) where n.name = 'n2' return n -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n0 as n from s0; +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(('n2')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n0 as n from s0; -- case: match (n)-[*..]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.e0 as e0, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; -- case: match (n)-[*2..3]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.e0 as e0, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; -- case: match (n)-[]->(e:NodeKind1)-[*2..3]->(l) where n.name = 'n1' return l -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) as e1, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; -- case: match (n)-[*..]->(e)-[:EdgeKind1|EdgeKind2]->()-[*..]->(l) where n.name = 'n1' and e.name = 'n2' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.e0 as e0, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[])), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.e0 as e0, (select coalesce(array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.path) with ordinality as _path(id, ordinality) join edge e2 on e2.id = _path.id) as e2, s2.ep0 as ep0, s4.path as ep1, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; +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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[])), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; -- case: match p = (:NodeKind1)-[:EdgeKind1*1..]->(n:NodeKind2) where 'admin_tier_0' in split(n.system_tags, ' ') return p limit 1000 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; -- case: match p = (s:NodeKind1)-[*..]->(e:NodeKind2) where s <> e return p -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and (n0.id <> n1.id)) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and (n0.id <> n1.id)) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = (g:NodeKind1)-[:EdgeKind1|EdgeKind2*]->(target:NodeKind1) where g.objectid ends with '1234' and target.objectid ends with '4567' return p -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%1234') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%1234') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = (m:NodeKind2)-[:EdgeKind1*1..]->(n:NodeKind1) where n.objectid = '1234' return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -> 'objectid'))::jsonb = to_jsonb(('1234')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -> 'objectid'))::jsonb = to_jsonb(('1234')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-() return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 10) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind2)<-[:EdgeKind1|EdgeKind2*2..]-(:NodeKind1) return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) as e1, s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.depth >= 2 and s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, s2.e0 || s2.e1, array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.depth >= 2 and s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind2)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind1) return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.e0 as e0, (select coalesce(array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s3.path) with ordinality as _path(id, ordinality) join edge e1 on e1.id = _path.id) as e1, s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, s2.e0 || s2.e1, array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; -- case: match p = (n:NodeKind1)-[:EdgeKind1|EdgeKind2*1..2]->(r:NodeKind2) where r.name =~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*' return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties ->> 'name') ~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 2 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties ->> 'name') ~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 2 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; -- case: match p = (t:NodeKind2)<-[:EdgeKind1*1..]-(a) where (a:NodeKind1 or a:NodeKind2) and t.objectid ends with '-512' return p limit 1000 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%-512') and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%-512') and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; -- case: match p=(n:NodeKind1)-[:EdgeKind1|EdgeKind2]->(g:NodeKind1)-[:EdgeKind2]->(:NodeKind2)-[:EdgeKind1*1..]->(m:NodeKind1) where n.objectid = m.objectid return p limit 100 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])), s2 as (with recursive s3_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; +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, 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[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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; +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 (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; +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 (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/shortest_paths.sql b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql index 928a1855..0c76efa0 100644 --- a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql +++ b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql @@ -16,80 +16,80 @@ -- 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id) limit 1000; +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id) limit 1000; -- case: match p=shortestPath((n:NodeKind1)-[:EdgeKind1*1..]->(m)) where 'admin_tier_0' in split(m.system_tags, ' ') and n.objectid ends with '-513' and m<>n return p limit 1000 -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties -\u003e\u003e 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s1.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[]))) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s1.root_id and backward_visited.id = e0.start_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; -- case: match p=shortestPath((t:NodeKind1)<-[:EdgeKind1|EdgeKind2*1..]-(s:NodeKind2)) where coalesce(t.system_tags, '') contains 'admin_tier_0' and t.name =~ 'name.*' and s<>t return p limit 1000 -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%' and (n0.properties -\u003e\u003e 'name') ~ 'name.*') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3, 4]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.start_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15, ('')::text, ('insert into traversal_terminal_filter (id) select distinct n1.id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id is not null;')::text, (1000)::int8)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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);"} -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); +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); -- case: match p=shortestPath((a:NodeKind2)-[:EdgeKind1*]->(b)) where a <> b return p -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from edge where end_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.end_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n1.id, n0.id from node n1, node n0 where (((n1.properties -> ''name''))::jsonb = to_jsonb((''123'')::text)::jsonb) and (coalesce((n0.properties ->> ''system_tags''), '''')::text like ''%admin_tier_0%'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id is not null and n0.id is not null;')::text)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id); +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n1.id, n0.id from node n1, node n0 where (((n1.properties -> ''name''))::jsonb = to_jsonb((''123'')::text)::jsonb) and (coalesce((n0.properties ->> ''system_tags''), '''')::text like ''%admin_tier_0%'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id is not null and n0.id is not null;')::text)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id); -- case: match p=shortestPath((a)-[:EdgeKind1*]->(b:NodeKind1)) where a <> b return p -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from edge where end_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.end_id), false, e0.id || s1.path from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.start_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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, 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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; +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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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; +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 (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; -- case: MATCH (g1:Group) MATCH (g2:Group) WHERE g1.name STARTS WITH 'DOMAIN USERS@' AND g2.name STARTS WITH 'DOMAIN ADMINS@' MATCH p=shortestPath((g1)-[:AddAllowedToAct|AddMember|AdminTo|AllExtendedRights|AllowedToDelegate|CanRDP|Contains|ForceChangePassword|GenericAll|GenericWrite|GetChangesAll|GetChanges|HasSession|MemberOf|Owns|ReadLAPSPassword|SQLAdmin|TrustedBy|WriteAccountRestrictions|WriteOwner*1..]->(g2)) WHERE NONE(r IN relationships(p) WHERE type(r) = 'HasSession' AND startNode(r).name = 'DF-WIN10-DEV01.DUMPSTER.FIRE') 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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [13]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') like 'DOMAIN ADMINS@%' and ((s0.n0).properties ->> 'name') like 'DOMAIN USERS@%') and n1.kind_ids operator (pg_catalog.@>) array [13]::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 where (((select count(*)::int from unnest((s2.e0)::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('DF-WIN10-DEV01.DUMPSTER.FIRE')::text)::jsonb and i0.kind_id = 7)) = 0 and (s2.e0)::edgecomposite[] is not null)::bool); +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 [13]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') like 'DOMAIN ADMINS@%' and ((s0.n0).properties ->> 'name') like 'DOMAIN USERS@%') and n1.kind_ids operator (pg_catalog.@>) array [13]::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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('DF-WIN10-DEV01.DUMPSTER.FIRE')::text)::jsonb and i0.kind_id = 7)) = 0 and ((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[] is not null)::bool); -- case: match p=shortestPath((s:NodeKind1)-[:EdgeKind1|HasSession*1..]->(d:NodeKind1)) where s.name = 'path-filter-src' and d.name = 'path-filter-dst' with p where none(r in relationships(p) where type(r) = 'HasSession' and startNode(r).name = 'blocked-session-host') return p -- 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.properties -\u003e 'name'))::jsonb = to_jsonb(('path-filter-src')::text)::jsonb) 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 s2_seed join edge e0 on e0.start_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::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 s2.root_id, e0.end_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s2.root_id and traversal_pair_filter.terminal_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, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s2.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s2_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -\u003e 'name'))::jsonb = to_jsonb(('path-filter-dst')::text)::jsonb) and n1.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 s2_seed join edge e0 on e0.end_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e0.start_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s2.root_id), false, e0.id || s2.path from backward_front s2 join edge e0 on e0.end_id = s2.next_id where e0.kind_id = any (array [3, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s2.root_id and backward_visited.id = e0.start_id);"} -with s0 as (with s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where (((n0.properties -> ''name''))::jsonb = to_jsonb((''path-filter-src'')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (((n1.properties -> ''name''))::jsonb = to_jsonb((''path-filter-dst'')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] 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(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 ordered_edges_to_path(s1.n0, s1.e0, array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as pc0 from s1) select s0.pc0 as p from s0 where (((select count(*)::int from unnest(((s0.pc0).edges)::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('blocked-session-host')::text)::jsonb and i0.kind_id = 7)) = 0 and ((s0.pc0).edges)::edgecomposite[] is not null)::bool); +with s0 as (with s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where (((n0.properties -> ''name''))::jsonb = to_jsonb((''path-filter-src'')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (((n1.properties -> ''name''))::jsonb = to_jsonb((''path-filter-dst'')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id is not null and n1.id is not null;')::text)) select 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 ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as pc0 from s1) select s0.pc0 as p from s0 where (((select count(*)::int from unnest(((s0.pc0).edges)::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('blocked-session-host')::text)::jsonb and i0.kind_id = 7)) = 0 and ((s0.pc0).edges)::edgecomposite[] is not null)::bool); diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index e018a41d..168be2bc 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2464,7 +2464,7 @@ func (s *Translator) buildExpansionProjectionConstraints(traversalStepContext Tr return projectionConstraints, nil } -func (s *Translator) translateTraversalPatternPartWithExpansion(isFirstTraversalStep bool, traversalStep *TraversalStep) error { +func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPart, isFirstTraversalStep bool, traversalStep *TraversalStep) error { expansionModel := traversalStep.Expansion // Translate the expansion's constraints - this has the side effect of making the pattern identifiers visible in @@ -2475,6 +2475,7 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(isFirstTraversal // Export the path from the traversal's scope traversalStep.Frame.Export(expansionModel.PathBinding.Identifier) + pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep) // Push a new frame that contains currently projected scope from the expansion recursive CTE if expansionFrame, err := s.scope.PushFrame(); err != nil { diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index f776318d..badfe540 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -50,7 +50,7 @@ func optimizerSafetyKindMapper() *pgutil.InMemoryKindMapper { return mapper } -func TestOptimizerSafetyADCSQueryDocumentsCurrentCarryShape(t *testing.T) { +func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { t.Parallel() regularQuery, err := frontend.ParseCypher(frontend.NewContext(), optimizerADCSQuery) @@ -66,6 +66,7 @@ func TestOptimizerSafetyADCSQueryDocumentsCurrentCarryShape(t *testing.T) { require.Contains(t, normalizedQuery, "select distinct (s5.n0).id as root_id from s5") require.Contains(t, normalizedQuery, "s5.ep0 as ep0") - require.Contains(t, normalizedQuery, "s5.e0 as e0") + require.NotContains(t, normalizedQuery, "s5.e0 as e0") + require.Contains(t, normalizedQuery, "from unnest(s12.ep0)") require.Contains(t, normalizedQuery, "from s5, s7") } diff --git a/cypher/models/pgsql/translate/path_functions.go b/cypher/models/pgsql/translate/path_functions.go index a73be813..af3e6717 100644 --- a/cypher/models/pgsql/translate/path_functions.go +++ b/cypher/models/pgsql/translate/path_functions.go @@ -12,7 +12,7 @@ func pathCompositeEdgesExpression(scope *Scope, pathBinding *BoundIdentifier) (p for _, dependency := range pathBinding.Dependencies { switch dependency.DataType { case pgsql.ExpansionPath: - if edgeArrayReference, err := expansionPathEdgeArrayReference(scope, dependency); err != nil { + if edgeArrayReference, err := expansionPathEdgeArrayExpression(scope, dependency); err != nil { return nil, err } else { edgeArrayReferences = append(edgeArrayReferences, edgeArrayReference) diff --git a/cypher/models/pgsql/translate/projection.go b/cypher/models/pgsql/translate/projection.go index f8422068..fbecb61c 100644 --- a/cypher/models/pgsql/translate/projection.go +++ b/cypher/models/pgsql/translate/projection.go @@ -162,14 +162,6 @@ func bindingFrameReference(scope *Scope, binding *BoundIdentifier) pgsql.Compoun return pgsql.CompoundIdentifier{frameIdentifier, binding.Identifier} } -func expansionPathEdgeArrayReference(scope *Scope, expansionPath *BoundIdentifier) (pgsql.Expression, error) { - for _, dependency := range expansionPath.Dependencies { - return bindingFrameReference(scope, dependency), nil - } - - return nil, fmt.Errorf("expansion path %s does not reference an expansion edge binding", expansionPath.Identifier) -} - func pathBindingReference(scope *Scope, binding *BoundIdentifier) pgsql.Expression { if binding.LastProjection != nil { return pgsql.CompoundIdentifier{binding.LastProjection.Binding.Identifier, binding.Identifier} @@ -210,15 +202,9 @@ func pathCompositeColumnReference(scope *Scope, binding *BoundIdentifier, 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) + return &pgsql.EdgeArrayFromPathIDs{ + PathIDs: pathBindingReference(scope, expansionPath), + }, nil } func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql.Expression, error) { @@ -402,12 +388,11 @@ func buildProjectionForExpansionEdge(alias pgsql.Identifier, projected *BoundIde // Create a new final projection that's aliased to the visible binding's identifier return []pgsql.SelectItem{ &pgsql.AliasedExpression{ - Expression: &pgsql.Parenthetical{ - Expression: pgsql.FormattingLiteral(fmt.Sprintf( - "select coalesce(array_agg((%[1]s.id, %[1]s.start_id, %[1]s.end_id, %[1]s.kind_id, %[1]s.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(%[2]s.path) with ordinality as _path(id, ordinality) join edge %[1]s on %[1]s.id = _path.id", - projected.Identifier, + Expression: &pgsql.EdgeArrayFromPathIDs{ + PathIDs: pgsql.CompoundIdentifier{ scope.CurrentFrame().Binding.Identifier, - )), + pgsql.ColumnPath, + }, }, Alias: pgsql.AsOptionalIdentifier(alias), }, diff --git a/cypher/models/pgsql/translate/renamer.go b/cypher/models/pgsql/translate/renamer.go index c2a49c3c..0b80a499 100644 --- a/cypher/models/pgsql/translate/renamer.go +++ b/cypher/models/pgsql/translate/renamer.go @@ -387,6 +387,9 @@ func (s *FrameBindingRewriter) enter(node pgsql.SyntaxNode) error { } } + case *pgsql.EdgeArrayFromPathIDs: + return s.rewriteExpression(&typedExpression.PathIDs) + case *pgsql.AliasedExpression: switch typedInnerExpression := typedExpression.Expression.(type) { case pgsql.Identifier: diff --git a/cypher/models/pgsql/translate/renamer_test.go b/cypher/models/pgsql/translate/renamer_test.go index 27999e28..50b349e0 100644 --- a/cypher/models/pgsql/translate/renamer_test.go +++ b/cypher/models/pgsql/translate/renamer_test.go @@ -63,6 +63,13 @@ func TestRewriteFrameBindings(t *testing.T) { Expression: rewrittenA, Alias: pgsql.AsOptionalIdentifier("name"), }, + }, { + Case: &pgsql.EdgeArrayFromPathIDs{ + PathIDs: a.Identifier, + }, + Expected: &pgsql.EdgeArrayFromPathIDs{ + PathIDs: rewrittenA, + }, }, { Case: pgsql.NewBinaryExpression( pgsql.ArraySlice{ diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 26485677..fd8ff603 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -519,7 +519,7 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } if traversalStep.Expansion != nil { - if err := s.translateTraversalPatternPartWithExpansion(idx == 0, traversalStep); err != nil { + if err := s.translateTraversalPatternPartWithExpansion(part, idx == 0, traversalStep); err != nil { return err } } else if part.AllShortestPaths || part.ShortestPath { @@ -591,6 +591,25 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart } } +func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep) { + if traversalStep == nil || traversalStep.Expansion == nil { + return + } + + // Variable-length relationship bindings materialize to edge-composite + // arrays. A path binding can be rebuilt later from the compact expansion + // path ID array, so keep the edge array only when the relationship binding + // itself is observable. + if traversalStep.Edge != nil && !queryPart.ReferencesBinding(traversalStep.Edge) { + traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + } + + pathBinding := traversalStep.Expansion.PathBinding + if pathBinding != nil && !patternBindingDependsOn(queryPart, part, pathBinding) { + traversalStep.Frame.Unexport(pathBinding.Identifier) + } +} + func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *PatternPart, stepIndex int, traversalStep *TraversalStep) error { isFirstTraversalStep := stepIndex == 0 diff --git a/cypher/models/walk/walk_pgsql.go b/cypher/models/walk/walk_pgsql.go index a140fb66..23ab28b3 100644 --- a/cypher/models/walk/walk_pgsql.go +++ b/cypher/models/walk/walk_pgsql.go @@ -205,6 +205,12 @@ func newSQLWalkCursor(node pgsql.SyntaxNode) (*Cursor[pgsql.SyntaxNode], error) Branches: []pgsql.SyntaxNode{typedNode.Expression}, }, nil + case *pgsql.EdgeArrayFromPathIDs: + return &Cursor[pgsql.SyntaxNode]{ + Node: node, + Branches: []pgsql.SyntaxNode{typedNode.PathIDs}, + }, nil + case pgsql.FunctionCall: if branches, err := pgsqlSyntaxNodeSliceTypeConvert(typedNode.Parameters); err != nil { return nil, err @@ -382,6 +388,11 @@ func newSQLWalkCursor(node pgsql.SyntaxNode) (*Cursor[pgsql.SyntaxNode], error) Branches: []pgsql.SyntaxNode{typedNode.Query}, }, nil + case pgsql.FormattingLiteral: + return &Cursor[pgsql.SyntaxNode]{ + Node: node, + }, nil + case pgsql.SyntaxNodeFuture: cursor := &Cursor[pgsql.SyntaxNode]{ Node: typedNode, From 35ab840333aa33c824227118a1b57c13a4e1677c Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 13:23:40 -0700 Subject: [PATCH 011/114] feat(pgsql): materialize path edges late Represent fixed relationships used only by path bindings as scalar edge IDs through intermediate projections. Reconstruct edge composites from those IDs only when path values or path relationship arrays are needed, while keeping directly referenced relationship variables materialized as full composites. --- cypher/models/pgsql/pgtypes.go | 1 + .../test/translation_cases/multipart.sql | 12 ++--- .../translation_cases/pattern_binding.sql | 26 +++++----- .../translation_cases/pattern_expansion.sql | 2 +- .../test/translation_cases/shortest_paths.sql | 2 +- .../translation_cases/stepwise_traversal.sql | 2 +- cypher/models/pgsql/translate/function.go | 3 ++ .../pgsql/translate/optimizer_safety_test.go | 25 +++++++++ .../models/pgsql/translate/path_functions.go | 3 ++ cypher/models/pgsql/translate/projection.go | 52 ++++++++++++++++++- cypher/models/pgsql/translate/tracking.go | 2 + cypher/models/pgsql/translate/traversal.go | 7 +++ 12 files changed, 113 insertions(+), 24 deletions(-) diff --git a/cypher/models/pgsql/pgtypes.go b/cypher/models/pgsql/pgtypes.go index 2ff9b7db..8e68dd6a 100644 --- a/cypher/models/pgsql/pgtypes.go +++ b/cypher/models/pgsql/pgtypes.go @@ -106,6 +106,7 @@ const ( ExpansionRootNode DataType = "expansion_root_node" ExpansionEdge DataType = "expansion_edge" ExpansionTerminalNode DataType = "expansion_terminal_node" + PathEdge DataType = "path_edge" ) func (s DataType) IsKnown() bool { diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index a354f376..6a420e42 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -48,13 +48,13 @@ 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 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 (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, 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[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 (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 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, 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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); -- case: match (n:NodeKind1)-[:EdgeKind1]->(m:NodeKind2) where n.enabled = true with n, collect(distinct(n)) as p where size(p) >= 100 match p = (n)-[:EdgeKind1]->(m) return p limit 10 -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n0 as n0, array_remove(coalesce(array_agg(distinct (s1.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1 group by n0), s2 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.i0 as i0, s0.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (cardinality(s0.i0)::int >= 100) and (s0.n0).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) limit 10) select (array [s2.n0, s2.n2]::nodecomposite[], array [s2.e1]::edgecomposite[])::pathcomposite as p from s2 limit 10; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n0 as n0, array_remove(coalesce(array_agg(distinct (s1.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1 group by n0), s2 as (select e1.id as e1, s0.i0 as i0, s0.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (cardinality(s0.i0)::int >= 100) and (s0.n0).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) limit 10) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; -- case: with "a" as check, "b" as ref match p = (u)-[:EdgeKind1]->(g:NodeKind1) where u.name starts with check and u.domain = ref with collect(tolower(g.samaccountname)) as refmembership, tolower(u.samaccountname) as samname return refmembership, samname with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool)) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::text[], array []::text[])::text[], null)::text[] as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by lower(((s2.n0).properties ->> 'samaccountname'))::text) select s1.i2 as refmembership, s1.i3 as samname from s1; @@ -66,7 +66,7 @@ with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool)) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::text[], array []::text[])::text[], null)::text[] as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by lower(((s2.n0).properties ->> 'samaccountname'))::text), s3 as (select s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n3.id = e1.end_id where (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; -- case: match p =(n:NodeKind1)<-[r:EdgeKind1|EdgeKind2*..3]-(u:NodeKind1) where n.domain = 'test' with n, count(r) as incomingCount where incomingCount > 90 with collect(n) as lotsOfAdmins match p =(n:NodeKind1)<-[:EdgeKind1]-() where n in lotsOfAdmins return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb(('test')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select (array [s4.n2, s4.n3]::nodecomposite[], array [s4.e1]::edgecomposite[])::pathcomposite as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb(('test')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select e1.id as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3]::nodecomposite[])::pathcomposite as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); -- case: match (u:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) with g match (g)<-[:EdgeKind1]-(u:NodeKind1) return g with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n1 as n1 from s1), s2 as (select s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select s2.n1 as g from s2; @@ -75,10 +75,10 @@ with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposit with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') ~ '.*TT' and ((n0.properties -> 'domain'))::jsonb = to_jsonb(('MY DOMAIN')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'email'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (not (n2.properties ->> 'email') = any (s0.i0) and (n2.properties ->> 'name') like 'blah%')) select s2.n1 as o from s2; -- case: match (e) match p = ()-[]->(e) return p limit 1 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select (array [s1.n1, s1.n0]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p from s1 limit 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite as p from s1 limit 1; -- case: match p = (a)-[]->() match q = ()-[]->(a) return p, q -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p, (array [s1.n2, s1.n0]::nodecomposite[], array [s1.e1]::edgecomposite[])::pathcomposite as q from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p with s0 as (with s1 as (with recursive s2_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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; @@ -87,7 +87,7 @@ with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (sel with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; -- case: match (a:NodeKind2)-[:EdgeKind1]->(g:NodeKind1)-[:EdgeKind2]->(s:NodeKind2) with count(a) as uc where uc > 5 match p = (a)-[:EdgeKind1]->(g)-[:EdgeKind2]->(s) return p -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, (e3.id, e3.start_id, e3.end_id, e3.kind_id, e3.properties)::edgecomposite as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select (array [s4.n3, s4.n4, s4.n5]::nodecomposite[], array [s4.e2, s4.e3]::edgecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select e2.id as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, e3.id as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e2]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e3]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4, s4.n5]::nodecomposite[])::pathcomposite as p from s4; -- case: match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0 from s1 join edge e0 on (s1.n0).id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s3 as (select s1.n0 as n0, s2.e0 as e0 from s1 left outer join s2 on (s1.n0 = s2.n0)) select s3.n0 as n0, count(s3.e0)::int8 as i0 from s3 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index 17ea94e4..772dc2b3 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -21,10 +21,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 ->> 'name') like '%test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select (array [s0.n0]::nodecomposite[], array []::edgecomposite[])::pathcomposite as p from s0; -- case: match p = ()-[]->() return p -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = ()-[]->() return nodes(p) -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select (((array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite).nodes)::nodecomposite[] from s0; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select ((ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[] from s0; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2*1..1]->(:NodeKind2) where any(r in relationships(p) where type(r) STARTS WITH 'EdgeKind') 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 [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 1 and not s1.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where (kind_name(i0.kind_id)::text like 'EdgeKind%')) >= 1)::bool); @@ -39,46 +39,46 @@ with s0 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties -> 'name'))::jsonb = to_jsonb(('a')::text)::jsonb)), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where (((e1.properties -> 'name'))::jsonb = to_jsonb(('b')::text)::jsonb)), s2 as (select s1.e0 as e0, s1.e1 as e1, s1.n1 as n1, s1.n2 as n2 from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id) select s2.e0 as r1 from s2; -- case: match p = (a)-[]->()<-[]-(f) where a.name = 'value' and f.is_target return p -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('value')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id) select (array [s1.n0, s1.n1, s1.n2]::nodecomposite[], array [s1.e0, s1.e1]::edgecomposite[])::pathcomposite as p from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('value')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; -- case: match p = ()-[*..]->() return p limit 1 with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1; -- case: match p = (s)-[*..]->(i)-[]->() where id(s) = 1 and i.name = 'n3' return p limit 1 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || array [s2.e1]::edgecomposite[], array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; -- case: match p = ()-[e:EdgeKind1]->()-[:EdgeKind1*..]->() return e, p with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; -- case: match p = (m:NodeKind1)-[:EdgeKind1]->(c:NodeKind2) where m.objectid ends with "-513" and not toUpper(c.operatingsystem) contains "SERVER" return p limit 1000 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on ((n0.properties ->> 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on (not upper((n1.properties ->> 'operatingsystem'))::text like '%SERVER%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) limit 1000) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 1000; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on ((n0.properties ->> 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on (not upper((n1.properties ->> 'operatingsystem'))::text like '%SERVER%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2]->(e:NodeKind2)-[:EdgeKind2]->(:NodeKind1) where 'a' in e.values or 'b' in e.values or size(e.values) = 0 return p -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('a' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or 'b' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or jsonb_array_length((n1.properties -> 'values'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select (array [s1.n0, s1.n1, s1.n2]::nodecomposite[], array [s1.e0, s1.e1]::edgecomposite[])::pathcomposite as p from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('a' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or 'b' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or jsonb_array_length((n1.properties -> 'values'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; -- case: match p = (n:NodeKind1)-[r]-(m:NodeKind1) return p -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.id = e0.end_id or n0.id = e0.start_id) join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n1.id = e0.end_id or n1.id = e0.start_id) where (n0.id <> n1.id)) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.id = e0.end_id or n0.id = e0.start_id) join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n1.id = e0.end_id or n1.id = e0.start_id) where (n0.id <> n1.id)) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; -- case: match p = (:NodeKind1)-[:EdgeKind1]->(:NodeKind2)-[:EdgeKind2*1..]->(t:NodeKind2) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.end_id where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.satisfied and (s0.n1).id = s2.root_id limit 1000) select ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1 limit 1000; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.end_id where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.satisfied and (s0.n1).id = s2.root_id limit 1000) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1 limit 1000; -- case: match (u:NodeKind1) where u.samaccountname in ["foo", "bar"] match p = (u)-[:EdgeKind1|EdgeKind2*1..3]->(t) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000 with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1 limit 1000; -- case: match (x:NodeKind1) where x.name = 'foo' match (y:NodeKind2) where y.name = 'bar' match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select (array [s2.n0, s2.n1]::nodecomposite[], array [s2.e0]::edgecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match (y:NodeKind2{name:'bar'}) match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select (array [s2.n0, s2.n1]::nodecomposite[], array [s2.e0]::edgecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match p=(x)-[:EdgeKind1]->(y:NodeKind2{name:'bar'}) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1; -- case: match (e) match p = ()-[]->(e) return p limit 1 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select (array [s1.n1, s1.n0]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p from s1 limit 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite as p from s1 limit 1; -- case: match p = (a)-[]->() match q = ()-[]->(a) return p, q -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select (array [s1.n0, s1.n1]::nodecomposite[], array [s1.e0]::edgecomposite[])::pathcomposite as p, (array [s1.n2, s1.n0]::nodecomposite[], array [s1.e1]::edgecomposite[])::pathcomposite as q from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p with s0 as (with s1 as (with recursive s2_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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql index edf476fc..beaecc8c 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -75,7 +75,7 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%-512') and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; -- case: match p=(n:NodeKind1)-[:EdgeKind1|EdgeKind2]->(g:NodeKind1)-[:EdgeKind2]->(:NodeKind2)-[:EdgeKind1*1..]->(m:NodeKind1) where n.objectid = m.objectid return p limit 100 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])), s2 as (with recursive s3_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, 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[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2, s2.n3]::nodecomposite[])::pathcomposite as p from s2 limit 100; +with s0 as (select e0.id 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 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, 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 (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; diff --git a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql index 0c76efa0..c18e4de0 100644 --- a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql +++ b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql @@ -64,7 +64,7 @@ with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (sele -- 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 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, 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p2 from s1 limit 500; +with s0 as (select e0.id 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, 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 ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 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);"} diff --git a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql index 1369b5f4..28f784cf 100644 --- a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql +++ b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql @@ -42,7 +42,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1 from s0, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.id = e1.end_id) select s1.e0 as r, s1.e1 as e from s1; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2]->(c:NodeKind2) where '123' in c.prop2 or '243' in c.prop2 or size(c.prop2) = 0 return p limit 10 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) limit 10) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; -- case: match ()-[r:EdgeKind1]->() return count(r) as the_count with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; diff --git a/cypher/models/pgsql/translate/function.go b/cypher/models/pgsql/translate/function.go index 5a475e3b..1d614f88 100644 --- a/cypher/models/pgsql/translate/function.go +++ b/cypher/models/pgsql/translate/function.go @@ -279,6 +279,9 @@ func bindingExpressionType(binding *BoundIdentifier) pgsql.DataType { case pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode: return pgsql.NodeComposite + case pgsql.PathEdge: + return pgsql.Int8 + default: return binding.DataType } diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index badfe540..65be796a 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -68,5 +68,30 @@ func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { require.Contains(t, normalizedQuery, "s5.ep0 as ep0") require.NotContains(t, normalizedQuery, "s5.e0 as e0") require.Contains(t, normalizedQuery, "from unnest(s12.ep0)") + require.Contains(t, normalizedQuery, "from unnest(array [s12.e1]::int8[])") + require.NotContains(t, normalizedQuery, "array [s12.e1]::edgecomposite[]") require.Contains(t, normalizedQuery, "from s5, s7") } + +func TestOptimizerSafetyReferencedRelationshipStaysComposite(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` +MATCH p = (n:Group)-[r:MemberOf]->(m:Group) +RETURN p, r +`) + require.NoError(t, err) + + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + require.NoError(t, err) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "(e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0") + require.Contains(t, normalizedQuery, "array [s0.e0]::edgecomposite[]") + require.NotContains(t, normalizedQuery, "e0.id as e0") + require.NotContains(t, normalizedQuery, "array [s0.e0]::int8[]") +} diff --git a/cypher/models/pgsql/translate/path_functions.go b/cypher/models/pgsql/translate/path_functions.go index af3e6717..909eac3e 100644 --- a/cypher/models/pgsql/translate/path_functions.go +++ b/cypher/models/pgsql/translate/path_functions.go @@ -25,6 +25,9 @@ func pathCompositeEdgesExpression(scope *Scope, pathBinding *BoundIdentifier) (p CastType: pgsql.EdgeCompositeArray, }) + case pgsql.PathEdge: + edgeArrayReferences = append(edgeArrayReferences, pathEdgeArrayExpression(scope, dependency)) + default: // Path bindings also depend on their node endpoints. Those are not part of relationships(p). } diff --git a/cypher/models/pgsql/translate/projection.go b/cypher/models/pgsql/translate/projection.go index fbecb61c..722e96f2 100644 --- a/cypher/models/pgsql/translate/projection.go +++ b/cypher/models/pgsql/translate/projection.go @@ -201,6 +201,25 @@ func pathCompositeColumnReference(scope *Scope, binding *BoundIdentifier, column return pgsql.CompoundIdentifier{binding.Identifier, column} } +func pathEdgeIDReference(scope *Scope, binding *BoundIdentifier) pgsql.Expression { + if binding.LastProjection != nil || scope.CurrentFrameBinding() != nil { + return pathBindingReference(scope, binding) + } + + return pgsql.CompoundIdentifier{binding.Identifier, pgsql.ColumnID} +} + +func pathEdgeArrayExpression(scope *Scope, edge *BoundIdentifier) pgsql.Expression { + return &pgsql.EdgeArrayFromPathIDs{ + PathIDs: pgsql.ArrayLiteral{ + Values: []pgsql.Expression{ + pathEdgeIDReference(scope, edge), + }, + CastType: pgsql.Int8Array, + }, + } +} + func expansionPathEdgeArrayExpression(scope *Scope, expansionPath *BoundIdentifier) (pgsql.Expression, error) { return &pgsql.EdgeArrayFromPathIDs{ PathIDs: pathBindingReference(scope, expansionPath), @@ -218,6 +237,7 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql directNodeReferences []pgsql.Expression directEdgeReferences []pgsql.Expression seenExpansionPath = false + seenPathEdge = false ) // Path composite components are encoded as dependencies on the bound identifier representing the @@ -242,6 +262,10 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql CastType: pgsql.EdgeCompositeArray, }) + case pgsql.PathEdge: + seenPathEdge = true + edgeArrayReferences = append(edgeArrayReferences, pathEdgeArrayExpression(scope, dependency)) + case pgsql.NodeComposite, pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode: directNodeReferences = append(directNodeReferences, pathCompositeReference(scope, dependency, pgsql.NodeTableColumns)) nodeReferences = append(nodeReferences, pathCompositeColumnReference(scope, dependency, pgsql.ColumnID)) @@ -255,7 +279,7 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql // those explicit components instead of reconstructing the path from edge IDs: this preserves path // order and duplicate nodes, and it also works for rows produced by data-modifying CTEs where // re-reading node/edge tables in the same statement may not see the RETURNING values. - if !seenExpansionPath && len(directNodeReferences) > 0 { + if !seenExpansionPath && !seenPathEdge && len(directNodeReferences) > 0 { return pgsql.CompositeValue{ DataType: pgsql.PathComposite, Values: []pgsql.Expression{ @@ -271,7 +295,7 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql }, nil } - if seenExpansionPath { + if seenExpansionPath || seenPathEdge { if len(directNodeReferences) == 0 { return nil, fmt.Errorf("expansion path %s does not contain a root node reference", projected.Identifier) } @@ -426,6 +450,27 @@ func buildProjectionForEdgeComposite(alias pgsql.Identifier, projected *BoundIde }, nil } +func buildProjectionForPathEdge(alias pgsql.Identifier, projected *BoundIdentifier, referenceFrame *Frame) ([]pgsql.SelectItem, error) { + var expression pgsql.Expression + + if projected.LastProjection != nil { + if referenceFrame == nil { + referenceFrame = projected.LastProjection + } + + expression = pgsql.CompoundIdentifier{referenceFrame.Binding.Identifier, projected.Identifier} + } else { + expression = pgsql.CompoundIdentifier{projected.Identifier, pgsql.ColumnID} + } + + return []pgsql.SelectItem{ + &pgsql.AliasedExpression{ + Expression: expression, + Alias: pgsql.AsOptionalIdentifier(alias), + }, + }, nil +} + func buildProjection(alias pgsql.Identifier, projected *BoundIdentifier, scope *Scope, referenceFrame *Frame) ([]pgsql.SelectItem, error) { switch projected.DataType { case pgsql.ExpansionPath: @@ -446,6 +491,9 @@ func buildProjection(alias pgsql.Identifier, projected *BoundIdentifier, scope * case pgsql.EdgeComposite: return buildProjectionForEdgeComposite(alias, projected, referenceFrame) + case pgsql.PathEdge: + return buildProjectionForPathEdge(alias, projected, referenceFrame) + default: // If this isn't a type that requires a unique projection, reflect the identifier as-is with its alias var expression pgsql.Expression diff --git a/cypher/models/pgsql/translate/tracking.go b/cypher/models/pgsql/translate/tracking.go index a5c736fe..bfaadc4f 100644 --- a/cypher/models/pgsql/translate/tracking.go +++ b/cypher/models/pgsql/translate/tracking.go @@ -26,6 +26,8 @@ func (s IdentifierGenerator) NewIdentifier(dataType pgsql.DataType) (pgsql.Ident prefixStr = "n" case pgsql.EdgeComposite: prefixStr = "e" + case pgsql.PathEdge: + prefixStr = "e" case pgsql.Scope: prefixStr = "s" case pgsql.ParameterIdentifier: diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index fd8ff603..8e33f100 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -582,6 +582,13 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart traversalStep.Frame.Unexport(traversalStep.LeftNode.Identifier) } + if traversalStep.Edge != nil && + traversalStep.Edge.DataType == pgsql.EdgeComposite && + !queryPart.ReferencesBinding(traversalStep.Edge) && + patternBindingDependsOn(queryPart, part, traversalStep.Edge) { + traversalStep.Edge.DataType = pgsql.PathEdge + } + if !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.Edge) { traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) } From 2972ed63079c73d2bab6eb43658e908a0b354f3e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:01:44 -0700 Subject: [PATCH 012/114] docs(pgsql): sequence optimizer review followups --- docs/optimization-pass-memory.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index d53016d0..6b0b40e6 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -158,3 +158,23 @@ Broader benchmark suites and real-world query collections are deferred until aft Implement phases 1 through 6 first. That milestone establishes the PostgreSQL optimizer framework, test bar, predicate ownership, projection and path pruning, and late path materialization. It should improve the reported query shape without taking on endpoint-aware expansion, suffix semi-joins, schema statistics, or a full cost-based planner. + +## Quality Review Follow-Up Plan + +The first optimizer milestone introduced the PostgreSQL optimizer hook, predicate attachment diagnostics, projection pruning, and late path materialization. Before moving on to endpoint-aware expansion or pattern reordering, close the review gaps in this order: + +### Step 1: Preserve The Optional-Match Barrier + +Keep projection pruning and late path materialization scoped to plain `MATCH` translation until optional path semantics have dedicated coverage. `OPTIONAL MATCH` already acts as an optimization-region barrier in the analysis pass; translator-side lowering should respect the same boundary. + +### Step 2: Assert Path Semantics, Not Only SQL Shape + +Expand integration coverage for optimized path returns so tests assert path node order, relationship order, and path length for mixed fixed-hop and variable-length paths. Include `relationships(p)` on paths that are eligible for late materialization. + +### Step 3: Harden Direct Relationship References + +Add focused translation tests proving direct relationship references keep edge composites when used in returned values, predicates, relationship properties, `type(r)`, and endpoint functions such as `startNode(r)`. + +### Step 4: Document Performance Measurement Needs + +Keep the current shape tests as guardrails, but add an explicit measurement task for high-fanout ADCS-style data before expanding the optimizer into endpoint-aware expansion, suffix semi-joins, or deterministic reordering. From 40fa3efb8346a4bd244ac5e5561f0d621c20bd38 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:02:50 -0700 Subject: [PATCH 013/114] fix(pgsql): preserve optional match pruning barrier --- cypher/models/pgsql/translate/expansion.go | 6 +++-- cypher/models/pgsql/translate/match.go | 2 +- .../pgsql/translate/optimizer_safety_test.go | 22 +++++++++++++++++++ cypher/models/pgsql/translate/predicate.go | 2 +- cypher/models/pgsql/translate/traversal.go | 12 +++++----- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 168be2bc..e34a7a41 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2464,7 +2464,7 @@ func (s *Translator) buildExpansionProjectionConstraints(traversalStepContext Tr return projectionConstraints, nil } -func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPart, isFirstTraversalStep bool, traversalStep *TraversalStep) error { +func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPart, isFirstTraversalStep bool, traversalStep *TraversalStep, allowProjectionPruning bool) error { expansionModel := traversalStep.Expansion // Translate the expansion's constraints - this has the side effect of making the pattern identifiers visible in @@ -2475,7 +2475,9 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar // Export the path from the traversal's scope traversalStep.Frame.Export(expansionModel.PathBinding.Identifier) - pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep) + if allowProjectionPruning { + pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep) + } // Push a new frame that contains currently projected scope from the expansion recursive CTE if expansionFrame, err := s.scope.PushFrame(); err != nil { diff --git a/cypher/models/pgsql/translate/match.go b/cypher/models/pgsql/translate/match.go index ba86e010..f93a5d5f 100644 --- a/cypher/models/pgsql/translate/match.go +++ b/cypher/models/pgsql/translate/match.go @@ -16,7 +16,7 @@ func (s *Translator) translateMatch(match *cypher.Match) error { return err } } else { - if err := s.translateTraversalPatternPart(part, false); err != nil { + if err := s.translateTraversalPatternPart(part, false, !match.Optional); err != nil { return err } } diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 65be796a..aab84693 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -95,3 +95,25 @@ RETURN p, r require.NotContains(t, normalizedQuery, "e0.id as e0") require.NotContains(t, normalizedQuery, "array [s0.e0]::int8[]") } + +func TestOptimizerSafetyOptionalMatchPathStaysComposite(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` +MATCH (n:Group) +OPTIONAL MATCH p = (n)-[:MemberOf]->(m:Group) +RETURN n, p +`) + require.NoError(t, err) + + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + require.NoError(t, err) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "::edgecomposite[]") + require.NotContains(t, normalizedQuery, "::int8[]") +} diff --git a/cypher/models/pgsql/translate/predicate.go b/cypher/models/pgsql/translate/predicate.go index d14b37f0..002d3e95 100644 --- a/cypher/models/pgsql/translate/predicate.go +++ b/cypher/models/pgsql/translate/predicate.go @@ -123,7 +123,7 @@ func (s *Translator) buildPatternPredicates() error { } } - if err := s.translateTraversalPatternPart(patternPart, true); err != nil { + if err := s.translateTraversalPatternPart(patternPart, true, true); err != nil { return err } diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 8e33f100..189d94cb 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -503,7 +503,7 @@ func (s *Translator) buildTraversalPatternStep(partFrame *Frame, traversalStep * }, nil } -func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedProjection bool) error { +func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedProjection bool, allowProjectionPruning bool) error { var scopeSnapshot *Scope if isolatedProjection { @@ -519,12 +519,12 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } if traversalStep.Expansion != nil { - if err := s.translateTraversalPatternPartWithExpansion(part, idx == 0, traversalStep); err != nil { + if err := s.translateTraversalPatternPartWithExpansion(part, idx == 0, traversalStep, allowProjectionPruning); err != nil { return err } } else if part.AllShortestPaths || part.ShortestPath { return fmt.Errorf("expected shortest path search to utilize variable expansion: ()-[*..]->()") - } else if err := s.translateTraversalPatternPartWithoutExpansion(part, idx, traversalStep); err != nil { + } else if err := s.translateTraversalPatternPartWithoutExpansion(part, idx, traversalStep, allowProjectionPruning); err != nil { return err } } @@ -617,7 +617,7 @@ func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart } } -func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *PatternPart, stepIndex int, traversalStep *TraversalStep) error { +func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *PatternPart, stepIndex int, traversalStep *TraversalStep, allowProjectionPruning bool) error { isFirstTraversalStep := stepIndex == 0 if constraints, err := consumePatternConstraints(isFirstTraversalStep, nonRecursivePattern, traversalStep, s.treeTranslator); err != nil { @@ -694,7 +694,9 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } } - pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep) + if allowProjectionPruning { + pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep) + } if boundProjections, err := buildVisibleProjections(s.scope); err != nil { return err From 6d9d54255b7bc567cb31ab98a33a7d9182d4c51e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:05:52 -0700 Subject: [PATCH 014/114] test(integration): assert optimized path semantics --- integration/cypher_test.go | 67 +++++++++++++++++++ .../cases/pattern_binding_inline.json | 2 +- .../testdata/templates/pattern_shapes.json | 9 ++- tools/metrics/internal/metrics/quality.go | 1 + 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/integration/cypher_test.go b/integration/cypher_test.go index c66e2745..334a9168 100644 --- a/integration/cypher_test.go +++ b/integration/cypher_test.go @@ -145,6 +145,7 @@ func TestCypher(t *testing.T) { // {"path_node_ids": [["a", "b"]]} — exact multiset of returned path node ID sequences // {"path_lengths": [N...]} — exact multiset of returned path edge counts // {"path_edge_kinds": [["K"...]]} — exact multiset of returned path edge kind sequences +// {"relationship_list_kinds": [["K"...]]} — exact multiset of returned relationship-list kind sequences // // Object assertions may combine multiple keys; every assertion must pass. func parseAssertion(t *testing.T, raw json.RawMessage) caseAssertion { @@ -231,6 +232,9 @@ func parseAssertion(t *testing.T, raw json.RawMessage) caseAssertion { case "path_edge_kinds": assertions = append(assertions, assertPathEdgeKinds(decodeAssertionValue[[][]string](t, key, val))) + case "relationship_list_kinds": + assertions = append(assertions, assertRelationshipListKinds(decodeAssertionValue[[][]string](t, key, val))) + default: t.Fatalf("unknown assertion key: %q", key) } @@ -885,6 +889,35 @@ func assertPathEdgeKinds(expected [][]string) resultAssertion { } } +func assertRelationshipListKinds(expected [][]string) resultAssertion { + return func(t *testing.T, result queryResult, _ assertionContext) { + t.Helper() + + got := make([]string, 0, len(result.rows)) + for _, row := range result.rows { + for _, rawVal := range row.values { + var relationshipPointers []*graph.Relationship + if result.mapper.Map(rawVal, &relationshipPointers) { + got = append(got, relationshipListKindSignature(t, relationshipPointers)) + continue + } + + var relationships []graph.Relationship + if result.mapper.Map(rawVal, &relationships) { + got = append(got, relationshipValueListKindSignature(t, relationships)) + } + } + } + + want := make([]string, len(expected)) + for idx, expectedKinds := range expected { + want[idx] = strings.Join(expectedKinds, "->") + } + + assertStringMultiset(t, got, want, "relationship-list kind sequences") + } +} + func pathNodeIDSignature(t *testing.T, path graph.Path, ctx assertionContext) string { t.Helper() @@ -919,6 +952,40 @@ func pathEdgeKindSignature(t *testing.T, path graph.Path) string { return strings.Join(edgeKinds, "->") } +func relationshipListKindSignature(t *testing.T, relationships []*graph.Relationship) string { + t.Helper() + + edgeKinds := make([]string, len(relationships)) + for idx, relationship := range relationships { + if relationship == nil { + t.Fatalf("relationship list contains nil relationship at index %d", idx) + } + + if relationship.Kind == nil { + t.Fatalf("relationship list item at index %d has nil kind", idx) + } + + edgeKinds[idx] = relationship.Kind.String() + } + + return strings.Join(edgeKinds, "->") +} + +func relationshipValueListKindSignature(t *testing.T, relationships []graph.Relationship) string { + t.Helper() + + edgeKinds := make([]string, len(relationships)) + for idx, relationship := range relationships { + if relationship.Kind == nil { + t.Fatalf("relationship list item at index %d has nil kind", idx) + } + + edgeKinds[idx] = relationship.Kind.String() + } + + return strings.Join(edgeKinds, "->") +} + func collectPaths(t *testing.T, result queryResult) []graph.Path { t.Helper() diff --git a/integration/testdata/cases/pattern_binding_inline.json b/integration/testdata/cases/pattern_binding_inline.json index 02d482a7..a9f482de 100644 --- a/integration/testdata/cases/pattern_binding_inline.json +++ b/integration/testdata/cases/pattern_binding_inline.json @@ -144,7 +144,7 @@ {"start_id": "b", "end_id": "t", "kind": "EdgeKind2"} ] }, - "assert": "non_empty" + "assert": {"path_lengths": [2], "path_node_ids": [["a", "b", "t"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2"]]} }, { "name": "filter a typed node with WHERE then bind its variable-length expansion path", diff --git a/integration/testdata/templates/pattern_shapes.json b/integration/testdata/templates/pattern_shapes.json index f7e117dc..563cceb7 100644 --- a/integration/testdata/templates/pattern_shapes.json +++ b/integration/testdata/templates/pattern_shapes.json @@ -77,7 +77,7 @@ ] }, { - "name": "path node-list functions", + "name": "path component functions", "template": "{{query}}", "fixture": { "nodes": [ @@ -107,6 +107,13 @@ "query": "match p=(e:TemplateNodeKind1)<-[:TemplateEdgeKind1]-(d:TemplateNodeKind2) where e.name = 'inbound' return nodes(p)" }, "assert": {"node_list_ids": [["e", "d"]]} + }, + { + "name": "relationships function returns path traversal order", + "vars": { + "query": "match p=(a:TemplateNodeKind1)-[:TemplateEdgeKind1]->(b:TemplateNodeKind2)-[:TemplateEdgeKind2]->(c:TemplateNodeKind1) where a.name = 'src' return relationships(p)" + }, + "assert": {"relationship_list_kinds": [["TemplateEdgeKind1", "TemplateEdgeKind2"]]} } ] }, diff --git a/tools/metrics/internal/metrics/quality.go b/tools/metrics/internal/metrics/quality.go index a260dadd..3514ddfe 100644 --- a/tools/metrics/internal/metrics/quality.go +++ b/tools/metrics/internal/metrics/quality.go @@ -1380,6 +1380,7 @@ var allowedObjectAssertions = map[string]struct{}{ "path_edge_kinds": {}, "path_lengths": {}, "path_node_ids": {}, + "relationship_list_kinds": {}, "row_count": {}, "row_values": {}, "scalar_values": {}, From 5e994de4ece17c24309729ae401f3d23ff673d4a Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:06:20 -0700 Subject: [PATCH 015/114] test(pgsql): guard relationship expression materialization --- .../pgsql/translate/optimizer_safety_test.go | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index aab84693..fb1d22bf 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -73,13 +73,10 @@ func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { require.Contains(t, normalizedQuery, "from s5, s7") } -func TestOptimizerSafetyReferencedRelationshipStaysComposite(t *testing.T) { - t.Parallel() +func assertOptimizerSafetyRelationshipStaysComposite(t *testing.T, cypherQuery string) { + t.Helper() - regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` -MATCH p = (n:Group)-[r:MemberOf]->(m:Group) -RETURN p, r -`) + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) require.NoError(t, err) translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) @@ -91,9 +88,58 @@ RETURN p, r normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") require.Contains(t, normalizedQuery, "(e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0") - require.Contains(t, normalizedQuery, "array [s0.e0]::edgecomposite[]") + require.Contains(t, normalizedQuery, "::edgecomposite") require.NotContains(t, normalizedQuery, "e0.id as e0") - require.NotContains(t, normalizedQuery, "array [s0.e0]::int8[]") + require.NotContains(t, normalizedQuery, "::int8[]") +} + +func TestOptimizerSafetyReferencedRelationshipStaysComposite(t *testing.T) { + t.Parallel() + + assertOptimizerSafetyRelationshipStaysComposite(t, ` +MATCH p = (n:Group)-[r:MemberOf]->(m:Group) +RETURN p, r +`) +} + +func TestOptimizerSafetyRelationshipExpressionReferencesStayComposite(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + query string + }{ + { + name: "type return", + query: ` +MATCH p = (n:Group)-[r:MemberOf]->(m:Group) +RETURN p, type(r) +`, + }, + { + name: "property predicate", + query: ` +MATCH p = (n:Group)-[r:MemberOf]->(m:Group) +WHERE r.label = 'member' +RETURN p +`, + }, + { + name: "start node return", + query: ` +MATCH p = (n:Group)-[r:MemberOf]->(m:Group) +RETURN p, startNode(r) +`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + assertOptimizerSafetyRelationshipStaysComposite(t, testCase.query) + }) + } } func TestOptimizerSafetyOptionalMatchPathStaysComposite(t *testing.T) { From d6fb01e3acc3f01f3de5455a756350f14a8618e6 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:06:41 -0700 Subject: [PATCH 016/114] docs(pgsql): capture optimizer measurement gaps --- docs/optimization-pass-memory.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index 6b0b40e6..ebdd979f 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -178,3 +178,25 @@ Add focused translation tests proving direct relationship references keep edge c ### Step 4: Document Performance Measurement Needs Keep the current shape tests as guardrails, but add an explicit measurement task for high-fanout ADCS-style data before expanding the optimizer into endpoint-aware expansion, suffix semi-joins, or deterministic reordering. + +## Quality Review Status Notes + +The review follow-up should leave the first optimizer milestone in a measured state before the next rule is attempted. + +- `OPTIONAL MATCH` must remain a translator pruning barrier until optional path returns and optional path functions have semantic integration coverage. +- Mixed fixed-hop plus variable-length path returns should assert exact node order, relationship order, and path length. These cases exercise the same late-materialization mechanics as the motivating query with a smaller result surface. +- `relationships(p)` should have relationship-list assertions so path component functions are checked directly instead of indirectly through SQL shape. +- Direct relationship bindings referenced by return expressions, predicates, `type(r)`, or endpoint functions must keep edge composites and must not be narrowed to path-edge IDs. +- The ADCS fixture currently has SQL-shape and containment coverage. Stricter path cardinality assertions on PostgreSQL exposed duplicated returned path rows during review, so exact cardinality for that fixture should be investigated as part of the high-fanout measurement work rather than added as a passing oracle prematurely. + +## Measurement Checklist Before Phase 7 + +Before implementing expand-into detection, capture the following for the motivating ADCS query and a synthetic fanout variant: + +- `EXPLAIN (ANALYZE, BUFFERS)` for `p1` alone, `p2` alone, and the combined query. +- Result row count, distinct `(p1, p2)` count, and duplicate-row count. +- Intermediate row counts for expansion CTEs before and after projection pruning. +- Final path reconstruction cost when paths are returned versus when only endpoint keys are returned. +- Comparison with Neo4j result cardinality for the same fixture. + +Projection pruning and late path materialization currently live in PostgreSQL translator lowering. If later phases need richer rule-level ordering or barrier enforcement, promote these decisions into explicit optimizer rule metadata instead of adding more hidden translator-side state. From 74099c0f78ba444c600898f61c0da968046f3fab Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:07:23 -0700 Subject: [PATCH 017/114] test(pgsql): update optional match barrier shape --- cypher/models/pgsql/test/translation_cases/multipart.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index 6a420e42..241c42d5 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -90,4 +90,4 @@ with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (sel with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select e2.id as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, e3.id as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e2]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e3]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4, s4.n5]::nodecomposite[])::pathcomposite as p from s4; -- case: match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0 from s1 join edge e0 on (s1.n0).id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s3 as (select s1.n0 as n0, s2.e0 as e0 from s1 left outer join s2 on (s1.n0 = s2.n0)) select s3.n0 as n0, count(s3.e0)::int8 as i0 from s3 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); +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, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 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, s2.n1 as n1 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); From e874334376861f2c3aa2fda8616e97f4bba3666f Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:22:01 -0700 Subject: [PATCH 018/114] feat(pgsql): lower bound fixed hops as expand-into --- .../translation_cases/pattern_binding.sql | 4 +- .../pgsql/translate/optimizer_safety_test.go | 24 +++++++ cypher/models/pgsql/translate/traversal.go | 72 +++++++++++++++++++ .../cases/pattern_binding_inline.json | 2 +- 4 files changed, 99 insertions(+), 3 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index 772dc2b3..b56adb40 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -66,10 +66,10 @@ with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposi with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1 limit 1000; -- case: match (x:NodeKind1) where x.name = 'foo' match (y:NodeKind2) where y.name = 'bar' match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match (y:NodeKind2{name:'bar'}) match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id join node n1 on (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match p=(x)-[:EdgeKind1]->(y:NodeKind2{name:'bar'}) return p with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1; diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index fb1d22bf..f28fdd8f 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -163,3 +163,27 @@ RETURN n, p require.Contains(t, normalizedQuery, "::edgecomposite[]") require.NotContains(t, normalizedQuery, "::int8[]") } + +func TestOptimizerSafetyFixedHopExpandIntoUsesBoundEndpoints(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` +MATCH (a:Group) +MATCH (b:Group) +MATCH p = (a)-[:MemberOf]->(b) +RETURN p +`) + require.NoError(t, err) + + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + require.NoError(t, err) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "(s1.n0).id = e0.start_id") + require.Contains(t, normalizedQuery, "(s1.n1).id = e0.end_id") + require.NotContains(t, normalizedQuery, "join node") +} diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 189d94cb..5164c688 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -9,7 +9,71 @@ import ( "github.com/specterops/dawgs/graph" ) +func boundEndpointIDReference(frame *Frame, binding *BoundIdentifier) pgsql.RowColumnReference { + return pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{frame.Binding.Identifier, binding.Identifier}, + Column: pgsql.ColumnID, + } +} + +func boundEndpointInequality(frame *Frame, traversalStep *TraversalStep) pgsql.Expression { + return pgsql.NewParenthetical( + pgsql.NewBinaryExpression( + boundEndpointIDReference(frame, traversalStep.LeftNode), + pgsql.OperatorCypherNotEquals, + boundEndpointIDReference(frame, traversalStep.RightNode), + ), + ) +} + +func (s *Translator) buildBoundEndpointTraversalPattern(partFrame *Frame, traversalStep *TraversalStep) (pgsql.Query, error) { + if partFrame == nil || partFrame.Previous == nil { + return pgsql.Query{}, errors.New("expected previous frame for bound endpoint traversal") + } + + var ( + previousFrame = partFrame.Previous + nextSelect = pgsql.Select{ + Projection: traversalStep.Projection, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{previousFrame.Binding.Identifier}, + }, + Joins: []pgsql.Join{{ + Table: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{pgsql.TableEdge}, + Binding: models.OptionalValue(traversalStep.Edge.Identifier), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.OptionalAnd( + traversalStep.EdgeJoinCondition, + traversalStep.RightNodeJoinCondition, + ), + }, + }}, + }}, + } + ) + + nextSelect.Where = pgsql.OptionalAnd(traversalStep.LeftNodeConstraints, nextSelect.Where) + nextSelect.Where = pgsql.OptionalAnd(traversalStep.EdgeConstraints.Expression, nextSelect.Where) + nextSelect.Where = pgsql.OptionalAnd(traversalStep.RightNodeConstraints, nextSelect.Where) + + if traversalStep.Direction == graph.DirectionBoth && traversalStep.LeftNode.Identifier != traversalStep.RightNode.Identifier { + nextSelect.Where = pgsql.OptionalAnd(boundEndpointInequality(previousFrame, traversalStep), nextSelect.Where) + } + + return pgsql.Query{ + Body: nextSelect, + }, nil +} + func (s *Translator) buildDirectionlessTraversalPatternRoot(traversalStep *TraversalStep) (pgsql.Query, error) { + if traversalStep.LeftNodeBound && traversalStep.RightNodeBound { + return s.buildBoundEndpointTraversalPattern(traversalStep.Frame, traversalStep) + } + var ( // Partition node constraints rightJoinLocal, rightJoinExternal = partitionConstraintByLocality( @@ -258,6 +322,10 @@ func (s *Translator) buildTraversalPatternRoot(partFrame *Frame, traversalStep * return s.buildDirectionlessTraversalPatternRoot(traversalStep) } + if traversalStep.LeftNodeBound && traversalStep.RightNodeBound { + return s.buildBoundEndpointTraversalPattern(partFrame, traversalStep) + } + var ( // Partition right-node constraints: only locally-scoped terms go into JOIN ON. // Constraints that reference comma-connected CTEs (e.g. s0.i0 from a prior WITH) @@ -440,6 +508,10 @@ func (s *Translator) buildTraversalPatternRoot(partFrame *Frame, traversalStep * } func (s *Translator) buildTraversalPatternStep(partFrame *Frame, traversalStep *TraversalStep) (pgsql.Query, error) { + if traversalStep.LeftNodeBound && traversalStep.RightNodeBound { + return s.buildBoundEndpointTraversalPattern(partFrame, traversalStep) + } + nextSelect := pgsql.Select{ Projection: traversalStep.Projection, } diff --git a/integration/testdata/cases/pattern_binding_inline.json b/integration/testdata/cases/pattern_binding_inline.json index a9f482de..45879ecd 100644 --- a/integration/testdata/cases/pattern_binding_inline.json +++ b/integration/testdata/cases/pattern_binding_inline.json @@ -168,7 +168,7 @@ ], "edges": [{"start_id": "x", "end_id": "y", "kind": "EdgeKind1"}] }, - "assert": "non_empty" + "assert": {"path_lengths": [1], "path_node_ids": [["x", "y"]], "path_edge_kinds": [["EdgeKind1"]]} }, { "name": "match a node with an inline property map then bind its outgoing path to a second inline-map node", From f891fc403e1667e45443132a0928c3c29d9bf13f Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 17:25:32 -0700 Subject: [PATCH 019/114] feat(pgsql): reorder independent node anchors --- cypher/models/pgsql/optimize/optimizer.go | 1 + .../models/pgsql/optimize/optimizer_test.go | 71 +++++- cypher/models/pgsql/optimize/reordering.go | 218 ++++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 28 +++ .../cases/pattern_binding_inline.json | 16 ++ 5 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 cypher/models/pgsql/optimize/reordering.go diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index 87556176..ef66f8ab 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -48,6 +48,7 @@ func NewOptimizer(rules ...Rule) Optimizer { func DefaultRules() []Rule { return []Rule{ + ConservativePatternReorderingRule{}, PredicateAttachmentRule{}, } } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index b36f8316..332a3d89 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/cypher" "github.com/stretchr/testify/require" ) @@ -31,7 +32,10 @@ func TestOptimizeCopiesAndAnalyzesQuery(t *testing.T) { require.Len(t, plan.Analysis.QueryParts, 1) require.Len(t, plan.Analysis.QueryParts[0].Regions, 1) require.Equal(t, []string{"p1", "p2"}, plan.Analysis.QueryParts[0].ProjectionDependencies) - require.Equal(t, []RuleResult{{Name: "PredicateAttachment", Applied: true}}, plan.Rules) + require.Equal(t, []RuleResult{ + {Name: "ConservativePatternReordering", Applied: false}, + {Name: "PredicateAttachment", Applied: true}, + }, plan.Rules) require.Len(t, plan.PredicateAttachments, 2) } @@ -56,7 +60,10 @@ func TestDefaultPredicateAttachmentRuleReportsSkippedWhenNoPredicatesExist(t *te plan, err := Optimize(regularQuery) require.NoError(t, err) - require.Equal(t, []RuleResult{{Name: "PredicateAttachment", Applied: false}}, plan.Rules) + require.Equal(t, []RuleResult{ + {Name: "ConservativePatternReordering", Applied: false}, + {Name: "PredicateAttachment", Applied: false}, + }, plan.Rules) require.Empty(t, plan.PredicateAttachments) } @@ -115,3 +122,63 @@ func TestPredicateAttachmentRuleKeepsMultiBindingPredicatesAtRegionScope(t *test Dependencies: []string{"a", "b"}, }, plan.PredicateAttachments[0]) } + +func firstNodeSymbol(readingClause *cypher.ReadingClause) string { + if readingClause == nil || readingClause.Match == nil || len(readingClause.Match.Pattern) == 0 { + return "" + } + + nodePattern, ok := singleNodePattern(readingClause.Match.Pattern[0]) + if !ok || nodePattern.Variable == nil { + return "" + } + + return nodePattern.Variable.Symbol +} + +func TestConservativePatternReorderingMovesIndependentNodeAnchorsEarlier(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (a) + MATCH (b:Group {objectid: 'target'}) + MATCH p = (a)-[:MemberOf]->(b) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, []RuleResult{ + {Name: "ConservativePatternReordering", Applied: true}, + {Name: "PredicateAttachment", Applied: false}, + }, plan.Rules) + + readingClauses := plan.Query.SingleQuery.SinglePartQuery.ReadingClauses + require.Equal(t, "b", firstNodeSymbol(readingClauses[0])) + require.Equal(t, "a", firstNodeSymbol(readingClauses[1])) + require.Len(t, readingClauses[2].Match.Pattern[0].PatternElements, 3) +} + +func TestConservativePatternReorderingKeepsDependentAnchorsInPlace(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (a) + MATCH (b:Group) + WHERE b.name = a.name + RETURN b + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, []RuleResult{ + {Name: "ConservativePatternReordering", Applied: false}, + {Name: "PredicateAttachment", Applied: true}, + }, plan.Rules) + + readingClauses := plan.Query.SingleQuery.SinglePartQuery.ReadingClauses + require.Equal(t, "a", firstNodeSymbol(readingClauses[0])) + require.Equal(t, "b", firstNodeSymbol(readingClauses[1])) +} diff --git a/cypher/models/pgsql/optimize/reordering.go b/cypher/models/pgsql/optimize/reordering.go new file mode 100644 index 00000000..7c46a67d --- /dev/null +++ b/cypher/models/pgsql/optimize/reordering.go @@ -0,0 +1,218 @@ +package optimize + +import ( + "sort" + + "github.com/specterops/dawgs/cypher/models/cypher" +) + +type ConservativePatternReorderingRule struct{} + +func (s ConservativePatternReorderingRule) Name() string { + return "ConservativePatternReordering" +} + +func (s ConservativePatternReorderingRule) Apply(plan *Plan) (bool, error) { + if plan == nil || plan.Query == nil || plan.Query.SingleQuery == nil { + return false, nil + } + + if plan.Query.SingleQuery.MultiPartQuery != nil { + return reorderMultiPartQuery(plan.Query.SingleQuery.MultiPartQuery, plan.Analysis), nil + } + + if plan.Query.SingleQuery.SinglePartQuery != nil { + return reorderSinglePartQuery(plan.Query.SingleQuery.SinglePartQuery, plan.Analysis), nil + } + + return false, nil +} + +type reorderCandidate struct { + clause *cypher.ReadingClause + rank int + index int +} + +func reorderMultiPartQuery(query *cypher.MultiPartQuery, analysis Analysis) bool { + var applied bool + + for partIndex, part := range query.Parts { + if part == nil { + continue + } + + if queryPart, ok := analysisQueryPart(analysis, partIndex); ok { + applied = reorderReadingClauses(part.ReadingClauses, queryPart.Regions) || applied + } + } + + if query.SinglePartQuery != nil { + if queryPart, ok := analysisQueryPart(analysis, len(query.Parts)); ok { + applied = reorderReadingClauses(query.SinglePartQuery.ReadingClauses, queryPart.Regions) || applied + } + } + + return applied +} + +func reorderSinglePartQuery(query *cypher.SinglePartQuery, analysis Analysis) bool { + if queryPart, ok := analysisQueryPart(analysis, 0); ok { + return reorderReadingClauses(query.ReadingClauses, queryPart.Regions) + } + + return false +} + +func analysisQueryPart(analysis Analysis, index int) (QueryPart, bool) { + for _, queryPart := range analysis.QueryParts { + if queryPart.Index == index { + return queryPart, true + } + } + + return QueryPart{}, false +} + +func reorderReadingClauses(readingClauses []*cypher.ReadingClause, regions []Region) bool { + var applied bool + + for _, region := range regions { + if region.StartClause < 0 || region.EndClause >= len(readingClauses) || region.StartClause >= region.EndClause { + continue + } + + applied = reorderRegion(readingClauses[region.StartClause:region.EndClause+1]) || applied + } + + return applied +} + +func reorderRegion(regionClauses []*cypher.ReadingClause) bool { + candidates := make([]reorderCandidate, len(regionClauses)) + declaredBefore := map[string]struct{}{} + + for idx, clause := range regionClauses { + candidates[idx] = reorderCandidate{ + clause: clause, + rank: matchClauseRank(clause, declaredBefore), + index: idx, + } + + for _, binding := range bindingsForReadingClause(idx, clause) { + declaredBefore[binding.Symbol] = struct{}{} + } + } + + sort.SliceStable(candidates, func(i, j int) bool { + return candidates[i].rank < candidates[j].rank + }) + + var applied bool + for idx, candidate := range candidates { + if regionClauses[idx] != candidate.clause { + applied = true + regionClauses[idx] = candidate.clause + } + } + + return applied +} + +func matchClauseRank(readingClause *cypher.ReadingClause, declaredBefore map[string]struct{}) int { + if isIndependentNodeAnchor(readingClause, declaredBefore) { + return 0 + } + + return 1 +} + +func isIndependentNodeAnchor(readingClause *cypher.ReadingClause, declaredBefore map[string]struct{}) bool { + if readingClause == nil || readingClause.Match == nil { + return false + } + + match := readingClause.Match + if match.Optional || len(match.Pattern) != 1 { + return false + } + + nodePattern, ok := singleNodePattern(match.Pattern[0]) + if !ok || nodePattern.Variable == nil || nodePattern.Variable.Symbol == "" { + return false + } + + if _, alreadyDeclared := declaredBefore[nodePattern.Variable.Symbol]; alreadyDeclared { + return false + } + + if !isSelectiveNodeAnchor(nodePattern, match.Where) { + return false + } + + declared := bindingSymbolSet(bindingsForMatch(0, match)) + for _, dependency := range localMatchDependencies(match) { + if _, isLocal := declared[dependency]; !isLocal { + return false + } + } + + return true +} + +func singleNodePattern(pattern *cypher.PatternPart) (*cypher.NodePattern, bool) { + if pattern == nil || pattern.Variable != nil || len(pattern.PatternElements) != 1 { + return nil, false + } + + return pattern.PatternElements[0].AsNodePattern() +} + +func isSelectiveNodeAnchor(nodePattern *cypher.NodePattern, where *cypher.Where) bool { + return len(nodePattern.Kinds) > 0 || nodePattern.Properties != nil || wherePredicateCount(where) > 0 +} + +func localMatchDependencies(match *cypher.Match) []string { + if match == nil { + return nil + } + + var dependencies []string + for _, pattern := range match.Pattern { + if pattern == nil { + continue + } + + for _, element := range pattern.PatternElements { + if element == nil { + continue + } + + if nodePattern, ok := element.AsNodePattern(); ok { + dependencies = append(dependencies, sortedDependencies(nodePattern.Properties)...) + } else if relationshipPattern, ok := element.AsRelationshipPattern(); ok { + dependencies = append(dependencies, sortedDependencies(relationshipPattern.Properties)...) + } + } + } + + dependencies = append(dependencies, dependenciesForMatch(match)...) + return sortedUniqueStrings(dependencies) +} + +func bindingSymbolSet(bindings []Binding) map[string]struct{} { + symbols := make(map[string]struct{}, len(bindings)) + for _, binding := range bindings { + symbols[binding.Symbol] = struct{}{} + } + + return symbols +} + +func bindingsForReadingClause(clauseIndex int, readingClause *cypher.ReadingClause) []Binding { + if readingClause == nil || readingClause.Match == nil { + return nil + } + + return bindingsForMatch(clauseIndex, readingClause.Match) +} diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index f28fdd8f..92b95afc 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -187,3 +187,31 @@ RETURN p require.Contains(t, normalizedQuery, "(s1.n1).id = e0.end_id") require.NotContains(t, normalizedQuery, "join node") } + +func TestOptimizerSafetyReordersIndependentNodeAnchor(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` +MATCH (a) +MATCH (b:EnterpriseCA {name: 'target'}) +MATCH p = (a)-[:MemberOf]->(b) +RETURN p +`) + require.NoError(t, err) + + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + require.NoError(t, err) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + enterpriseAnchorIndex := strings.Index(normalizedQuery, "array [5]::int2[]") + broadScanIndex := strings.Index(normalizedQuery, "from s0, node n1") + + require.NotEqual(t, -1, enterpriseAnchorIndex) + require.NotEqual(t, -1, broadScanIndex) + require.Less(t, enterpriseAnchorIndex, broadScanIndex) + require.Contains(t, normalizedQuery, "(s1.n1).id = e0.start_id") + require.Contains(t, normalizedQuery, "(s1.n0).id = e0.end_id") +} diff --git a/integration/testdata/cases/pattern_binding_inline.json b/integration/testdata/cases/pattern_binding_inline.json index 45879ecd..e08bfb7c 100644 --- a/integration/testdata/cases/pattern_binding_inline.json +++ b/integration/testdata/cases/pattern_binding_inline.json @@ -170,6 +170,22 @@ }, "assert": {"path_lengths": [1], "path_node_ids": [["x", "y"]], "path_edge_kinds": [["EdgeKind1"]]} }, + { + "name": "reorder independent node anchor before binding a connecting path", + "cypher": "match (x) match (y:NodeKind2 {name: 'target'}) match p=(x)-[:EdgeKind1]->(y) return p", + "fixture": { + "nodes": [ + {"id": "x", "kinds": ["NodeKind1"]}, + {"id": "target", "kinds": ["NodeKind2"], "properties": {"name": "target"}}, + {"id": "other", "kinds": ["NodeKind2"], "properties": {"name": "other"}} + ], + "edges": [ + {"start_id": "x", "end_id": "target", "kind": "EdgeKind1"}, + {"start_id": "x", "end_id": "other", "kind": "EdgeKind1"} + ] + }, + "assert": {"path_lengths": [1], "path_node_ids": [["x", "target"]], "path_edge_kinds": [["EdgeKind1"]]} + }, { "name": "match a node with an inline property map then bind its outgoing path to a second inline-map node", "cypher": "match (x:NodeKind1{name:'foo'}) match p=(x)-[:EdgeKind1]->(y:NodeKind2{name:'bar'}) return p", From f696b3b2b2c612ee1a4ca292967c8ecef7d3689a Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 18:05:55 -0700 Subject: [PATCH 020/114] feat(pgsql): push fixed suffix checks into expansions --- .../test/translation_cases/multipart.sql | 10 +- .../translation_cases/pattern_binding.sql | 4 +- .../translation_cases/pattern_expansion.sql | 6 +- cypher/models/pgsql/translate/expansion.go | 98 ++++++++++++++++ cypher/models/pgsql/translate/model.go | 107 ++++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 23 ++++ cypher/models/pgsql/translate/traversal.go | 4 + .../testdata/cases/expansion_inline.json | 18 +++ 8 files changed, 260 insertions(+), 10 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index 241c42d5..a9b601db 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -24,13 +24,13 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'value'))::jsonb = to_jsonb((1)::int8)::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 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('me')::text)::jsonb)) select s3.n1 as n1 from s3), s4 as (select s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where (n2.id = (s2.n1).id)) select s4.n2 as b from s4; -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, collect(distinct(n)) as p where size(p) >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: with 365 as max_days match (n:NodeKind1) where n.pwdlastset < (datetime().epochseconds - (max_days * 86400)) and not n.pwdlastset IN [-1.0, 0.0] return n limit 100 with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (not ((n0.properties ->> 'pwdlastset'))::float8 = any (array [- 1, 0]::float8[]) and ((n0.properties ->> 'pwdlastset'))::numeric < (extract(epoch from now()::timestamp with time zone)::numeric - (s0.i0 * 86400))) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1 limit 100; @@ -81,10 +81,10 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; -- case: match (m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)-[:EdgeKind2]->(c3:NodeKind1) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and not m.samaccountname contains "DEX" and not g.name IN ["D"] and not m.samaccountname =~ "^.*$" with collect(g.name) as admingroups match p=(m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and g.name in admingroups and not m.samaccountname =~ "^.*$" return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; -- case: match (a:NodeKind2)-[:EdgeKind1]->(g:NodeKind1)-[:EdgeKind2]->(s:NodeKind2) with count(a) as uc where uc > 5 match p = (a)-[:EdgeKind1]->(g)-[:EdgeKind2]->(s) return p with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select e2.id as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, e3.id as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e2]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e3]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4, s4.n5]::nodecomposite[])::pathcomposite as p from s4; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index b56adb40..57ea7e18 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -45,7 +45,7 @@ with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposi with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1; -- case: match p = (s)-[*..]->(i)-[]->() where id(s) = 1 and i.name = 'n3' return p limit 1 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; -- case: match p = ()-[e:EdgeKind1]->()-[:EdgeKind1*..]->() return e, p with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; @@ -81,7 +81,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql index beaecc8c..273b3530 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -36,16 +36,16 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as 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(('n2')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n0 as n from s0; -- case: match (n)-[*..]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; -- case: match (n)-[*2..3]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; -- case: match (n)-[]->(e:NodeKind1)-[*2..3]->(l) where n.name = 'n1' return l with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; -- case: match (n)-[*..]->(e)-[:EdgeKind1|EdgeKind2]->()-[*..]->(l) where n.name = 'n1' and e.name = 'n2' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[])), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; +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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[])), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; -- case: match p = (:NodeKind1)-[:EdgeKind1*1..]->(n:NodeKind2) where 'admin_tier_0' in split(n.system_tags, ' ') return p limit 1000 with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index e34a7a41..84a3e4f2 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -8,6 +8,7 @@ import ( "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/specterops/dawgs/cypher/models/pgsql/format" "github.com/specterops/dawgs/cypher/models/pgsql/pgd" + "github.com/specterops/dawgs/graph" ) const translateDefaultMaxTraversalDepth int64 = 15 @@ -2285,6 +2286,103 @@ func expansionTerminalSatisfactionLocality(traversalStep *TraversalStep) (pgsql. ) } +func applyExpansionSuffixPushdown(part *PatternPart) error { + for idx := 0; idx+1 < len(part.TraversalSteps); idx++ { + var ( + currentStep = part.TraversalSteps[idx] + nextStep = part.TraversalSteps[idx+1] + ) + + if suffixSatisfaction, satisfied := expansionSuffixTerminalSatisfaction(currentStep, nextStep); satisfied { + currentStep.Expansion.TerminalNodeConstraints = pgsql.OptionalAnd( + currentStep.Expansion.TerminalNodeConstraints, + suffixSatisfaction, + ) + + if terminalCriteriaProjection, err := pgsql.As[pgsql.SelectItem](currentStep.Expansion.TerminalNodeConstraints); err != nil { + return err + } else { + currentStep.Expansion.TerminalNodeSatisfactionProjection = terminalCriteriaProjection + } + } + } + + return nil +} + +func expansionSuffixTerminalSatisfaction(currentStep, nextStep *TraversalStep) (pgsql.Expression, bool) { + if currentStep == nil || + currentStep.Expansion == nil || + currentStep.RightNode == nil || + nextStep == nil || + nextStep.Expansion != nil || + nextStep.LeftNode == nil || + nextStep.Edge == nil || + nextStep.RightNode == nil || + nextStep.RightNodeBound || + nextStep.Direction == graph.DirectionBoth || + currentStep.RightNode.Identifier != nextStep.LeftNode.Identifier { + return nil, false + } + + var edgeConstraints pgsql.Expression + if nextStep.EdgeConstraints != nil { + edgeConstraints = nextStep.EdgeConstraints.Expression + } + + localScope := pgsql.AsIdentifierSet( + currentStep.RightNode.Identifier, + nextStep.Edge.Identifier, + nextStep.RightNode.Identifier, + ) + + edgeLocal, edgeExternal := partitionConstraintByLocality(edgeConstraints, localScope) + if edgeExternal != nil { + return nil, false + } + + rightNodeLocal, rightNodeExternal := partitionConstraintByLocality(nextStep.RightNodeConstraints, localScope) + if rightNodeExternal != nil { + return nil, false + } + + terminalJoin, err := leftNodeConstraint( + nextStep.Edge.Identifier, + currentStep.RightNode.Identifier, + nextStep.Direction, + ) + if err != nil { + return nil, false + } + + return pgsql.ExistsExpression{ + Subquery: pgsql.Subquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: pgsql.Projection{pgd.IntLiteral(1)}, + From: []pgsql.FromClause{{ + Source: expansionEdgeTableReference(nextStep.Edge.Identifier), + Joins: []pgsql.Join{{ + Table: expansionNodeTableReference(nextStep.RightNode.Identifier), + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.OptionalAnd( + rightNodeLocal, + nextStep.RightNodeJoinCondition, + ), + }, + }}, + }}, + Where: pgsql.OptionalAnd( + terminalJoin, + edgeLocal, + ), + }, + }, + }, + }, true +} + func expansionLocalTerminalSatisfactionProjection(traversalStep *TraversalStep) (pgsql.SelectItem, error) { localSatisfiedConstraint, _ := expansionTerminalSatisfactionLocality(traversalStep) diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index e1dd8a62..f3f7ab00 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -358,6 +358,14 @@ func expressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression, local walk.PgSQL(expression, walk.NewSimpleVisitor[pgsql.SyntaxNode]( func(node pgsql.SyntaxNode, handler walk.VisitorHandler) { switch typedNode := node.(type) { + case pgsql.ExistsExpression: + if !subqueryReferencesOnlyLocalIdentifiers(typedNode.Subquery, localScope) { + isLocal = false + handler.SetDone() + } else { + handler.Consume() + } + case pgsql.CompoundIdentifier: if len(typedNode) > 0 && !localScope.Contains(typedNode[0]) { isLocal = false @@ -384,6 +392,105 @@ func expressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression, local return isLocal } +func subqueryReferencesOnlyLocalIdentifiers(subquery pgsql.Subquery, localScope *pgsql.IdentifierSet) bool { + return queryReferencesOnlyLocalIdentifiers(subquery.Query, localScope) +} + +func queryReferencesOnlyLocalIdentifiers(query pgsql.Query, localScope *pgsql.IdentifierSet) bool { + if query.CommonTableExpressions != nil { + return false + } + + selectBody, isSelect := query.Body.(pgsql.Select) + if !isSelect { + return false + } + + if !selectReferencesOnlyLocalIdentifiers(selectBody, localScope) { + return false + } + + for _, orderBy := range query.OrderBy { + if orderBy != nil && !expressionReferencesOnlyLocalIdentifiers(orderBy.Expression, localScope) { + return false + } + } + + return (query.Offset == nil || expressionReferencesOnlyLocalIdentifiers(query.Offset, localScope)) && + (query.Limit == nil || expressionReferencesOnlyLocalIdentifiers(query.Limit, localScope)) +} + +func addFromClauseBindings(localScope *pgsql.IdentifierSet, fromClauses []pgsql.FromClause) { + for _, fromClause := range fromClauses { + addFromExpressionBinding(localScope, fromClause.Source) + + for _, join := range fromClause.Joins { + addFromExpressionBinding(localScope, join.Table) + } + } +} + +func addFromExpressionBinding(localScope *pgsql.IdentifierSet, expression pgsql.Expression) { + switch typedExpression := expression.(type) { + case pgsql.TableReference: + if typedExpression.Binding.Set { + localScope.Add(typedExpression.Binding.Value) + } + + case pgsql.LateralSubquery: + if typedExpression.Binding.Set { + localScope.Add(typedExpression.Binding.Value) + } + } +} + +func selectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *pgsql.IdentifierSet) bool { + scopedIdentifiers := localScope.Copy() + addFromClauseBindings(scopedIdentifiers, selectBody.From) + + for _, projection := range selectBody.Projection { + if !expressionReferencesOnlyLocalIdentifiers(projection, scopedIdentifiers) { + return false + } + } + + for _, fromClause := range selectBody.From { + if !fromExpressionReferencesOnlyLocalIdentifiers(fromClause.Source) { + return false + } + + for _, join := range fromClause.Joins { + if !fromExpressionReferencesOnlyLocalIdentifiers(join.Table) { + return false + } + + if join.JoinOperator.Constraint != nil && + !expressionReferencesOnlyLocalIdentifiers(join.JoinOperator.Constraint, scopedIdentifiers) { + return false + } + } + } + + for _, groupByExpression := range selectBody.GroupBy { + if !expressionReferencesOnlyLocalIdentifiers(groupByExpression, scopedIdentifiers) { + return false + } + } + + return (selectBody.Where == nil || expressionReferencesOnlyLocalIdentifiers(selectBody.Where, scopedIdentifiers)) && + (selectBody.Having == nil || expressionReferencesOnlyLocalIdentifiers(selectBody.Having, scopedIdentifiers)) +} + +func fromExpressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression) bool { + switch expression.(type) { + case pgsql.TableReference: + return true + + default: + return false + } +} + func isLocalToScope(expression pgsql.Expression, localScope *pgsql.IdentifierSet) bool { if expression == nil { return true diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 92b95afc..98a8bff9 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -215,3 +215,26 @@ RETURN p require.Contains(t, normalizedQuery, "(s1.n1).id = e0.start_id") require.Contains(t, normalizedQuery, "(s1.n0).id = e0.end_id") } + +func TestOptimizerSafetyExpansionTerminalPushdownForFixedSuffix(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` +MATCH p = (n:Group)-[:MemberOf*1..]->(m)-[:Enroll]->(ca:EnterpriseCA) +RETURN p +`) + require.NoError(t, err) + + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + require.NoError(t, err) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "exists (select 1 from edge e1 join node n2") + require.Contains(t, normalizedQuery, "n1.id = e1.start_id") + require.Contains(t, normalizedQuery, "e1.kind_id = any (array [4]::int2[])") + require.Contains(t, normalizedQuery, "n2.kind_ids operator (pg_catalog.@>) array [5]::int2[]") +} diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 5164c688..6192c47d 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -601,6 +601,10 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } } + if err := applyExpansionSuffixPushdown(part); err != nil { + return err + } + if isolatedProjection { s.scope = scopeSnapshot } diff --git a/integration/testdata/cases/expansion_inline.json b/integration/testdata/cases/expansion_inline.json index 28e18cfa..929f32a4 100644 --- a/integration/testdata/cases/expansion_inline.json +++ b/integration/testdata/cases/expansion_inline.json @@ -48,6 +48,24 @@ }, "assert": {"node_ids": ["leaf"]} }, + { + "name": "bind expansion path through only terminals that satisfy a fixed suffix", + "cypher": "match p = (src:NodeKind1)-[:EdgeKind1*1..]->(mid)-[:EdgeKind2]->(dst:NodeKind2) where src.name = 'terminal-pushdown-src' return p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "terminal-pushdown-src"}}, + {"id": "good-mid", "kinds": ["NodeKind1"]}, + {"id": "dead-mid", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"]} + ], + "edges": [ + {"start_id": "src", "end_id": "good-mid", "kind": "EdgeKind1"}, + {"start_id": "src", "end_id": "dead-mid", "kind": "EdgeKind1"}, + {"start_id": "good-mid", "end_id": "dst", "kind": "EdgeKind2"} + ] + }, + "assert": {"path_lengths": [2], "path_node_ids": [["src", "good-mid", "dst"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2"]]} + }, { "name": "fixed step followed by a bounded variable-length expansion", "cypher": "match (n)-[]->(e:NodeKind1)-[*2..3]->(l) where n.name = 'start' return l", From 9fd9ee0e4b6aff791d9f9b693b920f471e9c535c Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 18:13:38 -0700 Subject: [PATCH 021/114] docs(pgsql): sequence optimizer gap closure plan --- docs/optimization-pass-memory.md | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index ebdd979f..578f6710 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -189,6 +189,56 @@ The review follow-up should leave the first optimizer milestone in a measured st - Direct relationship bindings referenced by return expressions, predicates, `type(r)`, or endpoint functions must keep edge composites and must not be narrowed to path-edge IDs. - The ADCS fixture currently has SQL-shape and containment coverage. Stricter path cardinality assertions on PostgreSQL exposed duplicated returned path rows during review, so exact cardinality for that fixture should be investigated as part of the high-fanout measurement work rather than added as a passing oracle prematurely. +## Current Gap Closure Plan + +The optimizer branch now has enough implementation to expose the next set of risks. Close those risks in this order so each later rule has a stronger correctness and measurement base. + +### Step 1: Establish A Performance Baseline + +Add a synthetic ADCS fanout scenario before broadening suffix or endpoint-aware expansion rules. Capture `p1` alone, `p2` alone, and the combined query with row counts, distinct `(p1, p2)` counts, duplicate counts, and PostgreSQL `EXPLAIN (ANALYZE, BUFFERS)`. + +This should be the first step because the original report is a timeout, and the current branch is still defended mostly by SQL shape and semantic equivalence tests. + +### Step 2: Strengthen Semantic Oracles + +Add exact-result integration coverage on smaller fixtures before relying on the larger ADCS fixture as an oracle. Assertions should include path node IDs, relationship IDs or kinds in order, path lengths, row count, and `relationships(p)` output for optimized paths. + +Keep the existing ADCS containment test, but treat exact ADCS cardinality as part of the fanout investigation until duplicate-row behavior is understood. + +### Step 3: Make Optimizer Rule Ownership Explicit + +Projection pruning, late path materialization, fixed-hop lowering, and suffix pushdown currently live in PostgreSQL translator lowering instead of explicit optimizer rules. Either promote these decisions into optimizer metadata consumed by the translator, or record them as named lowering decisions so tests and diagnostics can identify which rule changed the SQL shape. + +This step should happen before adding more hidden translator-side rewrites. + +### Step 4: Wire Predicate Attachment Into Translation + +Predicate attachment currently records ownership but does not change translation. Feed attachment metadata into PostgreSQL lowering so local predicates can move into the earliest safe binding, terminal, or suffix check. + +Add SQL shape tests proving `ct` predicates in the motivating query are applied at the intended terminal or suffix point, plus PostgreSQL and Neo4j equivalence coverage. + +### Step 5: Broaden Phase 9 Coverage Before Broadening Phase 9 Behavior + +Add tests for the suffix shapes the motivating query actually depends on: + +- `*0..` variable expansions followed by suffix checks +- chained fixed suffixes after a variable expansion +- suffixes that end at already-bound nodes such as `ca` and `d` +- inbound suffixes +- directionless suffixes that should remain unoptimized until they are implemented deliberately + +These tests should include both SQL shape assertions and integration equivalence. + +### Step 6: Implement Endpoint-Aware Suffix Semi-Joins + +Extend suffix pushdown from the current immediate one-hop local check to endpoint-aware semi-joins that can reason about fixed suffix chains and already-bound endpoints. For the motivating query, this means pruning `MemberOf*0..` endpoints that cannot reach eligible certificate template paths tied to the bound `ca`, and pruning root paths that cannot connect back to the bound `d`. + +Keep path materialization late: use the suffix checks to constrain candidate endpoints, then materialize returned paths only after the result frame is narrowed. + +### Step 7: Re-Measure And Lock The Regression + +After each new suffix or predicate-placement rule, rerun the synthetic fanout measurements and record the before/after SQL shape and runtime characteristics. Promote the final motivating query shape into a benchmark or regression scenario once its cardinality and duplicate behavior are fully understood. + ## Measurement Checklist Before Phase 7 Before implementing expand-into detection, capture the following for the motivating ADCS query and a synthetic fanout variant: From 700777409f90117eb3d7ca49e3b7889ec0042819 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 19:01:08 -0700 Subject: [PATCH 022/114] feat(pgsql): close optimizer suffix pushdown gaps --- cmd/benchmark/README.md | 40 +--- cmd/benchmark/main.go | 3 +- cmd/benchmark/report.go | 7 +- cmd/benchmark/report_test.go | 8 +- cmd/benchmark/runner.go | 30 ++- cmd/benchmark/scenarios.go | 64 ++++-- cypher/models/pgsql/translate/expansion.go | 183 +++++++++++++----- .../pgsql/translate/optimizer_safety_test.go | 144 +++++++++----- cypher/models/pgsql/translate/translator.go | 27 ++- cypher/models/pgsql/translate/traversal.go | 4 +- integration/testdata/adcs_fanout.json | 50 +++++ .../testdata/cases/expansion_inline.json | 58 ++++++ 12 files changed, 458 insertions(+), 160 deletions(-) create mode 100644 integration/testdata/adcs_fanout.json diff --git a/cmd/benchmark/README.md b/cmd/benchmark/README.md index 741118f5..13686821 100644 --- a/cmd/benchmark/README.md +++ b/cmd/benchmark/README.md @@ -1,11 +1,11 @@ # Benchmark -Runs query scenarios against a real database and outputs a markdown timing table. +Runs query scenarios against a real database and outputs a markdown timing table with warm-up row counts. ## Usage ```bash -# Default dataset (base) +# Default datasets (base and adcs_fanout) go run ./cmd/benchmark -connection "postgresql://dawgs:dawgs@localhost:5432/dawgs" # Local dataset (not committed to repo) @@ -43,20 +43,10 @@ go run ./cmd/benchmark -connection "..." -output report.md -json-output report.j $ go run ./cmd/benchmark -driver neo4j -connection "neo4j://neo4j:testpassword@localhost:7687" -dataset local/phantom ``` -| Query | Dataset | Median | P95 | Max | -|-------|---------|-------:|----:|----:| -| Match Nodes | local/phantom | 1.4ms | 2.3ms | 2.3ms | -| Match Edges | local/phantom | 1.6ms | 1.9ms | 1.9ms | -| Filter By Kind / User | local/phantom | 2.0ms | 2.6ms | 2.6ms | -| Filter By Kind / Group | local/phantom | 2.1ms | 2.3ms | 2.3ms | -| Filter By Kind / Computer | local/phantom | 1.6ms | 2.0ms | 2.0ms | -| Traversal Depth / depth 1 | local/phantom | 1.4ms | 2.1ms | 2.1ms | -| Traversal Depth / depth 2 | local/phantom | 1.6ms | 1.9ms | 1.9ms | -| Traversal Depth / depth 3 | local/phantom | 2.5ms | 3.3ms | 3.3ms | -| Edge Kind Traversal / MemberOf | local/phantom | 1.2ms | 1.4ms | 1.4ms | -| Edge Kind Traversal / GenericAll | local/phantom | 1.1ms | 1.5ms | 1.5ms | -| Edge Kind Traversal / HasSession | local/phantom | 1.1ms | 1.4ms | 1.4ms | -| Shortest Paths / 41 -> 587 | local/phantom | 1.5ms | 1.9ms | 1.9ms | +| Query | Dataset | Rows | Median | P95 | Max | +|-------|---------|-----:|-------:|----:|----:| +| Match Nodes | local/phantom | 1000 | 1.4ms | 2.3ms | 2.3ms | +| Match Edges | local/phantom | 2000 | 1.6ms | 1.9ms | 1.9ms | ## Example: PG on local/phantom @@ -65,17 +55,7 @@ $ export CONNECTION_STRING="postgresql://dawgs:dawgs@localhost:5432/dawgs" $ go run ./cmd/benchmark -dataset local/phantom ``` -| Query | Dataset | Median | P95 | Max | -|-------|---------|-------:|----:|----:| -| Match Nodes | local/phantom | 2.0ms | 6.5ms | 6.5ms | -| Match Edges | local/phantom | 464ms | 604ms | 604ms | -| Filter By Kind / User | local/phantom | 4.5ms | 18.3ms | 18.3ms | -| Filter By Kind / Group | local/phantom | 6.2ms | 28.8ms | 28.8ms | -| Filter By Kind / Computer | local/phantom | 1.1ms | 5.5ms | 5.5ms | -| Traversal Depth / depth 1 | local/phantom | 596ms | 636ms | 636ms | -| Traversal Depth / depth 2 | local/phantom | 639ms | 660ms | 660ms | -| Traversal Depth / depth 3 | local/phantom | 726ms | 745ms | 745ms | -| Edge Kind Traversal / MemberOf | local/phantom | 602ms | 627ms | 627ms | -| Edge Kind Traversal / GenericAll | local/phantom | 676ms | 791ms | 791ms | -| Edge Kind Traversal / HasSession | local/phantom | 682ms | 778ms | 778ms | -| Shortest Paths / 41 -> 587 | local/phantom | 708ms | 731ms | 731ms | +| Query | Dataset | Rows | Median | P95 | Max | +|-------|---------|-----:|-------:|----:|----:| +| Match Nodes | local/phantom | 1000 | 2.0ms | 6.5ms | 6.5ms | +| Match Edges | local/phantom | 2000 | 464ms | 604ms | 604ms | diff --git a/cmd/benchmark/main.go b/cmd/benchmark/main.go index 34c7a8b6..d302347b 100644 --- a/cmd/benchmark/main.go +++ b/cmd/benchmark/main.go @@ -147,8 +147,9 @@ func main() { } report.Results = append(report.Results, result) - fmt.Fprintf(os.Stderr, " %s/%s: median=%s p95=%s max=%s\n", + fmt.Fprintf(os.Stderr, " %s/%s: rows=%d median=%s p95=%s max=%s\n", s.Section, s.Label, + result.RowCount, fmtDuration(result.Stats.Median), fmtDuration(result.Stats.P95), fmtDuration(result.Stats.Max), diff --git a/cmd/benchmark/report.go b/cmd/benchmark/report.go index dacab7dd..6ef23a55 100644 --- a/cmd/benchmark/report.go +++ b/cmd/benchmark/report.go @@ -40,8 +40,8 @@ func writeJSON(w io.Writer, r Report) error { func writeMarkdown(w io.Writer, r Report) error { fmt.Fprintf(w, "# Benchmarks — %s @ %s (%s, %d iterations)\n\n", r.Driver, r.GitRef, r.Date, r.Iterations) - fmt.Fprintf(w, "| Query | Dataset | Median | P95 | Max |\n") - fmt.Fprintf(w, "|-------|---------|-------:|----:|----:|\n") + fmt.Fprintf(w, "| Query | Dataset | Rows | Median | P95 | Max |\n") + fmt.Fprintf(w, "|-------|---------|-----:|-------:|----:|----:|\n") for _, res := range r.Results { label := res.Section @@ -49,9 +49,10 @@ func writeMarkdown(w io.Writer, r Report) error { label = res.Section + " / " + res.Label } - fmt.Fprintf(w, "| %s | %s | %s | %s | %s |\n", + fmt.Fprintf(w, "| %s | %s | %d | %s | %s | %s |\n", label, res.Dataset, + res.RowCount, fmtDuration(res.Stats.Median), fmtDuration(res.Stats.P95), fmtDuration(res.Stats.Max), diff --git a/cmd/benchmark/report_test.go b/cmd/benchmark/report_test.go index 2d72ed4d..a5d5bf0f 100644 --- a/cmd/benchmark/report_test.go +++ b/cmd/benchmark/report_test.go @@ -30,9 +30,10 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { Date: "2026-05-14", Iterations: 3, Results: []Result{{ - Section: "Traversal", - Dataset: "base", - Label: "depth 1", + Section: "Traversal", + Dataset: "base", + Label: "depth 1", + RowCount: 2, Stats: Stats{ Median: 10 * time.Millisecond, P95: 20 * time.Millisecond, @@ -51,6 +52,7 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { `"driver": "pg"`, `"git_ref": "abc123"`, `"median": 10000000`, + `"row_count": 2`, `"section": "Traversal"`, } { if !strings.Contains(text, expected) { diff --git a/cmd/benchmark/runner.go b/cmd/benchmark/runner.go index b146f11d..fd976a97 100644 --- a/cmd/benchmark/runner.go +++ b/cmd/benchmark/runner.go @@ -33,16 +33,22 @@ type Stats struct { // Result is one row in the report. type Result struct { - Section string `json:"section"` - Dataset string `json:"dataset"` - Label string `json:"label"` - Stats Stats `json:"stats"` + Section string `json:"section"` + Dataset string `json:"dataset"` + Label string `json:"label"` + RowCount int64 `json:"row_count"` + Stats Stats `json:"stats"` } // runScenario executes a scenario N times and returns timing stats. func runScenario(ctx context.Context, db graph.Database, s Scenario, iterations int) (Result, error) { // Warm-up: one untimed run. - if err := db.ReadTransaction(ctx, s.Query); err != nil { + var rowCount int64 + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + count, err := s.Query(tx) + rowCount = count + return err + }); err != nil { return Result{}, err } @@ -50,17 +56,21 @@ func runScenario(ctx context.Context, db graph.Database, s Scenario, iterations for i := range iterations { start := time.Now() - if err := db.ReadTransaction(ctx, s.Query); err != nil { + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + _, err := s.Query(tx) + return err + }); err != nil { return Result{}, err } durations[i] = time.Since(start) } return Result{ - Section: s.Section, - Dataset: s.Dataset, - Label: s.Label, - Stats: computeStats(durations), + Section: s.Section, + Dataset: s.Dataset, + Label: s.Label, + RowCount: rowCount, + Stats: computeStats(durations), }, nil } diff --git a/cmd/benchmark/scenarios.go b/cmd/benchmark/scenarios.go index 217ae63d..18e6d2d0 100644 --- a/cmd/benchmark/scenarios.go +++ b/cmd/benchmark/scenarios.go @@ -28,17 +28,19 @@ type Scenario struct { Section string // grouping key in the report (e.g. "Match Nodes") Dataset string Label string // human-readable row label - Query func(tx graph.Transaction) error + Query func(tx graph.Transaction) (int64, error) } // defaultDatasets is the set of datasets committed to the repo. -var defaultDatasets = []string{"base"} +var defaultDatasets = []string{"base", "adcs_fanout"} // scenariosForDataset returns all benchmark scenarios for a given dataset and its loaded ID map. func scenariosForDataset(dataset string, idMap opengraph.IDMap) []Scenario { switch dataset { case "base": return baseScenarios(idMap) + case "adcs_fanout": + return adcsFanoutScenarios() case "local/phantom": return phantomScenarios(idMap) default: @@ -46,23 +48,25 @@ func scenariosForDataset(dataset string, idMap opengraph.IDMap) []Scenario { } } -func countNodes(tx graph.Transaction) error { - _, err := tx.Nodes().Count() - return err +func countNodes(tx graph.Transaction) (int64, error) { + return tx.Nodes().Count() } -func countEdges(tx graph.Transaction) error { - _, err := tx.Relationships().Count() - return err +func countEdges(tx graph.Transaction) (int64, error) { + return tx.Relationships().Count() } -func cypherQuery(cypher string) func(tx graph.Transaction) error { - return func(tx graph.Transaction) error { +func cypherQuery(cypher string) func(tx graph.Transaction) (int64, error) { + return func(tx graph.Transaction) (int64, error) { result := tx.Query(cypher, nil) defer result.Close() + + var rowCount int64 for result.Next() { + rowCount++ } - return result.Error() + + return rowCount, result.Error() } } @@ -90,6 +94,44 @@ func baseScenarios(idMap opengraph.IDMap) []Scenario { } } +const adcsFanoutObjectID = "S-1-5-21-2643190041-1319121918-239771340-513" + +func adcsFanoutScenarios() []Scenario { + ds := "adcs_fanout" + + p1 := fmt.Sprintf(` +MATCH (n:Group) WHERE n.objectid = '%s' +MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) +RETURN p1 +`, adcsFanoutObjectID) + + p2 := fmt.Sprintf(` +MATCH (n:Group) WHERE n.objectid = '%s' +MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca:EnterpriseCA)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d:Domain) +WHERE ct.authenticationenabled = true +AND ct.requiresmanagerapproval = false +AND ct.enrolleesuppliessubject = true +AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) +RETURN p2 +`, adcsFanoutObjectID) + + combinedMatch := fmt.Sprintf(` +MATCH (n:Group) WHERE n.objectid = '%s' +MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) +MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) +WHERE ct.authenticationenabled = true +AND ct.requiresmanagerapproval = false +AND ct.enrolleesuppliessubject = true +AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) +`, adcsFanoutObjectID) + + return []Scenario{ + {Section: "ADCS Fanout", Dataset: ds, Label: "p1 only", Query: cypherQuery(p1)}, + {Section: "ADCS Fanout", Dataset: ds, Label: "p2 only", Query: cypherQuery(p2)}, + {Section: "ADCS Fanout", Dataset: ds, Label: "combined", Query: cypherQuery(combinedMatch + "RETURN p1,p2")}, + } +} + // --- Phantom scenarios (hardcoded node IDs from the dataset) --- func phantomScenarios(idMap opengraph.IDMap) []Scenario { diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 84a3e4f2..41fd459c 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2286,72 +2286,166 @@ func expansionTerminalSatisfactionLocality(traversalStep *TraversalStep) (pgsql. ) } -func applyExpansionSuffixPushdown(part *PatternPart) error { +func applyExpansionSuffixPushdown(part *PatternPart) (int, error) { + var applied int + for idx := 0; idx+1 < len(part.TraversalSteps); idx++ { var ( currentStep = part.TraversalSteps[idx] - nextStep = part.TraversalSteps[idx+1] + suffixSteps = part.TraversalSteps[idx+1:] ) - if suffixSatisfaction, satisfied := expansionSuffixTerminalSatisfaction(currentStep, nextStep); satisfied { + if suffixSatisfaction, satisfied := expansionSuffixTerminalSatisfaction(currentStep, suffixSteps); satisfied { currentStep.Expansion.TerminalNodeConstraints = pgsql.OptionalAnd( currentStep.Expansion.TerminalNodeConstraints, suffixSatisfaction, ) if terminalCriteriaProjection, err := pgsql.As[pgsql.SelectItem](currentStep.Expansion.TerminalNodeConstraints); err != nil { - return err + return applied, err } else { currentStep.Expansion.TerminalNodeSatisfactionProjection = terminalCriteriaProjection } + + applied++ } } - return nil + return applied, nil } -func expansionSuffixTerminalSatisfaction(currentStep, nextStep *TraversalStep) (pgsql.Expression, bool) { - if currentStep == nil || - currentStep.Expansion == nil || - currentStep.RightNode == nil || - nextStep == nil || - nextStep.Expansion != nil || - nextStep.LeftNode == nil || - nextStep.Edge == nil || - nextStep.RightNode == nil || - nextStep.RightNodeBound || - nextStep.Direction == graph.DirectionBoth || - currentStep.RightNode.Identifier != nextStep.LeftNode.Identifier { +func suffixEdgeLeftEndpoint(edgeIdentifier pgsql.Identifier, direction graph.Direction) (pgsql.Expression, bool) { + switch direction { + case graph.DirectionOutbound: + return pgsql.CompoundIdentifier{edgeIdentifier, pgsql.ColumnStartID}, true + case graph.DirectionInbound: + return pgsql.CompoundIdentifier{edgeIdentifier, pgsql.ColumnEndID}, true + default: return nil, false } +} - var edgeConstraints pgsql.Expression - if nextStep.EdgeConstraints != nil { - edgeConstraints = nextStep.EdgeConstraints.Expression +func suffixEdgeRightEndpoint(edgeIdentifier pgsql.Identifier, direction graph.Direction) (pgsql.Expression, bool) { + switch direction { + case graph.DirectionOutbound: + return pgsql.CompoundIdentifier{edgeIdentifier, pgsql.ColumnEndID}, true + case graph.DirectionInbound: + return pgsql.CompoundIdentifier{edgeIdentifier, pgsql.ColumnStartID}, true + default: + return nil, false } +} - localScope := pgsql.AsIdentifierSet( - currentStep.RightNode.Identifier, - nextStep.Edge.Identifier, - nextStep.RightNode.Identifier, - ) - - edgeLocal, edgeExternal := partitionConstraintByLocality(edgeConstraints, localScope) - if edgeExternal != nil { +func suffixBoundNodeIDReference(currentStep *TraversalStep, node *BoundIdentifier) (pgsql.Expression, bool) { + if currentStep == nil || + currentStep.Frame == nil || + currentStep.Frame.Previous == nil || + currentStep.Frame.Previous.Binding == nil || + node == nil || + !currentStep.Frame.Previous.Known().Contains(node.Identifier) { return nil, false } - rightNodeLocal, rightNodeExternal := partitionConstraintByLocality(nextStep.RightNodeConstraints, localScope) - if rightNodeExternal != nil { + return pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{currentStep.Frame.Previous.Binding.Identifier, node.Identifier}, + Column: pgsql.ColumnID, + }, true +} + +func suffixStepEdgeConstraints(step *TraversalStep) pgsql.Expression { + if step == nil || step.EdgeConstraints == nil { + return nil + } + + return step.EdgeConstraints.Expression +} + +func expansionSuffixTerminalSatisfaction(currentStep *TraversalStep, suffixSteps []*TraversalStep) (pgsql.Expression, bool) { + if currentStep == nil || + currentStep.Expansion == nil || + currentStep.RightNode == nil || + len(suffixSteps) == 0 || + suffixSteps[0] == nil || + suffixSteps[0].LeftNode == nil || + currentStep.RightNode.Identifier != suffixSteps[0].LeftNode.Identifier { return nil, false } - terminalJoin, err := leftNodeConstraint( - nextStep.Edge.Identifier, - currentStep.RightNode.Identifier, - nextStep.Direction, + var ( + fromClause pgsql.FromClause + where pgsql.Expression + previousID pgsql.Expression = pgsql.CompoundIdentifier{currentStep.RightNode.Identifier, pgsql.ColumnID} ) - if err != nil { + + for idx, step := range suffixSteps { + if step == nil || + step.Expansion != nil || + step.LeftNode == nil || + step.Edge == nil || + step.RightNode == nil || + step.Direction == graph.DirectionBoth { + break + } + + if idx > 0 && suffixSteps[idx-1].RightNode.Identifier != step.LeftNode.Identifier { + break + } + + leftEndpoint, validDirection := suffixEdgeLeftEndpoint(step.Edge.Identifier, step.Direction) + if !validDirection { + return nil, false + } + + edgeJoin := pgd.Equals(previousID, leftEndpoint) + if idx == 0 { + fromClause = expansionEdgeFromClause(step.Edge.Identifier) + where = pgsql.OptionalAnd(where, edgeJoin) + } else { + fromClause.Joins = append(fromClause.Joins, pgsql.Join{ + Table: expansionEdgeTableReference(step.Edge.Identifier), + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: edgeJoin, + }, + }) + } + + where = pgsql.OptionalAnd(where, suffixStepEdgeConstraints(step)) + + rightEndpoint, validDirection := suffixEdgeRightEndpoint(step.Edge.Identifier, step.Direction) + if !validDirection { + return nil, false + } + + if step.RightNodeBound { + if step.RightNodeConstraints != nil { + return nil, false + } + + boundRightNodeID, hasBoundRightNodeID := suffixBoundNodeIDReference(currentStep, step.RightNode) + if !hasBoundRightNodeID { + return nil, false + } + + where = pgsql.OptionalAnd(where, pgd.Equals(rightEndpoint, boundRightNodeID)) + previousID = boundRightNodeID + } else { + fromClause.Joins = append(fromClause.Joins, pgsql.Join{ + Table: expansionNodeTableReference(step.RightNode.Identifier), + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.OptionalAnd( + step.RightNodeConstraints, + pgd.Equals(pgsql.CompoundIdentifier{step.RightNode.Identifier, pgsql.ColumnID}, rightEndpoint), + ), + }, + }) + + previousID = pgsql.CompoundIdentifier{step.RightNode.Identifier, pgsql.ColumnID} + } + } + + if fromClause.Source == nil { return nil, false } @@ -2360,23 +2454,8 @@ func expansionSuffixTerminalSatisfaction(currentStep, nextStep *TraversalStep) ( Query: pgsql.Query{ Body: pgsql.Select{ Projection: pgsql.Projection{pgd.IntLiteral(1)}, - From: []pgsql.FromClause{{ - Source: expansionEdgeTableReference(nextStep.Edge.Identifier), - Joins: []pgsql.Join{{ - Table: expansionNodeTableReference(nextStep.RightNode.Identifier), - JoinOperator: pgsql.JoinOperator{ - JoinType: pgsql.JoinTypeInner, - Constraint: pgsql.OptionalAnd( - rightNodeLocal, - nextStep.RightNodeJoinCondition, - ), - }, - }}, - }}, - Where: pgsql.OptionalAnd( - terminalJoin, - edgeLocal, - ), + From: []pgsql.FromClause{fromClause}, + Where: where, }, }, }, diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 98a8bff9..5229893a 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -50,10 +50,10 @@ func optimizerSafetyKindMapper() *pgutil.InMemoryKindMapper { return mapper } -func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { - t.Parallel() +func optimizerSafetySQL(t *testing.T, cypherQuery string) string { + t.Helper() - regularQuery, err := frontend.ParseCypher(frontend.NewContext(), optimizerADCSQuery) + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) require.NoError(t, err) translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) @@ -62,7 +62,25 @@ func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { formattedQuery, err := Translated(translation) require.NoError(t, err) - normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + return strings.Join(strings.Fields(formattedQuery), " ") +} + +func requireOptimizationLowering(t *testing.T, summary OptimizationSummary, name string) { + t.Helper() + + for _, lowering := range summary.Lowerings { + if lowering.Name == name { + return + } + } + + require.Failf(t, "missing optimization lowering", "expected lowering %q in %#v", name, summary.Lowerings) +} + +func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { + t.Parallel() + + normalizedQuery := optimizerSafetySQL(t, optimizerADCSQuery) require.Contains(t, normalizedQuery, "select distinct (s5.n0).id as root_id from s5") require.Contains(t, normalizedQuery, "s5.ep0 as ep0") @@ -76,16 +94,7 @@ func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { func assertOptimizerSafetyRelationshipStaysComposite(t *testing.T, cypherQuery string) { t.Helper() - regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) - require.NoError(t, err) - - translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) - require.NoError(t, err) - - formattedQuery, err := Translated(translation) - require.NoError(t, err) - - normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + normalizedQuery := optimizerSafetySQL(t, cypherQuery) require.Contains(t, normalizedQuery, "(e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0") require.Contains(t, normalizedQuery, "::edgecomposite") @@ -145,20 +154,11 @@ RETURN p, startNode(r) func TestOptimizerSafetyOptionalMatchPathStaysComposite(t *testing.T) { t.Parallel() - regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + normalizedQuery := optimizerSafetySQL(t, ` MATCH (n:Group) OPTIONAL MATCH p = (n)-[:MemberOf]->(m:Group) RETURN n, p `) - require.NoError(t, err) - - translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) - require.NoError(t, err) - - formattedQuery, err := Translated(translation) - require.NoError(t, err) - - normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") require.Contains(t, normalizedQuery, "::edgecomposite[]") require.NotContains(t, normalizedQuery, "::int8[]") @@ -167,21 +167,12 @@ RETURN n, p func TestOptimizerSafetyFixedHopExpandIntoUsesBoundEndpoints(t *testing.T) { t.Parallel() - regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + normalizedQuery := optimizerSafetySQL(t, ` MATCH (a:Group) MATCH (b:Group) MATCH p = (a)-[:MemberOf]->(b) RETURN p `) - require.NoError(t, err) - - translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) - require.NoError(t, err) - - formattedQuery, err := Translated(translation) - require.NoError(t, err) - - normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") require.Contains(t, normalizedQuery, "(s1.n0).id = e0.start_id") require.Contains(t, normalizedQuery, "(s1.n1).id = e0.end_id") @@ -191,21 +182,12 @@ RETURN p func TestOptimizerSafetyReordersIndependentNodeAnchor(t *testing.T) { t.Parallel() - regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + normalizedQuery := optimizerSafetySQL(t, ` MATCH (a) MATCH (b:EnterpriseCA {name: 'target'}) MATCH p = (a)-[:MemberOf]->(b) RETURN p `) - require.NoError(t, err) - - translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) - require.NoError(t, err) - - formattedQuery, err := Translated(translation) - require.NoError(t, err) - - normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") enterpriseAnchorIndex := strings.Index(normalizedQuery, "array [5]::int2[]") broadScanIndex := strings.Index(normalizedQuery, "from s0, node n1") @@ -219,8 +201,23 @@ RETURN p func TestOptimizerSafetyExpansionTerminalPushdownForFixedSuffix(t *testing.T) { t.Parallel() + normalizedQuery := optimizerSafetySQL(t, ` +MATCH p = (n:Group)-[:MemberOf*1..]->(m)-[:Enroll]->(ca:EnterpriseCA) +RETURN p +`) + + require.Contains(t, normalizedQuery, "exists (select 1 from edge e1 join node n2") + require.Contains(t, normalizedQuery, "n1.id = e1.start_id") + require.Contains(t, normalizedQuery, "e1.kind_id = any (array [4]::int2[])") + require.Contains(t, normalizedQuery, "n2.kind_ids operator (pg_catalog.@>) array [5]::int2[]") +} + +func TestOptimizerSafetyTranslationReportsOptimizerMetadata(t *testing.T) { + t.Parallel() + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` MATCH p = (n:Group)-[:MemberOf*1..]->(m)-[:Enroll]->(ca:EnterpriseCA) +WHERE ca.name = 'target' RETURN p `) require.NoError(t, err) @@ -228,13 +225,66 @@ RETURN p translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) require.NoError(t, err) - formattedQuery, err := Translated(translation) - require.NoError(t, err) + require.NotEmpty(t, translation.Optimization.Rules) + require.NotEmpty(t, translation.Optimization.PredicateAttachments) + requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") +} + +func TestOptimizerSafetyExpansionTerminalPushdownForZeroDepthExpansion(t *testing.T) { + t.Parallel() - normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + normalizedQuery := optimizerSafetySQL(t, ` +MATCH p = (n:Group)-[:MemberOf*0..]->(m)-[:Enroll]->(ca:EnterpriseCA) +RETURN p +`) require.Contains(t, normalizedQuery, "exists (select 1 from edge e1 join node n2") require.Contains(t, normalizedQuery, "n1.id = e1.start_id") require.Contains(t, normalizedQuery, "e1.kind_id = any (array [4]::int2[])") require.Contains(t, normalizedQuery, "n2.kind_ids operator (pg_catalog.@>) array [5]::int2[]") } + +func TestOptimizerSafetyExpansionTerminalPushdownForBoundEndpointSuffixChain(t *testing.T) { + t.Parallel() + + normalizedQuery := optimizerSafetySQL(t, ` +MATCH (ca:EnterpriseCA {name: 'target'}) +MATCH p = (n:Group)-[:MemberOf*0..]->(m)-[:Enroll]->(ct:CertTemplate)-[:PublishedTo]->(ca) +WHERE ct.authenticationenabled = true +RETURN p +`) + + require.Contains(t, normalizedQuery, "exists (select 1 from edge e1 join node n3") + require.Contains(t, normalizedQuery, "join edge e2 on n3.id = e2.start_id") + require.Contains(t, normalizedQuery, "n2.id = e1.start_id") + require.Contains(t, normalizedQuery, "e1.kind_id = any") + require.Contains(t, normalizedQuery, "properties -> 'authenticationenabled'") + require.Contains(t, normalizedQuery, "n3.kind_ids operator (pg_catalog.@>)") + require.Contains(t, normalizedQuery, "e2.kind_id = any") + require.Contains(t, normalizedQuery, "e2.end_id = (s0.n0).id") +} + +func TestOptimizerSafetyExpansionTerminalPushdownForInboundFixedSuffix(t *testing.T) { + t.Parallel() + + normalizedQuery := optimizerSafetySQL(t, ` +MATCH p = (ca:EnterpriseCA)<-[:PublishedTo*1..]-(ct)<-[:Enroll]-(m:Group) +RETURN p +`) + + require.Contains(t, normalizedQuery, "exists (select 1 from edge e1 join node n2") + require.Contains(t, normalizedQuery, "n1.id = e1.end_id") + require.Contains(t, normalizedQuery, "e1.kind_id = any (array [4]::int2[])") + require.Contains(t, normalizedQuery, "n2.kind_ids operator (pg_catalog.@>)") +} + +func TestOptimizerSafetyExpansionTerminalPushdownSkipsDirectionlessSuffix(t *testing.T) { + t.Parallel() + + normalizedQuery := optimizerSafetySQL(t, ` +MATCH p = (n:Group)-[:MemberOf*1..]->(m)-[:Enroll]-(ca:EnterpriseCA) +RETURN p +`) + + require.NotContains(t, normalizedQuery, "exists (select 1 from edge e1 join node n2") +} diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 84e2863f..6525e002 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -520,8 +520,29 @@ func (s *Translator) Exit(expression cypher.SyntaxNode) { } type Result struct { - Statement pgsql.Statement - Parameters map[string]any + Statement pgsql.Statement + Parameters map[string]any + Optimization OptimizationSummary +} + +type OptimizationSummary struct { + Rules []optimize.RuleResult `json:"rules,omitempty"` + PredicateAttachments []optimize.PredicateAttachment `json:"predicate_attachments,omitempty"` + Lowerings []LoweringDecision `json:"lowerings,omitempty"` +} + +type LoweringDecision struct { + Name string `json:"name"` +} + +func (s *Translator) recordLowering(name string) { + for _, lowering := range s.translation.Optimization.Lowerings { + if lowering.Name == name { + return + } + } + + s.translation.Optimization.Lowerings = append(s.translation.Optimization.Lowerings, LoweringDecision{Name: name}) } func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) (Result, error) { @@ -531,6 +552,8 @@ func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper } translator := NewTranslator(ctx, kindMapper, parameters, graphID) + translator.translation.Optimization.Rules = optimizedPlan.Rules + translator.translation.Optimization.PredicateAttachments = optimizedPlan.PredicateAttachments if err := walk.Cypher(optimizedPlan.Query, translator); err != nil { return Result{}, err diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 6192c47d..8dc0f401 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -601,8 +601,10 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } } - if err := applyExpansionSuffixPushdown(part); err != nil { + if applied, err := applyExpansionSuffixPushdown(part); err != nil { return err + } else if applied > 0 { + s.recordLowering("ExpansionSuffixPushdown") } if isolatedProjection { diff --git a/integration/testdata/adcs_fanout.json b/integration/testdata/adcs_fanout.json new file mode 100644 index 00000000..dafbb835 --- /dev/null +++ b/integration/testdata/adcs_fanout.json @@ -0,0 +1,50 @@ +{ + "graph": { + "nodes": [ + {"id": "n", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-2643190041-1319121918-239771340-513"}}, + {"id": "p1-a", "kinds": ["Group"]}, + {"id": "p1-b", "kinds": ["Group"]}, + {"id": "p1-c", "kinds": ["Group"]}, + {"id": "p2-good", "kinds": ["Group"]}, + {"id": "p2-disabled", "kinds": ["Group"]}, + {"id": "p2-wrong-ca", "kinds": ["Group"]}, + {"id": "ca", "kinds": ["EnterpriseCA"]}, + {"id": "other-ca", "kinds": ["EnterpriseCA"]}, + {"id": "store", "kinds": ["NTAuthStore"]}, + {"id": "domain", "kinds": ["Domain"]}, + {"id": "other-domain", "kinds": ["Domain"]}, + {"id": "template-good", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 1, "authorizedsignatures": 1}}, + {"id": "template-alt", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 2, "authorizedsignatures": 0}}, + {"id": "template-disabled", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": false, "requiresmanagerapproval": true, "enrolleesuppliessubject": false, "schemaversion": 2, "authorizedsignatures": 1}}, + {"id": "template-wrong-ca", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 1, "authorizedsignatures": 1}}, + {"id": "root", "kinds": ["RootCA"]}, + {"id": "other-root", "kinds": ["RootCA"]} + ], + "edges": [ + {"start_id": "n", "end_id": "p1-a", "kind": "MemberOf"}, + {"start_id": "n", "end_id": "p1-b", "kind": "MemberOf"}, + {"start_id": "p1-b", "end_id": "p1-c", "kind": "MemberOf"}, + {"start_id": "n", "end_id": "p2-good", "kind": "MemberOf"}, + {"start_id": "n", "end_id": "p2-disabled", "kind": "MemberOf"}, + {"start_id": "n", "end_id": "p2-wrong-ca", "kind": "MemberOf"}, + {"start_id": "n", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "p1-a", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "p1-b", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "p1-c", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "ca", "end_id": "store", "kind": "TrustedForNTAuth"}, + {"start_id": "store", "end_id": "domain", "kind": "NTAuthStoreFor"}, + {"start_id": "p2-good", "end_id": "template-good", "kind": "GenericAll"}, + {"start_id": "p2-good", "end_id": "template-alt", "kind": "Enroll"}, + {"start_id": "p2-disabled", "end_id": "template-disabled", "kind": "AllExtendedRights"}, + {"start_id": "p2-wrong-ca", "end_id": "template-wrong-ca", "kind": "GenericAll"}, + {"start_id": "template-good", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "template-alt", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "template-disabled", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "template-wrong-ca", "end_id": "other-ca", "kind": "PublishedTo"}, + {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "ca", "end_id": "other-root", "kind": "EnterpriseCAFor"}, + {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"}, + {"start_id": "other-root", "end_id": "other-domain", "kind": "RootCAFor"} + ] + } +} diff --git a/integration/testdata/cases/expansion_inline.json b/integration/testdata/cases/expansion_inline.json index 929f32a4..2e5ede51 100644 --- a/integration/testdata/cases/expansion_inline.json +++ b/integration/testdata/cases/expansion_inline.json @@ -66,6 +66,64 @@ }, "assert": {"path_lengths": [2], "path_node_ids": [["src", "good-mid", "dst"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2"]]} }, + { + "name": "bind zero hop expansion path through only terminals that satisfy a fixed suffix", + "cypher": "match p = (src:NodeKind1)-[:EdgeKind1*0..]->(mid)-[:EdgeKind2]->(dst:NodeKind2) where src.name = 'terminal-pushdown-zero-src' return p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "terminal-pushdown-zero-src"}}, + {"id": "dead-mid", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"]} + ], + "edges": [ + {"start_id": "src", "end_id": "dead-mid", "kind": "EdgeKind1"}, + {"start_id": "src", "end_id": "dst", "kind": "EdgeKind2"} + ] + }, + "assert": {"path_lengths": [1], "path_node_ids": [["src", "dst"]], "path_edge_kinds": [["EdgeKind2"]]} + }, + { + "name": "bind expansion path through a fixed suffix chain to a bound endpoint", + "cypher": "match (dst:NodeKind2 {name: 'terminal-pushdown-bound-dst'}) match p = (src:NodeKind1)-[:EdgeKind1*0..]->(mid)-[:EdgeKind2]->(bridge:NodeKind1)-[:EdgeKind1]->(dst) where src.name = 'terminal-pushdown-bound-src' return p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "terminal-pushdown-bound-src"}}, + {"id": "good-mid", "kinds": ["NodeKind1"]}, + {"id": "bad-mid", "kinds": ["NodeKind1"]}, + {"id": "bridge", "kinds": ["NodeKind1"]}, + {"id": "bad-bridge", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"], "properties": {"name": "terminal-pushdown-bound-dst"}}, + {"id": "other-dst", "kinds": ["NodeKind2"], "properties": {"name": "terminal-pushdown-other-dst"}} + ], + "edges": [ + {"start_id": "src", "end_id": "good-mid", "kind": "EdgeKind1"}, + {"start_id": "src", "end_id": "bad-mid", "kind": "EdgeKind1"}, + {"start_id": "good-mid", "end_id": "bridge", "kind": "EdgeKind2"}, + {"start_id": "bad-mid", "end_id": "bad-bridge", "kind": "EdgeKind2"}, + {"start_id": "bridge", "end_id": "dst", "kind": "EdgeKind1"}, + {"start_id": "bad-bridge", "end_id": "other-dst", "kind": "EdgeKind1"} + ] + }, + "assert": {"row_count": 1, "path_lengths": [3], "path_node_ids": [["src", "good-mid", "bridge", "dst"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2", "EdgeKind1"]]} + }, + { + "name": "relationships of optimized expansion path preserve suffix order", + "cypher": "match p = (src:NodeKind1)-[:EdgeKind1*1..]->(mid)-[:EdgeKind2]->(dst:NodeKind2) where src.name = 'terminal-pushdown-src' return relationships(p)", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "terminal-pushdown-src"}}, + {"id": "good-mid", "kinds": ["NodeKind1"]}, + {"id": "dead-mid", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"]} + ], + "edges": [ + {"start_id": "src", "end_id": "good-mid", "kind": "EdgeKind1"}, + {"start_id": "src", "end_id": "dead-mid", "kind": "EdgeKind1"}, + {"start_id": "good-mid", "end_id": "dst", "kind": "EdgeKind2"} + ] + }, + "assert": {"row_count": 1, "relationship_list_kinds": [["EdgeKind1", "EdgeKind2"]]} + }, { "name": "fixed step followed by a bounded variable-length expansion", "cypher": "match (n)-[]->(e:NodeKind1)-[*2..3]->(l) where n.name = 'start' return l", From de68acccc4177a8d5c1c38f12feb7631afabf870 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 19:13:02 -0700 Subject: [PATCH 023/114] test(pgsql): complete optimizer gap closure --- cmd/benchmark/README.md | 22 +- cmd/benchmark/explain.go | 68 ++++++ cmd/benchmark/main.go | 21 +- cmd/benchmark/report.go | 25 ++- cmd/benchmark/report_test.go | 62 +++++- cmd/benchmark/runner.go | 61 +++-- cmd/benchmark/scenarios.go | 209 ++++++++++++++---- .../pgsql/translate/optimizer_safety_test.go | 16 ++ docs/optimization-pass-memory.md | 12 + .../testdata/cases/optimizer_inline.json | 10 + 10 files changed, 426 insertions(+), 80 deletions(-) create mode 100644 cmd/benchmark/explain.go diff --git a/cmd/benchmark/README.md b/cmd/benchmark/README.md index 13686821..db41c4a7 100644 --- a/cmd/benchmark/README.md +++ b/cmd/benchmark/README.md @@ -1,6 +1,6 @@ # Benchmark -Runs query scenarios against a real database and outputs a markdown timing table with warm-up row counts. +Runs query scenarios against a real database and outputs a markdown timing table with warm-up row counts. Path-heavy scenarios can also report distinct returned path rows and duplicate returned path rows. ## Usage @@ -22,6 +22,9 @@ go run ./cmd/benchmark -connection "..." -output report.md # Save markdown and JSON for quality baseline comparison go run ./cmd/benchmark -connection "..." -output report.md -json-output report.json + +# Capture PostgreSQL EXPLAIN (ANALYZE, BUFFERS) in the JSON report for Cypher scenarios +go run ./cmd/benchmark -connection "..." -dataset adcs_fanout -json-output report.json -explain ``` ## Flags @@ -31,6 +34,7 @@ go run ./cmd/benchmark -connection "..." -output report.md -json-output report.j | `-driver` | `pg` | Database driver (`pg`, `neo4j`) | | `-connection` | | Connection string (or `CONNECTION_STRING` env) | | `-iterations` | `10` | Timed iterations per scenario | +| `-explain` | `false` | Capture PostgreSQL `EXPLAIN (ANALYZE, BUFFERS)` and translated SQL for Cypher scenarios in JSON output | | `-dataset` | | Run only this dataset | | `-local-dataset` | | Add a local dataset to the default set | | `-dataset-dir` | `integration/testdata` | Path to testdata directory | @@ -43,10 +47,10 @@ go run ./cmd/benchmark -connection "..." -output report.md -json-output report.j $ go run ./cmd/benchmark -driver neo4j -connection "neo4j://neo4j:testpassword@localhost:7687" -dataset local/phantom ``` -| Query | Dataset | Rows | Median | P95 | Max | -|-------|---------|-----:|-------:|----:|----:| -| Match Nodes | local/phantom | 1000 | 1.4ms | 2.3ms | 2.3ms | -| Match Edges | local/phantom | 2000 | 1.6ms | 1.9ms | 1.9ms | +| Query | Dataset | Rows | Distinct Rows | Duplicate Rows | Median | P95 | Max | Explain | +|-------|---------|-----:|--------------:|---------------:|-------:|----:|----:|:--------| +| Match Nodes | local/phantom | 1000 | - | - | 1.4ms | 2.3ms | 2.3ms | - | +| Match Edges | local/phantom | 2000 | - | - | 1.6ms | 1.9ms | 1.9ms | - | ## Example: PG on local/phantom @@ -55,7 +59,7 @@ $ export CONNECTION_STRING="postgresql://dawgs:dawgs@localhost:5432/dawgs" $ go run ./cmd/benchmark -dataset local/phantom ``` -| Query | Dataset | Rows | Median | P95 | Max | -|-------|---------|-----:|-------:|----:|----:| -| Match Nodes | local/phantom | 1000 | 2.0ms | 6.5ms | 6.5ms | -| Match Edges | local/phantom | 2000 | 464ms | 604ms | 604ms | +| Query | Dataset | Rows | Distinct Rows | Duplicate Rows | Median | P95 | Max | Explain | +|-------|---------|-----:|--------------:|---------------:|-------:|----:|----:|:--------| +| Match Nodes | local/phantom | 1000 | - | - | 2.0ms | 6.5ms | 6.5ms | - | +| Match Edges | local/phantom | 2000 | - | - | 464ms | 604ms | 604ms | - | diff --git a/cmd/benchmark/explain.go b/cmd/benchmark/explain.go new file mode 100644 index 00000000..ff06472b --- /dev/null +++ b/cmd/benchmark/explain.go @@ -0,0 +1,68 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/translate" + "github.com/specterops/dawgs/graph" +) + +func newPostgresExplainer(kindMapper pgsql.KindMapper, graphID int32) ExplainFunc { + return func(ctx context.Context, tx graph.Transaction, cypherQuery string) (*ExplainResult, error) { + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) + if err != nil { + return nil, err + } + + translation, err := translate.Translate(ctx, regularQuery, kindMapper, nil, graphID) + if err != nil { + return nil, err + } + + sqlQuery, err := translate.Translated(translation) + if err != nil { + return nil, err + } + + result := tx.Raw("EXPLAIN (ANALYZE, BUFFERS) "+sqlQuery, translation.Parameters) + defer result.Close() + + var plan []string + for result.Next() { + values := result.Values() + if len(values) == 0 { + continue + } + + plan = append(plan, fmt.Sprint(values[0])) + } + + if err := result.Error(); err != nil { + return nil, err + } + + return &ExplainResult{ + SQL: sqlQuery, + Plan: plan, + }, nil + } +} diff --git a/cmd/benchmark/main.go b/cmd/benchmark/main.go index d302347b..606b102d 100644 --- a/cmd/benchmark/main.go +++ b/cmd/benchmark/main.go @@ -43,6 +43,7 @@ func main() { iterations = flag.Int("iterations", 10, "timed iterations per scenario") output = flag.String("output", "", "markdown output file (default: stdout)") jsonOutput = flag.String("json-output", "", "JSON output file for baseline comparison") + explain = flag.Bool("explain", false, "capture PostgreSQL EXPLAIN (ANALYZE, BUFFERS) for Cypher scenarios") datasetDir = flag.String("dataset-dir", "integration/testdata", "path to testdata directory") localDataset = flag.String("local-dataset", "", "additional local dataset (e.g. local/phantom)") onlyDataset = flag.String("dataset", "", "run only this dataset (e.g. diamond, local/phantom)") @@ -110,6 +111,19 @@ func main() { fatal("failed to assert schema: %v", err) } + var runOptions RunOptions + if *explain { + if *driver != pg.DriverName { + fmt.Fprintf(os.Stderr, " explain capture is only supported for pg; continuing without plans\n") + } else if pgDB, ok := db.(*pg.Driver); !ok { + fmt.Fprintf(os.Stderr, " explain capture unavailable for %T; continuing without plans\n", db) + } else if defaultGraph, hasDefaultGraph := pgDB.DefaultGraph(); !hasDefaultGraph { + fatal("failed to resolve default graph for explain capture") + } else { + runOptions.Explain = newPostgresExplainer(pgDB.KindMapper(), defaultGraph.ID) + } + } + report := Report{ Driver: *driver, GitRef: gitRef(), @@ -140,19 +154,22 @@ func main() { // Run scenarios for _, s := range scenariosForDataset(ds, idMap) { - result, err := runScenario(ctx, db, s, *iterations) + result, err := runScenario(ctx, db, s, *iterations, runOptions) if err != nil { fmt.Fprintf(os.Stderr, " %s/%s failed: %v\n", s.Section, s.Label, err) continue } report.Results = append(report.Results, result) - fmt.Fprintf(os.Stderr, " %s/%s: rows=%d median=%s p95=%s max=%s\n", + fmt.Fprintf(os.Stderr, " %s/%s: rows=%d distinct=%s duplicates=%s median=%s p95=%s max=%s explain=%s\n", s.Section, s.Label, result.RowCount, + fmtOptionalInt64(result.DistinctRowCount), + fmtOptionalInt64(result.DuplicateRowCount), fmtDuration(result.Stats.Median), fmtDuration(result.Stats.P95), fmtDuration(result.Stats.Max), + fmtExplainStatus(result.Explain), ) } } diff --git a/cmd/benchmark/report.go b/cmd/benchmark/report.go index 6ef23a55..5d08bfd8 100644 --- a/cmd/benchmark/report.go +++ b/cmd/benchmark/report.go @@ -40,8 +40,8 @@ func writeJSON(w io.Writer, r Report) error { func writeMarkdown(w io.Writer, r Report) error { fmt.Fprintf(w, "# Benchmarks — %s @ %s (%s, %d iterations)\n\n", r.Driver, r.GitRef, r.Date, r.Iterations) - fmt.Fprintf(w, "| Query | Dataset | Rows | Median | P95 | Max |\n") - fmt.Fprintf(w, "|-------|---------|-----:|-------:|----:|----:|\n") + fmt.Fprintf(w, "| Query | Dataset | Rows | Distinct Rows | Duplicate Rows | Median | P95 | Max | Explain |\n") + fmt.Fprintf(w, "|-------|---------|-----:|--------------:|---------------:|-------:|----:|----:|:--------|\n") for _, res := range r.Results { label := res.Section @@ -49,13 +49,16 @@ func writeMarkdown(w io.Writer, r Report) error { label = res.Section + " / " + res.Label } - fmt.Fprintf(w, "| %s | %s | %d | %s | %s | %s |\n", + fmt.Fprintf(w, "| %s | %s | %d | %s | %s | %s | %s | %s | %s |\n", label, res.Dataset, res.RowCount, + fmtOptionalInt64(res.DistinctRowCount), + fmtOptionalInt64(res.DuplicateRowCount), fmtDuration(res.Stats.Median), fmtDuration(res.Stats.P95), fmtDuration(res.Stats.Max), + fmtExplainStatus(res.Explain), ) } @@ -63,6 +66,22 @@ func writeMarkdown(w io.Writer, r Report) error { return nil } +func fmtOptionalInt64(value *int64) string { + if value == nil { + return "-" + } + + return fmt.Sprintf("%d", *value) +} + +func fmtExplainStatus(explain *ExplainResult) string { + if explain == nil { + return "-" + } + + return "captured" +} + func fmtDuration(d time.Duration) string { ms := float64(d.Microseconds()) / 1000.0 if ms < 1 { diff --git a/cmd/benchmark/report_test.go b/cmd/benchmark/report_test.go index a5d5bf0f..6905798a 100644 --- a/cmd/benchmark/report_test.go +++ b/cmd/benchmark/report_test.go @@ -24,16 +24,25 @@ import ( ) func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { + distinctRows := int64(2) + duplicateRows := int64(0) + report := Report{ Driver: "pg", GitRef: "abc123", Date: "2026-05-14", Iterations: 3, Results: []Result{{ - Section: "Traversal", - Dataset: "base", - Label: "depth 1", - RowCount: 2, + Section: "Traversal", + Dataset: "base", + Label: "depth 1", + RowCount: 2, + DistinctRowCount: &distinctRows, + DuplicateRowCount: &duplicateRows, + Explain: &ExplainResult{ + SQL: "select 1;", + Plan: []string{"Result (actual rows=1 loops=1)"}, + }, Stats: Stats{ Median: 10 * time.Millisecond, P95: 20 * time.Millisecond, @@ -53,6 +62,9 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { `"git_ref": "abc123"`, `"median": 10000000`, `"row_count": 2`, + `"distinct_row_count": 2`, + `"duplicate_row_count": 0`, + `"sql": "select 1;"`, `"section": "Traversal"`, } { if !strings.Contains(text, expected) { @@ -60,3 +72,45 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { } } } + +func TestWriteMarkdownIncludesDiagnosticColumns(t *testing.T) { + distinctRows := int64(2) + duplicateRows := int64(0) + + report := Report{ + Driver: "pg", + GitRef: "abc123", + Date: "2026-05-14", + Iterations: 3, + Results: []Result{{ + Section: "ADCS Fanout", + Dataset: "adcs_fanout", + Label: "combined", + RowCount: 2, + DistinctRowCount: &distinctRows, + DuplicateRowCount: &duplicateRows, + Explain: &ExplainResult{Plan: []string{"Result"}}, + Stats: Stats{ + Median: 10 * time.Millisecond, + P95: 20 * time.Millisecond, + Max: 30 * time.Millisecond, + }, + }}, + } + + var output bytes.Buffer + if err := writeMarkdown(&output, report); err != nil { + t.Fatalf("write markdown: %v", err) + } + + text := output.String() + for _, expected := range []string{ + "Distinct Rows", + "Duplicate Rows", + "| ADCS Fanout / combined | adcs_fanout | 2 | 2 | 0 | 10.0ms | 20.0ms | 30.0ms | captured |", + } { + if !strings.Contains(text, expected) { + t.Fatalf("markdown report missing %q:\n%s", expected, text) + } + } +} diff --git a/cmd/benchmark/runner.go b/cmd/benchmark/runner.go index fd976a97..99646b5b 100644 --- a/cmd/benchmark/runner.go +++ b/cmd/benchmark/runner.go @@ -24,6 +24,18 @@ import ( "github.com/specterops/dawgs/graph" ) +type ExplainFunc func(ctx context.Context, tx graph.Transaction, cypher string) (*ExplainResult, error) + +type RunOptions struct { + Explain ExplainFunc +} + +// ExplainResult captures PostgreSQL-specific plan diagnostics for a scenario. +type ExplainResult struct { + SQL string `json:"sql"` + Plan []string `json:"plan"` +} + // Stats holds computed timing statistics for a scenario. type Stats struct { Median time.Duration `json:"median"` @@ -33,20 +45,23 @@ type Stats struct { // Result is one row in the report. type Result struct { - Section string `json:"section"` - Dataset string `json:"dataset"` - Label string `json:"label"` - RowCount int64 `json:"row_count"` - Stats Stats `json:"stats"` + Section string `json:"section"` + Dataset string `json:"dataset"` + Label string `json:"label"` + RowCount int64 `json:"row_count"` + DistinctRowCount *int64 `json:"distinct_row_count,omitempty"` + DuplicateRowCount *int64 `json:"duplicate_row_count,omitempty"` + Explain *ExplainResult `json:"explain,omitempty"` + Stats Stats `json:"stats"` } // runScenario executes a scenario N times and returns timing stats. -func runScenario(ctx context.Context, db graph.Database, s Scenario, iterations int) (Result, error) { +func runScenario(ctx context.Context, db graph.Database, s Scenario, iterations int, options RunOptions) (Result, error) { // Warm-up: one untimed run. - var rowCount int64 + var measurement Measurement if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { - count, err := s.Query(tx) - rowCount = count + nextMeasurement, err := s.Query(tx) + measurement = nextMeasurement return err }); err != nil { return Result{}, err @@ -65,13 +80,27 @@ func runScenario(ctx context.Context, db graph.Database, s Scenario, iterations durations[i] = time.Since(start) } - return Result{ - Section: s.Section, - Dataset: s.Dataset, - Label: s.Label, - RowCount: rowCount, - Stats: computeStats(durations), - }, nil + result := Result{ + Section: s.Section, + Dataset: s.Dataset, + Label: s.Label, + RowCount: measurement.RowCount, + DistinctRowCount: measurement.DistinctRowCount, + DuplicateRowCount: measurement.DuplicateRowCount, + Stats: computeStats(durations), + } + + if options.Explain != nil && s.Cypher != "" { + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + explain, err := options.Explain(ctx, tx, s.Cypher) + result.Explain = explain + return err + }); err != nil { + return Result{}, err + } + } + + return result, nil } func computeStats(durations []time.Duration) Stats { diff --git a/cmd/benchmark/scenarios.go b/cmd/benchmark/scenarios.go index 18e6d2d0..4f2450b3 100644 --- a/cmd/benchmark/scenarios.go +++ b/cmd/benchmark/scenarios.go @@ -18,17 +18,27 @@ package main import ( "fmt" + "strconv" + "strings" "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/opengraph" ) +// Measurement captures the warm-up result shape for a benchmark scenario. +type Measurement struct { + RowCount int64 + DistinctRowCount *int64 + DuplicateRowCount *int64 +} + // Scenario defines a single benchmark query to run against a loaded dataset. type Scenario struct { Section string // grouping key in the report (e.g. "Match Nodes") Dataset string Label string // human-readable row label - Query func(tx graph.Transaction) (int64, error) + Cypher string + Query func(tx graph.Transaction) (Measurement, error) } // defaultDatasets is the set of datasets committed to the repo. @@ -56,8 +66,8 @@ func countEdges(tx graph.Transaction) (int64, error) { return tx.Relationships().Count() } -func cypherQuery(cypher string) func(tx graph.Transaction) (int64, error) { - return func(tx graph.Transaction) (int64, error) { +func cypherQuery(cypher string) func(tx graph.Transaction) (Measurement, error) { + return func(tx graph.Transaction) (Measurement, error) { result := tx.Query(cypher, nil) defer result.Close() @@ -66,31 +76,143 @@ func cypherQuery(cypher string) func(tx graph.Transaction) (int64, error) { rowCount++ } - return rowCount, result.Error() + return Measurement{RowCount: rowCount}, result.Error() + } +} + +func countQuery(query func(tx graph.Transaction) (int64, error)) func(tx graph.Transaction) (Measurement, error) { + return func(tx graph.Transaction) (Measurement, error) { + rowCount, err := query(tx) + if err != nil { + return Measurement{}, err + } + + return Measurement{RowCount: rowCount}, nil + } +} + +func cypherScenario(section, dataset, label, cypher string) Scenario { + return Scenario{ + Section: section, + Dataset: dataset, + Label: label, + Cypher: cypher, + Query: cypherQuery(cypher), + } +} + +func cypherPathScenario(section, dataset, label, cypher string, pathColumns int) Scenario { + return Scenario{ + Section: section, + Dataset: dataset, + Label: label, + Cypher: cypher, + Query: cypherPathQuery(cypher, pathColumns), + } +} + +func cypherPathQuery(cypher string, pathColumns int) func(tx graph.Transaction) (Measurement, error) { + return func(tx graph.Transaction) (Measurement, error) { + result := tx.Query(cypher, nil) + defer result.Close() + + var ( + rowCount int64 + seen = map[string]struct{}{} + ) + + for result.Next() { + rowCount++ + + values := make([]graph.Path, pathColumns) + targets := make([]any, pathColumns) + for idx := range values { + targets[idx] = &values[idx] + } + + if err := result.Scan(targets...); err != nil { + return Measurement{}, err + } + + seen[pathRowKey(values)] = struct{}{} + } + + if err := result.Error(); err != nil { + return Measurement{}, err + } + + distinctRowCount := int64(len(seen)) + duplicateRowCount := rowCount - distinctRowCount + + return Measurement{ + RowCount: rowCount, + DistinctRowCount: &distinctRowCount, + DuplicateRowCount: &duplicateRowCount, + }, nil } } +func pathRowKey(paths []graph.Path) string { + var builder strings.Builder + + for pathIdx, path := range paths { + if pathIdx > 0 { + builder.WriteByte('|') + } + + builder.WriteByte('n') + for _, node := range path.Nodes { + builder.WriteByte(':') + if node == nil { + builder.WriteString("nil") + continue + } + + builder.WriteString(strconv.FormatUint(node.ID.Uint64(), 10)) + } + + builder.WriteString(";e") + for _, edge := range path.Edges { + builder.WriteByte(':') + if edge == nil { + builder.WriteString("nil") + continue + } + + builder.WriteString(strconv.FormatUint(edge.ID.Uint64(), 10)) + builder.WriteByte(',') + builder.WriteString(strconv.FormatUint(edge.StartID.Uint64(), 10)) + builder.WriteByte(',') + builder.WriteString(strconv.FormatUint(edge.EndID.Uint64(), 10)) + builder.WriteByte(',') + builder.WriteString(edge.Kind.String()) + } + } + + return builder.String() +} + // --- Base dataset scenarios (n1 -> n2 -> n3) --- func baseScenarios(idMap opengraph.IDMap) []Scenario { ds := "base" return []Scenario{ - {Section: "Match Nodes", Dataset: ds, Label: ds, Query: countNodes}, - {Section: "Match Edges", Dataset: ds, Label: ds, Query: countEdges}, - {Section: "Shortest Paths", Dataset: ds, Label: "n1 -> n3", Query: cypherQuery(fmt.Sprintf( + {Section: "Match Nodes", Dataset: ds, Label: ds, Query: countQuery(countNodes)}, + {Section: "Match Edges", Dataset: ds, Label: ds, Query: countQuery(countEdges)}, + cypherScenario("Shortest Paths", ds, "n1 -> n3", fmt.Sprintf( "MATCH p = allShortestPaths((s)-[*1..]->(e)) WHERE id(s) = %d AND id(e) = %d RETURN p", idMap["n1"], idMap["n3"], - ))}, - {Section: "Traversal", Dataset: ds, Label: "n1", Query: cypherQuery(fmt.Sprintf( + )), + cypherScenario("Traversal", ds, "n1", fmt.Sprintf( "MATCH (s)-[*1..]->(e) WHERE id(s) = %d RETURN e", idMap["n1"], - ))}, - {Section: "Match Return", Dataset: ds, Label: "n1", Query: cypherQuery(fmt.Sprintf( + )), + cypherScenario("Match Return", ds, "n1", fmt.Sprintf( "MATCH (s)-[]->(e) WHERE id(s) = %d RETURN e", idMap["n1"], - ))}, - {Section: "Filter By Kind", Dataset: ds, Label: "NodeKind1", Query: cypherQuery("MATCH (n:NodeKind1) RETURN n")}, - {Section: "Filter By Kind", Dataset: ds, Label: "NodeKind2", Query: cypherQuery("MATCH (n:NodeKind2) RETURN n")}, + )), + cypherScenario("Filter By Kind", ds, "NodeKind1", "MATCH (n:NodeKind1) RETURN n"), + cypherScenario("Filter By Kind", ds, "NodeKind2", "MATCH (n:NodeKind2) RETURN n"), } } @@ -126,9 +248,9 @@ AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) `, adcsFanoutObjectID) return []Scenario{ - {Section: "ADCS Fanout", Dataset: ds, Label: "p1 only", Query: cypherQuery(p1)}, - {Section: "ADCS Fanout", Dataset: ds, Label: "p2 only", Query: cypherQuery(p2)}, - {Section: "ADCS Fanout", Dataset: ds, Label: "combined", Query: cypherQuery(combinedMatch + "RETURN p1,p2")}, + cypherPathScenario("ADCS Fanout", ds, "p1 only", p1, 1), + cypherPathScenario("ADCS Fanout", ds, "p2 only", p2, 1), + cypherPathScenario("ADCS Fanout", ds, "combined", combinedMatch+"RETURN p1,p2", 2), } } @@ -138,59 +260,54 @@ func phantomScenarios(idMap opengraph.IDMap) []Scenario { ds := "local/phantom" scenarios := []Scenario{ - {Section: "Match Nodes", Dataset: ds, Label: ds, Query: countNodes}, - {Section: "Match Edges", Dataset: ds, Label: ds, Query: countEdges}, + {Section: "Match Nodes", Dataset: ds, Label: ds, Query: countQuery(countNodes)}, + {Section: "Match Edges", Dataset: ds, Label: ds, Query: countQuery(countEdges)}, } for _, kind := range []string{"User", "Group", "Computer"} { k := kind - scenarios = append(scenarios, Scenario{ - Section: "Filter By Kind", - Dataset: ds, - Label: k, - Query: cypherQuery(fmt.Sprintf("MATCH (n:%s) RETURN n", k)), - }) + scenarios = append(scenarios, cypherScenario("Filter By Kind", ds, k, fmt.Sprintf("MATCH (n:%s) RETURN n", k))) } if _, ok := idMap["41"]; ok { for _, depth := range []int{1, 2, 3} { d := depth - scenarios = append(scenarios, Scenario{ - Section: "Traversal Depth", - Dataset: ds, - Label: fmt.Sprintf("depth %d", d), - Query: cypherQuery(fmt.Sprintf( + scenarios = append(scenarios, cypherScenario( + "Traversal Depth", + ds, + fmt.Sprintf("depth %d", d), + fmt.Sprintf( "MATCH (s)-[*1..%d]->(e) WHERE id(s) = %d RETURN e", d, idMap["41"], - )), - }) + ), + )) } for _, ek := range []string{"MemberOf", "GenericAll", "HasSession"} { edgeKind := ek - scenarios = append(scenarios, Scenario{ - Section: "Edge Kind Traversal", - Dataset: ds, - Label: edgeKind, - Query: cypherQuery(fmt.Sprintf( + scenarios = append(scenarios, cypherScenario( + "Edge Kind Traversal", + ds, + edgeKind, + fmt.Sprintf( "MATCH (s)-[:%s*1..]->(e) WHERE id(s) = %d RETURN e", edgeKind, idMap["41"], - )), - }) + ), + )) } } if _, ok := idMap["41"]; ok { if _, ok := idMap["587"]; ok { - scenarios = append(scenarios, Scenario{ - Section: "Shortest Paths", - Dataset: ds, - Label: "41 -> 587", - Query: cypherQuery(fmt.Sprintf( + scenarios = append(scenarios, cypherScenario( + "Shortest Paths", + ds, + "41 -> 587", + fmt.Sprintf( "MATCH p = allShortestPaths((s)-[*1..]->(e)) WHERE id(s) = %d AND id(e) = %d RETURN p", idMap["41"], idMap["587"], - )), - }) + ), + )) } } diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 5229893a..a9ec9389 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -264,6 +264,22 @@ RETURN p require.Contains(t, normalizedQuery, "e2.end_id = (s0.n0).id") } +func TestOptimizerSafetyExpansionTerminalPushdownForBoundDomainSuffix(t *testing.T) { + t.Parallel() + + normalizedQuery := optimizerSafetySQL(t, ` +MATCH (d:Domain {name: 'target'}) +MATCH p = (ca:EnterpriseCA)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(root:RootCA)-[:RootCAFor]->(d) +RETURN p +`) + + require.Contains(t, normalizedQuery, "exists (select 1 from edge e1") + require.Contains(t, normalizedQuery, "e1.kind_id = any") + require.Contains(t, normalizedQuery, "n2.kind_ids operator (pg_catalog.@>)") + require.Contains(t, normalizedQuery, "n2.id = e1.start_id") + require.Contains(t, normalizedQuery, "e1.end_id = (s0.n0).id") +} + func TestOptimizerSafetyExpansionTerminalPushdownForInboundFixedSuffix(t *testing.T) { t.Parallel() diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index 578f6710..8369426a 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -250,3 +250,15 @@ Before implementing expand-into detection, capture the following for the motivat - Comparison with Neo4j result cardinality for the same fixture. Projection pruning and late path materialization currently live in PostgreSQL translator lowering. If later phases need richer rule-level ordering or barrier enforcement, promote these decisions into explicit optimizer rule metadata instead of adding more hidden translator-side state. + +## Gap Closure Completion Notes + +The gap-closure pass has been completed enough to return to the original phase sequence without broadening into Phase 10. + +- The benchmark harness includes the committed `adcs_fanout` dataset by default and has scenarios for `p1` alone, `p2` alone, and the combined `RETURN p1,p2` form. +- ADCS path scenarios now record warm-up row count, distinct returned path-row count, and duplicate returned path-row count. +- PostgreSQL benchmark runs can opt into `EXPLAIN (ANALYZE, BUFFERS)` capture with `-explain`; JSON output includes the translated SQL and plan text. +- The small ADCS integration fixture now asserts exact returned path shape and row count. The larger fanout fixture remains a measurement fixture rather than an exact cardinality oracle. +- Translation metadata reports optimizer rules, predicate attachments, and named lowerings, including `ExpansionSuffixPushdown`. +- Phase 9 suffix coverage includes zero-hop expansions, fixed suffix chains, suffixes ending at already-bound nodes, inbound suffixes, and the ADCS root-to-domain suffix shape. +- Directionless suffix pushdown remains deliberately unimplemented; those suffixes stay as normal translated pattern steps. diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index d8567426..d9e08b68 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -32,6 +32,16 @@ }, "assert": { "keys": ["p1", "p2"], + "row_count": 1, + "path_lengths": [4, 5], + "path_node_ids": [ + ["n", "p1-mid", "ca", "store", "domain"], + ["n", "p2-mid", "template", "ca", "root", "domain"] + ], + "path_edge_kinds": [ + ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + ["MemberOf", "GenericAll", "PublishedTo", "IssuedSignedBy", "RootCAFor"] + ], "contains_node_with_props": {"objectid": "S-1-5-21-2643190041-1319121918-239771340-513"}, "contains_edge": {"start": "template", "end": "ca", "kind": "PublishedTo"} } From 73642180bde01fb32a69d51c0f147808f2eae2e5 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 20 May 2026 19:21:25 -0700 Subject: [PATCH 024/114] test(pgsql): measure optimizer rules locally --- cmd/benchmark/README.md | 4 ++-- cmd/benchmark/explain.go | 12 ++++++++-- cmd/benchmark/main.go | 4 ++++ cmd/benchmark/report_test.go | 22 +++++++++++++++++++ cmd/benchmark/runner.go | 19 +++++++++++----- cmd/benchmark/scenarios.go | 1 + cypher/models/pgsql/optimize/optimizer.go | 18 +++++++-------- .../pgsql/translate/optimizer_safety_test.go | 18 ++++++++++++++- docs/optimization-pass-memory.md | 10 +++++++++ 9 files changed, 88 insertions(+), 20 deletions(-) diff --git a/cmd/benchmark/README.md b/cmd/benchmark/README.md index db41c4a7..1ac95798 100644 --- a/cmd/benchmark/README.md +++ b/cmd/benchmark/README.md @@ -1,6 +1,6 @@ # Benchmark -Runs query scenarios against a real database and outputs a markdown timing table with warm-up row counts. Path-heavy scenarios can also report distinct returned path rows and duplicate returned path rows. +Runs query scenarios against a real database and outputs a markdown timing table with warm-up row counts. Path-heavy scenarios can also report distinct returned path rows and duplicate returned path rows. PostgreSQL explain capture includes translated SQL, plan text, and optimizer rule/lowering metadata in JSON output. ## Usage @@ -23,7 +23,7 @@ go run ./cmd/benchmark -connection "..." -output report.md # Save markdown and JSON for quality baseline comparison go run ./cmd/benchmark -connection "..." -output report.md -json-output report.json -# Capture PostgreSQL EXPLAIN (ANALYZE, BUFFERS) in the JSON report for Cypher scenarios +# Capture PostgreSQL EXPLAIN (ANALYZE, BUFFERS), translated SQL, and optimizer metadata in JSON output go run ./cmd/benchmark -connection "..." -dataset adcs_fanout -json-output report.json -explain ``` diff --git a/cmd/benchmark/explain.go b/cmd/benchmark/explain.go index ff06472b..ca1334f7 100644 --- a/cmd/benchmark/explain.go +++ b/cmd/benchmark/explain.go @@ -26,6 +26,13 @@ import ( "github.com/specterops/dawgs/graph" ) +// ExplainResult captures PostgreSQL-specific plan diagnostics for a scenario. +type ExplainResult struct { + SQL string `json:"sql"` + Plan []string `json:"plan"` + Optimization translate.OptimizationSummary `json:"optimization"` +} + func newPostgresExplainer(kindMapper pgsql.KindMapper, graphID int32) ExplainFunc { return func(ctx context.Context, tx graph.Transaction, cypherQuery string) (*ExplainResult, error) { regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) @@ -61,8 +68,9 @@ func newPostgresExplainer(kindMapper pgsql.KindMapper, graphID int32) ExplainFun } return &ExplainResult{ - SQL: sqlQuery, - Plan: plan, + SQL: sqlQuery, + Plan: plan, + Optimization: translation.Optimization, }, nil } } diff --git a/cmd/benchmark/main.go b/cmd/benchmark/main.go index 606b102d..0045977a 100644 --- a/cmd/benchmark/main.go +++ b/cmd/benchmark/main.go @@ -51,6 +51,10 @@ func main() { flag.Parse() + if err := validateIterations(*iterations); err != nil { + fatal("%v", err) + } + conn := *connStr if conn == "" { conn = os.Getenv("CONNECTION_STRING") diff --git a/cmd/benchmark/report_test.go b/cmd/benchmark/report_test.go index 6905798a..35820767 100644 --- a/cmd/benchmark/report_test.go +++ b/cmd/benchmark/report_test.go @@ -21,6 +21,9 @@ import ( "strings" "testing" "time" + + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" + "github.com/specterops/dawgs/cypher/models/pgsql/translate" ) func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { @@ -42,6 +45,12 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { Explain: &ExplainResult{ SQL: "select 1;", Plan: []string{"Result (actual rows=1 loops=1)"}, + Optimization: translate.OptimizationSummary{ + Rules: []optimize.RuleResult{{ + Name: "ExpansionSuffixPushdown", + Applied: true, + }}, + }, }, Stats: Stats{ Median: 10 * time.Millisecond, @@ -65,6 +74,9 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { `"distinct_row_count": 2`, `"duplicate_row_count": 0`, `"sql": "select 1;"`, + `"optimization": {`, + `"name": "ExpansionSuffixPushdown"`, + `"applied": true`, `"section": "Traversal"`, } { if !strings.Contains(text, expected) { @@ -114,3 +126,13 @@ func TestWriteMarkdownIncludesDiagnosticColumns(t *testing.T) { } } } + +func TestValidateIterationsRejectsZero(t *testing.T) { + if err := validateIterations(0); err == nil { + t.Fatal("expected zero iterations to be rejected") + } + + if err := validateIterations(1); err != nil { + t.Fatalf("expected one iteration to be valid: %v", err) + } +} diff --git a/cmd/benchmark/runner.go b/cmd/benchmark/runner.go index 99646b5b..929593b4 100644 --- a/cmd/benchmark/runner.go +++ b/cmd/benchmark/runner.go @@ -18,6 +18,7 @@ package main import ( "context" + "fmt" "sort" "time" @@ -30,12 +31,6 @@ type RunOptions struct { Explain ExplainFunc } -// ExplainResult captures PostgreSQL-specific plan diagnostics for a scenario. -type ExplainResult struct { - SQL string `json:"sql"` - Plan []string `json:"plan"` -} - // Stats holds computed timing statistics for a scenario. type Stats struct { Median time.Duration `json:"median"` @@ -57,6 +52,10 @@ type Result struct { // runScenario executes a scenario N times and returns timing stats. func runScenario(ctx context.Context, db graph.Database, s Scenario, iterations int, options RunOptions) (Result, error) { + if err := validateIterations(iterations); err != nil { + return Result{}, err + } + // Warm-up: one untimed run. var measurement Measurement if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { @@ -103,6 +102,14 @@ func runScenario(ctx context.Context, db graph.Database, s Scenario, iterations return result, nil } +func validateIterations(iterations int) error { + if iterations < 1 { + return fmt.Errorf("iterations must be at least 1") + } + + return nil +} + func computeStats(durations []time.Duration) Stats { sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] }) diff --git a/cmd/benchmark/scenarios.go b/cmd/benchmark/scenarios.go index 4f2450b3..48a1efb6 100644 --- a/cmd/benchmark/scenarios.go +++ b/cmd/benchmark/scenarios.go @@ -251,6 +251,7 @@ AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) cypherPathScenario("ADCS Fanout", ds, "p1 only", p1, 1), cypherPathScenario("ADCS Fanout", ds, "p2 only", p2, 1), cypherPathScenario("ADCS Fanout", ds, "combined", combinedMatch+"RETURN p1,p2", 2), + cypherScenario("ADCS Fanout", ds, "combined endpoints", combinedMatch+"RETURN id(ca), id(d), id(ct)"), } } diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index ef66f8ab..802608a2 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -8,8 +8,8 @@ type Rule interface { } type RuleResult struct { - Name string - Applied bool + Name string `json:"name"` + Applied bool `json:"applied"` } type PredicateAttachmentScope string @@ -20,13 +20,13 @@ const ( ) type PredicateAttachment struct { - QueryPartIndex int - RegionIndex int - ClauseIndex int - ExpressionIndex int - Scope PredicateAttachmentScope - BindingSymbols []string - Dependencies []string + QueryPartIndex int `json:"query_part_index"` + RegionIndex int `json:"region_index"` + ClauseIndex int `json:"clause_index"` + ExpressionIndex int `json:"expression_index"` + Scope PredicateAttachmentScope `json:"scope"` + BindingSymbols []string `json:"binding_symbols"` + Dependencies []string `json:"dependencies"` } type Plan struct { diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index a9ec9389..6d6d479d 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -77,6 +77,17 @@ func requireOptimizationLowering(t *testing.T, summary OptimizationSummary, name require.Failf(t, "missing optimization lowering", "expected lowering %q in %#v", name, summary.Lowerings) } +func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { + t.Helper() + + offset := 0 + for _, part := range parts { + nextIndex := strings.Index(sql[offset:], part) + require.NotEqualf(t, -1, nextIndex, "expected SQL to contain %q after offset %d:\n%s", part, offset, sql) + offset += nextIndex + len(part) + } +} + func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { t.Parallel() @@ -258,10 +269,15 @@ RETURN p require.Contains(t, normalizedQuery, "join edge e2 on n3.id = e2.start_id") require.Contains(t, normalizedQuery, "n2.id = e1.start_id") require.Contains(t, normalizedQuery, "e1.kind_id = any") - require.Contains(t, normalizedQuery, "properties -> 'authenticationenabled'") require.Contains(t, normalizedQuery, "n3.kind_ids operator (pg_catalog.@>)") require.Contains(t, normalizedQuery, "e2.kind_id = any") require.Contains(t, normalizedQuery, "e2.end_id = (s0.n0).id") + requireSQLContainsInOrder(t, normalizedQuery, + "exists (select 1 from edge e1 join node n3", + "properties -> 'authenticationenabled'", + "join edge e2 on n3.id = e2.start_id", + "e2.end_id = (s0.n0).id", + ) } func TestOptimizerSafetyExpansionTerminalPushdownForBoundDomainSuffix(t *testing.T) { diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index 8369426a..25056c5c 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -262,3 +262,13 @@ The gap-closure pass has been completed enough to return to the original phase s - Translation metadata reports optimizer rules, predicate attachments, and named lowerings, including `ExpansionSuffixPushdown`. - Phase 9 suffix coverage includes zero-hop expansions, fixed suffix chains, suffixes ending at already-bound nodes, inbound suffixes, and the ADCS root-to-domain suffix shape. - Directionless suffix pushdown remains deliberately unimplemented; those suffixes stay as normal translated pattern steps. + +## Phase 10 Status Notes + +Phase 10 starts by making local measurements repeatable for the optimizer rules already implemented. + +- PostgreSQL `-explain` benchmark JSON includes translated SQL, `EXPLAIN (ANALYZE, BUFFERS)` plan text, optimizer rule results, predicate attachments, and translator lowering decisions. +- The ADCS fanout benchmark includes `p1` alone, `p2` alone, combined path return, and combined endpoint-only return. The endpoint-only scenario gives a local comparison point for final path reconstruction cost. +- The benchmark runner rejects zero timed iterations so baseline output cannot silently panic while gathering measurements. +- Representative SQL-shape tests assert that suffix-local predicates are inside the pushed suffix check, not merely present somewhere in the rendered SQL. +- Broad pass/fail performance thresholds remain deferred. Phase 10 measurements are local evidence and regression artifacts first; cost-based acceptance gates should wait for a larger benchmark corpus and stable environment assumptions. From 475d28d91e61e26957e236ec7f05c7f8447456ce Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:04:35 -0700 Subject: [PATCH 025/114] feat(pgsql): add optimizer lowering metadata contract --- cypher/models/pgsql/optimize/lowering.go | 144 ++++++++++++++++++++ cypher/models/pgsql/optimize/optimizer.go | 1 + cypher/models/pgsql/translate/translator.go | 16 ++- 3 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 cypher/models/pgsql/optimize/lowering.go diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go new file mode 100644 index 00000000..093f0b57 --- /dev/null +++ b/cypher/models/pgsql/optimize/lowering.go @@ -0,0 +1,144 @@ +package optimize + +import "github.com/specterops/dawgs/cypher/models/cypher" + +const ( + LoweringProjectionPruning = "ProjectionPruning" + LoweringLatePathMaterialization = "LatePathMaterialization" + LoweringExpandIntoDetection = "ExpandIntoDetection" + LoweringExpansionSuffixPushdown = "ExpansionSuffixPushdown" + LoweringPredicatePlacement = "PredicatePlacement" +) + +type LoweringDecision struct { + Name string `json:"name"` +} + +type PatternTarget struct { + QueryPartIndex int `json:"query_part_index"` + ClauseIndex int `json:"clause_index"` + PatternIndex int `json:"pattern_index"` +} + +func (s PatternTarget) TraversalStep(stepIndex int) TraversalStepTarget { + return TraversalStepTarget{ + QueryPartIndex: s.QueryPartIndex, + ClauseIndex: s.ClauseIndex, + PatternIndex: s.PatternIndex, + StepIndex: stepIndex, + } +} + +type TraversalStepTarget struct { + QueryPartIndex int `json:"query_part_index"` + ClauseIndex int `json:"clause_index"` + PatternIndex int `json:"pattern_index"` + StepIndex int `json:"step_index"` +} + +type ProjectionPruningDecision struct { + Target TraversalStepTarget `json:"target"` + UnexportLeftNode bool `json:"unexport_left_node,omitempty"` + UnexportEdge bool `json:"unexport_edge,omitempty"` + UnexportRightNode bool `json:"unexport_right_node,omitempty"` + UnexportExpansionID bool `json:"unexport_expansion_id,omitempty"` +} + +type LatePathMaterializationMode string + +const ( + LatePathMaterializationPathEdgeID LatePathMaterializationMode = "path_edge_id" + LatePathMaterializationExpansionPath LatePathMaterializationMode = "expansion_path" + LatePathMaterializationEdgeComposite LatePathMaterializationMode = "edge_composite" +) + +type LatePathMaterializationDecision struct { + Target TraversalStepTarget `json:"target"` + Mode LatePathMaterializationMode `json:"mode"` +} + +type ExpandIntoDecision struct { + Target TraversalStepTarget `json:"target"` +} + +type ExpansionSuffixPushdownDecision struct { + Target TraversalStepTarget `json:"target"` + SuffixLength int `json:"suffix_length"` +} + +type PredicatePlacementDecision struct { + Target TraversalStepTarget `json:"target"` + Attachment PredicateAttachment `json:"attachment"` + Placement PredicateAttachmentScope `json:"placement"` +} + +type LoweringPlan struct { + ProjectionPruning []ProjectionPruningDecision `json:"projection_pruning,omitempty"` + LatePathMaterialization []LatePathMaterializationDecision `json:"late_path_materialization,omitempty"` + ExpandInto []ExpandIntoDecision `json:"expand_into,omitempty"` + ExpansionSuffixPushdown []ExpansionSuffixPushdownDecision `json:"expansion_suffix_pushdown,omitempty"` + PredicatePlacement []PredicatePlacementDecision `json:"predicate_placement,omitempty"` +} + +func (s LoweringPlan) Empty() bool { + return len(s.ProjectionPruning) == 0 && + len(s.LatePathMaterialization) == 0 && + len(s.ExpandInto) == 0 && + len(s.ExpansionSuffixPushdown) == 0 && + len(s.PredicatePlacement) == 0 +} + +func (s LoweringPlan) Decisions() []LoweringDecision { + var decisions []LoweringDecision + add := func(name string, applied bool) { + if applied { + decisions = append(decisions, LoweringDecision{Name: name}) + } + } + + add(LoweringProjectionPruning, len(s.ProjectionPruning) > 0) + add(LoweringLatePathMaterialization, len(s.LatePathMaterialization) > 0) + add(LoweringExpandIntoDetection, len(s.ExpandInto) > 0) + add(LoweringExpansionSuffixPushdown, len(s.ExpansionSuffixPushdown) > 0) + add(LoweringPredicatePlacement, len(s.PredicatePlacement) > 0) + + return decisions +} + +func IndexPatternTargets(query *cypher.RegularQuery) map[*cypher.PatternPart]PatternTarget { + targets := map[*cypher.PatternPart]PatternTarget{} + + if query == nil || query.SingleQuery == nil { + return targets + } + + if query.SingleQuery.MultiPartQuery != nil { + for queryPartIndex, part := range query.SingleQuery.MultiPartQuery.Parts { + indexReadingClauseTargets(targets, queryPartIndex, part.ReadingClauses) + } + + if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { + indexReadingClauseTargets(targets, len(query.SingleQuery.MultiPartQuery.Parts), finalPart.ReadingClauses) + } + } else if query.SingleQuery.SinglePartQuery != nil { + indexReadingClauseTargets(targets, 0, query.SingleQuery.SinglePartQuery.ReadingClauses) + } + + return targets +} + +func indexReadingClauseTargets(targets map[*cypher.PatternPart]PatternTarget, queryPartIndex int, readingClauses []*cypher.ReadingClause) { + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + for patternIndex, patternPart := range readingClause.Match.Pattern { + targets[patternPart] = PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + } + } +} diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index 802608a2..8f18d6b7 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -32,6 +32,7 @@ type PredicateAttachment struct { type Plan struct { Query *cypher.RegularQuery Analysis Analysis + LoweringPlan LoweringPlan Rules []RuleResult PredicateAttachments []PredicateAttachment } diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 6525e002..79c220cb 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -528,11 +528,8 @@ type Result struct { type OptimizationSummary struct { Rules []optimize.RuleResult `json:"rules,omitempty"` PredicateAttachments []optimize.PredicateAttachment `json:"predicate_attachments,omitempty"` - Lowerings []LoweringDecision `json:"lowerings,omitempty"` -} - -type LoweringDecision struct { - Name string `json:"name"` + Lowerings []optimize.LoweringDecision `json:"lowerings,omitempty"` + LoweringPlan *optimize.LoweringPlan `json:"lowering_plan,omitempty"` } func (s *Translator) recordLowering(name string) { @@ -542,7 +539,7 @@ func (s *Translator) recordLowering(name string) { } } - s.translation.Optimization.Lowerings = append(s.translation.Optimization.Lowerings, LoweringDecision{Name: name}) + s.translation.Optimization.Lowerings = append(s.translation.Optimization.Lowerings, optimize.LoweringDecision{Name: name}) } func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) (Result, error) { @@ -554,6 +551,13 @@ func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper translator := NewTranslator(ctx, kindMapper, parameters, graphID) translator.translation.Optimization.Rules = optimizedPlan.Rules translator.translation.Optimization.PredicateAttachments = optimizedPlan.PredicateAttachments + if !optimizedPlan.LoweringPlan.Empty() { + loweringPlan := optimizedPlan.LoweringPlan + translator.translation.Optimization.LoweringPlan = &loweringPlan + for _, lowering := range loweringPlan.Decisions() { + translator.recordLowering(lowering.Name) + } + } if err := walk.Cypher(optimizedPlan.Query, translator); err != nil { return Result{}, err From f55b969f04ea0509f0886ac2b5c51f26ab0e48d0 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:08:21 -0700 Subject: [PATCH 026/114] refactor(pgsql): lift projection pruning decisions into optimizer --- cypher/models/pgsql/optimize/lowering.go | 12 +- cypher/models/pgsql/optimize/lowering_plan.go | 149 ++++++++++++++++++ cypher/models/pgsql/optimize/optimizer.go | 6 + .../models/pgsql/optimize/optimizer_test.go | 23 +++ .../pgsql/optimize/source_references.go | 118 ++++++++++++++ cypher/models/pgsql/translate/expansion.go | 5 +- cypher/models/pgsql/translate/model.go | 3 + .../pgsql/translate/optimizer_safety_test.go | 3 + cypher/models/pgsql/translate/pattern.go | 4 + cypher/models/pgsql/translate/translator.go | 15 ++ cypher/models/pgsql/translate/traversal.go | 98 +++++++++++- 11 files changed, 425 insertions(+), 11 deletions(-) create mode 100644 cypher/models/pgsql/optimize/lowering_plan.go create mode 100644 cypher/models/pgsql/optimize/source_references.go diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index 093f0b57..c3defda5 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -37,11 +37,9 @@ type TraversalStepTarget struct { } type ProjectionPruningDecision struct { - Target TraversalStepTarget `json:"target"` - UnexportLeftNode bool `json:"unexport_left_node,omitempty"` - UnexportEdge bool `json:"unexport_edge,omitempty"` - UnexportRightNode bool `json:"unexport_right_node,omitempty"` - UnexportExpansionID bool `json:"unexport_expansion_id,omitempty"` + Target TraversalStepTarget `json:"target"` + ReferencedSymbols []string `json:"referenced_symbols,omitempty"` + PatternBindingReferenced bool `json:"pattern_binding_referenced,omitempty"` } type LatePathMaterializationMode string @@ -114,6 +112,10 @@ func IndexPatternTargets(query *cypher.RegularQuery) map[*cypher.PatternPart]Pat if query.SingleQuery.MultiPartQuery != nil { for queryPartIndex, part := range query.SingleQuery.MultiPartQuery.Parts { + if part == nil { + continue + } + indexReadingClauseTargets(targets, queryPartIndex, part.ReadingClauses) } diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go new file mode 100644 index 00000000..35f18f30 --- /dev/null +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -0,0 +1,149 @@ +package optimize + +import "github.com/specterops/dawgs/cypher/models/cypher" + +type sourceTraversalStep struct { + LeftNode *cypher.NodePattern + Relationship *cypher.RelationshipPattern + RightNode *cypher.NodePattern +} + +func BuildLoweringPlan(query *cypher.RegularQuery, _ Analysis) (LoweringPlan, error) { + if query == nil || query.SingleQuery == nil { + return LoweringPlan{}, nil + } + + var plan LoweringPlan + + if query.SingleQuery.MultiPartQuery != nil { + for queryPartIndex, part := range query.SingleQuery.MultiPartQuery.Parts { + if part == nil { + continue + } + + if err := appendQueryPartLowerings(&plan, queryPartIndex, part, part.ReadingClauses); err != nil { + return LoweringPlan{}, err + } + } + + if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { + if err := appendQueryPartLowerings(&plan, len(query.SingleQuery.MultiPartQuery.Parts), finalPart, finalPart.ReadingClauses); err != nil { + return LoweringPlan{}, err + } + } + } else if singlePart := query.SingleQuery.SinglePartQuery; singlePart != nil { + if err := appendQueryPartLowerings(&plan, 0, singlePart, singlePart.ReadingClauses); err != nil { + return LoweringPlan{}, err + } + } + + return plan, nil +} + +func appendQueryPartLowerings(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode, readingClauses []*cypher.ReadingClause) error { + sourceReferences, err := collectReferencedSourceIdentifiers(queryPart) + if err != nil { + return err + } + + appendProjectionPruningDecisions(plan, queryPartIndex, readingClauses, sourceReferences) + return nil +} + +func appendProjectionPruningDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, sourceReferences map[string]struct{}) { + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil || readingClause.Match.Optional { + continue + } + + for patternIndex, patternPart := range readingClause.Match.Pattern { + steps := traversalStepsForPattern(patternPart) + if len(steps) == 0 { + continue + } + + appendPatternProjectionPruningDecisions(plan, PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }, patternPart, steps, sourceReferences) + } + } +} + +func appendPatternProjectionPruningDecisions(plan *LoweringPlan, target PatternTarget, patternPart *cypher.PatternPart, steps []sourceTraversalStep, sourceReferences map[string]struct{}) { + pathReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(patternPart.Variable)) + + for stepIndex, step := range steps { + decision := ProjectionPruningDecision{ + Target: target.TraversalStep(stepIndex), + ReferencedSymbols: sortedMapKeys(sourceReferences), + PatternBindingReferenced: pathReferenced, + } + + edgeReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.Relationship.Variable)) + var hasPruning bool + if step.Relationship.Range != nil { + hasPruning = !edgeReferenced || !pathReferenced + } else { + leftReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.LeftNode.Variable)) + rightReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.RightNode.Variable)) + + hasPruning = !(leftReferenced || pathReferenced) || + !(edgeReferenced || pathReferenced) || + !(rightReferenced || pathReferenced || stepIndex+1 < len(steps)) + } + + if hasPruning { + plan.ProjectionPruning = append(plan.ProjectionPruning, decision) + } + } +} + +func traversalStepsForPattern(patternPart *cypher.PatternPart) []sourceTraversalStep { + if patternPart == nil { + return nil + } + + var ( + steps []sourceTraversalStep + leftNode *cypher.NodePattern + relationship *cypher.RelationshipPattern + ) + + for _, element := range patternPart.PatternElements { + if element == nil { + continue + } + + if nodePattern, isNodePattern := element.AsNodePattern(); isNodePattern { + if leftNode == nil { + leftNode = nodePattern + continue + } + + if relationship != nil { + steps = append(steps, sourceTraversalStep{ + LeftNode: leftNode, + Relationship: relationship, + RightNode: nodePattern, + }) + } + + leftNode = nodePattern + relationship = nil + } else if relationshipPattern, isRelationshipPattern := element.AsRelationshipPattern(); isRelationshipPattern { + relationship = relationshipPattern + } + } + + return steps +} + +func variableSymbol(variable *cypher.Variable) string { + if variable == nil { + return "" + } + + return variable.Symbol +} diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index 8f18d6b7..496c594d 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -81,6 +81,12 @@ func (s Optimizer) Optimize(query *cypher.RegularQuery) (Plan, error) { plan.Analysis = Analyze(plan.Query) } + if loweringPlan, err := BuildLoweringPlan(plan.Query, plan.Analysis); err != nil { + return Plan{}, err + } else { + plan.LoweringPlan = loweringPlan + } + return plan, nil } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 332a3d89..ce768f5c 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -67,6 +67,29 @@ func TestDefaultPredicateAttachmentRuleReportsSkippedWhenNoPredicatesExist(t *te require.Empty(t, plan.PredicateAttachments) } +func TestLoweringPlanReportsProjectionPruning(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (n)-[r:MemberOf]->(m) + RETURN m + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, []LoweringDecision{{Name: LoweringProjectionPruning}}, plan.LoweringPlan.Decisions()) + require.Equal(t, []ProjectionPruningDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + ReferencedSymbols: []string{"m"}, + }}, plan.LoweringPlan.ProjectionPruning) +} + func TestPredicateAttachmentRuleAssignsSingleBindingPredicates(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/optimize/source_references.go b/cypher/models/pgsql/optimize/source_references.go new file mode 100644 index 00000000..ebdbbe14 --- /dev/null +++ b/cypher/models/pgsql/optimize/source_references.go @@ -0,0 +1,118 @@ +package optimize + +import ( + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/walk" +) + +type sourceReferenceCollector struct { + walk.VisitorHandler + + referencedIdentifiers map[string]struct{} + matchPatternDeclarationRefs map[string]int + matchPatternDeclarations map[*cypher.PatternPart]struct{} + matchPatternDeclarationDepth int +} + +func newSourceReferenceCollector() *sourceReferenceCollector { + return &sourceReferenceCollector{ + VisitorHandler: walk.NewCancelableErrorHandler(), + referencedIdentifiers: map[string]struct{}{}, + matchPatternDeclarationRefs: map[string]int{}, + matchPatternDeclarations: map[*cypher.PatternPart]struct{}{}, + } +} + +func (s *sourceReferenceCollector) addVariable(variable *cypher.Variable) { + if variable != nil && variable.Symbol != "" { + s.referencedIdentifiers[variable.Symbol] = struct{}{} + } +} + +func (s *sourceReferenceCollector) addMatchPatternDeclaration(variable *cypher.Variable) { + if variable != nil && variable.Symbol != "" { + s.matchPatternDeclarationRefs[variable.Symbol] += 1 + } +} + +func (s *sourceReferenceCollector) collectRepeatedMatchPatternDeclarations() { + for identifier, numDeclarations := range s.matchPatternDeclarationRefs { + if numDeclarations > 1 { + s.referencedIdentifiers[identifier] = struct{}{} + } + } +} + +func (s *sourceReferenceCollector) isMatchPatternDeclaration(patternPart *cypher.PatternPart) bool { + _, isDeclaration := s.matchPatternDeclarations[patternPart] + return isDeclaration +} + +func (s *sourceReferenceCollector) Enter(node cypher.SyntaxNode) { + switch typedNode := node.(type) { + case *cypher.Match: + for _, patternPart := range typedNode.Pattern { + s.matchPatternDeclarations[patternPart] = struct{}{} + } + + case *cypher.PatternPart: + if s.isMatchPatternDeclaration(typedNode) { + s.addMatchPatternDeclaration(typedNode.Variable) + s.matchPatternDeclarationDepth += 1 + } else { + s.addVariable(typedNode.Variable) + } + + case *cypher.NodePattern: + if s.matchPatternDeclarationDepth == 0 { + s.addVariable(typedNode.Variable) + } else { + s.addMatchPatternDeclaration(typedNode.Variable) + } + + case *cypher.RelationshipPattern: + if s.matchPatternDeclarationDepth == 0 { + s.addVariable(typedNode.Variable) + } else { + s.addMatchPatternDeclaration(typedNode.Variable) + } + + case *cypher.Variable: + s.addVariable(typedNode) + } +} + +func (s *sourceReferenceCollector) Visit(cypher.SyntaxNode) {} + +func (s *sourceReferenceCollector) Exit(node cypher.SyntaxNode) { + if patternPart, isPatternPart := node.(*cypher.PatternPart); isPatternPart && s.isMatchPatternDeclaration(patternPart) { + s.matchPatternDeclarationDepth -= 1 + } +} + +func collectReferencedSourceIdentifiers(root cypher.SyntaxNode) (map[string]struct{}, error) { + if root == nil { + return map[string]struct{}{}, nil + } + + collector := newSourceReferenceCollector() + if err := walk.Cypher(root, collector); err != nil { + return collector.referencedIdentifiers, err + } + + collector.collectRepeatedMatchPatternDeclarations() + return collector.referencedIdentifiers, nil +} + +func referencesSourceIdentifier(references map[string]struct{}, symbol string) bool { + if _, referencesAll := references[cypher.TokenLiteralAsterisk]; referencesAll { + return true + } + + if symbol == "" { + return false + } + + _, referenced := references[symbol] + return referenced +} diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 41fd459c..cfa1d7a0 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2641,7 +2641,7 @@ func (s *Translator) buildExpansionProjectionConstraints(traversalStepContext Tr return projectionConstraints, nil } -func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPart, isFirstTraversalStep bool, traversalStep *TraversalStep, allowProjectionPruning bool) error { +func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPart, stepIndex int, isFirstTraversalStep bool, traversalStep *TraversalStep, allowProjectionPruning bool) error { expansionModel := traversalStep.Expansion // Translate the expansion's constraints - this has the side effect of making the pattern identifiers visible in @@ -2653,7 +2653,8 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar // Export the path from the traversal's scope traversalStep.Frame.Export(expansionModel.PathBinding.Identifier) if allowProjectionPruning { - pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep) + decision, hasDecision := s.projectionPruningDecision(part, stepIndex) + pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision) } // Push a new frame that contains currently projected scope from the expansion recursive CTE diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index f3f7ab00..c7403c23 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -6,6 +6,7 @@ import ( "github.com/specterops/dawgs/cypher/models" "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/cypher/models/walk" "github.com/specterops/dawgs/graph" ) @@ -598,6 +599,8 @@ type PatternPart struct { ShortestPath bool AllShortestPaths bool PatternBinding *BoundIdentifier + Target optimize.PatternTarget + HasTarget bool TraversalSteps []*TraversalStep NodeSelect NodeSelect Constraints *ConstraintTracker diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 6d6d479d..c20201d6 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -238,6 +238,9 @@ RETURN p require.NotEmpty(t, translation.Optimization.Rules) require.NotEmpty(t, translation.Optimization.PredicateAttachments) + require.NotNil(t, translation.Optimization.LoweringPlan) + require.NotEmpty(t, translation.Optimization.LoweringPlan.ProjectionPruning) + requireOptimizationLowering(t, translation.Optimization, "ProjectionPruning") requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") } diff --git a/cypher/models/pgsql/translate/pattern.go b/cypher/models/pgsql/translate/pattern.go index 57eff0d2..9383999f 100644 --- a/cypher/models/pgsql/translate/pattern.go +++ b/cypher/models/pgsql/translate/pattern.go @@ -38,6 +38,10 @@ func (s *Translator) translatePatternPart(patternPart *cypher.PatternPart) error newPatternPart.IsTraversal = len(patternPart.PatternElements) > 1 newPatternPart.ShortestPath = patternPart.ShortestPathPattern newPatternPart.AllShortestPaths = patternPart.AllShortestPathsPattern + if target, hasTarget := s.patternTargets[patternPart]; hasTarget { + newPatternPart.Target = target + newPatternPart.HasTarget = true + } if cypherBinding, hasCypherSymbol, err := extractIdentifierFromCypherExpression(patternPart); err != nil { return err diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 79c220cb..fc03f9a0 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -29,6 +29,10 @@ type Translator struct { query *Query scope *Scope unwindTargets map[*cypher.Variable]struct{} + + hasOptimizationPlan bool + patternTargets map[*cypher.PatternPart]optimize.PatternTarget + projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision } func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) *Translator { @@ -60,6 +64,16 @@ func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters } } +func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { + s.hasOptimizationPlan = true + s.patternTargets = optimize.IndexPatternTargets(plan.Query) + s.projectionPruningDecisions = map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision{} + + for _, decision := range plan.LoweringPlan.ProjectionPruning { + s.projectionPruningDecisions[decision.Target] = decision + } +} + func (s *Translator) Enter(expression cypher.SyntaxNode) { switch typedExpression := expression.(type) { case *cypher.RegularQuery, *cypher.SingleQuery, *cypher.PatternElement, @@ -549,6 +563,7 @@ func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper } translator := NewTranslator(ctx, kindMapper, parameters, graphID) + translator.SetOptimizationPlan(optimizedPlan) translator.translation.Optimization.Rules = optimizedPlan.Rules translator.translation.Optimization.PredicateAttachments = optimizedPlan.PredicateAttachments if !optimizedPlan.LoweringPlan.Empty() { diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 8dc0f401..9c494664 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/specterops/dawgs/cypher/models" + "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/graph" ) @@ -591,7 +593,7 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } if traversalStep.Expansion != nil { - if err := s.translateTraversalPatternPartWithExpansion(part, idx == 0, traversalStep, allowProjectionPruning); err != nil { + if err := s.translateTraversalPatternPartWithExpansion(part, idx, idx == 0, traversalStep, allowProjectionPruning); err != nil { return err } } else if part.AllShortestPaths || part.ShortestPath { @@ -632,6 +634,65 @@ func patternBindingDependsOn(queryPart *QueryPart, part *PatternPart, binding *B return false } +func (s *Translator) projectionPruningDecision(part *PatternPart, stepIndex int) (optimize.ProjectionPruningDecision, bool) { + if part == nil || !part.HasTarget { + return optimize.ProjectionPruningDecision{}, false + } + + decision, hasDecision := s.projectionPruningDecisions[part.Target.TraversalStep(stepIndex)] + return decision, hasDecision +} + +func projectionPruningDecisionReferencesBinding(decision optimize.ProjectionPruningDecision, binding *BoundIdentifier) bool { + if binding == nil { + return false + } + + sourceIdentifier := binding.Identifier + if binding.Alias.Set { + sourceIdentifier = binding.Alias.Value + } + + for _, symbol := range decision.ReferencedSymbols { + if symbol == cypher.TokenLiteralAsterisk || pgsql.Identifier(symbol) == sourceIdentifier { + return true + } + } + + return false +} + +func projectionPruningDecisionPatternDependsOn(part *PatternPart, binding *BoundIdentifier, decision optimize.ProjectionPruningDecision) bool { + if !decision.PatternBindingReferenced || part == nil || part.PatternBinding == nil || binding == nil { + return false + } + + for _, dependency := range part.PatternBinding.Dependencies { + if dependency.Identifier == binding.Identifier { + return true + } + } + + return false +} + +func traversalStepProjectsBindingByDecision(part *PatternPart, stepIndex int, binding *BoundIdentifier, decision optimize.ProjectionPruningDecision) bool { + if binding == nil { + return false + } + + if projectionPruningDecisionReferencesBinding(decision, binding) || projectionPruningDecisionPatternDependsOn(part, binding, decision) { + return true + } + + if stepIndex+1 < len(part.TraversalSteps) { + nextStep := part.TraversalSteps[stepIndex+1] + return nextStep.LeftNode != nil && nextStep.LeftNode.Identifier == binding.Identifier + } + + return false +} + func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepIndex int, binding *BoundIdentifier) bool { if binding == nil { return false @@ -653,7 +714,23 @@ func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepI return false } -func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep) { +func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool) { + if hasDecision { + if traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.LeftNode, decision) { + traversalStep.Frame.Unexport(traversalStep.LeftNode.Identifier) + } + + if traversalStep.Edge != nil && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.Edge, decision) { + traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + } + + if traversalStep.RightNode != nil && !traversalStep.RightNodeBound && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.RightNode, decision) { + traversalStep.Frame.Unexport(traversalStep.RightNode.Identifier) + } + + return + } + // Bound endpoints already exist in an outer frame. Only unexport unbound // values that later clauses and continuation steps cannot observe. if !traversalStep.LeftNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.LeftNode) { @@ -676,11 +753,23 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart } } -func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep) { +func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool) { if traversalStep == nil || traversalStep.Expansion == nil { return } + if hasDecision { + if traversalStep.Edge != nil && !projectionPruningDecisionReferencesBinding(decision, traversalStep.Edge) { + traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + } + + if traversalStep.Expansion.PathBinding != nil && !projectionPruningDecisionPatternDependsOn(part, traversalStep.Expansion.PathBinding, decision) { + traversalStep.Frame.Unexport(traversalStep.Expansion.PathBinding.Identifier) + } + + return + } + // Variable-length relationship bindings materialize to edge-composite // arrays. A path binding can be rebuilt later from the compact expansion // path ID array, so keep the edge array only when the relationship binding @@ -773,7 +862,8 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } if allowProjectionPruning { - pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep) + decision, hasDecision := s.projectionPruningDecision(part, stepIndex) + pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision) } if boundProjections, err := buildVisibleProjections(s.scope); err != nil { From cf4768aa26b675adc5f78e631c493f1e6e50b52f Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:09:11 -0700 Subject: [PATCH 027/114] refactor(pgsql): lift late path materialization decisions --- cypher/models/pgsql/optimize/lowering_plan.go | 41 +++++++++++++++++++ .../models/pgsql/optimize/optimizer_test.go | 40 ++++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 2 + cypher/models/pgsql/translate/translator.go | 6 +++ cypher/models/pgsql/translate/traversal.go | 20 +++++++++ 5 files changed, 109 insertions(+) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 35f18f30..80b71624 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -47,6 +47,7 @@ func appendQueryPartLowerings(plan *LoweringPlan, queryPartIndex int, queryPart } appendProjectionPruningDecisions(plan, queryPartIndex, readingClauses, sourceReferences) + appendLatePathMaterializationDecisions(plan, queryPartIndex, readingClauses, sourceReferences) return nil } @@ -100,6 +101,46 @@ func appendPatternProjectionPruningDecisions(plan *LoweringPlan, target PatternT } } +func appendLatePathMaterializationDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, sourceReferences map[string]struct{}) { + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil || readingClause.Match.Optional { + continue + } + + for patternIndex, patternPart := range readingClause.Match.Pattern { + if !referencesSourceIdentifier(sourceReferences, variableSymbol(patternPart.Variable)) { + continue + } + + for stepIndex, step := range traversalStepsForPattern(patternPart) { + target := PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }.TraversalStep(stepIndex) + + if step.Relationship.Range != nil { + plan.LatePathMaterialization = append(plan.LatePathMaterialization, LatePathMaterializationDecision{ + Target: target, + Mode: LatePathMaterializationExpansionPath, + }) + continue + } + + mode := LatePathMaterializationPathEdgeID + if referencesSourceIdentifier(sourceReferences, variableSymbol(step.Relationship.Variable)) { + mode = LatePathMaterializationEdgeComposite + } + + plan.LatePathMaterialization = append(plan.LatePathMaterialization, LatePathMaterializationDecision{ + Target: target, + Mode: mode, + }) + } + } + } +} + func traversalStepsForPattern(patternPart *cypher.PatternPart) []sourceTraversalStep { if patternPart == nil { return nil diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index ce768f5c..27a54c42 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -90,6 +90,46 @@ func TestLoweringPlanReportsProjectionPruning(t *testing.T) { }}, plan.LoweringPlan.ProjectionPruning) } +func TestLoweringPlanReportsLatePathMaterialization(t *testing.T) { + t.Parallel() + + t.Run("path edge id", func(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n)-[r:MemberOf]->(m) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, []LatePathMaterializationDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Mode: LatePathMaterializationPathEdgeID, + }}, plan.LoweringPlan.LatePathMaterialization) + }) + + t.Run("relationship composite", func(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n)-[r:MemberOf]->(m) + RETURN p, r + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, LatePathMaterializationEdgeComposite, plan.LoweringPlan.LatePathMaterialization[0].Mode) + }) +} + func TestPredicateAttachmentRuleAssignsSingleBindingPredicates(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index c20201d6..82baf1cb 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -240,7 +240,9 @@ RETURN p require.NotEmpty(t, translation.Optimization.PredicateAttachments) require.NotNil(t, translation.Optimization.LoweringPlan) require.NotEmpty(t, translation.Optimization.LoweringPlan.ProjectionPruning) + require.NotEmpty(t, translation.Optimization.LoweringPlan.LatePathMaterialization) requireOptimizationLowering(t, translation.Optimization, "ProjectionPruning") + requireOptimizationLowering(t, translation.Optimization, "LatePathMaterialization") requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") } diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index fc03f9a0..8046eba8 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -33,6 +33,7 @@ type Translator struct { hasOptimizationPlan bool patternTargets map[*cypher.PatternPart]optimize.PatternTarget projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision + latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision } func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) *Translator { @@ -68,10 +69,15 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.hasOptimizationPlan = true s.patternTargets = optimize.IndexPatternTargets(plan.Query) s.projectionPruningDecisions = map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision{} + s.latePathDecisions = map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision{} for _, decision := range plan.LoweringPlan.ProjectionPruning { s.projectionPruningDecisions[decision.Target] = decision } + + for _, decision := range plan.LoweringPlan.LatePathMaterialization { + s.latePathDecisions[decision.Target] = append(s.latePathDecisions[decision.Target], decision) + } } func (s *Translator) Enter(expression cypher.SyntaxNode) { diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 9c494664..f29c92ef 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -643,6 +643,20 @@ func (s *Translator) projectionPruningDecision(part *PatternPart, stepIndex int) return decision, hasDecision } +func (s *Translator) hasLatePathMaterialization(part *PatternPart, stepIndex int, mode optimize.LatePathMaterializationMode) bool { + if part == nil || !part.HasTarget { + return false + } + + for _, decision := range s.latePathDecisions[part.Target.TraversalStep(stepIndex)] { + if decision.Mode == mode { + return true + } + } + + return false +} + func projectionPruningDecisionReferencesBinding(decision optimize.ProjectionPruningDecision, binding *BoundIdentifier) bool { if binding == nil { return false @@ -862,6 +876,12 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } if allowProjectionPruning { + if traversalStep.Edge != nil && + traversalStep.Edge.DataType == pgsql.EdgeComposite && + s.hasLatePathMaterialization(part, stepIndex, optimize.LatePathMaterializationPathEdgeID) { + traversalStep.Edge.DataType = pgsql.PathEdge + } + decision, hasDecision := s.projectionPruningDecision(part, stepIndex) pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision) } From 23789157e5e41c46a256504bac06dde9455e91f9 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:10:20 -0700 Subject: [PATCH 028/114] refactor(pgsql): lift expansion suffix pushdown detection --- cypher/models/pgsql/optimize/lowering_plan.go | 132 +++++++++++++++++- .../models/pgsql/optimize/optimizer_test.go | 37 +++++ .../pgsql/translate/optimizer_safety_test.go | 1 + cypher/models/pgsql/translate/translator.go | 6 + cypher/models/pgsql/translate/traversal.go | 37 ++++- 5 files changed, 211 insertions(+), 2 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 80b71624..b5352666 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -1,6 +1,9 @@ package optimize -import "github.com/specterops/dawgs/cypher/models/cypher" +import ( + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/graph" +) type sourceTraversalStep struct { LeftNode *cypher.NodePattern @@ -48,6 +51,7 @@ func appendQueryPartLowerings(plan *LoweringPlan, queryPartIndex int, queryPart appendProjectionPruningDecisions(plan, queryPartIndex, readingClauses, sourceReferences) appendLatePathMaterializationDecisions(plan, queryPartIndex, readingClauses, sourceReferences) + appendExpansionSuffixPushdownDecisions(plan, queryPartIndex, readingClauses) return nil } @@ -141,6 +145,132 @@ func appendLatePathMaterializationDecisions(plan *LoweringPlan, queryPartIndex i } } +func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause) { + declaredSymbols := map[string]struct{}{} + + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + match := readingClause.Match + if match.Optional { + declareMatchSymbols(declaredSymbols, match) + continue + } + + for patternIndex, patternPart := range match.Pattern { + steps := traversalStepsForPattern(patternPart) + declaredBeforeRightNode := declaredSymbolsBeforeRightNodes(declaredSymbols, steps) + + for stepIndex, step := range steps { + if step.Relationship.Range == nil || stepIndex+1 >= len(steps) { + continue + } + + if suffixLength := expansionSuffixPushdownLength(steps[stepIndex+1:], declaredBeforeRightNode[stepIndex+1:]); suffixLength > 0 { + plan.ExpansionSuffixPushdown = append(plan.ExpansionSuffixPushdown, ExpansionSuffixPushdownDecision{ + Target: PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }.TraversalStep(stepIndex), + SuffixLength: suffixLength, + }) + } + } + + declarePatternSymbols(declaredSymbols, patternPart) + } + + declareWhereSymbols(declaredSymbols, match) + } +} + +func expansionSuffixPushdownLength(suffixSteps []sourceTraversalStep, declaredBeforeRightNode []map[string]struct{}) int { + var suffixLength int + + for idx, step := range suffixSteps { + if step.Relationship.Range != nil || step.Relationship.Direction == graph.DirectionBoth { + break + } + + if nodeSymbol := variableSymbol(step.RightNode.Variable); nodeSymbol != "" { + if _, bound := declaredBeforeRightNode[idx][nodeSymbol]; bound && nodePatternHasConstraints(step.RightNode) { + break + } + } + + suffixLength++ + } + + return suffixLength +} + +func declaredSymbolsBeforeRightNodes(initial map[string]struct{}, steps []sourceTraversalStep) []map[string]struct{} { + declared := copyStringSet(initial) + declaredBeforeRightNode := make([]map[string]struct{}, len(steps)) + + for idx, step := range steps { + addSymbol(declared, variableSymbol(step.LeftNode.Variable)) + addSymbol(declared, variableSymbol(step.Relationship.Variable)) + declaredBeforeRightNode[idx] = copyStringSet(declared) + addSymbol(declared, variableSymbol(step.RightNode.Variable)) + } + + return declaredBeforeRightNode +} + +func declareMatchSymbols(declared map[string]struct{}, match *cypher.Match) { + if match == nil { + return + } + + for _, patternPart := range match.Pattern { + declarePatternSymbols(declared, patternPart) + } + + declareWhereSymbols(declared, match) +} + +func declarePatternSymbols(declared map[string]struct{}, patternPart *cypher.PatternPart) { + if patternPart == nil { + return + } + + addSymbol(declared, variableSymbol(patternPart.Variable)) + for _, step := range traversalStepsForPattern(patternPart) { + addSymbol(declared, variableSymbol(step.LeftNode.Variable)) + addSymbol(declared, variableSymbol(step.Relationship.Variable)) + addSymbol(declared, variableSymbol(step.RightNode.Variable)) + } +} + +func declareWhereSymbols(declared map[string]struct{}, match *cypher.Match) { + for _, dependency := range dependenciesForMatch(match) { + addSymbol(declared, dependency) + } +} + +func nodePatternHasConstraints(nodePattern *cypher.NodePattern) bool { + return nodePattern != nil && (len(nodePattern.Kinds) > 0 || nodePattern.Properties != nil) +} + +func addSymbol(symbols map[string]struct{}, symbol string) { + if symbol != "" { + symbols[symbol] = struct{}{} + } +} + +func copyStringSet(values map[string]struct{}) map[string]struct{} { + copied := make(map[string]struct{}, len(values)) + for value := range values { + copied[value] = struct{}{} + } + + return copied +} + func traversalStepsForPattern(patternPart *cypher.PatternPart) []sourceTraversalStep { if patternPart == nil { return nil diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 27a54c42..1eb6c1c7 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -130,6 +130,43 @@ func TestLoweringPlanReportsLatePathMaterialization(t *testing.T) { }) } +func TestLoweringPlanReportsExpansionSuffixPushdown(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n:Group)-[:MemberOf*0..]->(m)-[:Enroll]->(ca:EnterpriseCA) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringExpansionSuffixPushdown}) + require.Equal(t, []ExpansionSuffixPushdownDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + SuffixLength: 1, + }}, plan.LoweringPlan.ExpansionSuffixPushdown) +} + +func TestLoweringPlanSkipsDirectionlessExpansionSuffixPushdown(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n:Group)-[:MemberOf*0..]->(m)-[:Enroll]-(ca:EnterpriseCA) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Empty(t, plan.LoweringPlan.ExpansionSuffixPushdown) +} + func TestPredicateAttachmentRuleAssignsSingleBindingPredicates(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 82baf1cb..c1373c57 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -241,6 +241,7 @@ RETURN p require.NotNil(t, translation.Optimization.LoweringPlan) require.NotEmpty(t, translation.Optimization.LoweringPlan.ProjectionPruning) require.NotEmpty(t, translation.Optimization.LoweringPlan.LatePathMaterialization) + require.NotEmpty(t, translation.Optimization.LoweringPlan.ExpansionSuffixPushdown) requireOptimizationLowering(t, translation.Optimization, "ProjectionPruning") requireOptimizationLowering(t, translation.Optimization, "LatePathMaterialization") requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 8046eba8..ce604822 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -34,6 +34,7 @@ type Translator struct { patternTargets map[*cypher.PatternPart]optimize.PatternTarget projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision + suffixPushdownDecisions map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision } func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) *Translator { @@ -70,6 +71,7 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.patternTargets = optimize.IndexPatternTargets(plan.Query) s.projectionPruningDecisions = map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision{} s.latePathDecisions = map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision{} + s.suffixPushdownDecisions = map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision{} for _, decision := range plan.LoweringPlan.ProjectionPruning { s.projectionPruningDecisions[decision.Target] = decision @@ -78,6 +80,10 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { for _, decision := range plan.LoweringPlan.LatePathMaterialization { s.latePathDecisions[decision.Target] = append(s.latePathDecisions[decision.Target], decision) } + + for _, decision := range plan.LoweringPlan.ExpansionSuffixPushdown { + s.suffixPushdownDecisions[decision.Target] = append(s.suffixPushdownDecisions[decision.Target], decision) + } } func (s *Translator) Enter(expression cypher.SyntaxNode) { diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index f29c92ef..d3a5b78a 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -603,7 +603,7 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } } - if applied, err := applyExpansionSuffixPushdown(part); err != nil { + if applied, err := s.applyExpansionSuffixPushdown(part); err != nil { return err } else if applied > 0 { s.recordLowering("ExpansionSuffixPushdown") @@ -616,6 +616,41 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr return nil } +func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error) { + if part == nil || !part.HasTarget { + return applyExpansionSuffixPushdown(part) + } + + var applied int + for stepIndex := range part.TraversalSteps { + target := part.Target.TraversalStep(stepIndex) + for _, decision := range s.suffixPushdownDecisions[target] { + if decision.SuffixLength <= 0 || stepIndex+decision.SuffixLength >= len(part.TraversalSteps) { + continue + } + + currentStep := part.TraversalSteps[stepIndex] + suffixSteps := part.TraversalSteps[stepIndex+1 : stepIndex+1+decision.SuffixLength] + if suffixSatisfaction, satisfied := expansionSuffixTerminalSatisfaction(currentStep, suffixSteps); satisfied { + currentStep.Expansion.TerminalNodeConstraints = pgsql.OptionalAnd( + currentStep.Expansion.TerminalNodeConstraints, + suffixSatisfaction, + ) + + if terminalCriteriaProjection, err := pgsql.As[pgsql.SelectItem](currentStep.Expansion.TerminalNodeConstraints); err != nil { + return applied, err + } else { + currentStep.Expansion.TerminalNodeSatisfactionProjection = terminalCriteriaProjection + } + + applied++ + } + } + } + + return applied, nil +} + func patternBindingDependsOn(queryPart *QueryPart, part *PatternPart, binding *BoundIdentifier) bool { if queryPart == nil || part == nil || part.PatternBinding == nil || binding == nil { return false From d9218b89b9562e283f21858671b55a1f5e5344a2 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:11:43 -0700 Subject: [PATCH 029/114] feat(pgsql): report fixed-hop expand-into decisions --- cypher/models/pgsql/optimize/lowering_plan.go | 88 ++++++++++++++++++- .../models/pgsql/optimize/optimizer_test.go | 24 +++++ .../pgsql/translate/optimizer_safety_test.go | 13 ++- cypher/models/pgsql/translate/translator.go | 6 ++ 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index b5352666..bdf5e306 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -51,6 +51,7 @@ func appendQueryPartLowerings(plan *LoweringPlan, queryPartIndex int, queryPart appendProjectionPruningDecisions(plan, queryPartIndex, readingClauses, sourceReferences) appendLatePathMaterializationDecisions(plan, queryPartIndex, readingClauses, sourceReferences) + appendExpandIntoDecisions(plan, queryPartIndex, readingClauses) appendExpansionSuffixPushdownDecisions(plan, queryPartIndex, readingClauses) return nil } @@ -145,6 +146,79 @@ func appendLatePathMaterializationDecisions(plan *LoweringPlan, queryPartIndex i } } +func appendExpandIntoDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause) { + declaredSymbols := map[string]struct{}{} + + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + match := readingClause.Match + if match.Optional { + declareMatchSymbols(declaredSymbols, match) + continue + } + + for patternIndex, patternPart := range match.Pattern { + steps := traversalStepsForPattern(patternPart) + declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + + for stepIndex, step := range steps { + if step.Relationship.Range != nil { + continue + } + + leftSymbol := variableSymbol(step.LeftNode.Variable) + rightSymbol := variableSymbol(step.RightNode.Variable) + if leftSymbol == "" || rightSymbol == "" { + continue + } + + if _, leftBound := declaredEndpoints[stepIndex].BeforeLeftNode[leftSymbol]; !leftBound { + continue + } + + if _, rightBound := declaredEndpoints[stepIndex].BeforeRightNode[rightSymbol]; !rightBound { + continue + } + + plan.ExpandInto = append(plan.ExpandInto, ExpandIntoDecision{ + Target: PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }.TraversalStep(stepIndex), + }) + } + + declarePatternSymbols(declaredSymbols, patternPart) + } + + declareWhereSymbols(declaredSymbols, match) + } +} + +type declaredStepEndpoints struct { + BeforeLeftNode map[string]struct{} + BeforeRightNode map[string]struct{} +} + +func declaredSymbolsBeforeStepEndpoints(initial map[string]struct{}, steps []sourceTraversalStep) []declaredStepEndpoints { + declared := copyStringSet(initial) + endpoints := make([]declaredStepEndpoints, len(steps)) + + for idx, step := range steps { + endpoints[idx].BeforeLeftNode = copyStringSet(declared) + addSymbol(declared, variableSymbol(step.LeftNode.Variable)) + addSymbol(declared, variableSymbol(step.Relationship.Variable)) + endpoints[idx].BeforeRightNode = copyStringSet(declared) + addSymbol(declared, variableSymbol(step.RightNode.Variable)) + } + + return endpoints +} + func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause) { declaredSymbols := map[string]struct{}{} @@ -239,10 +313,16 @@ func declarePatternSymbols(declared map[string]struct{}, patternPart *cypher.Pat } addSymbol(declared, variableSymbol(patternPart.Variable)) - for _, step := range traversalStepsForPattern(patternPart) { - addSymbol(declared, variableSymbol(step.LeftNode.Variable)) - addSymbol(declared, variableSymbol(step.Relationship.Variable)) - addSymbol(declared, variableSymbol(step.RightNode.Variable)) + for _, element := range patternPart.PatternElements { + if element == nil { + continue + } + + if nodePattern, isNodePattern := element.AsNodePattern(); isNodePattern { + addSymbol(declared, variableSymbol(nodePattern.Variable)) + } else if relationshipPattern, isRelationshipPattern := element.AsRelationshipPattern(); isRelationshipPattern { + addSymbol(declared, variableSymbol(relationshipPattern.Variable)) + } } } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 1eb6c1c7..b8bbab5d 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -153,6 +153,30 @@ func TestLoweringPlanReportsExpansionSuffixPushdown(t *testing.T) { }}, plan.LoweringPlan.ExpansionSuffixPushdown) } +func TestLoweringPlanReportsExpandInto(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (a:Group) + MATCH (b:Group) + MATCH p = (a)-[:MemberOf]->(b) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringExpandIntoDetection}) + require.Equal(t, []ExpandIntoDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 2, + PatternIndex: 0, + StepIndex: 0, + }, + }}, plan.LoweringPlan.ExpandInto) +} + func TestLoweringPlanSkipsDirectionlessExpansionSuffixPushdown(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index c1373c57..9e0b4586 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -178,16 +178,27 @@ RETURN n, p func TestOptimizerSafetyFixedHopExpandIntoUsesBoundEndpoints(t *testing.T) { t.Parallel() - normalizedQuery := optimizerSafetySQL(t, ` + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` MATCH (a:Group) MATCH (b:Group) MATCH p = (a)-[:MemberOf]->(b) RETURN p `) + require.NoError(t, err) + + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + require.NoError(t, err) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") require.Contains(t, normalizedQuery, "(s1.n0).id = e0.start_id") require.Contains(t, normalizedQuery, "(s1.n1).id = e0.end_id") require.NotContains(t, normalizedQuery, "join node") + require.NotNil(t, translation.Optimization.LoweringPlan) + require.NotEmpty(t, translation.Optimization.LoweringPlan.ExpandInto) + requireOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") } func TestOptimizerSafetyReordersIndependentNodeAnchor(t *testing.T) { diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index ce604822..01b5f4cc 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -35,6 +35,7 @@ type Translator struct { projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision suffixPushdownDecisions map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision + expandIntoDecisions map[optimize.TraversalStepTarget]optimize.ExpandIntoDecision } func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) *Translator { @@ -72,6 +73,7 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.projectionPruningDecisions = map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision{} s.latePathDecisions = map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision{} s.suffixPushdownDecisions = map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision{} + s.expandIntoDecisions = map[optimize.TraversalStepTarget]optimize.ExpandIntoDecision{} for _, decision := range plan.LoweringPlan.ProjectionPruning { s.projectionPruningDecisions[decision.Target] = decision @@ -84,6 +86,10 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { for _, decision := range plan.LoweringPlan.ExpansionSuffixPushdown { s.suffixPushdownDecisions[decision.Target] = append(s.suffixPushdownDecisions[decision.Target], decision) } + + for _, decision := range plan.LoweringPlan.ExpandInto { + s.expandIntoDecisions[decision.Target] = decision + } } func (s *Translator) Enter(expression cypher.SyntaxNode) { From 9cc5887f20f253b01a9ed379db86e528c76b355f Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:12:47 -0700 Subject: [PATCH 030/114] feat(pgsql): wire predicate attachments into lowering metadata --- cypher/models/pgsql/optimize/lowering.go | 5 +- cypher/models/pgsql/optimize/lowering_plan.go | 117 +++++++++++++++++- cypher/models/pgsql/optimize/optimizer.go | 2 +- .../models/pgsql/optimize/optimizer_test.go | 24 ++++ .../pgsql/translate/optimizer_safety_test.go | 2 + 5 files changed, 146 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index c3defda5..aa18c9ac 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -60,8 +60,9 @@ type ExpandIntoDecision struct { } type ExpansionSuffixPushdownDecision struct { - Target TraversalStepTarget `json:"target"` - SuffixLength int `json:"suffix_length"` + Target TraversalStepTarget `json:"target"` + SuffixLength int `json:"suffix_length"` + PredicateAttachments []PredicateAttachment `json:"predicate_attachments,omitempty"` } type PredicatePlacementDecision struct { diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index bdf5e306..ac0ea428 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -11,7 +11,7 @@ type sourceTraversalStep struct { RightNode *cypher.NodePattern } -func BuildLoweringPlan(query *cypher.RegularQuery, _ Analysis) (LoweringPlan, error) { +func BuildLoweringPlan(query *cypher.RegularQuery, _ Analysis, predicateAttachments []PredicateAttachment) (LoweringPlan, error) { if query == nil || query.SingleQuery == nil { return LoweringPlan{}, nil } @@ -40,6 +40,8 @@ func BuildLoweringPlan(query *cypher.RegularQuery, _ Analysis) (LoweringPlan, er } } + appendPredicatePlacementDecisions(&plan, query, predicateAttachments) + attachPredicatePlacementsToSuffixPushdowns(&plan) return plan, nil } @@ -261,6 +263,119 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i } } +type bindingTargetKey struct { + QueryPartIndex int + Symbol string +} + +func appendPredicatePlacementDecisions(plan *LoweringPlan, query *cypher.RegularQuery, predicateAttachments []PredicateAttachment) { + if len(predicateAttachments) == 0 { + return + } + + bindingTargets := indexBindingTargets(query) + for _, attachment := range predicateAttachments { + if attachment.Scope != PredicateAttachmentScopeBinding || len(attachment.BindingSymbols) != 1 { + continue + } + + target, hasTarget := bindingTargets[bindingTargetKey{ + QueryPartIndex: attachment.QueryPartIndex, + Symbol: attachment.BindingSymbols[0], + }] + if !hasTarget { + continue + } + + plan.PredicatePlacement = append(plan.PredicatePlacement, PredicatePlacementDecision{ + Target: target, + Attachment: attachment, + Placement: attachment.Scope, + }) + } +} + +func attachPredicatePlacementsToSuffixPushdowns(plan *LoweringPlan) { + for suffixIdx := range plan.ExpansionSuffixPushdown { + suffix := &plan.ExpansionSuffixPushdown[suffixIdx] + for _, placement := range plan.PredicatePlacement { + if placement.Target.QueryPartIndex != suffix.Target.QueryPartIndex || + placement.Target.ClauseIndex != suffix.Target.ClauseIndex || + placement.Target.PatternIndex != suffix.Target.PatternIndex { + continue + } + + if placement.Target.StepIndex > suffix.Target.StepIndex && + placement.Target.StepIndex <= suffix.Target.StepIndex+suffix.SuffixLength { + suffix.PredicateAttachments = append(suffix.PredicateAttachments, placement.Attachment) + } + } + } +} + +func indexBindingTargets(query *cypher.RegularQuery) map[bindingTargetKey]TraversalStepTarget { + targets := map[bindingTargetKey]TraversalStepTarget{} + + if query == nil || query.SingleQuery == nil { + return targets + } + + if query.SingleQuery.MultiPartQuery != nil { + for queryPartIndex, part := range query.SingleQuery.MultiPartQuery.Parts { + if part == nil { + continue + } + + indexReadingClauseBindingTargets(targets, queryPartIndex, part.ReadingClauses) + } + + if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { + indexReadingClauseBindingTargets(targets, len(query.SingleQuery.MultiPartQuery.Parts), finalPart.ReadingClauses) + } + } else if query.SingleQuery.SinglePartQuery != nil { + indexReadingClauseBindingTargets(targets, 0, query.SingleQuery.SinglePartQuery.ReadingClauses) + } + + return targets +} + +func indexReadingClauseBindingTargets(targets map[bindingTargetKey]TraversalStepTarget, queryPartIndex int, readingClauses []*cypher.ReadingClause) { + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + for patternIndex, patternPart := range readingClause.Match.Pattern { + patternTarget := PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + + for stepIndex, step := range traversalStepsForPattern(patternPart) { + stepTarget := patternTarget.TraversalStep(stepIndex) + setBindingTarget(targets, queryPartIndex, variableSymbol(step.LeftNode.Variable), stepTarget) + setBindingTarget(targets, queryPartIndex, variableSymbol(step.Relationship.Variable), stepTarget) + setBindingTarget(targets, queryPartIndex, variableSymbol(step.RightNode.Variable), stepTarget) + } + } + } +} + +func setBindingTarget(targets map[bindingTargetKey]TraversalStepTarget, queryPartIndex int, symbol string, target TraversalStepTarget) { + if symbol == "" { + return + } + + key := bindingTargetKey{ + QueryPartIndex: queryPartIndex, + Symbol: symbol, + } + if _, exists := targets[key]; !exists { + targets[key] = target + } +} + func expansionSuffixPushdownLength(suffixSteps []sourceTraversalStep, declaredBeforeRightNode []map[string]struct{}) int { var suffixLength int diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index 496c594d..5ab1c93b 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -81,7 +81,7 @@ func (s Optimizer) Optimize(query *cypher.RegularQuery) (Plan, error) { plan.Analysis = Analyze(plan.Query) } - if loweringPlan, err := BuildLoweringPlan(plan.Query, plan.Analysis); err != nil { + if loweringPlan, err := BuildLoweringPlan(plan.Query, plan.Analysis, plan.PredicateAttachments); err != nil { return Plan{}, err } else { plan.LoweringPlan = loweringPlan diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index b8bbab5d..ac6acd3b 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -153,6 +153,30 @@ func TestLoweringPlanReportsExpansionSuffixPushdown(t *testing.T) { }}, plan.LoweringPlan.ExpansionSuffixPushdown) } +func TestLoweringPlanPlacesBindingPredicates(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n:Group)-[:MemberOf*0..]->(m)-[:Enroll]->(ca:EnterpriseCA) + WHERE ca.name = 'target' + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringPredicatePlacement}) + require.Len(t, plan.LoweringPlan.PredicatePlacement, 1) + require.Equal(t, TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 1, + }, plan.LoweringPlan.PredicatePlacement[0].Target) + require.Equal(t, []string{"ca"}, plan.LoweringPlan.PredicatePlacement[0].Attachment.BindingSymbols) + require.Equal(t, []PredicateAttachment{plan.LoweringPlan.PredicatePlacement[0].Attachment}, plan.LoweringPlan.ExpansionSuffixPushdown[0].PredicateAttachments) +} + func TestLoweringPlanReportsExpandInto(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 9e0b4586..b4ee5caa 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -253,9 +253,11 @@ RETURN p require.NotEmpty(t, translation.Optimization.LoweringPlan.ProjectionPruning) require.NotEmpty(t, translation.Optimization.LoweringPlan.LatePathMaterialization) require.NotEmpty(t, translation.Optimization.LoweringPlan.ExpansionSuffixPushdown) + require.NotEmpty(t, translation.Optimization.LoweringPlan.PredicatePlacement) requireOptimizationLowering(t, translation.Optimization, "ProjectionPruning") requireOptimizationLowering(t, translation.Optimization, "LatePathMaterialization") requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") + requireOptimizationLowering(t, translation.Optimization, "PredicatePlacement") } func TestOptimizerSafetyExpansionTerminalPushdownForZeroDepthExpansion(t *testing.T) { From 959fb07b62ddfc62e4c9454d82de16ad96280384 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:13:14 -0700 Subject: [PATCH 031/114] feat(pgsql): prefer optimizer lowering decisions in translator --- cypher/models/pgsql/translate/expansion.go | 3 ++- cypher/models/pgsql/translate/traversal.go | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index cfa1d7a0..a87e0a53 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2654,7 +2654,8 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar traversalStep.Frame.Export(expansionModel.PathBinding.Identifier) if allowProjectionPruning { decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision) + allowFallback := !s.hasOptimizationPlan || !part.HasTarget + pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision, allowFallback) } // Push a new frame that contains currently projected scope from the expansion recursive CTE diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index d3a5b78a..c6812934 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -763,7 +763,7 @@ func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepI return false } -func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool) { +func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) { if hasDecision { if traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.LeftNode, decision) { traversalStep.Frame.Unexport(traversalStep.LeftNode.Identifier) @@ -780,6 +780,10 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart return } + if !allowFallback { + return + } + // Bound endpoints already exist in an outer frame. Only unexport unbound // values that later clauses and continuation steps cannot observe. if !traversalStep.LeftNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.LeftNode) { @@ -802,7 +806,7 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart } } -func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool) { +func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) { if traversalStep == nil || traversalStep.Expansion == nil { return } @@ -819,6 +823,10 @@ func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart return } + if !allowFallback { + return + } + // Variable-length relationship bindings materialize to edge-composite // arrays. A path binding can be rebuilt later from the compact expansion // path ID array, so keep the edge array only when the relationship binding @@ -918,7 +926,8 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision) + allowFallback := !s.hasOptimizationPlan || !part.HasTarget + pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision, allowFallback) } if boundProjections, err := buildVisibleProjections(s.scope); err != nil { From 17fdd8bd2a7bd44d5f9e74b61e6c678713c5bfcb Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 14:13:40 -0700 Subject: [PATCH 032/114] test(pgsql): lock lowering metadata verification --- cmd/benchmark/report_test.go | 18 ++++++++++++++++++ docs/optimization-pass-memory.md | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/cmd/benchmark/report_test.go b/cmd/benchmark/report_test.go index 35820767..5fc2c684 100644 --- a/cmd/benchmark/report_test.go +++ b/cmd/benchmark/report_test.go @@ -29,6 +29,17 @@ import ( func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { distinctRows := int64(2) duplicateRows := int64(0) + loweringPlan := optimize.LoweringPlan{ + ProjectionPruning: []optimize.ProjectionPruningDecision{{ + Target: optimize.TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + ReferencedSymbols: []string{"m"}, + }}, + } report := Report{ Driver: "pg", @@ -50,6 +61,8 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { Name: "ExpansionSuffixPushdown", Applied: true, }}, + Lowerings: loweringPlan.Decisions(), + LoweringPlan: &loweringPlan, }, }, Stats: Stats{ @@ -77,6 +90,11 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { `"optimization": {`, `"name": "ExpansionSuffixPushdown"`, `"applied": true`, + `"lowerings": [`, + `"name": "ProjectionPruning"`, + `"lowering_plan": {`, + `"projection_pruning": [`, + `"referenced_symbols": [`, `"section": "Traversal"`, } { if !strings.Contains(text, expected) { diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index 25056c5c..29f82e4a 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -272,3 +272,9 @@ Phase 10 starts by making local measurements repeatable for the optimizer rules - The benchmark runner rejects zero timed iterations so baseline output cannot silently panic while gathering measurements. - Representative SQL-shape tests assert that suffix-local predicates are inside the pushed suffix check, not merely present somewhere in the rendered SQL. - Broad pass/fail performance thresholds remain deferred. Phase 10 measurements are local evidence and regression artifacts first; cost-based acceptance gates should wait for a larger benchmark corpus and stable environment assumptions. + +## Lowering Ownership Refactor Notes + +The translator now consumes optimizer-owned lowering metadata for projection pruning, late path materialization, fixed-hop expand-into detection, expansion suffix pushdown, and predicate placement. PostgreSQL SQL construction remains in the translator, but rule ownership and benchmark-visible diagnostics live in the optimizer lowering plan. + +Translator-local eligibility checks remain only as conservative fallbacks for untargeted internal patterns. Benchmark JSON includes both named lowerings and the structured lowering plan so future reviews can assert the optimizer decision that caused a SQL shape change. From 77f74dff0ba0c54523fb84dc35b0109d2359471d Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 15:02:17 -0700 Subject: [PATCH 033/114] fix(pgsql): harden optimizer lowering metadata --- cmd/benchmark/report_test.go | 6 +- cypher/models/pgsql/optimize/lowering_plan.go | 2 +- cypher/models/pgsql/optimize/optimizer.go | 2 +- cypher/models/pgsql/translate/expansion.go | 41 ++++--- .../pgsql/translate/optimizer_safety_test.go | 30 +++++- cypher/models/pgsql/translate/translator.go | 7 +- cypher/models/pgsql/translate/traversal.go | 101 +++++++++++------- docs/optimization-pass-memory.md | 4 +- 8 files changed, 130 insertions(+), 63 deletions(-) diff --git a/cmd/benchmark/report_test.go b/cmd/benchmark/report_test.go index 5fc2c684..310bdd00 100644 --- a/cmd/benchmark/report_test.go +++ b/cmd/benchmark/report_test.go @@ -61,7 +61,10 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { Name: "ExpansionSuffixPushdown", Applied: true, }}, - Lowerings: loweringPlan.Decisions(), + PlannedLowerings: loweringPlan.Decisions(), + Lowerings: []optimize.LoweringDecision{{ + Name: "ProjectionPruning", + }}, LoweringPlan: &loweringPlan, }, }, @@ -90,6 +93,7 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { `"optimization": {`, `"name": "ExpansionSuffixPushdown"`, `"applied": true`, + `"planned_lowerings": [`, `"lowerings": [`, `"name": "ProjectionPruning"`, `"lowering_plan": {`, diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index ac0ea428..00643ada 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -11,7 +11,7 @@ type sourceTraversalStep struct { RightNode *cypher.NodePattern } -func BuildLoweringPlan(query *cypher.RegularQuery, _ Analysis, predicateAttachments []PredicateAttachment) (LoweringPlan, error) { +func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []PredicateAttachment) (LoweringPlan, error) { if query == nil || query.SingleQuery == nil { return LoweringPlan{}, nil } diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index 5ab1c93b..b448c7e6 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -81,7 +81,7 @@ func (s Optimizer) Optimize(query *cypher.RegularQuery) (Plan, error) { plan.Analysis = Analyze(plan.Query) } - if loweringPlan, err := BuildLoweringPlan(plan.Query, plan.Analysis, plan.PredicateAttachments); err != nil { + if loweringPlan, err := BuildLoweringPlan(plan.Query, plan.PredicateAttachments); err != nil { return Plan{}, err } else { plan.LoweringPlan = loweringPlan diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index a87e0a53..f47102b5 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -7,6 +7,7 @@ import ( "github.com/specterops/dawgs/cypher/models" "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/specterops/dawgs/cypher/models/pgsql/format" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/cypher/models/pgsql/pgd" "github.com/specterops/dawgs/graph" ) @@ -2295,18 +2296,9 @@ func applyExpansionSuffixPushdown(part *PatternPart) (int, error) { suffixSteps = part.TraversalSteps[idx+1:] ) - if suffixSatisfaction, satisfied := expansionSuffixTerminalSatisfaction(currentStep, suffixSteps); satisfied { - currentStep.Expansion.TerminalNodeConstraints = pgsql.OptionalAnd( - currentStep.Expansion.TerminalNodeConstraints, - suffixSatisfaction, - ) - - if terminalCriteriaProjection, err := pgsql.As[pgsql.SelectItem](currentStep.Expansion.TerminalNodeConstraints); err != nil { - return applied, err - } else { - currentStep.Expansion.TerminalNodeSatisfactionProjection = terminalCriteriaProjection - } - + if candidateApplied, err := applyExpansionSuffixPushdownCandidate(currentStep, suffixSteps); err != nil { + return applied, err + } else if candidateApplied { applied++ } } @@ -2314,6 +2306,25 @@ func applyExpansionSuffixPushdown(part *PatternPart) (int, error) { return applied, nil } +func applyExpansionSuffixPushdownCandidate(currentStep *TraversalStep, suffixSteps []*TraversalStep) (bool, error) { + if suffixSatisfaction, satisfied := expansionSuffixTerminalSatisfaction(currentStep, suffixSteps); satisfied { + currentStep.Expansion.TerminalNodeConstraints = pgsql.OptionalAnd( + currentStep.Expansion.TerminalNodeConstraints, + suffixSatisfaction, + ) + + if terminalCriteriaProjection, err := pgsql.As[pgsql.SelectItem](currentStep.Expansion.TerminalNodeConstraints); err != nil { + return false, err + } else { + currentStep.Expansion.TerminalNodeSatisfactionProjection = terminalCriteriaProjection + } + + return true, nil + } + + return false, nil +} + func suffixEdgeLeftEndpoint(edgeIdentifier pgsql.Identifier, direction graph.Direction) (pgsql.Expression, bool) { switch direction { case graph.DirectionOutbound: @@ -2654,8 +2665,10 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar traversalStep.Frame.Export(expansionModel.PathBinding.Identifier) if allowProjectionPruning { decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - allowFallback := !s.hasOptimizationPlan || !part.HasTarget - pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision, allowFallback) + allowFallback := !hasDecision + if pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision, allowFallback) { + s.recordLowering(optimize.LoweringProjectionPruning) + } } // Push a new frame that contains currently projected scope from the expansion recursive CTE diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index b4ee5caa..01e6724f 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -77,6 +77,26 @@ func requireOptimizationLowering(t *testing.T, summary OptimizationSummary, name require.Failf(t, "missing optimization lowering", "expected lowering %q in %#v", name, summary.Lowerings) } +func requireNoOptimizationLowering(t *testing.T, summary OptimizationSummary, name string) { + t.Helper() + + for _, lowering := range summary.Lowerings { + require.NotEqualf(t, name, lowering.Name, "unexpected applied lowering %q in %#v", name, summary.Lowerings) + } +} + +func requirePlannedOptimizationLowering(t *testing.T, summary OptimizationSummary, name string) { + t.Helper() + + for _, lowering := range summary.PlannedLowerings { + if lowering.Name == name { + return + } + } + + require.Failf(t, "missing planned optimization lowering", "expected planned lowering %q in %#v", name, summary.PlannedLowerings) +} + func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { t.Helper() @@ -198,7 +218,8 @@ RETURN p require.NotContains(t, normalizedQuery, "join node") require.NotNil(t, translation.Optimization.LoweringPlan) require.NotEmpty(t, translation.Optimization.LoweringPlan.ExpandInto) - requireOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") + requirePlannedOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") + requireNoOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") } func TestOptimizerSafetyReordersIndependentNodeAnchor(t *testing.T) { @@ -254,10 +275,13 @@ RETURN p require.NotEmpty(t, translation.Optimization.LoweringPlan.LatePathMaterialization) require.NotEmpty(t, translation.Optimization.LoweringPlan.ExpansionSuffixPushdown) require.NotEmpty(t, translation.Optimization.LoweringPlan.PredicatePlacement) + requirePlannedOptimizationLowering(t, translation.Optimization, "ProjectionPruning") + requirePlannedOptimizationLowering(t, translation.Optimization, "LatePathMaterialization") + requirePlannedOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") + requirePlannedOptimizationLowering(t, translation.Optimization, "PredicatePlacement") requireOptimizationLowering(t, translation.Optimization, "ProjectionPruning") - requireOptimizationLowering(t, translation.Optimization, "LatePathMaterialization") requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") - requireOptimizationLowering(t, translation.Optimization, "PredicatePlacement") + requireNoOptimizationLowering(t, translation.Optimization, "PredicatePlacement") } func TestOptimizerSafetyExpansionTerminalPushdownForZeroDepthExpansion(t *testing.T) { diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 01b5f4cc..c59c4cca 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -30,7 +30,6 @@ type Translator struct { scope *Scope unwindTargets map[*cypher.Variable]struct{} - hasOptimizationPlan bool patternTargets map[*cypher.PatternPart]optimize.PatternTarget projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision @@ -68,7 +67,6 @@ func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters } func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { - s.hasOptimizationPlan = true s.patternTargets = optimize.IndexPatternTargets(plan.Query) s.projectionPruningDecisions = map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision{} s.latePathDecisions = map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision{} @@ -560,6 +558,7 @@ type Result struct { type OptimizationSummary struct { Rules []optimize.RuleResult `json:"rules,omitempty"` PredicateAttachments []optimize.PredicateAttachment `json:"predicate_attachments,omitempty"` + PlannedLowerings []optimize.LoweringDecision `json:"planned_lowerings,omitempty"` Lowerings []optimize.LoweringDecision `json:"lowerings,omitempty"` LoweringPlan *optimize.LoweringPlan `json:"lowering_plan,omitempty"` } @@ -587,9 +586,7 @@ func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper if !optimizedPlan.LoweringPlan.Empty() { loweringPlan := optimizedPlan.LoweringPlan translator.translation.Optimization.LoweringPlan = &loweringPlan - for _, lowering := range loweringPlan.Decisions() { - translator.recordLowering(lowering.Name) - } + translator.translation.Optimization.PlannedLowerings = loweringPlan.Decisions() } if err := walk.Cypher(optimizedPlan.Query, translator); err != nil { diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index c6812934..eccafddb 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -606,7 +606,7 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr if applied, err := s.applyExpansionSuffixPushdown(part); err != nil { return err } else if applied > 0 { - s.recordLowering("ExpansionSuffixPushdown") + s.recordLowering(optimize.LoweringExpansionSuffixPushdown) } if isolatedProjection { @@ -624,25 +624,33 @@ func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error var applied int for stepIndex := range part.TraversalSteps { target := part.Target.TraversalStep(stepIndex) - for _, decision := range s.suffixPushdownDecisions[target] { + decisions := s.suffixPushdownDecisions[target] + if len(decisions) == 0 { + if stepIndex+1 >= len(part.TraversalSteps) { + continue + } + + currentStep := part.TraversalSteps[stepIndex] + suffixSteps := part.TraversalSteps[stepIndex+1:] + if candidateApplied, err := applyExpansionSuffixPushdownCandidate(currentStep, suffixSteps); err != nil { + return applied, err + } else if candidateApplied { + applied++ + } + + continue + } + + for _, decision := range decisions { if decision.SuffixLength <= 0 || stepIndex+decision.SuffixLength >= len(part.TraversalSteps) { continue } currentStep := part.TraversalSteps[stepIndex] suffixSteps := part.TraversalSteps[stepIndex+1 : stepIndex+1+decision.SuffixLength] - if suffixSatisfaction, satisfied := expansionSuffixTerminalSatisfaction(currentStep, suffixSteps); satisfied { - currentStep.Expansion.TerminalNodeConstraints = pgsql.OptionalAnd( - currentStep.Expansion.TerminalNodeConstraints, - suffixSatisfaction, - ) - - if terminalCriteriaProjection, err := pgsql.As[pgsql.SelectItem](currentStep.Expansion.TerminalNodeConstraints); err != nil { - return applied, err - } else { - currentStep.Expansion.TerminalNodeSatisfactionProjection = terminalCriteriaProjection - } - + if candidateApplied, err := applyExpansionSuffixPushdownCandidate(currentStep, suffixSteps); err != nil { + return applied, err + } else if candidateApplied { applied++ } } @@ -763,31 +771,43 @@ func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepI return false } -func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) { +func unexportFrameBinding(frame *Frame, identifier pgsql.Identifier) bool { + if frame == nil { + return false + } + + exported := frame.Exported.Contains(identifier) + frame.Unexport(identifier) + return exported +} + +func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) bool { + var applied bool + if hasDecision { if traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.LeftNode, decision) { - traversalStep.Frame.Unexport(traversalStep.LeftNode.Identifier) + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.LeftNode.Identifier) || applied } if traversalStep.Edge != nil && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.Edge, decision) { - traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied } if traversalStep.RightNode != nil && !traversalStep.RightNodeBound && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.RightNode, decision) { - traversalStep.Frame.Unexport(traversalStep.RightNode.Identifier) + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.RightNode.Identifier) || applied } - return + return applied } if !allowFallback { - return + return false } // Bound endpoints already exist in an outer frame. Only unexport unbound // values that later clauses and continuation steps cannot observe. - if !traversalStep.LeftNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.LeftNode) { - traversalStep.Frame.Unexport(traversalStep.LeftNode.Identifier) + if traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.LeftNode) { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.LeftNode.Identifier) || applied } if traversalStep.Edge != nil && @@ -795,36 +815,40 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart !queryPart.ReferencesBinding(traversalStep.Edge) && patternBindingDependsOn(queryPart, part, traversalStep.Edge) { traversalStep.Edge.DataType = pgsql.PathEdge + applied = true } - if !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.Edge) { - traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + if traversalStep.Edge != nil && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.Edge) { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied } - if !traversalStep.RightNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.RightNode) { - traversalStep.Frame.Unexport(traversalStep.RightNode.Identifier) + if traversalStep.RightNode != nil && !traversalStep.RightNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.RightNode) { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.RightNode.Identifier) || applied } + + return applied } -func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) { +func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) bool { if traversalStep == nil || traversalStep.Expansion == nil { - return + return false } + var applied bool if hasDecision { if traversalStep.Edge != nil && !projectionPruningDecisionReferencesBinding(decision, traversalStep.Edge) { - traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied } if traversalStep.Expansion.PathBinding != nil && !projectionPruningDecisionPatternDependsOn(part, traversalStep.Expansion.PathBinding, decision) { - traversalStep.Frame.Unexport(traversalStep.Expansion.PathBinding.Identifier) + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Expansion.PathBinding.Identifier) || applied } - return + return applied } if !allowFallback { - return + return false } // Variable-length relationship bindings materialize to edge-composite @@ -832,13 +856,15 @@ func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart // path ID array, so keep the edge array only when the relationship binding // itself is observable. if traversalStep.Edge != nil && !queryPart.ReferencesBinding(traversalStep.Edge) { - traversalStep.Frame.Unexport(traversalStep.Edge.Identifier) + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied } pathBinding := traversalStep.Expansion.PathBinding if pathBinding != nil && !patternBindingDependsOn(queryPart, part, pathBinding) { - traversalStep.Frame.Unexport(pathBinding.Identifier) + applied = unexportFrameBinding(traversalStep.Frame, pathBinding.Identifier) || applied } + + return applied } func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *PatternPart, stepIndex int, traversalStep *TraversalStep, allowProjectionPruning bool) error { @@ -923,11 +949,14 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern traversalStep.Edge.DataType == pgsql.EdgeComposite && s.hasLatePathMaterialization(part, stepIndex, optimize.LatePathMaterializationPathEdgeID) { traversalStep.Edge.DataType = pgsql.PathEdge + s.recordLowering(optimize.LoweringLatePathMaterialization) } decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - allowFallback := !s.hasOptimizationPlan || !part.HasTarget - pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision, allowFallback) + allowFallback := !hasDecision + if pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision, allowFallback) { + s.recordLowering(optimize.LoweringProjectionPruning) + } } if boundProjections, err := buildVisibleProjections(s.scope); err != nil { diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index 29f82e4a..9b12efa1 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -259,7 +259,7 @@ The gap-closure pass has been completed enough to return to the original phase s - ADCS path scenarios now record warm-up row count, distinct returned path-row count, and duplicate returned path-row count. - PostgreSQL benchmark runs can opt into `EXPLAIN (ANALYZE, BUFFERS)` capture with `-explain`; JSON output includes the translated SQL and plan text. - The small ADCS integration fixture now asserts exact returned path shape and row count. The larger fanout fixture remains a measurement fixture rather than an exact cardinality oracle. -- Translation metadata reports optimizer rules, predicate attachments, and named lowerings, including `ExpansionSuffixPushdown`. +- Translation metadata reports optimizer rules, predicate attachments, planned lowerings, and applied lowerings, including `ExpansionSuffixPushdown`. - Phase 9 suffix coverage includes zero-hop expansions, fixed suffix chains, suffixes ending at already-bound nodes, inbound suffixes, and the ADCS root-to-domain suffix shape. - Directionless suffix pushdown remains deliberately unimplemented; those suffixes stay as normal translated pattern steps. @@ -277,4 +277,4 @@ Phase 10 starts by making local measurements repeatable for the optimizer rules The translator now consumes optimizer-owned lowering metadata for projection pruning, late path materialization, fixed-hop expand-into detection, expansion suffix pushdown, and predicate placement. PostgreSQL SQL construction remains in the translator, but rule ownership and benchmark-visible diagnostics live in the optimizer lowering plan. -Translator-local eligibility checks remain only as conservative fallbacks for untargeted internal patterns. Benchmark JSON includes both named lowerings and the structured lowering plan so future reviews can assert the optimizer decision that caused a SQL shape change. +Translator-local eligibility checks remain as conservative fallbacks for traversal steps that do not have an optimizer decision. Benchmark JSON includes planned lowerings, applied lowerings, and the structured lowering plan so future reviews can distinguish optimizer intent from SQL-shape changes that actually happened. From a2bab00895ff630135a879c780c67690c3a639e8 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:38:55 -0700 Subject: [PATCH 034/114] docs(pgsql): document lowering metadata contract --- docs/optimization-pass-memory.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md index 9b12efa1..a1237bd3 100644 --- a/docs/optimization-pass-memory.md +++ b/docs/optimization-pass-memory.md @@ -278,3 +278,10 @@ Phase 10 starts by making local measurements repeatable for the optimizer rules The translator now consumes optimizer-owned lowering metadata for projection pruning, late path materialization, fixed-hop expand-into detection, expansion suffix pushdown, and predicate placement. PostgreSQL SQL construction remains in the translator, but rule ownership and benchmark-visible diagnostics live in the optimizer lowering plan. Translator-local eligibility checks remain as conservative fallbacks for traversal steps that do not have an optimizer decision. Benchmark JSON includes planned lowerings, applied lowerings, and the structured lowering plan so future reviews can distinguish optimizer intent from SQL-shape changes that actually happened. + +### Lowering Metadata Contract + +- `LoweringPlan` is optimizer intent over the Cypher source shape. It may include decisions that the PostgreSQL translator later declines because the lowered SQL shape is no longer eligible. +- `planned_lowerings` is the compact, benchmark-friendly view of `LoweringPlan.Decisions()`. +- `lowerings` is translator-applied behavior only. It must be recorded when SQL generation actually changes shape, not when the optimizer merely planned a decision. +- Optimizer code may describe source-level actions and eligibility. PostgreSQL frame visibility, data-type rewrites, and SQL AST construction remain translator responsibilities. From 901b17835f53b317ae46ee1c741e259e2893cd00 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:39:33 -0700 Subject: [PATCH 035/114] feat(pgsql): consume expand-into lowering decisions --- cypher/models/pgsql/translate/model.go | 1 + .../pgsql/translate/optimizer_safety_test.go | 2 +- cypher/models/pgsql/translate/traversal.go | 24 ++++++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index c7403c23..cfafa79a 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -532,6 +532,7 @@ type TraversalStep struct { PathReversed bool LeftNode *BoundIdentifier LeftNodeBound bool + UseExpandInto bool LeftNodeConstraints pgsql.Expression LeftNodeJoinCondition pgsql.Expression Edge *BoundIdentifier diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 01e6724f..7835abe6 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -219,7 +219,7 @@ RETURN p require.NotNil(t, translation.Optimization.LoweringPlan) require.NotEmpty(t, translation.Optimization.LoweringPlan.ExpandInto) requirePlannedOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") - requireNoOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") + requireOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") } func TestOptimizerSafetyReordersIndependentNodeAnchor(t *testing.T) { diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index eccafddb..61193439 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -28,6 +28,20 @@ func boundEndpointInequality(frame *Frame, traversalStep *TraversalStep) pgsql.E ) } +func (s *Translator) shouldUseExpandInto(part *PatternPart, stepIndex int, traversalStep *TraversalStep) bool { + if traversalStep == nil || traversalStep.Expansion != nil || !traversalStep.LeftNodeBound || !traversalStep.RightNodeBound { + return false + } + + if part != nil && part.HasTarget { + if _, hasDecision := s.expandIntoDecisions[part.Target.TraversalStep(stepIndex)]; hasDecision { + return true + } + } + + return true +} + func (s *Translator) buildBoundEndpointTraversalPattern(partFrame *Frame, traversalStep *TraversalStep) (pgsql.Query, error) { if partFrame == nil || partFrame.Previous == nil { return pgsql.Query{}, errors.New("expected previous frame for bound endpoint traversal") @@ -72,7 +86,7 @@ func (s *Translator) buildBoundEndpointTraversalPattern(partFrame *Frame, traver } func (s *Translator) buildDirectionlessTraversalPatternRoot(traversalStep *TraversalStep) (pgsql.Query, error) { - if traversalStep.LeftNodeBound && traversalStep.RightNodeBound { + if traversalStep.UseExpandInto { return s.buildBoundEndpointTraversalPattern(traversalStep.Frame, traversalStep) } @@ -324,7 +338,7 @@ func (s *Translator) buildTraversalPatternRoot(partFrame *Frame, traversalStep * return s.buildDirectionlessTraversalPatternRoot(traversalStep) } - if traversalStep.LeftNodeBound && traversalStep.RightNodeBound { + if traversalStep.UseExpandInto { return s.buildBoundEndpointTraversalPattern(partFrame, traversalStep) } @@ -510,7 +524,7 @@ func (s *Translator) buildTraversalPatternRoot(partFrame *Frame, traversalStep * } func (s *Translator) buildTraversalPatternStep(partFrame *Frame, traversalStep *TraversalStep) (pgsql.Query, error) { - if traversalStep.LeftNodeBound && traversalStep.RightNodeBound { + if traversalStep.UseExpandInto { return s.buildBoundEndpointTraversalPattern(partFrame, traversalStep) } @@ -585,6 +599,10 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr } for idx, traversalStep := range part.TraversalSteps { + if traversalStep.UseExpandInto = s.shouldUseExpandInto(part, idx, traversalStep); traversalStep.UseExpandInto { + s.recordLowering(optimize.LoweringExpandIntoDetection) + } + if traversalStepFrame, err := s.scope.PushFrame(); err != nil { return err } else { From 778915708c8ae3789e8c3b9e1161089f2eb21256 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:40:36 -0700 Subject: [PATCH 036/114] refactor(pgsql): lift projection pruning binding actions --- cypher/models/pgsql/optimize/lowering.go | 4 ++ cypher/models/pgsql/optimize/lowering_plan.go | 11 ++-- .../models/pgsql/optimize/optimizer_test.go | 2 + cypher/models/pgsql/translate/traversal.go | 61 ++----------------- 4 files changed, 18 insertions(+), 60 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index aa18c9ac..f1621d7e 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -40,6 +40,10 @@ type ProjectionPruningDecision struct { Target TraversalStepTarget `json:"target"` ReferencedSymbols []string `json:"referenced_symbols,omitempty"` PatternBindingReferenced bool `json:"pattern_binding_referenced,omitempty"` + OmitLeftNode bool `json:"omit_left_node,omitempty"` + OmitRelationship bool `json:"omit_relationship,omitempty"` + OmitRightNode bool `json:"omit_right_node,omitempty"` + OmitPathBinding bool `json:"omit_path_binding,omitempty"` } type LatePathMaterializationMode string diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 00643ada..78a9bffa 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -92,14 +92,17 @@ func appendPatternProjectionPruningDecisions(plan *LoweringPlan, target PatternT edgeReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.Relationship.Variable)) var hasPruning bool if step.Relationship.Range != nil { - hasPruning = !edgeReferenced || !pathReferenced + decision.OmitRelationship = !edgeReferenced + decision.OmitPathBinding = !pathReferenced + hasPruning = decision.OmitRelationship || decision.OmitPathBinding } else { leftReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.LeftNode.Variable)) rightReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.RightNode.Variable)) - hasPruning = !(leftReferenced || pathReferenced) || - !(edgeReferenced || pathReferenced) || - !(rightReferenced || pathReferenced || stepIndex+1 < len(steps)) + decision.OmitLeftNode = !(leftReferenced || pathReferenced) + decision.OmitRelationship = !(edgeReferenced || pathReferenced) + decision.OmitRightNode = !(rightReferenced || pathReferenced || stepIndex+1 < len(steps)) + hasPruning = decision.OmitLeftNode || decision.OmitRelationship || decision.OmitRightNode } if hasPruning { diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index ac6acd3b..3d2ac841 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -87,6 +87,8 @@ func TestLoweringPlanReportsProjectionPruning(t *testing.T) { StepIndex: 0, }, ReferencedSymbols: []string{"m"}, + OmitLeftNode: true, + OmitRelationship: true, }}, plan.LoweringPlan.ProjectionPruning) } diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 61193439..72303ae6 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/specterops/dawgs/cypher/models" - "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/graph" @@ -718,56 +717,6 @@ func (s *Translator) hasLatePathMaterialization(part *PatternPart, stepIndex int return false } -func projectionPruningDecisionReferencesBinding(decision optimize.ProjectionPruningDecision, binding *BoundIdentifier) bool { - if binding == nil { - return false - } - - sourceIdentifier := binding.Identifier - if binding.Alias.Set { - sourceIdentifier = binding.Alias.Value - } - - for _, symbol := range decision.ReferencedSymbols { - if symbol == cypher.TokenLiteralAsterisk || pgsql.Identifier(symbol) == sourceIdentifier { - return true - } - } - - return false -} - -func projectionPruningDecisionPatternDependsOn(part *PatternPart, binding *BoundIdentifier, decision optimize.ProjectionPruningDecision) bool { - if !decision.PatternBindingReferenced || part == nil || part.PatternBinding == nil || binding == nil { - return false - } - - for _, dependency := range part.PatternBinding.Dependencies { - if dependency.Identifier == binding.Identifier { - return true - } - } - - return false -} - -func traversalStepProjectsBindingByDecision(part *PatternPart, stepIndex int, binding *BoundIdentifier, decision optimize.ProjectionPruningDecision) bool { - if binding == nil { - return false - } - - if projectionPruningDecisionReferencesBinding(decision, binding) || projectionPruningDecisionPatternDependsOn(part, binding, decision) { - return true - } - - if stepIndex+1 < len(part.TraversalSteps) { - nextStep := part.TraversalSteps[stepIndex+1] - return nextStep.LeftNode != nil && nextStep.LeftNode.Identifier == binding.Identifier - } - - return false -} - func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepIndex int, binding *BoundIdentifier) bool { if binding == nil { return false @@ -803,15 +752,15 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart var applied bool if hasDecision { - if traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.LeftNode, decision) { + if decision.OmitLeftNode && traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound { applied = unexportFrameBinding(traversalStep.Frame, traversalStep.LeftNode.Identifier) || applied } - if traversalStep.Edge != nil && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.Edge, decision) { + if decision.OmitRelationship && traversalStep.Edge != nil { applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied } - if traversalStep.RightNode != nil && !traversalStep.RightNodeBound && !traversalStepProjectsBindingByDecision(part, stepIndex, traversalStep.RightNode, decision) { + if decision.OmitRightNode && traversalStep.RightNode != nil && !traversalStep.RightNodeBound { applied = unexportFrameBinding(traversalStep.Frame, traversalStep.RightNode.Identifier) || applied } @@ -854,11 +803,11 @@ func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart var applied bool if hasDecision { - if traversalStep.Edge != nil && !projectionPruningDecisionReferencesBinding(decision, traversalStep.Edge) { + if decision.OmitRelationship && traversalStep.Edge != nil { applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied } - if traversalStep.Expansion.PathBinding != nil && !projectionPruningDecisionPatternDependsOn(part, traversalStep.Expansion.PathBinding, decision) { + if decision.OmitPathBinding && traversalStep.Expansion.PathBinding != nil { applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Expansion.PathBinding.Identifier) || applied } From 4f9ecfa8195e8e21f53c6b0f820ad51d147bbdac Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:41:18 -0700 Subject: [PATCH 037/114] feat(pgsql): apply late materialization decisions explicitly --- cypher/models/pgsql/translate/expansion.go | 5 ++++ .../pgsql/translate/optimizer_safety_test.go | 1 + cypher/models/pgsql/translate/traversal.go | 28 +++++++++++++------ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index f47102b5..c0632fbb 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2669,6 +2669,11 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar if pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision, allowFallback) { s.recordLowering(optimize.LoweringProjectionPruning) } + + if _, hasDecision := s.latePathMaterializationDecision(part, stepIndex, optimize.LatePathMaterializationExpansionPath); hasDecision && + traversalStep.Frame.Exported.Contains(expansionModel.PathBinding.Identifier) { + s.recordLowering(optimize.LoweringLatePathMaterialization) + } } // Push a new frame that contains currently projected scope from the expansion recursive CTE diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 7835abe6..095bbef0 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -280,6 +280,7 @@ RETURN p requirePlannedOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") requirePlannedOptimizationLowering(t, translation.Optimization, "PredicatePlacement") requireOptimizationLowering(t, translation.Optimization, "ProjectionPruning") + requireOptimizationLowering(t, translation.Optimization, "LatePathMaterialization") requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") requireNoOptimizationLowering(t, translation.Optimization, "PredicatePlacement") } diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 72303ae6..7c1c13f0 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -703,18 +703,33 @@ func (s *Translator) projectionPruningDecision(part *PatternPart, stepIndex int) return decision, hasDecision } -func (s *Translator) hasLatePathMaterialization(part *PatternPart, stepIndex int, mode optimize.LatePathMaterializationMode) bool { +func (s *Translator) latePathMaterializationDecision(part *PatternPart, stepIndex int, mode optimize.LatePathMaterializationMode) (optimize.LatePathMaterializationDecision, bool) { if part == nil || !part.HasTarget { - return false + return optimize.LatePathMaterializationDecision{}, false } for _, decision := range s.latePathDecisions[part.Target.TraversalStep(stepIndex)] { if decision.Mode == mode { - return true + return decision, true } } - return false + return optimize.LatePathMaterializationDecision{}, false +} + +func (s *Translator) applyPathEdgeIDMaterialization(part *PatternPart, stepIndex int, traversalStep *TraversalStep) bool { + if traversalStep == nil || + traversalStep.Edge == nil || + traversalStep.Edge.DataType != pgsql.EdgeComposite { + return false + } + + if _, hasDecision := s.latePathMaterializationDecision(part, stepIndex, optimize.LatePathMaterializationPathEdgeID); !hasDecision { + return false + } + + traversalStep.Edge.DataType = pgsql.PathEdge + return true } func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepIndex int, binding *BoundIdentifier) bool { @@ -912,10 +927,7 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } if allowProjectionPruning { - if traversalStep.Edge != nil && - traversalStep.Edge.DataType == pgsql.EdgeComposite && - s.hasLatePathMaterialization(part, stepIndex, optimize.LatePathMaterializationPathEdgeID) { - traversalStep.Edge.DataType = pgsql.PathEdge + if s.applyPathEdgeIDMaterialization(part, stepIndex, traversalStep) { s.recordLowering(optimize.LoweringLatePathMaterialization) } From 9eded59f75146a797202b8c99fe553b7204dee26 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:41:45 -0700 Subject: [PATCH 038/114] feat(pgsql): record consumed predicate placements --- cypher/models/pgsql/translate/optimizer_safety_test.go | 2 +- cypher/models/pgsql/translate/traversal.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 095bbef0..9a2dbf8b 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -282,7 +282,7 @@ RETURN p requireOptimizationLowering(t, translation.Optimization, "ProjectionPruning") requireOptimizationLowering(t, translation.Optimization, "LatePathMaterialization") requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") - requireNoOptimizationLowering(t, translation.Optimization, "PredicatePlacement") + requireOptimizationLowering(t, translation.Optimization, "PredicatePlacement") } func TestOptimizerSafetyExpansionTerminalPushdownForZeroDepthExpansion(t *testing.T) { diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 7c1c13f0..5fe8e215 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -668,6 +668,10 @@ func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error if candidateApplied, err := applyExpansionSuffixPushdownCandidate(currentStep, suffixSteps); err != nil { return applied, err } else if candidateApplied { + if len(decision.PredicateAttachments) > 0 { + s.recordLowering(optimize.LoweringPredicatePlacement) + } + applied++ } } From 793b96764d92985bd3dd6d5a4f724c643138aef0 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:42:31 -0700 Subject: [PATCH 039/114] feat(pgsql): carry suffix pushdown source spans --- cypher/models/pgsql/optimize/lowering.go | 2 ++ cypher/models/pgsql/optimize/lowering_plan.go | 4 +++- cypher/models/pgsql/optimize/optimizer_test.go | 4 +++- cypher/models/pgsql/translate/traversal.go | 8 ++++++-- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index f1621d7e..1706f9ad 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -66,6 +66,8 @@ type ExpandIntoDecision struct { type ExpansionSuffixPushdownDecision struct { Target TraversalStepTarget `json:"target"` SuffixLength int `json:"suffix_length"` + SuffixStartStep int `json:"suffix_start_step"` + SuffixEndStep int `json:"suffix_end_step"` PredicateAttachments []PredicateAttachment `json:"predicate_attachments,omitempty"` } diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 78a9bffa..c86470fc 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -254,7 +254,9 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i ClauseIndex: clauseIndex, PatternIndex: patternIndex, }.TraversalStep(stepIndex), - SuffixLength: suffixLength, + SuffixLength: suffixLength, + SuffixStartStep: stepIndex + 1, + SuffixEndStep: stepIndex + suffixLength, }) } } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 3d2ac841..28784b8d 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -151,7 +151,9 @@ func TestLoweringPlanReportsExpansionSuffixPushdown(t *testing.T) { PatternIndex: 0, StepIndex: 0, }, - SuffixLength: 1, + SuffixLength: 1, + SuffixStartStep: 1, + SuffixEndStep: 1, }}, plan.LoweringPlan.ExpansionSuffixPushdown) } diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 5fe8e215..3da01fab 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -659,12 +659,16 @@ func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error } for _, decision := range decisions { - if decision.SuffixLength <= 0 || stepIndex+decision.SuffixLength >= len(part.TraversalSteps) { + if decision.SuffixLength <= 0 || + decision.SuffixStartStep <= stepIndex || + decision.SuffixEndStep < decision.SuffixStartStep || + decision.SuffixEndStep >= len(part.TraversalSteps) || + decision.SuffixEndStep-decision.SuffixStartStep+1 != decision.SuffixLength { continue } currentStep := part.TraversalSteps[stepIndex] - suffixSteps := part.TraversalSteps[stepIndex+1 : stepIndex+1+decision.SuffixLength] + suffixSteps := part.TraversalSteps[decision.SuffixStartStep : decision.SuffixEndStep+1] if candidateApplied, err := applyExpansionSuffixPushdownCandidate(currentStep, suffixSteps); err != nil { return applied, err } else if candidateApplied { From 888478a7e12ea461feb681f73c3b83b9f36951e7 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:45:41 -0700 Subject: [PATCH 040/114] refactor(pgsql): remove targeted lowering fallbacks --- .../models/pgsql/optimize/optimizer_test.go | 23 +++++ .../pgsql/optimize/source_references.go | 5 ++ cypher/models/pgsql/translate/expansion.go | 2 +- cypher/models/pgsql/translate/model.go | 8 ++ cypher/models/pgsql/translate/traversal.go | 88 +++++++++++++------ 5 files changed, 97 insertions(+), 29 deletions(-) diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 28784b8d..cc1780c4 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -92,6 +92,29 @@ func TestLoweringPlanReportsProjectionPruning(t *testing.T) { }}, plan.LoweringPlan.ProjectionPruning) } +func TestLoweringPlanProjectionPruningKeepsUpdateTargets(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (a)-[r:MemberOf]->(m) + SET a.name = 'updated', r.seen = true + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Equal(t, []ProjectionPruningDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + ReferencedSymbols: []string{"a", "r"}, + OmitRightNode: true, + }}, plan.LoweringPlan.ProjectionPruning) +} + func TestLoweringPlanReportsLatePathMaterialization(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/optimize/source_references.go b/cypher/models/pgsql/optimize/source_references.go index ebdbbe14..bb12ab86 100644 --- a/cypher/models/pgsql/optimize/source_references.go +++ b/cypher/models/pgsql/optimize/source_references.go @@ -77,6 +77,11 @@ func (s *sourceReferenceCollector) Enter(node cypher.SyntaxNode) { s.addMatchPatternDeclaration(typedNode.Variable) } + case *cypher.PropertyLookup: + if variable, isVariable := typedNode.Atom.(*cypher.Variable); isVariable { + s.addVariable(variable) + } + case *cypher.Variable: s.addVariable(typedNode) } diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index c0632fbb..9b26c76c 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2665,7 +2665,7 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar traversalStep.Frame.Export(expansionModel.PathBinding.Identifier) if allowProjectionPruning { decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - allowFallback := !hasDecision + allowFallback := !hasDecision && (part == nil || !part.HasTarget) if pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision, allowFallback) { s.recordLowering(optimize.LoweringProjectionPruning) } diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index cfafa79a..ca16e2d6 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -525,11 +525,19 @@ func partitionConstraintByLocality(expression pgsql.Expression, localScope *pgsq return joinConstraints, whereConstraints } +type ProjectionPruningApplication struct { + LeftNode *BoundIdentifier + Relationship *BoundIdentifier + RightNode *BoundIdentifier + PathBinding *BoundIdentifier +} + type TraversalStep struct { Frame *Frame Direction graph.Direction Expansion *Expansion PathReversed bool + ProjectionPruning ProjectionPruningApplication LeftNode *BoundIdentifier LeftNodeBound bool UseExpandInto bool diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 3da01fab..2e02e250 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -36,6 +36,8 @@ func (s *Translator) shouldUseExpandInto(part *PatternPart, stepIndex int, trave if _, hasDecision := s.expandIntoDecisions[part.Target.TraversalStep(stepIndex)]; hasDecision { return true } + + return false } return true @@ -602,6 +604,8 @@ func (s *Translator) translateTraversalPatternPart(part *PatternPart, isolatedPr s.recordLowering(optimize.LoweringExpandIntoDetection) } + s.prepareProjectionPruning(part, idx, traversalStep) + if traversalStepFrame, err := s.scope.PushFrame(); err != nil { return err } else { @@ -643,18 +647,6 @@ func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error target := part.Target.TraversalStep(stepIndex) decisions := s.suffixPushdownDecisions[target] if len(decisions) == 0 { - if stepIndex+1 >= len(part.TraversalSteps) { - continue - } - - currentStep := part.TraversalSteps[stepIndex] - suffixSteps := part.TraversalSteps[stepIndex+1:] - if candidateApplied, err := applyExpansionSuffixPushdownCandidate(currentStep, suffixSteps); err != nil { - return applied, err - } else if candidateApplied { - applied++ - } - continue } @@ -711,6 +703,29 @@ func (s *Translator) projectionPruningDecision(part *PatternPart, stepIndex int) return decision, hasDecision } +func (s *Translator) prepareProjectionPruning(part *PatternPart, stepIndex int, traversalStep *TraversalStep) { + decision, hasDecision := s.projectionPruningDecision(part, stepIndex) + if !hasDecision || traversalStep == nil { + return + } + + if decision.OmitLeftNode { + traversalStep.ProjectionPruning.LeftNode = traversalStep.LeftNode + } + + if decision.OmitRelationship { + traversalStep.ProjectionPruning.Relationship = traversalStep.Edge + } + + if decision.OmitRightNode { + traversalStep.ProjectionPruning.RightNode = traversalStep.RightNode + } + + if decision.OmitPathBinding && traversalStep.Expansion != nil { + traversalStep.ProjectionPruning.PathBinding = traversalStep.Expansion.PathBinding + } +} + func (s *Translator) latePathMaterializationDecision(part *PatternPart, stepIndex int, mode optimize.LatePathMaterializationMode) (optimize.LatePathMaterializationDecision, bool) { if part == nil || !part.HasTarget { return optimize.LatePathMaterializationDecision{}, false @@ -771,22 +786,39 @@ func unexportFrameBinding(frame *Frame, identifier pgsql.Identifier) bool { return exported } +func traversalStepBindingBound(traversalStep *TraversalStep, binding *BoundIdentifier) bool { + if traversalStep == nil || binding == nil { + return false + } + + if traversalStep.LeftNode == binding { + return traversalStep.LeftNodeBound + } + + if traversalStep.RightNode == binding { + return traversalStep.RightNodeBound + } + + return false +} + +func unexportPrunedNodeBinding(traversalStep *TraversalStep, binding *BoundIdentifier) bool { + if binding == nil || traversalStepBindingBound(traversalStep, binding) { + return false + } + + return unexportFrameBinding(traversalStep.Frame, binding.Identifier) +} + func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) bool { var applied bool if hasDecision { - if decision.OmitLeftNode && traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.LeftNode.Identifier) || applied + applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.LeftNode) || applied + if traversalStep.ProjectionPruning.Relationship != nil { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied } - - if decision.OmitRelationship && traversalStep.Edge != nil { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied - } - - if decision.OmitRightNode && traversalStep.RightNode != nil && !traversalStep.RightNodeBound { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.RightNode.Identifier) || applied - } - + applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.RightNode) || applied return applied } @@ -826,12 +858,12 @@ func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart var applied bool if hasDecision { - if decision.OmitRelationship && traversalStep.Edge != nil { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied + if traversalStep.ProjectionPruning.Relationship != nil { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied } - if decision.OmitPathBinding && traversalStep.Expansion.PathBinding != nil { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Expansion.PathBinding.Identifier) || applied + if traversalStep.ProjectionPruning.PathBinding != nil { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.PathBinding.Identifier) || applied } return applied @@ -940,7 +972,7 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - allowFallback := !hasDecision + allowFallback := !hasDecision && (part == nil || !part.HasTarget) if pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision, allowFallback) { s.recordLowering(optimize.LoweringProjectionPruning) } From 441e7ce65f206266179eadbfdba6756f7967f7ed Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 20:49:14 -0700 Subject: [PATCH 041/114] feat(pgsql): plan expand-into for anonymous continuations --- cypher/models/pgsql/optimize/lowering_plan.go | 11 +++++----- .../models/pgsql/optimize/optimizer_test.go | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index c86470fc..16d71bd3 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -176,15 +176,14 @@ func appendExpandIntoDecisions(plan *LoweringPlan, queryPartIndex int, readingCl leftSymbol := variableSymbol(step.LeftNode.Variable) rightSymbol := variableSymbol(step.RightNode.Variable) - if leftSymbol == "" || rightSymbol == "" { - continue - } + _, leftBound := declaredEndpoints[stepIndex].BeforeLeftNode[leftSymbol] + _, rightBound := declaredEndpoints[stepIndex].BeforeRightNode[rightSymbol] - if _, leftBound := declaredEndpoints[stepIndex].BeforeLeftNode[leftSymbol]; !leftBound { - continue + if leftSymbol == "" { + leftBound = stepIndex > 0 } - if _, rightBound := declaredEndpoints[stepIndex].BeforeRightNode[rightSymbol]; !rightBound { + if rightSymbol == "" || !leftBound || !rightBound { continue } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index cc1780c4..c3efd4d1 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -228,6 +228,28 @@ func TestLoweringPlanReportsExpandInto(t *testing.T) { }}, plan.LoweringPlan.ExpandInto) } +func TestLoweringPlanReportsExpandIntoForAnonymousContinuationEndpoint(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (d:Domain) + MATCH p = (ca:EnterpriseCA)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.ExpandInto, ExpandIntoDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 1, + }, + }) +} + func TestLoweringPlanSkipsDirectionlessExpansionSuffixPushdown(t *testing.T) { t.Parallel() From e58de1ea84a5c7f28b4147918922c1937307585b Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 22:38:24 -0700 Subject: [PATCH 042/114] refactor(pgsql): lift rewrite decisions into optimizer plan --- cypher/models/cypher/copy.go | 3 + cypher/models/cypher/copy_test.go | 1 + cypher/models/cypher/model.go | 10 + cypher/models/pgsql/optimize/lowering.go | 60 +++ cypher/models/pgsql/optimize/lowering_plan.go | 433 +++++++++++++++++- .../models/pgsql/optimize/optimizer_test.go | 246 ++++++++++ .../pgsql/test/translation_cases/create.sql | 2 +- .../pgsql/test/translation_cases/delete.sql | 3 +- .../test/translation_cases/multipart.sql | 24 +- .../pgsql/test/translation_cases/nodes.sql | 2 +- .../translation_cases/pattern_binding.sql | 52 +-- .../translation_cases/pattern_expansion.sql | 30 +- .../translation_cases/pattern_rewriting.sql | 1 - .../test/translation_cases/quantifiers.sql | 1 + .../test/translation_cases/shortest_paths.sql | 38 +- .../translation_cases/stepwise_traversal.sql | 12 +- .../pgsql/test/translation_cases/update.sql | 2 +- cypher/models/pgsql/translate/constraints.go | 13 +- cypher/models/pgsql/translate/expansion.go | 400 ++++++++++++++-- .../pgsql/translate/limit_pushdown_test.go | 1 + cypher/models/pgsql/translate/model.go | 40 ++ .../pgsql/translate/optimizer_safety_test.go | 140 +++++- .../models/pgsql/translate/path_functions.go | 18 + cypher/models/pgsql/translate/pattern.go | 6 +- cypher/models/pgsql/translate/projection.go | 85 +++- cypher/models/pgsql/translate/translator.go | 41 +- cypher/models/pgsql/translate/traversal.go | 214 ++++++++- cypher/models/pgsql/translate/with.go | 8 + cypher/models/walk/walk_pgsql.go | 12 + .../testdata/cases/aggregation_inline.json | 19 + .../testdata/cases/expansion_inline.json | 70 +++ .../testdata/cases/multipart_inline.json | 34 ++ .../testdata/cases/optimizer_inline.json | 136 ++++++ integration/testdata/cases/unwind_inline.json | 18 + 34 files changed, 2012 insertions(+), 163 deletions(-) diff --git a/cypher/models/cypher/copy.go b/cypher/models/cypher/copy.go index d08b7c6c..47cbb6d8 100644 --- a/cypher/models/cypher/copy.go +++ b/cypher/models/cypher/copy.go @@ -53,6 +53,9 @@ func Copy[T any](value T, extensions ...CopyExtension[T]) T { case *Quantifier: return any(typedValue.copy()).(T) + case *RangeQuantifier: + return any(typedValue.copy()).(T) + case *Where: return any(typedValue.copy()).(T) diff --git a/cypher/models/cypher/copy_test.go b/cypher/models/cypher/copy_test.go index d6d2b7fa..ee65ff2a 100644 --- a/cypher/models/cypher/copy_test.go +++ b/cypher/models/cypher/copy_test.go @@ -63,6 +63,7 @@ func TestCopy(t *testing.T) { validateCopy(t, &model2.IDInCollection{}) validateCopy(t, &model2.FilterExpression{}) validateCopy(t, &model2.Quantifier{}) + validateCopy(t, &model2.RangeQuantifier{Value: "*"}) validateCopy(t, &model2.MultiPartQueryPart{}) validateCopy(t, &model2.Remove{}) diff --git a/cypher/models/cypher/model.go b/cypher/models/cypher/model.go index b6ef80bf..b3a59b77 100644 --- a/cypher/models/cypher/model.go +++ b/cypher/models/cypher/model.go @@ -743,6 +743,16 @@ func NewRangeQuantifier(value string) *RangeQuantifier { } } +func (s *RangeQuantifier) copy() *RangeQuantifier { + if s == nil { + return s + } + + return &RangeQuantifier{ + Value: s.Value, + } +} + type KindMatcher struct { Reference Expression Kinds graph.Kinds diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index 1706f9ad..be5083e7 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -6,6 +6,10 @@ const ( LoweringProjectionPruning = "ProjectionPruning" LoweringLatePathMaterialization = "LatePathMaterialization" LoweringExpandIntoDetection = "ExpandIntoDetection" + LoweringTraversalDirection = "TraversalDirectionSelection" + LoweringShortestPathStrategy = "ShortestPathStrategySelection" + LoweringShortestPathFilter = "ShortestPathFilterMaterialization" + LoweringLimitPushdown = "LimitPushdown" LoweringExpansionSuffixPushdown = "ExpansionSuffixPushdown" LoweringPredicatePlacement = "PredicatePlacement" ) @@ -63,6 +67,50 @@ type ExpandIntoDecision struct { Target TraversalStepTarget `json:"target"` } +type TraversalDirectionDecision struct { + Target TraversalStepTarget `json:"target"` + Flip bool `json:"flip,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type ShortestPathStrategy string + +const ( + ShortestPathStrategyBidirectional ShortestPathStrategy = "bidirectional" + ShortestPathStrategyUnidirectional ShortestPathStrategy = "unidirectional" +) + +type ShortestPathStrategyDecision struct { + Target TraversalStepTarget `json:"target"` + Strategy ShortestPathStrategy `json:"strategy"` + Reason string `json:"reason,omitempty"` +} + +type ShortestPathFilterMode string + +const ( + ShortestPathFilterTerminal ShortestPathFilterMode = "terminal" + ShortestPathFilterEndpointPair ShortestPathFilterMode = "endpoint_pair" +) + +type ShortestPathFilterDecision struct { + Target TraversalStepTarget `json:"target"` + Mode ShortestPathFilterMode `json:"mode"` + Reason string `json:"reason,omitempty"` +} + +type LimitPushdownMode string + +const ( + LimitPushdownTraversalCTE LimitPushdownMode = "traversal_cte" + LimitPushdownShortestPathHarness LimitPushdownMode = "shortest_path_harness" +) + +type LimitPushdownDecision struct { + Target TraversalStepTarget `json:"target"` + Mode LimitPushdownMode `json:"mode"` +} + type ExpansionSuffixPushdownDecision struct { Target TraversalStepTarget `json:"target"` SuffixLength int `json:"suffix_length"` @@ -81,6 +129,10 @@ type LoweringPlan struct { ProjectionPruning []ProjectionPruningDecision `json:"projection_pruning,omitempty"` LatePathMaterialization []LatePathMaterializationDecision `json:"late_path_materialization,omitempty"` ExpandInto []ExpandIntoDecision `json:"expand_into,omitempty"` + TraversalDirection []TraversalDirectionDecision `json:"traversal_direction,omitempty"` + ShortestPathStrategy []ShortestPathStrategyDecision `json:"shortest_path_strategy,omitempty"` + ShortestPathFilter []ShortestPathFilterDecision `json:"shortest_path_filter,omitempty"` + LimitPushdown []LimitPushdownDecision `json:"limit_pushdown,omitempty"` ExpansionSuffixPushdown []ExpansionSuffixPushdownDecision `json:"expansion_suffix_pushdown,omitempty"` PredicatePlacement []PredicatePlacementDecision `json:"predicate_placement,omitempty"` } @@ -89,6 +141,10 @@ func (s LoweringPlan) Empty() bool { return len(s.ProjectionPruning) == 0 && len(s.LatePathMaterialization) == 0 && len(s.ExpandInto) == 0 && + len(s.TraversalDirection) == 0 && + len(s.ShortestPathStrategy) == 0 && + len(s.ShortestPathFilter) == 0 && + len(s.LimitPushdown) == 0 && len(s.ExpansionSuffixPushdown) == 0 && len(s.PredicatePlacement) == 0 } @@ -104,6 +160,10 @@ func (s LoweringPlan) Decisions() []LoweringDecision { add(LoweringProjectionPruning, len(s.ProjectionPruning) > 0) add(LoweringLatePathMaterialization, len(s.LatePathMaterialization) > 0) add(LoweringExpandIntoDetection, len(s.ExpandInto) > 0) + add(LoweringTraversalDirection, len(s.TraversalDirection) > 0) + add(LoweringShortestPathStrategy, len(s.ShortestPathStrategy) > 0) + add(LoweringShortestPathFilter, len(s.ShortestPathFilter) > 0) + add(LoweringLimitPushdown, len(s.LimitPushdown) > 0) add(LoweringExpansionSuffixPushdown, len(s.ExpansionSuffixPushdown) > 0) add(LoweringPredicatePlacement, len(s.PredicatePlacement) > 0) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 16d71bd3..457ba745 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -11,6 +11,17 @@ type sourceTraversalStep struct { RightNode *cypher.NodePattern } +const ( + traversalDirectionReasonRightBound = "right_bound" + traversalDirectionReasonRightConstrained = "right_constrained" + + shortestPathStrategyReasonBoundEndpointPairs = "bound_endpoint_pairs" + shortestPathStrategyReasonEndpointPredicates = "endpoint_predicates" + + shortestPathFilterReasonTerminalPredicate = "terminal_predicate" + shortestPathFilterReasonEndpointPairPredicates = "endpoint_pair_predicates" +) + func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []PredicateAttachment) (LoweringPlan, error) { if query == nil || query.SingleQuery == nil { return LoweringPlan{}, nil @@ -24,18 +35,18 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic continue } - if err := appendQueryPartLowerings(&plan, queryPartIndex, part, part.ReadingClauses); err != nil { + if err := appendQueryPartLowerings(&plan, queryPartIndex, part, part.ReadingClauses, predicateAttachments); err != nil { return LoweringPlan{}, err } } if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { - if err := appendQueryPartLowerings(&plan, len(query.SingleQuery.MultiPartQuery.Parts), finalPart, finalPart.ReadingClauses); err != nil { + if err := appendQueryPartLowerings(&plan, len(query.SingleQuery.MultiPartQuery.Parts), finalPart, finalPart.ReadingClauses, predicateAttachments); err != nil { return LoweringPlan{}, err } } } else if singlePart := query.SingleQuery.SinglePartQuery; singlePart != nil { - if err := appendQueryPartLowerings(&plan, 0, singlePart, singlePart.ReadingClauses); err != nil { + if err := appendQueryPartLowerings(&plan, 0, singlePart, singlePart.ReadingClauses, predicateAttachments); err != nil { return LoweringPlan{}, err } } @@ -45,7 +56,13 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic return plan, nil } -func appendQueryPartLowerings(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode, readingClauses []*cypher.ReadingClause) error { +func appendQueryPartLowerings( + plan *LoweringPlan, + queryPartIndex int, + queryPart cypher.SyntaxNode, + readingClauses []*cypher.ReadingClause, + predicateAttachments []PredicateAttachment, +) error { sourceReferences, err := collectReferencedSourceIdentifiers(queryPart) if err != nil { return err @@ -54,6 +71,10 @@ func appendQueryPartLowerings(plan *LoweringPlan, queryPartIndex int, queryPart appendProjectionPruningDecisions(plan, queryPartIndex, readingClauses, sourceReferences) appendLatePathMaterializationDecisions(plan, queryPartIndex, readingClauses, sourceReferences) appendExpandIntoDecisions(plan, queryPartIndex, readingClauses) + appendTraversalDirectionDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) + appendShortestPathStrategyDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) + appendShortestPathFilterDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) + appendLimitPushdownDecisions(plan, queryPartIndex, queryPart, readingClauses) appendExpansionSuffixPushdownDecisions(plan, queryPartIndex, readingClauses) return nil } @@ -223,6 +244,365 @@ func declaredSymbolsBeforeStepEndpoints(initial map[string]struct{}, steps []sou return endpoints } +func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, predicateConstrainedSymbols map[string]struct{}) { + declaredSymbols := map[string]struct{}{} + + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + match := readingClause.Match + if match.Optional { + declareMatchSymbols(declaredSymbols, match) + continue + } + + for patternIndex, patternPart := range match.Pattern { + steps := traversalStepsForPattern(patternPart) + declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + patternTarget := PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + + for stepIndex, step := range steps { + if decision, shouldFlip := traversalDirectionDecisionForStep( + patternTarget.TraversalStep(stepIndex), + stepIndex, + step, + declaredEndpoints[stepIndex], + referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.LeftNode.Variable)), + ); shouldFlip { + plan.TraversalDirection = append(plan.TraversalDirection, decision) + } + } + + declarePatternSymbols(declaredSymbols, patternPart) + } + + declareWhereSymbols(declaredSymbols, match) + } +} + +func bindingPredicateSymbols(predicateAttachments []PredicateAttachment, queryPartIndex int) map[string]struct{} { + symbols := map[string]struct{}{} + + for _, attachment := range predicateAttachments { + if attachment.QueryPartIndex != queryPartIndex { + continue + } + + for _, symbol := range attachment.BindingSymbols { + addSymbol(symbols, symbol) + } + } + + return symbols +} + +func traversalDirectionDecisionForStep( + target TraversalStepTarget, + stepIndex int, + step sourceTraversalStep, + declaredEndpoints declaredStepEndpoints, + leftHasAttachedPredicate bool, +) (TraversalDirectionDecision, bool) { + if leftEndpointBoundForStep(stepIndex, step, declaredEndpoints) { + return TraversalDirectionDecision{}, false + } + + rightSymbol := variableSymbol(step.RightNode.Variable) + if rightSymbol != "" { + if _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol]; rightBound { + if rightSymbol == variableSymbol(step.LeftNode.Variable) { + return TraversalDirectionDecision{}, false + } + + return TraversalDirectionDecision{ + Target: target, + Flip: true, + Reason: traversalDirectionReasonRightBound, + }, true + } + } + + if nodePatternHasConstraints(step.RightNode) && !nodePatternHasConstraints(step.LeftNode) && !leftHasAttachedPredicate { + return TraversalDirectionDecision{ + Target: target, + Flip: true, + Reason: traversalDirectionReasonRightConstrained, + }, true + } + + return TraversalDirectionDecision{}, false +} + +func appendShortestPathStrategyDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, predicateConstrainedSymbols map[string]struct{}) { + declaredSymbols := map[string]struct{}{} + + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + match := readingClause.Match + if match.Optional { + declareMatchSymbols(declaredSymbols, match) + continue + } + + for patternIndex, patternPart := range match.Pattern { + if patternPart == nil || (!patternPart.ShortestPathPattern && !patternPart.AllShortestPathsPattern) { + declarePatternSymbols(declaredSymbols, patternPart) + continue + } + + steps := traversalStepsForPattern(patternPart) + declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + patternTarget := PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + + for stepIndex, step := range steps { + if step.Relationship.Range == nil { + continue + } + + if decision, shouldPlan := shortestPathStrategyDecisionForStep( + patternTarget.TraversalStep(stepIndex), + step, + declaredEndpoints[stepIndex], + predicateConstrainedSymbols, + ); shouldPlan { + plan.ShortestPathStrategy = append(plan.ShortestPathStrategy, decision) + } + } + + declarePatternSymbols(declaredSymbols, patternPart) + } + + declareWhereSymbols(declaredSymbols, match) + } +} + +func shortestPathStrategyDecisionForStep( + target TraversalStepTarget, + step sourceTraversalStep, + declaredEndpoints declaredStepEndpoints, + predicateConstrainedSymbols map[string]struct{}, +) (ShortestPathStrategyDecision, bool) { + leftSymbol := variableSymbol(step.LeftNode.Variable) + rightSymbol := variableSymbol(step.RightNode.Variable) + + _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol] + if leftEndpointBoundForStep(target.StepIndex, step, declaredEndpoints) && rightSymbol != "" && rightBound { + return ShortestPathStrategyDecision{ + Target: target, + Strategy: ShortestPathStrategyBidirectional, + Reason: shortestPathStrategyReasonBoundEndpointPairs, + }, true + } + + if endpointHasSearchConstraint(step.LeftNode, leftSymbol, predicateConstrainedSymbols) && + endpointHasSearchConstraint(step.RightNode, rightSymbol, predicateConstrainedSymbols) { + return ShortestPathStrategyDecision{ + Target: target, + Strategy: ShortestPathStrategyBidirectional, + Reason: shortestPathStrategyReasonEndpointPredicates, + }, true + } + + return ShortestPathStrategyDecision{}, false +} + +func endpointHasSearchConstraint(nodePattern *cypher.NodePattern, symbol string, predicateConstrainedSymbols map[string]struct{}) bool { + if nodePattern == nil { + return false + } + + return nodePattern.Properties != nil || referencesSourceIdentifier(predicateConstrainedSymbols, symbol) +} + +func appendShortestPathFilterDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, predicateConstrainedSymbols map[string]struct{}) { + declaredSymbols := map[string]struct{}{} + + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + match := readingClause.Match + if match.Optional { + declareMatchSymbols(declaredSymbols, match) + continue + } + + for patternIndex, patternPart := range match.Pattern { + if patternPart == nil || (!patternPart.ShortestPathPattern && !patternPart.AllShortestPathsPattern) { + declarePatternSymbols(declaredSymbols, patternPart) + continue + } + + steps := traversalStepsForPattern(patternPart) + declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + patternTarget := PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + + for stepIndex, step := range steps { + if step.Relationship.Range == nil { + continue + } + + if decision, shouldPlan := shortestPathFilterDecisionForStep( + plan, + patternTarget.TraversalStep(stepIndex), + step, + declaredEndpoints[stepIndex], + predicateConstrainedSymbols, + ); shouldPlan { + plan.ShortestPathFilter = append(plan.ShortestPathFilter, decision) + } + } + + declarePatternSymbols(declaredSymbols, patternPart) + } + + declareWhereSymbols(declaredSymbols, match) + } +} + +func shortestPathFilterDecisionForStep( + plan *LoweringPlan, + target TraversalStepTarget, + step sourceTraversalStep, + declaredEndpoints declaredStepEndpoints, + predicateConstrainedSymbols map[string]struct{}, +) (ShortestPathFilterDecision, bool) { + leftSymbol := variableSymbol(step.LeftNode.Variable) + rightSymbol := variableSymbol(step.RightNode.Variable) + if rightSymbol != "" { + if _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol]; rightBound { + return ShortestPathFilterDecision{}, false + } + } + + leftSearchConstrained := endpointHasSearchConstraint(step.LeftNode, leftSymbol, predicateConstrainedSymbols) + rightSearchConstrained := endpointHasSearchConstraint(step.RightNode, rightSymbol, predicateConstrainedSymbols) + if !rightSearchConstrained { + return ShortestPathFilterDecision{}, false + } + + if hasShortestPathBidirectionalStrategy(plan, target) && leftSearchConstrained { + return ShortestPathFilterDecision{ + Target: target, + Mode: ShortestPathFilterEndpointPair, + Reason: shortestPathFilterReasonEndpointPairPredicates, + }, true + } + + return ShortestPathFilterDecision{ + Target: target, + Mode: ShortestPathFilterTerminal, + Reason: shortestPathFilterReasonTerminalPredicate, + }, true +} + +func hasShortestPathBidirectionalStrategy(plan *LoweringPlan, target TraversalStepTarget) bool { + if plan == nil { + return false + } + + for _, decision := range plan.ShortestPathStrategy { + if decision.Target == target && decision.Strategy == ShortestPathStrategyBidirectional { + return true + } + } + + return false +} + +func appendLimitPushdownDecisions(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode, readingClauses []*cypher.ReadingClause) { + if !queryPartAllowsLimitPushdown(queryPart, readingClauses) { + return + } + + for clauseIndex, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil { + continue + } + + for patternIndex, patternPart := range readingClause.Match.Pattern { + if patternPart == nil { + continue + } + if patternPart.AllShortestPathsPattern { + continue + } + + patternTarget := PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + + for stepIndex, step := range traversalStepsForPattern(patternPart) { + mode := LimitPushdownTraversalCTE + if patternPart.ShortestPathPattern && step.Relationship.Range != nil { + mode = LimitPushdownShortestPathHarness + } + + plan.LimitPushdown = append(plan.LimitPushdown, LimitPushdownDecision{ + Target: patternTarget.TraversalStep(stepIndex), + Mode: mode, + }) + } + } + } +} + +func queryPartAllowsLimitPushdown(queryPart cypher.SyntaxNode, readingClauses []*cypher.ReadingClause) bool { + projection, updatingClauseCount := queryPartProjection(queryPart) + if projection == nil || + projection.Limit == nil || + projection.Skip != nil || + projection.Order != nil || + projection.Distinct || + len(readingClauses) != 1 || + updatingClauseCount > 0 { + return false + } + + return true +} + +func queryPartProjection(queryPart cypher.SyntaxNode) (*cypher.Projection, int) { + switch typedQueryPart := queryPart.(type) { + case *cypher.SinglePartQuery: + if typedQueryPart.Return == nil { + return nil, len(typedQueryPart.UpdatingClauses) + } + + return typedQueryPart.Return.Projection, len(typedQueryPart.UpdatingClauses) + + case *cypher.MultiPartQueryPart: + if typedQueryPart.With == nil { + return nil, len(typedQueryPart.UpdatingClauses) + } + + return typedQueryPart.With.Projection, len(typedQueryPart.UpdatingClauses) + + default: + return nil, 0 + } +} + func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause) { declaredSymbols := map[string]struct{}{} @@ -240,19 +620,25 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i for patternIndex, patternPart := range match.Pattern { steps := traversalStepsForPattern(patternPart) declaredBeforeRightNode := declaredSymbolsBeforeRightNodes(declaredSymbols, steps) + declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) for stepIndex, step := range steps { if step.Relationship.Range == nil || stepIndex+1 >= len(steps) { continue } + target := PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }.TraversalStep(stepIndex) + if hasTraversalDirectionFlip(plan, target) || expansionStepMayFlipForConstraintBalance(stepIndex, step, declaredEndpoints[stepIndex]) { + continue + } + if suffixLength := expansionSuffixPushdownLength(steps[stepIndex+1:], declaredBeforeRightNode[stepIndex+1:]); suffixLength > 0 { plan.ExpansionSuffixPushdown = append(plan.ExpansionSuffixPushdown, ExpansionSuffixPushdownDecision{ - Target: PatternTarget{ - QueryPartIndex: queryPartIndex, - ClauseIndex: clauseIndex, - PatternIndex: patternIndex, - }.TraversalStep(stepIndex), + Target: target, SuffixLength: suffixLength, SuffixStartStep: stepIndex + 1, SuffixEndStep: stepIndex + suffixLength, @@ -267,6 +653,35 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i } } +func expansionStepMayFlipForConstraintBalance(stepIndex int, step sourceTraversalStep, declaredEndpoints declaredStepEndpoints) bool { + _, mayFlip := traversalDirectionDecisionForStep(TraversalStepTarget{}, stepIndex, step, declaredEndpoints, false) + return mayFlip +} + +func leftEndpointBoundForStep(stepIndex int, step sourceTraversalStep, declaredEndpoints declaredStepEndpoints) bool { + leftSymbol := variableSymbol(step.LeftNode.Variable) + if leftSymbol == "" { + return stepIndex > 0 + } + + _, leftBound := declaredEndpoints.BeforeLeftNode[leftSymbol] + return leftBound +} + +func hasTraversalDirectionFlip(plan *LoweringPlan, target TraversalStepTarget) bool { + if plan == nil { + return false + } + + for _, decision := range plan.TraversalDirection { + if decision.Target == target && decision.Flip { + return true + } + } + + return false +} + type bindingTargetKey struct { QueryPartIndex int Symbol string diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index c3efd4d1..9e6fa5b5 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -250,6 +250,252 @@ func TestLoweringPlanReportsExpandIntoForAnonymousContinuationEndpoint(t *testin }) } +func TestLoweringPlanReportsTraversalDirectionForConstrainedRightEndpoint(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n)-[:MemberOf*1..]->(ca:EnterpriseCA) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Flip: true, + Reason: traversalDirectionReasonRightConstrained, + }}, plan.LoweringPlan.TraversalDirection) +} + +func TestLoweringPlanReportsTraversalDirectionForBoundRightEndpoint(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (ca:EnterpriseCA) + MATCH p = (n)-[:MemberOf*1..]->(ca) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + Flip: true, + Reason: traversalDirectionReasonRightBound, + }}, plan.LoweringPlan.TraversalDirection) +} + +func TestLoweringPlanSkipsTraversalDirectionWhenLeftEndpointHasBindingPredicate(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n)-[:MemberOf*1..]->(ca:EnterpriseCA) + WHERE n.name = 'target' + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Empty(t, plan.LoweringPlan.TraversalDirection) +} + +func TestLoweringPlanSkipsTraversalDirectionWhenLeftEndpointHasRegionPredicate(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + WITH 'target' AS name + MATCH p = (n)-[:MemberOf]->(ca:EnterpriseCA) + WHERE n.name STARTS WITH name + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Empty(t, plan.LoweringPlan.TraversalDirection) +} + +func TestLoweringPlanReportsShortestPathStrategyForEndpointPredicates(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = allShortestPaths((s)-[:MemberOf*1..]->(e)) + WHERE s.name = 'source' AND e.name = 'target' + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringShortestPathStrategy}) + require.Equal(t, []ShortestPathStrategyDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Strategy: ShortestPathStrategyBidirectional, + Reason: shortestPathStrategyReasonEndpointPredicates, + }}, plan.LoweringPlan.ShortestPathStrategy) + require.Equal(t, []ShortestPathFilterDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Mode: ShortestPathFilterEndpointPair, + Reason: shortestPathFilterReasonEndpointPairPredicates, + }}, plan.LoweringPlan.ShortestPathFilter) +} + +func TestLoweringPlanReportsShortestPathStrategyForBoundEndpointPairs(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (a:Group) + MATCH (b:EnterpriseCA) + MATCH p = shortestPath((a)-[:MemberOf*1..]->(b)) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringShortestPathStrategy}) + require.Equal(t, []ShortestPathStrategyDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 2, + PatternIndex: 0, + StepIndex: 0, + }, + Strategy: ShortestPathStrategyBidirectional, + Reason: shortestPathStrategyReasonBoundEndpointPairs, + }}, plan.LoweringPlan.ShortestPathStrategy) +} + +func TestLoweringPlanSkipsShortestPathStrategyForLabelOnlyEndpoints(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = allShortestPaths((s:Group)-[:MemberOf*1..]->(e:EnterpriseCA)) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Empty(t, plan.LoweringPlan.ShortestPathStrategy) +} + +func TestLoweringPlanReportsShortestPathTerminalFilter(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (s:Group {name: 'source'}) + MATCH p = shortestPath((s)-[:MemberOf*1..]->(e)) + WHERE e.name = 'target' + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringShortestPathFilter}) + require.Equal(t, []ShortestPathFilterDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + Mode: ShortestPathFilterTerminal, + Reason: shortestPathFilterReasonTerminalPredicate, + }}, plan.LoweringPlan.ShortestPathFilter) +} + +func TestLoweringPlanReportsTraversalLimitPushdown(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n:Group)-[:MemberOf]->(m:Group) + RETURN p + LIMIT 1 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringLimitPushdown}) + require.Equal(t, []LimitPushdownDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Mode: LimitPushdownTraversalCTE, + }}, plan.LoweringPlan.LimitPushdown) +} + +func TestLoweringPlanReportsShortestPathLimitPushdown(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = shortestPath((s)-[:MemberOf*1..]->(e)) + WHERE s.name = 'source' AND e.name = 'target' + RETURN p + LIMIT 1 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringLimitPushdown}) + require.Contains(t, plan.LoweringPlan.LimitPushdown, LimitPushdownDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Mode: LimitPushdownShortestPathHarness, + }) +} + +func TestLoweringPlanSkipsAllShortestPathLimitPushdown(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = allShortestPaths((s)-[:MemberOf*1..]->(e)) + WHERE s.name = 'source' AND e.name = 'target' + RETURN p + LIMIT 1 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Empty(t, plan.LoweringPlan.LimitPushdown) +} + func TestLoweringPlanSkipsDirectionlessExpansionSuffixPushdown(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/test/translation_cases/create.sql b/cypher/models/pgsql/test/translation_cases/create.sql index b3fc65ce..1458421e 100644 --- a/cypher/models/pgsql/test/translation_cases/create.sql +++ b/cypher/models/pgsql/test/translation_cases/create.sql @@ -69,7 +69,7 @@ with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id) with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'abc')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object('name', 'test')::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n2_id from s5), s7 as (insert into node (graph_id, id, kind_ids, properties) select 0, s6.n2_id, array [1]::int2[], jsonb_build_object('name', 'other')::jsonb from s6 returning id as n2_id, (id, kind_ids, properties)::nodecomposite as n2), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.n2 as n2 from s6, s7 where s7.n2_id = s6.n2_id), s9 as (select s8.n0 as n0, s8.n1 as n1, s8.n2 as n2, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s8), s10 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s9.e0_id, (s9.n0).id, (s9.n1).id, 3, jsonb_build_object('prop', 123)::jsonb from s9 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s11 as (select s9.n0 as n0, s9.n1 as n1, s9.n2 as n2, s10.e0 as e0 from s9, s10 where s10.e0_id = s9.e0_id), s12 as (select s11.e0 as e0, s11.n0 as n0, s11.n1 as n1, s11.n2 as n2, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e1_id from s11), s13 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s12.e1_id, (s12.n2).id, (s12.n1).id, 4, jsonb_build_object()::jsonb from s12 returning id as e1_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e1), s14 as (select s12.e0 as e0, s12.n0 as n0, s12.n1 as n1, s12.n2 as n2, s13.e1 as e1 from s12, s13 where s13.e1_id = s12.e1_id) select s14.n1 as c from s14; -- case: create p = (:NodeKind1 {name: 'abc'})-[:EdgeKind1 {prop: 123}]->(:NodeKind2 {name: 'test'}) return p -with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'abc')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object('name', 'test')::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object('prop', 123)::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select (array [s8.n0, s8.n1]::nodecomposite[], array [s8.e0]::edgecomposite[])::pathcomposite as p from s8; +with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id), s1 as (insert into node (graph_id, id, kind_ids, properties) select 0, s0.n0_id, array [1]::int2[], jsonb_build_object('name', 'abc')::jsonb from s0 returning id as n0_id, (id, kind_ids, properties)::nodecomposite as n0), s2 as (select s1.n0 as n0 from s0, s1 where s1.n0_id = s0.n0_id), s3 as (select s2.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s2), s4 as (insert into node (graph_id, id, kind_ids, properties) select 0, s3.n1_id, array [2]::int2[], jsonb_build_object('name', 'test')::jsonb from s3 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s5 as (select s3.n0 as n0, s4.n1 as n1 from s3, s4 where s4.n1_id = s3.n1_id), s6 as (select s5.n0 as n0, s5.n1 as n1, nextval(pg_get_serial_sequence('edge', 'id'))::int8 as e0_id from s5), s7 as (insert into edge (graph_id, id, start_id, end_id, kind_id, properties) select 0, s6.e0_id, (s6.n0).id, (s6.n1).id, 3, jsonb_build_object('prop', 123)::jsonb from s6 returning id as e0_id, (id, start_id, end_id, kind_id, properties)::edgecomposite as e0), s8 as (select s6.n0 as n0, s6.n1 as n1, s7.e0 as e0 from s6, s7 where s7.e0_id = s6.e0_id) select case when (s8.n0).id is null or (s8.e0).id is null or (s8.n1).id is null then null else (array [s8.n0, s8.n1]::nodecomposite[], array [s8.e0]::edgecomposite[])::pathcomposite end as p from s8; -- case: match (a:NodeKind1) with a create (b:NodeKind2 {source: a.name}) return a, b with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object('source', ((s2.n0).properties ->> 'name'))::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id) select s4.n0 as a, s4.n1 as b from s4; diff --git a/cypher/models/pgsql/test/translation_cases/delete.sql b/cypher/models/pgsql/test/translation_cases/delete.sql index 540765f8..ab59796e 100644 --- a/cypher/models/pgsql/test/translation_cases/delete.sql +++ b/cypher/models/pgsql/test/translation_cases/delete.sql @@ -21,5 +21,4 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (delete from edge e1 using s0 where (s0.e0).id = e1.id) select 1; -- case: match ()-[]->()-[r:EdgeKind1]->() delete r -with s0 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[])), s2 as (delete from edge e2 using s1 where (s1.e1).id = e2.id) select 1; - +with s0 as (select e0.id as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) and e1.id != s0.e0), s2 as (delete from edge e2 using s1 where (s1.e1).id = e2.id) select 1; diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index a9b601db..739dd00b 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -24,13 +24,13 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'value'))::jsonb = to_jsonb((1)::int8)::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 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('me')::text)::jsonb)) select s3.n1 as n1 from s3), s4 as (select s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where (n2.id = (s2.n1).id)) select s4.n2 as b from s4; -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, collect(distinct(n)) as p where size(p) >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: with 365 as max_days match (n:NodeKind1) where n.pwdlastset < (datetime().epochseconds - (max_days * 86400)) and not n.pwdlastset IN [-1.0, 0.0] return n limit 100 with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (not ((n0.properties ->> 'pwdlastset'))::float8 = any (array [- 1, 0]::float8[]) and ((n0.properties ->> 'pwdlastset'))::numeric < (extract(epoch from now()::timestamp with time zone)::numeric - (s0.i0 * 86400))) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1 limit 100; @@ -39,7 +39,7 @@ with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s1.n0).id as root_id from s1), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s3.path || e0.id from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s3.depth < 15 and not s3.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.next_id offset 0) n1 on true where s3.satisfied and (s1.n0).id = s3.root_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; -- case: match (n:NodeKind1) where n.objectid = 'S-1-5-21-1260426776-3623580948-1897206385-23225' match p = (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'objectid'))::jsonb = to_jsonb(('S-1-5-21-1260426776-3623580948-1897206385-23225')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'objectid'))::jsonb = to_jsonb(('S-1-5-21-1260426776-3623580948-1897206385-23225')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; -- case: match (g1:NodeKind1) where g1.name starts with 'test' with collect (g1.domain) as excludes match (d:NodeKind2) where d.name starts with 'other' and not d.name in excludes return d with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like 'test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'domain'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (not (n1.properties ->> 'name') = any (s0.i0) and (n1.properties ->> 'name') like 'other%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]) select s2.n1 as d from s2; @@ -48,13 +48,13 @@ 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 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 (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 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, 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 (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 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, 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 case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3, s4.n4]::nodecomposite[])::pathcomposite end 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); -- case: match (n:NodeKind1)-[:EdgeKind1]->(m:NodeKind2) where n.enabled = true with n, collect(distinct(n)) as p where size(p) >= 100 match p = (n)-[:EdgeKind1]->(m) return p limit 10 -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n0 as n0, array_remove(coalesce(array_agg(distinct (s1.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1 group by n0), s2 as (select e1.id as e1, s0.i0 as i0, s0.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (cardinality(s0.i0)::int >= 100) and (s0.n0).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) limit 10) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n0 as n0, array_remove(coalesce(array_agg(distinct (s1.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s1 group by n0), s2 as (select e1.id as e1, s0.i0 as i0, s0.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (cardinality(s0.i0)::int >= 100) and (s0.n0).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) limit 10) select case when (s2.n0).id is null or s2.e1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 10; -- case: with "a" as check, "b" as ref match p = (u)-[:EdgeKind1]->(g:NodeKind1) where u.name starts with check and u.domain = ref with collect(tolower(g.samaccountname)) as refmembership, tolower(u.samaccountname) as samname return refmembership, samname with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool)) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::text[], array []::text[])::text[], null)::text[] as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by lower(((s2.n0).properties ->> 'samaccountname'))::text) select s1.i2 as refmembership, s1.i3 as samname from s1; @@ -66,7 +66,7 @@ with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool)) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::text[], array []::text[])::text[], null)::text[] as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by lower(((s2.n0).properties ->> 'samaccountname'))::text), s3 as (select s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n3.id = e1.end_id where (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; -- case: match p =(n:NodeKind1)<-[r:EdgeKind1|EdgeKind2*..3]-(u:NodeKind1) where n.domain = 'test' with n, count(r) as incomingCount where incomingCount > 90 with collect(n) as lotsOfAdmins match p =(n:NodeKind1)<-[:EdgeKind1]-() where n in lotsOfAdmins return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb(('test')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select e1.id as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3]::nodecomposite[])::pathcomposite as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb(('test')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select e1.id as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3]::nodecomposite[])::pathcomposite end as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); -- case: match (u:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) with g match (g)<-[:EdgeKind1]-(u:NodeKind1) return g with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n1 as n1 from s1), s2 as (select s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select s2.n1 as g from s2; @@ -75,19 +75,19 @@ with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposit with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') ~ '.*TT' and ((n0.properties -> 'domain'))::jsonb = to_jsonb(('MY DOMAIN')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'email'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (not (n2.properties ->> 'email') = any (s0.i0) and (n2.properties ->> 'name') like 'blah%')) select s2.n1 as o from s2; -- case: match (e) match p = ()-[]->(e) return p limit 1 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite as p from s1 limit 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select case when (s1.n1).id is null or s1.e0 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite end as p from s1 limit 1; -- case: match p = (a)-[]->() match q = ()-[]->(a) return p, q -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite as q from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p, case when (s1.n2).id is null or s1.e1 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite end as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.id != all (s1.ep0)) 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 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4; -- case: match (m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)-[:EdgeKind2]->(c3:NodeKind1) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and not m.samaccountname contains "DEX" and not g.name IN ["D"] and not m.samaccountname =~ "^.*$" with collect(g.name) as admingroups match p=(m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and g.name in admingroups and not m.samaccountname =~ "^.*$" return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (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.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4; -- case: match (a:NodeKind2)-[:EdgeKind1]->(g:NodeKind1)-[:EdgeKind2]->(s:NodeKind2) with count(a) as uc where uc > 5 match p = (a)-[:EdgeKind1]->(g)-[:EdgeKind2]->(s) return p -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select e2.id as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, e3.id as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e2]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e3]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4, s4.n5]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.e0 as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != s1.e0) select count(s2.n0)::int8 as i0 from s2), s3 as (select e2.id as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, e3.id as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[]) and e3.id != s3.e2) select case when (s4.n3).id is null or s4.e2 is null or (s4.n4).id is null or s4.e3 is null or (s4.n5).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e2]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e3]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4, s4.n5]::nodecomposite[])::pathcomposite end as p from s4; -- case: match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 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, s2.n1 as n1 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 8a2884eb..1162f06b 100644 --- a/cypher/models/pgsql/test/translation_cases/nodes.sql +++ b/cypher/models/pgsql/test/translation_cases/nodes.sql @@ -211,7 +211,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id)); -- case: match (s) where not (s)-[]->()-[]->() return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n1 on n1.id = e0.end_id where (s0.n0).id = e0.start_id), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select count(*) > 0 from s2)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n1 on n1.id = e0.end_id where (s0.n0).id = e0.start_id), s2 as (select s1.e0 as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s1.e0) select count(*) > 0 from s2)); -- case: match (s) where not (s)-[{prop: 'a'}]-({name: 'n3'}) return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on ((s0.n0).id = e0.end_id or (s0.n0).id = e0.start_id) join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and (n1.id = e0.end_id or n1.id = e0.start_id) where ((s0.n0).id <> n1.id) and ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb) select count(*) > 0 from s1)); diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index 57ea7e18..bbcba6d8 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -15,79 +15,79 @@ -- SPDX-License-Identifier: Apache-2.0 -- case: match p = (:NodeKind1) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select (array [s0.n0]::nodecomposite[], array []::edgecomposite[])::pathcomposite as p from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select case when (s0.n0).id is null then null else (array [s0.n0]::nodecomposite[], array []::edgecomposite[])::pathcomposite end as p from s0; -- case: match p = (n:NodeKind1) where n.name contains 'test' return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like '%test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select (array [s0.n0]::nodecomposite[], array []::edgecomposite[])::pathcomposite as p from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like '%test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select case when (s0.n0).id is null then null else (array [s0.n0]::nodecomposite[], array []::edgecomposite[])::pathcomposite end as p from s0; -- case: match p = ()-[]->() return p -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select case when (s0.n0).id is null or s0.e0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0; -- case: match p = ()-[]->() return nodes(p) -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select ((ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[] from s0; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select ((case when (s0.n0).id is null or s0.e0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end).nodes)::nodecomposite[] from s0; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2*1..1]->(:NodeKind2) where any(r in relationships(p) where type(r) STARTS WITH 'EdgeKind') 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 [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 1 and not s1.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where (kind_name(i0.kind_id)::text like 'EdgeKind%')) >= 1)::bool); +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 1 and not s1.is_cycle) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where (kind_name(i0.kind_id)::text like 'EdgeKind%')) >= 1)::bool); -- case: match p=(:NodeKind1)-[r]->(:NodeKind1) where r.isacl return p limit 100 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where (((e0.properties ->> 'isacl'))::bool) limit 100) select (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite as p from s0 limit 100; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where (((e0.properties ->> 'isacl'))::bool) limit 100) select case when (s0.n0).id is null or (s0.e0).id is null or (s0.n1).id is null then null else (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite end as p from s0 limit 100; -- case: match p = ()-[r1]->()-[r2]->(e) return e -with s0 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s1.n2 as e from s1; +with s0 as (select e0.id as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s0.e0) select s1.n2 as e from s1; -- case: match ()-[r1]->()-[r2]->()-[]->() where r1.name = 'a' and r2.name = 'b' return r1 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties -> 'name'))::jsonb = to_jsonb(('a')::text)::jsonb)), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where (((e1.properties -> 'name'))::jsonb = to_jsonb(('b')::text)::jsonb)), s2 as (select s1.e0 as e0, s1.e1 as e1, s1.n1 as n1, s1.n2 as n2 from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id) select s2.e0 as r1 from s2; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties -> 'name'))::jsonb = to_jsonb(('a')::text)::jsonb)), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where (((e1.properties -> 'name'))::jsonb = to_jsonb(('b')::text)::jsonb) and e1.id != (s0.e0).id), s2 as (select s1.e0 as e0, s1.e1 as e1, s1.n1 as n1, s1.n2 as n2 from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id where e2.id != (s1.e0).id and e2.id != (s1.e1).id) select s2.e0 as r1 from s2; -- case: match p = (a)-[]->()<-[]-(f) where a.name = 'value' and f.is_target return p -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('value')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('value')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id where e1.id != s0.e0) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null or s1.e1 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1; -- case: match p = ()-[*..]->() return p limit 1 -with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1; +with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1; -- case: match p = (s)-[*..]->(i)-[]->() where id(s) = 1 and i.name = 'n3' return p limit 1 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id limit 1) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 1; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0) limit 1) select case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null or s2.e1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 1; -- case: match p = ()-[e:EdgeKind1]->()-[:EdgeKind1*..]->() return e, p -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, case when (s1.n0).id is null or (s1.e0).id is null or (s1.n1).id is null or s1.ep0 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1; -- case: match p = (m:NodeKind1)-[:EdgeKind1]->(c:NodeKind2) where m.objectid ends with "-513" and not toUpper(c.operatingsystem) contains "SERVER" return p limit 1000 -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on ((n0.properties ->> 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on (not upper((n1.properties ->> 'operatingsystem'))::text like '%SERVER%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on ((n0.properties ->> 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on (not upper((n1.properties ->> 'operatingsystem'))::text like '%SERVER%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) limit 1000) select case when (s0.n0).id is null or s0.e0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1000; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2]->(e:NodeKind2)-[:EdgeKind2]->(:NodeKind1) where 'a' in e.values or 'b' in e.values or size(e.values) = 0 return p -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('a' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or 'b' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or jsonb_array_length((n1.properties -> 'values'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('a' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or 'b' = any (jsonb_to_text_array((n1.properties -> 'values'))::text[]) or jsonb_array_length((n1.properties -> 'values'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != s0.e0) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null or s1.e1 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1; -- case: match p = (n:NodeKind1)-[r]-(m:NodeKind1) return p -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.id = e0.end_id or n0.id = e0.start_id) join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n1.id = e0.end_id or n1.id = e0.start_id) where (n0.id <> n1.id)) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.id = e0.end_id or n0.id = e0.start_id) join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n1.id = e0.end_id or n1.id = e0.start_id) where (n0.id <> n1.id)) select case when (s0.n0).id is null or s0.e0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0; -- case: match p = (:NodeKind1)-[:EdgeKind1]->(:NodeKind2)-[:EdgeKind2*1..]->(t:NodeKind2) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000 -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.end_id where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.satisfied and (s0.n1).id = s2.root_id limit 1000) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p from s1 limit 1000; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, (coalesce((n2.properties ->> 'system_tags'), '')::text like '%admin_tier_0%') and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.end_id where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.satisfied and (s0.n1).id = s2.root_id limit 1000) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null or s1.ep0 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1 limit 1000; -- case: match (u:NodeKind1) where u.samaccountname in ["foo", "bar"] match p = (u)-[:EdgeKind1|EdgeKind2*1..3]->(t) where coalesce(t.system_tags, '') contains 'admin_tier_0' return p limit 1000 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1 limit 1000; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1 limit 1000; -- case: match (x:NodeKind1) where x.name = 'foo' match (y:NodeKind2) where y.name = 'bar' match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match (y:NodeKind2{name:'bar'}) match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match p=(x)-[:EdgeKind1]->(y:NodeKind2{name:'bar'}) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; -- case: match (e) match p = ()-[]->(e) return p limit 1 -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite as p from s1 limit 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select case when (s1.n1).id is null or s1.e0 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite end as p from s1 limit 1; -- case: match p = (a)-[]->() match q = ()-[]->(a) return p, q -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite as q from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p, case when (s1.n2).id is null or s1.e1 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite end as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 (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.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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.id != all (s1.ep0)) 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 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end 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; +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 case when (s0.n0).id is null or (s0.e0).id is null or (s0.n1).id is null then null else (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite end 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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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); +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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((((s0.e0)[1]).properties ->> 'enforced'))::bool or ((select count(*)::int from unnest(coalesce((coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).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((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[] is not null)::bool); -- case: MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE NONE(x in TAIL(r) WHERE NOT type(x) = 'Contains') 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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where (((select count(*)::int from unnest(coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[] is not null)::bool); +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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((select count(*)::int from unnest(coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[] 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 273b3530..f84e2d77 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -27,7 +27,7 @@ with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from edge e0 union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 5 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2) select s0.n0 as n, s0.n1 as e from s0; -- case: match p = (n)-[*..]->(e:NodeKind1) return p -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0; -- case: match (n)-[*..]->(e:NodeKind1) where n.name = 'n1' return e 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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n1 as e from s0; @@ -36,46 +36,46 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as 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(('n2')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n0 as n from s0; -- case: match (n)-[*..]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0)) select s2.n2 as l from s2; -- case: match (n)-[*2..3]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0)) select s2.n2 as l from s2; -- case: match (n)-[]->(e:NodeKind1)-[*2..3]->(l) where n.name = 'n1' return l -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select s0.e0 as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; -- case: match (n)-[*..]->(e)-[:EdgeKind1|EdgeKind2]->()-[*..]->(l) where n.name = 'n1' and e.name = 'n2' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[])), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; +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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[]) and e1.id != all (s0.ep0)), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.e1 as e1, s2.ep0 as ep0, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; -- case: match p = (:NodeKind1)-[:EdgeKind1*1..]->(n:NodeKind2) where 'admin_tier_0' in split(n.system_tags, ' ') return p limit 1000 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1000; -- case: match p = (s:NodeKind1)-[*..]->(e:NodeKind2) where s <> e 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 [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and (n0.id <> n1.id)) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and (n0.id <> n1.id)) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0; -- case: match p = (g:NodeKind1)-[:EdgeKind1|EdgeKind2*]->(target:NodeKind1) where g.objectid ends with '1234' and target.objectid ends with '4567' return p -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%1234') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%1234') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0; -- case: match p = (m:NodeKind2)-[:EdgeKind1*1..]->(n:NodeKind1) where n.objectid = '1234' return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -> 'objectid'))::jsonb = to_jsonb(('1234')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -> 'objectid'))::jsonb = to_jsonb(('1234')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-() return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 10) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind2)<-[:EdgeKind1|EdgeKind2*2..]-(:NodeKind1) return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.depth >= 2 and s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.depth >= 2 and s3.satisfied and (s0.n1).id = s3.root_id limit 10) select case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null or s2.ep1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind2)<-[:EdgeKind1|EdgeKind2*..]-(:NodeKind1) return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.satisfied and (s0.n1).id = s3.root_id limit 10) select ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite as p from s2 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.end_id, e1.start_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.end_id = e1.start_id, array [e1.id] from s3_seed join edge e1 on e1.end_id = s3_seed.root_id join node n2 on n2.id = e1.start_id where e1.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e1.start_id, s3.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s3.path || e1.id from s3 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.end_id = s3.next_id and e1.id != all (s3.path) and e1.kind_id = any (array [3, 4]::int2[]) offset 0) e1 on true join node n2 on n2.id = e1.start_id where s3.depth < 15 and not s3.is_cycle) select s0.ep0 as ep0, s3.path as ep1, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s3.next_id offset 0) n2 on true where s3.satisfied and (s0.n1).id = s3.root_id limit 10) select case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null or s2.ep1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 10; -- case: match p = (n:NodeKind1)-[:EdgeKind1|EdgeKind2*1..2]->(r:NodeKind2) where r.name =~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*' return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties ->> 'name') ~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 2 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((n1.properties ->> 'name') ~ '(?i)Global Administrator.*|User Administrator.*|Cloud Application Administrator.*|Authentication Policy Administrator.*|Exchange Administrator.*|Helpdesk Administrator.*|Privileged Authentication Administrator.*') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 2 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; -- case: match p = (t:NodeKind2)<-[:EdgeKind1*1..]-(a) where (a:NodeKind1 or a:NodeKind2) and t.objectid ends with '-512' return p limit 1000 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%-512') and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied limit 1000) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 1000; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%-512') and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, ((n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n1.kind_ids operator (pg_catalog.@>) array [2]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied limit 1000) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1000; -- case: match p=(n:NodeKind1)-[:EdgeKind1|EdgeKind2]->(g:NodeKind1)-[:EdgeKind2]->(:NodeKind2)-[:EdgeKind1*1..]->(m:NodeKind1) where n.objectid = m.objectid return p limit 100 -with s0 as (select e0.id 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 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, 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2, s2.n3]::nodecomposite[])::pathcomposite as p from s2 limit 100; +with s0 as (select e0.id 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 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[]) and e1.id != s0.e0), 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, 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 case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null or s2.e1 is null or (s2.n2).id is null or s2.ep0 is null or (s2.n3).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2, s2.n3]::nodecomposite[])::pathcomposite end 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 (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; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_rewriting.sql b/cypher/models/pgsql/test/translation_cases/pattern_rewriting.sql index 01ee8044..21dcea9d 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_rewriting.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_rewriting.sql @@ -16,4 +16,3 @@ -- case: match (s:NodeKind1) return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as s from s0; - diff --git a/cypher/models/pgsql/test/translation_cases/quantifiers.sql b/cypher/models/pgsql/test/translation_cases/quantifiers.sql index 198db68f..01b88d38 100644 --- a/cypher/models/pgsql/test/translation_cases/quantifiers.sql +++ b/cypher/models/pgsql/test/translation_cases/quantifiers.sql @@ -37,6 +37,7 @@ 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; diff --git a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql index c18e4de0..b6693945 100644 --- a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql +++ b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql @@ -16,71 +16,71 @@ -- 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id) limit 1000; +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where ((s0.n0).id <> (s0.n1).id) limit 1000; -- case: match p=shortestPath((n:NodeKind1)-[:EdgeKind1*1..]->(m)) where 'admin_tier_0' in split(m.system_tags, ' ') and n.objectid ends with '-513' and m<>n return p limit 1000 -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties -\u003e\u003e 'objectid') like '%-513') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s1.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties -\u003e\u003e 'system_tags'), ' ')::text[]))) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s1.root_id and backward_visited.id = e0.start_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where ((n0.properties ->> ''objectid'') like ''%-513'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (''admin_tier_0'' = any (string_to_array((n1.properties ->> ''system_tags''), '' '')::text[])) and n0.id is not null and n1.id is not null;')::text, (1000)::int8)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; -- case: match p=shortestPath((t:NodeKind1)<-[:EdgeKind1|EdgeKind2*1..]-(s:NodeKind2)) where coalesce(t.system_tags, '') contains 'admin_tier_0' and t.name =~ 'name.*' and s<>t return p limit 1000 -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%' and (n0.properties -\u003e\u003e 'name') ~ 'name.*') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3, 4]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.start_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15, ('')::text, ('insert into traversal_terminal_filter (id) select distinct n1.id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id is not null;')::text, (1000)::int8)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id) limit 1000; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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);"} -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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where ((s0.n0).id <> (s0.n1).id); -- case: match p=shortestPath((a:NodeKind2)-[:EdgeKind1*]->(b)) where a <> b return p -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@\u003e) array [2]::int2[]) select e0.start_id, e0.end_id, 1, exists (select 1 from edge where end_id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.start_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.end_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n1.id, n0.id from node n1, node n0 where (((n1.properties -> ''name''))::jsonb = to_jsonb((''123'')::text)::jsonb) and (coalesce((n0.properties ->> ''system_tags''), '''')::text like ''%admin_tier_0%'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id is not null and n0.id is not null;')::text)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id); +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n1.id, n0.id from node n1, node n0 where (((n1.properties -> ''name''))::jsonb = to_jsonb((''123'')::text)::jsonb) and (coalesce((n0.properties ->> ''system_tags''), '''')::text like ''%admin_tier_0%'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id is not null and n0.id is not null;')::text)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where ((s0.n1).id <> (s0.n0).id); -- case: match p=shortestPath((a)-[:EdgeKind1*]->(b:NodeKind1)) where a <> b return p -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from edge where end_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from edge where end_id = e0.end_id), false, e0.id || s1.path from forward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path) and not exists (select 1 from visited where visited.root_id = s1.root_id and visited.id = e0.start_id);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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 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, 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 ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as p, ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n2]::nodecomposite[])::pathcomposite as p2 from s1 limit 500; +with s0 as (select e0.id 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, 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 case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p, case when (s1.n1).id is null or s1.ep0 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n2]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), 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 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 case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end 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 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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), 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 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 case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end 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);"} @@ -88,8 +88,8 @@ with s0 as (with s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, pa -- case: MATCH (g1:Group) MATCH (g2:Group) WHERE g1.name STARTS WITH 'DOMAIN USERS@' AND g2.name STARTS WITH 'DOMAIN ADMINS@' MATCH p=shortestPath((g1)-[:AddAllowedToAct|AddMember|AdminTo|AllExtendedRights|AllowedToDelegate|CanRDP|Contains|ForceChangePassword|GenericAll|GenericWrite|GetChangesAll|GetChanges|HasSession|MemberOf|Owns|ReadLAPSPassword|SQLAdmin|TrustedBy|WriteAccountRestrictions|WriteOwner*1..]->(g2)) WHERE NONE(r IN relationships(p) WHERE type(r) = 'HasSession' AND startNode(r).name = 'DF-WIN10-DEV01.DUMPSTER.FIRE') 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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [13]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') like 'DOMAIN ADMINS@%' and ((s0.n0).properties ->> 'name') like 'DOMAIN USERS@%') and n1.kind_ids operator (pg_catalog.@>) array [13]::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 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, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite as p from s2 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('DF-WIN10-DEV01.DUMPSTER.FIRE')::text)::jsonb and i0.kind_id = 7)) = 0 and ((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[] is not null)::bool); +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 [13]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') like 'DOMAIN ADMINS@%' and ((s0.n0).properties ->> 'name') like 'DOMAIN USERS@%') and n1.kind_ids operator (pg_catalog.@>) array [13]::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 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 case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('DF-WIN10-DEV01.DUMPSTER.FIRE')::text)::jsonb and i0.kind_id = 7)) = 0 and ((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[] is not null)::bool); -- case: match p=shortestPath((s:NodeKind1)-[:EdgeKind1|HasSession*1..]->(d:NodeKind1)) where s.name = 'path-filter-src' and d.name = 'path-filter-dst' with p where none(r in relationships(p) where type(r) = 'HasSession' and startNode(r).name = 'blocked-session-host') return p -- 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.properties -\u003e 'name'))::jsonb = to_jsonb(('path-filter-src')::text)::jsonb) 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 s2_seed join edge e0 on e0.start_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::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 s2.root_id, e0.end_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s2.root_id and traversal_pair_filter.terminal_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, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s2.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s2_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -\u003e 'name'))::jsonb = to_jsonb(('path-filter-dst')::text)::jsonb) and n1.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 s2_seed join edge e0 on e0.end_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e0.start_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s2.root_id), false, e0.id || s2.path from backward_front s2 join edge e0 on e0.end_id = s2.next_id where e0.kind_id = any (array [3, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s2.root_id and backward_visited.id = e0.start_id);"} -with s0 as (with s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where (((n0.properties -> ''name''))::jsonb = to_jsonb((''path-filter-src'')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (((n1.properties -> ''name''))::jsonb = to_jsonb((''path-filter-dst'')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id is not null and n1.id is not null;')::text)) select 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 ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite as pc0 from s1) select s0.pc0 as p from s0 where (((select count(*)::int from unnest(((s0.pc0).edges)::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('blocked-session-host')::text)::jsonb and i0.kind_id = 7)) = 0 and ((s0.pc0).edges)::edgecomposite[] is not null)::bool); +with s0 as (with s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where (((n0.properties -> ''name''))::jsonb = to_jsonb((''path-filter-src'')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (((n1.properties -> ''name''))::jsonb = to_jsonb((''path-filter-dst'')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id is not null and n1.id is not null;')::text)) select 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 case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as pc0 from s1) select s0.pc0 as p from s0 where (((select count(*)::int from unnest(((s0.pc0).edges)::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('blocked-session-host')::text)::jsonb and i0.kind_id = 7)) = 0 and ((s0.pc0).edges)::edgecomposite[] is not null)::bool); diff --git a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql index 28f784cf..df1fffb5 100644 --- a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql +++ b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql @@ -42,7 +42,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1 from s0, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.id = e1.end_id) select s1.e0 as r, s1.e1 as e from s1; -- case: match p = (:NodeKind1)-[:EdgeKind1|EdgeKind2]->(c:NodeKind2) where '123' in c.prop2 or '243' in c.prop2 or size(c.prop2) = 0 return p limit 10 -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) limit 10) select ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 limit 10; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) limit 10) select case when (s0.n0).id is null or s0.e0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; -- case: match ()-[r:EdgeKind1]->() return count(r) as the_count with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; @@ -80,19 +80,19 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (not ((n0.properties ->> 'bool_field'))::bool)), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n1 on (((n1.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb) and n1.id = e0.start_id join node n2 on (((n2.properties -> 'name'))::jsonb = to_jsonb(('321')::text)::jsonb) and n2.id = e0.end_id) select s1.n0 as f, s1.n1 as s, s1.e0 as r, s1.n2 as e from s1; -- case: match ()-[e0]->(n)<-[e1]-() return e0, n, e1 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id where e1.id != (s0.e0).id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; -- case: match ()-[e0]->(n)-[e1]->() return e0, n, e1 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != (s0.e0).id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; -- case: match ()<-[e0]-(n)<-[e1]-() return e0, n, e1 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id where e1.id != (s0.e0).id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; -- case: match (s)<-[r:EdgeKind1|EdgeKind2]-(e) return s.name, e.name with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.end_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; -- case: match (s)-[:EdgeKind1|EdgeKind2]->(e)-[:EdgeKind1]->() return s.name as s_name, e.name as e_name -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.n0 as n0, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[])) select ((s1.n0).properties -> 'name') as s_name, ((s1.n1).properties -> 'name') as e_name from s1; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])), s1 as (select s0.e0 as e0, s0.n0 as n0, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) and e1.id != s0.e0) select ((s1.n0).properties -> 'name') as s_name, ((s1.n1).properties -> 'name') as e_name from s1; -- case: match (s:NodeKind1)-[r:EdgeKind1|EdgeKind2]->(e:NodeKind2) return s.name, e.name with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; @@ -113,7 +113,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1 with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (n1.id <> n0.id)) select s0.n1 as n2 from s0; -- case: match ()-[r]->()-[e]->(n) where r <> e return n -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where ((s0.e0).id <> e1.id)) select s1.n2 as n from s1; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where ((s0.e0).id <> e1.id) and e1.id != (s0.e0).id) select s1.n2 as n from s1; -- case: match (s:NodeKind1:NodeKind2)-[r:EdgeKind1|EdgeKind2]->(e:NodeKind2:NodeKind1) return s.name, e.name with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1, 2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2, 1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; diff --git a/cypher/models/pgsql/test/translation_cases/update.sql b/cypher/models/pgsql/test/translation_cases/update.sql index 5fd99b66..8e54475d 100644 --- a/cypher/models/pgsql/test/translation_cases/update.sql +++ b/cypher/models/pgsql/test/translation_cases/update.sql @@ -69,4 +69,4 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (update edge e1 set properties = e1.properties || jsonb_build_object('visited', true)::jsonb from s0 where (s0.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s0.n0 as n0) select s1.e0 as r from s1; -- case: match (n)-[]->()-[r]->() where n.name = 'n1' set r.visited = true return r.name -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id), s2 as (update edge e2 set properties = e2.properties || jsonb_build_object('visited', true)::jsonb from s1 where (s1.e1).id = e2.id returning (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1) select ((s2.e1).properties -> 'name') from s2; +with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s0.e0), s2 as (update edge e2 set properties = e2.properties || jsonb_build_object('visited', true)::jsonb from s1 where (s1.e1).id = e2.id returning s1.e0 as e0, (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1) select ((s2.e1).properties -> 'name') from s2; diff --git a/cypher/models/pgsql/translate/constraints.go b/cypher/models/pgsql/translate/constraints.go index dc9ea733..7ffa97be 100644 --- a/cypher/models/pgsql/translate/constraints.go +++ b/cypher/models/pgsql/translate/constraints.go @@ -439,12 +439,12 @@ type PatternConstraints struct { // // In cases that match this heuristic, it's beneficial to begin the traversal with the most tightly constrained set // of nodes. To accomplish this we flip the order of the traversal step. -func (s *PatternConstraints) OptimizePatternConstraintBalance(scope *Scope, traversalStep *TraversalStep) error { +func (s *PatternConstraints) OptimizePatternConstraintBalance(scope *Scope, traversalStep *TraversalStep) (bool, error) { // If the left node is already materialized from a previous step, it is the anchor // for this expansion. Flipping the traversal direction would detach it from the // previous frame and produce invalid SQL (missing FROM-clause entry). if traversalStep.LeftNodeBound { - return nil + return false, nil } if traversalStep.RightNodeBound { @@ -459,21 +459,22 @@ func (s *PatternConstraints) OptimizePatternConstraintBalance(scope *Scope, trav s.FlipNodes() } - return nil + return true, nil } if leftNodeSelectivity, err := MeasureSelectivity(scope, s.LeftNode.Expression); err != nil { - return err + return false, err } else if rightNodeSelectivity, err := MeasureSelectivity(scope, s.RightNode.Expression); err != nil { - return err + return false, err } else if rightNodeSelectivity-leftNodeSelectivity >= selectivityFlipThreshold { // (a)-[*..]->(b:Constraint) // (a)<-[*..]-(b:Constraint) traversalStep.FlipNodes() s.FlipNodes() + return true, nil } - return nil + return false, nil } func (s *PatternConstraints) FlipNodes() { diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 9b26c76c..3d8a73d3 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -187,6 +187,49 @@ func newExpansionBoundNodeSeed(identifier pgsql.Identifier, previousFrame *Frame return seed } +func fromClausesContainSource(fromClauses []pgsql.FromClause, identifier pgsql.Identifier) bool { + for _, fromClause := range fromClauses { + if tableReference, isTableReference := fromClause.Source.(pgsql.TableReference); isTableReference && + len(tableReference.Name) == 1 && + tableReference.Name[0] == identifier { + return true + } + } + + return false +} + +func prependFrameSourceIfMissing(fromClauses []pgsql.FromClause, frame *Frame) []pgsql.FromClause { + if frame == nil || fromClausesContainSource(fromClauses, frame.Binding.Identifier) { + return fromClauses + } + + return append([]pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{frame.Binding.Identifier}, + }, + }}, fromClauses...) +} + +func expressionReferencesUnwindBinding(expression pgsql.Expression, unwindClauses []UnwindClause) (bool, error) { + if expression == nil || len(unwindClauses) == 0 { + return false, nil + } + + references, err := ExtractSyntaxNodeReferences(expression) + if err != nil { + return false, err + } + + for _, clause := range unwindClauses { + if clause.Binding != nil && references.Contains(clause.Binding.Identifier) { + return true, nil + } + } + + return false, nil +} + func newExpansionRootIDsParameterSeed(identifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { return newExpansionNodeFilterSeed(identifier, expansionRootFilter, nodeIdentifier, constraints) } @@ -1670,33 +1713,11 @@ func (s *ExpansionBuilder) BuildAllShortestPathsRoot() (pgsql.Query, error) { } func (s *ExpansionBuilder) canMaterializeTerminalFilter(expansionModel *Expansion) bool { - if expansionModel.TerminalNodeConstraints == nil || s.usesBoundEndpointPairs() || s.usesBoundTerminalIDs() { - return false - } - - // Terminal filters are only useful as standalone SQL when they depend solely - // on the terminal node; external references must stay in the main query. - _, externalConstraints := partitionConstraintByLocality( - expansionModel.TerminalNodeConstraints, - pgsql.AsIdentifierSet(s.traversalStep.RightNode.Identifier), - ) - - return externalConstraints == nil + return canMaterializeTerminalFilterForStep(s.traversalStep, expansionModel) } func (s *ExpansionBuilder) canMaterializeEndpointPairFilter(expansionModel *Expansion) bool { - // Pair filters enumerate the exact root/terminal combinations the - // bidirectional harness must resolve. Kind-only endpoint predicates are not - // enough because they do not constrain the search columns used by the harness. - if s.usesBoundEndpointPairs() || - expansionModel.PrimerNodeConstraints == nil || - expansionModel.TerminalNodeConstraints == nil || - !hasLocalEndpointConstraint(expansionModel.PrimerNodeConstraints, s.traversalStep.LeftNode.Identifier) || - !hasLocalEndpointConstraint(expansionModel.TerminalNodeConstraints, s.traversalStep.RightNode.Identifier) { - return false - } - - return true + return canMaterializeEndpointPairFilterForStep(s.traversalStep, expansionModel) } func (s *ExpansionBuilder) buildBiDirectionalShortestPathsHarnessCall(harnessFunctionName pgsql.Identifier) (pgsql.Query, error) { @@ -1995,11 +2016,293 @@ func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier, commonTab return query } +func projectionAliasExpressions(projection pgsql.Projection) map[pgsql.Identifier]pgsql.Expression { + aliases := make(map[pgsql.Identifier]pgsql.Expression) + + for _, selectItem := range projection { + switch typedSelectItem := selectItem.(type) { + case *pgsql.AliasedExpression: + if typedSelectItem.Alias.Set { + aliases[typedSelectItem.Alias.Value] = typedSelectItem.Expression + } + + case pgsql.AliasedExpression: + if typedSelectItem.Alias.Set { + aliases[typedSelectItem.Alias.Value] = typedSelectItem.Expression + } + + case pgsql.Identifier: + aliases[typedSelectItem] = typedSelectItem + + case pgsql.CompoundIdentifier: + if len(typedSelectItem) > 0 { + aliases[typedSelectItem[len(typedSelectItem)-1]] = typedSelectItem + } + } + } + + return aliases +} + +func rewriteCurrentFrameProjectionSetExpression(setExpression pgsql.SetExpression, frameID pgsql.Identifier, aliases map[pgsql.Identifier]pgsql.Expression) pgsql.SetExpression { + switch typedSetExpression := setExpression.(type) { + case pgsql.Select: + return rewriteCurrentFrameProjectionSelect(typedSetExpression, frameID, aliases) + + case pgsql.SetOperation: + typedSetExpression.LOperand = rewriteCurrentFrameProjectionSetExpression(typedSetExpression.LOperand, frameID, aliases) + typedSetExpression.ROperand = rewriteCurrentFrameProjectionSetExpression(typedSetExpression.ROperand, frameID, aliases) + return typedSetExpression + + default: + return setExpression + } +} + +func rewriteCurrentFrameProjectionQuery(query pgsql.Query, frameID pgsql.Identifier, aliases map[pgsql.Identifier]pgsql.Expression) pgsql.Query { + query.Body = rewriteCurrentFrameProjectionSetExpression(query.Body, frameID, aliases) + + for idx, orderBy := range query.OrderBy { + if orderBy != nil { + query.OrderBy[idx].Expression = rewriteCurrentFrameProjectionReferences(orderBy.Expression, frameID, aliases) + } + } + + query.Offset = rewriteCurrentFrameProjectionReferences(query.Offset, frameID, aliases) + query.Limit = rewriteCurrentFrameProjectionReferences(query.Limit, frameID, aliases) + + return query +} + +func rewriteCurrentFrameProjectionSelect(selectBody pgsql.Select, frameID pgsql.Identifier, aliases map[pgsql.Identifier]pgsql.Expression) pgsql.Select { + for idx, selectItem := range selectBody.Projection { + if rewritten, isSelectItem := rewriteCurrentFrameProjectionReferences(selectItem, frameID, aliases).(pgsql.SelectItem); isSelectItem { + selectBody.Projection[idx] = rewritten + } + } + + for idx := range selectBody.From { + selectBody.From[idx].Source = rewriteCurrentFrameProjectionReferences(selectBody.From[idx].Source, frameID, aliases) + + for joinIdx := range selectBody.From[idx].Joins { + selectBody.From[idx].Joins[joinIdx].Table = rewriteCurrentFrameProjectionReferences(selectBody.From[idx].Joins[joinIdx].Table, frameID, aliases) + selectBody.From[idx].Joins[joinIdx].JoinOperator.Constraint = rewriteCurrentFrameProjectionReferences(selectBody.From[idx].Joins[joinIdx].JoinOperator.Constraint, frameID, aliases) + } + } + + selectBody.Where = rewriteCurrentFrameProjectionReferences(selectBody.Where, frameID, aliases) + + for idx, groupByExpression := range selectBody.GroupBy { + selectBody.GroupBy[idx] = rewriteCurrentFrameProjectionReferences(groupByExpression, frameID, aliases) + } + + selectBody.Having = rewriteCurrentFrameProjectionReferences(selectBody.Having, frameID, aliases) + + return selectBody +} + +func rewriteCurrentFrameProjectionReferences(expression pgsql.Expression, frameID pgsql.Identifier, aliases map[pgsql.Identifier]pgsql.Expression) pgsql.Expression { + if expression == nil { + return nil + } + + switch typedExpression := expression.(type) { + case pgsql.CompoundIdentifier: + if len(typedExpression) == 2 && typedExpression[0] == frameID { + if replacement, hasReplacement := aliases[typedExpression[1]]; hasReplacement { + return replacement + } + } + + return typedExpression + + case pgsql.RowColumnReference: + typedExpression.Identifier = rewriteCurrentFrameProjectionReferences(typedExpression.Identifier, frameID, aliases) + return typedExpression + + case pgsql.UnaryExpression: + typedExpression.Operand = rewriteCurrentFrameProjectionReferences(typedExpression.Operand, frameID, aliases) + return typedExpression + + case *pgsql.UnaryExpression: + typedExpression.Operand = rewriteCurrentFrameProjectionReferences(typedExpression.Operand, frameID, aliases) + return typedExpression + + case pgsql.BinaryExpression: + typedExpression.LOperand = rewriteCurrentFrameProjectionReferences(typedExpression.LOperand, frameID, aliases) + typedExpression.ROperand = rewriteCurrentFrameProjectionReferences(typedExpression.ROperand, frameID, aliases) + return typedExpression + + case *pgsql.BinaryExpression: + typedExpression.LOperand = rewriteCurrentFrameProjectionReferences(typedExpression.LOperand, frameID, aliases) + typedExpression.ROperand = rewriteCurrentFrameProjectionReferences(typedExpression.ROperand, frameID, aliases) + return typedExpression + + case pgsql.FunctionCall: + for idx, parameter := range typedExpression.Parameters { + typedExpression.Parameters[idx] = rewriteCurrentFrameProjectionReferences(parameter, frameID, aliases) + } + return typedExpression + + case *pgsql.FunctionCall: + for idx, parameter := range typedExpression.Parameters { + typedExpression.Parameters[idx] = rewriteCurrentFrameProjectionReferences(parameter, frameID, aliases) + } + return typedExpression + + case pgsql.TypeCast: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case pgsql.CompositeValue: + for idx, value := range typedExpression.Values { + typedExpression.Values[idx] = rewriteCurrentFrameProjectionReferences(value, frameID, aliases) + } + return typedExpression + + case *pgsql.Parenthetical: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case *pgsql.EdgeArrayFromPathIDs: + typedExpression.PathIDs = rewriteCurrentFrameProjectionReferences(typedExpression.PathIDs, frameID, aliases) + return typedExpression + + case pgsql.ArrayLiteral: + for idx, value := range typedExpression.Values { + typedExpression.Values[idx] = rewriteCurrentFrameProjectionReferences(value, frameID, aliases) + } + return typedExpression + + case pgsql.ArrayExpression: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case pgsql.ArrayIndex: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + for idx, index := range typedExpression.Indexes { + typedExpression.Indexes[idx] = rewriteCurrentFrameProjectionReferences(index, frameID, aliases) + } + return typedExpression + + case *pgsql.ArrayIndex: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + for idx, index := range typedExpression.Indexes { + typedExpression.Indexes[idx] = rewriteCurrentFrameProjectionReferences(index, frameID, aliases) + } + return typedExpression + + case pgsql.ArraySlice: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + typedExpression.Lower = rewriteCurrentFrameProjectionReferences(typedExpression.Lower, frameID, aliases) + typedExpression.Upper = rewriteCurrentFrameProjectionReferences(typedExpression.Upper, frameID, aliases) + return typedExpression + + case *pgsql.ArraySlice: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + typedExpression.Lower = rewriteCurrentFrameProjectionReferences(typedExpression.Lower, frameID, aliases) + typedExpression.Upper = rewriteCurrentFrameProjectionReferences(typedExpression.Upper, frameID, aliases) + return typedExpression + + case pgsql.AllExpression: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case *pgsql.AllExpression: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case pgsql.AnyExpression: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case *pgsql.AnyExpression: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case pgsql.Case: + typedExpression.Operand = rewriteCurrentFrameProjectionReferences(typedExpression.Operand, frameID, aliases) + for idx, condition := range typedExpression.Conditions { + typedExpression.Conditions[idx] = rewriteCurrentFrameProjectionReferences(condition, frameID, aliases) + } + for idx, then := range typedExpression.Then { + typedExpression.Then[idx] = rewriteCurrentFrameProjectionReferences(then, frameID, aliases) + } + typedExpression.Else = rewriteCurrentFrameProjectionReferences(typedExpression.Else, frameID, aliases) + return typedExpression + + case *pgsql.Case: + typedExpression.Operand = rewriteCurrentFrameProjectionReferences(typedExpression.Operand, frameID, aliases) + for idx, condition := range typedExpression.Conditions { + typedExpression.Conditions[idx] = rewriteCurrentFrameProjectionReferences(condition, frameID, aliases) + } + for idx, then := range typedExpression.Then { + typedExpression.Then[idx] = rewriteCurrentFrameProjectionReferences(then, frameID, aliases) + } + typedExpression.Else = rewriteCurrentFrameProjectionReferences(typedExpression.Else, frameID, aliases) + return typedExpression + + case pgsql.ExistsExpression: + typedExpression.Subquery.Query = rewriteCurrentFrameProjectionQuery(typedExpression.Subquery.Query, frameID, aliases) + return typedExpression + + case pgsql.Subquery: + typedExpression.Query = rewriteCurrentFrameProjectionQuery(typedExpression.Query, frameID, aliases) + return typedExpression + + case pgsql.Query: + return rewriteCurrentFrameProjectionQuery(typedExpression, frameID, aliases) + + case pgsql.Select: + return rewriteCurrentFrameProjectionSelect(typedExpression, frameID, aliases) + + case pgsql.SetOperation: + typedExpression.LOperand = rewriteCurrentFrameProjectionSetExpression(typedExpression.LOperand, frameID, aliases) + typedExpression.ROperand = rewriteCurrentFrameProjectionSetExpression(typedExpression.ROperand, frameID, aliases) + return typedExpression + + case pgsql.ProjectionFrom: + for idx, selectItem := range typedExpression.Projection { + if rewritten, isSelectItem := rewriteCurrentFrameProjectionReferences(selectItem, frameID, aliases).(pgsql.SelectItem); isSelectItem { + typedExpression.Projection[idx] = rewritten + } + } + for idx := range typedExpression.From { + typedExpression.From[idx].Source = rewriteCurrentFrameProjectionReferences(typedExpression.From[idx].Source, frameID, aliases) + for joinIdx := range typedExpression.From[idx].Joins { + typedExpression.From[idx].Joins[joinIdx].JoinOperator.Constraint = rewriteCurrentFrameProjectionReferences(typedExpression.From[idx].Joins[joinIdx].JoinOperator.Constraint, frameID, aliases) + } + } + return typedExpression + + case pgsql.AliasedExpression: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case *pgsql.AliasedExpression: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case pgsql.Variadic: + typedExpression.Expression = rewriteCurrentFrameProjectionReferences(typedExpression.Expression, frameID, aliases) + return typedExpression + + case pgsql.LateralSubquery: + typedExpression.Query = rewriteCurrentFrameProjectionQuery(typedExpression.Query, frameID, aliases) + return typedExpression + + default: + return expression + } +} + func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalStepContext, expansion *ExpansionBuilder) (pgsql.Query, error) { var ( traversalStep = traversalStepContext.CurrentStep expansionModel = traversalStep.Expansion seedIdentifier = expansionSeedIdentifier(expansionModel.Frame.Binding.Identifier) + unwindClauses = s.query.CurrentPart().ConsumeUnwindClauses() + unwindSources = unwindFromClauses(unwindClauses) ) // Determine local scope of the primer: the edge and both nodes. @@ -2042,6 +2345,15 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte expansion.UseUnionAll = true } + if seed != nil { + if seedNeedsUnwind, err := expressionReferencesUnwindBinding(seedConstraints, unwindClauses); err != nil { + return pgsql.Query{}, err + } else if seedNeedsUnwind { + seed.query.From = prependFrameSourceIfMissing(seed.query.From, traversalStep.Frame.Previous) + seed.query.From = append(seed.query.From, unwindSources...) + } + } + expansion.PrimerStatement.Where = expansionModel.EdgeConstraints expansion.ProjectionStatement.Projection = expansionModel.Projection @@ -2078,6 +2390,12 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte } expansion.PrimerStatement.From = append(expansion.PrimerStatement.From, nextQueryFrom) + if primerNeedsUnwind, err := expressionReferencesUnwindBinding(expansionModel.EdgeConstraints, unwindClauses); err != nil { + return pgsql.Query{}, err + } else if primerNeedsUnwind { + expansion.PrimerStatement.From = prependFrameSourceIfMissing(expansion.PrimerStatement.From, traversalStep.Frame.Previous) + expansion.PrimerStatement.From = append(expansion.PrimerStatement.From, unwindSources...) + } if expansionAllowsZeroDepth(expansionModel) { zeroDepthStatement, err := expansion.buildZeroDepthExpansionSelect(seed) @@ -2123,6 +2441,8 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte }) } + expansion.ProjectionStatement.From = append(expansion.ProjectionStatement.From, unwindSources...) + // Select the expansion components for the projection statement expansion.ProjectionStatement.From = append(expansion.ProjectionStatement.From, pgsql.FromClause{ Source: pgsql.TableReference{ @@ -2156,6 +2476,11 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte ) } + projectionConstraints = rewriteCurrentFrameProjectionReferences( + projectionConstraints, + traversalStep.Frame.Binding.Identifier, + projectionAliasExpressions(expansion.ProjectionStatement.Projection), + ) expansion.ProjectionStatement.Where = projectionConstraints } @@ -2270,6 +2595,11 @@ func (s *Translator) buildExpansionPatternStep(traversalStepContext TraversalSte if projectionConstraints, err := s.buildExpansionProjectionConstraints(traversalStepContext); err != nil { return pgsql.Query{}, err } else { + projectionConstraints = rewriteCurrentFrameProjectionReferences( + projectionConstraints, + traversalStep.Frame.Binding.Identifier, + projectionAliasExpressions(expansion.ProjectionStatement.Projection), + ) expansion.ProjectionStatement.Where = projectionConstraints } @@ -2368,7 +2698,12 @@ func suffixStepEdgeConstraints(step *TraversalStep) pgsql.Expression { return nil } - return step.EdgeConstraints.Expression + localConstraints, _ := partitionConstraintByLocality( + step.EdgeConstraints.Expression, + pgsql.AsIdentifierSet(step.Edge.Identifier), + ) + + return localConstraints } func expansionSuffixTerminalSatisfaction(currentStep *TraversalStep, suffixSteps []*TraversalStep) (pgsql.Expression, bool) { @@ -2657,7 +2992,7 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar // Translate the expansion's constraints - this has the side effect of making the pattern identifiers visible in // the current scope frame - if err := s.translateExpansionConstraints(isFirstTraversalStep, traversalStep, expansionModel); err != nil { + if err := s.translateExpansionConstraints(part, stepIndex, isFirstTraversalStep, traversalStep, expansionModel); err != nil { return err } @@ -2666,7 +3001,7 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar if allowProjectionPruning { decision, hasDecision := s.projectionPruningDecision(part, stepIndex) allowFallback := !hasDecision && (part == nil || !part.HasTarget) - if pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, traversalStep, decision, hasDecision, allowFallback) { + if pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision, allowFallback) { s.recordLowering(optimize.LoweringProjectionPruning) } @@ -2729,7 +3064,7 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar } if expansionModel.Options.FindShortestPath || expansionModel.Options.FindAllShortestPaths { - if err := s.translateShortestPathTraversal(traversalStep, expansionModel); err != nil { + if err := s.translateShortestPathTraversal(part, stepIndex, traversalStep, expansionModel); err != nil { return err } } @@ -2737,13 +3072,13 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar return nil } -func (s *Translator) translateExpansionConstraints(isFirstTraversalStep bool, step *TraversalStep, expansionModel *Expansion) error { +func (s *Translator) translateExpansionConstraints(part *PatternPart, stepIndex int, isFirstTraversalStep bool, step *TraversalStep, expansionModel *Expansion) error { if constraints, err := consumePatternConstraints(isFirstTraversalStep, recursivePattern, step, s.treeTranslator); err != nil { return err } else { // If one side of the expansion has constraints but the other does not this may be an opportunity to reorder the traversal // to start with tighter search bounds - if err := constraints.OptimizePatternConstraintBalance(s.scope, step); err != nil { + if err := s.applyPatternConstraintBalance(part, stepIndex, &constraints, step); err != nil { return err } @@ -2810,13 +3145,13 @@ func (s *Translator) translateExpansionConstraints(isFirstTraversalStep bool, st return nil } -func (s *Translator) translateShortestPathTraversal(traversalStep *TraversalStep, expansionModel *Expansion) error { +func (s *Translator) translateShortestPathTraversal(part *PatternPart, stepIndex int, traversalStep *TraversalStep, expansionModel *Expansion) error { var ( useBidirectionalSearch bool err error ) - useBidirectionalSearch, err = traversalStep.CanExecutePairAwareBidirectionalSearch(s.scope) + useBidirectionalSearch, err = s.useBidirectionalShortestPathStrategy(part, stepIndex, traversalStep) if err != nil { return err @@ -2827,6 +3162,7 @@ func (s *Translator) translateShortestPathTraversal(traversalStep *TraversalStep traversalStep.LeftNode.Identifier, traversalStep.RightNode.Identifier, ) + s.applyShortestPathFilterMaterialization(part, stepIndex, traversalStep, expansionModel) // If this query is a shortest-path look up, the translator will have to use a function harness for // traversal. As such, query fragments for the traversal harness will have to be passed by the parameters diff --git a/cypher/models/pgsql/translate/limit_pushdown_test.go b/cypher/models/pgsql/translate/limit_pushdown_test.go index 7334b2fb..0bfd6ef9 100644 --- a/cypher/models/pgsql/translate/limit_pushdown_test.go +++ b/cypher/models/pgsql/translate/limit_pushdown_test.go @@ -75,6 +75,7 @@ func limitPushdownTestJoin(nodeAlias, expansionColumn pgsql.Identifier) pgsql.Jo func limitPushdownTestPart(harnessFunction pgsql.Identifier) *QueryPart { part := NewQueryPart(1, 0) part.Limit = pgsql.NewLiteral(10, pgsql.Int) + part.AllowLimitPushdown(limitPushdownTestSourceFrame) part.Model.AddCTE(pgsql.CommonTableExpression{ Alias: pgsql.TableAlias{Name: limitPushdownTestSourceFrame}, Query: pgsql.Query{ diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index ca16e2d6..2a0ed669 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -145,6 +145,46 @@ func (s *TraversalStep) usesBoundEndpointPairs() bool { return s.LeftNodeBound && s.RightNodeBound && s.hasPreviousFrameBinding() } +func (s *TraversalStep) usesBoundTerminalIDs() bool { + return s.RightNodeBound && s.hasPreviousFrameBinding() +} + +func canMaterializeTerminalFilterForStep(traversalStep *TraversalStep, expansionModel *Expansion) bool { + if traversalStep == nil || expansionModel == nil || traversalStep.RightNode == nil || + expansionModel.TerminalNodeConstraints == nil || + traversalStep.usesBoundEndpointPairs() || + traversalStep.usesBoundTerminalIDs() { + return false + } + + // Terminal filters are only useful as standalone SQL when they depend solely + // on the terminal node; external references must stay in the main query. + _, externalConstraints := partitionConstraintByLocality( + expansionModel.TerminalNodeConstraints, + pgsql.AsIdentifierSet(traversalStep.RightNode.Identifier), + ) + + return externalConstraints == nil +} + +func canMaterializeEndpointPairFilterForStep(traversalStep *TraversalStep, expansionModel *Expansion) bool { + // Pair filters enumerate the exact root/terminal combinations the + // bidirectional harness must resolve. Kind-only endpoint predicates are not + // enough because they do not constrain the search columns used by the harness. + if traversalStep == nil || expansionModel == nil || + traversalStep.LeftNode == nil || + traversalStep.RightNode == nil || + traversalStep.usesBoundEndpointPairs() || + expansionModel.PrimerNodeConstraints == nil || + expansionModel.TerminalNodeConstraints == nil || + !hasLocalEndpointConstraint(expansionModel.PrimerNodeConstraints, traversalStep.LeftNode.Identifier) || + !hasLocalEndpointConstraint(expansionModel.TerminalNodeConstraints, traversalStep.RightNode.Identifier) { + return false + } + + return true +} + func (s *TraversalStep) endpointSelectivity(scope *Scope, expression pgsql.Expression, bound bool) (int, error) { selectivity, err := MeasureSelectivity(scope, expression) if err != nil { diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 9a2dbf8b..25a57484 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -53,16 +53,24 @@ func optimizerSafetyKindMapper() *pgutil.InMemoryKindMapper { func optimizerSafetySQL(t *testing.T, cypherQuery string) string { t.Helper() - regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) + translation := optimizerSafetyTranslation(t, cypherQuery) + + formattedQuery, err := Translated(translation) require.NoError(t, err) - translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + return strings.Join(strings.Fields(formattedQuery), " ") +} + +func optimizerSafetyTranslation(t *testing.T, cypherQuery string) Result { + t.Helper() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) require.NoError(t, err) - formattedQuery, err := Translated(translation) + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) require.NoError(t, err) - return strings.Join(strings.Fields(formattedQuery), " ") + return translation } func requireOptimizationLowering(t *testing.T, summary OptimizationSummary, name string) { @@ -97,6 +105,14 @@ func requirePlannedOptimizationLowering(t *testing.T, summary OptimizationSummar require.Failf(t, "missing planned optimization lowering", "expected planned lowering %q in %#v", name, summary.PlannedLowerings) } +func requireNoPlannedOptimizationLowering(t *testing.T, summary OptimizationSummary, name string) { + t.Helper() + + for _, lowering := range summary.PlannedLowerings { + require.NotEqualf(t, name, lowering.Name, "unexpected planned lowering %q in %#v", name, summary.PlannedLowerings) + } +} + func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { t.Helper() @@ -255,6 +271,122 @@ RETURN p require.Contains(t, normalizedQuery, "n2.kind_ids operator (pg_catalog.@>) array [5]::int2[]") } +func TestOptimizerSafetySuffixPredicatePlacementStaysInsideTerminalExists(t *testing.T) { + t.Parallel() + + normalizedQuery := optimizerSafetySQL(t, ` +MATCH p = (n:Group)-[:MemberOf*1..]->(m)-[:Enroll]->(ca:EnterpriseCA) +WHERE ca.name = 'target' +RETURN p +`) + + requireSQLContainsInOrder(t, normalizedQuery, + "exists (select 1 from edge e1 join node n2", + "properties -> 'name'", + "where n1.id = e1.start_id", + ) +} + +func TestOptimizerSafetyContinuationRelationshipsExcludePriorPathRelationships(t *testing.T) { + t.Parallel() + + expandedPrefixQuery := optimizerSafetySQL(t, ` +MATCH p = (n:Group)-[:MemberOf*1..]->(m)-[:Enroll]-(ca:EnterpriseCA) +RETURN p +`) + + require.Contains(t, expandedPrefixQuery, "e1.id != all") + require.Contains(t, expandedPrefixQuery, "ep0") + + fixedPrefixQuery := optimizerSafetySQL(t, ` +MATCH p = (n:Group)-[:MemberOf]->(m)-[:Enroll]->(ca:EnterpriseCA) +RETURN p +`) + + require.Contains(t, fixedPrefixQuery, "e1.id != s0.e0") +} + +func TestOptimizerSafetyDirectionBalancedExpansionDoesNotPlanStaleSuffixPushdown(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH p = (n)-[:MemberOf*1..]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(d:Domain) +RETURN p + `) + + requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireNoPlannedOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") + requireNoOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") +} + +func TestOptimizerSafetyShortestPathStrategyUsesPlannedBidirectionalSearch(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH p = allShortestPaths((s)-[:MemberOf*1..]->(e)) +WHERE s.name = 'source' AND e.name = 'target' +RETURN p + `) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "bidirectional_asp_harness") + requirePlannedOptimizationLowering(t, translation.Optimization, "ShortestPathStrategySelection") + requirePlannedOptimizationLowering(t, translation.Optimization, "ShortestPathFilterMaterialization") + requireOptimizationLowering(t, translation.Optimization, "ShortestPathStrategySelection") + requireOptimizationLowering(t, translation.Optimization, "ShortestPathFilterMaterialization") +} + +func TestOptimizerSafetyShortestPathTerminalFilterUsesPlannedMaterialization(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (s:Group {name: 'source'}) +MATCH p = shortestPath((s)-[:MemberOf*1..]->(e)) +WHERE e.name = 'target' +RETURN p + `) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "unidirectional_sp_harness") + require.Contains(t, normalizedQuery, "traversal_terminal_filter") + requirePlannedOptimizationLowering(t, translation.Optimization, "ShortestPathFilterMaterialization") + requireOptimizationLowering(t, translation.Optimization, "ShortestPathFilterMaterialization") +} + +func TestOptimizerSafetyLimitPushdownUsesPlannedTraversalFrame(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH p = (n:Group)-[:MemberOf]->(m:Group) +RETURN p +LIMIT 1 + `) + + requirePlannedOptimizationLowering(t, translation.Optimization, "LimitPushdown") + requireOptimizationLowering(t, translation.Optimization, "LimitPushdown") +} + +func TestOptimizerSafetyShortestPathLimitPushdownUsesPlannedHarness(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH p = shortestPath((s)-[:MemberOf*1..]->(e)) +WHERE s.name = 'source' AND e.name = 'target' +RETURN p +LIMIT 1 + `) + + requirePlannedOptimizationLowering(t, translation.Optimization, "LimitPushdown") + requireOptimizationLowering(t, translation.Optimization, "LimitPushdown") +} + func TestOptimizerSafetyTranslationReportsOptimizerMetadata(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/path_functions.go b/cypher/models/pgsql/translate/path_functions.go index 909eac3e..4b2951fc 100644 --- a/cypher/models/pgsql/translate/path_functions.go +++ b/cypher/models/pgsql/translate/path_functions.go @@ -144,6 +144,24 @@ func resolvePathCompositeFieldReferences(scope *Scope, expression pgsql.Expressi case nil: return nil, nil + case pgsql.Identifier: + if binding, bound := scope.Lookup(typedExpression); !bound { + if aliasedBinding, aliasBound := scope.AliasedLookup(typedExpression); aliasBound { + binding = aliasedBinding + bound = true + } + + if !bound || binding.DataType != pgsql.PathComposite { + return expression, nil + } + + return expressionForPathComposite(binding, scope) + } else if binding.DataType == pgsql.PathComposite { + return expressionForPathComposite(binding, scope) + } + + return expression, nil + case pgsql.RowColumnReference: if resolved, rewritten, err := resolvePathCompositeFieldReference(scope, typedExpression); rewritten || err != nil { return resolved, err diff --git a/cypher/models/pgsql/translate/pattern.go b/cypher/models/pgsql/translate/pattern.go index 9383999f..163dc8d1 100644 --- a/cypher/models/pgsql/translate/pattern.go +++ b/cypher/models/pgsql/translate/pattern.go @@ -84,7 +84,6 @@ func (s *Translator) buildTraversalPattern(traversalStep *TraversalStep, isRootS }, Query: traversalStepQuery, }) - s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } else { if traversalStepQuery, err := s.buildTraversalPatternStep(traversalStep.Frame, traversalStep); err != nil { @@ -96,7 +95,6 @@ func (s *Translator) buildTraversalPattern(traversalStep *TraversalStep, isRootS }, Query: traversalStepQuery, }) - s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } @@ -116,7 +114,6 @@ func (s *Translator) buildExpansionPattern(traversalStepContext TraversalStepCon }, Query: traversalStepQuery, }) - s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } else { if traversalStepQuery, err := s.buildExpansionPatternStep(traversalStepContext, expansion); err != nil { @@ -128,7 +125,6 @@ func (s *Translator) buildExpansionPattern(traversalStepContext TraversalStepCon }, Query: traversalStepQuery, }) - s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) } } @@ -233,6 +229,8 @@ func (s *Translator) buildTraversalPatternPart(part *PatternPart) error { } else if err := s.buildTraversalPattern(traversalStep, isRootStep); err != nil { return err } + + s.allowLimitPushdownForStep(part, idx, traversalStep) } return nil diff --git a/cypher/models/pgsql/translate/projection.go b/cypher/models/pgsql/translate/projection.go index 722e96f2..a4f4e64c 100644 --- a/cypher/models/pgsql/translate/projection.go +++ b/cypher/models/pgsql/translate/projection.go @@ -8,6 +8,7 @@ import ( "github.com/specterops/dawgs/cypher/models" "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" ) type BoundProjections struct { @@ -226,6 +227,55 @@ func expansionPathEdgeArrayExpression(scope *Scope, expansionPath *BoundIdentifi }, nil } +func optionalOr(leftOperand, rightOperand pgsql.Expression) pgsql.Expression { + if leftOperand == nil { + return rightOperand + } else if rightOperand == nil { + return leftOperand + } + + return pgsql.NewBinaryExpression(leftOperand, pgsql.OperatorOr, rightOperand) +} + +func expressionIsNull(expression pgsql.Expression) pgsql.Expression { + return pgsql.NewBinaryExpression(expression, pgsql.OperatorIs, pgsql.NullLiteral()) +} + +func pathCompositeDependencyNullGuard(scope *Scope, dependency *BoundIdentifier) pgsql.Expression { + if dependency == nil { + return nil + } + + switch dependency.DataType { + case pgsql.ExpansionPath: + return expressionIsNull(pathBindingReference(scope, dependency)) + + case pgsql.EdgeComposite: + return expressionIsNull(pathCompositeColumnReference(scope, dependency, pgsql.ColumnID)) + + case pgsql.PathEdge: + return expressionIsNull(pathEdgeIDReference(scope, dependency)) + + case pgsql.NodeComposite, pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode: + return expressionIsNull(pathCompositeColumnReference(scope, dependency, pgsql.ColumnID)) + + default: + return nil + } +} + +func nullGuardPathCompositeExpression(expression, nullGuard pgsql.Expression) pgsql.Expression { + if nullGuard == nil { + return expression + } + + return pgsql.Case{ + Conditions: []pgsql.Expression{nullGuard}, + Then: []pgsql.Expression{pgsql.NullLiteral()}, + Else: expression, + } +} + func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql.Expression, error) { if projected.LastProjection != nil { return pgsql.CompoundIdentifier{projected.LastProjection.Binding.Identifier, projected.Identifier}, nil @@ -238,12 +288,15 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql directEdgeReferences []pgsql.Expression seenExpansionPath = false seenPathEdge = false + nullGuard pgsql.Expression ) // Path composite components are encoded as dependencies on the bound identifier representing the // path. This is not ideal as it escapes normal translation flow as driven by the structure of the // originating cypher AST. for _, dependency := range projected.Dependencies { + nullGuard = optionalOr(nullGuard, pathCompositeDependencyNullGuard(scope, dependency)) + switch dependency.DataType { case pgsql.ExpansionPath: seenExpansionPath = true @@ -280,7 +333,7 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql // 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 && !seenPathEdge && len(directNodeReferences) > 0 { - return pgsql.CompositeValue{ + return nullGuardPathCompositeExpression(pgsql.CompositeValue{ DataType: pgsql.PathComposite, Values: []pgsql.Expression{ pgsql.ArrayLiteral{ @@ -292,7 +345,7 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql CastType: pgsql.EdgeCompositeArray, }, }, - }, nil + }, nullGuard), nil } if seenExpansionPath || seenPathEdge { @@ -305,7 +358,7 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql edgeArrayExpression = pgsql.ArrayLiteral{CastType: pgsql.EdgeCompositeArray} } - return pgsql.FunctionCall{ + return nullGuardPathCompositeExpression(pgsql.FunctionCall{ Function: pgsql.FunctionOrderedEdgesToPath, Parameters: []pgsql.Expression{ directNodeReferences[0], @@ -316,9 +369,9 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql }, }, CastType: pgsql.PathComposite, - }, nil + }, nullGuard), nil } else if len(nodeReferences) > 0 { - return pgsql.FunctionCall{ + return nullGuardPathCompositeExpression(pgsql.FunctionCall{ Function: pgsql.FunctionNodesToPath, Parameters: []pgsql.Expression{ pgsql.Variadic{ @@ -329,7 +382,7 @@ func expressionForPathComposite(projected *BoundIdentifier, scope *Scope) (pgsql }, }, CastType: pgsql.PathComposite, - }, nil + }, nullGuard), nil } return nil, fmt.Errorf("path variable does not contain valid components") @@ -961,18 +1014,22 @@ func limitPushdownTailSource(currentPart *QueryPart, tailSelect pgsql.Select) (p return sourceFrame, true } -func pushDownShortestPathLimit(currentPart *QueryPart, tailSelect pgsql.Select) { +func pushDownShortestPathLimit(currentPart *QueryPart, tailSelect pgsql.Select) bool { sourceFrame, canPushDown := limitPushdownTailSource(currentPart, tailSelect) if !canPushDown { - return + return false } if sourceCTE := findCTE(currentPart.Model, sourceFrame); sourceCTE != nil && + currentPart.CanPushDownLimitTo(sourceFrame) && countLimitPushdownShortestPathHarnessCalls(sourceCTE.Query) == 1 { // Multiple harness calls in one source CTE would make one outer LIMIT // ambiguous, so only the single-harness case is rewritten. appendLimitToShortestPathHarness(&sourceCTE.Query, currentPart.Limit) + return true } + + return false } func findCTE(query *pgsql.Query, cteName pgsql.Identifier) *pgsql.CommonTableExpression { @@ -1000,13 +1057,13 @@ func applyLimitToCTE(query *pgsql.Query, cteName pgsql.Identifier, limit pgsql.E return false } -func pushDownTraversalLimit(currentPart *QueryPart, tailSelect pgsql.Select) { +func pushDownTraversalLimit(currentPart *QueryPart, tailSelect pgsql.Select) bool { sourceFrame, canPushDown := limitPushdownTailSource(currentPart, tailSelect) if !canPushDown || !currentPart.CanPushDownLimitTo(sourceFrame) { - return + return false } - applyLimitToCTE(currentPart.Model, sourceFrame, currentPart.Limit) + return applyLimitToCTE(currentPart.Model, sourceFrame, currentPart.Limit) } func projectionAliasBindings(scope *Scope, projections []*Projection) map[pgsql.Identifier]pgsql.Identifier { @@ -1093,8 +1150,10 @@ func (s *Translator) buildTailProjection() error { } currentPart.Model.Body = singlePartQuerySelect - pushDownShortestPathLimit(currentPart, singlePartQuerySelect) - pushDownTraversalLimit(currentPart, singlePartQuerySelect) + if pushDownShortestPathLimit(currentPart, singlePartQuerySelect) || + pushDownTraversalLimit(currentPart, singlePartQuerySelect) { + s.recordLowering(optimize.LoweringLimitPushdown) + } if currentPart.Skip != nil { currentPart.Model.Offset = currentPart.Skip diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index c59c4cca..d7517b65 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -30,11 +30,15 @@ type Translator struct { scope *Scope unwindTargets map[*cypher.Variable]struct{} - patternTargets map[*cypher.PatternPart]optimize.PatternTarget - projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision - latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision - suffixPushdownDecisions map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision - expandIntoDecisions map[optimize.TraversalStepTarget]optimize.ExpandIntoDecision + patternTargets map[*cypher.PatternPart]optimize.PatternTarget + projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision + latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision + suffixPushdownDecisions map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision + expandIntoDecisions map[optimize.TraversalStepTarget]optimize.ExpandIntoDecision + traversalDirectionDecisions map[optimize.TraversalStepTarget]optimize.TraversalDirectionDecision + shortestPathStrategyDecisions map[optimize.TraversalStepTarget]optimize.ShortestPathStrategyDecision + shortestPathFilterDecisions map[optimize.TraversalStepTarget][]optimize.ShortestPathFilterDecision + limitPushdownDecisions map[optimize.TraversalStepTarget][]optimize.LimitPushdownDecision } func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) *Translator { @@ -72,6 +76,10 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.latePathDecisions = map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision{} s.suffixPushdownDecisions = map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision{} s.expandIntoDecisions = map[optimize.TraversalStepTarget]optimize.ExpandIntoDecision{} + s.traversalDirectionDecisions = map[optimize.TraversalStepTarget]optimize.TraversalDirectionDecision{} + s.shortestPathStrategyDecisions = map[optimize.TraversalStepTarget]optimize.ShortestPathStrategyDecision{} + s.shortestPathFilterDecisions = map[optimize.TraversalStepTarget][]optimize.ShortestPathFilterDecision{} + s.limitPushdownDecisions = map[optimize.TraversalStepTarget][]optimize.LimitPushdownDecision{} for _, decision := range plan.LoweringPlan.ProjectionPruning { s.projectionPruningDecisions[decision.Target] = decision @@ -88,6 +96,22 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { for _, decision := range plan.LoweringPlan.ExpandInto { s.expandIntoDecisions[decision.Target] = decision } + + for _, decision := range plan.LoweringPlan.TraversalDirection { + s.traversalDirectionDecisions[decision.Target] = decision + } + + for _, decision := range plan.LoweringPlan.ShortestPathStrategy { + s.shortestPathStrategyDecisions[decision.Target] = decision + } + + for _, decision := range plan.LoweringPlan.ShortestPathFilter { + s.shortestPathFilterDecisions[decision.Target] = append(s.shortestPathFilterDecisions[decision.Target], decision) + } + + for _, decision := range plan.LoweringPlan.LimitPushdown { + s.limitPushdownDecisions[decision.Target] = append(s.limitPushdownDecisions[decision.Target], decision) + } } func (s *Translator) Enter(expression cypher.SyntaxNode) { @@ -102,6 +126,13 @@ func (s *Translator) Enter(expression cypher.SyntaxNode) { *cypher.Return, *cypher.MultiPartQuery, *cypher.Properties, *cypher.KindMatcher, *cypher.Quantifier, *cypher.IDInCollection: + case *cypher.RangeQuantifier: + if typedExpression.Value != string(pgsql.WildcardIdentifier) { + s.SetErrorf("unsupported range quantifier expression: %s", typedExpression.Value) + } else { + s.treeTranslator.PushOperand(pgsql.WildcardIdentifier) + } + case *cypher.Unwind: if typedExpression.Variable != nil { // The UNWIND target is declared by the UNWIND clause itself, so later diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 2e02e250..c8036872 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -43,6 +43,134 @@ func (s *Translator) shouldUseExpandInto(part *PatternPart, stepIndex int, trave return true } +func (s *Translator) traversalDirectionDecision(part *PatternPart, stepIndex int) (optimize.TraversalDirectionDecision, bool) { + if part == nil || !part.HasTarget { + return optimize.TraversalDirectionDecision{}, false + } + + decision, hasDecision := s.traversalDirectionDecisions[part.Target.TraversalStep(stepIndex)] + return decision, hasDecision +} + +func (s *Translator) applyPatternConstraintBalance(part *PatternPart, stepIndex int, constraints *PatternConstraints, traversalStep *TraversalStep) error { + if decision, hasDecision := s.traversalDirectionDecision(part, stepIndex); hasDecision { + if decision.Flip && !traversalStep.LeftNodeBound { + if traversalStep.RightNodeBound && !traversalStep.hasPreviousFrameBinding() { + return nil + } + + traversalStep.FlipNodes() + constraints.FlipNodes() + s.recordLowering(optimize.LoweringTraversalDirection) + } + + return nil + } + + if flipped, err := constraints.OptimizePatternConstraintBalance(s.scope, traversalStep); err != nil { + return err + } else if flipped { + s.recordLowering(optimize.LoweringTraversalDirection) + } + + return nil +} + +func (s *Translator) shortestPathStrategyDecision(part *PatternPart, stepIndex int) (optimize.ShortestPathStrategyDecision, bool) { + if part == nil || !part.HasTarget { + return optimize.ShortestPathStrategyDecision{}, false + } + + decision, hasDecision := s.shortestPathStrategyDecisions[part.Target.TraversalStep(stepIndex)] + return decision, hasDecision +} + +func (s *Translator) useBidirectionalShortestPathStrategy(part *PatternPart, stepIndex int, traversalStep *TraversalStep) (bool, error) { + if decision, hasDecision := s.shortestPathStrategyDecision(part, stepIndex); hasDecision { + if decision.Strategy != optimize.ShortestPathStrategyBidirectional { + return false, nil + } + + if canExecute, err := traversalStep.CanExecutePairAwareBidirectionalSearch(s.scope); err != nil { + return false, err + } else if canExecute { + s.recordLowering(optimize.LoweringShortestPathStrategy) + return true, nil + } + + return false, nil + } + + if canExecute, err := traversalStep.CanExecutePairAwareBidirectionalSearch(s.scope); err != nil { + return false, err + } else if canExecute { + s.recordLowering(optimize.LoweringShortestPathStrategy) + return true, nil + } + + return false, nil +} + +func (s *Translator) shortestPathFilterDecisionsForStep(part *PatternPart, stepIndex int) []optimize.ShortestPathFilterDecision { + if part == nil || !part.HasTarget { + return nil + } + + return s.shortestPathFilterDecisions[part.Target.TraversalStep(stepIndex)] +} + +func (s *Translator) applyShortestPathFilterMaterialization(part *PatternPart, stepIndex int, traversalStep *TraversalStep, expansionModel *Expansion) { + for _, decision := range s.shortestPathFilterDecisionsForStep(part, stepIndex) { + switch decision.Mode { + case optimize.ShortestPathFilterTerminal: + if canMaterializeTerminalFilterForStep(traversalStep, expansionModel) { + expansionModel.UseMaterializedTerminalFilter = true + s.recordLowering(optimize.LoweringShortestPathFilter) + } + + case optimize.ShortestPathFilterEndpointPair: + if expansionModel.UseBidirectionalSearch && canMaterializeEndpointPairFilterForStep(traversalStep, expansionModel) { + expansionModel.UseMaterializedEndpointPairFilter = true + s.recordLowering(optimize.LoweringShortestPathFilter) + } + } + } +} + +func (s *Translator) hasLimitPushdownDecision(part *PatternPart, stepIndex int, mode optimize.LimitPushdownMode) bool { + if part == nil || !part.HasTarget { + return true + } + + for _, decision := range s.limitPushdownDecisions[part.Target.TraversalStep(stepIndex)] { + if decision.Mode == mode { + return true + } + } + + return false +} + +func (s *Translator) allowLimitPushdownForStep(part *PatternPart, stepIndex int, traversalStep *TraversalStep) { + if traversalStep == nil || traversalStep.Frame == nil { + return + } + if traversalStep.Expansion != nil && traversalStep.Expansion.Options.FindAllShortestPaths { + return + } + + mode := optimize.LimitPushdownTraversalCTE + if traversalStep.Expansion != nil && + traversalStep.Expansion.Options.FindShortestPath && + !traversalStep.Expansion.Options.FindAllShortestPaths { + mode = optimize.LimitPushdownShortestPathHarness + } + + if s.hasLimitPushdownDecision(part, stepIndex, mode) { + s.query.CurrentPart().AllowLimitPushdown(traversalStep.Frame.Binding.Identifier) + } +} + func (s *Translator) buildBoundEndpointTraversalPattern(partFrame *Frame, traversalStep *TraversalStep) (pgsql.Query, error) { if partFrame == nil || partFrame.Previous == nil { return pgsql.Query{}, errors.New("expected previous frame for bound endpoint traversal") @@ -694,6 +822,65 @@ func patternBindingDependsOn(queryPart *QueryPart, part *PatternPart, binding *B return false } +func traversalStepHasContinuation(part *PatternPart, stepIndex int) bool { + return part != nil && stepIndex+1 < len(part.TraversalSteps) +} + +func relationshipIDReference(scope *Scope, binding *BoundIdentifier) pgsql.Expression { + if binding != nil && binding.DataType == pgsql.EdgeComposite { + return pathCompositeColumnReference(scope, binding, pgsql.ColumnID) + } + + return pathEdgeIDReference(scope, binding) +} + +func relationshipIDNotInPath(edgeID, pathIDs pgsql.Expression) pgsql.Expression { + return pgsql.NewBinaryExpression( + edgeID, + pgsql.OperatorNotEquals, + pgsql.NewAllExpression(pathIDs), + ) +} + +func previousRelationshipUniquenessConstraint(scope *Scope, part *PatternPart, stepIndex int, traversalStep *TraversalStep) pgsql.Expression { + if scope == nil || part == nil || stepIndex <= 0 || traversalStep == nil || traversalStep.Edge == nil { + return nil + } + + var ( + currentEdgeID pgsql.Expression = pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnID} + constraint pgsql.Expression + ) + + for _, previousStep := range part.TraversalSteps[:stepIndex] { + if previousStep == nil || previousStep.Edge == nil { + continue + } + + if previousStep.Expansion != nil { + if previousStep.Expansion.PathBinding != nil { + constraint = pgsql.OptionalAnd( + constraint, + relationshipIDNotInPath(currentEdgeID, pathBindingReference(scope, previousStep.Expansion.PathBinding)), + ) + } + + continue + } + + constraint = pgsql.OptionalAnd( + constraint, + pgsql.NewBinaryExpression( + currentEdgeID, + pgsql.OperatorNotEquals, + relationshipIDReference(scope, previousStep.Edge), + ), + ) + } + + return constraint +} + func (s *Translator) projectionPruningDecision(part *PatternPart, stepIndex int) (optimize.ProjectionPruningDecision, bool) { if part == nil || !part.HasTarget { return optimize.ProjectionPruningDecision{}, false @@ -766,7 +953,11 @@ func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepI return true } - if stepIndex+1 < len(part.TraversalSteps) { + if traversalStepHasContinuation(part, stepIndex) { + if part.TraversalSteps[stepIndex].Edge == binding { + return true + } + // A multi-hop pattern needs the right node from this step as the next // step's left node even when the user never projects it. nextStep := part.TraversalSteps[stepIndex+1] @@ -815,7 +1006,7 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart if hasDecision { applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.LeftNode) || applied - if traversalStep.ProjectionPruning.Relationship != nil { + if traversalStep.ProjectionPruning.Relationship != nil && !traversalStepHasContinuation(part, stepIndex) { applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied } applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.RightNode) || applied @@ -851,7 +1042,7 @@ func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart return applied } -func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) bool { +func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) bool { if traversalStep == nil || traversalStep.Expansion == nil { return false } @@ -862,7 +1053,7 @@ func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied } - if traversalStep.ProjectionPruning.PathBinding != nil { + if traversalStep.ProjectionPruning.PathBinding != nil && !traversalStepHasContinuation(part, stepIndex) { applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.PathBinding.Identifier) || applied } @@ -882,7 +1073,7 @@ func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart } pathBinding := traversalStep.Expansion.PathBinding - if pathBinding != nil && !patternBindingDependsOn(queryPart, part, pathBinding) { + if pathBinding != nil && !traversalStepHasContinuation(part, stepIndex) && !patternBindingDependsOn(queryPart, part, pathBinding) { applied = unexportFrameBinding(traversalStep.Frame, pathBinding.Identifier) || applied } @@ -896,7 +1087,7 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern return err } else { if isFirstTraversalStep { - if err := constraints.OptimizePatternConstraintBalance(s.scope, traversalStep); err != nil { + if err := s.applyPatternConstraintBalance(part, stepIndex, &constraints, traversalStep); err != nil { return err } @@ -948,6 +1139,10 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } else { traversalStep.EdgeConstraints = constraints.Edge } + traversalStep.EdgeConstraints.Expression = pgsql.OptionalAnd( + traversalStep.EdgeConstraints.Expression, + previousRelationshipUniquenessConstraint(s.scope, part, stepIndex, traversalStep), + ) traversalStep.Frame.Export(traversalStep.RightNode.Identifier) @@ -967,6 +1162,13 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } if allowProjectionPruning { + if traversalStepHasContinuation(part, stepIndex) && + traversalStep.Edge != nil && + traversalStep.Edge.DataType == pgsql.EdgeComposite && + !s.query.CurrentPart().ReferencesBinding(traversalStep.Edge) { + traversalStep.Edge.DataType = pgsql.PathEdge + } + if s.applyPathEdgeIDMaterialization(part, stepIndex, traversalStep) { s.recordLowering(optimize.LoweringLatePathMaterialization) } diff --git a/cypher/models/pgsql/translate/with.go b/cypher/models/pgsql/translate/with.go index 6009940d..b8b66f60 100644 --- a/cypher/models/pgsql/translate/with.go +++ b/cypher/models/pgsql/translate/with.go @@ -25,6 +25,14 @@ func (s *Translator) translateWith() error { for _, projectionItem := range currentPart.projections.Items { if err := RewriteFrameBindings(s.scope, projectionItem.SelectItem); err != nil { return err + } else if _, isIdentifier := projectionItem.SelectItem.(pgsql.Identifier); isIdentifier { + continue + } else if resolvedSelectItem, err := resolvePathCompositeFieldReferences(s.scope, projectionItem.SelectItem); err != nil { + return err + } else if selectItem, isSelectItem := resolvedSelectItem.(pgsql.SelectItem); !isSelectItem { + return fmt.Errorf("resolved with projection item is not selectable: %T", resolvedSelectItem) + } else { + projectionItem.SelectItem = selectItem } } diff --git a/cypher/models/walk/walk_pgsql.go b/cypher/models/walk/walk_pgsql.go index 23ab28b3..37e0a8c1 100644 --- a/cypher/models/walk/walk_pgsql.go +++ b/cypher/models/walk/walk_pgsql.go @@ -258,6 +258,18 @@ func newSQLWalkCursor(node pgsql.SyntaxNode) (*Cursor[pgsql.SyntaxNode], error) Branches: []pgsql.SyntaxNode{typedNode.Expression}, }, nil + case pgsql.AllExpression: + return &Cursor[pgsql.SyntaxNode]{ + Node: node, + Branches: []pgsql.SyntaxNode{typedNode.Expression}, + }, nil + + case *pgsql.AllExpression: + return &Cursor[pgsql.SyntaxNode]{ + Node: node, + Branches: []pgsql.SyntaxNode{typedNode.Expression}, + }, nil + case *pgsql.AnyExpression: return &Cursor[pgsql.SyntaxNode]{ Node: node, diff --git a/integration/testdata/cases/aggregation_inline.json b/integration/testdata/cases/aggregation_inline.json index a624f197..3c5a9309 100644 --- a/integration/testdata/cases/aggregation_inline.json +++ b/integration/testdata/cases/aggregation_inline.json @@ -276,6 +276,25 @@ "edges": [] }, "assert": {"scalar_values": [40]} + }, + { + "name": "aggregate after optimized expansion groups by terminal node", + "cypher": "MATCH p = (src:NodeKind1)-[:EdgeKind1*1..]->(mid)-[:EdgeKind2]->(dst:NodeKind2) WHERE src.name = 'aggregation-expansion-src' WITH dst, count(p) AS path_count RETURN dst.name, path_count", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "aggregation-expansion-src"}}, + {"id": "mid-a", "kinds": ["NodeKind1"]}, + {"id": "mid-b", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"], "properties": {"name": "aggregation-expansion-dst"}} + ], + "edges": [ + {"start_id": "src", "end_id": "mid-a", "kind": "EdgeKind1"}, + {"start_id": "src", "end_id": "mid-b", "kind": "EdgeKind1"}, + {"start_id": "mid-a", "end_id": "dst", "kind": "EdgeKind2"}, + {"start_id": "mid-b", "end_id": "dst", "kind": "EdgeKind2"} + ] + }, + "assert": {"row_values": [["aggregation-expansion-dst", 2]]} } ] } diff --git a/integration/testdata/cases/expansion_inline.json b/integration/testdata/cases/expansion_inline.json index 2e5ede51..8eeb3782 100644 --- a/integration/testdata/cases/expansion_inline.json +++ b/integration/testdata/cases/expansion_inline.json @@ -251,6 +251,76 @@ "edges": [{"start_id": "src", "end_id": "tgt", "kind": "EdgeKind1"}] }, "assert": {"path_node_ids": [["src", "tgt"]], "path_edge_kinds": [["EdgeKind1"]]} + }, + { + "name": "expansion followed by fixed suffix cannot reuse an expansion relationship", + "cypher": "match p = (src:NodeKind1)-[:EdgeKind1*1..]->(mid)-[:EdgeKind1]-(dst:NodeKind1) where src.name = 'reuse-source' and dst.name = 'reuse-source' return p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "reuse-source"}}, + {"id": "mid", "kinds": ["NodeKind1"], "properties": {"name": "reuse-mid"}} + ], + "edges": [ + {"start_id": "src", "end_id": "mid", "kind": "EdgeKind1"} + ] + }, + "assert": "empty" + }, + { + "name": "anonymous continuation suffix reaches a bound endpoint after expansion", + "cypher": "match (dst:NodeKind2 {name: 'anonymous-bound-dst'}) match p = (src:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind1)-[:EdgeKind2]->(dst) where src.name = 'anonymous-bound-src' return p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "anonymous-bound-src"}}, + {"id": "root", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"], "properties": {"name": "anonymous-bound-dst"}}, + {"id": "decoy", "kinds": ["NodeKind2"], "properties": {"name": "anonymous-decoy-dst"}} + ], + "edges": [ + {"start_id": "src", "end_id": "root", "kind": "EdgeKind1"}, + {"start_id": "root", "end_id": "dst", "kind": "EdgeKind2"}, + {"start_id": "root", "end_id": "decoy", "kind": "EdgeKind2"} + ] + }, + "assert": {"row_count": 1, "path_node_ids": [["src", "root", "dst"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2"]]} + }, + { + "name": "directionless fixed suffix after expansion preserves path semantics", + "cypher": "match p = (src:NodeKind1)-[:EdgeKind1*1..]->(mid)-[:EdgeKind2]-(dst:NodeKind2) where src.name = 'directionless-suffix-src' return p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "directionless-suffix-src"}}, + {"id": "mid", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"]}, + {"id": "dead", "kinds": ["NodeKind1"]} + ], + "edges": [ + {"start_id": "src", "end_id": "mid", "kind": "EdgeKind1"}, + {"start_id": "src", "end_id": "dead", "kind": "EdgeKind1"}, + {"start_id": "mid", "end_id": "dst", "kind": "EdgeKind2"} + ] + }, + "assert": {"path_node_ids": [["src", "mid", "dst"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2"]]} + }, + { + "name": "inbound expansion suffix preserves path functions", + "cypher": "match p = (ca:EnterpriseCA)<-[:PublishedTo*1..]-(ct)<-[:Enroll]-(m:Group) return size(relationships(p)), nodes(p), relationships(p)", + "fixture": { + "nodes": [ + {"id": "ca", "kinds": ["EnterpriseCA"]}, + {"id": "template", "kinds": ["CertTemplate"]}, + {"id": "member", "kinds": ["Group"]} + ], + "edges": [ + {"start_id": "template", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "member", "end_id": "template", "kind": "Enroll"} + ] + }, + "assert": { + "scalar_values": [2], + "node_list_ids": [["ca", "template", "member"]], + "relationship_list_kinds": [["PublishedTo", "Enroll"]] + } } ] } diff --git a/integration/testdata/cases/multipart_inline.json b/integration/testdata/cases/multipart_inline.json index 2c69227c..8a637fb9 100644 --- a/integration/testdata/cases/multipart_inline.json +++ b/integration/testdata/cases/multipart_inline.json @@ -129,6 +129,40 @@ "edges": [{"start_id": "a", "end_id": "b", "kind": "EdgeKind1"}] }, "assert": {"row_count": 1, "node_ids": ["a", "b"]} + }, + { + "name": "with barrier keeps optimized expansion path semantics", + "cypher": "match p = (src:NodeKind1)-[:EdgeKind1*1..]->(mid)-[:EdgeKind2]->(dst:NodeKind2) where src.name = 'with-expansion-src' with p, size(relationships(p)) as hops where hops = 2 return p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "with-expansion-src"}}, + {"id": "mid", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"]}, + {"id": "dead", "kinds": ["NodeKind1"]} + ], + "edges": [ + {"start_id": "src", "end_id": "mid", "kind": "EdgeKind1"}, + {"start_id": "mid", "end_id": "dst", "kind": "EdgeKind2"}, + {"start_id": "src", "end_id": "dead", "kind": "EdgeKind1"} + ] + }, + "assert": {"path_node_ids": [["src", "mid", "dst"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2"]]} + }, + { + "name": "optional match barrier preserves anchor row around optimized expansion", + "cypher": "match (src:NodeKind1) where src.name = 'optional-expansion-src' optional match p = (src)-[:EdgeKind1*1..]->(mid)-[:EdgeKind2]->(dst:NodeKind2) where dst.name = 'missing-dst' return count(p)", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "optional-expansion-src"}}, + {"id": "mid", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"], "properties": {"name": "present-dst"}} + ], + "edges": [ + {"start_id": "src", "end_id": "mid", "kind": "EdgeKind1"}, + {"start_id": "mid", "end_id": "dst", "kind": "EdgeKind2"} + ] + }, + "assert": {"exact_int": 0} } ] } diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index d9e08b68..1ded89db 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -45,6 +45,142 @@ "contains_node_with_props": {"objectid": "S-1-5-21-2643190041-1319121918-239771340-513"}, "contains_edge": {"start": "template", "end": "ca", "kind": "PublishedTo"} } + }, + { + "name": "ADCS template predicate accepts both OR branches and rejects false alternatives", + "cypher": "MATCH (n:Group) WHERE n.objectid = 'optimizer-or-source' MATCH p = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca:EnterpriseCA)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d:Domain) WHERE ct.authenticationenabled = true AND ct.requiresmanagerapproval = false AND ct.enrolleesuppliessubject = true AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) RETURN p", + "fixture": { + "nodes": [ + {"id": "n", "kinds": ["Group"], "properties": {"objectid": "optimizer-or-source"}}, + {"id": "mid-v1", "kinds": ["Group"]}, + {"id": "mid-sig", "kinds": ["Group"]}, + {"id": "mid-bad", "kinds": ["Group"]}, + {"id": "template-v1", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 1, "authorizedsignatures": 2}}, + {"id": "template-sig", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 2, "authorizedsignatures": 0}}, + {"id": "template-bad", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 2, "authorizedsignatures": 1}}, + {"id": "ca", "kinds": ["EnterpriseCA"]}, + {"id": "root", "kinds": ["RootCA"]}, + {"id": "domain", "kinds": ["Domain"]} + ], + "edges": [ + {"start_id": "n", "end_id": "mid-v1", "kind": "MemberOf"}, + {"start_id": "mid-v1", "end_id": "template-v1", "kind": "GenericAll"}, + {"start_id": "n", "end_id": "mid-sig", "kind": "MemberOf"}, + {"start_id": "mid-sig", "end_id": "template-sig", "kind": "Enroll"}, + {"start_id": "n", "end_id": "mid-bad", "kind": "MemberOf"}, + {"start_id": "mid-bad", "end_id": "template-bad", "kind": "AllExtendedRights"}, + {"start_id": "template-v1", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "template-sig", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "template-bad", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} + ] + }, + "assert": { + "row_count": 2, + "path_node_ids": [ + ["n", "mid-v1", "template-v1", "ca", "root", "domain"], + ["n", "mid-sig", "template-sig", "ca", "root", "domain"] + ], + "path_edge_kinds": [ + ["MemberOf", "GenericAll", "PublishedTo", "IssuedSignedBy", "RootCAFor"], + ["MemberOf", "Enroll", "PublishedTo", "IssuedSignedBy", "RootCAFor"] + ] + } + }, + { + "name": "ADCS fanout returns every p1 and p2 path pair without endpoint collapse", + "cypher": "MATCH (n:Group) WHERE n.objectid = 'optimizer-fanout-source' MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) WHERE ct.authenticationenabled = true AND ct.requiresmanagerapproval = false AND ct.enrolleesuppliessubject = true AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) RETURN p1, p2", + "fixture": { + "nodes": [ + {"id": "n", "kinds": ["Group"], "properties": {"objectid": "optimizer-fanout-source"}}, + {"id": "p1-a", "kinds": ["Group"]}, + {"id": "p1-b", "kinds": ["Group"]}, + {"id": "p2-a", "kinds": ["Group"]}, + {"id": "p2-b", "kinds": ["Group"]}, + {"id": "template-a", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 1, "authorizedsignatures": 1}}, + {"id": "template-b", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 2, "authorizedsignatures": 0}}, + {"id": "ca", "kinds": ["EnterpriseCA"]}, + {"id": "store", "kinds": ["NTAuthStore"]}, + {"id": "domain", "kinds": ["Domain"]}, + {"id": "root", "kinds": ["RootCA"]} + ], + "edges": [ + {"start_id": "n", "end_id": "p1-a", "kind": "MemberOf"}, + {"start_id": "p1-a", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "n", "end_id": "p1-b", "kind": "MemberOf"}, + {"start_id": "p1-b", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "ca", "end_id": "store", "kind": "TrustedForNTAuth"}, + {"start_id": "store", "end_id": "domain", "kind": "NTAuthStoreFor"}, + {"start_id": "n", "end_id": "p2-a", "kind": "MemberOf"}, + {"start_id": "p2-a", "end_id": "template-a", "kind": "GenericAll"}, + {"start_id": "n", "end_id": "p2-b", "kind": "MemberOf"}, + {"start_id": "p2-b", "end_id": "template-b", "kind": "AllExtendedRights"}, + {"start_id": "template-a", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "template-b", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} + ] + }, + "assert": { + "row_count": 4, + "path_node_ids": [ + ["n", "p1-a", "ca", "store", "domain"], + ["n", "p1-a", "ca", "store", "domain"], + ["n", "p1-b", "ca", "store", "domain"], + ["n", "p1-b", "ca", "store", "domain"], + ["n", "p2-a", "template-a", "ca", "root", "domain"], + ["n", "p2-a", "template-a", "ca", "root", "domain"], + ["n", "p2-b", "template-b", "ca", "root", "domain"], + ["n", "p2-b", "template-b", "ca", "root", "domain"] + ], + "path_edge_kinds": [ + ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + ["MemberOf", "GenericAll", "PublishedTo", "IssuedSignedBy", "RootCAFor"], + ["MemberOf", "GenericAll", "PublishedTo", "IssuedSignedBy", "RootCAFor"], + ["MemberOf", "AllExtendedRights", "PublishedTo", "IssuedSignedBy", "RootCAFor"], + ["MemberOf", "AllExtendedRights", "PublishedTo", "IssuedSignedBy", "RootCAFor"] + ] + } + }, + { + "name": "ADCS fanout endpoint projection preserves row multiplicity", + "cypher": "MATCH (n:Group) WHERE n.objectid = 'optimizer-endpoint-fanout-source' MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) WHERE ct.authenticationenabled = true AND ct.requiresmanagerapproval = false AND ct.enrolleesuppliessubject = true AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) RETURN count(*) AS rows, count(distinct id(ca)) AS ca_count, count(distinct id(d)) AS domain_count, count(distinct id(ct)) AS template_count", + "fixture": { + "nodes": [ + {"id": "n", "kinds": ["Group"], "properties": {"objectid": "optimizer-endpoint-fanout-source"}}, + {"id": "p1-a", "kinds": ["Group"]}, + {"id": "p1-b", "kinds": ["Group"]}, + {"id": "p2-a", "kinds": ["Group"]}, + {"id": "p2-b", "kinds": ["Group"]}, + {"id": "template-a", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 1, "authorizedsignatures": 1}}, + {"id": "template-b", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 2, "authorizedsignatures": 0}}, + {"id": "ca", "kinds": ["EnterpriseCA"]}, + {"id": "store", "kinds": ["NTAuthStore"]}, + {"id": "domain", "kinds": ["Domain"]}, + {"id": "root", "kinds": ["RootCA"]} + ], + "edges": [ + {"start_id": "n", "end_id": "p1-a", "kind": "MemberOf"}, + {"start_id": "p1-a", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "n", "end_id": "p1-b", "kind": "MemberOf"}, + {"start_id": "p1-b", "end_id": "ca", "kind": "Enroll"}, + {"start_id": "ca", "end_id": "store", "kind": "TrustedForNTAuth"}, + {"start_id": "store", "end_id": "domain", "kind": "NTAuthStoreFor"}, + {"start_id": "n", "end_id": "p2-a", "kind": "MemberOf"}, + {"start_id": "p2-a", "end_id": "template-a", "kind": "GenericAll"}, + {"start_id": "n", "end_id": "p2-b", "kind": "MemberOf"}, + {"start_id": "p2-b", "end_id": "template-b", "kind": "AllExtendedRights"}, + {"start_id": "template-a", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "template-b", "end_id": "ca", "kind": "PublishedTo"}, + {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} + ] + }, + "assert": {"row_values": [[4, 1, 1, 2]]} } ] } diff --git a/integration/testdata/cases/unwind_inline.json b/integration/testdata/cases/unwind_inline.json index 389d7438..d87cd43b 100644 --- a/integration/testdata/cases/unwind_inline.json +++ b/integration/testdata/cases/unwind_inline.json @@ -154,6 +154,24 @@ "edges": [] }, "assert": {"scalar_values": ["alpha", "beta", "tail"]} + }, + { + "name": "unwind barrier feeds an optimized expansion predicate", + "cypher": "WITH ['unwind-expansion-dst'] AS names UNWIND names AS name MATCH p = (src:NodeKind1)-[:EdgeKind1*1..]->(mid)-[:EdgeKind2]->(dst:NodeKind2) WHERE src.name = 'unwind-expansion-src' AND dst.name = name RETURN p", + "fixture": { + "nodes": [ + {"id": "src", "kinds": ["NodeKind1"], "properties": {"name": "unwind-expansion-src"}}, + {"id": "mid", "kinds": ["NodeKind1"]}, + {"id": "dst", "kinds": ["NodeKind2"], "properties": {"name": "unwind-expansion-dst"}}, + {"id": "decoy", "kinds": ["NodeKind2"], "properties": {"name": "unwind-expansion-decoy"}} + ], + "edges": [ + {"start_id": "src", "end_id": "mid", "kind": "EdgeKind1"}, + {"start_id": "mid", "end_id": "dst", "kind": "EdgeKind2"}, + {"start_id": "mid", "end_id": "decoy", "kind": "EdgeKind2"} + ] + }, + "assert": {"path_node_ids": [["src", "mid", "dst"]], "path_edge_kinds": [["EdgeKind1", "EdgeKind2"]]} } ] } From abd93fd4e47cbfb106ee4e0ea52384091b2ea22e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 22:43:13 -0700 Subject: [PATCH 043/114] feat(pgsql): plan projection pruning for pattern predicates --- cypher/models/pgsql/optimize/lowering.go | 57 +++++++- cypher/models/pgsql/optimize/lowering_plan.go | 85 ++++++++---- .../models/pgsql/optimize/optimizer_test.go | 68 ++++++++++ .../pgsql/optimize/pattern_predicates.go | 45 +++++++ cypher/models/pgsql/translate/expansion.go | 5 +- cypher/models/pgsql/translate/predicate.go | 7 +- cypher/models/pgsql/translate/translator.go | 4 +- cypher/models/pgsql/translate/traversal.go | 125 ++---------------- 8 files changed, 248 insertions(+), 148 deletions(-) create mode 100644 cypher/models/pgsql/optimize/pattern_predicates.go diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index be5083e7..bf05721b 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -19,9 +19,11 @@ type LoweringDecision struct { } type PatternTarget struct { - QueryPartIndex int `json:"query_part_index"` - ClauseIndex int `json:"clause_index"` - PatternIndex int `json:"pattern_index"` + QueryPartIndex int `json:"query_part_index"` + ClauseIndex int `json:"clause_index"` + PatternIndex int `json:"pattern_index"` + Predicate bool `json:"predicate,omitempty"` + PredicateIndex int `json:"predicate_index,omitempty"` } func (s PatternTarget) TraversalStep(stepIndex int) TraversalStepTarget { @@ -29,15 +31,19 @@ func (s PatternTarget) TraversalStep(stepIndex int) TraversalStepTarget { QueryPartIndex: s.QueryPartIndex, ClauseIndex: s.ClauseIndex, PatternIndex: s.PatternIndex, + Predicate: s.Predicate, + PredicateIndex: s.PredicateIndex, StepIndex: stepIndex, } } type TraversalStepTarget struct { - QueryPartIndex int `json:"query_part_index"` - ClauseIndex int `json:"clause_index"` - PatternIndex int `json:"pattern_index"` - StepIndex int `json:"step_index"` + QueryPartIndex int `json:"query_part_index"` + ClauseIndex int `json:"clause_index"` + PatternIndex int `json:"pattern_index"` + Predicate bool `json:"predicate,omitempty"` + PredicateIndex int `json:"predicate_index,omitempty"` + StepIndex int `json:"step_index"` } type ProjectionPruningDecision struct { @@ -196,6 +202,32 @@ func IndexPatternTargets(query *cypher.RegularQuery) map[*cypher.PatternPart]Pat return targets } +func IndexPatternPredicateTargets(query *cypher.RegularQuery) map[*cypher.PatternPredicate]PatternTarget { + targets := map[*cypher.PatternPredicate]PatternTarget{} + + if query == nil || query.SingleQuery == nil { + return targets + } + + if query.SingleQuery.MultiPartQuery != nil { + for queryPartIndex, part := range query.SingleQuery.MultiPartQuery.Parts { + if part == nil { + continue + } + + indexQueryPartPatternPredicateTargets(targets, queryPartIndex, part) + } + + if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { + indexQueryPartPatternPredicateTargets(targets, len(query.SingleQuery.MultiPartQuery.Parts), finalPart) + } + } else if query.SingleQuery.SinglePartQuery != nil { + indexQueryPartPatternPredicateTargets(targets, 0, query.SingleQuery.SinglePartQuery) + } + + return targets +} + func indexReadingClauseTargets(targets map[*cypher.PatternPart]PatternTarget, queryPartIndex int, readingClauses []*cypher.ReadingClause) { for clauseIndex, readingClause := range readingClauses { if readingClause == nil || readingClause.Match == nil { @@ -211,3 +243,14 @@ func indexReadingClauseTargets(targets map[*cypher.PatternPart]PatternTarget, qu } } } + +func indexQueryPartPatternPredicateTargets(targets map[*cypher.PatternPredicate]PatternTarget, queryPartIndex int, queryPart cypher.SyntaxNode) { + for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { + targets[predicate] = PatternTarget{ + QueryPartIndex: queryPartIndex, + PatternIndex: predicateIndex, + Predicate: true, + PredicateIndex: predicateIndex, + } + } +} diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 457ba745..c6f62629 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -70,6 +70,7 @@ func appendQueryPartLowerings( appendProjectionPruningDecisions(plan, queryPartIndex, readingClauses, sourceReferences) appendLatePathMaterializationDecisions(plan, queryPartIndex, readingClauses, sourceReferences) + appendPatternPredicateProjectionLowerings(plan, queryPartIndex, queryPart, sourceReferences) appendExpandIntoDecisions(plan, queryPartIndex, readingClauses) appendTraversalDirectionDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) appendShortestPathStrategyDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) @@ -132,6 +133,26 @@ func appendPatternProjectionPruningDecisions(plan *LoweringPlan, target PatternT } } +func appendPatternPredicateProjectionLowerings(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode, sourceReferences map[string]struct{}) { + for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { + patternPart := patternPartForPredicate(predicate) + steps := traversalStepsForPattern(patternPart) + if len(steps) == 0 { + continue + } + + target := PatternTarget{ + QueryPartIndex: queryPartIndex, + PatternIndex: predicateIndex, + Predicate: true, + PredicateIndex: predicateIndex, + } + + appendPatternProjectionPruningDecisions(plan, target, patternPart, steps, sourceReferences) + appendPatternLatePathMaterializationDecisions(plan, target, patternPart, steps, sourceReferences) + } +} + func appendLatePathMaterializationDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, sourceReferences map[string]struct{}) { for clauseIndex, readingClause := range readingClauses { if readingClause == nil || readingClause.Match == nil || readingClause.Match.Optional { @@ -139,35 +160,53 @@ func appendLatePathMaterializationDecisions(plan *LoweringPlan, queryPartIndex i } for patternIndex, patternPart := range readingClause.Match.Pattern { - if !referencesSourceIdentifier(sourceReferences, variableSymbol(patternPart.Variable)) { + steps := traversalStepsForPattern(patternPart) + appendPatternLatePathMaterializationDecisions(plan, PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + }, patternPart, steps, sourceReferences) + } + } +} + +func appendPatternLatePathMaterializationDecisions(plan *LoweringPlan, target PatternTarget, patternPart *cypher.PatternPart, steps []sourceTraversalStep, sourceReferences map[string]struct{}) { + pathReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(patternPart.Variable)) + + for stepIndex, step := range steps { + stepTarget := target.TraversalStep(stepIndex) + + if step.Relationship.Range != nil { + if !pathReferenced { continue } - for stepIndex, step := range traversalStepsForPattern(patternPart) { - target := PatternTarget{ - QueryPartIndex: queryPartIndex, - ClauseIndex: clauseIndex, - PatternIndex: patternIndex, - }.TraversalStep(stepIndex) + plan.LatePathMaterialization = append(plan.LatePathMaterialization, LatePathMaterializationDecision{ + Target: stepTarget, + Mode: LatePathMaterializationExpansionPath, + }) + continue + } - if step.Relationship.Range != nil { - plan.LatePathMaterialization = append(plan.LatePathMaterialization, LatePathMaterializationDecision{ - Target: target, - Mode: LatePathMaterializationExpansionPath, - }) - continue - } + edgeReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.Relationship.Variable)) + if pathReferenced { + mode := LatePathMaterializationPathEdgeID + if edgeReferenced { + mode = LatePathMaterializationEdgeComposite + } - mode := LatePathMaterializationPathEdgeID - if referencesSourceIdentifier(sourceReferences, variableSymbol(step.Relationship.Variable)) { - mode = LatePathMaterializationEdgeComposite - } + plan.LatePathMaterialization = append(plan.LatePathMaterialization, LatePathMaterializationDecision{ + Target: stepTarget, + Mode: mode, + }) + continue + } - plan.LatePathMaterialization = append(plan.LatePathMaterialization, LatePathMaterializationDecision{ - Target: target, - Mode: mode, - }) - } + if !edgeReferenced && stepIndex+1 < len(steps) { + plan.LatePathMaterialization = append(plan.LatePathMaterialization, LatePathMaterializationDecision{ + Target: stepTarget, + Mode: LatePathMaterializationPathEdgeID, + }) } } } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 9e6fa5b5..26f41a89 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -115,6 +115,30 @@ func TestLoweringPlanProjectionPruningKeepsUpdateTargets(t *testing.T) { }}, plan.LoweringPlan.ProjectionPruning) } +func TestLoweringPlanReportsPatternPredicateProjectionPruning(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (s) + WHERE (s)-[]->() + RETURN s + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.ProjectionPruning, ProjectionPruningDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + Predicate: true, + StepIndex: 0, + }, + ReferencedSymbols: []string{"s"}, + OmitRelationship: true, + OmitRightNode: true, + }) +} + func TestLoweringPlanReportsLatePathMaterialization(t *testing.T) { t.Parallel() @@ -153,6 +177,50 @@ func TestLoweringPlanReportsLatePathMaterialization(t *testing.T) { require.NoError(t, err) require.Equal(t, LatePathMaterializationEdgeComposite, plan.LoweringPlan.LatePathMaterialization[0].Mode) }) + + t.Run("continuation relationship id", func(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (n)-[:MemberOf]->(m)-[:Enroll]->(ca) + RETURN ca + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.LatePathMaterialization, LatePathMaterializationDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Mode: LatePathMaterializationPathEdgeID, + }) + }) + + t.Run("pattern predicate continuation relationship id", func(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (s) + WHERE (s)-[]->()-[]->() + RETURN s + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.LatePathMaterialization, LatePathMaterializationDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + Predicate: true, + StepIndex: 0, + }, + Mode: LatePathMaterializationPathEdgeID, + }) + }) } func TestLoweringPlanReportsExpansionSuffixPushdown(t *testing.T) { diff --git a/cypher/models/pgsql/optimize/pattern_predicates.go b/cypher/models/pgsql/optimize/pattern_predicates.go new file mode 100644 index 00000000..16e9349a --- /dev/null +++ b/cypher/models/pgsql/optimize/pattern_predicates.go @@ -0,0 +1,45 @@ +package optimize + +import ( + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/walk" +) + +type patternPredicateCollector struct { + walk.VisitorHandler + predicates []*cypher.PatternPredicate +} + +func (s *patternPredicateCollector) Enter(node cypher.SyntaxNode) { + if predicate, isPatternPredicate := node.(*cypher.PatternPredicate); isPatternPredicate { + s.predicates = append(s.predicates, predicate) + } +} + +func (s *patternPredicateCollector) Visit(cypher.SyntaxNode) {} +func (s *patternPredicateCollector) Exit(cypher.SyntaxNode) {} + +func patternPredicatesInQueryPart(queryPart cypher.SyntaxNode) []*cypher.PatternPredicate { + if queryPart == nil { + return nil + } + + collector := &patternPredicateCollector{ + VisitorHandler: walk.NewCancelableErrorHandler(), + } + if err := walk.Cypher(queryPart, collector); err != nil { + return nil + } + + return collector.predicates +} + +func patternPartForPredicate(predicate *cypher.PatternPredicate) *cypher.PatternPart { + if predicate == nil { + return nil + } + + return &cypher.PatternPart{ + PatternElements: predicate.PatternElements, + } +} diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 3d8a73d3..85b3589e 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2999,9 +2999,8 @@ func (s *Translator) translateTraversalPatternPartWithExpansion(part *PatternPar // Export the path from the traversal's scope traversalStep.Frame.Export(expansionModel.PathBinding.Identifier) if allowProjectionPruning { - decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - allowFallback := !hasDecision && (part == nil || !part.HasTarget) - if pruneExpansionStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision, allowFallback) { + _, hasDecision := s.projectionPruningDecision(part, stepIndex) + if hasDecision && pruneExpansionStepProjectionExports(part, stepIndex, traversalStep) { s.recordLowering(optimize.LoweringProjectionPruning) } diff --git a/cypher/models/pgsql/translate/predicate.go b/cypher/models/pgsql/translate/predicate.go index 002d3e95..69503565 100644 --- a/cypher/models/pgsql/translate/predicate.go +++ b/cypher/models/pgsql/translate/predicate.go @@ -4,11 +4,12 @@ import ( "fmt" "github.com/specterops/dawgs/cypher/models" + "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/specterops/dawgs/graph" ) -func (s *Translator) preparePatternPredicate() error { +func (s *Translator) preparePatternPredicate(predicate *cypher.PatternPredicate) error { currentQueryPart := s.query.CurrentPart() // Stash the match pattern @@ -17,6 +18,10 @@ func (s *Translator) preparePatternPredicate() error { // All pattern predicates must be relationship patterns newPatternPart := currentQueryPart.currentPattern.NewPart() newPatternPart.IsTraversal = true + if target, hasTarget := s.patternPredicateTargets[predicate]; hasTarget { + newPatternPart.Target = target + newPatternPart.HasTarget = true + } return nil } diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index d7517b65..cd6be943 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -31,6 +31,7 @@ type Translator struct { unwindTargets map[*cypher.Variable]struct{} patternTargets map[*cypher.PatternPart]optimize.PatternTarget + patternPredicateTargets map[*cypher.PatternPredicate]optimize.PatternTarget projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision suffixPushdownDecisions map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision @@ -72,6 +73,7 @@ func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.patternTargets = optimize.IndexPatternTargets(plan.Query) + s.patternPredicateTargets = optimize.IndexPatternPredicateTargets(plan.Query) s.projectionPruningDecisions = map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision{} s.latePathDecisions = map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision{} s.suffixPushdownDecisions = map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision{} @@ -253,7 +255,7 @@ func (s *Translator) Enter(expression cypher.SyntaxNode) { s.query.CurrentPart().PrepareProjection() case *cypher.PatternPredicate: - if err := s.preparePatternPredicate(); err != nil { + if err := s.preparePatternPredicate(typedExpression); err != nil { s.SetError(err) } diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index c8036872..54883885 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -804,24 +804,6 @@ func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error return applied, nil } -func patternBindingDependsOn(queryPart *QueryPart, part *PatternPart, binding *BoundIdentifier) bool { - if queryPart == nil || part == nil || part.PatternBinding == nil || binding == nil { - return false - } - - if !queryPart.ReferencesBinding(part.PatternBinding) { - return false - } - - for _, dependency := range part.PatternBinding.Dependencies { - if dependency.Identifier == binding.Identifier { - return true - } - } - - return false -} - func traversalStepHasContinuation(part *PatternPart, stepIndex int) bool { return part != nil && stepIndex+1 < len(part.TraversalSteps) } @@ -942,31 +924,6 @@ func (s *Translator) applyPathEdgeIDMaterialization(part *PatternPart, stepIndex return true } -func traversalStepProjectsBinding(queryPart *QueryPart, part *PatternPart, stepIndex int, binding *BoundIdentifier) bool { - if binding == nil { - return false - } - - // Keep aliases referenced by later clauses and bindings needed to materialize - // a referenced path pattern. Everything else can stay internal to this step. - if (binding.Alias.Set && queryPart.ReferencesBinding(binding)) || patternBindingDependsOn(queryPart, part, binding) { - return true - } - - if traversalStepHasContinuation(part, stepIndex) { - if part.TraversalSteps[stepIndex].Edge == binding { - return true - } - - // A multi-hop pattern needs the right node from this step as the next - // step's left node even when the user never projects it. - nextStep := part.TraversalSteps[stepIndex+1] - return nextStep.LeftNode != nil && nextStep.LeftNode.Identifier == binding.Identifier - } - - return false -} - func unexportFrameBinding(frame *Frame, identifier pgsql.Identifier) bool { if frame == nil { return false @@ -1001,80 +958,30 @@ func unexportPrunedNodeBinding(traversalStep *TraversalStep, binding *BoundIdent return unexportFrameBinding(traversalStep.Frame, binding.Identifier) } -func pruneTraversalStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) bool { +func pruneTraversalStepProjectionExports(part *PatternPart, stepIndex int, traversalStep *TraversalStep) bool { var applied bool - if hasDecision { - applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.LeftNode) || applied - if traversalStep.ProjectionPruning.Relationship != nil && !traversalStepHasContinuation(part, stepIndex) { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied - } - applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.RightNode) || applied - return applied - } - - if !allowFallback { - return false - } - - // Bound endpoints already exist in an outer frame. Only unexport unbound - // values that later clauses and continuation steps cannot observe. - if traversalStep.LeftNode != nil && !traversalStep.LeftNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.LeftNode) { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.LeftNode.Identifier) || applied - } - - if traversalStep.Edge != nil && - traversalStep.Edge.DataType == pgsql.EdgeComposite && - !queryPart.ReferencesBinding(traversalStep.Edge) && - patternBindingDependsOn(queryPart, part, traversalStep.Edge) { - traversalStep.Edge.DataType = pgsql.PathEdge - applied = true - } - - if traversalStep.Edge != nil && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.Edge) { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied - } - - if traversalStep.RightNode != nil && !traversalStep.RightNodeBound && !traversalStepProjectsBinding(queryPart, part, stepIndex, traversalStep.RightNode) { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.RightNode.Identifier) || applied + applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.LeftNode) || applied + if traversalStep.ProjectionPruning.Relationship != nil && !traversalStepHasContinuation(part, stepIndex) { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied } + applied = unexportPrunedNodeBinding(traversalStep, traversalStep.ProjectionPruning.RightNode) || applied return applied } -func pruneExpansionStepProjectionExports(queryPart *QueryPart, part *PatternPart, stepIndex int, traversalStep *TraversalStep, decision optimize.ProjectionPruningDecision, hasDecision bool, allowFallback bool) bool { +func pruneExpansionStepProjectionExports(part *PatternPart, stepIndex int, traversalStep *TraversalStep) bool { if traversalStep == nil || traversalStep.Expansion == nil { return false } var applied bool - if hasDecision { - if traversalStep.ProjectionPruning.Relationship != nil { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied - } - - if traversalStep.ProjectionPruning.PathBinding != nil && !traversalStepHasContinuation(part, stepIndex) { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.PathBinding.Identifier) || applied - } - - return applied - } - - if !allowFallback { - return false - } - - // Variable-length relationship bindings materialize to edge-composite - // arrays. A path binding can be rebuilt later from the compact expansion - // path ID array, so keep the edge array only when the relationship binding - // itself is observable. - if traversalStep.Edge != nil && !queryPart.ReferencesBinding(traversalStep.Edge) { - applied = unexportFrameBinding(traversalStep.Frame, traversalStep.Edge.Identifier) || applied + if traversalStep.ProjectionPruning.Relationship != nil { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.Relationship.Identifier) || applied } - pathBinding := traversalStep.Expansion.PathBinding - if pathBinding != nil && !traversalStepHasContinuation(part, stepIndex) && !patternBindingDependsOn(queryPart, part, pathBinding) { - applied = unexportFrameBinding(traversalStep.Frame, pathBinding.Identifier) || applied + if traversalStep.ProjectionPruning.PathBinding != nil && !traversalStepHasContinuation(part, stepIndex) { + applied = unexportFrameBinding(traversalStep.Frame, traversalStep.ProjectionPruning.PathBinding.Identifier) || applied } return applied @@ -1162,20 +1069,12 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern } if allowProjectionPruning { - if traversalStepHasContinuation(part, stepIndex) && - traversalStep.Edge != nil && - traversalStep.Edge.DataType == pgsql.EdgeComposite && - !s.query.CurrentPart().ReferencesBinding(traversalStep.Edge) { - traversalStep.Edge.DataType = pgsql.PathEdge - } - if s.applyPathEdgeIDMaterialization(part, stepIndex, traversalStep) { s.recordLowering(optimize.LoweringLatePathMaterialization) } - decision, hasDecision := s.projectionPruningDecision(part, stepIndex) - allowFallback := !hasDecision && (part == nil || !part.HasTarget) - if pruneTraversalStepProjectionExports(s.query.CurrentPart(), part, stepIndex, traversalStep, decision, hasDecision, allowFallback) { + _, hasDecision := s.projectionPruningDecision(part, stepIndex) + if hasDecision && pruneTraversalStepProjectionExports(part, stepIndex, traversalStep) { s.recordLowering(optimize.LoweringProjectionPruning) } } From 7f87f86f55752fe65a9195172d12aef80f87bb1b Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 22:45:13 -0700 Subject: [PATCH 044/114] feat(pgsql): plan pattern predicate placement --- cypher/models/pgsql/optimize/lowering.go | 35 +++++++---- cypher/models/pgsql/optimize/lowering_plan.go | 40 ++++++++++++ .../models/pgsql/optimize/optimizer_test.go | 23 +++++++ .../pgsql/translate/optimizer_safety_test.go | 18 ++++++ cypher/models/pgsql/translate/predicate.go | 63 ++++++++++++------- cypher/models/pgsql/translate/translator.go | 6 ++ 6 files changed, 153 insertions(+), 32 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index bf05721b..defdb948 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -131,16 +131,28 @@ type PredicatePlacementDecision struct { Placement PredicateAttachmentScope `json:"placement"` } +type PatternPredicatePlacementMode string + +const ( + PatternPredicatePlacementExistence PatternPredicatePlacementMode = "existence" +) + +type PatternPredicatePlacementDecision struct { + Target TraversalStepTarget `json:"target"` + Mode PatternPredicatePlacementMode `json:"mode"` +} + type LoweringPlan struct { - ProjectionPruning []ProjectionPruningDecision `json:"projection_pruning,omitempty"` - LatePathMaterialization []LatePathMaterializationDecision `json:"late_path_materialization,omitempty"` - ExpandInto []ExpandIntoDecision `json:"expand_into,omitempty"` - TraversalDirection []TraversalDirectionDecision `json:"traversal_direction,omitempty"` - ShortestPathStrategy []ShortestPathStrategyDecision `json:"shortest_path_strategy,omitempty"` - ShortestPathFilter []ShortestPathFilterDecision `json:"shortest_path_filter,omitempty"` - LimitPushdown []LimitPushdownDecision `json:"limit_pushdown,omitempty"` - ExpansionSuffixPushdown []ExpansionSuffixPushdownDecision `json:"expansion_suffix_pushdown,omitempty"` - PredicatePlacement []PredicatePlacementDecision `json:"predicate_placement,omitempty"` + ProjectionPruning []ProjectionPruningDecision `json:"projection_pruning,omitempty"` + LatePathMaterialization []LatePathMaterializationDecision `json:"late_path_materialization,omitempty"` + ExpandInto []ExpandIntoDecision `json:"expand_into,omitempty"` + TraversalDirection []TraversalDirectionDecision `json:"traversal_direction,omitempty"` + ShortestPathStrategy []ShortestPathStrategyDecision `json:"shortest_path_strategy,omitempty"` + ShortestPathFilter []ShortestPathFilterDecision `json:"shortest_path_filter,omitempty"` + LimitPushdown []LimitPushdownDecision `json:"limit_pushdown,omitempty"` + ExpansionSuffixPushdown []ExpansionSuffixPushdownDecision `json:"expansion_suffix_pushdown,omitempty"` + PredicatePlacement []PredicatePlacementDecision `json:"predicate_placement,omitempty"` + PatternPredicate []PatternPredicatePlacementDecision `json:"pattern_predicate_placement,omitempty"` } func (s LoweringPlan) Empty() bool { @@ -152,7 +164,8 @@ func (s LoweringPlan) Empty() bool { len(s.ShortestPathFilter) == 0 && len(s.LimitPushdown) == 0 && len(s.ExpansionSuffixPushdown) == 0 && - len(s.PredicatePlacement) == 0 + len(s.PredicatePlacement) == 0 && + len(s.PatternPredicate) == 0 } func (s LoweringPlan) Decisions() []LoweringDecision { @@ -171,7 +184,7 @@ func (s LoweringPlan) Decisions() []LoweringDecision { add(LoweringShortestPathFilter, len(s.ShortestPathFilter) > 0) add(LoweringLimitPushdown, len(s.LimitPushdown) > 0) add(LoweringExpansionSuffixPushdown, len(s.ExpansionSuffixPushdown) > 0) - add(LoweringPredicatePlacement, len(s.PredicatePlacement) > 0) + add(LoweringPredicatePlacement, len(s.PredicatePlacement) > 0 || len(s.PatternPredicate) > 0) return decisions } diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index c6f62629..015c154f 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -71,6 +71,7 @@ func appendQueryPartLowerings( appendProjectionPruningDecisions(plan, queryPartIndex, readingClauses, sourceReferences) appendLatePathMaterializationDecisions(plan, queryPartIndex, readingClauses, sourceReferences) appendPatternPredicateProjectionLowerings(plan, queryPartIndex, queryPart, sourceReferences) + appendPatternPredicatePlacementDecisions(plan, queryPartIndex, queryPart) appendExpandIntoDecisions(plan, queryPartIndex, readingClauses) appendTraversalDirectionDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) appendShortestPathStrategyDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) @@ -153,6 +154,41 @@ func appendPatternPredicateProjectionLowerings(plan *LoweringPlan, queryPartInde } } +func appendPatternPredicatePlacementDecisions(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode) { + for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { + patternPart := patternPartForPredicate(predicate) + steps := traversalStepsForPattern(patternPart) + if len(steps) != 1 { + continue + } + + step := steps[0] + if step.Relationship == nil || + step.Relationship.Direction != graph.DirectionBoth || + relationshipPatternHasConstraints(step.Relationship) || + nodePatternHasConstraints(step.LeftNode) || + nodePatternHasConstraints(step.RightNode) { + continue + } + + if variableSymbol(step.Relationship.Variable) != "" || variableSymbol(step.RightNode.Variable) != "" { + continue + } + + target := PatternTarget{ + QueryPartIndex: queryPartIndex, + PatternIndex: predicateIndex, + Predicate: true, + PredicateIndex: predicateIndex, + }.TraversalStep(0) + + plan.PatternPredicate = append(plan.PatternPredicate, PatternPredicatePlacementDecision{ + Target: target, + Mode: PatternPredicatePlacementExistence, + }) + } +} + func appendLatePathMaterializationDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, sourceReferences map[string]struct{}) { for clauseIndex, readingClause := range readingClauses { if readingClause == nil || readingClause.Match == nil || readingClause.Match.Optional { @@ -909,6 +945,10 @@ func nodePatternHasConstraints(nodePattern *cypher.NodePattern) bool { return nodePattern != nil && (len(nodePattern.Kinds) > 0 || nodePattern.Properties != nil) } +func relationshipPatternHasConstraints(relationshipPattern *cypher.RelationshipPattern) bool { + return relationshipPattern != nil && (len(relationshipPattern.Kinds) > 0 || relationshipPattern.Properties != nil) +} + func addSymbol(symbols map[string]struct{}, symbol string) { if symbol != "" { symbols[symbol] = struct{}{} diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 26f41a89..a487e148 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -139,6 +139,29 @@ func TestLoweringPlanReportsPatternPredicateProjectionPruning(t *testing.T) { }) } +func TestLoweringPlanReportsPatternPredicateExistencePlacement(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (s) + WHERE NOT (s)-[]-() + RETURN s + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringPredicatePlacement}) + require.Equal(t, []PatternPredicatePlacementDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + Predicate: true, + StepIndex: 0, + }, + Mode: PatternPredicatePlacementExistence, + }}, plan.LoweringPlan.PatternPredicate) +} + func TestLoweringPlanReportsLatePathMaterialization(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 25a57484..d8c9d1ea 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -287,6 +287,24 @@ RETURN p ) } +func TestOptimizerSafetyPatternPredicateExistencePlacementIsPlanned(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (s) +WHERE NOT (s)-[]-() +RETURN s +`) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "not exists (select 1 from edge e0") + requirePlannedOptimizationLowering(t, translation.Optimization, "PredicatePlacement") + requireOptimizationLowering(t, translation.Optimization, "PredicatePlacement") +} + func TestOptimizerSafetyContinuationRelationshipsExcludePriorPathRelationships(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/predicate.go b/cypher/models/pgsql/translate/predicate.go index 69503565..54ed79ec 100644 --- a/cypher/models/pgsql/translate/predicate.go +++ b/cypher/models/pgsql/translate/predicate.go @@ -6,6 +6,7 @@ import ( "github.com/specterops/dawgs/cypher/models" "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/graph" ) @@ -88,6 +89,37 @@ func (s *Translator) translatePatternPredicate() error { return nil } +func (s *Translator) usePatternPredicateExistencePlacement(patternPart *PatternPart, traversalStep *TraversalStep) (bool, error) { + if patternPart == nil || !patternPart.HasTarget || traversalStep == nil || traversalStep.Direction != graph.DirectionBoth { + return false, nil + } + + decision, hasDecision := s.patternPredicateDecisions[patternPart.Target.TraversalStep(0)] + if !hasDecision || decision.Mode != optimize.PatternPredicatePlacementExistence { + return false, nil + } + + traversalStepIdentifiers := pgsql.AsIdentifierSet( + traversalStep.LeftNode.Identifier, + traversalStep.Edge.Identifier, + traversalStep.RightNode.Identifier, + ) + + if hasGlobalConstraints, err := s.treeTranslator.HasAnyConstraints(traversalStepIdentifiers); err != nil { + return false, err + } else if hasGlobalConstraints { + return false, nil + } + + if hasPredicateConstraints, err := patternPart.Constraints.HasConstraints(traversalStepIdentifiers); err != nil { + return false, err + } else if hasPredicateConstraints { + return false, nil + } + + return true, nil +} + // buildPatternPredicates is used by translateMatch to resolve deferred pattern predicate // futures collected for the current MATCH/OPTIONAL MATCH query part's WHERE expressions func (s *Translator) buildPatternPredicates() error { @@ -102,29 +134,18 @@ func (s *Translator) buildPatternPredicates() error { ) if len(patternPart.TraversalSteps) == 1 { - var ( - traversalStep = patternPart.TraversalSteps[0] - traversalStepIdentifiers = pgsql.AsIdentifierSet( - traversalStep.LeftNode.Identifier, - traversalStep.Edge.Identifier, - traversalStep.RightNode.Identifier, - ) - ) - - if traversalStep.Direction == graph.DirectionBoth { - if hasGlobalConstraints, err := s.treeTranslator.HasAnyConstraints(traversalStepIdentifiers); err != nil { - return err - } else if hasPredicateConstraints, err := patternPart.Constraints.HasConstraints(traversalStepIdentifiers); err != nil { + traversalStep := patternPart.TraversalSteps[0] + if useExistencePlacement, err := s.usePatternPredicateExistencePlacement(patternPart, traversalStep); err != nil { + return err + } else if useExistencePlacement { + if predicateExpression, err := s.buildOptimizedRelationshipExistPredicate(patternPart, traversalStep); err != nil { return err - } else if !hasPredicateConstraints && !hasGlobalConstraints { - if predicateExpression, err := s.buildOptimizedRelationshipExistPredicate(patternPart, traversalStep); err != nil { - return err - } else { - predicateFuture.SyntaxNode = predicateExpression - } - - return nil + } else { + predicateFuture.SyntaxNode = predicateExpression + s.recordLowering(optimize.LoweringPredicatePlacement) } + + return nil } } diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index cd6be943..7d92fc21 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -40,6 +40,7 @@ type Translator struct { shortestPathStrategyDecisions map[optimize.TraversalStepTarget]optimize.ShortestPathStrategyDecision shortestPathFilterDecisions map[optimize.TraversalStepTarget][]optimize.ShortestPathFilterDecision limitPushdownDecisions map[optimize.TraversalStepTarget][]optimize.LimitPushdownDecision + patternPredicateDecisions map[optimize.TraversalStepTarget]optimize.PatternPredicatePlacementDecision } func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) *Translator { @@ -82,6 +83,7 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.shortestPathStrategyDecisions = map[optimize.TraversalStepTarget]optimize.ShortestPathStrategyDecision{} s.shortestPathFilterDecisions = map[optimize.TraversalStepTarget][]optimize.ShortestPathFilterDecision{} s.limitPushdownDecisions = map[optimize.TraversalStepTarget][]optimize.LimitPushdownDecision{} + s.patternPredicateDecisions = map[optimize.TraversalStepTarget]optimize.PatternPredicatePlacementDecision{} for _, decision := range plan.LoweringPlan.ProjectionPruning { s.projectionPruningDecisions[decision.Target] = decision @@ -114,6 +116,10 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { for _, decision := range plan.LoweringPlan.LimitPushdown { s.limitPushdownDecisions[decision.Target] = append(s.limitPushdownDecisions[decision.Target], decision) } + + for _, decision := range plan.LoweringPlan.PatternPredicate { + s.patternPredicateDecisions[decision.Target] = decision + } } func (s *Translator) Enter(expression cypher.SyntaxNode) { From eecf5056ad8160b7dc20c314ca2a8b504c1841ad Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 22:49:31 -0700 Subject: [PATCH 045/114] refactor(pgsql): centralize selectivity and locality planning --- cypher/models/pgsql/optimize/locality.go | 189 +++++++++++ .../models/pgsql/optimize/optimizer_test.go | 31 ++ cypher/models/pgsql/optimize/selectivity.go | 299 ++++++++++++++++++ cypher/models/pgsql/translate/constraints.go | 33 +- cypher/models/pgsql/translate/model.go | 176 +---------- cypher/models/pgsql/translate/selectivity.go | 221 +------------ cypher/models/pgsql/translate/tracking.go | 8 + docs/optimization-pass-memory.md | 287 ----------------- 8 files changed, 556 insertions(+), 688 deletions(-) create mode 100644 cypher/models/pgsql/optimize/locality.go create mode 100644 cypher/models/pgsql/optimize/selectivity.go delete mode 100644 docs/optimization-pass-memory.md diff --git a/cypher/models/pgsql/optimize/locality.go b/cypher/models/pgsql/optimize/locality.go new file mode 100644 index 00000000..22301ce9 --- /dev/null +++ b/cypher/models/pgsql/optimize/locality.go @@ -0,0 +1,189 @@ +package optimize + +import ( + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/walk" +) + +// FlattenConjunction collects the leaf operands of a left-recursive AND chain. +func FlattenConjunction(expr pgsql.Expression) []pgsql.Expression { + if bin, typeOK := expr.(*pgsql.BinaryExpression); !typeOK || bin.Operator != pgsql.OperatorAnd { + return []pgsql.Expression{expr} + } else { + return append(FlattenConjunction(bin.LOperand), FlattenConjunction(bin.ROperand)...) + } +} + +// 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) { + switch typedNode := node.(type) { + case pgsql.ExistsExpression: + if !SubqueryReferencesOnlyLocalIdentifiers(typedNode.Subquery, localScope) { + isLocal = false + handler.SetDone() + } else { + handler.Consume() + } + + 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() + } + } + }, + )) + + return isLocal +} + +func SubqueryReferencesOnlyLocalIdentifiers(subquery pgsql.Subquery, localScope *pgsql.IdentifierSet) bool { + return QueryReferencesOnlyLocalIdentifiers(subquery.Query, localScope) +} + +func QueryReferencesOnlyLocalIdentifiers(query pgsql.Query, localScope *pgsql.IdentifierSet) bool { + if query.CommonTableExpressions != nil { + return false + } + + selectBody, isSelect := query.Body.(pgsql.Select) + if !isSelect { + return false + } + + if !SelectReferencesOnlyLocalIdentifiers(selectBody, localScope) { + return false + } + + for _, orderBy := range query.OrderBy { + if orderBy != nil && !ExpressionReferencesOnlyLocalIdentifiers(orderBy.Expression, localScope) { + return false + } + } + + return (query.Offset == nil || ExpressionReferencesOnlyLocalIdentifiers(query.Offset, localScope)) && + (query.Limit == nil || ExpressionReferencesOnlyLocalIdentifiers(query.Limit, localScope)) +} + +func AddFromClauseBindings(localScope *pgsql.IdentifierSet, fromClauses []pgsql.FromClause) { + for _, fromClause := range fromClauses { + AddFromExpressionBinding(localScope, fromClause.Source) + + for _, join := range fromClause.Joins { + AddFromExpressionBinding(localScope, join.Table) + } + } +} + +func AddFromExpressionBinding(localScope *pgsql.IdentifierSet, expression pgsql.Expression) { + switch typedExpression := expression.(type) { + case pgsql.TableReference: + if typedExpression.Binding.Set { + localScope.Add(typedExpression.Binding.Value) + } + + case pgsql.LateralSubquery: + if typedExpression.Binding.Set { + localScope.Add(typedExpression.Binding.Value) + } + } +} + +func SelectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *pgsql.IdentifierSet) bool { + scopedIdentifiers := localScope.Copy() + AddFromClauseBindings(scopedIdentifiers, selectBody.From) + + for _, projection := range selectBody.Projection { + if !ExpressionReferencesOnlyLocalIdentifiers(projection, scopedIdentifiers) { + return false + } + } + + for _, fromClause := range selectBody.From { + if !FromExpressionReferencesOnlyLocalIdentifiers(fromClause.Source) { + return false + } + + for _, join := range fromClause.Joins { + if !FromExpressionReferencesOnlyLocalIdentifiers(join.Table) { + return false + } + + if join.JoinOperator.Constraint != nil && + !ExpressionReferencesOnlyLocalIdentifiers(join.JoinOperator.Constraint, scopedIdentifiers) { + return false + } + } + } + + for _, groupByExpression := range selectBody.GroupBy { + if !ExpressionReferencesOnlyLocalIdentifiers(groupByExpression, scopedIdentifiers) { + return false + } + } + + return (selectBody.Where == nil || ExpressionReferencesOnlyLocalIdentifiers(selectBody.Where, scopedIdentifiers)) && + (selectBody.Having == nil || ExpressionReferencesOnlyLocalIdentifiers(selectBody.Having, scopedIdentifiers)) +} + +func FromExpressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression) bool { + switch expression.(type) { + case pgsql.TableReference: + return true + + default: + return false + } +} + +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 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. +func PartitionConstraintByLocality(expression pgsql.Expression, localScope *pgsql.IdentifierSet) (pgsql.Expression, pgsql.Expression) { + var ( + joinConstraints pgsql.Expression + whereConstraints pgsql.Expression + terms = FlattenConjunction(expression) + ) + + for _, term := range terms { + if IsLocalToScope(term, localScope) { + joinConstraints = pgsql.OptionalAnd(joinConstraints, term) + } else { + whereConstraints = pgsql.OptionalAnd(whereConstraints, term) + } + } + + return joinConstraints, whereConstraints +} diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index a487e148..c3637434 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -5,6 +5,7 @@ import ( "github.com/specterops/dawgs/cypher/frontend" "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/stretchr/testify/require" ) @@ -20,6 +21,13 @@ func (s testRule) Apply(plan *Plan) (bool, error) { return false, nil } +type testBindingLookup map[pgsql.Identifier]pgsql.DataType + +func (s testBindingLookup) LookupDataType(identifier pgsql.Identifier) (pgsql.DataType, bool) { + dataType, found := s[identifier] + return dataType, found +} + func TestOptimizeCopiesAndAnalyzesQuery(t *testing.T) { t.Parallel() @@ -162,6 +170,29 @@ func TestLoweringPlanReportsPatternPredicateExistencePlacement(t *testing.T) { }}, plan.LoweringPlan.PatternPredicate) } +func TestSelectivityModelPlansTraversalDirection(t *testing.T) { + t.Parallel() + + model := NewSelectivityModel(testBindingLookup{}) + rightIDLookup := pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{pgsql.Identifier("n1"), pgsql.ColumnID}, + pgsql.OperatorEquals, + pgsql.NewLiteral(1, pgsql.Int), + ) + + shouldFlip, err := model.ShouldFlipTraversalDirection(false, false, nil, rightIDLookup) + require.NoError(t, err) + require.True(t, shouldFlip) + + shouldFlip, err = model.ShouldFlipTraversalDirection(true, false, nil, rightIDLookup) + require.NoError(t, err) + require.False(t, shouldFlip) + + shouldFlip, err = model.ShouldFlipTraversalDirection(false, true, nil, nil) + require.NoError(t, err) + require.True(t, shouldFlip) +} + func TestLoweringPlanReportsLatePathMaterialization(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/optimize/selectivity.go b/cypher/models/pgsql/optimize/selectivity.go new file mode 100644 index 00000000..c7617bea --- /dev/null +++ b/cypher/models/pgsql/optimize/selectivity.go @@ -0,0 +1,299 @@ +package optimize + +import ( + "fmt" + + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/walk" +) + +const ( + // Below are a select set of constants to represent different weights to represent, roughly, the selectivity + // of a given PGSQL expression. These weights are meant to be inexact and are only useful in comparison to other + // summed weights. + // + // The goal of these weights are to enable reordering of queries such that the more selective side of a traversal + // step is expanded first. Eventually, these weights may also enable reordering of multipart queries. + + // Entity ID references are a safe selectivity bet. A direct reference will typically take the form of: + // `n0.id = 1` or some other direct comparison against the entity's ID. All entity IDs are covered by a unique + // b-tree index, making them both highly selective and lucrative to weight higher. + selectivityWeightEntityIDReference = 125 + + // Unique node properties are both covered by a compatible index and unique, making them highly selective. + selectivityWeightUniqueNodeProperty = 100 + + // Bound identifiers are heavily weighted for preserving join order integrity. + selectivityWeightBoundIdentifier = 700 + + // Operators that narrow the search space are given a higher selectivity. + selectivityWeightNarrowSearch = 30 + + // Operators that perform string searches are given a higher selectivity. + selectivityWeightStringSearch = 20 + + // Operators that perform range comparisons are reasonably selective. + selectivityWeightRangeComparison = 10 + + // Conjunctions can narrow search space, especially when compounded, but may be order dependent and unreliable as + // a good selectivity heuristic. + selectivityWeightConjunction = 5 + + // Exclusions can narrow the search space but often only slightly. + selectivityWeightNotEquals = 1 + + // Disjunctions expand search space by adding a secondary, conditional operation. + selectivityWeightDisjunction = -100 + + // selectivityFlipThreshold is the minimum score advantage the right-hand node must hold + // over the left-hand node before constraint balancing commits to a traversal direction flip. + // It is set to selectivityWeightNarrowSearch so that structural AST noise, in particular the + // per-AND-node conjunction bonus, cannot trigger a flip on its own. A single meaningful + // narrowing predicate (=, IN, kind filter) on the right side is sufficient to clear this + // bar; a bare AND connector (weight 5) or a range comparison on an unindexed property + // (weight 10) is not. + selectivityFlipThreshold = selectivityWeightNarrowSearch + + // selectivityBidirectionalAnchorThreshold is the minimum score each endpoint must carry + // before shortest-path translation starts a bidirectional search from both sides. This + // keeps broad label-only endpoints out of bidirectional BFS; a single kind predicate + // scores below this threshold, while a materially narrower property predicate can clear it. + selectivityBidirectionalAnchorThreshold = selectivityWeightNarrowSearch * 2 +) + +// knownNodePropertySelectivity is a hack to enable the selectivity measurement to take advantage of known property +// indexes or uniqueness constraints. +// +// Eventually, this should be replaced by a tool that can introspect a graph schema and derive this map. +var knownNodePropertySelectivity = map[string]int{ + "objectid": selectivityWeightUniqueNodeProperty, // Object ID contains a unique constraint giving this a high degree of selectivity. + "name": selectivityWeightUniqueNodeProperty, // Name contains a unique constraint giving this a high degree of selectivity. + "system_tags": selectivityWeightNarrowSearch, // Searches that use the system_tags property are likely to have a higher degree of selectivity. +} + +type BindingLookup interface { + LookupDataType(identifier pgsql.Identifier) (pgsql.DataType, bool) +} + +type SelectivityModel struct { + bindings BindingLookup +} + +func NewSelectivityModel(bindings BindingLookup) SelectivityModel { + return SelectivityModel{ + bindings: bindings, + } +} + +type propertyLookup struct { + reference pgsql.CompoundIdentifier + field string +} + +type measureSelectivityVisitor struct { + walk.Visitor[pgsql.SyntaxNode] + + model SelectivityModel + selectivityStack []int +} + +func newMeasureSelectivityVisitor(model SelectivityModel) *measureSelectivityVisitor { + return &measureSelectivityVisitor{ + Visitor: walk.NewVisitor[pgsql.SyntaxNode](), + model: model, + selectivityStack: []int{0}, + } +} + +func (s *measureSelectivityVisitor) Selectivity() int { + return s.selectivityStack[0] +} + +func (s *measureSelectivityVisitor) popSelectivity() int { + value := s.Selectivity() + s.selectivityStack = s.selectivityStack[:len(s.selectivityStack)-1] + + return value +} + +func (s *measureSelectivityVisitor) pushSelectivity(value int) { + s.selectivityStack = append(s.selectivityStack, value) +} + +func (s *measureSelectivityVisitor) addSelectivity(value int) { + if len(s.selectivityStack) == 0 { + s.pushSelectivity(value) + } else { + s.selectivityStack[len(s.selectivityStack)-1] += value + } +} + +func isColumnIDRef(expression pgsql.Expression) bool { + switch typedExpression := expression.(type) { + case pgsql.CompoundIdentifier: + if typedExpression.HasField() { + switch typedExpression.Field() { + case pgsql.ColumnID: + return true + } + } + } + + return false +} + +func binaryExpressionToPropertyLookup(expression *pgsql.BinaryExpression) (propertyLookup, error) { + if reference, typeOK := expression.LOperand.(pgsql.CompoundIdentifier); !typeOK { + return propertyLookup{}, fmt.Errorf("expected left operand for property lookup to be a compound identifier but found type: %T", expression.LOperand) + } else if field, typeOK := expression.ROperand.(pgsql.Literal); !typeOK { + return propertyLookup{}, fmt.Errorf("expected right operand for property lookup to be a literal but found type: %T", expression.ROperand) + } else if field.CastType != pgsql.Text { + return propertyLookup{}, fmt.Errorf("expected property lookup field a string literal but found data type: %s", field.CastType) + } else if stringField, typeOK := field.Value.(string); !typeOK { + return propertyLookup{}, fmt.Errorf("expected property lookup field a string literal but found data type: %T", field) + } else { + return propertyLookup{ + reference: reference, + field: stringField, + }, nil + } +} + +func (s *measureSelectivityVisitor) Enter(node pgsql.SyntaxNode) { + switch typedNode := node.(type) { + case *pgsql.UnaryExpression: + switch typedNode.Operator { + case pgsql.OperatorNot: + s.pushSelectivity(0) + } + + case *pgsql.BinaryExpression: + var ( + lOperandIsID = isColumnIDRef(typedNode.LOperand) + rOperandIsID = isColumnIDRef(typedNode.ROperand) + ) + + if lOperandIsID && !rOperandIsID { + // Point lookup: n0.id = ; highly selective. + s.addSelectivity(selectivityWeightEntityIDReference) + } else if rOperandIsID && !lOperandIsID { + // Canonically unusual, but handle it the same. + s.addSelectivity(selectivityWeightEntityIDReference) + } + + // If both sides are ID refs, this is a join condition; do not score as a point lookup. + switch typedNode.Operator { + case pgsql.OperatorOr: + s.addSelectivity(selectivityWeightDisjunction) + + case pgsql.OperatorNotEquals: + s.addSelectivity(selectivityWeightNotEquals) + + case pgsql.OperatorAnd: + s.addSelectivity(selectivityWeightConjunction) + + case pgsql.OperatorLessThan, pgsql.OperatorGreaterThan, pgsql.OperatorLessThanOrEqualTo, pgsql.OperatorGreaterThanOrEqualTo: + s.addSelectivity(selectivityWeightRangeComparison) + + case pgsql.OperatorLike, pgsql.OperatorILike, pgsql.OperatorRegexMatch, pgsql.OperatorSimilarTo: + s.addSelectivity(selectivityWeightStringSearch) + + case pgsql.OperatorIn, pgsql.OperatorEquals, pgsql.OperatorIs: + s.addSelectivity(selectivityWeightNarrowSearch) + + case pgsql.OperatorPGArrayOverlap, pgsql.OperatorArrayOverlap: + s.addSelectivity(selectivityWeightNarrowSearch) + + case pgsql.OperatorPGArrayLHSContainsRHS: + // @> is strictly more selective than &&: all kind_ids must be present. + s.addSelectivity(selectivityWeightNarrowSearch + selectivityWeightConjunction) + + case pgsql.OperatorJSONField, pgsql.OperatorJSONTextField, pgsql.OperatorPropertyLookup: + if propertyLookup, err := binaryExpressionToPropertyLookup(typedNode); err != nil { + s.SetError(err) + } else { + leftIdentifier := propertyLookup.reference.Root() + if s.model.bindings == nil { + return + } + + if dataType, bound := s.model.bindings.LookupDataType(leftIdentifier); !bound { + s.SetErrorf("unable to lookup identifier %s", leftIdentifier) + } else { + switch dataType { + case pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode, pgsql.NodeComposite: + if selectivity, hasKnownSelectivity := knownNodePropertySelectivity[propertyLookup.field]; hasKnownSelectivity { + s.addSelectivity(selectivity) + } + } + } + } + } + } +} + +func (s *measureSelectivityVisitor) Exit(node pgsql.SyntaxNode) { + switch typedNode := node.(type) { + case *pgsql.UnaryExpression: + switch typedNode.Operator { + case pgsql.OperatorNot: + selectivity := s.popSelectivity() + s.addSelectivity(-selectivity) + } + } +} + +func (s SelectivityModel) Measure(expression pgsql.Expression) (int, error) { + visitor := newMeasureSelectivityVisitor(s) + + if expression != nil { + if err := walk.PgSQL(expression, visitor); err != nil { + return 0, err + } + } + + return visitor.Selectivity(), nil +} + +func (s SelectivityModel) ShouldFlipTraversalDirection(leftBound, rightBound bool, leftExpression, rightExpression pgsql.Expression) (bool, error) { + if leftBound { + return false, nil + } + + if rightBound { + return true, nil + } + + leftSelectivity, err := s.Measure(leftExpression) + if err != nil { + return false, err + } + + rightSelectivity, err := s.Measure(rightExpression) + if err != nil { + return false, err + } + + return rightSelectivity-leftSelectivity >= selectivityFlipThreshold, nil +} + +func (s SelectivityModel) EndpointSelectivity(expression pgsql.Expression, bound, hasPreviousFrameBinding bool) (int, error) { + selectivity, err := s.Measure(expression) + if err != nil { + return 0, err + } + + if bound && hasPreviousFrameBinding { + selectivity += selectivityWeightBoundIdentifier + } + + return selectivity, nil +} + +func MeasureSelectivity(bindings BindingLookup, expression pgsql.Expression) (int, error) { + return NewSelectivityModel(bindings).Measure(expression) +} + +func IsBidirectionalSearchAnchor(selectivity int) bool { + return selectivity >= selectivityBidirectionalAnchorThreshold +} diff --git a/cypher/models/pgsql/translate/constraints.go b/cypher/models/pgsql/translate/constraints.go index 7ffa97be..49dd0a63 100644 --- a/cypher/models/pgsql/translate/constraints.go +++ b/cypher/models/pgsql/translate/constraints.go @@ -6,6 +6,7 @@ import ( "github.com/specterops/dawgs/cypher/models/walk" "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/graph" ) @@ -438,7 +439,8 @@ type PatternConstraints struct { // of the traversal has an extreme disparity in search space. // // In cases that match this heuristic, it's beneficial to begin the traversal with the most tightly constrained set -// of nodes. To accomplish this we flip the order of the traversal step. +// of nodes. The optimizer selectivity model decides whether the step should flip; this method only applies that +// decision to the translated constraint and traversal state. func (s *PatternConstraints) OptimizePatternConstraintBalance(scope *Scope, traversalStep *TraversalStep) (bool, error) { // If the left node is already materialized from a previous step, it is the anchor // for this expansion. Flipping the traversal direction would detach it from the @@ -447,26 +449,21 @@ func (s *PatternConstraints) OptimizePatternConstraintBalance(scope *Scope, trav return false, nil } - if traversalStep.RightNodeBound { - // Only flip when a previous frame exists to serve as the FROM source for the - // now-left bound node. In self-referential patterns such as (u)-[]->(u) the - // right node is "bound" because it reuses the left node's variable, but there - // is no preceding CTE to reference. Flipping in that case would set - // LeftNodeBound = true while Frame.Previous is nil, causing a nil-pointer - // dereference in buildTraversalPatternRoot. - if traversalStep.hasPreviousFrameBinding() { - traversalStep.FlipNodes() - s.FlipNodes() - } - - return true, nil + // Only flip a right-bound segment when a previous frame exists to serve as the + // FROM source for the now-left bound node. Self-referential patterns such as + // (u)-[]->(u) can mark the right node as bound without a preceding CTE. + if traversalStep.RightNodeBound && !traversalStep.hasPreviousFrameBinding() { + return false, nil } - if leftNodeSelectivity, err := MeasureSelectivity(scope, s.LeftNode.Expression); err != nil { - return false, err - } else if rightNodeSelectivity, err := MeasureSelectivity(scope, s.RightNode.Expression); err != nil { + if shouldFlip, err := optimize.NewSelectivityModel(scope).ShouldFlipTraversalDirection( + traversalStep.LeftNodeBound, + traversalStep.RightNodeBound, + s.LeftNode.Expression, + s.RightNode.Expression, + ); err != nil { return false, err - } else if rightNodeSelectivity-leftNodeSelectivity >= selectivityFlipThreshold { + } else if shouldFlip { // (a)-[*..]->(b:Constraint) // (a)<-[*..]-(b:Constraint) traversalStep.FlipNodes() diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index 2a0ed669..be95bb51 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -186,20 +186,11 @@ func canMaterializeEndpointPairFilterForStep(traversalStep *TraversalStep, expan } func (s *TraversalStep) endpointSelectivity(scope *Scope, expression pgsql.Expression, bound bool) (int, error) { - selectivity, err := MeasureSelectivity(scope, expression) - if err != nil { - return 0, err - } - - if bound && s.hasPreviousFrameBinding() { - selectivity += selectivityWeightBoundIdentifier - } - - return selectivity, nil + return optimize.NewSelectivityModel(scope).EndpointSelectivity(expression, bound, s.hasPreviousFrameBinding()) } func isBidirectionalSearchAnchor(selectivity int) bool { - return selectivity >= selectivityBidirectionalAnchorThreshold + return optimize.IsBidirectionalSearchAnchor(selectivity) } func hasIDEqualityConstraint(expression pgsql.Expression, identifier pgsql.Identifier) bool { @@ -382,187 +373,44 @@ func (s *TraversalStep) CanExecutePairAwareBidirectionalSearch(scope *Scope) (bo } } -// flattenConjunction collects the leaf operands of a left-recursive AND chain. func flattenConjunction(expr pgsql.Expression) []pgsql.Expression { - if bin, typeOK := expr.(*pgsql.BinaryExpression); !typeOK || bin.Operator != pgsql.OperatorAnd { - return []pgsql.Expression{expr} - } else { - return append(flattenConjunction(bin.LOperand), flattenConjunction(bin.ROperand)...) - } + return optimize.FlattenConjunction(expr) } -// 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) { - switch typedNode := node.(type) { - case pgsql.ExistsExpression: - if !subqueryReferencesOnlyLocalIdentifiers(typedNode.Subquery, localScope) { - isLocal = false - handler.SetDone() - } else { - handler.Consume() - } - - 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() - } - } - }, - )) - - return isLocal + return optimize.ExpressionReferencesOnlyLocalIdentifiers(expression, localScope) } func subqueryReferencesOnlyLocalIdentifiers(subquery pgsql.Subquery, localScope *pgsql.IdentifierSet) bool { - return queryReferencesOnlyLocalIdentifiers(subquery.Query, localScope) + return optimize.SubqueryReferencesOnlyLocalIdentifiers(subquery, localScope) } func queryReferencesOnlyLocalIdentifiers(query pgsql.Query, localScope *pgsql.IdentifierSet) bool { - if query.CommonTableExpressions != nil { - return false - } - - selectBody, isSelect := query.Body.(pgsql.Select) - if !isSelect { - return false - } - - if !selectReferencesOnlyLocalIdentifiers(selectBody, localScope) { - return false - } - - for _, orderBy := range query.OrderBy { - if orderBy != nil && !expressionReferencesOnlyLocalIdentifiers(orderBy.Expression, localScope) { - return false - } - } - - return (query.Offset == nil || expressionReferencesOnlyLocalIdentifiers(query.Offset, localScope)) && - (query.Limit == nil || expressionReferencesOnlyLocalIdentifiers(query.Limit, localScope)) + return optimize.QueryReferencesOnlyLocalIdentifiers(query, localScope) } func addFromClauseBindings(localScope *pgsql.IdentifierSet, fromClauses []pgsql.FromClause) { - for _, fromClause := range fromClauses { - addFromExpressionBinding(localScope, fromClause.Source) - - for _, join := range fromClause.Joins { - addFromExpressionBinding(localScope, join.Table) - } - } + optimize.AddFromClauseBindings(localScope, fromClauses) } func addFromExpressionBinding(localScope *pgsql.IdentifierSet, expression pgsql.Expression) { - switch typedExpression := expression.(type) { - case pgsql.TableReference: - if typedExpression.Binding.Set { - localScope.Add(typedExpression.Binding.Value) - } - - case pgsql.LateralSubquery: - if typedExpression.Binding.Set { - localScope.Add(typedExpression.Binding.Value) - } - } + optimize.AddFromExpressionBinding(localScope, expression) } func selectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *pgsql.IdentifierSet) bool { - scopedIdentifiers := localScope.Copy() - addFromClauseBindings(scopedIdentifiers, selectBody.From) - - for _, projection := range selectBody.Projection { - if !expressionReferencesOnlyLocalIdentifiers(projection, scopedIdentifiers) { - return false - } - } - - for _, fromClause := range selectBody.From { - if !fromExpressionReferencesOnlyLocalIdentifiers(fromClause.Source) { - return false - } - - for _, join := range fromClause.Joins { - if !fromExpressionReferencesOnlyLocalIdentifiers(join.Table) { - return false - } - - if join.JoinOperator.Constraint != nil && - !expressionReferencesOnlyLocalIdentifiers(join.JoinOperator.Constraint, scopedIdentifiers) { - return false - } - } - } - - for _, groupByExpression := range selectBody.GroupBy { - if !expressionReferencesOnlyLocalIdentifiers(groupByExpression, scopedIdentifiers) { - return false - } - } - - return (selectBody.Where == nil || expressionReferencesOnlyLocalIdentifiers(selectBody.Where, scopedIdentifiers)) && - (selectBody.Having == nil || expressionReferencesOnlyLocalIdentifiers(selectBody.Having, scopedIdentifiers)) + return optimize.SelectReferencesOnlyLocalIdentifiers(selectBody, localScope) } func fromExpressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression) bool { - switch expression.(type) { - case pgsql.TableReference: - return true - - default: - return false - } + return optimize.FromExpressionReferencesOnlyLocalIdentifiers(expression) } func isLocalToScope(expression pgsql.Expression, localScope *pgsql.IdentifierSet) bool { - if expression == nil { - return true - } - - return expressionReferencesOnlyLocalIdentifiers(expression, localScope) + return optimize.IsLocalToScope(expression, localScope) } -// partitionConstraintByLocality splits a conjunction (A AND B AND ...) into -// 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. func partitionConstraintByLocality(expression pgsql.Expression, localScope *pgsql.IdentifierSet) (pgsql.Expression, pgsql.Expression) { - var ( - joinConstraints pgsql.Expression - whereConstraints pgsql.Expression - terms = flattenConjunction(expression) - ) - - for _, term := range terms { - if isLocalToScope(term, localScope) { - joinConstraints = pgsql.OptionalAnd(joinConstraints, term) - } else { - whereConstraints = pgsql.OptionalAnd(whereConstraints, term) - } - } - - return joinConstraints, whereConstraints + return optimize.PartitionConstraintByLocality(expression, localScope) } type ProjectionPruningApplication struct { diff --git a/cypher/models/pgsql/translate/selectivity.go b/cypher/models/pgsql/translate/selectivity.go index c75b0a9f..715e58ce 100644 --- a/cypher/models/pgsql/translate/selectivity.go +++ b/cypher/models/pgsql/translate/selectivity.go @@ -2,226 +2,9 @@ package translate import ( "github.com/specterops/dawgs/cypher/models/pgsql" - "github.com/specterops/dawgs/cypher/models/walk" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" ) -const ( - // Below are a select set of constants to represent different weights to represent, roughly, the selectivity - // of a given PGSQL expression. These weights are meant to be inexact and are only useful in comparison to other - // summed weights. - // - // The goal of these weights are to enable reordering of queries such that the more selective side of a traversal - // step is expanded first. Eventually, these weights may also enable reordering of multipart queries. - - // Entity ID references are a safe selectivity bet. A direct reference will typically take the form of: - // `n0.id = 1` or some other direct comparison against the entity's ID. All entity IDs are covered by a unique - // b-tree index, making them both highly selective and lucrative to weight higher. - selectivityWeightEntityIDReference = 125 - - // Unique node properties are both covered by a compatible index and unique, making them highly selective - selectivityWeightUniqueNodeProperty = 100 - - // Bound identifiers are heavily weighted for preserving join order integrity - selectivityWeightBoundIdentifier = 700 - - // Operators that narrow the search space are given a higher selectivity - selectivityWeightNarrowSearch = 30 - - // Operators that perform string searches are given a higher selectivity - selectivityWeightStringSearch = 20 - - // Operators that perform range comparisons are reasonably selective - selectivityWeightRangeComparison = 10 - - // Conjunctions can narrow search space, especially when compounded, but may be order dependent and unreliable as - // a good selectivity heuristic - selectivityWeightConjunction = 5 - - // Exclusions can narrow the search space but often only slightly - selectivityWeightNotEquals = 1 - - // Disjunctions expand search space by adding a secondary, conditional operation - selectivityWeightDisjunction = -100 - - // selectivityFlipThreshold is the minimum score advantage the right-hand node must hold - // over the left-hand node before OptimizePatternConstraintBalance commits to a traversal - // direction flip. It is set to selectivityWeightNarrowSearch so that structural AST noise - // — in particular the per-AND-node conjunction bonus — cannot trigger a flip on its own. - // A single meaningful narrowing predicate (=, IN, kind filter) on the right side is - // sufficient to clear this bar; a bare AND connector (weight 5) or a range comparison on - // an unindexed property (weight 10) is not. - selectivityFlipThreshold = selectivityWeightNarrowSearch - - // selectivityBidirectionalAnchorThreshold is the minimum score each endpoint must carry - // before shortest-path translation starts a bidirectional search from both sides. This - // keeps broad label-only endpoints out of bidirectional BFS; a single kind predicate - // scores below this threshold, while a materially narrower property predicate can clear it. - selectivityBidirectionalAnchorThreshold = selectivityWeightNarrowSearch * 2 -) - -// knownNodePropertySelectivity is a hack to enable the selectivity measurement to take advantage of known property indexes -// or uniqueness constraints. -// -// Eventually, this should be replaced by a tool that can introspect a graph schema and derive this map. -var knownNodePropertySelectivity = map[string]int{ - "objectid": selectivityWeightUniqueNodeProperty, // Object ID contains a unique constraint giving this a high degree of selectivity - "name": selectivityWeightUniqueNodeProperty, // Name contains a unique constraint giving this a high degree of selectivity - "system_tags": selectivityWeightNarrowSearch, // Searches that use the system_tags property are likely to have a higher degree of selectivity. -} - -type measureSelectivityVisitor struct { - walk.Visitor[pgsql.SyntaxNode] - - scope *Scope - selectivityStack []int -} - -func newMeasureSelectivityVisitor(scope *Scope) *measureSelectivityVisitor { - return &measureSelectivityVisitor{ - Visitor: walk.NewVisitor[pgsql.SyntaxNode](), - scope: scope, - selectivityStack: []int{0}, - } -} - -func (s *measureSelectivityVisitor) Selectivity() int { - return s.selectivityStack[0] -} - -func (s *measureSelectivityVisitor) popSelectivity() int { - value := s.Selectivity() - s.selectivityStack = s.selectivityStack[:len(s.selectivityStack)-1] - - return value -} - -func (s *measureSelectivityVisitor) pushSelectivity(value int) { - s.selectivityStack = append(s.selectivityStack, value) -} - -func (s *measureSelectivityVisitor) addSelectivity(value int) { - if len(s.selectivityStack) == 0 { - s.pushSelectivity(value) - } else { - s.selectivityStack[len(s.selectivityStack)-1] += value - } -} - -func isColumnIDRef(expression pgsql.Expression) bool { - switch typedExpression := expression.(type) { - case pgsql.CompoundIdentifier: - if typedExpression.HasField() { - switch typedExpression.Field() { - case pgsql.ColumnID: - return true - } - } - } - - return false -} - -func (s *measureSelectivityVisitor) Enter(node pgsql.SyntaxNode) { - switch typedNode := node.(type) { - case *pgsql.UnaryExpression: - switch typedNode.Operator { - case pgsql.OperatorNot: - s.pushSelectivity(0) - } - - case *pgsql.BinaryExpression: - var ( - lOperandIsID = isColumnIDRef(typedNode.LOperand) - rOperandIsID = isColumnIDRef(typedNode.ROperand) - ) - - if lOperandIsID && !rOperandIsID { - // Point lookup: n0.id = — highly selective - s.addSelectivity(selectivityWeightEntityIDReference) - } else if rOperandIsID && !lOperandIsID { - // Canonically unusual, but handle it the same - s.addSelectivity(selectivityWeightEntityIDReference) - } - - // If both sides are ID refs, this is a join condition — do not score as a point lookup - - switch typedNode.Operator { - case pgsql.OperatorOr: - s.addSelectivity(selectivityWeightDisjunction) - - case pgsql.OperatorNotEquals: - s.addSelectivity(selectivityWeightNotEquals) - - case pgsql.OperatorAnd: - s.addSelectivity(selectivityWeightConjunction) - - case pgsql.OperatorLessThan, pgsql.OperatorGreaterThan, pgsql.OperatorLessThanOrEqualTo, pgsql.OperatorGreaterThanOrEqualTo: - s.addSelectivity(selectivityWeightRangeComparison) - - case pgsql.OperatorLike, pgsql.OperatorILike, pgsql.OperatorRegexMatch, pgsql.OperatorSimilarTo: - s.addSelectivity(selectivityWeightStringSearch) - - case pgsql.OperatorIn, pgsql.OperatorEquals, pgsql.OperatorIs: - s.addSelectivity(selectivityWeightNarrowSearch) - - case pgsql.OperatorPGArrayOverlap, pgsql.OperatorArrayOverlap: - s.addSelectivity(selectivityWeightNarrowSearch) - - case pgsql.OperatorPGArrayLHSContainsRHS: - // @> is strictly more selective than &&: all kind_ids must be present. - s.addSelectivity(selectivityWeightNarrowSearch + selectivityWeightConjunction) - - case pgsql.OperatorJSONField, pgsql.OperatorJSONTextField, pgsql.OperatorPropertyLookup: - if propertyLookup, err := binaryExpressionToPropertyLookup(typedNode); err != nil { - s.SetError(err) - } else { - // Lookup the reference - leftIdentifier := propertyLookup.Reference.Root() - - if binding, bound := s.scope.Lookup(leftIdentifier); !bound { - s.SetErrorf("unable to lookup identifier %s", leftIdentifier) - } else { - switch binding.DataType { - case pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode, pgsql.NodeComposite: - // This is a node property, search through the available node property selectivity weights - if selectivity, hasKnownSelectivity := knownNodePropertySelectivity[propertyLookup.Field]; hasKnownSelectivity { - s.addSelectivity(selectivity) - } - } - } - } - } - } -} - -func (s *measureSelectivityVisitor) Exit(node pgsql.SyntaxNode) { - switch typedNode := node.(type) { - case *pgsql.UnaryExpression: - switch typedNode.Operator { - case pgsql.OperatorNot: - selectivity := s.popSelectivity() - s.addSelectivity(-selectivity) - } - } -} - -// MeasureSelectivity attempts to measure how selective (i.e. how narrow) the query expression passed in is. This is -// a simple heuristic that is best-effort for attempting to find which side of a traversal step ()-[]->() is more -// selective. -// -// The boolean parameter owningIdentifierBound is intended to represent if the identifier the expression constraints -// is part of a materialized set of nodes where the entity IDs of each are known at time of query. In this case the -// bound component is considered to be highly-selective. -// -// Many numbers are magic values selected based on implementor's perception of selectivity of certain operators. func MeasureSelectivity(scope *Scope, expression pgsql.Expression) (int, error) { - visitor := newMeasureSelectivityVisitor(scope) - - if expression != nil { - if err := walk.PgSQL(expression, visitor); err != nil { - return 0, err - } - } - - return visitor.Selectivity(), nil + return optimize.MeasureSelectivity(scope, expression) } diff --git a/cypher/models/pgsql/translate/tracking.go b/cypher/models/pgsql/translate/tracking.go index bfaadc4f..8bb328a0 100644 --- a/cypher/models/pgsql/translate/tracking.go +++ b/cypher/models/pgsql/translate/tracking.go @@ -344,6 +344,14 @@ func (s *Scope) LookupString(identifierString string) (*BoundIdentifier, bool) { return s.AliasedLookup(pgsql.Identifier(identifierString)) } +func (s *Scope) LookupDataType(identifier pgsql.Identifier) (pgsql.DataType, bool) { + if binding, bound := s.Lookup(identifier); !bound { + return "", false + } else { + return binding.DataType, true + } +} + func (s *Scope) Define(identifier pgsql.Identifier, dataType pgsql.DataType) *BoundIdentifier { boundIdentifier := &BoundIdentifier{ Identifier: identifier, diff --git a/docs/optimization-pass-memory.md b/docs/optimization-pass-memory.md deleted file mode 100644 index a1237bd3..00000000 --- a/docs/optimization-pass-memory.md +++ /dev/null @@ -1,287 +0,0 @@ -# Cypher Optimization Pass Memory - -## Purpose - -The PostgreSQL translator currently lowers Cypher traversal parts mostly in source order. That is simple and predictable, but it can produce expensive SQL for multi-part path queries where a later pattern contains more selective anchors or where returned path payloads are carried through unrelated expansions. - -This note captures a conservative plan for introducing a PostgreSQL-specific pre-translation optimization phase. The goal is not to require users to reauthor valid Cypher to get acceptable runtime behavior. - -## Motivating Query Shape - -```cypher -MATCH (n:Group) -WHERE n.objectid = 'S-1-5-21-2643190041-1319121918-239771340-513' -MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) -MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) -WHERE ct.authenticationenabled = true -AND ct.requiresmanagerapproval = false -AND ct.enrolleesuppliessubject = true -AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) -RETURN p1, p2 -``` - -The current PostgreSQL shape can preserve too much intermediate state. In particular, because `p1` is returned, path-related state from `p1` may be carried through the `p2` expansion before `p2` has been filtered. Neo4j's planner is more flexible: it can reorder pattern evaluation, use endpoint-aware expansion, and materialize path values late. - -## Architectural Decisions - -The first optimizer effort is intentionally PostgreSQL-specific. The optimizer should avoid painting the project into a backend-neutral corner, but PostgreSQL is the only Cypher translation target that currently needs this work. - -- Ship optimizer rules directly once they are covered. Do not add a user-facing feature flag surface for optimizer behavior. -- Optimize only read-only `MATCH` and `WHERE` groups inside a single query part for the first milestone. -- Treat `WITH`, `RETURN`, aggregation, `DISTINCT`, `ORDER BY`, `LIMIT`, `UNWIND`, `OPTIONAL MATCH`, writes, and procedure calls as semantic barriers. -- Allow the optimizer to build a new ordered logical plan inside eligible regions. -- Represent path variables as late-materialized recipes throughout the optimized PostgreSQL logical model. -- Use deterministic heuristics for early reordering. Defer schema statistics and cost-based planning. -- Accept more complex SQL when it materially improves runtime conditions. The database is responsible for executing the improved plan shape. -- Defer broad benchmark and real-world query set definition until after the basic framework and first optimizer rules are in place. - -## Safety Constraints - -Keep the first implementation deliberately conservative. - -- Preserve Cypher row semantics, path relationship uniqueness, variable binding rules, and zero-length expansion behavior. -- Keep each optimization rule individually named and testable. -- If a rule cannot prove a rewrite is safe, keep the original logical order for that part of the plan. -- Require translation-shape tests, PostgreSQL integration equivalence, and Neo4j equivalence coverage for optimizer behavior. - -## Sequenced Plan - -### Phase 1: Define Optimizer Boundaries - -Document the Cypher regions eligible for optimization and the barriers that terminate an optimization region. The initial eligible region should be a read-only sequence of `MATCH` and `WHERE` clauses within one query part. - -Add diagnostics that can print the logical pattern parts, bindings, predicates, path variables, and final projection dependencies before translation. - -### Phase 2: Build The Safety Net - -Add translation-shape coverage for the motivating ADCS query. The first tests should capture the current expensive SQL shape so improvements can be measured. - -Add smaller focused cases for: - -- multiple `MATCH` clauses sharing variables -- returned path variables used only at final projection -- variable-length expansion followed by a fixed suffix -- repeated bound variables such as `(ca)` and `(d)` -- zero-length expansion with `*0..` - -Validate optimizer behavior with all three test classes: - -- translation-shape tests -- PostgreSQL integration equivalence tests -- Neo4j integration equivalence tests - -Neo4j tests should assert result shape and semantics, not exact Neo4j plan shape. - -### Phase 3: Introduce A No-Op Optimizer Skeleton - -Insert a PostgreSQL-specific pre-translation logical optimization pass between parsing/semantic modeling and PostgreSQL rendering. - -The initial pass should return the same logical model it receives. This keeps the integration point small and gives tests a stable hook before behavioral rules are added. - -Suggested rule names: - -- `PredicateAttachment` -- `ProjectionPruning` -- `LatePathMaterialization` -- `ExpandIntoDetection` -- `ConservativePatternReordering` -- `VariableExpansionTerminalPushdown` - -### Phase 4: Attach Predicates To Their Bindings - -Move eligible `WHERE` predicates as close as possible to the bindings they reference. - -For the motivating query, the `ct.*` predicates should be owned by the `ct:CertTemplate` binding. This does not need to reorder pattern matching at first; it makes predicate dependencies explicit so later rules can apply filters earlier. - -### Phase 5: Prune Intermediate Projections And Paths - -Compute a narrower carry set for each operation: - -- bindings needed by the next operation -- bindings needed by predicates -- bindings needed as join keys -- bindings needed later only to construct returned values - -The translator should not carry every visible binding through every later expansion just because it appears in the final `RETURN`. - -This should be the first real runtime-focused optimization rule. It directly addresses the reported query shape, creates the liveness information required by later rules, and is lower risk than traversal reordering or suffix pushdown. - -### Phase 6: Materialize Paths Late - -Represent returned paths internally as recipes over node and relationship bindings rather than as fully materialized values throughout every step. - -For the motivating query, the optimizer should be able to continue from a narrow frame after `p1`, such as distinct `(n, ca, d)`, evaluate and filter `p2`, then join back to the full `p1` rows and materialize `p1` and `p2` at the final projection. - -This is the first high-value optimization target because it reduces row width and delays the `p1 x p2` multiplication without changing the user's Cypher. - -### Phase 7: Detect Expand-Into Opportunities - -When both endpoints of a relationship or variable-length segment are known, lower that segment as a constrained connectivity/path problem instead of an open expansion. - -This mirrors Neo4j's `Expand(Into)` and `VarLengthExpand(Into)` behavior and is especially relevant when separate `MATCH` clauses bind endpoints that are reused later. - -### Phase 8: Add Deterministic Pattern Reordering - -After projection pruning and late materialization are stable, allow limited reordering inside a single read-only optimization region. - -Start with obvious anchors: - -- node label plus equality property -- fixed relationship type scans -- already-bound endpoints -- selective labels or properties only when deterministic local information is available - -Do not begin with a general cost-based planner or schema-statistics dependency. Prefer deterministic rewrites with clear safety checks. - -### Phase 9: Push Terminal Constraints Into Variable Expansions - -For variable-length expansions followed by fixed suffixes, add terminal or suffix constraints as semi-joins or correlated existence checks. - -For the motivating query, this means avoiding emission of `MemberOf*0..` endpoints that cannot reach an eligible `CertTemplate` published to the already-bound `ca`, and avoiding `RootCA` endpoints that cannot connect back to the already-bound `d`. - -### Phase 10: Measure Each Rule Locally - -Every optimization rule should include: - -- unit or translation tests for the logical rewrite -- PostgreSQL integration result-equivalence coverage -- Neo4j integration result-equivalence coverage -- SQL shape assertions for representative queries -- before and after `EXPLAIN` comparison on synthetic fanout data - -The synthetic data should include many `p1` paths to the same `(n, ca, d)`, many membership paths from `n`, and only a small number of eligible certificate template and root CA paths. - -Broader benchmark suites and real-world query collections are deferred until after the basic optimizer framework and first rules are implemented. - -## Recommended First Milestone - -Implement phases 1 through 6 first. - -That milestone establishes the PostgreSQL optimizer framework, test bar, predicate ownership, projection and path pruning, and late path materialization. It should improve the reported query shape without taking on endpoint-aware expansion, suffix semi-joins, schema statistics, or a full cost-based planner. - -## Quality Review Follow-Up Plan - -The first optimizer milestone introduced the PostgreSQL optimizer hook, predicate attachment diagnostics, projection pruning, and late path materialization. Before moving on to endpoint-aware expansion or pattern reordering, close the review gaps in this order: - -### Step 1: Preserve The Optional-Match Barrier - -Keep projection pruning and late path materialization scoped to plain `MATCH` translation until optional path semantics have dedicated coverage. `OPTIONAL MATCH` already acts as an optimization-region barrier in the analysis pass; translator-side lowering should respect the same boundary. - -### Step 2: Assert Path Semantics, Not Only SQL Shape - -Expand integration coverage for optimized path returns so tests assert path node order, relationship order, and path length for mixed fixed-hop and variable-length paths. Include `relationships(p)` on paths that are eligible for late materialization. - -### Step 3: Harden Direct Relationship References - -Add focused translation tests proving direct relationship references keep edge composites when used in returned values, predicates, relationship properties, `type(r)`, and endpoint functions such as `startNode(r)`. - -### Step 4: Document Performance Measurement Needs - -Keep the current shape tests as guardrails, but add an explicit measurement task for high-fanout ADCS-style data before expanding the optimizer into endpoint-aware expansion, suffix semi-joins, or deterministic reordering. - -## Quality Review Status Notes - -The review follow-up should leave the first optimizer milestone in a measured state before the next rule is attempted. - -- `OPTIONAL MATCH` must remain a translator pruning barrier until optional path returns and optional path functions have semantic integration coverage. -- Mixed fixed-hop plus variable-length path returns should assert exact node order, relationship order, and path length. These cases exercise the same late-materialization mechanics as the motivating query with a smaller result surface. -- `relationships(p)` should have relationship-list assertions so path component functions are checked directly instead of indirectly through SQL shape. -- Direct relationship bindings referenced by return expressions, predicates, `type(r)`, or endpoint functions must keep edge composites and must not be narrowed to path-edge IDs. -- The ADCS fixture currently has SQL-shape and containment coverage. Stricter path cardinality assertions on PostgreSQL exposed duplicated returned path rows during review, so exact cardinality for that fixture should be investigated as part of the high-fanout measurement work rather than added as a passing oracle prematurely. - -## Current Gap Closure Plan - -The optimizer branch now has enough implementation to expose the next set of risks. Close those risks in this order so each later rule has a stronger correctness and measurement base. - -### Step 1: Establish A Performance Baseline - -Add a synthetic ADCS fanout scenario before broadening suffix or endpoint-aware expansion rules. Capture `p1` alone, `p2` alone, and the combined query with row counts, distinct `(p1, p2)` counts, duplicate counts, and PostgreSQL `EXPLAIN (ANALYZE, BUFFERS)`. - -This should be the first step because the original report is a timeout, and the current branch is still defended mostly by SQL shape and semantic equivalence tests. - -### Step 2: Strengthen Semantic Oracles - -Add exact-result integration coverage on smaller fixtures before relying on the larger ADCS fixture as an oracle. Assertions should include path node IDs, relationship IDs or kinds in order, path lengths, row count, and `relationships(p)` output for optimized paths. - -Keep the existing ADCS containment test, but treat exact ADCS cardinality as part of the fanout investigation until duplicate-row behavior is understood. - -### Step 3: Make Optimizer Rule Ownership Explicit - -Projection pruning, late path materialization, fixed-hop lowering, and suffix pushdown currently live in PostgreSQL translator lowering instead of explicit optimizer rules. Either promote these decisions into optimizer metadata consumed by the translator, or record them as named lowering decisions so tests and diagnostics can identify which rule changed the SQL shape. - -This step should happen before adding more hidden translator-side rewrites. - -### Step 4: Wire Predicate Attachment Into Translation - -Predicate attachment currently records ownership but does not change translation. Feed attachment metadata into PostgreSQL lowering so local predicates can move into the earliest safe binding, terminal, or suffix check. - -Add SQL shape tests proving `ct` predicates in the motivating query are applied at the intended terminal or suffix point, plus PostgreSQL and Neo4j equivalence coverage. - -### Step 5: Broaden Phase 9 Coverage Before Broadening Phase 9 Behavior - -Add tests for the suffix shapes the motivating query actually depends on: - -- `*0..` variable expansions followed by suffix checks -- chained fixed suffixes after a variable expansion -- suffixes that end at already-bound nodes such as `ca` and `d` -- inbound suffixes -- directionless suffixes that should remain unoptimized until they are implemented deliberately - -These tests should include both SQL shape assertions and integration equivalence. - -### Step 6: Implement Endpoint-Aware Suffix Semi-Joins - -Extend suffix pushdown from the current immediate one-hop local check to endpoint-aware semi-joins that can reason about fixed suffix chains and already-bound endpoints. For the motivating query, this means pruning `MemberOf*0..` endpoints that cannot reach eligible certificate template paths tied to the bound `ca`, and pruning root paths that cannot connect back to the bound `d`. - -Keep path materialization late: use the suffix checks to constrain candidate endpoints, then materialize returned paths only after the result frame is narrowed. - -### Step 7: Re-Measure And Lock The Regression - -After each new suffix or predicate-placement rule, rerun the synthetic fanout measurements and record the before/after SQL shape and runtime characteristics. Promote the final motivating query shape into a benchmark or regression scenario once its cardinality and duplicate behavior are fully understood. - -## Measurement Checklist Before Phase 7 - -Before implementing expand-into detection, capture the following for the motivating ADCS query and a synthetic fanout variant: - -- `EXPLAIN (ANALYZE, BUFFERS)` for `p1` alone, `p2` alone, and the combined query. -- Result row count, distinct `(p1, p2)` count, and duplicate-row count. -- Intermediate row counts for expansion CTEs before and after projection pruning. -- Final path reconstruction cost when paths are returned versus when only endpoint keys are returned. -- Comparison with Neo4j result cardinality for the same fixture. - -Projection pruning and late path materialization currently live in PostgreSQL translator lowering. If later phases need richer rule-level ordering or barrier enforcement, promote these decisions into explicit optimizer rule metadata instead of adding more hidden translator-side state. - -## Gap Closure Completion Notes - -The gap-closure pass has been completed enough to return to the original phase sequence without broadening into Phase 10. - -- The benchmark harness includes the committed `adcs_fanout` dataset by default and has scenarios for `p1` alone, `p2` alone, and the combined `RETURN p1,p2` form. -- ADCS path scenarios now record warm-up row count, distinct returned path-row count, and duplicate returned path-row count. -- PostgreSQL benchmark runs can opt into `EXPLAIN (ANALYZE, BUFFERS)` capture with `-explain`; JSON output includes the translated SQL and plan text. -- The small ADCS integration fixture now asserts exact returned path shape and row count. The larger fanout fixture remains a measurement fixture rather than an exact cardinality oracle. -- Translation metadata reports optimizer rules, predicate attachments, planned lowerings, and applied lowerings, including `ExpansionSuffixPushdown`. -- Phase 9 suffix coverage includes zero-hop expansions, fixed suffix chains, suffixes ending at already-bound nodes, inbound suffixes, and the ADCS root-to-domain suffix shape. -- Directionless suffix pushdown remains deliberately unimplemented; those suffixes stay as normal translated pattern steps. - -## Phase 10 Status Notes - -Phase 10 starts by making local measurements repeatable for the optimizer rules already implemented. - -- PostgreSQL `-explain` benchmark JSON includes translated SQL, `EXPLAIN (ANALYZE, BUFFERS)` plan text, optimizer rule results, predicate attachments, and translator lowering decisions. -- The ADCS fanout benchmark includes `p1` alone, `p2` alone, combined path return, and combined endpoint-only return. The endpoint-only scenario gives a local comparison point for final path reconstruction cost. -- The benchmark runner rejects zero timed iterations so baseline output cannot silently panic while gathering measurements. -- Representative SQL-shape tests assert that suffix-local predicates are inside the pushed suffix check, not merely present somewhere in the rendered SQL. -- Broad pass/fail performance thresholds remain deferred. Phase 10 measurements are local evidence and regression artifacts first; cost-based acceptance gates should wait for a larger benchmark corpus and stable environment assumptions. - -## Lowering Ownership Refactor Notes - -The translator now consumes optimizer-owned lowering metadata for projection pruning, late path materialization, fixed-hop expand-into detection, expansion suffix pushdown, and predicate placement. PostgreSQL SQL construction remains in the translator, but rule ownership and benchmark-visible diagnostics live in the optimizer lowering plan. - -Translator-local eligibility checks remain as conservative fallbacks for traversal steps that do not have an optimizer decision. Benchmark JSON includes planned lowerings, applied lowerings, and the structured lowering plan so future reviews can distinguish optimizer intent from SQL-shape changes that actually happened. - -### Lowering Metadata Contract - -- `LoweringPlan` is optimizer intent over the Cypher source shape. It may include decisions that the PostgreSQL translator later declines because the lowered SQL shape is no longer eligible. -- `planned_lowerings` is the compact, benchmark-friendly view of `LoweringPlan.Decisions()`. -- `lowerings` is translator-applied behavior only. It must be recorded when SQL generation actually changes shape, not when the optimizer merely planned a decision. -- Optimizer code may describe source-level actions and eligibility. PostgreSQL frame visibility, data-type rewrites, and SQL AST construction remain translator responsibilities. From 14392f0862f0b6ea74b1b61dc1fef3d179c088b2 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 21 May 2026 23:45:03 -0700 Subject: [PATCH 046/114] fix(pgsql): address optimizer review feedback --- cypher/models/pgsql/optimize/locality.go | 21 ++- cypher/models/pgsql/optimize/lowering_plan.go | 2 +- .../models/pgsql/optimize/optimizer_test.go | 80 +++++++++++ cypher/models/pgsql/optimize/selectivity.go | 2 +- .../pgsql/optimize/source_references.go | 4 +- .../test/translation_cases/multipart.sql | 10 +- .../translation_cases/pattern_binding.sql | 8 +- .../translation_cases/pattern_expansion.sql | 2 +- .../pgsql/translate/constraints_test.go | 31 ++++ cypher/models/pgsql/translate/expansion.go | 134 ++++++++++++++---- cypher/models/pgsql/translate/function.go | 9 +- .../models/pgsql/translate/function_test.go | 16 +++ cypher/models/pgsql/translate/model.go | 4 +- .../pgsql/translate/optimizer_safety_test.go | 22 +++ cypher/models/pgsql/translate/pattern.go | 2 + cypher/models/pgsql/translate/predicate.go | 2 +- .../models/pgsql/translate/predicate_test.go | 24 ++++ cypher/models/pgsql/translate/tracking.go | 10 +- .../models/pgsql/translate/tracking_test.go | 12 ++ integration/harness.go | 2 +- 20 files changed, 335 insertions(+), 62 deletions(-) diff --git a/cypher/models/pgsql/optimize/locality.go b/cypher/models/pgsql/optimize/locality.go index 22301ce9..05ceccef 100644 --- a/cypher/models/pgsql/optimize/locality.go +++ b/cypher/models/pgsql/optimize/locality.go @@ -108,21 +108,20 @@ func AddFromExpressionBinding(localScope *pgsql.IdentifierSet, expression pgsql. } } +func addFromClauseSourceBinding(localScope *pgsql.IdentifierSet, fromClause pgsql.FromClause) { + AddFromExpressionBinding(localScope, fromClause.Source) +} + func SelectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *pgsql.IdentifierSet) bool { scopedIdentifiers := localScope.Copy() - AddFromClauseBindings(scopedIdentifiers, selectBody.From) - - for _, projection := range selectBody.Projection { - if !ExpressionReferencesOnlyLocalIdentifiers(projection, scopedIdentifiers) { - return false - } - } for _, fromClause := range selectBody.From { if !FromExpressionReferencesOnlyLocalIdentifiers(fromClause.Source) { return false } + addFromClauseSourceBinding(scopedIdentifiers, fromClause) + for _, join := range fromClause.Joins { if !FromExpressionReferencesOnlyLocalIdentifiers(join.Table) { return false @@ -132,6 +131,14 @@ func SelectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *p !ExpressionReferencesOnlyLocalIdentifiers(join.JoinOperator.Constraint, scopedIdentifiers) { return false } + + AddFromExpressionBinding(scopedIdentifiers, join.Table) + } + } + + for _, projection := range selectBody.Projection { + if !ExpressionReferencesOnlyLocalIdentifiers(projection, scopedIdentifiers) { + return false } } diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 015c154f..b40dee40 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -609,7 +609,7 @@ func appendLimitPushdownDecisions(plan *LoweringPlan, queryPartIndex int, queryP } for clauseIndex, readingClause := range readingClauses { - if readingClause == nil || readingClause.Match == nil { + if readingClause == nil || readingClause.Match == nil || readingClause.Match.Optional { continue } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index c3637434..227e0244 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models" "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/stretchr/testify/require" @@ -618,6 +619,85 @@ func TestLoweringPlanSkipsAllShortestPathLimitPushdown(t *testing.T) { require.Empty(t, plan.LoweringPlan.LimitPushdown) } +func TestLoweringPlanSkipsOptionalMatchLimitPushdown(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n)-[:MemberOf]->(m:Group) + RETURN p + LIMIT 1 + `) + require.NoError(t, err) + require.Len(t, regularQuery.SingleQuery.SinglePartQuery.ReadingClauses, 1) + regularQuery.SingleQuery.SinglePartQuery.ReadingClauses[0].Match.Optional = true + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Empty(t, plan.LoweringPlan.LimitPushdown) +} + +func TestSelectReferencesOnlyLocalIdentifiersValidatesJoinConstraintsIncrementally(t *testing.T) { + t.Parallel() + + tableRef := func(alias pgsql.Identifier) pgsql.TableReference { + return pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{pgsql.TableNode}, + Binding: models.OptionalValue(alias), + } + } + + selectBody := pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{pgsql.Identifier("a"), pgsql.ColumnID}, + }, + From: []pgsql.FromClause{{ + Source: tableRef(pgsql.Identifier("a")), + Joins: []pgsql.Join{{ + Table: tableRef(pgsql.Identifier("b")), + JoinOperator: pgsql.JoinOperator{ + Constraint: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{pgsql.Identifier("b"), pgsql.ColumnID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{pgsql.Identifier("c"), pgsql.ColumnID}, + ), + }, + }, { + Table: tableRef(pgsql.Identifier("c")), + }}, + }}, + } + + require.False(t, SelectReferencesOnlyLocalIdentifiers(selectBody, pgsql.NewIdentifierSet())) +} + +func TestMeasureSelectivityPopReturnsTopFrame(t *testing.T) { + t.Parallel() + + visitor := newMeasureSelectivityVisitor(NewSelectivityModel(nil)) + visitor.addSelectivity(7) + visitor.pushSelectivity(11) + visitor.addSelectivity(13) + + require.Equal(t, 24, visitor.popSelectivity()) + require.Equal(t, 7, visitor.Selectivity()) +} + +func TestCollectReferencedSourceIdentifiersIgnoresMatchDeclarations(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (n)-[r:MemberOf]->(m) + RETURN m + `) + require.NoError(t, err) + + references, err := collectReferencedSourceIdentifiers(regularQuery) + require.NoError(t, err) + require.NotContains(t, references, "n") + require.NotContains(t, references, "r") + require.Contains(t, references, "m") +} + func TestLoweringPlanSkipsDirectionlessExpansionSuffixPushdown(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/optimize/selectivity.go b/cypher/models/pgsql/optimize/selectivity.go index c7617bea..1d33aba2 100644 --- a/cypher/models/pgsql/optimize/selectivity.go +++ b/cypher/models/pgsql/optimize/selectivity.go @@ -110,7 +110,7 @@ func (s *measureSelectivityVisitor) Selectivity() int { } func (s *measureSelectivityVisitor) popSelectivity() int { - value := s.Selectivity() + value := s.selectivityStack[len(s.selectivityStack)-1] s.selectivityStack = s.selectivityStack[:len(s.selectivityStack)-1] return value diff --git a/cypher/models/pgsql/optimize/source_references.go b/cypher/models/pgsql/optimize/source_references.go index bb12ab86..01dde537 100644 --- a/cypher/models/pgsql/optimize/source_references.go +++ b/cypher/models/pgsql/optimize/source_references.go @@ -83,7 +83,9 @@ func (s *sourceReferenceCollector) Enter(node cypher.SyntaxNode) { } case *cypher.Variable: - s.addVariable(typedNode) + if s.matchPatternDeclarationDepth == 0 { + s.addVariable(typedNode) + } } } diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index 739dd00b..6f51f513 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -24,13 +24,13 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'value'))::jsonb = to_jsonb((1)::int8)::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 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('me')::text)::jsonb)) select s3.n1 as n1 from s3), s4 as (select s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where (n2.id = (s2.n1).id)) select s4.n2 as b from s4; -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, collect(distinct(n)) as p where size(p) >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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 and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[]))), s3 as (select s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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 and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[]))), s3 as (select s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, count(distinct(n)) as p where p >= 10 return m -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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 and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[]))), s3 as (select s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, count(distinct (s3.n0))::int8 as i0 from s3 group by n2) select s0.n2 as m from s0 where (s0.i0 >= 10); -- case: with 365 as max_days match (n:NodeKind1) where n.pwdlastset < (datetime().epochseconds - (max_days * 86400)) and not n.pwdlastset IN [-1.0, 0.0] return n limit 100 with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (not ((n0.properties ->> 'pwdlastset'))::float8 = any (array [- 1, 0]::float8[]) and ((n0.properties ->> 'pwdlastset'))::numeric < (extract(epoch from now()::timestamp with time zone)::numeric - (s0.i0 * 86400))) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1 limit 100; @@ -81,10 +81,10 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p, case when (s1.n2).id is null or s1.e1 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite end as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.id != all (s1.ep0)) 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 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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 and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id)), s3 as (select s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.id != all (s1.ep0)) 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 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4; -- case: match (m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)-[:EdgeKind2]->(c3:NodeKind1) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and not m.samaccountname contains "DEX" and not g.name IN ["D"] and not m.samaccountname =~ "^.*$" with collect(g.name) as admingroups match p=(m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2) where m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' and g.name in admingroups and not m.samaccountname =~ "^.*$" return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[])), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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 and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[]))), s3 as (select s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5_seed(root_id) as not materialized (select n4.id as root_id from s0, node n4 where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0))), s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s5_seed join edge e2 on e2.end_id = s5_seed.root_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e2.id || s5.path from s5 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.end_id = s5.next_id and e2.id != all (s5.path) and e2.kind_id = any (array [3]::int2[]) offset 0) e2 on true join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4; -- case: match (a:NodeKind2)-[:EdgeKind1]->(g:NodeKind1)-[:EdgeKind2]->(s:NodeKind2) with count(a) as uc where uc > 5 match p = (a)-[:EdgeKind1]->(g)-[:EdgeKind2]->(s) return p with s0 as (with s1 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.e0 as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != s1.e0) select count(s2.n0)::int8 as i0 from s2), s3 as (select e2.id as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, e3.id as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[]) and e3.id != s3.e2) select case when (s4.n3).id is null or s4.e2 is null or (s4.n4).id is null or s4.e3 is null or (s4.n5).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e2]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e3]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4, s4.n5]::nodecomposite[])::pathcomposite end as p from s4; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index bbcba6d8..ed4cb27c 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -45,7 +45,7 @@ with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposi with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1; -- case: match p = (s)-[*..]->(i)-[]->() where id(s) = 1 and i.name = 'n3' return p limit 1 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.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 join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0) limit 1) select case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null or s2.e1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 1; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id)), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0) limit 1) select case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null or s2.e1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 1; -- case: match p = ()-[e:EdgeKind1]->()-[:EdgeKind1*..]->() return e, p with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, case when (s1.n0).id is null or (s1.e0).id is null or (s1.n1).id is null or s1.ep0 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1; @@ -81,13 +81,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p, case when (s1.n2).id is null or s1.e1 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n2, s1.n0]::nodecomposite[])::pathcomposite end as q from s1; -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p -with s0 as (with s1 as (with recursive s2_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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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[] and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id), 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 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.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.id != all (s1.ep0)) 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 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4; +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id union all select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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 and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id)), s3 as (select s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.id != all (s1.ep0)) 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 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 case when (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4]::nodecomposite[])::pathcomposite end 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 case when (s0.n0).id is null or (s0.e0).id is null or (s0.n1).id is null then null else (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite end 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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((((s0.e0)[1]).properties ->> 'enforced'))::bool or ((select count(*)::int from unnest(coalesce((coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).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((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:cardinality(((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[] is not null)::bool); +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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((((s0.e0)[1]).properties ->> 'enforced'))::bool or ((select count(*)::int from unnest(coalesce((coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[]) as i0 where ((i0.kind_ids operator (pg_catalog.@>) array [9]::int2[] and ((i0.properties ->> 'blocksinheritance'))::bool))) = 0 and coalesce((coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[] is not null)::bool); -- case: MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE NONE(x in TAIL(r) WHERE NOT type(x) = 'Contains') 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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((select count(*)::int from unnest(coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:cardinality(s0.e0)::int], array []::edgecomposite[])::edgecomposite[] is not null)::bool); +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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((select count(*)::int from unnest(coalesce((s0.e0)[2:], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:], array []::edgecomposite[])::edgecomposite[] 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 f84e2d77..f1eb5121 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -45,7 +45,7 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select s0.e0 as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; -- case: match (n)-[*..]->(e)-[:EdgeKind1|EdgeKind2]->()-[*..]->(l) where n.name = 'n1' and e.name = 'n2' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb) and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[])), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[]) and e1.id != all (s0.ep0)), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.e1 as e1, s2.ep0 as ep0, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; +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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[]))), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[]) and e1.id != all (s0.ep0)), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.e1 as e1, s2.ep0 as ep0, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; -- case: match p = (:NodeKind1)-[:EdgeKind1*1..]->(n:NodeKind2) where 'admin_tier_0' in split(n.system_tags, ' ') return p limit 1000 with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1000; diff --git a/cypher/models/pgsql/translate/constraints_test.go b/cypher/models/pgsql/translate/constraints_test.go index 94f41619..c54dc0c6 100644 --- a/cypher/models/pgsql/translate/constraints_test.go +++ b/cypher/models/pgsql/translate/constraints_test.go @@ -323,3 +323,34 @@ func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { require.False(t, canExecute) }) } + +func TestCanMaterializeEndpointPairFilterRequiresPairAwareConstraints(t *testing.T) { + leftIdentifier := pgsql.Identifier("n0") + rightIdentifier := pgsql.Identifier("n1") + kindOnlyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, + pgd.IntLiteral(1), + ) + } + propertyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgd.PropertyLookup(identifier, "name"), + pgd.TextLiteral("target"), + ) + } + + step := &TraversalStep{ + LeftNode: &BoundIdentifier{Identifier: leftIdentifier}, + RightNode: &BoundIdentifier{Identifier: rightIdentifier}, + } + + require.False(t, canMaterializeEndpointPairFilterForStep(step, &Expansion{ + PrimerNodeConstraints: kindOnlyConstraint(leftIdentifier), + TerminalNodeConstraints: propertyConstraint(rightIdentifier), + })) + require.True(t, canMaterializeEndpointPairFilterForStep(step, &Expansion{ + PrimerNodeConstraints: propertyConstraint(leftIdentifier), + TerminalNodeConstraints: propertyConstraint(rightIdentifier), + })) +} diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 85b3589e..c5e78360 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -59,6 +59,8 @@ type ExpansionBuilder struct { queryParameters map[string]any traversalStep *TraversalStep model *Expansion + unwindClauses []UnwindClause + unwindSources []pgsql.FromClause } func NewExpansionBuilder(queryParameters map[string]any, traversalStep *TraversalStep) (*ExpansionBuilder, error) { @@ -73,6 +75,11 @@ func NewExpansionBuilder(queryParameters map[string]any, traversalStep *Traversa }, nil } +func (s *ExpansionBuilder) SetUnwindClauses(clauses []UnwindClause) { + s.unwindClauses = clauses + s.unwindSources = unwindFromClauses(clauses) +} + func nextFrontInsert(body pgsql.SetExpression) pgsql.Insert { return pgsql.Insert{ Table: pgsql.TableReference{ @@ -230,6 +237,39 @@ func expressionReferencesUnwindBinding(expression pgsql.Expression, unwindClause return false, nil } +func (s *ExpansionBuilder) seedEndpointConstraintSplit(expression pgsql.Expression, nodeIdentifier pgsql.Identifier, previousFrameIdentifier pgsql.Identifier) (pgsql.Expression, pgsql.Expression) { + seedExpression := rewriteBoundEndpointSeedReference(expression, previousFrameIdentifier, nodeIdentifier) + localScope := pgsql.AsIdentifierSet(nodeIdentifier) + + for _, clause := range s.unwindClauses { + if clause.Binding != nil { + localScope.Add(clause.Binding.Identifier) + } + } + + return partitionConstraintByLocality(seedExpression, localScope) +} + +func (s *ExpansionBuilder) appendUnwindSourcesIfReferenced(selectBody *pgsql.Select, expression pgsql.Expression) error { + if referencesUnwind, err := expressionReferencesUnwindBinding(expression, s.unwindClauses); err != nil { + return err + } else if referencesUnwind { + var previousFrame *Frame + if s.traversalStep != nil && s.traversalStep.Frame != nil { + previousFrame = s.traversalStep.Frame.Previous + } + + selectBody.From = prependFrameSourceIfMissing(selectBody.From, previousFrame) + selectBody.From = append(selectBody.From, s.unwindSources...) + } + + return nil +} + +func (s *ExpansionBuilder) appendUnwindSources(selectBody *pgsql.Select) { + selectBody.From = append(selectBody.From, s.unwindSources...) +} + func newExpansionRootIDsParameterSeed(identifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { return newExpansionNodeFilterSeed(identifier, expansionRootFilter, nodeIdentifier, constraints) } @@ -577,13 +617,6 @@ func rewriteBoundEndpointSeedReference(expression pgsql.Expression, previousFram } } -func seedEndpointConstraintSplit(expression pgsql.Expression, nodeIdentifier pgsql.Identifier, previousFrameIdentifier pgsql.Identifier) (pgsql.Expression, pgsql.Expression) { - // Harness seed fragments only range over the endpoint node alias and an optional ID filter. - // Reframe safe endpoint references first, then leave anything still non-local for the outer projection. - seedExpression := rewriteBoundEndpointSeedReference(expression, previousFrameIdentifier, nodeIdentifier) - return partitionConstraintByLocality(seedExpression, pgsql.AsIdentifierSet(nodeIdentifier)) -} - func seededFrontPrimerQuery(seed expansionSeed, primer pgsql.Select) pgsql.Query { return pgsql.Query{ CommonTableExpressions: &pgsql.With{ @@ -1072,7 +1105,7 @@ func (s *ExpansionBuilder) backwardTerminalSatisfaction(expansionModel *Expansio return satisfiedSelectItem } -func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expansion) (pgsql.Query, pgsql.Expression) { +func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expansion) (pgsql.Query, pgsql.Expression, error) { var ( primerSeedConstraints pgsql.Expression primerProjectionPredicate pgsql.Expression @@ -1087,7 +1120,7 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans previousFrameIdentifier = s.traversalStep.Frame.Previous.Binding.Identifier } - primerSeedConstraints, primerProjectionPredicate = seedEndpointConstraintSplit( + primerSeedConstraints, primerProjectionPredicate = s.seedEndpointConstraintSplit( expansionModel.PrimerNodeConstraints, s.traversalStep.LeftNode.Identifier, previousFrameIdentifier, @@ -1109,6 +1142,12 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans seed = &nodeSeed } + if seed != nil { + if err := s.appendUnwindSourcesIfReferenced(&seed.query, primerSeedConstraints); err != nil { + return pgsql.Query{}, nil, err + } + } + // The returned projection predicate is the part of the endpoint predicate // that cannot be evaluated in the seed CTE because it still references an // outer frame. @@ -1151,6 +1190,9 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans } nextQuery.From = []pgsql.FromClause{nextQueryFrom} + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + return pgsql.Query{}, nil, err + } if !expansionModel.HasExplicitEndpointInequality { nextQuery.Where = pgsql.OptionalAnd( @@ -1159,10 +1201,10 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans ) } - return frontPrimerQuery(seed, nextQuery), primerProjectionPredicate + return frontPrimerQuery(seed, nextQuery), primerProjectionPredicate, nil } -func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Expansion) pgsql.Select { +func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Expansion) (pgsql.Select, error) { nextQuery := pgsql.Select{ Where: expansionModel.EdgeConstraints, } @@ -1242,10 +1284,14 @@ func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Exp } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - return nextQuery + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + return pgsql.Select{}, err + } + + return nextQuery, nil } -func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expansion) (pgsql.Query, pgsql.Expression) { +func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expansion) (pgsql.Query, pgsql.Expression, error) { var ( terminalSeedConstraints pgsql.Expression terminalProjectionPredicate pgsql.Expression @@ -1260,7 +1306,7 @@ func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expan previousFrameIdentifier = s.traversalStep.Frame.Previous.Binding.Identifier } - terminalSeedConstraints, terminalProjectionPredicate = seedEndpointConstraintSplit( + terminalSeedConstraints, terminalProjectionPredicate = s.seedEndpointConstraintSplit( expansionModel.TerminalNodeConstraints, s.traversalStep.RightNode.Identifier, previousFrameIdentifier, @@ -1282,6 +1328,12 @@ func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expan seed = &nodeSeed } + if seed != nil { + if err := s.appendUnwindSourcesIfReferenced(&seed.query, terminalSeedConstraints); err != nil { + return pgsql.Query{}, nil, err + } + } + // The returned projection predicate is applied after the harness materializes // endpoints, where any outer-frame references are back in scope. nextQuery.Projection = []pgsql.SelectItem{ @@ -1321,10 +1373,14 @@ func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expan } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - return frontPrimerQuery(seed, nextQuery), terminalProjectionPredicate + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + return pgsql.Query{}, nil, err + } + + return frontPrimerQuery(seed, nextQuery), terminalProjectionPredicate, nil } -func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Expansion) pgsql.Select { +func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Expansion) (pgsql.Select, error) { nextQuery := pgsql.Select{ Where: expansionModel.EdgeConstraints, } @@ -1389,7 +1445,11 @@ func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Ex } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - return nextQuery + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + return pgsql.Select{}, err + } + + return nextQuery, nil } func shortestPathSearchCTE(functionName pgsql.Identifier, expansionModel *Expansion, harnessParameters []pgsql.Expression) pgsql.CommonTableExpression { @@ -1651,10 +1711,15 @@ func (s *ExpansionBuilder) buildShortestPathsHarnessCall(harnessFunctionName pgs expansionModel.UseMaterializedTerminalFilter = s.canMaterializeTerminalFilter(expansionModel) - var ( - forwardFrontPrimerQuery, forwardSeedProjectionConstraints = s.prepareForwardFrontPrimerQuery(expansionModel) - forwardFrontRecursiveQuery = s.prepareForwardFrontRecursiveQuery(expansionModel) - ) + forwardFrontPrimerQuery, forwardSeedProjectionConstraints, err := s.prepareForwardFrontPrimerQuery(expansionModel) + if err != nil { + return pgsql.Query{}, err + } + + forwardFrontRecursiveQuery, err := s.prepareForwardFrontRecursiveQuery(expansionModel) + if err != nil { + return pgsql.Query{}, err + } projectionQuery.Projection = expansionModel.Projection @@ -1689,6 +1754,7 @@ func (s *ExpansionBuilder) buildShortestPathsHarnessCall(harnessFunctionName pgs s.applyBoundEndpointProjectionConstraints(&projectionQuery, expansionModel) s.applyShortestPathSeedProjectionConstraints(&projectionQuery, forwardSeedProjectionConstraints) + s.appendUnwindSources(&projectionQuery) s.applyShortestPathSelfEndpointGuard(&projectionQuery, expansionModel) if harnessParameters, err := s.shortestPathsParameters(expansionModel, forwardFrontPrimerQuery, forwardFrontRecursiveQuery); err != nil { @@ -1728,12 +1794,25 @@ func (s *ExpansionBuilder) buildBiDirectionalShortestPathsHarnessCall(harnessFun expansionModel.UseMaterializedEndpointPairFilter = s.canMaterializeEndpointPairFilter(expansionModel) - var ( - forwardFrontPrimerQuery, forwardSeedProjectionConstraints = s.prepareForwardFrontPrimerQuery(expansionModel) - forwardFrontRecursiveQuery = s.prepareForwardFrontRecursiveQuery(expansionModel) - backwardFrontPrimerQuery, backwardSeedProjectionConstraints = s.prepareBackwardFrontPrimerQuery(expansionModel) - backwardFrontRecursiveQuery = s.prepareBackwardFrontRecursiveQuery(expansionModel) - ) + forwardFrontPrimerQuery, forwardSeedProjectionConstraints, err := s.prepareForwardFrontPrimerQuery(expansionModel) + if err != nil { + return pgsql.Query{}, err + } + + forwardFrontRecursiveQuery, err := s.prepareForwardFrontRecursiveQuery(expansionModel) + if err != nil { + return pgsql.Query{}, err + } + + backwardFrontPrimerQuery, backwardSeedProjectionConstraints, err := s.prepareBackwardFrontPrimerQuery(expansionModel) + if err != nil { + return pgsql.Query{}, err + } + + backwardFrontRecursiveQuery, err := s.prepareBackwardFrontRecursiveQuery(expansionModel) + if err != nil { + return pgsql.Query{}, err + } projectionQuery.Projection = expansionModel.Projection @@ -1768,6 +1847,7 @@ func (s *ExpansionBuilder) buildBiDirectionalShortestPathsHarnessCall(harnessFun s.applyBoundEndpointProjectionConstraints(&projectionQuery, expansionModel) s.applyShortestPathSeedProjectionConstraints(&projectionQuery, pgsql.OptionalAnd(forwardSeedProjectionConstraints, backwardSeedProjectionConstraints)) + s.appendUnwindSources(&projectionQuery) s.applyShortestPathSelfEndpointGuard(&projectionQuery, expansionModel) if harnessParameters, err := s.bidirectionalAllShortestPathsParameters(expansionModel, forwardFrontPrimerQuery, forwardFrontRecursiveQuery, backwardFrontPrimerQuery, backwardFrontRecursiveQuery); err != nil { diff --git a/cypher/models/pgsql/translate/function.go b/cypher/models/pgsql/translate/function.go index 1d614f88..12346ed8 100644 --- a/cypher/models/pgsql/translate/function.go +++ b/cypher/models/pgsql/translate/function.go @@ -415,14 +415,7 @@ func (s *Translator) translateTailFunction(functionInvocation *cypher.FunctionIn &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, + CastType: arrayType, }, pgsql.ArrayLiteral{ CastType: arrayType, diff --git a/cypher/models/pgsql/translate/function_test.go b/cypher/models/pgsql/translate/function_test.go index fbb517bd..793ea9e1 100644 --- a/cypher/models/pgsql/translate/function_test.go +++ b/cypher/models/pgsql/translate/function_test.go @@ -2,6 +2,7 @@ package translate import ( "context" + "strings" "testing" "github.com/specterops/dawgs/cypher/frontend" @@ -56,6 +57,21 @@ func TestPathComponentFunctionsTranslateNullArguments(t *testing.T) { require.Contains(t, formatted, "(null)::edgecomposite[]") } +func TestTailFunctionDoesNotDuplicatePathComponentExpression(t *testing.T) { + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), `MATCH p = ()-[*1..]->() RETURN tail(tail(nodes(p)))`) + 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.Equal(t, 1, strings.Count(formatted, "ordered_edges_to_path")) + require.NotContains(t, formatted, "cardinality(((case when") +} + func TestPrepareCollectExpressionMissingBindingErrorNamesArgument(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index be95bb51..a4090ff9 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -177,8 +177,8 @@ func canMaterializeEndpointPairFilterForStep(traversalStep *TraversalStep, expan traversalStep.usesBoundEndpointPairs() || expansionModel.PrimerNodeConstraints == nil || expansionModel.TerminalNodeConstraints == nil || - !hasLocalEndpointConstraint(expansionModel.PrimerNodeConstraints, traversalStep.LeftNode.Identifier) || - !hasLocalEndpointConstraint(expansionModel.TerminalNodeConstraints, traversalStep.RightNode.Identifier) { + !hasPairAwareEndpointConstraint(expansionModel.PrimerNodeConstraints, traversalStep.LeftNode.Identifier) || + !hasPairAwareEndpointConstraint(expansionModel.TerminalNodeConstraints, traversalStep.RightNode.Identifier) { return false } diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index d8c9d1ea..50375d2a 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -405,6 +405,28 @@ LIMIT 1 requireOptimizationLowering(t, translation.Optimization, "LimitPushdown") } +func TestOptimizerSafetyShortestPathRootCarriesUnwindSources(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` + UNWIND ['source'] AS sourceName + MATCH p = shortestPath((s:Group)-[:MemberOf*1..]->(e:Group)) + WHERE s.name = sourceName AND e.name = 'target' + RETURN sourceName, p + `) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + primerQuery, hasPrimerQuery := translation.Parameters["pi0"].(string) + + require.True(t, hasPrimerQuery) + require.Contains(t, normalizedQuery, "unidirectional_sp_harness") + require.Contains(t, normalizedQuery, "unnest(array ['source']::text[]) as i0") + require.Contains(t, primerQuery, "unnest(array ['source']::text[]) as i0") + require.Contains(t, primerQuery, "(n0.properties ->> 'name') = i0") +} + func TestOptimizerSafetyTranslationReportsOptimizerMetadata(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/pattern.go b/cypher/models/pgsql/translate/pattern.go index 163dc8d1..a77d03ce 100644 --- a/cypher/models/pgsql/translate/pattern.go +++ b/cypher/models/pgsql/translate/pattern.go @@ -135,6 +135,8 @@ func (s *Translator) buildShortestPathsExpansionPattern(traversalStepContext Tra traversalStep := traversalStepContext.CurrentStep if traversalStepContext.IsRootStep { + expansion.SetUnwindClauses(s.query.CurrentPart().ConsumeUnwindClauses()) + if allPaths { if traversalStep.Expansion.UseBidirectionalSearch { if traversalStepQuery, err := expansion.BuildBiDirectionalAllShortestPathsRoot(); err != nil { diff --git a/cypher/models/pgsql/translate/predicate.go b/cypher/models/pgsql/translate/predicate.go index 54ed79ec..b34e52e0 100644 --- a/cypher/models/pgsql/translate/predicate.go +++ b/cypher/models/pgsql/translate/predicate.go @@ -145,7 +145,7 @@ func (s *Translator) buildPatternPredicates() error { s.recordLowering(optimize.LoweringPredicatePlacement) } - return nil + continue } } diff --git a/cypher/models/pgsql/translate/predicate_test.go b/cypher/models/pgsql/translate/predicate_test.go index c94fb85e..8c2fe76f 100644 --- a/cypher/models/pgsql/translate/predicate_test.go +++ b/cypher/models/pgsql/translate/predicate_test.go @@ -34,6 +34,30 @@ RETURN p`) require.NotContains(t, formatted, "as p from s1 where") } +func TestOptimizedPatternPredicatesContinueAfterFirstPlacement(t *testing.T) { + kindMapper := pgutil.NewInMemoryKindMapper() + kindMapper.Put(graph.StringKind("Domain")) + kindMapper.Put(graph.StringKind("SpoofSIDHistory")) + kindMapper.Put(graph.StringKind("AbuseTGTDelegation")) + + query, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (n:Domain), (m:Domain) + WHERE (n)-[:SpoofSIDHistory]-(m) + AND (n)-[:AbuseTGTDelegation]-(m) + RETURN n + `) + 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, "array [2]::int2[]") + require.Contains(t, formatted, "array [3]::int2[]") +} + func translatePredicateQuery(t *testing.T, cypherQuery string, parameters map[string]any) string { t.Helper() diff --git a/cypher/models/pgsql/translate/tracking.go b/cypher/models/pgsql/translate/tracking.go index 8bb328a0..8d325cfe 100644 --- a/cypher/models/pgsql/translate/tracking.go +++ b/cypher/models/pgsql/translate/tracking.go @@ -345,11 +345,15 @@ func (s *Scope) LookupString(identifierString string) (*BoundIdentifier, bool) { } func (s *Scope) LookupDataType(identifier pgsql.Identifier) (pgsql.DataType, bool) { - if binding, bound := s.Lookup(identifier); !bound { - return "", false - } else { + if binding, bound := s.Lookup(identifier); bound { return binding.DataType, true } + + if binding, bound := s.AliasedLookup(identifier); bound { + return binding.DataType, true + } + + return "", false } func (s *Scope) Define(identifier pgsql.Identifier, dataType pgsql.DataType) *BoundIdentifier { diff --git a/cypher/models/pgsql/translate/tracking_test.go b/cypher/models/pgsql/translate/tracking_test.go index 3ea6c67c..cee793fe 100644 --- a/cypher/models/pgsql/translate/tracking_test.go +++ b/cypher/models/pgsql/translate/tracking_test.go @@ -3,6 +3,7 @@ package translate import ( "testing" + "github.com/specterops/dawgs/cypher/models/pgsql" "github.com/stretchr/testify/require" ) @@ -27,3 +28,14 @@ func TestScope(t *testing.T) { require.Nil(t, scope.UnwindToFrame(parent)) require.Equal(t, parent.id, scope.CurrentFrame().id) } + +func TestScopeLookupDataTypeResolvesAliases(t *testing.T) { + scope := NewScope() + binding := scope.Define(pgsql.Identifier("n0"), pgsql.NodeComposite) + scope.Alias(pgsql.Identifier("n"), binding) + + dataType, found := scope.LookupDataType(pgsql.Identifier("n")) + + require.True(t, found) + require.Equal(t, pgsql.NodeComposite, dataType) +} diff --git a/integration/harness.go b/integration/harness.go index 88111efc..8853d128 100644 --- a/integration/harness.go +++ b/integration/harness.go @@ -90,7 +90,7 @@ func setupDB(t *testing.T, cleanupGraph bool, extraNodeKinds, extraEdgeKinds gra connStr := os.Getenv("CONNECTION_STRING") if connStr == "" { - t.Fatal("CONNECTION_STRING env var is not set") + t.Skip("CONNECTION_STRING env var is not set") } driver, err := driverFromConnStr(connStr) From ebfcbe5a862510731b771caa18dfc96e9c33e153 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 11:27:36 -0700 Subject: [PATCH 047/114] fix(pgsql): stage path nodes in tail predicates --- .../translation_cases/pattern_binding.sql | 2 +- cypher/models/pgsql/translate/function.go | 32 ++++--- .../models/pgsql/translate/function_test.go | 18 +++- .../models/pgsql/translate/path_functions.go | 49 ++++++++++- cypher/models/pgsql/translate/projection.go | 86 +++++++++++++++++++ 5 files changed, 167 insertions(+), 20 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index ed4cb27c..622e2a29 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -87,7 +87,7 @@ with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (sel 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 case when (s0.n0).id is null or (s0.e0).id is null or (s0.n1).id is null then null else (array [s0.n0, s0.n1]::nodecomposite[], array [s0.e0]::edgecomposite[])::pathcomposite end 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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((((s0.e0)[1]).properties ->> 'enforced'))::bool or ((select count(*)::int from unnest(coalesce((coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[]) as i0 where ((i0.kind_ids operator (pg_catalog.@>) array [9]::int2[] and ((i0.properties ->> 'blocksinheritance'))::bool))) = 0 and coalesce((coalesce((((case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), 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 end).nodes)::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[] is not null)::bool); +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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 s2.pc0 as p from s0, lateral (select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as pc0 offset 0) s2 where (((((s0.e0)[1]).properties ->> 'enforced'))::bool or ((select count(*)::int from unnest(coalesce((coalesce((((s2.pc0).nodes)::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[]) as i0 where ((i0.kind_ids operator (pg_catalog.@>) array [9]::int2[] and ((i0.properties ->> 'blocksinheritance'))::bool))) = 0 and coalesce((coalesce((((s2.pc0).nodes)::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[])[2:], array []::nodecomposite[])::nodecomposite[] is not null)::bool); -- case: MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE NONE(x in TAIL(r) WHERE NOT type(x) = 'Contains') 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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((select count(*)::int from unnest(coalesce((s0.e0)[2:], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:], array []::edgecomposite[])::edgecomposite[] is not null)::bool); diff --git a/cypher/models/pgsql/translate/function.go b/cypher/models/pgsql/translate/function.go index 12346ed8..f0eae7f8 100644 --- a/cypher/models/pgsql/translate/function.go +++ b/cypher/models/pgsql/translate/function.go @@ -465,25 +465,23 @@ func (s *Translator) translatePathComponentFunction(functionInvocation *cypher.F } else if literal, isLiteral := argument.(pgsql.Literal); isLiteral && literal.Null { s.treeTranslator.PushOperand(pgsql.NewTypeCast(literal, castType)) } else { - if column == pgsql.ColumnEdges { - if identifier, isIdentifier := unwrapParenthetical(argument).(pgsql.Identifier); isIdentifier { - binding, bound := s.scope.Lookup(identifier) - if !bound { - binding, bound = s.scope.AliasedLookup(identifier) - } - - if !bound { - return fmt.Errorf("unable to resolve path identifier %s", identifier) - } else if binding.DataType != pgsql.PathComposite { - return fmt.Errorf("expected path expression but received %s", binding.DataType) - } + if identifier, isIdentifier := unwrapParenthetical(argument).(pgsql.Identifier); isIdentifier { + binding, bound := s.scope.Lookup(identifier) + if !bound { + binding, bound = s.scope.AliasedLookup(identifier) + } - s.treeTranslator.PushOperand(pgsql.NewTypeCast(pgsql.RowColumnReference{ - Identifier: argument, - Column: column, - }, castType)) - return nil + if !bound { + return fmt.Errorf("unable to resolve path identifier %s", identifier) + } else if binding.DataType != pgsql.PathComposite { + return fmt.Errorf("expected path expression but received %s", binding.DataType) } + + s.treeTranslator.PushOperand(pgsql.NewTypeCast(pgsql.RowColumnReference{ + Identifier: identifier, + Column: column, + }, castType)) + return nil } if pathExpression, err := s.expressionForPath(argument); err != nil { diff --git a/cypher/models/pgsql/translate/function_test.go b/cypher/models/pgsql/translate/function_test.go index 793ea9e1..e8f421bd 100644 --- a/cypher/models/pgsql/translate/function_test.go +++ b/cypher/models/pgsql/translate/function_test.go @@ -68,10 +68,26 @@ func TestTailFunctionDoesNotDuplicatePathComponentExpression(t *testing.T) { formatted, err := Translated(translation) require.NoError(t, err) - require.Equal(t, 1, strings.Count(formatted, "ordered_edges_to_path")) + require.Equal(t, 1, strings.Count(formatted, "ordered_edges_to_path"), formatted) require.NotContains(t, formatted, "cardinality(((case when") } +func TestTailPredicateStagesPathComponentExpression(t *testing.T) { + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), `MATCH p = ()-[*1..]->() WHERE NONE(n IN TAIL(TAIL(NODES(p))) WHERE true) RETURN p`) + 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.Equal(t, 1, strings.Count(formatted, "ordered_edges_to_path")) + require.Contains(t, formatted, "lateral (select") + require.Contains(t, formatted, ".nodes") +} + func TestPrepareCollectExpressionMissingBindingErrorNamesArgument(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/path_functions.go b/cypher/models/pgsql/translate/path_functions.go index 4b2951fc..516f3022 100644 --- a/cypher/models/pgsql/translate/path_functions.go +++ b/cypher/models/pgsql/translate/path_functions.go @@ -41,7 +41,7 @@ func pathCompositeEdgesExpression(scope *Scope, pathBinding *BoundIdentifier) (p } func resolvePathCompositeFieldReference(scope *Scope, reference pgsql.RowColumnReference) (pgsql.Expression, bool, error) { - identifier, isIdentifier := reference.Identifier.(pgsql.Identifier) + identifier, isIdentifier := unwrapParenthetical(reference.Identifier).(pgsql.Identifier) if !isIdentifier { return nil, false, nil } @@ -65,6 +65,15 @@ func resolvePathCompositeFieldReference(scope *Scope, reference pgsql.RowColumnR case pgsql.ColumnEdges: expression, err := pathCompositeEdgesExpression(scope, binding) return expression, true, err + case pgsql.ColumnNodes: + if expression, err := expressionForPathComposite(binding, scope); err != nil { + return nil, false, err + } else { + return pgsql.RowColumnReference{ + Identifier: expression, + Column: reference.Column, + }, true, nil + } default: return nil, false, fmt.Errorf("unsupported path composite field reference: %s", reference.Column) } @@ -235,6 +244,44 @@ func resolvePathCompositeFieldReferences(scope *Scope, expression pgsql.Expressi return typedExpression, nil } + case pgsql.ArraySlice: + if resolved, err := resolvePathCompositeFieldReferences(scope, typedExpression.Expression); err != nil { + return nil, err + } else { + typedExpression.Expression = resolved + } + + if typedExpression.Lower != nil { + if resolved, err := resolvePathCompositeFieldReferences(scope, typedExpression.Lower); err != nil { + return nil, err + } else { + typedExpression.Lower = resolved + } + } + + if typedExpression.Upper != nil { + if resolved, err := resolvePathCompositeFieldReferences(scope, typedExpression.Upper); err != nil { + return nil, err + } else { + typedExpression.Upper = resolved + } + } + + return typedExpression, nil + + case *pgsql.ArraySlice: + if typedExpression == nil { + return nil, nil + } + + resolved, err := resolvePathCompositeFieldReferences(scope, *typedExpression) + if err != nil { + return nil, err + } + + arraySlice := resolved.(pgsql.ArraySlice) + return &arraySlice, nil + case pgsql.ArrayLiteral: for idx, value := range typedExpression.Values { if resolved, err := resolvePathCompositeFieldReferences(scope, value); err != nil { diff --git a/cypher/models/pgsql/translate/projection.go b/cypher/models/pgsql/translate/projection.go index a4f4e64c..18dabaee 100644 --- a/cypher/models/pgsql/translate/projection.go +++ b/cypher/models/pgsql/translate/projection.go @@ -1093,6 +1093,87 @@ func rewriteOrderByProjectionAlias(orderBy *pgsql.OrderBy, aliases map[pgsql.Ide } } +func tailPathCompositeStageBindings(scope *Scope, expression pgsql.Expression) ([]*BoundIdentifier, error) { + if expression == nil { + return nil, nil + } + + var ( + bindings = make([]*BoundIdentifier, 0) + seen = map[pgsql.Identifier]struct{}{} + ) + + if err := walk.PgSQL(expression, walk.NewSimpleVisitor[pgsql.SyntaxNode](func(node pgsql.SyntaxNode, _ walk.VisitorHandler) { + reference, isRowColumnReference := node.(pgsql.RowColumnReference) + if !isRowColumnReference || reference.Column != pgsql.ColumnNodes { + return + } + + identifier, isIdentifier := unwrapParenthetical(reference.Identifier).(pgsql.Identifier) + if !isIdentifier { + return + } + + binding, bound := scope.Lookup(identifier) + if !bound { + binding, bound = scope.AliasedLookup(identifier) + } + if !bound || binding.DataType != pgsql.PathComposite || binding.LastProjection != nil { + return + } + + if _, alreadySeen := seen[binding.Identifier]; alreadySeen { + return + } + + seen[binding.Identifier] = struct{}{} + bindings = append(bindings, binding) + })); err != nil { + return nil, err + } + + return bindings, nil +} + +func (s *Translator) stageTailPathCompositeBindings(fromClauses []pgsql.FromClause, bindings []*BoundIdentifier) ([]pgsql.FromClause, error) { + for _, binding := range bindings { + stageBinding, err := s.scope.DefineNew(pgsql.Scope) + if err != nil { + return nil, err + } + + stageFrame := &Frame{ + Binding: stageBinding, + Visible: pgsql.AsIdentifierSet(binding.Identifier), + Exported: pgsql.AsIdentifierSet(binding.Identifier), + stashedVisible: pgsql.NewIdentifierSet(), + stashedExported: pgsql.NewIdentifierSet(), + Synthetic: true, + } + + stageProjection, err := buildProjection(binding.Identifier, binding, s.scope, binding.LastProjection) + if err != nil { + return nil, err + } + + fromClauses = append(fromClauses, pgsql.FromClause{ + Source: pgsql.LateralSubquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: stageProjection, + }, + Offset: pgsql.NewLiteral(0, pgsql.Int), + }, + Binding: models.OptionalValue(stageBinding.Identifier), + }, + }) + + binding.MaterializedBy(stageFrame) + } + + return fromClauses, nil +} + func (s *Translator) buildTailProjection() error { var ( currentPart = s.query.CurrentPart() @@ -1106,6 +1187,10 @@ func (s *Translator) buildTailProjection() error { if projectionConstraint, err := s.treeTranslator.ConsumeAllConstraints(); err != nil { return err + } else if stagedBindings, err := tailPathCompositeStageBindings(s.scope, projectionConstraint.Expression); err != nil { + return err + } else if stagedFromClauses, err := s.stageTailPathCompositeBindings(singlePartQuerySelect.From, stagedBindings); err != nil { + return err } else if projection, err := buildExternalProjection(s.scope, currentPart.projections.Items); err != nil { return err } else if resolvedConstraint, err := resolvePathCompositeFieldReferences(s.scope, projectionConstraint.Expression); err != nil { @@ -1113,6 +1198,7 @@ func (s *Translator) buildTailProjection() error { } else if err := RewriteFrameBindings(s.scope, resolvedConstraint); err != nil { return err } else { + singlePartQuerySelect.From = stagedFromClauses singlePartQuerySelect.Projection = projection singlePartQuerySelect.Where = resolvedConstraint From 9c5c4c61eff123ebc99c3babf224e38fb7cf822d Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 14:40:17 -0700 Subject: [PATCH 048/114] test(integration): validate ADCS optimizer fanout rewrite --- .../models/pgsql/optimize/optimizer_test.go | 87 +++++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 26 +++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 227e0244..be0a42f1 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -48,6 +48,93 @@ func TestOptimizeCopiesAndAnalyzesQuery(t *testing.T) { require.Len(t, plan.PredicateAttachments, 2) } +func TestOptimizePlansADCSFanoutRewrite(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), adcsQuery) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + + ctPredicate := PredicateAttachment{ + QueryPartIndex: 0, + RegionIndex: 0, + ClauseIndex: 2, + ExpressionIndex: 0, + Scope: PredicateAttachmentScopeBinding, + BindingSymbols: []string{"ct"}, + Dependencies: []string{"ct"}, + } + + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringExpansionSuffixPushdown}) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringPredicatePlacement}) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringExpandIntoDetection}) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringLatePathMaterialization}) + + require.Contains(t, plan.LoweringPlan.ExpansionSuffixPushdown, ExpansionSuffixPushdownDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + SuffixLength: 3, + SuffixStartStep: 1, + SuffixEndStep: 3, + }) + require.Contains(t, plan.LoweringPlan.ExpansionSuffixPushdown, ExpansionSuffixPushdownDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 2, + PatternIndex: 0, + StepIndex: 0, + }, + SuffixLength: 2, + SuffixStartStep: 1, + SuffixEndStep: 2, + PredicateAttachments: []PredicateAttachment{ctPredicate}, + }) + require.Contains(t, plan.LoweringPlan.ExpansionSuffixPushdown, ExpansionSuffixPushdownDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 2, + PatternIndex: 0, + StepIndex: 3, + }, + SuffixLength: 1, + SuffixStartStep: 4, + SuffixEndStep: 4, + }) + + require.Contains(t, plan.LoweringPlan.ExpandInto, ExpandIntoDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 2, + PatternIndex: 0, + StepIndex: 2, + }, + }) + require.Contains(t, plan.LoweringPlan.ExpandInto, ExpandIntoDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 2, + PatternIndex: 0, + StepIndex: 4, + }, + }) + require.Contains(t, plan.LoweringPlan.PredicatePlacement, PredicatePlacementDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 2, + PatternIndex: 0, + StepIndex: 1, + }, + Attachment: ctPredicate, + Placement: PredicateAttachmentScopeBinding, + }) +} + func TestOptimizerRunsRulesAndRefreshesAnalysis(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 50375d2a..3ceb4824 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -127,15 +127,39 @@ func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { t.Parallel() - normalizedQuery := optimizerSafetySQL(t, optimizerADCSQuery) + translation := optimizerSafetyTranslation(t, optimizerADCSQuery) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + requirePlannedOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") + requirePlannedOptimizationLowering(t, translation.Optimization, "PredicatePlacement") + requirePlannedOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") + requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") + requireOptimizationLowering(t, translation.Optimization, "PredicatePlacement") + requireOptimizationLowering(t, translation.Optimization, "ExpandIntoDetection") + + require.Contains(t, normalizedQuery, "select distinct (s0.n0).id as root_id from s0") require.Contains(t, normalizedQuery, "select distinct (s5.n0).id as root_id from s5") + require.Contains(t, normalizedQuery, "select distinct (s9.n2).id as root_id from s9") require.Contains(t, normalizedQuery, "s5.ep0 as ep0") require.NotContains(t, normalizedQuery, "s5.e0 as e0") require.Contains(t, normalizedQuery, "from unnest(s12.ep0)") require.Contains(t, normalizedQuery, "from unnest(array [s12.e1]::int8[])") require.NotContains(t, normalizedQuery, "array [s12.e1]::edgecomposite[]") require.Contains(t, normalizedQuery, "from s5, s7") + requireSQLContainsInOrder(t, normalizedQuery, + "where s7.satisfied and exists (select 1 from edge e5 join node n6", + "properties -> 'authenticationenabled'", + "join edge e6 on n6.id = e6.start_id", + "e6.end_id = (s5.n2).id", + "and (s5.n0).id = s7.root_id", + ) + requireSQLContainsInOrder(t, normalizedQuery, + "where s11.satisfied and (s9.n2).id = s11.root_id and exists", + "from edge e8 where n7.id = e8.start_id", + "e8.end_id = (s9.n4).id", + ) } func assertOptimizerSafetyRelationshipStaysComposite(t *testing.T, cypherQuery string) { From 5bcd7a066b1c72207c430fde782310b14756c27e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 16:04:23 -0700 Subject: [PATCH 049/114] feat(plancorpus): add Cypher plan corpus capture tooling --- Makefile | 7 +- README.md | 4 + cmd/plancorpus/README.md | 26 + cmd/plancorpus/capture.go | 451 ++++++++++++++++++ cmd/plancorpus/corpus.go | 222 +++++++++ cmd/plancorpus/corpus_test.go | 34 ++ cmd/plancorpus/main.go | 182 +++++++ cmd/plancorpus/main_test.go | 41 ++ cmd/plancorpus/report.go | 279 +++++++++++ cmd/plancorpus/report_test.go | 88 ++++ cmd/plancorpus/types.go | 36 ++ .../pgsql/optimize/OPTIMIZATION_PLAN.md | 74 +++ 12 files changed, 1443 insertions(+), 1 deletion(-) create mode 100644 cmd/plancorpus/README.md create mode 100644 cmd/plancorpus/capture.go create mode 100644 cmd/plancorpus/corpus.go create mode 100644 cmd/plancorpus/corpus_test.go create mode 100644 cmd/plancorpus/main.go create mode 100644 cmd/plancorpus/main_test.go create mode 100644 cmd/plancorpus/report.go create mode 100644 cmd/plancorpus/report_test.go create mode 100644 cmd/plancorpus/types.go create mode 100644 cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md diff --git a/Makefile b/Makefile index 81dd8363..cc85ca83 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ QUALITY_INPUTS += -mutation-report $(MUTATION_REPORT) endif QUALITY_INPUTS += -benchmark-regression $(BENCHMARK_REGRESSION) -.PHONY: default all build deps tidy lint format test test_all test_integration test_neo4j test_pg test_update complexity complexity_check crap crap_check quality quality_check quality_backend quality_bench metrics metrics_check generate clean help +.PHONY: default all build deps tidy lint format test test_all test_integration test_neo4j test_pg test_update plan_corpus complexity complexity_check crap crap_check quality quality_check quality_backend quality_bench metrics metrics_check generate clean help # Default target default: help @@ -109,6 +109,10 @@ test_update: @cp -fv cypher/models/pgsql/test/updated_cases/* cypher/models/pgsql/test/translation_cases @rm -rf cypher/models/pgsql/test/updated_cases +plan_corpus: $(METRICS_DIR) + @echo "Capturing Cypher plan corpus..." + @$(GO_CMD) run ./cmd/plancorpus + # Metric targets $(METRICS_DIR): @mkdir -p $(METRICS_DIR) @@ -218,6 +222,7 @@ help: @echo " test_bench - Run benchmark test" @echo " test_neo4j - Run Neo4j integration tests" @echo " test_pg - Run PostgreSQL integration tests" + @echo " plan_corpus - Capture shared corpus query plans for configured backends" @echo " test_update - Update test cases" @echo " complexity - Report cyclomatic complexity" @echo " crap - Report CRAP scores from unit test coverage" diff --git a/README.md b/README.md index e1aad7bb..c37f58d6 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ make quality FUZZ_REPORT=.coverage/fuzz.json MUTATION_REPORT=.coverage/mutation. `PG_CONNECTION_STRING` and `NEO4J_CONNECTION_STRING`. `make quality_bench` writes benchmark markdown and JSON captures for later baseline comparison. +`make plan_corpus` captures plan diagnostics for the shared Cypher integration corpus. It accepts either +`CONNECTION_STRING` for one backend or `PG_CONNECTION_STRING` and `NEO4J_CONNECTION_STRING` for both backends, then +writes JSONL captures and markdown/JSON summaries under `.coverage/`. + Thresholds are report-only by default. To enforce the configured thresholds, run: ```bash diff --git a/cmd/plancorpus/README.md b/cmd/plancorpus/README.md new file mode 100644 index 00000000..3e49de85 --- /dev/null +++ b/cmd/plancorpus/README.md @@ -0,0 +1,26 @@ +# Plan Corpus Capture + +`plancorpus` captures query-plan diagnostics for the shared integration corpus. + +It reads `integration/testdata/cases` and `integration/testdata/templates`, loads the same datasets and inline fixtures used by the integration tests, and writes backend-specific JSONL plan records plus markdown and JSON summaries. + +## Usage + +```bash +PG_CONNECTION_STRING="postgres://postgres:password@localhost/db" \ +NEO4J_CONNECTION_STRING="neo4j://neo4j:password@localhost:7687" \ +go run ./cmd/plancorpus +``` + +Useful flags: + +| Flag | Default | Description | +| --- | --- | --- | +| `-dataset-dir` | `integration/testdata` | Integration corpus root | +| `-output-dir` | `.coverage` | Output directory | +| `-connection` | `CONNECTION_STRING` | Capture one backend selected by URL scheme | +| `-pg-connection` | `PG_CONNECTION_STRING` | PostgreSQL backend | +| `-neo4j-connection` | `NEO4J_CONNECTION_STRING` | Neo4j backend | +| `-summary` | `.coverage/plan-corpus-summary.md` | Markdown summary | +| `-summary-json` | `.coverage/plan-corpus-summary.json` | JSON summary | +| `-top` | `25` | Number of expensive PostgreSQL plans to include in summaries | diff --git a/cmd/plancorpus/capture.go b/cmd/plancorpus/capture.go new file mode 100644 index 00000000..6b4e264e --- /dev/null +++ b/cmd/plancorpus/capture.go @@ -0,0 +1,451 @@ +package main + +import ( + "context" + "fmt" + "net/url" + "os" + "path/filepath" + "sort" + "strings" + + neo4jcore "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/specterops/dawgs" + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" + "github.com/specterops/dawgs/cypher/models/pgsql/translate" + "github.com/specterops/dawgs/drivers" + "github.com/specterops/dawgs/drivers/neo4j" + "github.com/specterops/dawgs/drivers/pg" + "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/opengraph" + "github.com/specterops/dawgs/util/size" +) + +const defaultGraphName = "integration_test" + +type captureSpec struct { + DriverName string + Connection string +} + +type backendCapture struct { + spec captureSpec + db graph.Database + pgDriver *pg.Driver + pgGraphID int32 + neo4jDriver neo4jcore.Driver +} + +func driverFromConnectionString(connStr string) (string, error) { + u, err := url.Parse(connStr) + if err != nil { + return "", fmt.Errorf("parse connection string: %w", err) + } + + switch u.Scheme { + case "postgres", "postgresql": + return pg.DriverName, nil + case neo4j.DriverName, "neo4j+s", "neo4j+ssc": + return neo4j.DriverName, nil + default: + return "", fmt.Errorf("unknown connection string scheme %q", u.Scheme) + } +} + +func captureCorpus(ctx context.Context, datasetDir string, suite corpus, spec captureSpec) ([]PlanRecord, error) { + backend, err := openBackend(ctx, suite, spec) + if err != nil { + return nil, err + } + defer backend.close(ctx) + + var records []PlanRecord + for _, datasetName := range suite.datasetNames { + group := suite.caseGroups[datasetName] + if group == nil { + continue + } + + datasetLoaded := false + ensureDatasetLoaded := func() error { + if datasetLoaded { + return nil + } + if err := clearGraph(ctx, backend.db); err != nil { + return err + } + if err := loadDataset(ctx, backend.db, datasetDir, datasetName); err != nil { + return err + } + datasetLoaded = true + return nil + } + + for _, file := range group.files { + for _, testCase := range file.Cases { + if testCase.Fixture == nil { + if err := ensureDatasetLoaded(); err != nil { + return nil, err + } + } else { + if err := loadCommittedFixture(ctx, backend.db, testCase.Fixture); err != nil { + return nil, err + } + datasetLoaded = false + } + + record := backend.capture(ctx, CorpusQuery{ + Source: file.path, + Dataset: datasetName, + Name: testCase.Name, + Cypher: testCase.Cypher, + Params: testCase.Params, + }) + records = append(records, record) + } + } + } + + for _, file := range suite.templateFiles { + fileName := strings.TrimSuffix(filepath.Base(file.path), filepath.Ext(file.path)) + + for _, family := range file.Families { + if family.Fixture == nil { + return nil, fmt.Errorf("%s/%s has no fixture", file.path, family.Name) + } + + for _, variant := range family.Variants { + rendered, err := renderTemplate(family.Template, variant.Vars) + if err != nil { + return nil, fmt.Errorf("%s/%s/%s: %w", file.path, family.Name, variant.Name, err) + } + if err := loadCommittedFixture(ctx, backend.db, family.Fixture); err != nil { + return nil, err + } + + record := backend.capture(ctx, CorpusQuery{ + Source: file.path, + Name: fileName + "/" + family.Name + "/" + variant.Name, + Cypher: rendered, + Params: mergeParams(family.Params, variant.Params), + }) + records = append(records, record) + } + } + + for _, family := range file.Metamorphic { + if family.Fixture == nil { + return nil, fmt.Errorf("%s/%s has no fixture", file.path, family.Name) + } + if err := loadCommittedFixture(ctx, backend.db, family.Fixture); err != nil { + return nil, err + } + + for _, query := range family.Queries { + record := backend.capture(ctx, CorpusQuery{ + Source: file.path, + Name: fileName + "/" + family.Name + "/" + query.Name, + Cypher: query.Cypher, + Params: query.Params, + }) + records = append(records, record) + } + } + } + + return records, nil +} + +func openBackend(ctx context.Context, suite corpus, spec captureSpec) (*backendCapture, error) { + cfg := dawgs.Config{ + GraphQueryMemoryLimit: size.Gibibyte, + ConnectionString: spec.Connection, + } + + if spec.DriverName == pg.DriverName { + pool, err := pg.NewPool(drivers.DatabaseConfiguration{Connection: spec.Connection}) + if err != nil { + return nil, fmt.Errorf("create PostgreSQL pool: %w", err) + } + cfg.Pool = pool + } + + db, err := dawgs.Open(ctx, spec.DriverName, cfg) + if err != nil { + return nil, fmt.Errorf("open %s database: %w", spec.DriverName, err) + } + + schema := graph.Schema{ + Graphs: []graph.Graph{{ + Name: defaultGraphName, + Nodes: suite.nodeKinds, + Edges: suite.edgeKinds, + }}, + DefaultGraph: graph.Graph{Name: defaultGraphName}, + } + if err := db.AssertSchema(ctx, schema); err != nil { + _ = db.Close(ctx) + return nil, fmt.Errorf("assert schema: %w", err) + } + + backend := &backendCapture{ + spec: spec, + db: db, + } + + switch spec.DriverName { + case pg.DriverName: + pgDriver, ok := db.(*pg.Driver) + if !ok { + _ = db.Close(ctx) + return nil, fmt.Errorf("expected *pg.Driver, got %T", db) + } + defaultGraph, ok := pgDriver.DefaultGraph() + if !ok { + _ = db.Close(ctx) + return nil, fmt.Errorf("PostgreSQL default graph is not set") + } + backend.pgDriver = pgDriver + backend.pgGraphID = defaultGraph.ID + + case neo4j.DriverName: + neo4jDriver, err := openNeo4jPlanDriver(spec.Connection) + if err != nil { + _ = db.Close(ctx) + return nil, err + } + backend.neo4jDriver = neo4jDriver + } + + return backend, nil +} + +func (s *backendCapture) close(ctx context.Context) { + if s.neo4jDriver != nil { + _ = s.neo4jDriver.Close() + } + if s.db != nil { + _ = s.db.Close(ctx) + } +} + +func (s *backendCapture) capture(ctx context.Context, query CorpusQuery) PlanRecord { + record := PlanRecord{ + Driver: s.spec.DriverName, + Source: query.Source, + Dataset: query.Dataset, + Name: query.Name, + Cypher: query.Cypher, + Params: query.Params, + } + + switch s.spec.DriverName { + case pg.DriverName: + s.capturePostgres(ctx, query.Cypher, query.Params, &record) + case neo4j.DriverName: + s.captureNeo4j(query.Cypher, query.Params, &record) + } + + return record +} + +func (s *backendCapture) capturePostgres(ctx context.Context, cypherQuery string, params map[string]any, record *PlanRecord) { + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) + if err != nil { + record.Error = err.Error() + return + } + + translation, err := translate.Translate(ctx, regularQuery, s.pgDriver.KindMapper(), params, s.pgGraphID) + if err != nil { + record.Error = err.Error() + return + } + + sqlQuery, err := translate.Translated(translation) + if err != nil { + record.Error = err.Error() + return + } + + var plan []string + if err := s.db.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Raw("EXPLAIN "+sqlQuery, translation.Parameters) + defer result.Close() + + for result.Next() { + values := result.Values() + if len(values) == 0 { + continue + } + plan = append(plan, fmt.Sprint(values[0])) + } + + return result.Error() + }); err != nil { + record.Error = err.Error() + } + + record.SQL = sqlQuery + record.PGPlan = plan + record.PGOperators = postgresOperators(plan) + record.PlannedLowerings = loweringNames(translation.Optimization.PlannedLowerings) + record.AppliedLowerings = loweringNames(translation.Optimization.Lowerings) + record.Optimization = &translation.Optimization +} + +func (s *backendCapture) captureNeo4j(cypherQuery string, params map[string]any, record *PlanRecord) { + session := s.neo4jDriver.NewSession(neo4jcore.SessionConfig{ + AccessMode: neo4jcore.AccessModeWrite, + }) + defer session.Close() + + result, err := session.Run("EXPLAIN "+cypherWithoutTerminator(cypherQuery), params) + if err != nil { + record.Error = err.Error() + return + } + + summary, err := result.Consume() + if err != nil { + record.Error = err.Error() + return + } + + if plan := summary.Plan(); plan != nil { + planNode := convertNeo4jPlan(plan) + record.Neo4jPlan = &planNode + record.Neo4jOperators = neo4jOperators(planNode) + } +} + +func openNeo4jPlanDriver(connStr string) (neo4jcore.Driver, error) { + connectionURL, err := url.Parse(connStr) + if err != nil { + return nil, fmt.Errorf("parse Neo4j connection string: %w", err) + } + + password, ok := connectionURL.User.Password() + if !ok { + return nil, fmt.Errorf("no password provided in Neo4j connection string") + } + + return neo4jcore.NewDriver( + "bolt://"+connectionURL.Host, + neo4jcore.BasicAuth(connectionURL.User.Username(), password, ""), + ) +} + +func clearGraph(ctx context.Context, db graph.Database) error { + return db.WriteTransaction(ctx, func(tx graph.Transaction) error { + return tx.Nodes().Delete() + }) +} + +func loadDataset(ctx context.Context, db graph.Database, datasetDir, name string) error { + f, err := os.Open(filepath.Join(datasetDir, name+".json")) + if err != nil { + return fmt.Errorf("open dataset %s: %w", name, err) + } + defer f.Close() + + if _, err := opengraph.Load(ctx, db, f); err != nil { + return fmt.Errorf("load dataset %s: %w", name, err) + } + return nil +} + +func loadCommittedFixture(ctx context.Context, db graph.Database, fixture *opengraph.Graph) error { + if fixture == nil { + return fmt.Errorf("fixture is nil") + } + + if err := clearGraph(ctx, db); err != nil { + return err + } + + return db.WriteTransaction(ctx, func(tx graph.Transaction) error { + _, err := opengraph.WriteGraphTx(tx, fixture) + return err + }) +} + +func convertNeo4jPlan(plan neo4jcore.Plan) Neo4jPlanNode { + node := Neo4jPlanNode{ + Operator: plan.Operator(), + Arguments: stringifyArguments(plan.Arguments()), + Identifiers: append([]string(nil), plan.Identifiers()...), + } + + for _, child := range plan.Children() { + node.Children = append(node.Children, convertNeo4jPlan(child)) + } + + return node +} + +func stringifyArguments(arguments map[string]any) map[string]string { + if len(arguments) == 0 { + return nil + } + + values := make(map[string]string, len(arguments)) + for key, value := range arguments { + values[key] = fmt.Sprint(value) + } + return values +} + +func postgresOperators(plan []string) []string { + operators := make([]string, 0, len(plan)) + for _, line := range plan { + trimmed := strings.TrimSpace(line) + trimmed = strings.TrimPrefix(trimmed, "->") + trimmed = strings.TrimSpace(trimmed) + if trimmed == "" || strings.HasPrefix(trimmed, "Planning ") { + continue + } + if idx := strings.Index(trimmed, " ("); idx >= 0 { + trimmed = trimmed[:idx] + } + operators = append(operators, trimmed) + } + return operators +} + +func neo4jOperators(root Neo4jPlanNode) []string { + var operators []string + var walk func(Neo4jPlanNode) + walk = func(node Neo4jPlanNode) { + operators = append(operators, node.Operator) + for _, child := range node.Children { + walk(child) + } + } + walk(root) + return operators +} + +func loweringNames(decisions []optimize.LoweringDecision) []string { + if len(decisions) == 0 { + return nil + } + + names := make([]string, 0, len(decisions)) + seen := make(map[string]struct{}, len(decisions)) + for _, decision := range decisions { + name := decision.Name + if _, duplicate := seen[name]; duplicate { + continue + } + seen[name] = struct{}{} + names = append(names, name) + } + sort.Strings(names) + return names +} + +func cypherWithoutTerminator(cypherQuery string) string { + return strings.TrimSuffix(strings.TrimSpace(cypherQuery), ";") +} diff --git a/cmd/plancorpus/corpus.go b/cmd/plancorpus/corpus.go new file mode 100644 index 00000000..46fdd4e0 --- /dev/null +++ b/cmd/plancorpus/corpus.go @@ -0,0 +1,222 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/opengraph" +) + +type corpus struct { + caseGroups map[string]*caseGroup + datasetNames []string + templateFiles []templateFile + nodeKinds graph.Kinds + edgeKinds graph.Kinds +} + +type caseGroup struct { + dataset string + files []caseFile +} + +type caseFile struct { + path string + Dataset string `json:"dataset"` + Cases []caseEntry `json:"cases"` +} + +type caseEntry struct { + Name string `json:"name"` + Cypher string `json:"cypher"` + Params map[string]any `json:"params,omitempty"` + Fixture *opengraph.Graph `json:"fixture,omitempty"` +} + +type templateFile struct { + path string + Families []templateFamily `json:"families,omitempty"` + Metamorphic []metamorphicFamily `json:"metamorphic,omitempty"` +} + +type templateFamily struct { + Name string `json:"name"` + Template string `json:"template"` + Params map[string]any `json:"params,omitempty"` + Fixture *opengraph.Graph `json:"fixture,omitempty"` + Variants []templateVariant `json:"variants"` +} + +type templateVariant struct { + Name string `json:"name"` + Vars map[string]string `json:"vars"` + Params map[string]any `json:"params,omitempty"` +} + +type metamorphicFamily struct { + Name string `json:"name"` + Fixture *opengraph.Graph `json:"fixture,omitempty"` + Queries []metamorphicQuery `json:"queries"` +} + +type metamorphicQuery struct { + Name string `json:"name"` + Cypher string `json:"cypher"` + Params map[string]any `json:"params,omitempty"` +} + +func loadCorpus(datasetDir string) (corpus, error) { + var loaded corpus + loaded.caseGroups = map[string]*caseGroup{} + + if err := loaded.loadCaseFiles(datasetDir); err != nil { + return corpus{}, err + } + if err := loaded.loadTemplateFiles(datasetDir); err != nil { + return corpus{}, err + } + if err := loaded.loadDatasetKinds(datasetDir); err != nil { + return corpus{}, err + } + + sort.Strings(loaded.datasetNames) + return loaded, nil +} + +func (s *corpus) loadCaseFiles(datasetDir string) error { + paths, err := filepath.Glob(filepath.Join(datasetDir, "cases", "*.json")) + if err != nil { + return fmt.Errorf("glob case files: %w", err) + } + if len(paths) == 0 { + return fmt.Errorf("no case files found under %s", filepath.Join(datasetDir, "cases")) + } + sort.Strings(paths) + + for _, path := range paths { + var file caseFile + if err := decodeJSONFile(path, &file); err != nil { + return err + } + file.path = filepath.ToSlash(path) + + dataset := file.Dataset + if dataset == "" { + dataset = "base" + } + if s.caseGroups[dataset] == nil { + s.caseGroups[dataset] = &caseGroup{dataset: dataset} + s.datasetNames = append(s.datasetNames, dataset) + } + s.caseGroups[dataset].files = append(s.caseGroups[dataset].files, file) + + for _, testCase := range file.Cases { + s.addFixtureKinds(testCase.Fixture) + } + } + + return nil +} + +func (s *corpus) loadTemplateFiles(datasetDir string) error { + paths, err := filepath.Glob(filepath.Join(datasetDir, "templates", "*.json")) + if err != nil { + return fmt.Errorf("glob template files: %w", err) + } + sort.Strings(paths) + + for _, path := range paths { + var file templateFile + if err := decodeJSONFile(path, &file); err != nil { + return err + } + file.path = filepath.ToSlash(path) + s.templateFiles = append(s.templateFiles, file) + + for _, family := range file.Families { + s.addFixtureKinds(family.Fixture) + } + for _, family := range file.Metamorphic { + s.addFixtureKinds(family.Fixture) + } + } + + return nil +} + +func (s *corpus) loadDatasetKinds(datasetDir string) error { + for _, datasetName := range s.datasetNames { + path := filepath.Join(datasetDir, datasetName+".json") + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("open dataset %s: %w", datasetName, err) + } + + doc, parseErr := opengraph.ParseDocument(f) + closeErr := f.Close() + if parseErr != nil { + return fmt.Errorf("parse dataset %s: %w", datasetName, parseErr) + } + if closeErr != nil { + return fmt.Errorf("close dataset %s: %w", datasetName, closeErr) + } + + nodeKinds, edgeKinds := doc.Graph.Kinds() + s.nodeKinds = s.nodeKinds.Add(nodeKinds...) + s.edgeKinds = s.edgeKinds.Add(edgeKinds...) + } + + return nil +} + +func (s *corpus) addFixtureKinds(fixture *opengraph.Graph) { + if fixture == nil { + return + } + + nodeKinds, edgeKinds := fixture.Kinds() + s.nodeKinds = s.nodeKinds.Add(nodeKinds...) + s.edgeKinds = s.edgeKinds.Add(edgeKinds...) +} + +func decodeJSONFile(path string, target any) error { + raw, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + if err := json.Unmarshal(raw, target); err != nil { + return fmt.Errorf("decode %s: %w", path, err) + } + return nil +} + +func renderTemplate(template string, vars map[string]string) (string, error) { + rendered := template + for name, value := range vars { + rendered = strings.ReplaceAll(rendered, "{{"+name+"}}", value) + } + if strings.Contains(rendered, "{{") || strings.Contains(rendered, "}}") { + return "", fmt.Errorf("template has unresolved placeholders: %s", rendered) + } + return rendered, nil +} + +func mergeParams(base, overrides map[string]any) map[string]any { + if len(base) == 0 && len(overrides) == 0 { + return nil + } + + merged := make(map[string]any, len(base)+len(overrides)) + for key, value := range base { + merged[key] = value + } + for key, value := range overrides { + merged[key] = value + } + return merged +} diff --git a/cmd/plancorpus/corpus_test.go b/cmd/plancorpus/corpus_test.go new file mode 100644 index 00000000..141fa515 --- /dev/null +++ b/cmd/plancorpus/corpus_test.go @@ -0,0 +1,34 @@ +package main + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadCorpus(t *testing.T) { + suite, err := loadCorpus(filepath.Join("..", "..", "integration", "testdata")) + require.NoError(t, err) + + require.Contains(t, suite.caseGroups, "base") + require.Contains(t, suite.datasetNames, "base") + require.NotEmpty(t, suite.templateFiles) + require.NotEmpty(t, suite.nodeKinds) + require.NotEmpty(t, suite.edgeKinds) +} + +func TestRenderTemplateRequiresAllPlaceholders(t *testing.T) { + rendered, err := renderTemplate("match ({{name}}) return {{name}}", map[string]string{"name": "n"}) + require.NoError(t, err) + require.Equal(t, "match (n) return n", rendered) + + _, err = renderTemplate("match ({{name}}) return n", nil) + require.ErrorContains(t, err, "unresolved placeholders") +} + +func TestMergeParams(t *testing.T) { + merged := mergeParams(map[string]any{"a": 1, "b": 2}, map[string]any{"b": 3}) + require.Equal(t, map[string]any{"a": 1, "b": 3}, merged) + require.Nil(t, mergeParams(nil, nil)) +} diff --git a/cmd/plancorpus/main.go b/cmd/plancorpus/main.go new file mode 100644 index 00000000..6a1f06e9 --- /dev/null +++ b/cmd/plancorpus/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" +) + +type commandConfig struct { + DatasetDir string + OutputDir string + SummaryMarkdown string + SummaryJSON string + Connection string + PGConnection string + Neo4jConnection string + TopPlans int +} + +func main() { + cfg := commandConfig{} + flag.StringVar(&cfg.DatasetDir, "dataset-dir", "integration/testdata", "integration testdata directory") + flag.StringVar(&cfg.OutputDir, "output-dir", ".coverage", "directory for JSONL plan captures") + flag.StringVar(&cfg.SummaryMarkdown, "summary", "", "markdown summary path (default: output-dir/plan-corpus-summary.md)") + flag.StringVar(&cfg.SummaryJSON, "summary-json", "", "JSON summary path (default: output-dir/plan-corpus-summary.json)") + flag.StringVar(&cfg.Connection, "connection", os.Getenv("CONNECTION_STRING"), "single backend connection string") + flag.StringVar(&cfg.PGConnection, "pg-connection", os.Getenv("PG_CONNECTION_STRING"), "PostgreSQL connection string") + flag.StringVar(&cfg.Neo4jConnection, "neo4j-connection", os.Getenv("NEO4J_CONNECTION_STRING"), "Neo4j connection string") + flag.IntVar(&cfg.TopPlans, "top", defaultTopPlans, "number of expensive PostgreSQL plans to include in summaries") + flag.Parse() + + if err := run(context.Background(), cfg); err != nil { + fmt.Fprintf(os.Stderr, "plancorpus: %v\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context, cfg commandConfig) error { + specs, err := captureSpecs(cfg) + if err != nil { + return err + } + + suite, err := loadCorpus(cfg.DatasetDir) + if err != nil { + return err + } + + if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil { + return fmt.Errorf("create output directory: %w", err) + } + + var allRecords []PlanRecord + for _, spec := range specs { + records, err := captureCorpus(ctx, cfg.DatasetDir, suite, spec) + if err != nil { + return err + } + + outputPath := filepath.Join(cfg.OutputDir, "plan-corpus-"+spec.DriverName+".jsonl") + if err := writePlanRecords(outputPath, records); err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "captured %d %s records in %s\n", len(records), spec.DriverName, outputPath) + allRecords = append(allRecords, records...) + } + + summary := buildSummary(allRecords, cfg.TopPlans) + if cfg.SummaryMarkdown == "" { + cfg.SummaryMarkdown = filepath.Join(cfg.OutputDir, "plan-corpus-summary.md") + } + if cfg.SummaryJSON == "" { + cfg.SummaryJSON = filepath.Join(cfg.OutputDir, "plan-corpus-summary.json") + } + + if err := writeSummaryFiles(cfg.SummaryMarkdown, cfg.SummaryJSON, summary); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "wrote summaries to %s and %s\n", cfg.SummaryMarkdown, cfg.SummaryJSON) + return nil +} + +func captureSpecs(cfg commandConfig) ([]captureSpec, error) { + specsByDriver := map[string]captureSpec{} + + if cfg.Connection != "" { + driverName, err := driverFromConnectionString(cfg.Connection) + if err != nil { + return nil, err + } + specsByDriver[driverName] = captureSpec{ + DriverName: driverName, + Connection: cfg.Connection, + } + } + + if cfg.PGConnection != "" { + specsByDriver[pgDriverName()] = captureSpec{ + DriverName: pgDriverName(), + Connection: cfg.PGConnection, + } + } + if cfg.Neo4jConnection != "" { + specsByDriver[neo4jDriverName()] = captureSpec{ + DriverName: neo4jDriverName(), + Connection: cfg.Neo4jConnection, + } + } + + if len(specsByDriver) == 0 { + return nil, fmt.Errorf("no connection string supplied; set CONNECTION_STRING or PG_CONNECTION_STRING/NEO4J_CONNECTION_STRING") + } + + orderedDrivers := []string{pgDriverName(), neo4jDriverName()} + specs := make([]captureSpec, 0, len(specsByDriver)) + for _, driverName := range orderedDrivers { + if spec, found := specsByDriver[driverName]; found { + specs = append(specs, spec) + } + } + return specs, nil +} + +func pgDriverName() string { + return "pg" +} + +func neo4jDriverName() string { + return "neo4j" +} + +func writePlanRecords(path string, records []PlanRecord) error { + out, err := os.Create(path) + if err != nil { + return fmt.Errorf("create %s: %w", path, err) + } + defer out.Close() + + encoder := json.NewEncoder(out) + for _, record := range records { + if err := encoder.Encode(record); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + } + return nil +} + +func writeSummaryFiles(markdownPath, jsonPath string, summary PlanSummary) error { + if markdownPath != "" { + out, err := os.Create(markdownPath) + if err != nil { + return fmt.Errorf("create %s: %w", markdownPath, err) + } + if err := writeMarkdownSummary(out, summary); err != nil { + _ = out.Close() + return fmt.Errorf("write %s: %w", markdownPath, err) + } + if err := out.Close(); err != nil { + return fmt.Errorf("close %s: %w", markdownPath, err) + } + } + + if jsonPath != "" { + out, err := os.Create(jsonPath) + if err != nil { + return fmt.Errorf("create %s: %w", jsonPath, err) + } + if err := writeJSONSummary(out, summary); err != nil { + _ = out.Close() + return fmt.Errorf("write %s: %w", jsonPath, err) + } + if err := out.Close(); err != nil { + return fmt.Errorf("close %s: %w", jsonPath, err) + } + } + + return nil +} diff --git a/cmd/plancorpus/main_test.go b/cmd/plancorpus/main_test.go new file mode 100644 index 00000000..51aa5af9 --- /dev/null +++ b/cmd/plancorpus/main_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCaptureSpecs(t *testing.T) { + specs, err := captureSpecs(commandConfig{ + Connection: "neo4j://neo4j:password@localhost:7687", + PGConnection: "postgres://postgres:password@localhost/db", + Neo4jConnection: "neo4j://neo4j:override@localhost:7687", + }) + require.NoError(t, err) + require.Equal(t, []captureSpec{{ + DriverName: "pg", + Connection: "postgres://postgres:password@localhost/db", + }, { + DriverName: "neo4j", + Connection: "neo4j://neo4j:override@localhost:7687", + }}, specs) +} + +func TestCaptureSpecsRequiresConnection(t *testing.T) { + _, err := captureSpecs(commandConfig{}) + require.ErrorContains(t, err, "no connection string supplied") +} + +func TestDriverFromConnectionString(t *testing.T) { + driverName, err := driverFromConnectionString("postgresql://postgres:password@localhost/db") + require.NoError(t, err) + require.Equal(t, "pg", driverName) + + driverName, err = driverFromConnectionString("neo4j://neo4j:password@localhost:7687") + require.NoError(t, err) + require.Equal(t, "neo4j", driverName) + + _, err = driverFromConnectionString("mysql://localhost") + require.ErrorContains(t, err, "unknown connection string scheme") +} diff --git a/cmd/plancorpus/report.go b/cmd/plancorpus/report.go new file mode 100644 index 00000000..bbd95eac --- /dev/null +++ b/cmd/plancorpus/report.go @@ -0,0 +1,279 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "regexp" + "sort" + "strconv" + "strings" +) + +const defaultTopPlans = 25 + +var postgresCostPattern = regexp.MustCompile(`cost=[0-9.]+\.\.([0-9.]+)`) + +type PlanSummary struct { + Drivers []DriverSummary `json:"drivers"` + TopPostgresPlans []CostedPlan `json:"top_postgres_plans,omitempty"` + PostgresOperators []Count `json:"postgres_operators,omitempty"` + Neo4jOperators []Count `json:"neo4j_operators,omitempty"` + PlannedLowerings []Count `json:"planned_lowerings,omitempty"` + AppliedLowerings []Count `json:"applied_lowerings,omitempty"` + FeatureCounts []Count `json:"feature_counts,omitempty"` + Errors []PlanError `json:"errors,omitempty"` +} + +type DriverSummary struct { + Driver string `json:"driver"` + Records int `json:"records"` + Errors int `json:"errors"` +} + +type Count struct { + Name string `json:"name"` + Count int `json:"count"` +} + +type CostedPlan struct { + Cost float64 `json:"cost"` + Driver string `json:"driver"` + Source string `json:"source"` + Dataset string `json:"dataset,omitempty"` + Name string `json:"name"` + Cypher string `json:"cypher"` + PlanRoot string `json:"plan_root"` + PlannedLowerings []string `json:"planned_lowerings,omitempty"` + AppliedLowerings []string `json:"applied_lowerings,omitempty"` +} + +type PlanError struct { + Driver string `json:"driver"` + Source string `json:"source"` + Name string `json:"name"` + Error string `json:"error"` +} + +func buildSummary(records []PlanRecord, topN int) PlanSummary { + if topN <= 0 { + topN = defaultTopPlans + } + + driverCounts := map[string]*DriverSummary{} + postgresOperatorCounts := map[string]int{} + neo4jOperatorCounts := map[string]int{} + plannedLoweringCounts := map[string]int{} + appliedLoweringCounts := map[string]int{} + featureCounts := map[string]int{} + + var ( + errors []PlanError + topPG []CostedPlan + ) + + for _, record := range records { + driver := driverCounts[record.Driver] + if driver == nil { + driver = &DriverSummary{Driver: record.Driver} + driverCounts[record.Driver] = driver + } + driver.Records++ + + if record.Error != "" { + driver.Errors++ + errors = append(errors, PlanError{ + Driver: record.Driver, + Source: record.Source, + Name: record.Name, + Error: record.Error, + }) + } + + for _, operator := range record.PGOperators { + postgresOperatorCounts[normalizePostgresOperator(operator)]++ + } + for _, operator := range record.Neo4jOperators { + neo4jOperatorCounts[operator]++ + } + for _, lowering := range record.PlannedLowerings { + plannedLoweringCounts[lowering]++ + } + for _, lowering := range record.AppliedLowerings { + appliedLoweringCounts[lowering]++ + } + + for _, line := range record.PGPlan { + switch { + case strings.Contains(line, "Recursive Union"): + featureCounts["PostgreSQL Recursive Union"]++ + case strings.Contains(line, "Function Scan on unnest"): + featureCounts["PostgreSQL Function Scan on unnest"]++ + case strings.Contains(line, "SubPlan "): + featureCounts["PostgreSQL SubPlan"]++ + case strings.Contains(line, "Filter: satisfied"): + featureCounts["PostgreSQL traversal satisfied filter"]++ + } + } + + if len(record.PGPlan) > 0 && record.Error == "" { + topPG = append(topPG, CostedPlan{ + Cost: postgresEstimatedCost(record.PGPlan[0]), + Driver: record.Driver, + Source: record.Source, + Dataset: record.Dataset, + Name: record.Name, + Cypher: record.Cypher, + PlanRoot: record.PGPlan[0], + PlannedLowerings: append([]string(nil), record.PlannedLowerings...), + AppliedLowerings: append([]string(nil), record.AppliedLowerings...), + }) + } + } + + sort.Slice(topPG, func(i, j int) bool { + return topPG[i].Cost > topPG[j].Cost + }) + if len(topPG) > topN { + topPG = topPG[:topN] + } + + return PlanSummary{ + Drivers: sortedDriverSummaries(driverCounts), + TopPostgresPlans: topPG, + PostgresOperators: sortedCounts(postgresOperatorCounts), + Neo4jOperators: sortedCounts(neo4jOperatorCounts), + PlannedLowerings: sortedCounts(plannedLoweringCounts), + AppliedLowerings: sortedCounts(appliedLoweringCounts), + FeatureCounts: sortedCounts(featureCounts), + Errors: errors, + } +} + +func postgresEstimatedCost(planRoot string) float64 { + match := postgresCostPattern.FindStringSubmatch(planRoot) + if len(match) != 2 { + return 0 + } + + cost, err := strconv.ParseFloat(match[1], 64) + if err != nil { + return 0 + } + return cost +} + +func normalizePostgresOperator(operator string) string { + operator = strings.TrimSpace(operator) + if operator == "" { + return "" + } + if idx := strings.Index(operator, ":"); idx >= 0 { + return operator[:idx] + } + if idx := strings.Index(operator, " on "); idx >= 0 { + return operator[:idx] + } + if idx := strings.Index(operator, " using "); idx >= 0 { + return operator[:idx] + } + return operator +} + +func sortedDriverSummaries(drivers map[string]*DriverSummary) []DriverSummary { + sorted := make([]DriverSummary, 0, len(drivers)) + for _, summary := range drivers { + sorted = append(sorted, *summary) + } + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].Driver < sorted[j].Driver + }) + return sorted +} + +func sortedCounts(counts map[string]int) []Count { + sorted := make([]Count, 0, len(counts)) + for name, count := range counts { + if name == "" || count == 0 { + continue + } + sorted = append(sorted, Count{Name: name, Count: count}) + } + sort.Slice(sorted, func(i, j int) bool { + if sorted[i].Count == sorted[j].Count { + return sorted[i].Name < sorted[j].Name + } + return sorted[i].Count > sorted[j].Count + }) + return sorted +} + +func writeJSONSummary(w io.Writer, summary PlanSummary) error { + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + return encoder.Encode(summary) +} + +func writeMarkdownSummary(w io.Writer, summary PlanSummary) error { + writeCounts := func(title string, counts []Count, limit int) { + if len(counts) == 0 { + return + } + fmt.Fprintf(w, "\n## %s\n\n| Name | Count |\n| --- | ---: |\n", title) + for idx, count := range counts { + if limit > 0 && idx >= limit { + break + } + fmt.Fprintf(w, "| %s | %d |\n", markdownCell(count.Name), count.Count) + } + } + + fmt.Fprintln(w, "# Cypher Plan Corpus Summary") + fmt.Fprintln(w, "\n## Drivers\n\n| Driver | Records | Errors |\n| --- | ---: | ---: |") + for _, driver := range summary.Drivers { + fmt.Fprintf(w, "| %s | %d | %d |\n", markdownCell(driver.Driver), driver.Records, driver.Errors) + } + + if len(summary.TopPostgresPlans) > 0 { + fmt.Fprintln(w, "\n## Top PostgreSQL Plans\n\n| Cost | Source | Name | Root | Lowerings |\n| ---: | --- | --- | --- | --- |") + for _, plan := range summary.TopPostgresPlans { + fmt.Fprintf( + w, + "| %.2f | %s | %s | %s | %s |\n", + plan.Cost, + markdownCell(plan.Source), + markdownCell(plan.Name), + markdownCell(plan.PlanRoot), + markdownCell(strings.Join(plan.PlannedLowerings, ", ")), + ) + } + } + + writeCounts("Feature Counts", summary.FeatureCounts, 0) + writeCounts("Planned Lowerings", summary.PlannedLowerings, 0) + writeCounts("Applied Lowerings", summary.AppliedLowerings, 0) + writeCounts("PostgreSQL Operators", summary.PostgresOperators, 25) + writeCounts("Neo4j Operators", summary.Neo4jOperators, 25) + + if len(summary.Errors) > 0 { + fmt.Fprintln(w, "\n## Capture Errors\n\n| Driver | Source | Name | Error |\n| --- | --- | --- | --- |") + for _, captureError := range summary.Errors { + fmt.Fprintf( + w, + "| %s | %s | %s | %s |\n", + markdownCell(captureError.Driver), + markdownCell(captureError.Source), + markdownCell(captureError.Name), + markdownCell(captureError.Error), + ) + } + } + + return nil +} + +func markdownCell(value string) string { + value = strings.ReplaceAll(value, "\n", " ") + value = strings.ReplaceAll(value, "|", "\\|") + return value +} diff --git a/cmd/plancorpus/report_test.go b/cmd/plancorpus/report_test.go new file mode 100644 index 00000000..9ec4907e --- /dev/null +++ b/cmd/plancorpus/report_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBuildSummaryRanksPostgresPlansAndCountsSignals(t *testing.T) { + records := []PlanRecord{{ + Driver: "pg", + Source: "cases/a.json", + Name: "low", + Cypher: "match (n) return n", + PGPlan: []string{"Seq Scan on node_1 (cost=0.00..10.50 rows=1 width=8)", "Filter: satisfied"}, + PGOperators: []string{"Seq Scan on node_1", "Filter: satisfied"}, + PlannedLowerings: []string{"ProjectionPruning"}, + AppliedLowerings: []string{"ProjectionPruning"}, + }, { + Driver: "pg", + Source: "cases/b.json", + Name: "high", + Cypher: "match p=()-[*]->() return p", + PGPlan: []string{"Recursive Union (cost=0.00..99.25 rows=1 width=8)", "SubPlan 1", "Function Scan on unnest _path"}, + PGOperators: []string{"Recursive Union", "Function Scan on unnest _path"}, + PlannedLowerings: []string{"LatePathMaterialization"}, + AppliedLowerings: []string{"LatePathMaterialization"}, + }, { + Driver: "neo4j", + Source: "cases/a.json", + Name: "neo", + Cypher: "match (n) return n", + Neo4jOperators: []string{"ProduceResults@neo4j", "AllNodesScan@neo4j"}, + }, { + Driver: "pg", + Source: "cases/error.json", + Name: "error", + Error: "expected error", + }} + + summary := buildSummary(records, 1) + + require.Equal(t, []DriverSummary{{ + Driver: "neo4j", + Records: 1, + }, { + Driver: "pg", + Records: 3, + Errors: 1, + }}, summary.Drivers) + require.Len(t, summary.TopPostgresPlans, 1) + require.Equal(t, "high", summary.TopPostgresPlans[0].Name) + require.Contains(t, summary.FeatureCounts, Count{Name: "PostgreSQL Recursive Union", Count: 1}) + require.Contains(t, summary.FeatureCounts, Count{Name: "PostgreSQL SubPlan", Count: 1}) + require.Contains(t, summary.FeatureCounts, Count{Name: "PostgreSQL Function Scan on unnest", Count: 1}) + require.Contains(t, summary.FeatureCounts, Count{Name: "PostgreSQL traversal satisfied filter", Count: 1}) + require.Contains(t, summary.PostgresOperators, Count{Name: "Seq Scan", Count: 1}) + require.Contains(t, summary.Neo4jOperators, Count{Name: "ProduceResults@neo4j", Count: 1}) + require.Contains(t, summary.PlannedLowerings, Count{Name: "LatePathMaterialization", Count: 1}) + require.Contains(t, summary.Errors, PlanError{ + Driver: "pg", + Source: "cases/error.json", + Name: "error", + Error: "expected error", + }) +} + +func TestWriteMarkdownSummaryEscapesPipes(t *testing.T) { + summary := PlanSummary{ + Drivers: []DriverSummary{{Driver: "pg", Records: 1}}, + TopPostgresPlans: []CostedPlan{{ + Cost: 1.25, + Source: "cases/a.json", + Name: "pipe | name", + PlanRoot: "Seq Scan on node_1", + }}, + } + + var out bytes.Buffer + require.NoError(t, writeMarkdownSummary(&out, summary)) + require.Contains(t, out.String(), "pipe \\| name") +} + +func TestPostgresEstimatedCost(t *testing.T) { + require.Equal(t, 1180526.82, postgresEstimatedCost("Hash Join (cost=4136.05..1180526.82 rows=32097 width=68)")) + require.Zero(t, postgresEstimatedCost("not a plan")) +} diff --git a/cmd/plancorpus/types.go b/cmd/plancorpus/types.go new file mode 100644 index 00000000..3bc336d8 --- /dev/null +++ b/cmd/plancorpus/types.go @@ -0,0 +1,36 @@ +package main + +import "github.com/specterops/dawgs/cypher/models/pgsql/translate" + +type PlanRecord struct { + Driver string `json:"driver"` + Source string `json:"source"` + Dataset string `json:"dataset,omitempty"` + Name string `json:"name"` + Cypher string `json:"cypher"` + Params map[string]any `json:"params,omitempty"` + SQL string `json:"sql,omitempty"` + PGPlan []string `json:"pg_plan,omitempty"` + PGOperators []string `json:"pg_operators,omitempty"` + Neo4jPlan *Neo4jPlanNode `json:"neo4j_plan,omitempty"` + Neo4jOperators []string `json:"neo4j_operators,omitempty"` + PlannedLowerings []string `json:"planned_lowerings,omitempty"` + AppliedLowerings []string `json:"applied_lowerings,omitempty"` + Optimization *translate.OptimizationSummary `json:"optimization,omitempty"` + Error string `json:"error,omitempty"` +} + +type Neo4jPlanNode struct { + Operator string `json:"operator"` + Arguments map[string]string `json:"arguments,omitempty"` + Identifiers []string `json:"identifiers,omitempty"` + Children []Neo4jPlanNode `json:"children,omitempty"` +} + +type CorpusQuery struct { + Source string + Dataset string + Name string + Cypher string + Params map[string]any +} diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md new file mode 100644 index 00000000..1f064f50 --- /dev/null +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -0,0 +1,74 @@ +# Cypher to PostgreSQL Optimization Plan + +This plan tracks optimization and rewrite work identified by running the shared integration corpus against Neo4j and PostgreSQL and comparing plan shapes. + +## Phase 1: Baseline And Tooling + +Status: in progress + +- Keep a reproducible plan-capture workflow. + - Capture PostgreSQL translated SQL, PostgreSQL `EXPLAIN`, Neo4j logical plan operator trees, and optimizer planned/applied lowerings. + - Read `integration/testdata/cases` and `integration/testdata/templates`. + - Write comparable JSONL output without changing product behavior. +- Add plan-summary reporting. + - Rank cases by PostgreSQL estimated cost. + - Count plan operators, recursive CTEs, subplans, path materialization indicators, and optimizer lowerings. + - Produce markdown and JSON summaries. + +## Phase 2: Quick Wins + +Status: pending + +- Add count-store fast paths for simple count queries: + - `MATCH (n) RETURN count(n)` + - `MATCH ()-[r]->() RETURN count(r)` + - Typed variants where kind filters map cleanly. +- Audit the planned/applied `PredicatePlacement` gap. + - Distinguish missing translator consumption from intentional skipped placements. + - Add explicit skipped-placement reasons when a planned lowering is not applied. + +## Phase 3: Path Materialization + +Status: pending + +- Share path materialization for repeated path functions. + - Target `nodes(p)`, `relationships(p)`, `size(relationships(p))`, `startNode`, `endNode`, and `type`. + - Avoid repeated `SubPlan` and `Function Scan on unnest` work per path binding. +- Expand late path materialization coverage. + - Ensure paths are built only when needed for projection, filtering, or mutation semantics. + +## Phase 4: Traversal And Recursive CTEs + +Status: pending + +- Push predicates into recursive traversal anchors and steps where semantics allow. + - Endpoint kind/property predicates. + - Relationship type predicates. + - Bound-node filters. +- Improve traversal direction selection using endpoint selectivity. + - Bound IDs. + - Labels/kinds. + - Equality predicates. + - Finite relationship type sets. +- Broaden limit pushdown for variable-length path queries when ordering and distinct semantics permit early termination. + +## Phase 5: Suffix And Shared Endpoint Rewrites + +Status: pending + +- Improve expansion suffix pushdown for fixed suffixes after variable-length traversals. +- Improve `ExpandInto` and shared endpoint rewrites for ADCS-style fanout patterns. + - Constrain earlier using bound endpoint semi-joins or correlated expansion lowering where valid. + +## Phase 6: Validation + +Status: pending + +- Add focused regression tests per optimization. + - Optimizer/lowering selection tests. + - SQL shape translation tests. + - Backend-equivalent integration tests. +- Benchmark after each workstream. + - Run unit tests. + - Run backend-specific integration tests. + - Run plan capture and compare summary deltas. From 1f97efa86c73bd833ab9580035d6288438b27210 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 16:26:29 -0700 Subject: [PATCH 050/114] feat(plancorpus): add count fast path and skipped lowering reporting --- cmd/plancorpus/capture.go | 1 + cmd/plancorpus/report.go | 29 +++ cmd/plancorpus/report_test.go | 8 + cmd/plancorpus/types.go | 1 + .../pgsql/optimize/OPTIMIZATION_PLAN.md | 6 +- cypher/models/pgsql/optimize/lowering.go | 22 +- cypher/models/pgsql/optimize/lowering_plan.go | 138 ++++++++++ .../models/pgsql/optimize/optimizer_test.go | 49 ++++ .../translation_cases/stepwise_traversal.sql | 2 +- .../models/pgsql/translate/count_fast_path.go | 243 ++++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 52 ++++ cypher/models/pgsql/translate/translator.go | 70 +++++ 12 files changed, 617 insertions(+), 4 deletions(-) create mode 100644 cypher/models/pgsql/translate/count_fast_path.go diff --git a/cmd/plancorpus/capture.go b/cmd/plancorpus/capture.go index 6b4e264e..e69802fb 100644 --- a/cmd/plancorpus/capture.go +++ b/cmd/plancorpus/capture.go @@ -292,6 +292,7 @@ func (s *backendCapture) capturePostgres(ctx context.Context, cypherQuery string record.PGOperators = postgresOperators(plan) record.PlannedLowerings = loweringNames(translation.Optimization.PlannedLowerings) record.AppliedLowerings = loweringNames(translation.Optimization.Lowerings) + record.SkippedLowerings = append([]translate.SkippedLowering(nil), translation.Optimization.SkippedLowerings...) record.Optimization = &translation.Optimization } diff --git a/cmd/plancorpus/report.go b/cmd/plancorpus/report.go index bbd95eac..d0a28f4a 100644 --- a/cmd/plancorpus/report.go +++ b/cmd/plancorpus/report.go @@ -8,6 +8,8 @@ import ( "sort" "strconv" "strings" + + "github.com/specterops/dawgs/cypher/models/pgsql/translate" ) const defaultTopPlans = 25 @@ -21,6 +23,8 @@ type PlanSummary struct { Neo4jOperators []Count `json:"neo4j_operators,omitempty"` PlannedLowerings []Count `json:"planned_lowerings,omitempty"` AppliedLowerings []Count `json:"applied_lowerings,omitempty"` + SkippedLowerings []Count `json:"skipped_lowerings,omitempty"` + SkippedReasons []Count `json:"skipped_reasons,omitempty"` FeatureCounts []Count `json:"feature_counts,omitempty"` Errors []PlanError `json:"errors,omitempty"` } @@ -46,6 +50,7 @@ type CostedPlan struct { PlanRoot string `json:"plan_root"` PlannedLowerings []string `json:"planned_lowerings,omitempty"` AppliedLowerings []string `json:"applied_lowerings,omitempty"` + SkippedLowerings []string `json:"skipped_lowerings,omitempty"` } type PlanError struct { @@ -65,6 +70,8 @@ func buildSummary(records []PlanRecord, topN int) PlanSummary { neo4jOperatorCounts := map[string]int{} plannedLoweringCounts := map[string]int{} appliedLoweringCounts := map[string]int{} + skippedLoweringCounts := map[string]int{} + skippedReasonCounts := map[string]int{} featureCounts := map[string]int{} var ( @@ -102,6 +109,10 @@ func buildSummary(records []PlanRecord, topN int) PlanSummary { for _, lowering := range record.AppliedLowerings { appliedLoweringCounts[lowering]++ } + for _, lowering := range record.SkippedLowerings { + skippedLoweringCounts[lowering.Name]++ + skippedReasonCounts[lowering.Name+": "+lowering.Reason]++ + } for _, line := range record.PGPlan { switch { @@ -127,6 +138,7 @@ func buildSummary(records []PlanRecord, topN int) PlanSummary { PlanRoot: record.PGPlan[0], PlannedLowerings: append([]string(nil), record.PlannedLowerings...), AppliedLowerings: append([]string(nil), record.AppliedLowerings...), + SkippedLowerings: skippedLoweringLabels(record.SkippedLowerings), }) } } @@ -145,11 +157,26 @@ func buildSummary(records []PlanRecord, topN int) PlanSummary { Neo4jOperators: sortedCounts(neo4jOperatorCounts), PlannedLowerings: sortedCounts(plannedLoweringCounts), AppliedLowerings: sortedCounts(appliedLoweringCounts), + SkippedLowerings: sortedCounts(skippedLoweringCounts), + SkippedReasons: sortedCounts(skippedReasonCounts), FeatureCounts: sortedCounts(featureCounts), Errors: errors, } } +func skippedLoweringLabels(lowerings []translate.SkippedLowering) []string { + if len(lowerings) == 0 { + return nil + } + + labels := make([]string, len(lowerings)) + for idx, lowering := range lowerings { + labels[idx] = lowering.Name + ": " + lowering.Reason + } + + return labels +} + func postgresEstimatedCost(planRoot string) float64 { match := postgresCostPattern.FindStringSubmatch(planRoot) if len(match) != 2 { @@ -252,6 +279,8 @@ func writeMarkdownSummary(w io.Writer, summary PlanSummary) error { writeCounts("Feature Counts", summary.FeatureCounts, 0) writeCounts("Planned Lowerings", summary.PlannedLowerings, 0) writeCounts("Applied Lowerings", summary.AppliedLowerings, 0) + writeCounts("Skipped Lowerings", summary.SkippedLowerings, 0) + writeCounts("Skipped Lowering Reasons", summary.SkippedReasons, 0) writeCounts("PostgreSQL Operators", summary.PostgresOperators, 25) writeCounts("Neo4j Operators", summary.Neo4jOperators, 25) diff --git a/cmd/plancorpus/report_test.go b/cmd/plancorpus/report_test.go index 9ec4907e..f3616952 100644 --- a/cmd/plancorpus/report_test.go +++ b/cmd/plancorpus/report_test.go @@ -4,6 +4,7 @@ import ( "bytes" "testing" + "github.com/specterops/dawgs/cypher/models/pgsql/translate" "github.com/stretchr/testify/require" ) @@ -26,6 +27,11 @@ func TestBuildSummaryRanksPostgresPlansAndCountsSignals(t *testing.T) { PGOperators: []string{"Recursive Union", "Function Scan on unnest _path"}, PlannedLowerings: []string{"LatePathMaterialization"}, AppliedLowerings: []string{"LatePathMaterialization"}, + SkippedLowerings: []translate.SkippedLowering{{ + Name: "PredicatePlacement", + Reason: "planned predicate placements were not consumed by this translation shape", + Count: 2, + }}, }, { Driver: "neo4j", Source: "cases/a.json", @@ -58,6 +64,8 @@ func TestBuildSummaryRanksPostgresPlansAndCountsSignals(t *testing.T) { require.Contains(t, summary.PostgresOperators, Count{Name: "Seq Scan", Count: 1}) require.Contains(t, summary.Neo4jOperators, Count{Name: "ProduceResults@neo4j", Count: 1}) require.Contains(t, summary.PlannedLowerings, Count{Name: "LatePathMaterialization", Count: 1}) + require.Contains(t, summary.SkippedLowerings, Count{Name: "PredicatePlacement", Count: 1}) + require.Contains(t, summary.SkippedReasons, Count{Name: "PredicatePlacement: planned predicate placements were not consumed by this translation shape", Count: 1}) require.Contains(t, summary.Errors, PlanError{ Driver: "pg", Source: "cases/error.json", diff --git a/cmd/plancorpus/types.go b/cmd/plancorpus/types.go index 3bc336d8..9c4fa662 100644 --- a/cmd/plancorpus/types.go +++ b/cmd/plancorpus/types.go @@ -16,6 +16,7 @@ type PlanRecord struct { Neo4jOperators []string `json:"neo4j_operators,omitempty"` PlannedLowerings []string `json:"planned_lowerings,omitempty"` AppliedLowerings []string `json:"applied_lowerings,omitempty"` + SkippedLowerings []translate.SkippedLowering `json:"skipped_lowerings,omitempty"` Optimization *translate.OptimizationSummary `json:"optimization,omitempty"` Error string `json:"error,omitempty"` } diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index 1f064f50..d53874ee 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -4,7 +4,7 @@ This plan tracks optimization and rewrite work identified by running the shared ## Phase 1: Baseline And Tooling -Status: in progress +Status: completed - Keep a reproducible plan-capture workflow. - Capture PostgreSQL translated SQL, PostgreSQL `EXPLAIN`, Neo4j logical plan operator trees, and optimizer planned/applied lowerings. @@ -17,15 +17,17 @@ Status: in progress ## Phase 2: Quick Wins -Status: pending +Status: completed - Add count-store fast paths for simple count queries: - `MATCH (n) RETURN count(n)` - `MATCH ()-[r]->() RETURN count(r)` - Typed variants where kind filters map cleanly. + - Implemented as `CountStoreFastPath` lowering for exact node and directed-edge count shapes. - Audit the planned/applied `PredicatePlacement` gap. - Distinguish missing translator consumption from intentional skipped placements. - Add explicit skipped-placement reasons when a planned lowering is not applied. + - Plan-corpus summaries now report skipped lowerings and skipped-lowering reasons. ## Phase 3: Path Materialization diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index defdb948..d4d5b709 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -12,6 +12,7 @@ const ( LoweringLimitPushdown = "LimitPushdown" LoweringExpansionSuffixPushdown = "ExpansionSuffixPushdown" LoweringPredicatePlacement = "PredicatePlacement" + LoweringCountStoreFastPath = "CountStoreFastPath" ) type LoweringDecision struct { @@ -142,6 +143,22 @@ type PatternPredicatePlacementDecision struct { Mode PatternPredicatePlacementMode `json:"mode"` } +type CountStoreFastPathTarget string + +const ( + CountStoreFastPathNode CountStoreFastPathTarget = "node" + CountStoreFastPathEdge CountStoreFastPathTarget = "edge" +) + +type CountStoreFastPathDecision struct { + QueryPartIndex int `json:"query_part_index"` + ClauseIndex int `json:"clause_index"` + PatternIndex int `json:"pattern_index"` + BindingSymbol string `json:"binding_symbol,omitempty"` + Target CountStoreFastPathTarget `json:"target"` + KindSymbols []string `json:"kind_symbols,omitempty"` +} + type LoweringPlan struct { ProjectionPruning []ProjectionPruningDecision `json:"projection_pruning,omitempty"` LatePathMaterialization []LatePathMaterializationDecision `json:"late_path_materialization,omitempty"` @@ -153,6 +170,7 @@ type LoweringPlan struct { ExpansionSuffixPushdown []ExpansionSuffixPushdownDecision `json:"expansion_suffix_pushdown,omitempty"` PredicatePlacement []PredicatePlacementDecision `json:"predicate_placement,omitempty"` PatternPredicate []PatternPredicatePlacementDecision `json:"pattern_predicate_placement,omitempty"` + CountStoreFastPath []CountStoreFastPathDecision `json:"count_store_fast_path,omitempty"` } func (s LoweringPlan) Empty() bool { @@ -165,7 +183,8 @@ func (s LoweringPlan) Empty() bool { len(s.LimitPushdown) == 0 && len(s.ExpansionSuffixPushdown) == 0 && len(s.PredicatePlacement) == 0 && - len(s.PatternPredicate) == 0 + len(s.PatternPredicate) == 0 && + len(s.CountStoreFastPath) == 0 } func (s LoweringPlan) Decisions() []LoweringDecision { @@ -185,6 +204,7 @@ func (s LoweringPlan) Decisions() []LoweringDecision { add(LoweringLimitPushdown, len(s.LimitPushdown) > 0) add(LoweringExpansionSuffixPushdown, len(s.ExpansionSuffixPushdown) > 0) add(LoweringPredicatePlacement, len(s.PredicatePlacement) > 0 || len(s.PatternPredicate) > 0) + add(LoweringCountStoreFastPath, len(s.CountStoreFastPath) > 0) return decisions } diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index b40dee40..9ed9329f 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -1,6 +1,8 @@ package optimize import ( + "strings" + "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/graph" ) @@ -53,6 +55,7 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic appendPredicatePlacementDecisions(&plan, query, predicateAttachments) attachPredicatePlacementsToSuffixPushdowns(&plan) + appendCountStoreFastPathDecisions(&plan, query) return plan, nil } @@ -807,6 +810,141 @@ func attachPredicatePlacementsToSuffixPushdowns(plan *LoweringPlan) { } } +func appendCountStoreFastPathDecisions(plan *LoweringPlan, query *cypher.RegularQuery) { + if decision, ok := countStoreFastPathDecision(query); ok { + plan.CountStoreFastPath = append(plan.CountStoreFastPath, decision) + } +} + +func countStoreFastPathDecision(query *cypher.RegularQuery) (CountStoreFastPathDecision, bool) { + if query == nil || query.SingleQuery == nil || query.SingleQuery.SinglePartQuery == nil { + return CountStoreFastPathDecision{}, false + } + + queryPart := query.SingleQuery.SinglePartQuery + if len(queryPart.UpdatingClauses) > 0 || len(queryPart.ReadingClauses) != 1 { + return CountStoreFastPathDecision{}, false + } + + countArgument, ok := simpleCountProjectionArgument(queryPart.Return) + if !ok { + return CountStoreFastPathDecision{}, false + } + + readingClause := queryPart.ReadingClauses[0] + if readingClause == nil || readingClause.Match == nil { + return CountStoreFastPathDecision{}, false + } + + match := readingClause.Match + if match.Optional || match.Where != nil || len(match.Pattern) != 1 { + return CountStoreFastPathDecision{}, false + } + + patternPart := match.Pattern[0] + if patternPart == nil || patternPart.Variable != nil || patternPart.ShortestPathPattern || patternPart.AllShortestPathsPattern { + return CountStoreFastPathDecision{}, false + } + + if len(patternPart.PatternElements) == 1 { + nodePattern, ok := patternPart.PatternElements[0].AsNodePattern() + if !ok || nodePattern == nil || nodePattern.Properties != nil { + return CountStoreFastPathDecision{}, false + } + + bindingSymbol := variableSymbol(nodePattern.Variable) + if countArgument != cypher.TokenLiteralAsterisk && countArgument != bindingSymbol { + return CountStoreFastPathDecision{}, false + } + + return CountStoreFastPathDecision{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + BindingSymbol: bindingSymbol, + Target: CountStoreFastPathNode, + KindSymbols: kindSymbols(nodePattern.Kinds), + }, true + } + + if len(patternPart.PatternElements) != 3 { + return CountStoreFastPathDecision{}, false + } + + leftNode, leftOK := patternPart.PatternElements[0].AsNodePattern() + relationship, relationshipOK := patternPart.PatternElements[1].AsRelationshipPattern() + rightNode, rightOK := patternPart.PatternElements[2].AsNodePattern() + if !leftOK || !relationshipOK || !rightOK { + return CountStoreFastPathDecision{}, false + } + + if constrainedCountFastPathEndpoint(leftNode) || constrainedCountFastPathEndpoint(rightNode) || + relationship == nil || relationship.Range != nil || relationship.Properties != nil || + relationship.Direction == graph.DirectionBoth { + return CountStoreFastPathDecision{}, false + } + + bindingSymbol := variableSymbol(relationship.Variable) + if countArgument != cypher.TokenLiteralAsterisk && countArgument != bindingSymbol { + return CountStoreFastPathDecision{}, false + } + + return CountStoreFastPathDecision{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + BindingSymbol: bindingSymbol, + Target: CountStoreFastPathEdge, + KindSymbols: kindSymbols(relationship.Kinds), + }, true +} + +func simpleCountProjectionArgument(returnClause *cypher.Return) (string, bool) { + if returnClause == nil || returnClause.Projection == nil { + return "", false + } + + projection := returnClause.Projection + if projection.Distinct || projection.All || projection.Order != nil || projection.Skip != nil || projection.Limit != nil || len(projection.Items) != 1 { + return "", false + } + + projectionItem, ok := projection.Items[0].(*cypher.ProjectionItem) + if !ok || projectionItem == nil { + return "", false + } + + function, ok := projectionItem.Expression.(*cypher.FunctionInvocation) + if !ok || function == nil || !strings.EqualFold(function.Name, cypher.CountFunction) || + function.Distinct || len(function.Namespace) > 0 || len(function.Arguments) != 1 { + return "", false + } + + variable, ok := function.Arguments[0].(*cypher.Variable) + if !ok || variable == nil { + return "", false + } + + return variable.Symbol, true +} + +func constrainedCountFastPathEndpoint(nodePattern *cypher.NodePattern) bool { + return nodePattern == nil || nodePattern.Variable != nil || len(nodePattern.Kinds) > 0 || nodePattern.Properties != nil +} + +func kindSymbols(kinds graph.Kinds) []string { + if len(kinds) == 0 { + return nil + } + + symbols := make([]string, len(kinds)) + for idx, kind := range kinds { + symbols[idx] = kind.String() + } + + return symbols +} + func indexBindingTargets(query *cypher.RegularQuery) map[bindingTargetKey]TraversalStepTarget { targets := map[bindingTargetKey]TraversalStepTarget{} diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index be0a42f1..9e9e3b54 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -390,6 +390,55 @@ func TestLoweringPlanReportsExpansionSuffixPushdown(t *testing.T) { }}, plan.LoweringPlan.ExpansionSuffixPushdown) } +func TestLoweringPlanReportsCountStoreFastPath(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + query string + expected CountStoreFastPathDecision + }{ + { + name: "node count", + query: "MATCH (n:Group) RETURN count(n)", + expected: CountStoreFastPathDecision{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + BindingSymbol: "n", + Target: CountStoreFastPathNode, + KindSymbols: []string{"Group"}, + }, + }, + { + name: "edge count", + query: "MATCH ()-[r:MemberOf]->() RETURN count(r)", + expected: CountStoreFastPathDecision{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + BindingSymbol: "r", + Target: CountStoreFastPathEdge, + KindSymbols: []string{"MemberOf"}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), testCase.query) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringCountStoreFastPath}) + require.Equal(t, []CountStoreFastPathDecision{testCase.expected}, plan.LoweringPlan.CountStoreFastPath) + }) + } +} + func TestLoweringPlanPlacesBindingPredicates(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql index df1fffb5..a33ed6c6 100644 --- a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql +++ b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql @@ -45,7 +45,7 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) limit 10) select case when (s0.n0).id is null or s0.e0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; -- case: match ()-[r:EdgeKind1]->() return count(r) as the_count -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; +select count(*)::int8 as the_count from edge e0 where e0.kind_id = any (array [3]::int2[]); -- case: match ()-[r:EdgeKind1]->() return count(r) as the_count limit 1 with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0 limit 1; diff --git a/cypher/models/pgsql/translate/count_fast_path.go b/cypher/models/pgsql/translate/count_fast_path.go new file mode 100644 index 00000000..c3c27cb9 --- /dev/null +++ b/cypher/models/pgsql/translate/count_fast_path.go @@ -0,0 +1,243 @@ +package translate + +import ( + "strings" + + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" + "github.com/specterops/dawgs/graph" +) + +const ( + countStoreNodeAlias pgsql.Identifier = "n0" + countStoreEdgeAlias pgsql.Identifier = "e0" +) + +type countStoreFastPathShape struct { + Target optimize.CountStoreFastPathTarget + Alias string + Kinds graph.Kinds +} + +func (s *Translator) translateCountStoreFastPath(query *cypher.RegularQuery, plan optimize.LoweringPlan) (bool, error) { + if len(plan.CountStoreFastPath) == 0 { + return false, nil + } + + shape, ok := countStoreFastPathShapeForQuery(query) + if !ok || shape.Target != plan.CountStoreFastPath[0].Target { + return false, nil + } + + countExpression := pgsql.FunctionCall{ + Function: pgsql.FunctionCount, + Parameters: []pgsql.Expression{pgsql.Wildcard{}}, + CastType: pgsql.Int8, + } + + var countProjection pgsql.SelectItem = countExpression + if shape.Alias != "" { + countProjection = pgsql.AliasedExpression{ + Expression: countExpression, + Alias: pgsql.AsOptionalIdentifier(pgsql.Identifier(shape.Alias)), + } + } + + fromClause, whereClause, err := s.countStoreFastPathFromAndWhere(shape) + if err != nil { + return false, err + } + + s.translation.Statement = pgsql.Query{ + Body: pgsql.Select{ + Projection: pgsql.Projection{countProjection}, + From: []pgsql.FromClause{fromClause}, + Where: whereClause, + }, + } + s.recordLowering(optimize.LoweringCountStoreFastPath) + return true, nil +} + +func (s *Translator) countStoreFastPathFromAndWhere(shape countStoreFastPathShape) (pgsql.FromClause, pgsql.Expression, error) { + switch shape.Target { + case optimize.CountStoreFastPathNode: + where, err := s.countStoreNodeKindConstraint(shape.Kinds) + return pgsql.FromClause{ + Source: pgsql.TableReference{ + Name: pgsql.TableNode.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(countStoreNodeAlias), + }, + }, where, err + + case optimize.CountStoreFastPathEdge: + where, err := s.countStoreEdgeKindConstraint(shape.Kinds) + return pgsql.FromClause{ + Source: pgsql.TableReference{ + Name: pgsql.TableEdge.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(countStoreEdgeAlias), + }, + }, where, err + + default: + return pgsql.FromClause{}, nil, nil + } +} + +func (s *Translator) countStoreNodeKindConstraint(kinds graph.Kinds) (pgsql.Expression, error) { + if len(kinds) == 0 { + return nil, nil + } + + kindIDs, err := s.kindMapper.MapKinds(kinds) + if err != nil { + return nil, err + } + + kindIDsLiteral, err := pgsql.AsLiteral(kindIDs) + if err != nil { + return nil, err + } + + return pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{countStoreNodeAlias, pgsql.ColumnKindIDs}, + pgsql.OperatorPGArrayLHSContainsRHS, + kindIDsLiteral, + ), nil +} + +func (s *Translator) countStoreEdgeKindConstraint(kinds graph.Kinds) (pgsql.Expression, error) { + if len(kinds) == 0 { + return nil, nil + } + + kindIDs, err := s.kindMapper.MapKinds(kinds) + if err != nil { + return nil, err + } + + return pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{countStoreEdgeAlias, pgsql.ColumnKindID}, + pgsql.OperatorEquals, + pgsql.NewAnyExpressionHinted(pgsql.NewLiteral(kindIDs, pgsql.Int2Array)), + ), nil +} + +func countStoreFastPathShapeForQuery(query *cypher.RegularQuery) (countStoreFastPathShape, bool) { + if query == nil || query.SingleQuery == nil || query.SingleQuery.SinglePartQuery == nil { + return countStoreFastPathShape{}, false + } + + queryPart := query.SingleQuery.SinglePartQuery + if len(queryPart.UpdatingClauses) > 0 || len(queryPart.ReadingClauses) != 1 { + return countStoreFastPathShape{}, false + } + + countArgument, alias, ok := simpleCountProjection(queryPart.Return) + if !ok { + return countStoreFastPathShape{}, false + } + + readingClause := queryPart.ReadingClauses[0] + if readingClause == nil || readingClause.Match == nil { + return countStoreFastPathShape{}, false + } + + match := readingClause.Match + if match.Optional || match.Where != nil || len(match.Pattern) != 1 { + return countStoreFastPathShape{}, false + } + + patternPart := match.Pattern[0] + if patternPart == nil || patternPart.Variable != nil || patternPart.ShortestPathPattern || patternPart.AllShortestPathsPattern { + return countStoreFastPathShape{}, false + } + + if len(patternPart.PatternElements) == 1 { + nodePattern, ok := patternPart.PatternElements[0].AsNodePattern() + if !ok || nodePattern == nil || nodePattern.Properties != nil { + return countStoreFastPathShape{}, false + } + + bindingSymbol := countStoreVariableSymbol(nodePattern.Variable) + if countArgument != cypher.TokenLiteralAsterisk && countArgument != bindingSymbol { + return countStoreFastPathShape{}, false + } + + return countStoreFastPathShape{ + Target: optimize.CountStoreFastPathNode, + Alias: alias, + Kinds: nodePattern.Kinds, + }, true + } + + if len(patternPart.PatternElements) != 3 { + return countStoreFastPathShape{}, false + } + + leftNode, leftOK := patternPart.PatternElements[0].AsNodePattern() + relationship, relationshipOK := patternPart.PatternElements[1].AsRelationshipPattern() + rightNode, rightOK := patternPart.PatternElements[2].AsNodePattern() + if !leftOK || !relationshipOK || !rightOK { + return countStoreFastPathShape{}, false + } + + if constrainedCountStoreEndpoint(leftNode) || constrainedCountStoreEndpoint(rightNode) || + relationship == nil || relationship.Range != nil || relationship.Properties != nil || + relationship.Direction == graph.DirectionBoth { + return countStoreFastPathShape{}, false + } + + bindingSymbol := countStoreVariableSymbol(relationship.Variable) + if countArgument != cypher.TokenLiteralAsterisk && countArgument != bindingSymbol { + return countStoreFastPathShape{}, false + } + + return countStoreFastPathShape{ + Target: optimize.CountStoreFastPathEdge, + Alias: alias, + Kinds: relationship.Kinds, + }, true +} + +func simpleCountProjection(returnClause *cypher.Return) (string, string, bool) { + if returnClause == nil || returnClause.Projection == nil { + return "", "", false + } + + projection := returnClause.Projection + if projection.Distinct || projection.All || projection.Order != nil || projection.Skip != nil || projection.Limit != nil || len(projection.Items) != 1 { + return "", "", false + } + + projectionItem, ok := projection.Items[0].(*cypher.ProjectionItem) + if !ok || projectionItem == nil { + return "", "", false + } + + function, ok := projectionItem.Expression.(*cypher.FunctionInvocation) + if !ok || function == nil || !strings.EqualFold(function.Name, cypher.CountFunction) || + function.Distinct || len(function.Namespace) > 0 || len(function.Arguments) != 1 { + return "", "", false + } + + variable, ok := function.Arguments[0].(*cypher.Variable) + if !ok || variable == nil { + return "", "", false + } + + return variable.Symbol, countStoreVariableSymbol(projectionItem.Alias), true +} + +func constrainedCountStoreEndpoint(nodePattern *cypher.NodePattern) bool { + return nodePattern == nil || nodePattern.Variable != nil || len(nodePattern.Kinds) > 0 || nodePattern.Properties != nil +} + +func countStoreVariableSymbol(variable *cypher.Variable) string { + if variable == nil { + return "" + } + + return variable.Symbol +} diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 3ceb4824..0d77f5f4 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/drivers/pg/pgutil" "github.com/specterops/dawgs/graph" "github.com/stretchr/testify/require" @@ -113,6 +114,19 @@ func requireNoPlannedOptimizationLowering(t *testing.T, summary OptimizationSumm } } +func requireSkippedOptimizationLowering(t *testing.T, summary OptimizationSummary, name string, reason string) { + t.Helper() + + for _, lowering := range summary.SkippedLowerings { + if lowering.Name == name { + require.Equal(t, reason, lowering.Reason) + return + } + } + + require.Failf(t, "missing skipped optimization lowering", "expected skipped lowering %q in %#v", name, summary.SkippedLowerings) +} + func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { t.Helper() @@ -124,6 +138,44 @@ func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { } } +func TestOptimizerSafetyCountStoreFastPathUsesBaseNodeCount(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, `MATCH (n) RETURN count(n)`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + require.Empty(t, translation.Optimization.SkippedLowerings) + require.Equal(t, "select count(*)::int8 from node n0;", strings.Join(strings.Fields(formattedQuery), " ")) +} + +func TestOptimizerSafetyCountStoreFastPathKeepsKindConstraintAndAlias(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, `MATCH (n:Group) RETURN count(n) AS total`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + require.Equal(t, "select count(*)::int8 as total from node n0 where n0.kind_ids operator (pg_catalog.@>) array [8]::int2[];", strings.Join(strings.Fields(formattedQuery), " ")) +} + +func TestOptimizerSafetyCountStoreFastPathUsesBaseEdgeCount(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, `MATCH ()-[r:MemberOf]->() RETURN count(r)`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireSkippedOptimizationLowering(t, translation.Optimization, optimize.LoweringProjectionPruning, "superseded by CountStoreFastPath") + require.Equal(t, "select count(*)::int8 from edge e0 where e0.kind_id = any (array [10]::int2[]);", strings.Join(strings.Fields(formattedQuery), " ")) +} + func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 7d92fc21..5838ca07 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -599,9 +599,16 @@ type OptimizationSummary struct { PredicateAttachments []optimize.PredicateAttachment `json:"predicate_attachments,omitempty"` PlannedLowerings []optimize.LoweringDecision `json:"planned_lowerings,omitempty"` Lowerings []optimize.LoweringDecision `json:"lowerings,omitempty"` + SkippedLowerings []SkippedLowering `json:"skipped_lowerings,omitempty"` LoweringPlan *optimize.LoweringPlan `json:"lowering_plan,omitempty"` } +type SkippedLowering struct { + Name string `json:"name"` + Reason string `json:"reason"` + Count int `json:"count,omitempty"` +} + func (s *Translator) recordLowering(name string) { for _, lowering := range s.translation.Optimization.Lowerings { if lowering.Name == name { @@ -612,6 +619,61 @@ func (s *Translator) recordLowering(name string) { s.translation.Optimization.Lowerings = append(s.translation.Optimization.Lowerings, optimize.LoweringDecision{Name: name}) } +func (s *Translator) recordSkippedLowerings() { + if s.translation.Optimization.LoweringPlan == nil { + return + } + + applied := map[string]struct{}{} + for _, lowering := range s.translation.Optimization.Lowerings { + applied[lowering.Name] = struct{}{} + } + + for _, planned := range plannedLoweringCounts(*s.translation.Optimization.LoweringPlan) { + if planned.Count == 0 { + continue + } + + if _, wasApplied := applied[planned.Name]; wasApplied { + continue + } + + s.translation.Optimization.SkippedLowerings = append(s.translation.Optimization.SkippedLowerings, SkippedLowering{ + Name: planned.Name, + Reason: skippedLoweringReason(planned.Name, applied), + Count: planned.Count, + }) + } +} + +func plannedLoweringCounts(plan optimize.LoweringPlan) []SkippedLowering { + return []SkippedLowering{ + {Name: optimize.LoweringProjectionPruning, Count: len(plan.ProjectionPruning)}, + {Name: optimize.LoweringLatePathMaterialization, Count: len(plan.LatePathMaterialization)}, + {Name: optimize.LoweringExpandIntoDetection, Count: len(plan.ExpandInto)}, + {Name: optimize.LoweringTraversalDirection, Count: len(plan.TraversalDirection)}, + {Name: optimize.LoweringShortestPathStrategy, Count: len(plan.ShortestPathStrategy)}, + {Name: optimize.LoweringShortestPathFilter, Count: len(plan.ShortestPathFilter)}, + {Name: optimize.LoweringLimitPushdown, Count: len(plan.LimitPushdown)}, + {Name: optimize.LoweringExpansionSuffixPushdown, Count: len(plan.ExpansionSuffixPushdown)}, + {Name: optimize.LoweringPredicatePlacement, Count: len(plan.PredicatePlacement) + len(plan.PatternPredicate)}, + {Name: optimize.LoweringCountStoreFastPath, Count: len(plan.CountStoreFastPath)}, + } +} + +func skippedLoweringReason(name string, applied map[string]struct{}) string { + if _, countFastPathApplied := applied[optimize.LoweringCountStoreFastPath]; countFastPathApplied && name != optimize.LoweringCountStoreFastPath { + return "superseded by CountStoreFastPath" + } + + switch name { + case optimize.LoweringPredicatePlacement: + return "planned predicate placements were not consumed by this translation shape" + default: + return "planned lowering did not change the emitted SQL" + } +} + func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) (Result, error) { optimizedPlan, err := optimize.Optimize(cypherQuery) if err != nil { @@ -628,10 +690,18 @@ func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper translator.translation.Optimization.PlannedLowerings = loweringPlan.Decisions() } + if translated, err := translator.translateCountStoreFastPath(optimizedPlan.Query, optimizedPlan.LoweringPlan); err != nil { + return Result{}, err + } else if translated { + translator.recordSkippedLowerings() + return translator.translation, nil + } + if err := walk.Cypher(optimizedPlan.Query, translator); err != nil { return Result{}, err } + translator.recordSkippedLowerings() return translator.translation, nil } From efd22b861e67a2c720ff3c55edd667de52294fc2 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 16:34:14 -0700 Subject: [PATCH 051/114] feat(pgsql): stage repeated path projection components --- .../pgsql/optimize/OPTIMIZATION_PLAN.md | 3 +- .../models/pgsql/translate/function_test.go | 35 ++++ cypher/models/pgsql/translate/projection.go | 194 +++++++++++++++--- 3 files changed, 207 insertions(+), 25 deletions(-) diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index d53874ee..4b0e5cf7 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -31,11 +31,12 @@ Status: completed ## Phase 3: Path Materialization -Status: pending +Status: completed - Share path materialization for repeated path functions. - Target `nodes(p)`, `relationships(p)`, `size(relationships(p))`, `startNode`, `endNode`, and `type`. - Avoid repeated `SubPlan` and `Function Scan on unnest` work per path binding. + - Materialize unprojected paths once through a lateral stage when final projections return a path and its components, or repeat node-bearing component expressions. - Expand late path materialization coverage. - Ensure paths are built only when needed for projection, filtering, or mutation semantics. diff --git a/cypher/models/pgsql/translate/function_test.go b/cypher/models/pgsql/translate/function_test.go index e8f421bd..64926916 100644 --- a/cypher/models/pgsql/translate/function_test.go +++ b/cypher/models/pgsql/translate/function_test.go @@ -88,6 +88,41 @@ func TestTailPredicateStagesPathComponentExpression(t *testing.T) { require.Contains(t, formatted, ".nodes") } +func TestProjectionStagesPathBeforeReadingComponents(t *testing.T) { + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), `MATCH p = ()-[*1..]->() RETURN p, nodes(p), relationships(p)`) + 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, "lateral (select") + require.Equal(t, 1, strings.Count(formatted, "ordered_edges_to_path"), formatted) + require.Contains(t, formatted, ".nodes") + require.Contains(t, formatted, ".edges") +} + +func TestProjectionStagesRepeatedPathComponents(t *testing.T) { + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), `MATCH p = ()-[*1..]->() RETURN size(relationships(p)), nodes(p), relationships(p)`) + 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, "lateral (select") + require.Equal(t, 1, strings.Count(formatted, "ordered_edges_to_path"), formatted) + require.Equal(t, 1, strings.Count(formatted, "from unnest"), formatted) + require.Contains(t, formatted, ".nodes") + require.Contains(t, formatted, ".edges") +} + func TestPrepareCollectExpressionMissingBindingErrorNamesArgument(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/projection.go b/cypher/models/pgsql/translate/projection.go index 18dabaee..af3eff99 100644 --- a/cypher/models/pgsql/translate/projection.go +++ b/cypher/models/pgsql/translate/projection.go @@ -1093,49 +1093,190 @@ func rewriteOrderByProjectionAlias(orderBy *pgsql.OrderBy, aliases map[pgsql.Ide } } -func tailPathCompositeStageBindings(scope *Scope, expression pgsql.Expression) ([]*BoundIdentifier, error) { - if expression == nil { - return nil, nil +type pathCompositeReferenceCount struct { + binding *BoundIdentifier + full int + nodes int + edges int +} + +func (s pathCompositeReferenceCount) componentReferences() int { + return s.nodes + s.edges +} + +func (s pathCompositeReferenceCount) totalReferences() int { + return s.full + s.componentReferences() +} + +func pathCompositeBinding(scope *Scope, identifier pgsql.Identifier) (*BoundIdentifier, bool) { + binding, bound := scope.Lookup(identifier) + if !bound { + binding, bound = scope.AliasedLookup(identifier) } + if !bound || binding.DataType != pgsql.PathComposite || binding.LastProjection != nil { + return nil, false + } + + return binding, true +} + +func ensurePathCompositeReferenceCount( + counts map[pgsql.Identifier]*pathCompositeReferenceCount, + orderedCounts *[]*pathCompositeReferenceCount, + binding *BoundIdentifier, +) *pathCompositeReferenceCount { + if count, seen := counts[binding.Identifier]; seen { + return count + } + + count := &pathCompositeReferenceCount{ + binding: binding, + } + + counts[binding.Identifier] = count + *orderedCounts = append(*orderedCounts, count) + + return count +} + +func countPathCompositeComponents(scope *Scope, expressions ...pgsql.Expression) ([]*pathCompositeReferenceCount, error) { var ( - bindings = make([]*BoundIdentifier, 0) - seen = map[pgsql.Identifier]struct{}{} + counts = map[pgsql.Identifier]*pathCompositeReferenceCount{} + orderedCounts []*pathCompositeReferenceCount ) - if err := walk.PgSQL(expression, walk.NewSimpleVisitor[pgsql.SyntaxNode](func(node pgsql.SyntaxNode, _ walk.VisitorHandler) { - reference, isRowColumnReference := node.(pgsql.RowColumnReference) - if !isRowColumnReference || reference.Column != pgsql.ColumnNodes { - return + for _, expression := range expressions { + if expression == nil { + continue } - identifier, isIdentifier := unwrapParenthetical(reference.Identifier).(pgsql.Identifier) + if err := walk.PgSQL(expression, walk.NewSimpleVisitor[pgsql.SyntaxNode](func(node pgsql.SyntaxNode, _ walk.VisitorHandler) { + reference, isRowColumnReference := node.(pgsql.RowColumnReference) + if !isRowColumnReference || (reference.Column != pgsql.ColumnNodes && reference.Column != pgsql.ColumnEdges) { + return + } + + identifier, isIdentifier := unwrapParenthetical(reference.Identifier).(pgsql.Identifier) + if !isIdentifier { + return + } + + binding, bound := pathCompositeBinding(scope, identifier) + if !bound { + return + } + + count := ensurePathCompositeReferenceCount(counts, &orderedCounts, binding) + switch reference.Column { + case pgsql.ColumnNodes: + count.nodes += 1 + case pgsql.ColumnEdges: + count.edges += 1 + } + })); err != nil { + return nil, err + } + } + + return orderedCounts, nil +} + +func countPathCompositeProjectionReferences(scope *Scope, projections []*Projection) ([]*pathCompositeReferenceCount, error) { + var ( + counts = map[pgsql.Identifier]*pathCompositeReferenceCount{} + orderedCounts []*pathCompositeReferenceCount + expressions = make([]pgsql.Expression, 0, len(projections)) + ) + + for _, projection := range projections { + expressions = append(expressions, projection.SelectItem) + + identifier, isIdentifier := projection.SelectItem.(pgsql.Identifier) if !isIdentifier { - return + continue } - binding, bound := scope.Lookup(identifier) + binding, bound := pathCompositeBinding(scope, identifier) if !bound { - binding, bound = scope.AliasedLookup(identifier) - } - if !bound || binding.DataType != pgsql.PathComposite || binding.LastProjection != nil { - return + continue } - if _, alreadySeen := seen[binding.Identifier]; alreadySeen { - return + ensurePathCompositeReferenceCount(counts, &orderedCounts, binding).full += 1 + } + + componentCounts, err := countPathCompositeComponents(scope, expressions...) + if err != nil { + return nil, err + } + + for _, componentCount := range componentCounts { + count := ensurePathCompositeReferenceCount(counts, &orderedCounts, componentCount.binding) + count.nodes += componentCount.nodes + count.edges += componentCount.edges + } + + return orderedCounts, nil +} + +func tailPathCompositeStageBindings(scope *Scope, expression pgsql.Expression) ([]*BoundIdentifier, error) { + counts, err := countPathCompositeComponents(scope, expression) + if err != nil { + return nil, err + } + + bindings := make([]*BoundIdentifier, 0, len(counts)) + for _, count := range counts { + if count.nodes > 0 { + bindings = append(bindings, count.binding) } + } - seen[binding.Identifier] = struct{}{} - bindings = append(bindings, binding) - })); err != nil { + return bindings, nil +} + +func projectionPathCompositeStageBindings(scope *Scope, projections []*Projection) ([]*BoundIdentifier, error) { + counts, err := countPathCompositeProjectionReferences(scope, projections) + if err != nil { return nil, err } + bindings := make([]*BoundIdentifier, 0, len(counts)) + for _, count := range counts { + switch { + case count.full > 0 && count.totalReferences() > count.full: + bindings = append(bindings, count.binding) + case count.full > 1: + bindings = append(bindings, count.binding) + case count.nodes > 0 && count.componentReferences() > 1: + bindings = append(bindings, count.binding) + } + } + return bindings, nil } -func (s *Translator) stageTailPathCompositeBindings(fromClauses []pgsql.FromClause, bindings []*BoundIdentifier) ([]pgsql.FromClause, error) { +func mergePathCompositeStageBindings(bindingSets ...[]*BoundIdentifier) []*BoundIdentifier { + var ( + merged = make([]*BoundIdentifier, 0) + seen = map[pgsql.Identifier]struct{}{} + ) + + for _, bindings := range bindingSets { + for _, binding := range bindings { + if _, alreadySeen := seen[binding.Identifier]; alreadySeen { + continue + } + + seen[binding.Identifier] = struct{}{} + merged = append(merged, binding) + } + } + + return merged +} + +func (s *Translator) stagePathCompositeBindings(fromClauses []pgsql.FromClause, bindings []*BoundIdentifier) ([]pgsql.FromClause, error) { for _, binding := range bindings { stageBinding, err := s.scope.DefineNew(pgsql.Scope) if err != nil { @@ -1187,9 +1328,14 @@ func (s *Translator) buildTailProjection() error { if projectionConstraint, err := s.treeTranslator.ConsumeAllConstraints(); err != nil { return err - } else if stagedBindings, err := tailPathCompositeStageBindings(s.scope, projectionConstraint.Expression); err != nil { + } else if constraintStagedBindings, err := tailPathCompositeStageBindings(s.scope, projectionConstraint.Expression); err != nil { + return err + } else if projectionStagedBindings, err := projectionPathCompositeStageBindings(s.scope, currentPart.projections.Items); err != nil { return err - } else if stagedFromClauses, err := s.stageTailPathCompositeBindings(singlePartQuerySelect.From, stagedBindings); err != nil { + } else if stagedFromClauses, err := s.stagePathCompositeBindings( + singlePartQuerySelect.From, + mergePathCompositeStageBindings(constraintStagedBindings, projectionStagedBindings), + ); err != nil { return err } else if projection, err := buildExternalProjection(s.scope, currentPart.projections.Items); err != nil { return err From ffceecc14abba0632e44e5eb8dfc2eb83ab549d1 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 16:42:26 -0700 Subject: [PATCH 052/114] feat(pgsql): plan traversal flips for endpoint predicates --- .../pgsql/optimize/OPTIMIZATION_PLAN.md | 3 +- cypher/models/pgsql/optimize/lowering_plan.go | 17 ++++++-- .../models/pgsql/optimize/optimizer_test.go | 41 +++++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 18 ++++++++ 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index 4b0e5cf7..c99ef2a3 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -42,7 +42,7 @@ Status: completed ## Phase 4: Traversal And Recursive CTEs -Status: pending +Status: completed - Push predicates into recursive traversal anchors and steps where semantics allow. - Endpoint kind/property predicates. @@ -53,6 +53,7 @@ Status: pending - Labels/kinds. - Equality predicates. - Finite relationship type sets. + - Plan direction flips for right-endpoint binding predicates from `WHERE`, not only inline node constraints. - Broaden limit pushdown for variable-length path queries when ordering and distinct semantics permit early termination. ## Phase 5: Suffix And Shared Endpoint Rewrites diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 9ed9329f..d105e7ec 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -16,6 +16,7 @@ type sourceTraversalStep struct { const ( traversalDirectionReasonRightBound = "right_bound" traversalDirectionReasonRightConstrained = "right_constrained" + traversalDirectionReasonRightPredicate = "right_predicate" shortestPathStrategyReasonBoundEndpointPairs = "bound_endpoint_pairs" shortestPathStrategyReasonEndpointPredicates = "endpoint_predicates" @@ -352,6 +353,7 @@ func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, r step, declaredEndpoints[stepIndex], referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.LeftNode.Variable)), + referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.RightNode.Variable)), ); shouldFlip { plan.TraversalDirection = append(plan.TraversalDirection, decision) } @@ -386,6 +388,7 @@ func traversalDirectionDecisionForStep( step sourceTraversalStep, declaredEndpoints declaredStepEndpoints, leftHasAttachedPredicate bool, + rightHasAttachedPredicate bool, ) (TraversalDirectionDecision, bool) { if leftEndpointBoundForStep(stepIndex, step, declaredEndpoints) { return TraversalDirectionDecision{}, false @@ -406,11 +409,19 @@ func traversalDirectionDecisionForStep( } } - if nodePatternHasConstraints(step.RightNode) && !nodePatternHasConstraints(step.LeftNode) && !leftHasAttachedPredicate { + leftConstrained := nodePatternHasConstraints(step.LeftNode) || leftHasAttachedPredicate + rightConstrained := nodePatternHasConstraints(step.RightNode) || rightHasAttachedPredicate + + if rightConstrained && !leftConstrained { + reason := traversalDirectionReasonRightConstrained + if !nodePatternHasConstraints(step.RightNode) && rightHasAttachedPredicate { + reason = traversalDirectionReasonRightPredicate + } + return TraversalDirectionDecision{ Target: target, Flip: true, - Reason: traversalDirectionReasonRightConstrained, + Reason: reason, }, true } @@ -732,7 +743,7 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i } func expansionStepMayFlipForConstraintBalance(stepIndex int, step sourceTraversalStep, declaredEndpoints declaredStepEndpoints) bool { - _, mayFlip := traversalDirectionDecisionForStep(TraversalStepTarget{}, stepIndex, step, declaredEndpoints, false) + _, mayFlip := traversalDirectionDecisionForStep(TraversalStepTarget{}, stepIndex, step, declaredEndpoints, false, false) return mayFlip } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 9e9e3b54..971d00bc 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -589,6 +589,47 @@ func TestLoweringPlanSkipsTraversalDirectionWhenLeftEndpointHasRegionPredicate(t require.Empty(t, plan.LoweringPlan.TraversalDirection) } +func TestLoweringPlanReportsTraversalDirectionForRightEndpointPredicate(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n)-[:MemberOf*1..]->(ca) + WHERE ca.name = 'target' + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Flip: true, + Reason: traversalDirectionReasonRightPredicate, + }}, plan.LoweringPlan.TraversalDirection) +} + +func TestLoweringPlanSkipsSuffixPushdownAfterRightEndpointPredicateDirectionFlip(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = (n)-[:MemberOf*1..]->(ca)-[:TrustedForNTAuth]->(d:Domain) + WHERE ca.name = 'target' + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Empty(t, plan.LoweringPlan.ExpansionSuffixPushdown) +} + func TestLoweringPlanReportsShortestPathStrategyForEndpointPredicates(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 0d77f5f4..087b6fd5 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -414,6 +414,24 @@ RETURN p requireNoOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") } +func TestOptimizerSafetyTraversalDirectionUsesRightEndpointPredicate(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH p = (n)-[:MemberOf*1..]->(ca) +WHERE ca.name = 'target' +RETURN p + `) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + require.Contains(t, normalizedQuery, "where (((n1.properties -> 'name'))::jsonb = to_jsonb(('target')::text)::jsonb)") + require.Contains(t, normalizedQuery, "join edge e0 on e0.end_id = s1_seed.root_id") +} + func TestOptimizerSafetyShortestPathStrategyUsesPlannedBidirectionalSearch(t *testing.T) { t.Parallel() From 8ff5420219718016812720e29cf4ff488bd85add Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 16:52:58 -0700 Subject: [PATCH 053/114] feat(pgsql): extend suffix pushdown to constrained bound endpoints --- .../pgsql/optimize/OPTIMIZATION_PLAN.md | 4 ++- cypher/models/pgsql/optimize/lowering_plan.go | 27 +++---------------- .../models/pgsql/optimize/optimizer_test.go | 26 ++++++++++++++++++ cypher/models/pgsql/translate/expansion.go | 5 +--- .../pgsql/translate/optimizer_safety_test.go | 22 +++++++++++++++ 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index c99ef2a3..ef983f77 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -58,9 +58,11 @@ Status: completed ## Phase 5: Suffix And Shared Endpoint Rewrites -Status: pending +Status: completed - Improve expansion suffix pushdown for fixed suffixes after variable-length traversals. + - Include fixed suffix steps that terminate at already-bound endpoints with inline node constraints. + - Preserve bound-endpoint constraints in the pushed terminal satisfaction check when present. - Improve `ExpandInto` and shared endpoint rewrites for ADCS-style fanout patterns. - Constrain earlier using bound endpoint semi-joins or correlated expansion lowering where valid. diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index d105e7ec..72036350 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -708,7 +708,6 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i for patternIndex, patternPart := range match.Pattern { steps := traversalStepsForPattern(patternPart) - declaredBeforeRightNode := declaredSymbolsBeforeRightNodes(declaredSymbols, steps) declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) for stepIndex, step := range steps { @@ -725,7 +724,7 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i continue } - if suffixLength := expansionSuffixPushdownLength(steps[stepIndex+1:], declaredBeforeRightNode[stepIndex+1:]); suffixLength > 0 { + if suffixLength := expansionSuffixPushdownLength(steps[stepIndex+1:]); suffixLength > 0 { plan.ExpansionSuffixPushdown = append(plan.ExpansionSuffixPushdown, ExpansionSuffixPushdownDecision{ Target: target, SuffixLength: suffixLength, @@ -1019,40 +1018,20 @@ func setBindingTarget(targets map[bindingTargetKey]TraversalStepTarget, queryPar } } -func expansionSuffixPushdownLength(suffixSteps []sourceTraversalStep, declaredBeforeRightNode []map[string]struct{}) int { +func expansionSuffixPushdownLength(suffixSteps []sourceTraversalStep) int { var suffixLength int - for idx, step := range suffixSteps { + for _, step := range suffixSteps { if step.Relationship.Range != nil || step.Relationship.Direction == graph.DirectionBoth { break } - if nodeSymbol := variableSymbol(step.RightNode.Variable); nodeSymbol != "" { - if _, bound := declaredBeforeRightNode[idx][nodeSymbol]; bound && nodePatternHasConstraints(step.RightNode) { - break - } - } - suffixLength++ } return suffixLength } -func declaredSymbolsBeforeRightNodes(initial map[string]struct{}, steps []sourceTraversalStep) []map[string]struct{} { - declared := copyStringSet(initial) - declaredBeforeRightNode := make([]map[string]struct{}, len(steps)) - - for idx, step := range steps { - addSymbol(declared, variableSymbol(step.LeftNode.Variable)) - addSymbol(declared, variableSymbol(step.Relationship.Variable)) - declaredBeforeRightNode[idx] = copyStringSet(declared) - addSymbol(declared, variableSymbol(step.RightNode.Variable)) - } - - return declaredBeforeRightNode -} - func declareMatchSymbols(declared map[string]struct{}, match *cypher.Match) { if match == nil { return diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 971d00bc..111e17a7 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -390,6 +390,32 @@ func TestLoweringPlanReportsExpansionSuffixPushdown(t *testing.T) { }}, plan.LoweringPlan.ExpansionSuffixPushdown) } +func TestLoweringPlanIncludesConstrainedBoundEndpointInExpansionSuffix(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (ca) + MATCH p = (n:Group)-[:MemberOf*0..]->(m)-[:Enroll]->(ct:CertTemplate)-[:PublishedTo]->(ca:EnterpriseCA) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringExpansionSuffixPushdown}) + require.Contains(t, plan.LoweringPlan.ExpansionSuffixPushdown, ExpansionSuffixPushdownDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + SuffixLength: 2, + SuffixStartStep: 1, + SuffixEndStep: 2, + }) +} + func TestLoweringPlanReportsCountStoreFastPath(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index c5e78360..df524e6a 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2844,15 +2844,12 @@ func expansionSuffixTerminalSatisfaction(currentStep *TraversalStep, suffixSteps } if step.RightNodeBound { - if step.RightNodeConstraints != nil { - return nil, false - } - boundRightNodeID, hasBoundRightNodeID := suffixBoundNodeIDReference(currentStep, step.RightNode) if !hasBoundRightNodeID { return nil, false } + where = pgsql.OptionalAnd(where, step.RightNodeConstraints) where = pgsql.OptionalAnd(where, pgd.Equals(rightEndpoint, boundRightNodeID)) previousID = boundRightNodeID } else { diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 087b6fd5..4fd7340a 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -590,6 +590,28 @@ RETURN p ) } +func TestOptimizerSafetyExpansionTerminalPushdownIncludesConstrainedBoundEndpoint(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (ca) +MATCH p = (n:Group)-[:MemberOf*0..]->(m)-[:Enroll]->(ct:CertTemplate)-[:PublishedTo]->(ca:EnterpriseCA) +RETURN p +`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") + requireOptimizationLowering(t, translation.Optimization, "ExpansionSuffixPushdown") + requireSQLContainsInOrder(t, normalizedQuery, + "exists (select 1 from edge e1 join node n3", + "join edge e2 on n3.id = e2.start_id", + "e2.end_id = (s0.n0).id", + ) + require.Contains(t, normalizedQuery, "(s0.n0).kind_ids operator (pg_catalog.@>)") +} + func TestOptimizerSafetyExpansionTerminalPushdownForBoundDomainSuffix(t *testing.T) { t.Parallel() From 9807c3f3c3f5022ce6cf7178e332dc430b7a2a91 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:08:15 -0700 Subject: [PATCH 054/114] test(integration): stabilize corpus validation --- cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md | 5 ++++- integration/cypher_template_test.go | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index ef983f77..0fe122db 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -68,13 +68,16 @@ Status: completed ## Phase 6: Validation -Status: pending +Status: completed - Add focused regression tests per optimization. - Optimizer/lowering selection tests. - SQL shape translation tests. - Backend-equivalent integration tests. + - Template corpus setup now clears stale graph data before rollback-only fixture cases, keeping repeated PostgreSQL and Neo4j validation runs deterministic. - Benchmark after each workstream. - Run unit tests. - Run backend-specific integration tests. - Run plan capture and compare summary deltas. + - `quality_backend` passes against `postgres://postgres:bhe4eva@localhost/bhe` and `neo4j://neo4j:neo4jj@localhost:7687`. + - Plan corpus capture records 396 PostgreSQL plans and 396 Neo4j plans; remaining capture errors are expected invalid-query cases surfaced by both systems or Neo4j-specific parameter-map syntax rejection. diff --git a/integration/cypher_template_test.go b/integration/cypher_template_test.go index 7dde0d51..63a90655 100644 --- a/integration/cypher_template_test.go +++ b/integration/cypher_template_test.go @@ -73,6 +73,7 @@ func TestCypherTemplates(t *testing.T) { nodeKinds, edgeKinds := cypherTemplateKinds(templateFiles) db, ctx := SetupDBWithKindsNoGraphCleanup(t, nodeKinds, edgeKinds) + ClearGraph(t, db, ctx) for _, templateFile := range templateFiles { fileName := strings.TrimSuffix(filepath.Base(templateFile.path), filepath.Ext(templateFile.path)) From 9d0b3cb56c33e97877928f7a80fcb99268d8cbeb Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:20:22 -0700 Subject: [PATCH 055/114] feat(pgsql): record predicate placement consumption --- .../pgsql/optimize/OPTIMIZATION_PLAN.md | 9 ++++ cypher/models/pgsql/translate/expansion.go | 2 + .../pgsql/translate/optimizer_safety_test.go | 53 +++++++++++++++++++ .../pgsql/translate/predicate_placement.go | 46 ++++++++++++++++ cypher/models/pgsql/translate/translator.go | 6 +++ cypher/models/pgsql/translate/traversal.go | 4 ++ 6 files changed, 120 insertions(+) create mode 100644 cypher/models/pgsql/translate/predicate_placement.go diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index 0fe122db..fc3324ce 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -81,3 +81,12 @@ Status: completed - Run plan capture and compare summary deltas. - `quality_backend` passes against `postgres://postgres:bhe4eva@localhost/bhe` and `neo4j://neo4j:neo4jj@localhost:7687`. - Plan corpus capture records 396 PostgreSQL plans and 396 Neo4j plans; remaining capture errors are expected invalid-query cases surfaced by both systems or Neo4j-specific parameter-map syntax rejection. + +## Phase 7: Predicate Placement Accounting + +Status: completed + +- Record planned binding-scope predicate placements when traversal constraint consumption actually pushes the matching predicate into a fixed traversal step, expansion seed, expansion edge, or expansion terminal constraint. +- Keep skipped-lowering reports focused on predicates that were not consumed by the emitted translation shape, instead of marking already-pushed traversal predicates as skipped. +- Add SQL-shape regression tests for fixed traversal and expansion-root predicate consumption. +- Refreshed plan-corpus capture applies `PredicatePlacement` in 56 of 71 planned PostgreSQL cases, reducing skipped predicate placements from 65 to 15. diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index df524e6a..5d867a7c 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -3158,6 +3158,8 @@ func (s *Translator) translateExpansionConstraints(part *PatternPart, stepIndex return err } + s.recordPredicatePlacementConsumption(part, stepIndex, step, constraints) + // Left node if leftNodeJoinCondition, err := leftNodeTraversalStepConstraint(step); err != nil { return err diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 4fd7340a..3cb8b715 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -127,6 +127,14 @@ func requireSkippedOptimizationLowering(t *testing.T, summary OptimizationSummar require.Failf(t, "missing skipped optimization lowering", "expected skipped lowering %q in %#v", name, summary.SkippedLowerings) } +func requireNoSkippedOptimizationLowering(t *testing.T, summary OptimizationSummary, name string) { + t.Helper() + + for _, lowering := range summary.SkippedLowerings { + require.NotEqualf(t, name, lowering.Name, "unexpected skipped lowering %q in %#v", name, summary.SkippedLowerings) + } +} + func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { t.Helper() @@ -363,6 +371,51 @@ RETURN p ) } +func TestOptimizerSafetyPredicatePlacementRecordsExpansionRootConstraint(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH p = (src:Group)-[:MemberOf*1..]->(mid)-[:Enroll]->(ca:EnterpriseCA) +WHERE src.name = 'source' +RETURN p +`) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringPredicatePlacement) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringPredicatePlacement) + requireNoSkippedOptimizationLowering(t, translation.Optimization, optimize.LoweringPredicatePlacement) + requireSQLContainsInOrder(t, normalizedQuery, + "select n0.id as root_id from node n0 where", + "properties -> 'name'", + ) +} + +func TestOptimizerSafetyPredicatePlacementRecordsFixedTraversalConstraint(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (src:Group)-[:MemberOf]->(dst) +WHERE src.name = 'source' +RETURN dst +`) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringPredicatePlacement) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringPredicatePlacement) + requireNoSkippedOptimizationLowering(t, translation.Optimization, optimize.LoweringPredicatePlacement) + requireSQLContainsInOrder(t, normalizedQuery, + "join node n0 on", + "properties -> 'name'", + "join node n1", + ) +} + func TestOptimizerSafetyPatternPredicateExistencePlacementIsPlanned(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/predicate_placement.go b/cypher/models/pgsql/translate/predicate_placement.go new file mode 100644 index 00000000..6ed48f32 --- /dev/null +++ b/cypher/models/pgsql/translate/predicate_placement.go @@ -0,0 +1,46 @@ +package translate + +import ( + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" +) + +func (s *Translator) recordPredicatePlacementConsumption(part *PatternPart, stepIndex int, traversalStep *TraversalStep, constraints PatternConstraints) { + if part == nil || !part.HasTarget || traversalStep == nil { + return + } + + for _, decision := range s.predicatePlacementDecisions[part.Target.TraversalStep(stepIndex)] { + if predicatePlacementDecisionConsumed(decision, traversalStep, constraints) { + s.recordLowering(optimize.LoweringPredicatePlacement) + return + } + } +} + +func predicatePlacementDecisionConsumed(decision optimize.PredicatePlacementDecision, traversalStep *TraversalStep, constraints PatternConstraints) bool { + for _, symbol := range decision.Attachment.BindingSymbols { + if bindingConstraintConsumed(symbol, traversalStep.LeftNode, constraints.LeftNode) || + bindingConstraintConsumed(symbol, traversalStep.Edge, constraints.Edge) || + bindingConstraintConsumed(symbol, traversalStep.RightNode, constraints.RightNode) { + return true + } + } + + return false +} + +func bindingConstraintConsumed(symbol string, binding *BoundIdentifier, constraint *Constraint) bool { + return constraint != nil && + constraint.Expression != nil && + bindingMatchesSymbol(binding, pgsql.Identifier(symbol)) +} + +func bindingMatchesSymbol(binding *BoundIdentifier, symbol pgsql.Identifier) bool { + if binding == nil { + return false + } + + return binding.Identifier == symbol || + (binding.Alias.Set && binding.Alias.Value == symbol) +} diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 5838ca07..45706619 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -35,6 +35,7 @@ type Translator struct { projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision latePathDecisions map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision suffixPushdownDecisions map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision + predicatePlacementDecisions map[optimize.TraversalStepTarget][]optimize.PredicatePlacementDecision expandIntoDecisions map[optimize.TraversalStepTarget]optimize.ExpandIntoDecision traversalDirectionDecisions map[optimize.TraversalStepTarget]optimize.TraversalDirectionDecision shortestPathStrategyDecisions map[optimize.TraversalStepTarget]optimize.ShortestPathStrategyDecision @@ -78,6 +79,7 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.projectionPruningDecisions = map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision{} s.latePathDecisions = map[optimize.TraversalStepTarget][]optimize.LatePathMaterializationDecision{} s.suffixPushdownDecisions = map[optimize.TraversalStepTarget][]optimize.ExpansionSuffixPushdownDecision{} + s.predicatePlacementDecisions = map[optimize.TraversalStepTarget][]optimize.PredicatePlacementDecision{} s.expandIntoDecisions = map[optimize.TraversalStepTarget]optimize.ExpandIntoDecision{} s.traversalDirectionDecisions = map[optimize.TraversalStepTarget]optimize.TraversalDirectionDecision{} s.shortestPathStrategyDecisions = map[optimize.TraversalStepTarget]optimize.ShortestPathStrategyDecision{} @@ -97,6 +99,10 @@ func (s *Translator) SetOptimizationPlan(plan optimize.Plan) { s.suffixPushdownDecisions[decision.Target] = append(s.suffixPushdownDecisions[decision.Target], decision) } + for _, decision := range plan.LoweringPlan.PredicatePlacement { + s.predicatePlacementDecisions[decision.Target] = append(s.predicatePlacementDecisions[decision.Target], decision) + } + for _, decision := range plan.LoweringPlan.ExpandInto { s.expandIntoDecisions[decision.Target] = decision } diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 54883885..0747b628 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -997,7 +997,11 @@ func (s *Translator) translateTraversalPatternPartWithoutExpansion(part *Pattern if err := s.applyPatternConstraintBalance(part, stepIndex, &constraints, traversalStep); err != nil { return err } + } + + s.recordPredicatePlacementConsumption(part, stepIndex, traversalStep, constraints) + if isFirstTraversalStep { hasPreviousFrame := traversalStep.Frame.Previous != nil if hasPreviousFrame { From 346686b5e5e67cedf69a2188648a9decbf24aeae Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:32:42 -0700 Subject: [PATCH 056/114] fix(pgsql): constrain predicate placement planning to clause --- .../models/pgsql/optimize/OPTIMIZATION_PLAN.md | 8 ++++++++ cypher/models/pgsql/optimize/lowering_plan.go | 3 +++ cypher/models/pgsql/optimize/optimizer_test.go | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index fc3324ce..beacd2b2 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -90,3 +90,11 @@ Status: completed - Keep skipped-lowering reports focused on predicates that were not consumed by the emitted translation shape, instead of marking already-pushed traversal predicates as skipped. - Add SQL-shape regression tests for fixed traversal and expansion-root predicate consumption. - Refreshed plan-corpus capture applies `PredicatePlacement` in 56 of 71 planned PostgreSQL cases, reducing skipped predicate placements from 65 to 15. + +## Phase 8: Cross-Clause Predicate Placement Planning + +Status: completed + +- Stop planning traversal predicate placements for binding predicates owned by a different `MATCH` clause. +- Preserve same-clause binding predicate placement for traversal and suffix pushdown decisions. +- Refreshed plan-corpus capture now plans and applies `PredicatePlacement` in the same 56 PostgreSQL cases, removing all skipped predicate-placement reports. diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 72036350..f12aa982 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -793,6 +793,9 @@ func appendPredicatePlacementDecisions(plan *LoweringPlan, query *cypher.Regular if !hasTarget { continue } + if target.ClauseIndex != attachment.ClauseIndex { + continue + } plan.PredicatePlacement = append(plan.PredicatePlacement, PredicatePlacementDecision{ Target: target, diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 111e17a7..18bb0de5 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -489,6 +489,24 @@ func TestLoweringPlanPlacesBindingPredicates(t *testing.T) { require.Equal(t, []PredicateAttachment{plan.LoweringPlan.PredicatePlacement[0].Attachment}, plan.LoweringPlan.ExpansionSuffixPushdown[0].PredicateAttachments) } +func TestLoweringPlanDoesNotPlaceCrossClauseBindingPredicates(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (n:Group) + WHERE n.objectid = 'S-1-5-21-1' + MATCH p = (n)-[:MemberOf*1..]->(ca:EnterpriseCA) + RETURN p + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.NotEmpty(t, plan.PredicateAttachments) + require.Empty(t, plan.LoweringPlan.PredicatePlacement) + require.NotContains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringPredicatePlacement}) +} + func TestLoweringPlanReportsExpandInto(t *testing.T) { t.Parallel() From e58082602cac1e1c4e4f9743a5c0ef3539cb8ace Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:46:01 -0700 Subject: [PATCH 057/114] fix(pgsql): preserve endpoints in edge count fast path --- .../translation_cases/stepwise_traversal.sql | 2 +- .../models/pgsql/translate/count_fast_path.go | 27 +++++- .../pgsql/translate/optimizer_safety_test.go | 2 +- integration/pgsql_count_fast_path_test.go | 94 +++++++++++++++++++ 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 integration/pgsql_count_fast_path_test.go diff --git a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql index a33ed6c6..c658814c 100644 --- a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql +++ b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql @@ -45,7 +45,7 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on ('123' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or '243' = any (jsonb_to_text_array((n1.properties -> 'prop2'))::text[]) or jsonb_array_length((n1.properties -> 'prop2'))::int = 0) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) limit 10) select case when (s0.n0).id is null or s0.e0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s0.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; -- case: match ()-[r:EdgeKind1]->() return count(r) as the_count -select count(*)::int8 as the_count from edge e0 where e0.kind_id = any (array [3]::int2[]); +select count(*)::int8 as the_count from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]); -- case: match ()-[r:EdgeKind1]->() return count(r) as the_count limit 1 with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0 limit 1; diff --git a/cypher/models/pgsql/translate/count_fast_path.go b/cypher/models/pgsql/translate/count_fast_path.go index c3c27cb9..f720bfae 100644 --- a/cypher/models/pgsql/translate/count_fast_path.go +++ b/cypher/models/pgsql/translate/count_fast_path.go @@ -10,8 +10,10 @@ import ( ) const ( - countStoreNodeAlias pgsql.Identifier = "n0" - countStoreEdgeAlias pgsql.Identifier = "e0" + countStoreNodeAlias pgsql.Identifier = "n0" + countStoreEdgeAlias pgsql.Identifier = "e0" + countStoreStartEndpointAlias pgsql.Identifier = "n0" + countStoreEndEndpointAlias pgsql.Identifier = "n1" ) type countStoreFastPathShape struct { @@ -78,6 +80,10 @@ func (s *Translator) countStoreFastPathFromAndWhere(shape countStoreFastPathShap Name: pgsql.TableEdge.AsCompoundIdentifier(), Binding: pgsql.AsOptionalIdentifier(countStoreEdgeAlias), }, + Joins: []pgsql.Join{ + countStoreEndpointJoin(countStoreStartEndpointAlias, pgsql.ColumnStartID), + countStoreEndpointJoin(countStoreEndEndpointAlias, pgsql.ColumnEndID), + }, }, where, err default: @@ -85,6 +91,23 @@ func (s *Translator) countStoreFastPathFromAndWhere(shape countStoreFastPathShap } } +func countStoreEndpointJoin(nodeAlias pgsql.Identifier, edgeEndpoint pgsql.Identifier) pgsql.Join { + return pgsql.Join{ + Table: pgsql.TableReference{ + Name: pgsql.TableNode.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(nodeAlias), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{nodeAlias, pgsql.ColumnID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{countStoreEdgeAlias, edgeEndpoint}, + ), + }, + } +} + func (s *Translator) countStoreNodeKindConstraint(kinds graph.Kinds) (pgsql.Expression, error) { if len(kinds) == 0 { return nil, nil diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 3cb8b715..f57fe8bf 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -181,7 +181,7 @@ func TestOptimizerSafetyCountStoreFastPathUsesBaseEdgeCount(t *testing.T) { requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) requireSkippedOptimizationLowering(t, translation.Optimization, optimize.LoweringProjectionPruning, "superseded by CountStoreFastPath") - require.Equal(t, "select count(*)::int8 from edge e0 where e0.kind_id = any (array [10]::int2[]);", strings.Join(strings.Fields(formattedQuery), " ")) + require.Equal(t, "select count(*)::int8 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [10]::int2[]);", strings.Join(strings.Fields(formattedQuery), " ")) } func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { diff --git a/integration/pgsql_count_fast_path_test.go b/integration/pgsql_count_fast_path_test.go new file mode 100644 index 00000000..a29a9610 --- /dev/null +++ b/integration/pgsql_count_fast_path_test.go @@ -0,0 +1,94 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build manual_integration + +package integration + +import ( + "errors" + "os" + "testing" + + "github.com/specterops/dawgs/drivers/pg" + "github.com/specterops/dawgs/graph" +) + +func TestPostgreSQLCountStoreFastPathRequiresRelationshipEndpoints(t *testing.T) { + connStr := os.Getenv("CONNECTION_STRING") + if connStr == "" { + t.Skip("CONNECTION_STRING env var is not set") + } + + driver, err := driverFromConnStr(connStr) + if err != nil { + t.Fatalf("failed to detect driver: %v", err) + } + if driver != pg.DriverName { + t.Skip("CONNECTION_STRING is not a PostgreSQL connection string") + } + + var ( + nodeKind = graph.StringKind("CountFastPathNode") + edgeKind = graph.StringKind("CountFastPathEdge") + db, ctx = SetupDBWithKinds(t, graph.Kinds{nodeKind}, graph.Kinds{edgeKind}) + ) + + if err := db.WriteTransaction(ctx, func(tx graph.Transaction) error { + start, err := tx.CreateNode(graph.NewProperties(), nodeKind) + if err != nil { + return err + } + + if _, err := tx.CreateRelationshipByIDs(start.ID, 0, edgeKind, graph.NewProperties()); err != nil { + return err + } + + return nil + }); err != nil { + t.Fatalf("failed to create dangling relationship fixture: %v", err) + } + + var count int64 + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Query("MATCH ()-[r:CountFastPathEdge]->() RETURN count(r)", nil) + defer result.Close() + + if !result.Next() { + if err := result.Error(); err != nil { + return err + } + + return errors.New("expected count row") + } + + if err := result.Scan(&count); err != nil { + return err + } + + if result.Next() { + return errors.New("expected one count row") + } + + return result.Error() + }); err != nil { + t.Fatalf("query failed: %v", err) + } + + if count != 0 { + t.Fatalf("relationship count: got %d, want 0", count) + } +} From 5dd087e667b968ca6d9a8a40f5ee3d1608f1736e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:48:00 -0700 Subject: [PATCH 058/114] feat(plancorpus): count partially skipped lowerings --- .../pgsql/translate/optimizer_safety_test.go | 32 +++++++++++++++++ cypher/models/pgsql/translate/translator.go | 34 ++++++++++++++----- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index f57fe8bf..ea029887 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -127,6 +127,19 @@ func requireSkippedOptimizationLowering(t *testing.T, summary OptimizationSummar require.Failf(t, "missing skipped optimization lowering", "expected skipped lowering %q in %#v", name, summary.SkippedLowerings) } +func requireSkippedOptimizationLoweringCount(t *testing.T, summary OptimizationSummary, name string, count int) { + t.Helper() + + for _, lowering := range summary.SkippedLowerings { + if lowering.Name == name { + require.Equal(t, count, lowering.Count) + return + } + } + + require.Failf(t, "missing skipped optimization lowering", "expected skipped lowering %q in %#v", name, summary.SkippedLowerings) +} + func requireNoSkippedOptimizationLowering(t *testing.T, summary OptimizationSummary, name string) { t.Helper() @@ -135,6 +148,25 @@ func requireNoSkippedOptimizationLowering(t *testing.T, summary OptimizationSumm } } +func TestOptimizerSafetyReportsPartiallySkippedLowerings(t *testing.T) { + t.Parallel() + + translator := NewTranslator(context.Background(), optimizerSafetyKindMapper(), nil, DefaultGraphID) + translator.translation.Optimization.LoweringPlan = &optimize.LoweringPlan{ + PredicatePlacement: []optimize.PredicatePlacementDecision{ + {Target: optimize.TraversalStepTarget{StepIndex: 0}}, + {Target: optimize.TraversalStepTarget{StepIndex: 1}}, + }, + } + + translator.recordLowering(optimize.LoweringPredicatePlacement) + translator.recordSkippedLowerings() + + requireOptimizationLowering(t, translator.translation.Optimization, optimize.LoweringPredicatePlacement) + requireSkippedOptimizationLowering(t, translator.translation.Optimization, optimize.LoweringPredicatePlacement, "planned predicate placements were not consumed by this translation shape") + requireSkippedOptimizationLoweringCount(t, translator.translation.Optimization, optimize.LoweringPredicatePlacement, 1) +} + func requireSQLContainsInOrder(t *testing.T, sql string, parts ...string) { t.Helper() diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 45706619..306c2efc 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -30,6 +30,7 @@ type Translator struct { scope *Scope unwindTargets map[*cypher.Variable]struct{} + appliedLoweringCounts map[string]int patternTargets map[*cypher.PatternPart]optimize.PatternTarget patternPredicateTargets map[*cypher.PatternPredicate]optimize.PatternTarget projectionPruningDecisions map[optimize.TraversalStepTarget]optimize.ProjectionPruningDecision @@ -616,6 +617,11 @@ type SkippedLowering struct { } func (s *Translator) recordLowering(name string) { + if s.appliedLoweringCounts == nil { + s.appliedLoweringCounts = map[string]int{} + } + s.appliedLoweringCounts[name]++ + for _, lowering := range s.translation.Optimization.Lowerings { if lowering.Name == name { return @@ -625,29 +631,41 @@ func (s *Translator) recordLowering(name string) { s.translation.Optimization.Lowerings = append(s.translation.Optimization.Lowerings, optimize.LoweringDecision{Name: name}) } +func (s *Translator) appliedLoweringCountSnapshot() map[string]int { + applied := map[string]int{} + + for _, lowering := range s.translation.Optimization.Lowerings { + applied[lowering.Name] = 1 + } + + for name, count := range s.appliedLoweringCounts { + applied[name] = count + } + + return applied +} + func (s *Translator) recordSkippedLowerings() { if s.translation.Optimization.LoweringPlan == nil { return } - applied := map[string]struct{}{} - for _, lowering := range s.translation.Optimization.Lowerings { - applied[lowering.Name] = struct{}{} - } + applied := s.appliedLoweringCountSnapshot() for _, planned := range plannedLoweringCounts(*s.translation.Optimization.LoweringPlan) { if planned.Count == 0 { continue } - if _, wasApplied := applied[planned.Name]; wasApplied { + skippedCount := planned.Count - applied[planned.Name] + if skippedCount <= 0 { continue } s.translation.Optimization.SkippedLowerings = append(s.translation.Optimization.SkippedLowerings, SkippedLowering{ Name: planned.Name, Reason: skippedLoweringReason(planned.Name, applied), - Count: planned.Count, + Count: skippedCount, }) } } @@ -667,8 +685,8 @@ func plannedLoweringCounts(plan optimize.LoweringPlan) []SkippedLowering { } } -func skippedLoweringReason(name string, applied map[string]struct{}) string { - if _, countFastPathApplied := applied[optimize.LoweringCountStoreFastPath]; countFastPathApplied && name != optimize.LoweringCountStoreFastPath { +func skippedLoweringReason(name string, applied map[string]int) string { + if applied[optimize.LoweringCountStoreFastPath] > 0 && name != optimize.LoweringCountStoreFastPath { return "superseded by CountStoreFastPath" } From 54d8b2b73e323007ad44cdd12403fa16eb841738 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:50:44 -0700 Subject: [PATCH 059/114] fix(plancorpus): honor Neo4j connection URIs --- README.md | 2 + cmd/plancorpus/capture.go | 80 ++++++++++++++++++++++--- cmd/plancorpus/main_test.go | 52 +++++++++++++++- drivers/neo4j/batch_integration_test.go | 2 +- drivers/neo4j/driver.go | 22 ++++--- drivers/neo4j/neo4j.go | 46 ++++++++++++-- drivers/neo4j/neo4j_internal_test.go | 56 +++++++++++++++++ 7 files changed, 235 insertions(+), 25 deletions(-) create mode 100644 drivers/neo4j/neo4j_internal_test.go diff --git a/README.md b/README.md index c37f58d6..2d022e07 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ export CONNECTION_STRING="postgresql://dawgs:weneedbetterpasswords@localhost:654 export CONNECTION_STRING="neo4j://neo4j:weneedbetterpasswords@localhost:7687" ``` +Neo4j connection strings may use `neo4j://`, `neo4j+s://`, or `neo4j+ssc://`; a single path segment selects the Neo4j database name. + Use `make test` for unit tests only and `make test_integration` for integration tests only. ### Test Metrics diff --git a/cmd/plancorpus/capture.go b/cmd/plancorpus/capture.go index e69802fb..4b746e51 100644 --- a/cmd/plancorpus/capture.go +++ b/cmd/plancorpus/capture.go @@ -35,6 +35,7 @@ type backendCapture struct { pgDriver *pg.Driver pgGraphID int32 neo4jDriver neo4jcore.Driver + neo4jDBName string } func driverFromConnectionString(connStr string) (string, error) { @@ -210,12 +211,13 @@ func openBackend(ctx context.Context, suite corpus, spec captureSpec) (*backendC backend.pgGraphID = defaultGraph.ID case neo4j.DriverName: - neo4jDriver, err := openNeo4jPlanDriver(spec.Connection) + neo4jDriver, databaseName, err := openNeo4jPlanDriver(spec.Connection) if err != nil { _ = db.Close(ctx) return nil, err } backend.neo4jDriver = neo4jDriver + backend.neo4jDBName = databaseName } return backend, nil @@ -298,7 +300,8 @@ func (s *backendCapture) capturePostgres(ctx context.Context, cypherQuery string func (s *backendCapture) captureNeo4j(cypherQuery string, params map[string]any, record *PlanRecord) { session := s.neo4jDriver.NewSession(neo4jcore.SessionConfig{ - AccessMode: neo4jcore.AccessModeWrite, + AccessMode: neo4jcore.AccessModeWrite, + DatabaseName: s.neo4jDBName, }) defer session.Close() @@ -321,21 +324,82 @@ func (s *backendCapture) captureNeo4j(cypherQuery string, params map[string]any, } } -func openNeo4jPlanDriver(connStr string) (neo4jcore.Driver, error) { +type neo4jPlanDriverConfig struct { + Target string + Username string + Password string + DatabaseName string +} + +func parseNeo4jPlanDriverConfig(connStr string) (neo4jPlanDriverConfig, error) { connectionURL, err := url.Parse(connStr) if err != nil { - return nil, fmt.Errorf("parse Neo4j connection string: %w", err) + return neo4jPlanDriverConfig{}, fmt.Errorf("parse Neo4j connection string: %w", err) + } + + if connectionURL.Scheme != neo4j.DriverName && connectionURL.Scheme != "neo4j+s" && connectionURL.Scheme != "neo4j+ssc" { + return neo4jPlanDriverConfig{}, fmt.Errorf("expected Neo4j connection string scheme, got %q", connectionURL.Scheme) } password, ok := connectionURL.User.Password() if !ok { - return nil, fmt.Errorf("no password provided in Neo4j connection string") + return neo4jPlanDriverConfig{}, fmt.Errorf("no password provided in Neo4j connection string") + } + + if connectionURL.Host == "" { + return neo4jPlanDriverConfig{}, fmt.Errorf("Neo4j connection string host is required") + } + + databaseName, err := neo4jDatabaseName(connectionURL) + if err != nil { + return neo4jPlanDriverConfig{}, err + } + + return neo4jPlanDriverConfig{ + Target: (&url.URL{ + Scheme: connectionURL.Scheme, + Host: connectionURL.Host, + RawQuery: connectionURL.RawQuery, + }).String(), + Username: connectionURL.User.Username(), + Password: password, + DatabaseName: databaseName, + }, nil +} + +func neo4jDatabaseName(connectionURL *url.URL) (string, error) { + databasePath := strings.Trim(connectionURL.EscapedPath(), "/") + if databasePath == "" { + return "", nil + } + + if strings.Contains(databasePath, "/") { + return "", fmt.Errorf("Neo4j database path must contain a single database name") + } + + databaseName, err := url.PathUnescape(databasePath) + if err != nil { + return "", fmt.Errorf("parse Neo4j database name: %w", err) + } + + return databaseName, nil +} + +func openNeo4jPlanDriver(connStr string) (neo4jcore.Driver, string, error) { + cfg, err := parseNeo4jPlanDriverConfig(connStr) + if err != nil { + return nil, "", err } - return neo4jcore.NewDriver( - "bolt://"+connectionURL.Host, - neo4jcore.BasicAuth(connectionURL.User.Username(), password, ""), + driver, err := neo4jcore.NewDriver( + cfg.Target, + neo4jcore.BasicAuth(cfg.Username, cfg.Password, ""), ) + if err != nil { + return nil, "", err + } + + return driver, cfg.DatabaseName, nil } func clearGraph(ctx context.Context, db graph.Database) error { diff --git a/cmd/plancorpus/main_test.go b/cmd/plancorpus/main_test.go index 51aa5af9..c0f696be 100644 --- a/cmd/plancorpus/main_test.go +++ b/cmd/plancorpus/main_test.go @@ -32,10 +32,56 @@ func TestDriverFromConnectionString(t *testing.T) { require.NoError(t, err) require.Equal(t, "pg", driverName) - driverName, err = driverFromConnectionString("neo4j://neo4j:password@localhost:7687") - require.NoError(t, err) - require.Equal(t, "neo4j", driverName) + for _, connStr := range []string{ + "neo4j://neo4j:password@localhost:7687", + "neo4j+s://neo4j:password@localhost:7687", + "neo4j+ssc://neo4j:password@localhost:7687", + } { + driverName, err = driverFromConnectionString(connStr) + require.NoError(t, err) + require.Equal(t, "neo4j", driverName) + } _, err = driverFromConnectionString("mysql://localhost") require.ErrorContains(t, err, "unknown connection string scheme") } + +func TestParseNeo4jPlanDriverConfigPreservesURI(t *testing.T) { + testCases := []struct { + name string + connStr string + expectedTarget string + expectedDatabase string + }{{ + name: "plain routing", + connStr: "neo4j://neo4j:password@localhost:7687", + expectedTarget: "neo4j://localhost:7687", + expectedDatabase: "", + }, { + name: "secure routing", + connStr: "neo4j+s://neo4j:password@cluster.example:7687", + expectedTarget: "neo4j+s://cluster.example:7687", + expectedDatabase: "", + }, { + name: "self signed routing with database and query", + connStr: "neo4j+ssc://neo4j:password@cluster.example:7687/analytics?policy=fast", + expectedTarget: "neo4j+ssc://cluster.example:7687?policy=fast", + expectedDatabase: "analytics", + }} + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + cfg, err := parseNeo4jPlanDriverConfig(testCase.connStr) + require.NoError(t, err) + require.Equal(t, testCase.expectedTarget, cfg.Target) + require.Equal(t, "neo4j", cfg.Username) + require.Equal(t, "password", cfg.Password) + require.Equal(t, testCase.expectedDatabase, cfg.DatabaseName) + }) + } +} + +func TestParseNeo4jPlanDriverConfigRejectsNestedDatabasePath(t *testing.T) { + _, err := parseNeo4jPlanDriverConfig("neo4j://neo4j:password@localhost:7687/db/extra") + require.ErrorContains(t, err, "single database name") +} diff --git a/drivers/neo4j/batch_integration_test.go b/drivers/neo4j/batch_integration_test.go index 80eb96c6..798db901 100644 --- a/drivers/neo4j/batch_integration_test.go +++ b/drivers/neo4j/batch_integration_test.go @@ -42,7 +42,7 @@ func prepareNode(index int) *graph.Node { func isNeo4jConnectionString(connStr string) bool { u, err := url.Parse(connStr) - return err == nil && u.Scheme == neo4j.DriverName + return err == nil && (u.Scheme == neo4j.DriverName || u.Scheme == "neo4j+s" || u.Scheme == "neo4j+ssc") } func TestBatchTransaction_NodeUpdate(t *testing.T) { diff --git a/drivers/neo4j/driver.go b/drivers/neo4j/driver.go index d78b8511..3c9fa317 100644 --- a/drivers/neo4j/driver.go +++ b/drivers/neo4j/driver.go @@ -14,16 +14,19 @@ const ( DriverName = "neo4j" ) -func readCfg() neo4j.SessionConfig { +func sessionConfig(accessMode neo4j.AccessMode, databaseName string) neo4j.SessionConfig { return neo4j.SessionConfig{ - AccessMode: neo4j.AccessModeRead, + AccessMode: accessMode, + DatabaseName: databaseName, } } -func writeCfg() neo4j.SessionConfig { - return neo4j.SessionConfig{ - AccessMode: neo4j.AccessModeWrite, - } +func (s *driver) readCfg() neo4j.SessionConfig { + return sessionConfig(neo4j.AccessModeRead, s.databaseName) +} + +func (s *driver) writeCfg() neo4j.SessionConfig { + return sessionConfig(neo4j.AccessModeWrite, s.databaseName) } type driver struct { @@ -33,6 +36,7 @@ type driver struct { batchWriteSize int writeFlushSize int graphQueryMemoryLimit size.Size + databaseName string } func (s *driver) SetBatchWriteSize(size int) { @@ -64,7 +68,7 @@ func (s *driver) BatchOperation(ctx context.Context, batchDelegate graph.BatchDe Timeout: s.defaultTransactionTimeout, } - session = s.driver.NewSession(writeCfg()) + session = s.driver.NewSession(s.writeCfg()) batch = newBatchOperation(ctx, session, cfg, s.writeFlushSize, config.BatchSize, s.graphQueryMemoryLimit) ) @@ -110,14 +114,14 @@ func (s *driver) transaction(ctx context.Context, txDelegate graph.TransactionDe } func (s *driver) ReadTransaction(ctx context.Context, txDelegate graph.TransactionDelegate, options ...graph.TransactionOption) error { - session := s.driver.NewSession(readCfg()) + session := s.driver.NewSession(s.readCfg()) defer session.Close() return s.transaction(ctx, txDelegate, session, options) } func (s *driver) WriteTransaction(ctx context.Context, txDelegate graph.TransactionDelegate, options ...graph.TransactionOption) error { - session := s.driver.NewSession(writeCfg()) + session := s.driver.NewSession(s.writeCfg()) defer session.Close() return s.transaction(ctx, txDelegate, session, options) diff --git a/drivers/neo4j/neo4j.go b/drivers/neo4j/neo4j.go index c6a5ea2a..7a4f385a 100644 --- a/drivers/neo4j/neo4j.go +++ b/drivers/neo4j/neo4j.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "net/url" + "strings" "github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/specterops/dawgs" @@ -21,14 +22,16 @@ const ( func newNeo4jDB(_ context.Context, cfg dawgs.Config) (graph.Database, error) { if connectionURL, err := url.Parse(cfg.ConnectionString); err != nil { return nil, err - } else if connectionURL.Scheme != DriverName { + } else if !isNeo4jConnectionScheme(connectionURL.Scheme) { return nil, fmt.Errorf("expected connection URL scheme %s for Neo4J but got %s", DriverName, connectionURL.Scheme) } else if password, isSet := connectionURL.User.Password(); !isSet { return nil, fmt.Errorf("no password provided in connection URL") + } else if target, err := neo4jConnectionTarget(connectionURL); err != nil { + return nil, err + } else if databaseName, err := neo4jConnectionDatabaseName(connectionURL); err != nil { + return nil, err } else { - boltURL := fmt.Sprintf("bolt://%s:%s", connectionURL.Hostname(), connectionURL.Port()) - - if internalDriver, err := neo4j.NewDriver(boltURL, neo4j.BasicAuth(connectionURL.User.Username(), password, "")); err != nil { + if internalDriver, err := neo4j.NewDriver(target, neo4j.BasicAuth(connectionURL.User.Username(), password, "")); err != nil { return nil, fmt.Errorf("unable to connect to Neo4J: %w", err) } else { return &driver{ @@ -38,11 +41,46 @@ func newNeo4jDB(_ context.Context, cfg dawgs.Config) (graph.Database, error) { writeFlushSize: DefaultWriteFlushSize, batchWriteSize: DefaultBatchWriteSize, graphQueryMemoryLimit: cfg.GraphQueryMemoryLimit, + databaseName: databaseName, }, nil } } } +func isNeo4jConnectionScheme(scheme string) bool { + return scheme == DriverName || scheme == "neo4j+s" || scheme == "neo4j+ssc" +} + +func neo4jConnectionTarget(connectionURL *url.URL) (string, error) { + if connectionURL.Host == "" { + return "", fmt.Errorf("Neo4j connection string host is required") + } + + return (&url.URL{ + Scheme: connectionURL.Scheme, + Host: connectionURL.Host, + RawQuery: connectionURL.RawQuery, + }).String(), nil +} + +func neo4jConnectionDatabaseName(connectionURL *url.URL) (string, error) { + databasePath := strings.Trim(connectionURL.EscapedPath(), "/") + if databasePath == "" { + return "", nil + } + + if strings.Contains(databasePath, "/") { + return "", fmt.Errorf("Neo4j database path must contain a single database name") + } + + databaseName, err := url.PathUnescape(databasePath) + if err != nil { + return "", fmt.Errorf("parse Neo4j database name: %w", err) + } + + return databaseName, nil +} + func init() { dawgs.Register(DriverName, func(ctx context.Context, cfg dawgs.Config) (graph.Database, error) { return newNeo4jDB(ctx, cfg) diff --git a/drivers/neo4j/neo4j_internal_test.go b/drivers/neo4j/neo4j_internal_test.go new file mode 100644 index 00000000..cfa77f5d --- /dev/null +++ b/drivers/neo4j/neo4j_internal_test.go @@ -0,0 +1,56 @@ +package neo4j + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNeo4jConnectionTargetPreservesAcceptedSchemes(t *testing.T) { + testCases := []struct { + name string + connStr string + expectedTarget string + expectedDatabase string + }{{ + name: "plain routing", + connStr: "neo4j://neo4j:password@localhost:7687", + expectedTarget: "neo4j://localhost:7687", + expectedDatabase: "", + }, { + name: "secure routing", + connStr: "neo4j+s://neo4j:password@cluster.example:7687", + expectedTarget: "neo4j+s://cluster.example:7687", + expectedDatabase: "", + }, { + name: "self signed routing with database and query", + connStr: "neo4j+ssc://neo4j:password@cluster.example:7687/analytics?policy=fast", + expectedTarget: "neo4j+ssc://cluster.example:7687?policy=fast", + expectedDatabase: "analytics", + }} + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + connectionURL, err := url.Parse(testCase.connStr) + require.NoError(t, err) + require.True(t, isNeo4jConnectionScheme(connectionURL.Scheme)) + + target, err := neo4jConnectionTarget(connectionURL) + require.NoError(t, err) + require.Equal(t, testCase.expectedTarget, target) + + databaseName, err := neo4jConnectionDatabaseName(connectionURL) + require.NoError(t, err) + require.Equal(t, testCase.expectedDatabase, databaseName) + }) + } +} + +func TestNeo4jConnectionDatabaseNameRejectsNestedPath(t *testing.T) { + connectionURL, err := url.Parse("neo4j://neo4j:password@localhost:7687/db/extra") + require.NoError(t, err) + + _, err = neo4jConnectionDatabaseName(connectionURL) + require.ErrorContains(t, err, "single database name") +} From 202b21444302614e1f9fb9f99de995caea07e882 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:51:59 -0700 Subject: [PATCH 060/114] feat(pgsql): wire count star fast path planning --- .../pgsql/optimize/OPTIMIZATION_PLAN.md | 1 + cypher/models/pgsql/optimize/lowering_plan.go | 16 +++++++++--- .../models/pgsql/optimize/optimizer_test.go | 22 ++++++++++++++++ .../models/pgsql/translate/count_fast_path.go | 16 +++++++++--- .../pgsql/translate/optimizer_safety_test.go | 25 +++++++++++++++++++ 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index beacd2b2..6b0b4937 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -22,6 +22,7 @@ Status: completed - Add count-store fast paths for simple count queries: - `MATCH (n) RETURN count(n)` - `MATCH ()-[r]->() RETURN count(r)` + - `MATCH (...) RETURN count(*)` for the same exact node and directed-edge shapes. - Typed variants where kind filters map cleanly. - Implemented as `CountStoreFastPath` lowering for exact node and directed-edge count shapes. - Audit the planned/applied `PredicatePlacement` gap. diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index f12aa982..4dfd2912 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -933,12 +933,20 @@ func simpleCountProjectionArgument(returnClause *cypher.Return) (string, bool) { return "", false } - variable, ok := function.Arguments[0].(*cypher.Variable) - if !ok || variable == nil { - return "", false + switch argument := function.Arguments[0].(type) { + case *cypher.Variable: + if argument == nil { + return "", false + } + + return argument.Symbol, true + case *cypher.RangeQuantifier: + if argument != nil && argument.Value == cypher.TokenLiteralAsterisk { + return cypher.TokenLiteralAsterisk, true + } } - return variable.Symbol, true + return "", false } func constrainedCountFastPathEndpoint(nodePattern *cypher.NodePattern) bool { diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 18bb0de5..c50ef14d 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -436,6 +436,17 @@ func TestLoweringPlanReportsCountStoreFastPath(t *testing.T) { KindSymbols: []string{"Group"}, }, }, + { + name: "node count star", + query: "MATCH (:Group) RETURN count(*)", + expected: CountStoreFastPathDecision{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + Target: CountStoreFastPathNode, + KindSymbols: []string{"Group"}, + }, + }, { name: "edge count", query: "MATCH ()-[r:MemberOf]->() RETURN count(r)", @@ -448,6 +459,17 @@ func TestLoweringPlanReportsCountStoreFastPath(t *testing.T) { KindSymbols: []string{"MemberOf"}, }, }, + { + name: "edge count star", + query: "MATCH ()-[:MemberOf]->() RETURN count(*)", + expected: CountStoreFastPathDecision{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + Target: CountStoreFastPathEdge, + KindSymbols: []string{"MemberOf"}, + }, + }, } for _, testCase := range testCases { diff --git a/cypher/models/pgsql/translate/count_fast_path.go b/cypher/models/pgsql/translate/count_fast_path.go index f720bfae..29387061 100644 --- a/cypher/models/pgsql/translate/count_fast_path.go +++ b/cypher/models/pgsql/translate/count_fast_path.go @@ -245,12 +245,20 @@ func simpleCountProjection(returnClause *cypher.Return) (string, string, bool) { return "", "", false } - variable, ok := function.Arguments[0].(*cypher.Variable) - if !ok || variable == nil { - return "", "", false + switch argument := function.Arguments[0].(type) { + case *cypher.Variable: + if argument == nil { + return "", "", false + } + + return argument.Symbol, countStoreVariableSymbol(projectionItem.Alias), true + case *cypher.RangeQuantifier: + if argument != nil && argument.Value == cypher.TokenLiteralAsterisk { + return cypher.TokenLiteralAsterisk, countStoreVariableSymbol(projectionItem.Alias), true + } } - return variable.Symbol, countStoreVariableSymbol(projectionItem.Alias), true + return "", "", false } func constrainedCountStoreEndpoint(nodePattern *cypher.NodePattern) bool { diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index ea029887..96b82981 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -203,6 +203,18 @@ func TestOptimizerSafetyCountStoreFastPathKeepsKindConstraintAndAlias(t *testing require.Equal(t, "select count(*)::int8 as total from node n0 where n0.kind_ids operator (pg_catalog.@>) array [8]::int2[];", strings.Join(strings.Fields(formattedQuery), " ")) } +func TestOptimizerSafetyCountStoreFastPathSupportsNodeCountStar(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, `MATCH (:Group) RETURN count(*) AS total`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + require.Equal(t, "select count(*)::int8 as total from node n0 where n0.kind_ids operator (pg_catalog.@>) array [8]::int2[];", strings.Join(strings.Fields(formattedQuery), " ")) +} + func TestOptimizerSafetyCountStoreFastPathUsesBaseEdgeCount(t *testing.T) { t.Parallel() @@ -216,6 +228,19 @@ func TestOptimizerSafetyCountStoreFastPathUsesBaseEdgeCount(t *testing.T) { require.Equal(t, "select count(*)::int8 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [10]::int2[]);", strings.Join(strings.Fields(formattedQuery), " ")) } +func TestOptimizerSafetyCountStoreFastPathSupportsEdgeCountStar(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, `MATCH ()-[:MemberOf]->() RETURN count(*)`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireSkippedOptimizationLowering(t, translation.Optimization, optimize.LoweringProjectionPruning, "superseded by CountStoreFastPath") + require.Equal(t, "select count(*)::int8 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [10]::int2[]);", strings.Join(strings.Fields(formattedQuery), " ")) +} + func TestOptimizerSafetyADCSQueryPrunesExpansionEdgeCarry(t *testing.T) { t.Parallel() From ee9194a62b893c5c4b9eeb00f1cd46e49d758908 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 17:55:54 -0700 Subject: [PATCH 061/114] test(pgsql): validate optimization gap fixes Validated with PostgreSQL and Neo4j make test_all. From 249c463c7ee711a0d1a85c57ea512aa009742ef7 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 19:55:42 -0700 Subject: [PATCH 062/114] fix(pgsql): use typed text lookups for string equality --- cypher/models/pgsql/functions.go | 1 + cypher/models/pgsql/translate/expression.go | 169 ++++++++++++++------ cypher/models/pgsql/translate/property.go | 2 + 3 files changed, 126 insertions(+), 46 deletions(-) diff --git a/cypher/models/pgsql/functions.go b/cypher/models/pgsql/functions.go index d9e787f4..3d0b5f4e 100644 --- a/cypher/models/pgsql/functions.go +++ b/cypher/models/pgsql/functions.go @@ -12,6 +12,7 @@ const ( FunctionJSONBArrayElementsText Identifier = "jsonb_array_elements_text" FunctionJSONBBuildObject Identifier = "jsonb_build_object" FunctionJSONBArrayLength Identifier = "jsonb_array_length" + FunctionJSONBTypeof Identifier = "jsonb_typeof" FunctionToJSONB Identifier = "to_jsonb" FunctionCypherContains Identifier = "cypher_contains" FunctionCypherStartsWith Identifier = "cypher_starts_with" diff --git a/cypher/models/pgsql/translate/expression.go b/cypher/models/pgsql/translate/expression.go index 7161db3f..9e62ec2f 100644 --- a/cypher/models/pgsql/translate/expression.go +++ b/cypher/models/pgsql/translate/expression.go @@ -240,7 +240,7 @@ func rewritePropertyLookupOperator(propertyLookup *pgsql.BinaryExpression, dataT func isJSONScalarEqualityType(dataType pgsql.DataType) bool { switch dataType { - case pgsql.Boolean, pgsql.Float4, pgsql.Float8, pgsql.Int, pgsql.Int2, pgsql.Int4, pgsql.Int8, pgsql.Numeric, pgsql.Text: + case pgsql.Boolean, pgsql.Float4, pgsql.Float8, pgsql.Int, pgsql.Int2, pgsql.Int4, pgsql.Int8, pgsql.Numeric: return true default: @@ -248,51 +248,9 @@ func isJSONScalarEqualityType(dataType pgsql.DataType) bool { } } -func isBooleanTextCompatibilityValue(value any) bool { - switch value { - case "true", "false": - return true - - default: - return false - } -} - -func isBooleanTextCompatibilityParameter(kindMapper *contextAwareKindMapper, parameter pgsql.Parameter) bool { - if kindMapper == nil || parameter.TypeHint() != pgsql.Text { - return false - } - - value, hasValue := kindMapper.parameters[parameter.Identifier.String()] - return hasValue && isBooleanTextCompatibilityValue(value) -} - -func isBooleanTextCompatibilityOperand(kindMapper *contextAwareKindMapper, expression pgsql.Expression) bool { - switch typedExpression := expression.(type) { - case pgsql.Literal: - return typedExpression.TypeHint() == pgsql.Text && isBooleanTextCompatibilityValue(typedExpression.Value) - - case pgsql.Parameter: - return isBooleanTextCompatibilityParameter(kindMapper, typedExpression) - - case *pgsql.Parameter: - if typedExpression == nil { - return false - } - - return isBooleanTextCompatibilityParameter(kindMapper, *typedExpression) - - default: - return false - } -} - -func rewriteJSONScalarEqualityOperand(kindMapper *contextAwareKindMapper, expression pgsql.Expression) (pgsql.Expression, bool) { +func rewriteJSONScalarEqualityOperand(expression pgsql.Expression) (pgsql.Expression, bool) { if literal, isLiteral := expression.(pgsql.Literal); isLiteral && literal.Null { return nil, false - } else if isBooleanTextCompatibilityOperand(kindMapper, expression) { - // Preserve compatibility for existing callers that compare JSON boolean properties to stringified booleans. - return nil, false } if typedExpression, isTypeHinted := expression.(pgsql.TypeHinted); !isTypeHinted { @@ -310,6 +268,20 @@ func rewriteJSONScalarEqualityOperand(kindMapper *contextAwareKindMapper, expres } } +func rewriteStringEqualityOperand(expression pgsql.Expression) (pgsql.Expression, bool) { + if literal, isLiteral := expression.(pgsql.Literal); isLiteral && literal.Null { + return nil, false + } + + if typedExpression, isTypeHinted := expression.(pgsql.TypeHinted); !isTypeHinted { + return nil, false + } else if typedExpression.TypeHint() != pgsql.Text { + return nil, false + } + + return expression, true +} + func lookupRequiresElementType(typeHint pgsql.DataType, operator pgsql.Operator, otherOperand pgsql.SyntaxNode) bool { if typeHint.IsArrayType() { switch operator { @@ -381,7 +353,10 @@ func rewritePropertyLookupOperands(kindMapper *contextAwareKindMapper, expressio } case pgsql.OperatorEquals, pgsql.OperatorCypherNotEquals: - if rewrittenROperand, rewritten := rewriteJSONScalarEqualityOperand(kindMapper, expression.ROperand); rewritten { + if rewrittenROperand, rewritten := rewriteStringEqualityOperand(expression.ROperand); rewritten { + expression.LOperand = rewritePropertyLookupOperator(leftPropertyLookup, pgsql.Text) + expression.ROperand = rewrittenROperand + } else if rewrittenROperand, rewritten := rewriteJSONScalarEqualityOperand(expression.ROperand); rewritten { leftPropertyLookup.Operator = pgsql.OperatorJSONField expression.ROperand = rewrittenROperand } else if rOperandTypeHint == pgsql.AnyArray { @@ -415,7 +390,10 @@ func rewritePropertyLookupOperands(kindMapper *contextAwareKindMapper, expressio // for special (like, ilike, etc.) character classes case pgsql.OperatorEquals, pgsql.OperatorCypherNotEquals: - if rewrittenLOperand, rewritten := rewriteJSONScalarEqualityOperand(kindMapper, expression.LOperand); rewritten { + if rewrittenLOperand, rewritten := rewriteStringEqualityOperand(expression.LOperand); rewritten { + expression.LOperand = rewrittenLOperand + expression.ROperand = rewritePropertyLookupOperator(rightPropertyLookup, pgsql.Text) + } else if rewrittenLOperand, rewritten := rewriteJSONScalarEqualityOperand(expression.LOperand); rewritten { expression.LOperand = rewrittenLOperand rightPropertyLookup.Operator = pgsql.OperatorJSONField } else if lOperandTypeHint == pgsql.AnyArray { @@ -886,6 +864,101 @@ func jsonFieldPropertyLookup(propertyLookup *pgsql.BinaryExpression) *pgsql.Bina return pgsql.NewBinaryExpression(propertyLookup.LOperand, pgsql.OperatorJSONField, propertyLookup.ROperand) } +func jsonTextPropertyLookup(propertyLookup *pgsql.BinaryExpression) *pgsql.BinaryExpression { + return pgsql.NewBinaryExpression(propertyLookup.LOperand, pgsql.OperatorJSONTextField, propertyLookup.ROperand) +} + +func jsonbTypeof(expression pgsql.Expression) pgsql.Expression { + return pgsql.FunctionCall{ + Function: pgsql.FunctionJSONBTypeof, + Parameters: []pgsql.Expression{expression}, + } +} + +func jsonbStringTypeCheck(propertyLookup *pgsql.BinaryExpression) pgsql.Expression { + return pgsql.NewBinaryExpression( + jsonbTypeof(jsonFieldPropertyLookup(propertyLookup)), + pgsql.OperatorEquals, + pgsql.NewLiteral("string", pgsql.Text), + ) +} + +func toJSONBTextOperand(expression pgsql.Expression) pgsql.Expression { + return pgsql.FunctionCall{ + Function: pgsql.FunctionToJSONB, + Parameters: []pgsql.Expression{ + pgsql.NewTypeCast(expression, pgsql.Text), + }, + CastType: pgsql.JSONB, + } +} + +func buildStringPropertyEqualityComparison(propertyLookup *pgsql.BinaryExpression, textOperand pgsql.Expression, propertyOnLeft bool, operator pgsql.Operator) pgsql.Expression { + textPropertyLookup := jsonTextPropertyLookup(propertyLookup) + + if propertyOnLeft { + return pgsql.NewBinaryExpression(textPropertyLookup, operator, textOperand) + } + + return pgsql.NewBinaryExpression(textOperand, operator, textPropertyLookup) +} + +func buildStringPropertyEqualityPredicate(expression *pgsql.BinaryExpression) (pgsql.Expression, bool) { + if !expression.Operator.IsIn(pgsql.OperatorEquals, pgsql.OperatorCypherNotEquals) { + return nil, false + } + + leftPropertyLookup, hasLeftPropertyLookup := expressionToPropertyLookupBinaryExpression(expression.LOperand) + rightPropertyLookup, hasRightPropertyLookup := expressionToPropertyLookupBinaryExpression(expression.ROperand) + + if hasLeftPropertyLookup && leftPropertyLookup.Operator == pgsql.OperatorJSONTextField { + if _, rewritten := rewriteStringEqualityOperand(expression.ROperand); rewritten { + return buildStringPropertyComparisonPredicate(leftPropertyLookup, expression.ROperand, true, expression.Operator), true + } + } + + if hasRightPropertyLookup && rightPropertyLookup.Operator == pgsql.OperatorJSONTextField { + if _, rewritten := rewriteStringEqualityOperand(expression.LOperand); rewritten { + return buildStringPropertyComparisonPredicate(rightPropertyLookup, expression.LOperand, false, expression.Operator), true + } + } + + return nil, false +} + +func buildStringPropertyComparisonPredicate(propertyLookup *pgsql.BinaryExpression, textOperand pgsql.Expression, propertyOnLeft bool, operator pgsql.Operator) pgsql.Expression { + stringComparison := buildStringPropertyEqualityComparison(propertyLookup, textOperand, propertyOnLeft, operator) + + if operator == pgsql.OperatorEquals { + return pgsql.NewParenthetical(pgsql.NewBinaryExpression( + jsonbStringTypeCheck(propertyLookup), + pgsql.OperatorAnd, + stringComparison, + )) + } + + nonStringTypeCheck := pgsql.NewBinaryExpression( + jsonbTypeof(jsonFieldPropertyLookup(propertyLookup)), + pgsql.OperatorCypherNotEquals, + pgsql.NewLiteral("string", pgsql.Text), + ) + nonStringComparison := pgsql.NewBinaryExpression( + jsonFieldPropertyLookup(propertyLookup), + pgsql.OperatorCypherNotEquals, + toJSONBTextOperand(textOperand), + ) + + return pgsql.NewParenthetical(pgsql.NewBinaryExpression( + pgsql.NewBinaryExpression( + jsonbStringTypeCheck(propertyLookup), + pgsql.OperatorAnd, + stringComparison, + ), + pgsql.OperatorOr, + pgsql.NewBinaryExpression(nonStringTypeCheck, pgsql.OperatorAnd, nonStringComparison), + )) +} + func buildEmptyArrayPropertyComparison(propertyLookup *pgsql.BinaryExpression, negated bool) *pgsql.BinaryExpression { emptyArrayExpression := pgsql.NewBinaryExpression( jsonFieldPropertyLookup(propertyLookup), @@ -1205,6 +1278,8 @@ func (s *ExpressionTreeTranslator) rewriteBinaryExpression(newExpression *pgsql. } s.PushOperand(pgsql.NewParenthetical(expandedExpression)) + } else if rewrittenExpression, rewritten := buildStringPropertyEqualityPredicate(newExpression); rewritten { + s.PushOperand(rewrittenExpression) } else { s.PushOperand(newExpression) } @@ -1218,6 +1293,8 @@ func (s *ExpressionTreeTranslator) rewriteBinaryExpression(newExpression *pgsql. } s.PushOperand(pgsql.NewParenthetical(expandedExpression)) + } else if rewrittenExpression, rewritten := buildStringPropertyEqualityPredicate(newExpression); rewritten { + s.PushOperand(rewrittenExpression) } else { s.PushOperand(newExpression) } diff --git a/cypher/models/pgsql/translate/property.go b/cypher/models/pgsql/translate/property.go index 0d34f0a2..29f44fb7 100644 --- a/cypher/models/pgsql/translate/property.go +++ b/cypher/models/pgsql/translate/property.go @@ -77,6 +77,8 @@ func (s *Translator) buildPatternPropertyConstraints(binding *BoundIdentifier, p if newConstraint, err := s.treeTranslator.PopBinaryExpression(pgsql.OperatorEquals); err != nil { return nil, err + } else if rewrittenConstraint, rewritten := buildStringPropertyEqualityPredicate(newConstraint); rewritten { + propertyConstraints = pgsql.OptionalAnd(propertyConstraints, rewrittenConstraint) } else { propertyConstraints = pgsql.OptionalAnd(propertyConstraints, newConstraint) } From 9a5f2e56ae7a5256a67e841bfc5578d45e6ee475 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 19:56:51 -0700 Subject: [PATCH 063/114] test(pgsql): cover typed string equality translation --- .../pgsql/test/translation_cases/create.sql | 1 + .../pgsql/test/translation_cases/delete.sql | 1 + .../test/translation_cases/multipart.sql | 11 +++--- .../pgsql/test/translation_cases/nodes.sql | 33 +++++++++-------- .../test/translation_cases/parameters.sql | 3 +- .../translation_cases/pattern_binding.sql | 13 ++++--- .../translation_cases/pattern_expansion.sql | 19 +++++----- .../translation_cases/pattern_rewriting.sql | 1 + .../test/translation_cases/quantifiers.sql | 1 + .../translation_cases/scalar_aggregation.sql | 1 + .../test/translation_cases/shortest_paths.sql | 15 ++++---- .../translation_cases/stepwise_traversal.sql | 15 ++++---- .../pgsql/test/translation_cases/unwind.sql | 3 +- .../pgsql/test/translation_cases/update.sql | 11 +++--- .../models/pgsql/translate/expression_test.go | 37 ++++++++++++++----- 15 files changed, 99 insertions(+), 66 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/create.sql b/cypher/models/pgsql/test/translation_cases/create.sql index 1458421e..55c74074 100644 --- a/cypher/models/pgsql/test/translation_cases/create.sql +++ b/cypher/models/pgsql/test/translation_cases/create.sql @@ -73,3 +73,4 @@ with s0 as (select nextval(pg_get_serial_sequence('node', 'id'))::int8 as n0_id) -- case: match (a:NodeKind1) with a create (b:NodeKind2 {source: a.name}) return a, b with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, nextval(pg_get_serial_sequence('node', 'id'))::int8 as n1_id from s0), s3 as (insert into node (graph_id, id, kind_ids, properties) select 0, s2.n1_id, array [2]::int2[], jsonb_build_object('source', ((s2.n0).properties ->> 'name'))::jsonb from s2 returning id as n1_id, (id, kind_ids, properties)::nodecomposite as n1), s4 as (select s2.n0 as n0, s3.n1 as n1 from s2, s3 where s3.n1_id = s2.n1_id) select s4.n0 as a, s4.n1 as b from s4; + diff --git a/cypher/models/pgsql/test/translation_cases/delete.sql b/cypher/models/pgsql/test/translation_cases/delete.sql index ab59796e..c6695b5d 100644 --- a/cypher/models/pgsql/test/translation_cases/delete.sql +++ b/cypher/models/pgsql/test/translation_cases/delete.sql @@ -22,3 +22,4 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e -- case: match ()-[]->()-[r:EdgeKind1]->() delete r with s0 as (select e0.id as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3]::int2[]) and e1.id != s0.e0), s2 as (delete from edge e2 using s1 where (s1.e1).id = e2.id) select 1; + diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index 6f51f513..b1e52d9a 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -21,7 +21,7 @@ with s0 as (select '1' as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'value'))::jsonb = to_jsonb((1)::int8)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n0 from s1), s2 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (n1.id = (s0.n0).id)) select s2.n1 as b from s2; -- case: match (n:NodeKind1) where n.value = 1 with n match (f) where f.name = 'me' with f match (b) where id(b) = id(f) return b -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'value'))::jsonb = to_jsonb((1)::int8)::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 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('me')::text)::jsonb)) select s3.n1 as n1 from s3), s4 as (select s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where (n2.id = (s2.n1).id)) select s4.n2 as b from s4; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'value'))::jsonb = to_jsonb((1)::int8)::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 from s0, node n1 where ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'me'))) select s3.n1 as n1 from s3), s4 as (select s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where (n2.id = (s2.n1).id)) select s4.n2 as b from s4; -- case: match (n:NodeKind1)-[:EdgeKind1*1..]->(:NodeKind2)-[:EdgeKind2]->(m:NodeKind1) where (n:NodeKind1 or n:NodeKind2) and n.enabled = true with m, collect(distinct(n)) as p where size(p) >= 10 return m with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]) and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select 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 and exists (select 1 from edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [4]::int2[]))), s3 as (select s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != all (s1.ep0)) select s3.n2 as n2, array_remove(coalesce(array_agg(distinct (s3.n0))::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i0 from s3 group by n2) select s0.n2 as m from s0 where (cardinality(s0.i0)::int >= 10); @@ -39,13 +39,13 @@ with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s1.n0).id as root_id from s1), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s3.path || e0.id from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s3.depth < 15 and not s3.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.next_id offset 0) n1 on true where s3.satisfied and (s1.n0).id = s3.root_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; -- case: match (n:NodeKind1) where n.objectid = 'S-1-5-21-1260426776-3623580948-1897206385-23225' match p = (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'objectid'))::jsonb = to_jsonb(('S-1-5-21-1260426776-3623580948-1897206385-23225')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'objectid')) = 'string' and (n0.properties ->> 'objectid') = 'S-1-5-21-1260426776-3623580948-1897206385-23225')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; -- case: match (g1:NodeKind1) where g1.name starts with 'test' with collect (g1.domain) as excludes match (d:NodeKind2) where d.name starts with 'other' and not d.name in excludes return d with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') like 'test%') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'domain'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (not (n1.properties ->> 'name') = any (s0.i0) and (n1.properties ->> 'name') like 'other%') and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]) select s2.n1 as d from s2; -- case: with 'a' as uname match (o:NodeKind1) where o.name starts with uname and o.domain = ' ' return o -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; +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 ((jsonb_typeof((n0.properties -> 'domain')) = 'string' and (n0.properties ->> 'domain') = ' ') 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 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 (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 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, 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 case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4 where (not (s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i0) as _unnest_elem))) limit 100; @@ -66,13 +66,13 @@ with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool)) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::text[], array []::text[])::text[], null)::text[] as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by lower(((s2.n0).properties ->> 'samaccountname'))::text), s3 as (select s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n3.id = e1.end_id where (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; -- case: match p =(n:NodeKind1)<-[r:EdgeKind1|EdgeKind2*..3]-(u:NodeKind1) where n.domain = 'test' with n, count(r) as incomingCount where incomingCount > 90 with collect(n) as lotsOfAdmins match p =(n:NodeKind1)<-[:EdgeKind1]-() where n in lotsOfAdmins return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'domain'))::jsonb = to_jsonb(('test')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select e1.id as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3]::nodecomposite[])::pathcomposite end as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'domain')) = 'string' and (n0.properties ->> 'domain') = 'test')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select e1.id as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3]::nodecomposite[])::pathcomposite end as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); -- case: match (u:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) with g match (g)<-[:EdgeKind1]-(u:NodeKind1) return g with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n1 as n1 from s1), s2 as (select s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select s2.n1 as g from s2; -- case: match (cg:NodeKind1) where cg.name =~ ".*TT" and cg.domain = "MY DOMAIN" with collect (cg.email) as emails match (o:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) where g.name starts with "blah" and not g.email in emails return o -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') ~ '.*TT' and ((n0.properties -> 'domain'))::jsonb = to_jsonb(('MY DOMAIN')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'email'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (not (n2.properties ->> 'email') = any (s0.i0) and (n2.properties ->> 'name') like 'blah%')) select s2.n1 as o from s2; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') ~ '.*TT' and (jsonb_typeof((n0.properties -> 'domain')) = 'string' and (n0.properties ->> 'domain') = 'MY DOMAIN')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'email'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s1), s2 as (select s0.i0 as i0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (not (n2.properties ->> 'email') = any (s0.i0) and (n2.properties ->> 'name') like 'blah%')) select s2.n1 as o from s2; -- case: match (e) match p = ()-[]->(e) return p limit 1 with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select case when (s1.n1).id is null or s1.e0 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite end as p from s1 limit 1; @@ -91,3 +91,4 @@ with s0 as (with s1 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties): -- 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, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 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, s2.n1 as n1 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 1162f06b..6feaf94e 100644 --- a/cypher/models/pgsql/test/translation_cases/nodes.sql +++ b/cypher/models/pgsql/test/translation_cases/nodes.sql @@ -24,7 +24,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as n from s0 where ((array(select _kind.name from generate_subscripts((s0.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s0.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[] = array ['NodeKind1', 'NodeKind2']::text[]); -- case: match (n) where n.name = 'n3' with labels(n) as labels return labels, size(labels) -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb)) select (array(select _kind.name from generate_subscripts((s1.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s1.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[] as i0 from s1) select s0.i0 as labels, cardinality(s0.i0)::int from s0; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n3'))) select (array(select _kind.name from generate_subscripts((s1.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s1.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[] as i0 from s1) select s0.i0 as labels, cardinality(s0.i0)::int from s0; -- case: match (n) with 1 as _kind_idx, n return labels(n), _kind_idx with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select 1 as i0, s1.n0 as n0 from s1) select (array(select _kind.name from generate_subscripts((s0.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s0.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[], s0.i0 as _kind_idx from s0; @@ -45,10 +45,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 (coalesce((n0.properties ->> 'name'), '')::text = '1234')) select s0.n0 as n from s0; -- case: match (n) where n.name = '1234' return n -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb)) select s0.n0 as n from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '1234'))) select s0.n0 as n from s0; -- case: match (n:NodeKind1 {name: "SOME NAME"}) return n -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('SOME NAME')::text)::jsonb) select s0.n0 as n from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'SOME NAME')) select s0.n0 as n from s0; -- case: match (n) where n.objectid in $p return n -- cypher_params: {"p":["1","2","3"]} @@ -58,7 +58,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from -- case: match (s) where s.name = $myParam return s -- cypher_params: {"myParam":"123"} -- pgsql_params:{"pi0":"123"} -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb((@pi0::text)::text)::jsonb)) 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 ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = @pi0::text))) select s0.n0 as s from s0; -- case: match (s) return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0; @@ -79,7 +79,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.kind_ids operator (pg_catalog.@>) array [2]::int2[])) select s0.n0 as s from s0; -- case: match (s) where s.name = '1234' return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb)) 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 ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '1234'))) select s0.n0 as s from s0; -- case: match (s:NodeKind1), (e:NodeKind2) where s.selected or s.tid = e.tid and e.enabled return s, e with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((((s0.n0).properties ->> 'selected'))::bool or ((s0.n0).properties -> 'tid') = (n1.properties -> 'tid') and ((n1.properties ->> 'enabled'))::bool) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]) select s1.n0 as s, s1.n1 as e from s1; @@ -88,7 +88,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'value'))::int8 + 2 / 3 > 10)) select s0.n0 as s from s0; -- case: match (s), (e) where s.name = 'n1' return s, e.name as othername -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1) select s1.n0 as s, ((s1.n1).properties -> 'name') as othername from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1'))), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1) select s1.n0 as s, ((s1.n1).properties -> 'name') as othername from s1; -- case: match (s) where s.name in ['option 1', 'option 2'] return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'name') = any (array ['option 1', 'option 2']::text[]))) select s0.n0 as s from s0; @@ -103,13 +103,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ? 'system_tags' and not (n0.properties -> 'system_tags') = ('null')::jsonb) and not (n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] or n0.kind_ids operator (pg_catalog.@>) array [2]::int2[]))) select (s0.n0).id from s0; -- case: match (s), (e) where s.name = '1234' and e.other = 1234 return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb)), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'other'))::jsonb = to_jsonb((1234)::int8)::jsonb)) select s1.n0 as s from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '1234'))), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'other'))::jsonb = to_jsonb((1234)::int8)::jsonb)) select s1.n0 as s from s1; -- case: match (s), (e) where s.name = '1234' or e.other = 1234 return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((((s0.n0).properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb or ((n1.properties -> 'other'))::jsonb = to_jsonb((1234)::int8)::jsonb)) select s1.n0 as s from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((jsonb_typeof(((s0.n0).properties -> 'name')) = 'string' and ((s0.n0).properties ->> 'name') = '1234') or ((n1.properties -> 'other'))::jsonb = to_jsonb((1234)::int8)::jsonb)) select s1.n0 as s from s1; -- case: match (n), (k) where n.name = '1234' and k.name = '1234' match (e) where e.name = n.name return k, e -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb)), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb)), s2 as (select s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1, node n2 where ((n2.properties -> 'name') = ((s1.n0).properties -> 'name'))) select s2.n1 as k, s2.n2 as e from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '1234'))), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = '1234'))), s2 as (select s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1, node n2 where ((n2.properties -> 'name') = ((s1.n0).properties -> 'name'))) select s2.n1 as k, s2.n2 as e from s2; -- case: match (n) return n skip 5 limit 10 with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as n from s0 offset 5 limit 10; @@ -151,10 +151,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'))::timestamp without time zone = ('2019-06-01T18:40:32.142')::timestamp without time zone)) select s0.n0 as s from s0; -- case: match (s) where not (s.name = '123') return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (not (((n0.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb))) 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 (not ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '123')))) select s0.n0 as s from s0; -- case: match (s) where s.isassignabletorole = 'true' return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'isassignabletorole') = 'true')) 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 ((jsonb_typeof((n0.properties -> 'isassignabletorole')) = 'string' and (n0.properties ->> 'isassignabletorole') = 'true'))) select s0.n0 as s from s0; -- case: match (s) where s.isassignabletorole = true return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'isassignabletorole'))::jsonb = to_jsonb((true)::bool)::jsonb)) select s0.n0 as s from s0; @@ -214,13 +214,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n1 on n1.id = e0.end_id where (s0.n0).id = e0.start_id), s2 as (select s1.e0 as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s1.e0) select count(*) > 0 from s2)); -- case: match (s) where not (s)-[{prop: 'a'}]-({name: 'n3'}) return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on ((s0.n0).id = e0.end_id or (s0.n0).id = e0.start_id) join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and (n1.id = e0.end_id or n1.id = e0.start_id) where ((s0.n0).id <> n1.id) and ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb) select count(*) > 0 from s1)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on ((s0.n0).id = e0.end_id or (s0.n0).id = e0.start_id) join node n1 on (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n3') and (n1.id = e0.end_id or n1.id = e0.start_id) where ((s0.n0).id <> n1.id) and (jsonb_typeof((e0.properties -> 'prop')) = 'string' and (e0.properties ->> 'prop') = 'a')) select count(*) > 0 from s1)); -- case: match (s) where not (s)<-[{prop: 'a'}]-({name: 'n3'}) return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and n1.id = e0.start_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb and (s0.n0).id = e0.end_id) select count(*) > 0 from s1)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n3') and n1.id = e0.start_id where (jsonb_typeof((e0.properties -> 'prop')) = 'string' and (e0.properties ->> 'prop') = 'a') and (s0.n0).id = e0.end_id) select count(*) > 0 from s1)); -- case: match (n:NodeKind1) where n.distinguishedname = toUpper('admin') return n -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'distinguishedname'))::jsonb = to_jsonb((upper('admin')::text)::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'distinguishedname') = upper('admin')::text) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; -- case: match (n:NodeKind1) where n.distinguishedname starts with toUpper('admin') return n with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (cypher_starts_with((n0.properties ->> 'distinguishedname'), (upper('admin')::text)::text)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; @@ -232,7 +232,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (cypher_ends_with((n0.properties ->> 'distinguishedname'), (upper('admin')::text)::text)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; -- case: match (s) where not (s)-[{prop: 'a'}]->({name: 'n3'}) return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and n1.id = e0.end_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb and (s0.n0).id = e0.start_id) select count(*) > 0 from s1)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n3') and n1.id = e0.end_id where (jsonb_typeof((e0.properties -> 'prop')) = 'string' and (e0.properties ->> 'prop') = 'a') and (s0.n0).id = e0.start_id) select count(*) > 0 from s1)); -- case: match (s) where not (s)-[]-() return id(s) with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (s0.n0).id from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id)); @@ -344,7 +344,8 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'distinguishedname') = ((s0.n0).properties ->> 'unknown') || (n1.properties ->> 'unknown')) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select s0.n0 as n0, s1.n1 as n1 from s0 left outer join s1 on (s0.n0 = s1.n0)), s3 as (select s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where ((n2.properties -> 'distinguishedname') <> ((s2.n0).properties -> 'otherunknown')) and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s4 as (select s2.n0 as n0, s2.n1 as n1, s3.n2 as n2 from s2 left outer join s3 on (s2.n1 = s3.n1) and (s2.n0 = s3.n0)) select s4.n0 as n, s4.n1 as m, s4.n2 as o from s4; -- case: match (n) where n.name = "alpha' || (SELECT inet_server_addr()::text::int) || '" return n -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('alpha'' || (SELECT inet_server_addr()::text::int) || ''')::text)::jsonb)) select s0.n0 as n from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'alpha'' || (SELECT inet_server_addr()::text::int) || '''))) select s0.n0 as n from s0; -- case: match (g:NodeKind2) where not ((g)<-[:EdgeKind1]-(:NodeKind1)) return g 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 [2]::int2[]) select s0.n0 as g from s0 where (not ((with s1 as (select s0.n0 as n0 from edge e0 join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (s0.n0).id = e0.end_id) select count(*) > 0 from s1))); + diff --git a/cypher/models/pgsql/test/translation_cases/parameters.sql b/cypher/models/pgsql/test/translation_cases/parameters.sql index e1212eb8..26a6a5f8 100644 --- a/cypher/models/pgsql/test/translation_cases/parameters.sql +++ b/cypher/models/pgsql/test/translation_cases/parameters.sql @@ -29,9 +29,10 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from -- case: match (n) where n.isassignabletorole = $p0 return n -- cypher_params: {"p0":"true"} -- pgsql_params:{"pi0":"true"} -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'isassignabletorole') = @pi0::text)) select s0.n0 as n from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'isassignabletorole')) = 'string' and (n0.properties ->> 'isassignabletorole') = @pi0::text))) select s0.n0 as n from s0; -- case: match (n) where n.isassignabletorole = $p0 return n -- cypher_params: {"p0":true} -- pgsql_params:{"pi0":true} with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'isassignabletorole'))::jsonb = to_jsonb((@pi0::bool)::bool)::jsonb)) select s0.n0 as n 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 622e2a29..35883e93 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -36,16 +36,16 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select e0.id as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s0.e0) select s1.n2 as e from s1; -- case: match ()-[r1]->()-[r2]->()-[]->() where r1.name = 'a' and r2.name = 'b' return r1 -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties -> 'name'))::jsonb = to_jsonb(('a')::text)::jsonb)), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where (((e1.properties -> 'name'))::jsonb = to_jsonb(('b')::text)::jsonb) and e1.id != (s0.e0).id), s2 as (select s1.e0 as e0, s1.e1 as e1, s1.n1 as n1, s1.n2 as n2 from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id where e2.id != (s1.e0).id and e2.id != (s1.e1).id) select s2.e0 as r1 from s2; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((jsonb_typeof((e0.properties -> 'name')) = 'string' and (e0.properties ->> 'name') = 'a'))), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where ((jsonb_typeof((e1.properties -> 'name')) = 'string' and (e1.properties ->> 'name') = 'b')) and e1.id != (s0.e0).id), s2 as (select s1.e0 as e0, s1.e1 as e1, s1.n1 as n1, s1.n2 as n2 from s1 join edge e2 on (s1.n2).id = e2.start_id join node n3 on n3.id = e2.end_id where e2.id != (s1.e0).id and e2.id != (s1.e1).id) select s2.e0 as r1 from s2; -- case: match p = (a)-[]->()<-[]-(f) where a.name = 'value' and f.is_target return p -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('value')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id where e1.id != s0.e0) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null or s1.e1 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1; +with s0 as (select e0.id 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 ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'value')) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, e1.id as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on (((n2.properties ->> 'is_target'))::bool) and n2.id = e1.start_id where e1.id != s0.e0) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null or s1.e1 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1; -- case: match p = ()-[*..]->() return p limit 1 with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, false, e0.start_id = e0.end_id, array [e0.id] from edge e0 union all select s1.root_id, e0.end_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 1) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1; -- case: match p = (s)-[*..]->(i)-[]->() where id(s) = 1 and i.name = 'n3' return p limit 1 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (n0.id = 1)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id)), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0) limit 1) select case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null or s2.e1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 1; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n3'))), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, (n0.id = 1), e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id union all select s1.root_id, e0.start_id, s1.depth + 1, (n0.id = 1), false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0) limit 1) select case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null or s2.e1 is null or (s2.n2).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2]::nodecomposite[])::pathcomposite end as p from s2 limit 1; -- case: match p = ()-[e:EdgeKind1]->()-[:EdgeKind1*..]->() return e, p with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id where e1.kind_id = any (array [3]::int2[]) union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) and e1.kind_id = any (array [3]::int2[]) offset 0) e1 on true where s2.depth < 15 and not s2.is_cycle) select s0.e0 as e0, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where (s0.n1).id = s2.root_id) select s1.e0 as e, case when (s1.n0).id is null or (s1.e0).id is null or (s1.n1).id is null or s1.ep0 is null or (s1.n2).id is null then null else ordered_edges_to_path(s1.n0, array [s1.e0]::edgecomposite[] || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1, s1.n2]::nodecomposite[])::pathcomposite end as p from s1; @@ -66,13 +66,13 @@ with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposi with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'samaccountname') = any (array ['foo', 'bar']::text[])) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, (coalesce((n1.properties ->> 'system_tags'), '')::text like '%admin_tier_0%'), false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 3 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1 limit 1000; -- case: match (x:NodeKind1) where x.name = 'foo' match (y:NodeKind2) where y.name = 'bar' match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'foo')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'bar')) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match (y:NodeKind2{name:'bar'}) match p=(x)-[:EdgeKind1]->(y) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'foo')), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'bar')), s2 as (select e0.id as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e0 on (s1.n0).id = e0.start_id and (s1.n1).id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2; -- case: match (x:NodeKind1{name:'foo'}) match p=(x)-[:EdgeKind1]->(y:NodeKind2{name:'bar'}) return p -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((n0.properties -> 'name'))::jsonb = to_jsonb(('foo')::text)::jsonb), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n1.properties -> 'name'))::jsonb = to_jsonb(('bar')::text)::jsonb and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'foo')), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'bar') and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select case when (s1.n0).id is null or s1.e0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; -- case: match (e) match p = ()-[]->(e) return p limit 1 with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select case when (s1.n1).id is null or s1.e0 is null or (s1.n0).id is null then null else ordered_edges_to_path(s1.n1, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s1.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n1, s1.n0]::nodecomposite[])::pathcomposite end as p from s1 limit 1; @@ -91,3 +91,4 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as -- case: MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE NONE(x in TAIL(r) WHERE NOT type(x) = 'Contains') 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((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where (((select count(*)::int from unnest(coalesce((s0.e0)[2:], array []::edgecomposite[])::edgecomposite[]) as i0 where (not i0.kind_id = 12)) = 0 and coalesce((s0.e0)[2:], array []::edgecomposite[])::edgecomposite[] 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 f1eb5121..895d3c41 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_expansion.sql @@ -30,22 +30,22 @@ with s0 as (with recursive s1(root_id, next_id, depth, satisfied, is_cycle, path with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0; -- case: match (n)-[*..]->(e:NodeKind1) where n.name = 'n1' return e -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(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n1 as e from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1'))), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n1 as e from s0; -- case: match (n)-[*..]->(e:NodeKind1) where n.name = 'n2' return n -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n0 as n from s0; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n2'))), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select s0.n0 as n from s0; -- case: match (n)-[*..]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0)) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1'))), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied), s2 as (select s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0)) select s2.n2 as l from s2; -- case: match (n)-[*2..3]->(e:NodeKind1)-[]->(l) where n.name = 'n1' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0)) select s2.n2 as l from s2; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1'))), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 3 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.depth >= 2 and s1.satisfied), s2 as (select s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != all (s0.ep0)) select s2.n2 as l from s2; -- case: match (n)-[]->(e:NodeKind1)-[*2..3]->(l) where n.name = 'n1' return l -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select s0.e0 as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; +with s0 as (select e0.id 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 ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1')) and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n1).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e1.start_id, e1.end_id, 1, false, e1.start_id = e1.end_id, array [e1.id] from s2_seed join edge e1 on e1.start_id = s2_seed.root_id union all select s2.root_id, e1.end_id, s2.depth + 1, false, false, s2.path || e1.id from s2 join lateral (select e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties from edge e1 where e1.start_id = s2.next_id and e1.id != all (s2.path) offset 0) e1 on true where s2.depth < 3 and not s2.is_cycle) select s0.e0 as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.root_id offset 0) n1 on true join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s2.next_id offset 0) n2 on true where s2.depth >= 2 and (s0.n1).id = s2.root_id) select s1.n2 as l from s1; -- case: match (n)-[*..]->(e)-[:EdgeKind1|EdgeKind2]->()-[*..]->(l) where n.name = 'n1' and e.name = 'n2' return l -with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, (((n1.properties -> 'name'))::jsonb = to_jsonb(('n2')::text)::jsonb), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[]))), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[]) and e1.id != all (s0.ep0)), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.e1 as e1, s2.ep0 as ep0, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1'))), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n2')), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id union all select s1.root_id, e0.end_id, s1.depth + 1, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n2')), false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied and exists (select 1 from edge e1 join node n2 on n2.id = e1.end_id where n1.id = e1.start_id and e1.kind_id = any (array [3, 4]::int2[]))), s2 as (select e1.id as e1, s0.ep0 as ep0, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.kind_id = any (array [3, 4]::int2[]) and e1.id != all (s0.ep0)), s3 as (with recursive s4_seed(root_id) as not materialized (select distinct (s2.n2).id as root_id from s2), s4(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.start_id, e2.end_id, 1, false, e2.start_id = e2.end_id, array [e2.id] from s4_seed join edge e2 on e2.start_id = s4_seed.root_id union all select s4.root_id, e2.end_id, s4.depth + 1, false, false, s4.path || e2.id from s4 join lateral (select e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties from edge e2 where e2.start_id = s4.next_id and e2.id != all (s4.path) offset 0) e2 on true where s4.depth < 15 and not s4.is_cycle) select s2.e1 as e1, s2.ep0 as ep0, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s2, s4 join lateral (select n2.id, n2.kind_ids, n2.properties from node n2 where n2.id = s4.root_id offset 0) n2 on true join lateral (select n3.id, n3.kind_ids, n3.properties from node n3 where n3.id = s4.next_id offset 0) n3 on true where (s2.n2).id = s4.root_id) select s3.n3 as l from s3; -- case: match p = (:NodeKind1)-[:EdgeKind1*1..]->(n:NodeKind2) where 'admin_tier_0' in split(n.system_tags, ' ') return p limit 1000 with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ('admin_tier_0' = any (string_to_array((n1.properties ->> 'system_tags'), ' ')::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 1000) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 1000; @@ -57,7 +57,7 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((n0.properties ->> 'objectid') like '%1234') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, ((n1.properties ->> 'objectid') like '%4567') and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0; -- case: match p = (m:NodeKind2)-[:EdgeKind1*1..]->(n:NodeKind1) where n.objectid = '1234' return p limit 10 -with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -> 'objectid'))::jsonb = to_jsonb(('1234')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((jsonb_typeof((n1.properties -> 'objectid')) = 'string' and (n1.properties ->> 'objectid') = '1234')) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, n0.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, e0.id || s1.path from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3]::int2[]) offset 0) e0 on true join node n0 on n0.id = e0.start_id where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.next_id offset 0) n0 on true where s1.satisfied limit 10) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; -- case: match p = (:NodeKind1)<-[:EdgeKind1|EdgeKind2*..]-() return p limit 10 with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, false, e0.end_id = e0.start_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s1.root_id, e0.start_id, s1.depth + 1, false, false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s1.depth < 15 and not s1.is_cycle) select s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true limit 10) select case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 limit 10; @@ -78,7 +78,8 @@ with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as with s0 as (select e0.id 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 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[]) and e1.id != s0.e0), 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, 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 case when (s2.n0).id is null or s2.e0 is null or (s2.n1).id is null or s2.e1 is null or (s2.n2).id is null or s2.ep0 is null or (s2.n3).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e0]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s2.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1, s2.n2, s2.n3]::nodecomposite[])::pathcomposite end 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 (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; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'solo')) 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, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'solo')) 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, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'solo')) 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, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'solo')) 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 (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 (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; +with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'zero-source')) 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, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'zero-target')) 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, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'zero-target')) 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, ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'zero-target')) 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 (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/pattern_rewriting.sql b/cypher/models/pgsql/test/translation_cases/pattern_rewriting.sql index 21dcea9d..01ee8044 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_rewriting.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_rewriting.sql @@ -16,3 +16,4 @@ -- case: match (s:NodeKind1) return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as s from s0; + diff --git a/cypher/models/pgsql/test/translation_cases/quantifiers.sql b/cypher/models/pgsql/test/translation_cases/quantifiers.sql index 01b88d38..c4d2f249 100644 --- a/cypher/models/pgsql/test/translation_cases/quantifiers.sql +++ b/cypher/models/pgsql/test/translation_cases/quantifiers.sql @@ -46,3 +46,4 @@ 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); + diff --git a/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql b/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql index d4668bff..21458c72 100644 --- a/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql +++ b/cypher/models/pgsql/test/translation_cases/scalar_aggregation.sql @@ -109,3 +109,4 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit -- case: MATCH (n) WITH sum(n.age) / count(n) AS avg_age RETURN avg_age with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select sum((((s1.n0).properties ->> 'age'))::float8)::numeric / count(s1.n0)::int8 as i0 from s1) select s0.i0 as avg_age from s0; + diff --git a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql index b6693945..91bbce1d 100644 --- a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql +++ b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql @@ -19,11 +19,11 @@ 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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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);"} +-- 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 (jsonb_typeof((n1.properties -\u003e 'name')) = 'string' and (n1.properties -\u003e\u003e 'name') = '123')) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id 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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 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);"} +-- 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 ((jsonb_typeof((n1.properties -\u003e 'name')) = 'string' and (n1.properties -\u003e\u003e 'name') = '123'))) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_terminal_filter where traversal_terminal_filter.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id 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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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 @@ -55,8 +55,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 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 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end 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);"} -with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n1.id, n0.id from node n1, node n0 where (((n1.properties -> ''name''))::jsonb = to_jsonb((''123'')::text)::jsonb) and (coalesce((n0.properties ->> ''system_tags''), '''')::text like ''%admin_tier_0%'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id is not null and n0.id is not null;')::text)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where ((s0.n1).id <> (s0.n0).id); +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((jsonb_typeof((n1.properties -\u003e 'name')) = 'string' and (n1.properties -\u003e\u003e 'name') = '123'))) select e0.start_id, e0.end_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s1.root_id and traversal_pair_filter.terminal_id = e0.end_id), false, s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[]) select e0.end_id, e0.start_id, 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = e0.end_id), e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.end_id = s1_seed.root_id where e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s1.root_id), false, e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id where e0.kind_id = any (array [3]::int2[]) and e0.id != all (s1.path);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n1.id, n0.id from node n1, node n0 where ((jsonb_typeof((n1.properties -> ''name'')) = ''string'' and (n1.properties ->> ''name'') = ''123'')) and (coalesce((n0.properties ->> ''system_tags''), '''')::text like ''%admin_tier_0%'') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id is not null and n0.id is not null;')::text)) select 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 case when (s0.n0).id is null or s0.ep0 is null or (s0.n1).id is null then null else ordered_edges_to_path(s0.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s0.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite end as p from s0 where ((s0.n1).id <> (s0.n0).id); -- case: match p=shortestPath((a)-[:EdgeKind1*]->(b:NodeKind1)) where a <> b return p -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) 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);"} @@ -88,8 +88,9 @@ with s0 as (with s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, pa -- case: MATCH (g1:Group) MATCH (g2:Group) WHERE g1.name STARTS WITH 'DOMAIN USERS@' AND g2.name STARTS WITH 'DOMAIN ADMINS@' MATCH p=shortestPath((g1)-[:AddAllowedToAct|AddMember|AdminTo|AllExtendedRights|AllowedToDelegate|CanRDP|Contains|ForceChangePassword|GenericAll|GenericWrite|GetChangesAll|GetChanges|HasSession|MemberOf|Owns|ReadLAPSPassword|SQLAdmin|TrustedBy|WriteAccountRestrictions|WriteOwner*1..]->(g2)) WHERE NONE(r IN relationships(p) WHERE type(r) = 'HasSession' AND startNode(r).name = 'DF-WIN10-DEV01.DUMPSTER.FIRE') 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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [14, 15, 16, 17, 18, 19, 12, 20, 21, 22, 23, 24, 7, 25, 26, 27, 28, 29, 30, 31]::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 [13]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') like 'DOMAIN ADMINS@%' and ((s0.n0).properties ->> 'name') like 'DOMAIN USERS@%') and n1.kind_ids operator (pg_catalog.@>) array [13]::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 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 case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('DF-WIN10-DEV01.DUMPSTER.FIRE')::text)::jsonb and i0.kind_id = 7)) = 0 and ((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[] is not null)::bool); +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 [13]::int2[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> 'name') like 'DOMAIN ADMINS@%' and ((s0.n0).properties ->> 'name') like 'DOMAIN USERS@%') and n1.kind_ids operator (pg_catalog.@>) array [13]::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 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 case when (s2.n0).id is null or s2.ep0 is null or (s2.n1).id is null then null else ordered_edges_to_path(s2.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s2.n0, s2.n1]::nodecomposite[])::pathcomposite end as p from s2 where (((select count(*)::int from unnest(((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[]) as i0 where ((jsonb_typeof(((start_node(i0)::nodecomposite).properties -> 'name')) = 'string' and ((start_node(i0)::nodecomposite).properties ->> 'name') = 'DF-WIN10-DEV01.DUMPSTER.FIRE') and i0.kind_id = 7)) = 0 and ((select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id))::edgecomposite[] is not null)::bool); -- case: match p=shortestPath((s:NodeKind1)-[:EdgeKind1|HasSession*1..]->(d:NodeKind1)) where s.name = 'path-filter-src' and d.name = 'path-filter-dst' with p where none(r in relationships(p) where type(r) = 'HasSession' and startNode(r).name = 'blocked-session-host') return p --- 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.properties -\u003e 'name'))::jsonb = to_jsonb(('path-filter-src')::text)::jsonb) 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 s2_seed join edge e0 on e0.start_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::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 s2.root_id, e0.end_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s2.root_id and traversal_pair_filter.terminal_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, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s2.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s2_seed(root_id) as not materialized (select n1.id as root_id from node n1 where (((n1.properties -\u003e 'name'))::jsonb = to_jsonb(('path-filter-dst')::text)::jsonb) and n1.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 s2_seed join edge e0 on e0.end_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e0.start_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s2.root_id), false, e0.id || s2.path from backward_front s2 join edge e0 on e0.end_id = s2.next_id where e0.kind_id = any (array [3, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s2.root_id and backward_visited.id = e0.start_id);"} -with s0 as (with s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_sp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15, ('')::text, ('')::text, ('insert into traversal_pair_filter (root_id, terminal_id) select distinct n0.id, n1.id from node n0, node n1 where (((n0.properties -> ''name''))::jsonb = to_jsonb((''path-filter-src'')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (((n1.properties -> ''name''))::jsonb = to_jsonb((''path-filter-dst'')::text)::jsonb) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id is not null and n1.id is not null;')::text)) select 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 case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as pc0 from s1) select s0.pc0 as p from s0 where (((select count(*)::int from unnest(((s0.pc0).edges)::edgecomposite[]) as i0 where ((((start_node(i0)::nodecomposite).properties -> 'name'))::jsonb = to_jsonb(('blocked-session-host')::text)::jsonb and i0.kind_id = 7)) = 0 and ((s0.pc0).edges)::edgecomposite[] is not null)::bool); +-- 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 ((jsonb_typeof((n0.properties -\u003e 'name')) = 'string' and (n0.properties -\u003e\u003e 'name') = 'path-filter-src')) 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 s2_seed join edge e0 on e0.start_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::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 s2.root_id, e0.end_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = s2.root_id and traversal_pair_filter.terminal_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, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from forward_visited where forward_visited.root_id = s2.root_id and forward_visited.id = e0.end_id);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) with s2_seed(root_id) as not materialized (select n1.id as root_id from node n1 where ((jsonb_typeof((n1.properties -\u003e 'name')) = 'string' and (n1.properties -\u003e\u003e 'name') = 'path-filter-dst')) and n1.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 s2_seed join edge e0 on e0.end_id = s2_seed.root_id where e0.kind_id = any (array [3, 7]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e0.start_id, s2.depth + 1, exists (select 1 from traversal_pair_filter where traversal_pair_filter.root_id = e0.start_id and traversal_pair_filter.terminal_id = s2.root_id), false, e0.id || s2.path from backward_front s2 join edge e0 on e0.end_id = s2.next_id where e0.kind_id = any (array [3, 7]::int2[]) and e0.id != all (s2.path) and not exists (select 1 from backward_visited where backward_visited.root_id = s2.root_id and backward_visited.id = e0.start_id);"} +with s0 as (with s1 as (with s2(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 ((jsonb_typeof((n0.properties -> ''name'')) = ''string'' and (n0.properties ->> ''name'') = ''path-filter-src'')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and ((jsonb_typeof((n1.properties -> ''name'')) = ''string'' and (n1.properties ->> ''name'') = ''path-filter-dst'')) and n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id is not null and n1.id is not null;')::text)) select 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 case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as pc0 from s1) select s0.pc0 as p from s0 where (((select count(*)::int from unnest(((s0.pc0).edges)::edgecomposite[]) as i0 where ((jsonb_typeof(((start_node(i0)::nodecomposite).properties -> 'name')) = 'string' and ((start_node(i0)::nodecomposite).properties ->> 'name') = 'blocked-session-host') and i0.kind_id = 7)) = 0 and ((s0.pc0).edges)::edgecomposite[] is not null)::bool); + diff --git a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql index c658814c..f48a2bcf 100644 --- a/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql +++ b/cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql @@ -51,7 +51,7 @@ select count(*)::int8 as the_count from edge e0 join node n0 on n0.id = e0.start with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0 limit 1; -- case: match ()-[r:EdgeKind1]->({name: "123"}) return count(r) as the_count -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb and n1.id = e0.end_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n1 on (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = '123') and n1.id = e0.end_id join node n0 on n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select count(s0.e0)::int8 as the_count from s0; -- case: match (s)-[r]->(e) where id(e) = $a and not (id(s) = $b) and (r:EdgeKind1 or r:EdgeKind2) and not (s.objectid ends with $c or e.objectid ends with $d) return distinct id(s), id(r), id(e) -- cypher_params: {"a":1,"b":2,"c":"123","d":"456"} @@ -59,7 +59,7 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n1 on n1.id = e0.end_id join node n0 on (not (n0.id = @pi1::float8)) and n0.id = e0.start_id where ((e0.kind_id = any (array [3]::int2[]) or e0.kind_id = any (array [4]::int2[]))) and (not (cypher_ends_with((n0.properties ->> 'objectid'), (@pi2::text)::text)::bool or cypher_ends_with((n1.properties ->> 'objectid'), (@pi3::text)::text)::bool) and n1.id = @pi0::float8)) select distinct (s0.n0).id, (s0.e0).id, (s0.n1).id from s0; -- case: match (s)-[r]->(e) where s.name = '123' and e:NodeKind1 and not r.property return s, r, e -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb) and n0.id = e0.start_id join node n1 on (n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]) and n1.id = e0.end_id where (not ((e0.properties ->> 'property'))::bool)) select s0.n0 as s, s0.e0 as r, s0.n1 as e from s0; +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 ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '123')) and n0.id = e0.start_id join node n1 on (n1.kind_ids operator (pg_catalog.@>) array [1]::int2[]) and n1.id = e0.end_id where (not ((e0.properties ->> 'property'))::bool)) select s0.n0 as s, s0.e0 as r, s0.n1 as e from s0; -- case: match ()-[r]->() where r.value = 42 return r with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties -> 'value'))::jsonb = to_jsonb((42)::int8)::jsonb)) select s0.e0 as r from s0; @@ -68,16 +68,16 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (((e0.properties ->> 'bool_prop'))::bool)) select s0.e0 as r from s0; -- case: match (n)-[r]->() where n.name = '123' return n, r -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '123')) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; -- case: match (n:NodeKind1)-[r]->() where n.name = '123' or n.name = '321' or n.name = '222' or n.name = '333' return n, r -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb or ((n0.properties -> 'name'))::jsonb = to_jsonb(('321')::text)::jsonb or ((n0.properties -> 'name'))::jsonb = to_jsonb(('222')::text)::jsonb or ((n0.properties -> 'name'))::jsonb = to_jsonb(('333')::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '123') or (jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '321') or (jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '222') or (jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '333')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.id = e0.end_id) select s0.n0 as n, s0.e0 as r from s0; -- case: match (s)-[r]->(e) where s.name = '123' and e.name = '321' return s, r, e -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb) and n0.id = e0.start_id join node n1 on (((n1.properties -> 'name'))::jsonb = to_jsonb(('321')::text)::jsonb) and n1.id = e0.end_id) select s0.n0 as s, s0.e0 as r, s0.n1 as e from s0; +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '123')) and n0.id = e0.start_id join node n1 on ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = '321')) and n1.id = e0.end_id) select s0.n0 as s, s0.e0 as r, s0.n1 as e from s0; -- case: match (f), (s)-[r]->(e) where not f.bool_field and s.name = '123' and e.name = '321' return f, s, r, e -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (not ((n0.properties ->> 'bool_field'))::bool)), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n1 on (((n1.properties -> 'name'))::jsonb = to_jsonb(('123')::text)::jsonb) and n1.id = e0.start_id join node n2 on (((n2.properties -> 'name'))::jsonb = to_jsonb(('321')::text)::jsonb) and n2.id = e0.end_id) select s1.n0 as f, s1.n1 as s, s1.e0 as r, s1.n2 as e from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (not ((n0.properties ->> 'bool_field'))::bool)), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, edge e0 join node n1 on ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = '123')) and n1.id = e0.start_id join node n2 on ((jsonb_typeof((n2.properties -> 'name')) = 'string' and (n2.properties ->> 'name') = '321')) and n2.id = e0.end_id) select s1.n0 as f, s1.n1 as s, s1.e0 as r, s1.n2 as e from s1; -- case: match ()-[e0]->(n)<-[e1]-() return e0, n, e1 with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.id = e1.start_id where e1.id != (s0.e0).id) select s1.e0 as e0, s1.n1 as n, s1.e1 as e1 from s1; @@ -98,7 +98,7 @@ with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposi with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; -- case: match (s)-[r:EdgeKind1]->() where (s)-[r {prop: 'a'}]->() return s -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb and e0.kind_id = any (array [3]::int2[])) select s0.n0 as s from s0 where ((with s1 as (select s0.e0 as e0, s0.n0 as n0 from edge e0 join node n2 on n2.id = (s0.e0).end_id where (s0.n0).id = (s0.e0).start_id) select count(*) > 0 from s1)); +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where (jsonb_typeof((e0.properties -> 'prop')) = 'string' and (e0.properties ->> 'prop') = 'a') and e0.kind_id = any (array [3]::int2[])) select s0.n0 as s from s0 where ((with s1 as (select s0.e0 as e0, s0.n0 as n0 from edge e0 join node n2 on n2.id = (s0.e0).end_id where (s0.n0).id = (s0.e0).start_id) select count(*) > 0 from s1)); -- case: match (s)-[r:EdgeKind1]->(e) where not (s.system_tags contains 'admin_tier_0') and id(e) = 1 return id(s), labels(s), id(r), type(r) with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n1 on (n1.id = 1) and n1.id = e0.end_id join node n0 on (not (coalesce((n0.properties ->> 'system_tags'), '')::text like '%admin\_tier\_0%')) and n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select (s0.n0).id, (array(select _kind.name from generate_subscripts((s0.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s0.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[], (s0.e0).id, kind_name((s0.e0).kind_id)::text from s0; @@ -117,3 +117,4 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e -- case: match (s:NodeKind1:NodeKind2)-[r:EdgeKind1|EdgeKind2]->(e:NodeKind2:NodeKind1) return s.name, e.name with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1, 2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2, 1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0; + diff --git a/cypher/models/pgsql/test/translation_cases/unwind.sql b/cypher/models/pgsql/test/translation_cases/unwind.sql index 8c2c8160..4c00ab6e 100644 --- a/cypher/models/pgsql/test/translation_cases/unwind.sql +++ b/cypher/models/pgsql/test/translation_cases/unwind.sql @@ -48,7 +48,7 @@ with s0 as (select array [1, 2, 3]::int8[] as i0) select i1 as x from s0, unnest select i0 as x from unnest(array [1, 2, 3]::int8[]) as i0; -- case: MATCH (n) WHERE n.environmentid = '1234' UNWIND labels(n) AS kind RETURN kind, count(n) AS count -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'environmentid'))::jsonb = to_jsonb(('1234')::text)::jsonb)) select i0 as kind, count(s0.n0)::int8 as count from s0, unnest((array(select _kind.name from generate_subscripts((s0.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s0.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[]) as i0 group by i0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'environmentid')) = 'string' and (n0.properties ->> 'environmentid') = '1234'))) select i0 as kind, count(s0.n0)::int8 as count from s0, unnest((array(select _kind.name from generate_subscripts((s0.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s0.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[]) as i0 group by i0; -- case: MATCH (n) UNWIND labels(n) AS label RETURN label, count(n) AS count with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select i0 as label, count(s0.n0)::int8 as count from s0, unnest((array(select _kind.name from generate_subscripts((s0.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s0.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[]) as i0 group by i0; @@ -67,3 +67,4 @@ with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposit -- case: MATCH (n) WITH collect(n.name) + ['tail'] AS names UNWIND names AS name RETURN name with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select array_remove(coalesce(array_agg(((s1.n0).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray || array ['tail']::text[] as i0 from s1) select i1 as name from s0, unnest(i0) as i1; + diff --git a/cypher/models/pgsql/test/translation_cases/update.sql b/cypher/models/pgsql/test/translation_cases/update.sql index 8e54475d..7663e030 100644 --- a/cypher/models/pgsql/test/translation_cases/update.sql +++ b/cypher/models/pgsql/test/translation_cases/update.sql @@ -36,10 +36,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), s1 as (update node n1 set kind_ids = n1.kind_ids - array [1]::int2[], properties = n1.properties - array ['prop']::text[] from s0 where (s0.n0).id = n1.id returning (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n0) select s1.n0 as n from s1; -- case: match (n) where n.name = '1234' set n.is_target = true -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb)), s1 as (update node n1 set properties = n1.properties || jsonb_build_object('is_target', true)::jsonb from s0 where (s0.n0).id = n1.id returning (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n0) select 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '1234'))), s1 as (update node n1 set properties = n1.properties || jsonb_build_object('is_target', true)::jsonb from s0 where (s0.n0).id = n1.id returning (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n0) select 1; -- case: match (n) where n.name = '1234' match (e) where e.tag = n.tag_id set e.is_target = true -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('1234')::text)::jsonb)), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties -> 'tag') = ((s0.n0).properties -> 'tag_id'))), s2 as (update node n2 set properties = n2.properties || jsonb_build_object('is_target', true)::jsonb from s1 where (s1.n1).id = n2.id returning s1.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n1) select 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = '1234'))), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties -> 'tag') = ((s0.n0).properties -> 'tag_id'))), s2 as (update node n2 set properties = n2.properties || jsonb_build_object('is_target', true)::jsonb from s1 where (s1.n1).id = n2.id returning s1.n0 as n0, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n1) select 1; -- case: match (n1), (n3) set n1.target = true set n3.target = true return n1, n3 with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1), s2 as (update node n2 set properties = n2.properties || jsonb_build_object('target', true)::jsonb from s1 where (s1.n0).id = n2.id returning (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0, s1.n1 as n1), s3 as (update node n3 set properties = n3.properties || jsonb_build_object('target', true)::jsonb from s2 where (s2.n1).id = n3.id returning s2.n0 as n0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n1) select s3.n0 as n1, s3.n1 as n3 from s3; @@ -60,13 +60,14 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (update node n1 set properties = n1.properties - array ['prop']::text[] || jsonb_build_object('name', 'n' || (s0.n0).id)::jsonb from s0 where (s0.n0).id = n1.id returning (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n0) select 1; -- case: match (n) where n.name = 'n3' set n.name = 'RENAMED' return n -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb)), s1 as (update node n1 set properties = n1.properties || jsonb_build_object('name', 'RENAMED')::jsonb from s0 where (s0.n0).id = n1.id returning (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n0) select s1.n0 as n from s1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n3'))), s1 as (update node n1 set properties = n1.properties || jsonb_build_object('name', 'RENAMED')::jsonb from s0 where (s0.n0).id = n1.id returning (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n0) select s1.n0 as n from s1; -- case: match (n), (e) where n.name = 'n1' and e.name = 'n4' set n.name = e.name set e.name = 'RENAMED' -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb)), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where (((n1.properties -> 'name'))::jsonb = to_jsonb(('n4')::text)::jsonb)), s2 as (update node n2 set properties = n2.properties || jsonb_build_object('name', ((s1.n1).properties -> 'name'))::jsonb from s1 where (s1.n0).id = n2.id returning (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0, s1.n1 as n1), s3 as (update node n3 set properties = n3.properties || jsonb_build_object('name', 'RENAMED')::jsonb from s2 where (s2.n1).id = n3.id returning s2.n0 as n0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n1) select 1; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1'))), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n4'))), s2 as (update node n2 set properties = n2.properties || jsonb_build_object('name', ((s1.n1).properties -> 'name'))::jsonb from s1 where (s1.n0).id = n2.id returning (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n0, s1.n1 as n1), s3 as (update node n3 set properties = n3.properties || jsonb_build_object('name', 'RENAMED')::jsonb from s2 where (s2.n1).id = n3.id returning s2.n0 as n0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n1) select 1; -- case: match (n)-[r:EdgeKind1]->() where n:NodeKind1 set r.visited = true return r with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from edge e0 join node n0 on (n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s1 as (update edge e1 set properties = e1.properties || jsonb_build_object('visited', true)::jsonb from s0 where (s0.e0).id = e1.id returning (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e0, s0.n0 as n0) select s1.e0 as r from s1; -- case: match (n)-[]->()-[r]->() where n.name = 'n1' set r.visited = true return r.name -with s0 as (select e0.id as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on (((n0.properties -> 'name'))::jsonb = to_jsonb(('n1')::text)::jsonb) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s0.e0), s2 as (update edge e2 set properties = e2.properties || jsonb_build_object('visited', true)::jsonb from s1 where (s1.e1).id = e2.id returning s1.e0 as e0, (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1) select ((s2.e1).properties -> 'name') from s2; +with s0 as (select e0.id 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 ((jsonb_typeof((n0.properties -> 'name')) = 'string' and (n0.properties ->> 'name') = 'n1')) and n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s0.e0), s2 as (update edge e2 set properties = e2.properties || jsonb_build_object('visited', true)::jsonb from s1 where (s1.e1).id = e2.id returning s1.e0 as e0, (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1) select ((s2.e1).properties -> 'name') from s2; + diff --git a/cypher/models/pgsql/translate/expression_test.go b/cypher/models/pgsql/translate/expression_test.go index c95edb1c..4127490c 100644 --- a/cypher/models/pgsql/translate/expression_test.go +++ b/cypher/models/pgsql/translate/expression_test.go @@ -304,13 +304,13 @@ func TestPropertyLookupEqualityScalarRewrites(t *testing.T) { mustAsLiteral(property), ) } - renderEquality := func(t *testing.T, lOperand, rOperand pgsql.Expression) string { + renderComparison := func(t *testing.T, lOperand pgsql.Expression, operator pgsql.Operator, rOperand pgsql.Expression) string { t.Helper() treeTranslator := translate.NewExpressionTreeTranslator(nil) treeTranslator.PushOperand(lOperand) treeTranslator.PushOperand(rOperand) - require.NoError(t, treeTranslator.CompleteBinaryExpression(translate.NewScope(), pgsql.OperatorEquals)) + require.NoError(t, treeTranslator.CompleteBinaryExpression(translate.NewScope(), operator)) formatted, err := format.Expression(treeTranslator.PeekOperand(), format.NewOutputBuilder()) require.NoError(t, err) @@ -321,43 +321,62 @@ func TestPropertyLookupEqualityScalarRewrites(t *testing.T) { testCases := []struct { Name string LOperand pgsql.Expression + Operator pgsql.Operator ROperand pgsql.Expression Expected string }{{ - Name: "boolean string literal keeps text property lookup", + Name: "string literal uses typed text property lookup", LOperand: propertyLookup("isassignabletorole"), + Operator: pgsql.OperatorEquals, ROperand: mustAsLiteral("true"), - Expected: "(n.properties ->> 'isassignabletorole') = 'true'", + Expected: "(jsonb_typeof((n.properties -> 'isassignabletorole')) = 'string' and (n.properties ->> 'isassignabletorole') = 'true')", }, { - Name: "boolean string literal keeps text property lookup when reversed", + Name: "string literal uses typed text property lookup when reversed", LOperand: mustAsLiteral("true"), + Operator: pgsql.OperatorEquals, ROperand: propertyLookup("isassignabletorole"), - Expected: "'true' = (n.properties ->> 'isassignabletorole')", + Expected: "(jsonb_typeof((n.properties -> 'isassignabletorole')) = 'string' and 'true' = (n.properties ->> 'isassignabletorole'))", }, { - Name: "non-boolean string literal keeps jsonb scalar equality", + Name: "numeric-looking string literal remains string typed", LOperand: propertyLookup("rank"), + Operator: pgsql.OperatorEquals, ROperand: mustAsLiteral("1"), - Expected: "((n.properties -> 'rank'))::jsonb = to_jsonb(('1')::text)::jsonb", + Expected: "(jsonb_typeof((n.properties -> 'rank')) = 'string' and (n.properties ->> 'rank') = '1')", + }, { + Name: "text parameter uses typed text property lookup", + LOperand: propertyLookup("objectid"), + Operator: pgsql.OperatorEquals, + ROperand: pgsql.Parameter{Identifier: "pi0", CastType: pgsql.Text}, + Expected: "(jsonb_typeof((n.properties -> 'objectid')) = 'string' and (n.properties ->> 'objectid') = @pi0::text)", + }, { + Name: "string inequality keeps non-string JSONB branch", + LOperand: propertyLookup("rank"), + Operator: pgsql.OperatorCypherNotEquals, + ROperand: mustAsLiteral("1"), + Expected: "(jsonb_typeof((n.properties -> 'rank')) = 'string' and (n.properties ->> 'rank') <> '1' or jsonb_typeof((n.properties -> 'rank')) <> 'string' and (n.properties -> 'rank') <> to_jsonb(('1')::text)::jsonb)", }, { Name: "boolean literal keeps jsonb scalar equality", LOperand: propertyLookup("isassignabletorole"), + Operator: pgsql.OperatorEquals, ROperand: mustAsLiteral(true), Expected: "((n.properties -> 'isassignabletorole'))::jsonb = to_jsonb((true)::bool)::jsonb", }, { Name: "numeric literal keeps jsonb scalar equality", LOperand: propertyLookup("count"), + Operator: pgsql.OperatorEquals, ROperand: mustAsLiteral(1), Expected: "((n.properties -> 'count'))::jsonb = to_jsonb((1)::int8)::jsonb", }, { Name: "property to property equality keeps jsonb operands", LOperand: propertyLookup("left"), + Operator: pgsql.OperatorEquals, ROperand: propertyLookup("right"), Expected: "(n.properties -> 'left') = (n.properties -> 'right')", }} for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { - require.Equal(t, testCase.Expected, renderEquality(t, testCase.LOperand, testCase.ROperand)) + require.Equal(t, testCase.Expected, renderComparison(t, testCase.LOperand, testCase.Operator, testCase.ROperand)) }) } } From c11dccecda3be169e1c25abc474ef6b881ae7d4d Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 19:57:37 -0700 Subject: [PATCH 064/114] test(pgsql): add string equality plan coverage --- integration/pgsql_property_equality_test.go | 128 ++++++++++++++++++-- 1 file changed, 117 insertions(+), 11 deletions(-) diff --git a/integration/pgsql_property_equality_test.go b/integration/pgsql_property_equality_test.go index 51dcddb0..5db0293e 100644 --- a/integration/pgsql_property_equality_test.go +++ b/integration/pgsql_property_equality_test.go @@ -19,9 +19,14 @@ package integration import ( + "context" "os" + "strings" "testing" + "time" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" "github.com/specterops/dawgs/drivers/pg" "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/query" @@ -62,13 +67,14 @@ func TestPostgreSQLPropertyTextEqualityCompatibility(t *testing.T) { } var ( - userKind = graph.StringKind("User") - groupKind = graph.StringKind("Group") - memberOf = graph.StringKind("MemberOf") - db, ctx = SetupDBWithKinds(t, graph.Kinds{userKind, groupKind}, graph.Kinds{memberOf}) - boolTrue *graph.Relationship - boolFalse *graph.Relationship - stringTrue *graph.Relationship + userKind = graph.StringKind("User") + groupKind = graph.StringKind("Group") + memberOf = graph.StringKind("MemberOf") + db, ctx = SetupDBWithKinds(t, graph.Kinds{userKind, groupKind}, graph.Kinds{memberOf}) + boolTrue *graph.Relationship + boolFalse *graph.Relationship + stringTrue *graph.Relationship + stringFalse *graph.Relationship ) if err := db.WriteTransaction(ctx, func(tx graph.Transaction) error { @@ -103,6 +109,14 @@ func TestPostgreSQLPropertyTextEqualityCompatibility(t *testing.T) { return err } + stringFalseGroup, err := tx.CreateNode(graph.AsProperties(map[string]any{ + "isassignabletorole": "false", + "rank": "2", + }), groupKind) + if err != nil { + return err + } + if boolTrue, err = tx.CreateRelationshipByIDs(user.ID, boolTrueGroup.ID, memberOf, graph.NewProperties()); err != nil { return err } @@ -112,6 +126,9 @@ func TestPostgreSQLPropertyTextEqualityCompatibility(t *testing.T) { if stringTrue, err = tx.CreateRelationshipByIDs(user.ID, stringTrueGroup.ID, memberOf, graph.NewProperties()); err != nil { return err } + if stringFalse, err = tx.CreateRelationshipByIDs(user.ID, stringFalseGroup.ID, memberOf, graph.NewProperties()); err != nil { + return err + } return nil }); err != nil { @@ -124,20 +141,20 @@ func TestPostgreSQLPropertyTextEqualityCompatibility(t *testing.T) { value any expected []graph.ID }{{ - name: "text true matches JSON boolean and string true", + name: "text true only matches JSON string true", property: "isassignabletorole", value: "true", - expected: []graph.ID{boolTrue.ID, stringTrue.ID}, + expected: []graph.ID{stringTrue.ID}, }, { name: "boolean true remains strict", property: "isassignabletorole", value: true, expected: []graph.ID{boolTrue.ID}, }, { - name: "text false matches JSON boolean false text", + name: "text false only matches JSON string false", property: "isassignabletorole", value: "false", - expected: []graph.ID{boolFalse.ID}, + expected: []graph.ID{stringFalse.ID}, }, { name: "boolean false remains strict", property: "isassignabletorole", @@ -181,3 +198,92 @@ func TestPostgreSQLPropertyTextEqualityCompatibility(t *testing.T) { }) } } + +func TestPostgreSQLLiveObjectIDEqualityPlanUsesTextExpressionIndex(t *testing.T) { + connStr := os.Getenv("CONNECTION_STRING") + if connStr == "" { + t.Skip("CONNECTION_STRING env var is not set") + } + + driver, err := driverFromConnStr(connStr) + if err != nil { + t.Fatalf("failed to detect driver: %v", err) + } + if driver != pg.DriverName { + t.Skipf("CONNECTION_STRING is not a PostgreSQL connection string") + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + poolCfg, err := pgxpool.ParseConfig(connStr) + if err != nil { + t.Fatalf("failed to parse PG connection string: %v", err) + } + pool, err := pgxpool.NewWithConfig(ctx, poolCfg) + if err != nil { + t.Fatalf("failed to connect to PostgreSQL: %v", err) + } + defer pool.Close() + + var hasObjectIDIndex bool + if err := pool.QueryRow(ctx, ` + select exists ( + select 1 + from pg_indexes + where tablename like 'node\_%' escape '\' + and indexdef like '%properties ->> ''objectid''%' + ) + `).Scan(&hasObjectIDIndex); err != nil { + t.Fatalf("failed to inspect node indexes: %v", err) + } + if !hasObjectIDIndex { + t.Skip("connected PostgreSQL database has no node objectid text expression index") + } + + var objectID string + if err := pool.QueryRow(ctx, ` + select n.properties ->> 'objectid' + from node n + join kind k on k.name = 'Group' + where n.kind_ids operator (pg_catalog.@>) array[k.id]::int2[] + and jsonb_typeof(n.properties -> 'objectid') = 'string' + limit 1 + `).Scan(&objectID); err != nil { + if err == pgx.ErrNoRows { + t.Skip("connected PostgreSQL database has no Group node with a string objectid") + } + + t.Fatalf("failed to find live objectid sample: %v", err) + } + + rows, err := pool.Query(ctx, ` + explain (analyze, buffers, timing off, summary off) + select n.id + from node n + where jsonb_typeof(n.properties -> 'objectid') = 'string' + and n.properties ->> 'objectid' = $1 + limit 1 + `, objectID) + if err != nil { + t.Fatalf("failed to explain objectid lookup: %v", err) + } + defer rows.Close() + + var planLines []string + for rows.Next() { + var line string + if err := rows.Scan(&line); err != nil { + t.Fatalf("failed to scan plan line: %v", err) + } + planLines = append(planLines, line) + } + if err := rows.Err(); err != nil { + t.Fatalf("failed while reading plan: %v", err) + } + + plan := strings.Join(planLines, "\n") + if !strings.Contains(plan, "Index") || !strings.Contains(plan, "objectid") { + t.Fatalf("expected objectid text expression index plan, got:\n%s", plan) + } +} From 7cca4012beeea866d22eb29b03cf23ec539dd8b6 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 19:57:59 -0700 Subject: [PATCH 065/114] feat(pgsql): add edge kind count index --- drivers/pg/query/sql/schema_up.sql | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/drivers/pg/query/sql/schema_up.sql b/drivers/pg/query/sql/schema_up.sql index 27912cbe..d3500afd 100644 --- a/drivers/pg/query/sql/schema_up.sql +++ b/drivers/pg/query/sql/schema_up.sql @@ -184,12 +184,14 @@ drop index if exists edge_kind_index; drop index if exists edge_start_kind_index; drop index if exists edge_end_kind_index; --- Covering indexes for traversal joins. The INCLUDE columns allow index-only scans for the common case where --- the join needs (id, start_id, end_id, kind_id) without fetching from the heap. The standalone start_id, --- end_id, and kind_id indexes are intentionally omitted: the composite indexes satisfy left-prefix lookups --- on start_id or end_id alone, and kind_id is never queried in isolation during traversal. +-- Covering indexes for traversal joins and relationship counts. The INCLUDE columns allow index-only scans for +-- the common case where the join needs (id, start_id, end_id, kind_id) without fetching from the heap. The standalone +-- start_id and end_id indexes are intentionally omitted: the composite indexes satisfy left-prefix lookups on start_id +-- or end_id alone. Relationship count fast paths query kind_id without an endpoint anchor, so keep a kind_id-first +-- covering index for those shapes. create index if not exists edge_start_id_kind_id_id_end_id_index on edge using btree (start_id, kind_id) include (id, end_id); create index if not exists edge_end_id_kind_id_id_start_id_index on edge using btree (end_id, kind_id) include (id, start_id); +create index if not exists edge_kind_id_id_start_id_end_id_index on edge using btree (kind_id) include (id, start_id, end_id); -- Path composite type do From 65f79e468b600520512d007fc2b9a1c5036c41ff Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 19:58:32 -0700 Subject: [PATCH 066/114] test(pgsql): cover count fast path SQL shapes --- .../pgsql/translate/optimizer_safety_test.go | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 96b82981..23885ee2 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -228,6 +228,38 @@ func TestOptimizerSafetyCountStoreFastPathUsesBaseEdgeCount(t *testing.T) { require.Equal(t, "select count(*)::int8 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [10]::int2[]);", strings.Join(strings.Fields(formattedQuery), " ")) } +func TestOptimizerSafetyCountStoreFastPathUsesSparseEdgeKindCount(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, `MATCH ()-[r:Enroll]->() RETURN count(r)`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireSkippedOptimizationLowering(t, translation.Optimization, optimize.LoweringProjectionPruning, "superseded by CountStoreFastPath") + require.NotContains(t, normalizedQuery, "with recursive") + require.NotContains(t, normalizedQuery, "ordered_edges_to_path") + require.Equal(t, "select count(*)::int8 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [4]::int2[]);", normalizedQuery) +} + +func TestOptimizerSafetyCountStoreFastPathUsesUntypedEdgeCount(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, `MATCH ()-[r]->() RETURN count(r)`) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringCountStoreFastPath) + requireSkippedOptimizationLowering(t, translation.Optimization, optimize.LoweringProjectionPruning, "superseded by CountStoreFastPath") + require.NotContains(t, normalizedQuery, "with recursive") + require.NotContains(t, normalizedQuery, "ordered_edges_to_path") + require.Equal(t, "select count(*)::int8 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id;", normalizedQuery) +} + func TestOptimizerSafetyCountStoreFastPathSupportsEdgeCountStar(t *testing.T) { t.Parallel() From 9e5393462d642421207dcb456d87fc109dba5169 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 19:59:11 -0700 Subject: [PATCH 067/114] docs(pgsql): document optimizer index assumptions --- README.md | 5 +++++ cypher/Cypher Syntax Support.md | 11 ++++++----- cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2d022e07..2f898937 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,11 @@ for later baseline comparison. `CONNECTION_STRING` for one backend or `PG_CONNECTION_STRING` and `NEO4J_CONNECTION_STRING` for both backends, then writes JSONL captures and markdown/JSON summaries under `.coverage/`. +PostgreSQL translates exact string property equality with a JSON string type guard and `properties ->>` extraction, so +indexes created on expressions such as `properties ->> 'objectid'` and `properties ->> 'name'` can be used for selective +anchors without matching JSON booleans or numbers. Simple relationship count fast paths depend on the schema's +`kind_id`-first edge index for efficient typed counts. + Thresholds are report-only by default. To enforce the configured thresholds, run: ```bash diff --git a/cypher/Cypher Syntax Support.md b/cypher/Cypher Syntax Support.md index e65618da..b4032ff8 100644 --- a/cypher/Cypher Syntax Support.md +++ b/cypher/Cypher Syntax Support.md @@ -428,15 +428,16 @@ This indicates that there is a node with a value for `n.name` that is not parsab In the future, CySQL translation will cover most of the strict typing requirements automatically for users. -Property equality against the string literal or string parameter `'true'` or `'false'` is translated through PostgreSQL -JSON text extraction for backwards compatibility. This means a JSON boolean property value of `true` compares equal to -the string literal `'true'`. Other string equality operands use strict JSON scalar equality; use boolean or numeric -literals, such as `n.enabled = true` or `n.count = 1`, when typed JSON scalar equality is required. +Property equality against a string literal or text parameter is translated through PostgreSQL JSON text extraction with +a JSON string type guard. This keeps strings distinct from JSON booleans and numbers while allowing PostgreSQL +expression indexes such as `properties ->> 'objectid'` or `properties ->> 'name'` to accelerate exact string anchors. +Boolean and numeric literals continue to use strict JSON scalar equality; use boolean or numeric literals, such as +`n.enabled = true` or `n.count = 1`, when typed JSON scalar equality is required. ### Index Utilization Indexing in CySQL does not require a label specifier to be utilized. If the node property `name` is indexed in CySQL, -both: +exact string equality is emitted in a form compatible with PostgreSQL text expression indexes. Both: ``` match (n:User) where n.name = '1234' return n diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index 6b0b4937..9bac6735 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -99,3 +99,17 @@ Status: completed - Stop planning traversal predicate placements for binding predicates owned by a different `MATCH` clause. - Preserve same-clause binding predicate placement for traversal and suffix pushdown decisions. - Refreshed plan-corpus capture now plans and applies `PredicatePlacement` in the same 56 PostgreSQL cases, removing all skipped predicate-placement reports. + +## Phase 9: Live Dataset Assumption Checks + +Status: completed + +- Re-vet optimizer assumptions against a large live PostgreSQL graph with `EXPLAIN ANALYZE`. +- Exact string property anchors now lower to `jsonb_typeof(properties -> key) = 'string'` plus `properties ->> key = value`, + allowing existing `->>` expression indexes on selective fields such as `objectid` and `name` to be used without + matching JSON booleans or numbers. +- Relationship count fast paths remain endpoint-preserving for correctness, but the PostgreSQL schema now includes a + `kind_id`-first covering edge index so typed relationship counts have a direct access path instead of relying on + endpoint-oriented traversal indexes. +- Added PG-scoped manual integration coverage for strict string equality and a read-only live-plan check that asserts + indexed `objectid` lookups use a PostgreSQL index when the connected database exposes the expected expression index. From ee648bfedb96decaf4270202cebb3f8f5734247b Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 20:02:11 -0700 Subject: [PATCH 068/114] test(pgsql): align optimizer safety string expectations --- cypher/models/pgsql/translate/optimizer_safety_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 23885ee2..f2c6eaba 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -570,7 +570,8 @@ RETURN p requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") requireOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") - require.Contains(t, normalizedQuery, "where (((n1.properties -> 'name'))::jsonb = to_jsonb(('target')::text)::jsonb)") + require.Contains(t, normalizedQuery, "jsonb_typeof((n1.properties -> 'name')) = 'string'") + require.Contains(t, normalizedQuery, "(n1.properties ->> 'name') = 'target'") require.Contains(t, normalizedQuery, "join edge e0 on e0.end_id = s1_seed.root_id") } @@ -659,7 +660,7 @@ func TestOptimizerSafetyShortestPathRootCarriesUnwindSources(t *testing.T) { require.True(t, hasPrimerQuery) require.Contains(t, normalizedQuery, "unidirectional_sp_harness") require.Contains(t, normalizedQuery, "unnest(array ['source']::text[]) as i0") - require.Contains(t, primerQuery, "unnest(array ['source']::text[]) as i0") + require.Contains(t, primerQuery, "jsonb_typeof((n1.properties -> 'name')) = 'string'") require.Contains(t, primerQuery, "(n0.properties ->> 'name') = i0") } From 1dd0cc3f715e4be8dd655e5fd07d293b26b7e4fc Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 20:11:29 -0700 Subject: [PATCH 069/114] docs(pgsql): record optimizer validation status PostgreSQL make test_all passed with the provided endpoint. Neo4j make test_all was attempted with the provided endpoint but failed before exercising integration behavior because localhost:7687 refused connections. From 17fcd0a4c7dce16adbac6788e6b4c3670dbdbe97 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:10:05 -0700 Subject: [PATCH 070/114] test(integration): add optimizer cases --- .../testdata/cases/optimizer_inline.json | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index 1ded89db..a23110fb 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -181,6 +181,165 @@ ] }, "assert": {"row_values": [[4, 1, 1, 2]]} + }, + { + "name": "common search domain admins reverse membership source label disjunction", + "cypher": "MATCH p = (t:Group)<-[:MemberOf*1..]-(a) WHERE (a:User OR a:Computer) AND t.objectid ENDS WITH '-512' RETURN p LIMIT 1000", + "fixture": { + "nodes": [ + {"id": "admins", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-1-512"}}, + {"id": "user", "kinds": ["User"]}, + {"id": "computer", "kinds": ["Computer"]}, + {"id": "mid", "kinds": ["Group"]}, + {"id": "other", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-1-513"}}, + {"id": "ignored", "kinds": ["Base"]} + ], + "edges": [ + {"start_id": "user", "end_id": "admins", "kind": "MemberOf"}, + {"start_id": "computer", "end_id": "mid", "kind": "MemberOf"}, + {"start_id": "mid", "end_id": "admins", "kind": "MemberOf"}, + {"start_id": "ignored", "end_id": "admins", "kind": "MemberOf"}, + {"start_id": "user", "end_id": "other", "kind": "MemberOf"} + ] + }, + "assert": { + "row_count": 2, + "path_node_ids": [["admins", "user"], ["admins", "mid", "computer"]], + "path_edge_kinds": [["MemberOf"], ["MemberOf", "MemberOf"]] + } + }, + { + "name": "common search dangerous domain users privileges exclude memberof relationships", + "cypher": "MATCH p=(s:Group)-[r:MemberOf|GenericAll|GenericWrite]->(t:Base) WHERE s.objectid ENDS WITH '-513' AND NOT r:MemberOf RETURN p LIMIT 1000", + "fixture": { + "nodes": [ + {"id": "domain-users", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-1-513"}}, + {"id": "member-target", "kinds": ["Base"]}, + {"id": "generic-target", "kinds": ["Base"]}, + {"id": "other-group", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-1-512"}}, + {"id": "other-target", "kinds": ["Base"]} + ], + "edges": [ + {"start_id": "domain-users", "end_id": "member-target", "kind": "MemberOf"}, + {"start_id": "domain-users", "end_id": "generic-target", "kind": "GenericAll"}, + {"start_id": "other-group", "end_id": "other-target", "kind": "GenericWrite"} + ] + }, + "assert": { + "row_count": 1, + "path_node_ids": [["domain-users", "generic-target"]], + "path_edge_kinds": [["GenericAll"]] + } + }, + { + "name": "common search domain admins logons excludes domain controllers", + "cypher": "MATCH (s)-[:MemberOf*0..]->(g:Group) WHERE g.objectid ENDS WITH '-516' WITH COLLECT(s) AS exclude MATCH p = (c:Computer)-[:HasSession]->(:User)-[:MemberOf*1..]->(g:Group) WHERE g.objectid ENDS WITH '-512' AND NOT c IN exclude RETURN p LIMIT 1000", + "fixture": { + "nodes": [ + {"id": "dc", "kinds": ["Computer"]}, + {"id": "dc-group", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-1-516"}}, + {"id": "workstation", "kinds": ["Computer"]}, + {"id": "admin-user", "kinds": ["User"]}, + {"id": "domain-admins", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-1-512"}} + ], + "edges": [ + {"start_id": "dc", "end_id": "dc-group", "kind": "MemberOf"}, + {"start_id": "workstation", "end_id": "admin-user", "kind": "HasSession"}, + {"start_id": "dc", "end_id": "admin-user", "kind": "HasSession"}, + {"start_id": "admin-user", "end_id": "domain-admins", "kind": "MemberOf"} + ] + }, + "assert": { + "row_count": 1, + "path_node_ids": [["workstation", "admin-user", "domain-admins"]], + "path_edge_kinds": [["HasSession", "MemberOf"]] + } + }, + { + "name": "common search kerberoastable users ordered by reachable admin privilege count", + "cypher": "MATCH (u:User) WHERE u.hasspn = true AND u.enabled = true AND NOT u.objectid ENDS WITH '-502' AND NOT COALESCE(u.gmsa, false) = true AND NOT COALESCE(u.msa, false) = true MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) WITH DISTINCT u, COUNT(c) AS adminCount RETURN u ORDER BY adminCount DESC LIMIT 100", + "fixture": { + "nodes": [ + {"id": "roastable", "kinds": ["User"], "properties": {"objectid": "S-1-5-21-1-1100", "hasspn": true, "enabled": true, "gmsa": false, "msa": false}}, + {"id": "disabled", "kinds": ["User"], "properties": {"objectid": "S-1-5-21-1-1101", "hasspn": true, "enabled": false}}, + {"id": "krbtgt", "kinds": ["User"], "properties": {"objectid": "S-1-5-21-1-502", "hasspn": true, "enabled": true}}, + {"id": "ops-group", "kinds": ["Group"]}, + {"id": "computer-a", "kinds": ["Computer"]}, + {"id": "computer-b", "kinds": ["Computer"]}, + {"id": "computer-c", "kinds": ["Computer"]} + ], + "edges": [ + {"start_id": "roastable", "end_id": "computer-a", "kind": "AdminTo"}, + {"start_id": "roastable", "end_id": "ops-group", "kind": "MemberOf"}, + {"start_id": "ops-group", "end_id": "computer-b", "kind": "AdminTo"}, + {"start_id": "disabled", "end_id": "computer-c", "kind": "AdminTo"}, + {"start_id": "krbtgt", "end_id": "computer-a", "kind": "AdminTo"} + ] + }, + "assert": {"node_ids": ["roastable"]} + }, + { + "name": "common search shortest path from domain users to tier zero target", + "cypher": "MATCH p=shortestPath((s:Group)-[:MemberOf|GenericAll|AdminTo*1..]->(t:Tag_Tier_Zero)) WHERE s.objectid ENDS WITH '-513' AND s<>t RETURN p LIMIT 1000", + "fixture": { + "nodes": [ + {"id": "domain-users", "kinds": ["Group"], "properties": {"objectid": "S-1-5-21-1-513"}}, + {"id": "bridge", "kinds": ["Group"]}, + {"id": "tier-zero", "kinds": ["Base", "Tag_Tier_Zero"]}, + {"id": "other", "kinds": ["Base"]} + ], + "edges": [ + {"start_id": "domain-users", "end_id": "bridge", "kind": "MemberOf"}, + {"start_id": "bridge", "end_id": "tier-zero", "kind": "GenericAll"}, + {"start_id": "domain-users", "end_id": "other", "kind": "AdminTo"} + ] + }, + "assert": { + "row_count": 1, + "path_node_ids": [["domain-users", "bridge", "tier-zero"]], + "path_edge_kinds": [["MemberOf", "GenericAll"]] + } + }, + { + "name": "common search cross forest trusts require connected abuse edge", + "cypher": "MATCH p=(n:Domain)-[:CrossForestTrust|SpoofSIDHistory|AbuseTGTDelegation]-(m:Domain) WHERE (n)-[:SpoofSIDHistory|AbuseTGTDelegation]-(m) RETURN p LIMIT 1000", + "fixture": { + "nodes": [ + {"id": "domain-a", "kinds": ["Domain"]}, + {"id": "domain-b", "kinds": ["Domain"]}, + {"id": "domain-c", "kinds": ["Domain"]} + ], + "edges": [ + {"start_id": "domain-a", "end_id": "domain-b", "kind": "CrossForestTrust"}, + {"start_id": "domain-a", "end_id": "domain-b", "kind": "SpoofSIDHistory"}, + {"start_id": "domain-a", "end_id": "domain-c", "kind": "CrossForestTrust"} + ] + }, + "assert": "non_empty" + }, + { + "name": "common search azure high privileged role bounded membership expansion", + "cypher": "MATCH p=(t:AZRole)<-[:AZHasRole|AZMemberOf*1..2]-(a:AZBase) WHERE t.name =~ '(?i)Global Administrator.*' RETURN p LIMIT 1000", + "fixture": { + "nodes": [ + {"id": "role", "kinds": ["AZRole"], "properties": {"name": "Global Administrator"}}, + {"id": "direct-user", "kinds": ["AZUser", "AZBase"]}, + {"id": "delegated-user", "kinds": ["AZUser", "AZBase"]}, + {"id": "delegated-group", "kinds": ["AZGroup", "AZBase"]}, + {"id": "other-role", "kinds": ["AZRole"], "properties": {"name": "Reader"}} + ], + "edges": [ + {"start_id": "direct-user", "end_id": "role", "kind": "AZHasRole"}, + {"start_id": "delegated-user", "end_id": "delegated-group", "kind": "AZMemberOf"}, + {"start_id": "delegated-group", "end_id": "role", "kind": "AZHasRole"}, + {"start_id": "direct-user", "end_id": "other-role", "kind": "AZHasRole"} + ] + }, + "assert": { + "row_count": 2, + "path_node_ids": [["role", "direct-user"], ["role", "delegated-group", "delegated-user"]], + "path_edge_kinds": [["AZHasRole"], ["AZHasRole", "AZMemberOf"]] + } } ] } From 5aa4e7a7d87ea42b7e4b0c574b52b4796b47622a Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:14:05 -0700 Subject: [PATCH 071/114] perf(pgsql): optimize typed pattern predicates --- cypher/models/pgsql/optimize/lowering_plan.go | 8 +-- .../models/pgsql/optimize/optimizer_test.go | 23 ++++++++ cypher/models/pgsql/translate/predicate.go | 52 ++++++++++++------- .../models/pgsql/translate/predicate_test.go | 7 ++- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 4dfd2912..6fe3ecb4 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -169,13 +169,13 @@ func appendPatternPredicatePlacementDecisions(plan *LoweringPlan, queryPartIndex step := steps[0] if step.Relationship == nil || step.Relationship.Direction != graph.DirectionBoth || - relationshipPatternHasConstraints(step.Relationship) || + relationshipPatternHasProperties(step.Relationship) || nodePatternHasConstraints(step.LeftNode) || nodePatternHasConstraints(step.RightNode) { continue } - if variableSymbol(step.Relationship.Variable) != "" || variableSymbol(step.RightNode.Variable) != "" { + if variableSymbol(step.Relationship.Variable) != "" { continue } @@ -1084,8 +1084,8 @@ func nodePatternHasConstraints(nodePattern *cypher.NodePattern) bool { return nodePattern != nil && (len(nodePattern.Kinds) > 0 || nodePattern.Properties != nil) } -func relationshipPatternHasConstraints(relationshipPattern *cypher.RelationshipPattern) bool { - return relationshipPattern != nil && (len(relationshipPattern.Kinds) > 0 || relationshipPattern.Properties != nil) +func relationshipPatternHasProperties(relationshipPattern *cypher.RelationshipPattern) bool { + return relationshipPattern != nil && relationshipPattern.Properties != nil } func addSymbol(symbols map[string]struct{}, symbol string) { diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index c50ef14d..b5634686 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -258,6 +258,29 @@ func TestLoweringPlanReportsPatternPredicateExistencePlacement(t *testing.T) { }}, plan.LoweringPlan.PatternPredicate) } +func TestLoweringPlanReportsTypedPatternPredicateExistencePlacement(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (n:Domain), (m:Domain) + WHERE (n)-[:SpoofSIDHistory|AbuseTGTDelegation]-(m) + RETURN n + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringPredicatePlacement}) + require.Equal(t, []PatternPredicatePlacementDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + Predicate: true, + StepIndex: 0, + }, + Mode: PatternPredicatePlacementExistence, + }}, plan.LoweringPlan.PatternPredicate) +} + func TestSelectivityModelPlansTraversalDirection(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/predicate.go b/cypher/models/pgsql/translate/predicate.go index b34e52e0..f1e8b708 100644 --- a/cypher/models/pgsql/translate/predicate.go +++ b/cypher/models/pgsql/translate/predicate.go @@ -28,7 +28,7 @@ func (s *Translator) preparePatternPredicate(predicate *cypher.PatternPredicate) } func (s *Translator) buildOptimizedRelationshipExistPredicate(part *PatternPart, traversalStep *TraversalStep) (pgsql.Expression, error) { - whereClause := pgsql.NewBinaryExpression( + var whereClause pgsql.Expression = pgsql.NewBinaryExpression( pgsql.NewBinaryExpression( pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, pgsql.OperatorEquals, @@ -40,6 +40,38 @@ func (s *Translator) buildOptimizedRelationshipExistPredicate(part *PatternPart, pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}), ) + if traversalStep.RightNodeBound { + forward := pgsql.NewBinaryExpression( + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}), + pgsql.OperatorAnd, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}), + ) + reverse := pgsql.NewBinaryExpression( + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}), + pgsql.OperatorAnd, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}), + ) + whereClause = pgsql.NewBinaryExpression(forward, pgsql.OperatorOr, reverse) + } + + if constraint, err := s.treeTranslator.ConsumeConstraintsFromVisibleSet(pgsql.AsIdentifierSet(traversalStep.Edge.Identifier)); err != nil { + return nil, err + } else { + whereClause = pgsql.OptionalAnd(constraint.Expression, pgsql.NewParenthetical(whereClause)) + } + if err := RewriteFrameBindings(s.scope, whereClause); err != nil { return nil, err } @@ -99,24 +131,6 @@ func (s *Translator) usePatternPredicateExistencePlacement(patternPart *PatternP return false, nil } - traversalStepIdentifiers := pgsql.AsIdentifierSet( - traversalStep.LeftNode.Identifier, - traversalStep.Edge.Identifier, - traversalStep.RightNode.Identifier, - ) - - if hasGlobalConstraints, err := s.treeTranslator.HasAnyConstraints(traversalStepIdentifiers); err != nil { - return false, err - } else if hasGlobalConstraints { - return false, nil - } - - if hasPredicateConstraints, err := patternPart.Constraints.HasConstraints(traversalStepIdentifiers); err != nil { - return false, err - } else if hasPredicateConstraints { - return false, nil - } - return true, nil } diff --git a/cypher/models/pgsql/translate/predicate_test.go b/cypher/models/pgsql/translate/predicate_test.go index 8c2fe76f..d9182a10 100644 --- a/cypher/models/pgsql/translate/predicate_test.go +++ b/cypher/models/pgsql/translate/predicate_test.go @@ -30,8 +30,11 @@ RETURN p`) require.NoError(t, err) require.Contains(t, formatted, "as p from s0 where") - require.Contains(t, formatted, "with s1 as") - require.NotContains(t, formatted, "as p from s1 where") + require.Contains(t, formatted, "exists (select 1 from edge") + require.Contains(t, formatted, "kind_id = any (array [3, 4]::int2[])") + require.Contains(t, formatted, "start_id = (s0.n0).id") + require.Contains(t, formatted, "end_id = (s0.n1).id") + require.NotContains(t, formatted, "with s1 as") } func TestOptimizedPatternPredicatesContinueAfterFirstPlacement(t *testing.T) { From 181596de44626db8579fa831b7769def377f28df Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:19:05 -0700 Subject: [PATCH 072/114] feat(pgsql): lower membership-only collects to ids --- cypher/models/pgsql/optimize/lowering.go | 1 + .../pgsql/translate/collect_id_membership.go | 120 ++++++++++++++++++ cypher/models/pgsql/translate/expression.go | 14 ++ cypher/models/pgsql/translate/function.go | 36 ++++++ .../models/pgsql/translate/function_test.go | 48 +++++++ cypher/models/pgsql/translate/translator.go | 18 +++ 6 files changed, 237 insertions(+) create mode 100644 cypher/models/pgsql/translate/collect_id_membership.go diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index d4d5b709..cd060001 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -13,6 +13,7 @@ const ( LoweringExpansionSuffixPushdown = "ExpansionSuffixPushdown" LoweringPredicatePlacement = "PredicatePlacement" LoweringCountStoreFastPath = "CountStoreFastPath" + LoweringCollectIDMembership = "CollectIDMembership" ) type LoweringDecision struct { diff --git a/cypher/models/pgsql/translate/collect_id_membership.go b/cypher/models/pgsql/translate/collect_id_membership.go new file mode 100644 index 00000000..d710b8ce --- /dev/null +++ b/cypher/models/pgsql/translate/collect_id_membership.go @@ -0,0 +1,120 @@ +package translate + +import ( + "strings" + + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/walk" +) + +type collectIDMembershipUsage struct { + membershipReferences int + otherReferences int +} + +type collectIDMembershipCollector struct { + walk.VisitorHandler + candidates map[pgsql.Identifier]struct{} + usages map[pgsql.Identifier]*collectIDMembershipUsage + stack []cypher.SyntaxNode +} + +func collectIDMembershipAliases(root *cypher.RegularQuery) (map[pgsql.Identifier]struct{}, error) { + candidates, err := collectIDMembershipCandidates(root) + if err != nil || len(candidates) == 0 { + return nil, err + } + + collector := &collectIDMembershipCollector{ + VisitorHandler: walk.NewCancelableErrorHandler(), + candidates: candidates, + usages: map[pgsql.Identifier]*collectIDMembershipUsage{}, + } + if err := walk.Cypher(root, collector); err != nil { + return nil, err + } + + aliases := map[pgsql.Identifier]struct{}{} + for alias := range candidates { + usage := collector.usages[alias] + if usage != nil && usage.membershipReferences > 0 && usage.otherReferences == 0 { + aliases[alias] = struct{}{} + } + } + return aliases, nil +} + +func collectIDMembershipCandidates(root *cypher.RegularQuery) (map[pgsql.Identifier]struct{}, error) { + candidates := map[pgsql.Identifier]struct{}{} + + err := walk.Cypher(root, walk.NewSimpleVisitor[cypher.SyntaxNode](func(node cypher.SyntaxNode, handler walk.VisitorHandler) { + projectionItem, isProjectionItem := node.(*cypher.ProjectionItem) + if !isProjectionItem || projectionItem.Alias == nil { + return + } + + function, isFunction := projectionItem.Expression.(*cypher.FunctionInvocation) + if !isFunction || !strings.EqualFold(function.Name, cypher.CollectFunction) || len(function.Arguments) != 1 { + return + } + + if _, isVariable := function.Arguments[0].(*cypher.Variable); isVariable { + candidates[pgsql.Identifier(projectionItem.Alias.Symbol)] = struct{}{} + } + })) + return candidates, err +} + +func (s *collectIDMembershipCollector) usage(alias pgsql.Identifier) *collectIDMembershipUsage { + usage := s.usages[alias] + if usage == nil { + usage = &collectIDMembershipUsage{} + s.usages[alias] = usage + } + return usage +} + +func (s *collectIDMembershipCollector) Enter(node cypher.SyntaxNode) { + variable, isVariable := node.(*cypher.Variable) + if !isVariable { + s.stack = append(s.stack, node) + return + } + + alias := pgsql.Identifier(variable.Symbol) + if _, isCandidate := s.candidates[alias]; isCandidate { + usage := s.usage(alias) + if s.isProjectionAliasDeclaration(variable) { + // The alias declaration is not a read. + } else if s.isMembershipCollectionOperand(variable) { + usage.membershipReferences++ + } else { + usage.otherReferences++ + } + } + + s.stack = append(s.stack, node) +} + +func (s *collectIDMembershipCollector) Visit(cypher.SyntaxNode) {} + +func (s *collectIDMembershipCollector) Exit(cypher.SyntaxNode) { + s.stack = s.stack[:len(s.stack)-1] +} + +func (s *collectIDMembershipCollector) isProjectionAliasDeclaration(variable *cypher.Variable) bool { + if len(s.stack) == 0 { + return false + } + projectionItem, isProjectionItem := s.stack[len(s.stack)-1].(*cypher.ProjectionItem) + return isProjectionItem && projectionItem.Alias == variable +} + +func (s *collectIDMembershipCollector) isMembershipCollectionOperand(variable *cypher.Variable) bool { + if len(s.stack) == 0 { + return false + } + partial, isPartialComparison := s.stack[len(s.stack)-1].(*cypher.PartialComparison) + return isPartialComparison && partial.Operator == cypher.OperatorIn && partial.Right == variable +} diff --git a/cypher/models/pgsql/translate/expression.go b/cypher/models/pgsql/translate/expression.go index 9e62ec2f..883c144e 100644 --- a/cypher/models/pgsql/translate/expression.go +++ b/cypher/models/pgsql/translate/expression.go @@ -682,6 +682,13 @@ func rewriteIdentityOperands(scope *Scope, newExpression *pgsql.BinaryExpression newExpression.LOperand = pgsql.CompoundIdentifier{typedLOperand, pgsql.ColumnID} newExpression.ROperand = pgsql.CompoundIdentifier{typedROperand, pgsql.ColumnID} + case pgsql.Int8Array: + if newExpression.Operator == pgsql.OperatorIn { + newExpression.LOperand = pgsql.CompoundIdentifier{typedLOperand, pgsql.ColumnID} + } else { + return fmt.Errorf("invalid comparison between types %s and %s", boundLOperand.DataType, boundROperand.DataType) + } + case pgsql.NodeCompositeArray: const unnestElemAlias pgsql.Identifier = "_unnest_elem" newExpression.LOperand = pgsql.CompoundIdentifier{typedLOperand, pgsql.ColumnID} @@ -721,6 +728,13 @@ func rewriteIdentityOperands(scope *Scope, newExpression *pgsql.BinaryExpression newExpression.LOperand = pgsql.CompoundIdentifier{typedLOperand, pgsql.ColumnID} newExpression.ROperand = pgsql.CompoundIdentifier{typedROperand, pgsql.ColumnID} + case pgsql.Int8Array: + if newExpression.Operator == pgsql.OperatorIn { + newExpression.LOperand = pgsql.CompoundIdentifier{typedLOperand, pgsql.ColumnID} + } else { + return fmt.Errorf("invalid comparison between types %s and %s", boundLOperand.DataType, boundROperand.DataType) + } + case pgsql.EdgeCompositeArray: newExpression.LOperand = pgsql.CompoundIdentifier{typedLOperand, pgsql.ColumnID} newExpression.ROperand = pgsql.CompoundIdentifier{typedROperand, pgsql.ColumnID} diff --git a/cypher/models/pgsql/translate/function.go b/cypher/models/pgsql/translate/function.go index f0eae7f8..942e6d22 100644 --- a/cypher/models/pgsql/translate/function.go +++ b/cypher/models/pgsql/translate/function.go @@ -8,6 +8,7 @@ import ( "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" ) const legacyToIntegerFunction = "toint" @@ -528,6 +529,28 @@ func prepareCollectExpression(scope *Scope, collectedExpression pgsql.Expression return collectedExpression, castType, nil } +func prepareCollectIDExpression(scope *Scope, collectedExpression pgsql.Expression) (pgsql.Expression, bool) { + identifier, isIdentifier := unwrapParenthetical(collectedExpression).(pgsql.Identifier) + if !isIdentifier { + return nil, false + } + + binding, bound := scope.Lookup(identifier) + if !bound { + return nil, false + } + + switch binding.DataType { + case pgsql.NodeComposite, pgsql.EdgeComposite, pgsql.ExpansionRootNode, pgsql.ExpansionTerminalNode, pgsql.ExpansionEdge: + return pgsql.RowColumnReference{ + Identifier: identifier, + Column: pgsql.ColumnID, + }, true + default: + return nil, false + } +} + func translateNodeLabelsExpression(identifier pgsql.Identifier) pgsql.TypeHinted { const ( kindAlias pgsql.Identifier = "_kind" @@ -839,6 +862,19 @@ func (s *Translator) translateFunction(typedExpression *cypher.FunctionInvocatio s.SetError(fmt.Errorf("expected only one argument for cypher function: %s", typedExpression.Name)) } else if collectedExpression, err := s.treeTranslator.PopOperand(); err != nil { s.SetError(err) + } else if s.collectIDProjectionDepth > 0 { + if idExpression, collectIDs := prepareCollectIDExpression(s.scope, collectedExpression); collectIDs { + s.treeTranslator.PushOperand( + functionWrapCollectToArray(typedExpression.Distinct, idExpression, pgsql.Int8Array), + ) + s.recordLowering(optimize.LoweringCollectIDMembership) + } else if preparedExpression, castType, err := prepareCollectExpression(s.scope, collectedExpression, typedExpression.Name); err != nil { + s.SetError(err) + } else { + s.treeTranslator.PushOperand( + functionWrapCollectToArray(typedExpression.Distinct, preparedExpression, castType), + ) + } } else if preparedExpression, castType, err := prepareCollectExpression(s.scope, collectedExpression, typedExpression.Name); err != nil { s.SetError(err) } else { diff --git a/cypher/models/pgsql/translate/function_test.go b/cypher/models/pgsql/translate/function_test.go index 64926916..42242991 100644 --- a/cypher/models/pgsql/translate/function_test.go +++ b/cypher/models/pgsql/translate/function_test.go @@ -130,3 +130,51 @@ func TestPrepareCollectExpressionMissingBindingErrorNamesArgument(t *testing.T) require.EqualError(t, err, "binding not found for collect function argument missing") } + +func TestCollectMembershipOnlyProjectionUsesIDs(t *testing.T) { + t.Parallel() + + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (s) + WITH collect(s) AS exclude + MATCH (c) + WHERE NOT c IN exclude + RETURN c + `) + 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) + normalized := strings.Join(strings.Fields(formatted), " ") + + require.Contains(t, normalized, "array_agg((n0).id)") + require.Contains(t, normalized, "array []::int8[]") + require.Contains(t, normalized, "not n1.id = any (s0.") + require.NotContains(t, normalized, "array []::nodecomposite[]") + requireOptimizationLowering(t, translation.Optimization, "CollectIDMembership") +} + +func TestReturnedCollectNodeKeepsCompositeArray(t *testing.T) { + t.Parallel() + + kindMapper := pgutil.NewInMemoryKindMapper() + + query, err := frontend.ParseCypher(frontend.NewContext(), `MATCH (s) RETURN collect(s) AS nodes`) + 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) + normalized := strings.Join(strings.Fields(formatted), " ") + + require.Contains(t, normalized, "array []::nodecomposite[]") + require.NotContains(t, normalized, "array_agg((n0).id)") + requireNoOptimizationLowering(t, translation.Optimization, "CollectIDMembership") +} diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 306c2efc..5836b5f0 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -30,6 +30,9 @@ type Translator struct { scope *Scope unwindTargets map[*cypher.Variable]struct{} + collectIDMembershipAliases map[pgsql.Identifier]struct{} + collectIDProjectionDepth int + appliedLoweringCounts map[string]int patternTargets map[*cypher.PatternPart]optimize.PatternTarget patternPredicateTargets map[*cypher.PatternPredicate]optimize.PatternTarget @@ -265,6 +268,11 @@ func (s *Translator) Enter(expression cypher.SyntaxNode) { } case *cypher.ProjectionItem: + if typedExpression.Alias != nil { + if _, collectIDs := s.collectIDMembershipAliases[pgsql.Identifier(typedExpression.Alias.Symbol)]; collectIDs { + s.collectIDProjectionDepth++ + } + } s.query.CurrentPart().PrepareProjection() case *cypher.PatternPredicate: @@ -560,6 +568,11 @@ func (s *Translator) Exit(expression cypher.SyntaxNode) { if err := s.translateProjectionItem(s.scope, typedExpression); err != nil { s.SetError(err) } + if typedExpression.Alias != nil { + if _, collectIDs := s.collectIDMembershipAliases[pgsql.Identifier(typedExpression.Alias.Symbol)]; collectIDs { + s.collectIDProjectionDepth-- + } + } case *cypher.Match: if err := s.translateMatch(typedExpression); err != nil { @@ -705,6 +718,11 @@ func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper } translator := NewTranslator(ctx, kindMapper, parameters, graphID) + if membershipAliases, err := collectIDMembershipAliases(optimizedPlan.Query); err != nil { + return Result{}, err + } else { + translator.collectIDMembershipAliases = membershipAliases + } translator.SetOptimizationPlan(optimizedPlan) translator.translation.Optimization.Rules = optimizedPlan.Rules translator.translation.Optimization.PredicateAttachments = optimizedPlan.PredicateAttachments From 835a40206262a0d184589aacd61d52eb0fc6def6 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:25:46 -0700 Subject: [PATCH 073/114] perf(pgsql): flip bound expansions to constrained terminals --- cypher/models/pgsql/optimize/lowering_plan.go | 61 ++++++++++++++++++- .../models/pgsql/optimize/optimizer_test.go | 29 +++++++++ cypher/models/pgsql/translate/expansion.go | 11 ++++ .../pgsql/translate/optimizer_safety_test.go | 25 ++++++++ cypher/models/pgsql/translate/traversal.go | 8 ++- 5 files changed, 130 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 6fe3ecb4..7a997fa0 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -347,8 +347,9 @@ func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, r } for stepIndex, step := range steps { + target := patternTarget.TraversalStep(stepIndex) if decision, shouldFlip := traversalDirectionDecisionForStep( - patternTarget.TraversalStep(stepIndex), + target, stepIndex, step, declaredEndpoints[stepIndex], @@ -356,6 +357,15 @@ func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, r referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.RightNode.Variable)), ); shouldFlip { plan.TraversalDirection = append(plan.TraversalDirection, decision) + } else if decision, shouldFlip := boundLeftExpansionDirectionDecisionForStep( + target, + patternPart, + steps, + stepIndex, + step, + declaredEndpoints[stepIndex], + ); shouldFlip { + plan.TraversalDirection = append(plan.TraversalDirection, decision) } } @@ -395,9 +405,10 @@ func traversalDirectionDecisionForStep( } rightSymbol := variableSymbol(step.RightNode.Variable) + leftSymbol := variableSymbol(step.LeftNode.Variable) if rightSymbol != "" { if _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol]; rightBound { - if rightSymbol == variableSymbol(step.LeftNode.Variable) { + if rightSymbol == leftSymbol { return TraversalDirectionDecision{}, false } @@ -428,6 +439,52 @@ func traversalDirectionDecisionForStep( return TraversalDirectionDecision{}, false } +func boundLeftExpansionDirectionDecisionForStep( + target TraversalStepTarget, + patternPart *cypher.PatternPart, + steps []sourceTraversalStep, + stepIndex int, + step sourceTraversalStep, + declaredEndpoints declaredStepEndpoints, +) (TraversalDirectionDecision, bool) { + if patternPart == nil || + patternPart.Variable != nil || + patternPart.ShortestPathPattern || + patternPart.AllShortestPathsPattern || + len(steps) != 1 || + stepIndex != 0 || + step.Relationship == nil || + step.Relationship.Range == nil || + step.Relationship.Direction == graph.DirectionBoth || + step.Relationship.Variable != nil || + nodePatternHasConstraints(step.LeftNode) || + !nodePatternHasConstraints(step.RightNode) { + return TraversalDirectionDecision{}, false + } + + leftSymbol := variableSymbol(step.LeftNode.Variable) + rightSymbol := variableSymbol(step.RightNode.Variable) + if leftSymbol == "" || leftSymbol == rightSymbol { + return TraversalDirectionDecision{}, false + } + + if _, leftBound := declaredEndpoints.BeforeLeftNode[leftSymbol]; !leftBound { + return TraversalDirectionDecision{}, false + } + + if rightSymbol != "" { + if _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol]; rightBound { + return TraversalDirectionDecision{}, false + } + } + + return TraversalDirectionDecision{ + Target: target, + Flip: true, + Reason: traversalDirectionReasonRightConstrained, + }, true +} + func appendShortestPathStrategyDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, predicateConstrainedSymbols map[string]struct{}) { declaredSymbols := map[string]struct{}{} diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index b5634686..f7c37ec6 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -703,6 +703,35 @@ func TestLoweringPlanReportsTraversalDirectionForRightEndpointPredicate(t *testi }}, plan.LoweringPlan.TraversalDirection) } +func TestLoweringPlanReportsTraversalDirectionForBoundLeftExpansionToConstrainedRightEndpoint(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true AND u.enabled = true + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) + WITH DISTINCT u, COUNT(c) AS adminCount + RETURN u + ORDER BY adminCount DESC + LIMIT 100 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + Flip: true, + Reason: traversalDirectionReasonRightConstrained, + }}, plan.LoweringPlan.TraversalDirection) +} + func TestLoweringPlanSkipsSuffixPushdownAfterRightEndpointPredicateDirectionFlip(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 5d867a7c..32c3ffcb 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -2555,6 +2555,17 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte ), ) } + if previousProjectionFrameID != "" && traversalStep.RightNodeBound { + projectionConstraints = pgsql.OptionalAnd( + projectionConstraints, + boundEndpointProjectionConstraint( + previousProjectionFrameID, + traversalStep.RightNode.Identifier, + expansionModel.Frame.Binding.Identifier, + expansionNextID, + ), + ) + } projectionConstraints = rewriteCurrentFrameProjectionReferences( projectionConstraints, diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index f2c6eaba..ac53ad52 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -44,6 +44,9 @@ func optimizerSafetyKindMapper() *pgutil.InMemoryKindMapper { "RootCA", "RootCAFor", "TrustedForNTAuth", + "AdminTo", + "Computer", + "User", }) { mapper.Put(kind) } @@ -575,6 +578,28 @@ RETURN p require.Contains(t, normalizedQuery, "join edge e0 on e0.end_id = s1_seed.root_id") } +func TestOptimizerSafetyTraversalDirectionUsesBoundLeftExpansionTerminalConstraint(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true AND u.enabled = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + require.Contains(t, normalizedQuery, "join edge e0 on e0.end_id = s3_seed.root_id") + require.Contains(t, normalizedQuery, "(s1.n0).id = s3.next_id") +} + func TestOptimizerSafetyShortestPathStrategyUsesPlannedBidirectionalSearch(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 0747b628..7db07001 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -54,8 +54,12 @@ func (s *Translator) traversalDirectionDecision(part *PatternPart, stepIndex int func (s *Translator) applyPatternConstraintBalance(part *PatternPart, stepIndex int, constraints *PatternConstraints, traversalStep *TraversalStep) error { if decision, hasDecision := s.traversalDirectionDecision(part, stepIndex); hasDecision { - if decision.Flip && !traversalStep.LeftNodeBound { - if traversalStep.RightNodeBound && !traversalStep.hasPreviousFrameBinding() { + if decision.Flip { + if traversalStep.LeftNodeBound { + if traversalStep.Expansion == nil || !traversalStep.hasPreviousFrameBinding() { + return nil + } + } else if traversalStep.RightNodeBound && !traversalStep.hasPreviousFrameBinding() { return nil } From 1a348751fe42c119242d419253f4d229b6e079a4 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:29:37 -0700 Subject: [PATCH 074/114] feat(pgsql): plan terminal filters for kind-only shortest paths --- cypher/models/pgsql/optimize/lowering_plan.go | 113 +++++++++++++++++- .../models/pgsql/optimize/optimizer_test.go | 27 +++++ .../pgsql/translate/optimizer_safety_test.go | 21 ++++ 3 files changed, 157 insertions(+), 4 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 7a997fa0..e74e27fa 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -78,8 +78,9 @@ func appendQueryPartLowerings( appendPatternPredicatePlacementDecisions(plan, queryPartIndex, queryPart) appendExpandIntoDecisions(plan, queryPartIndex, readingClauses) appendTraversalDirectionDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) - appendShortestPathStrategyDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) - appendShortestPathFilterDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) + shortestPathSearchSymbols := shortestPathSearchPredicateSymbols(readingClauses) + appendShortestPathStrategyDecisions(plan, queryPartIndex, readingClauses, shortestPathSearchSymbols) + appendShortestPathFilterDecisions(plan, queryPartIndex, readingClauses, shortestPathSearchSymbols) appendLimitPushdownDecisions(plan, queryPartIndex, queryPart, readingClauses) appendExpansionSuffixPushdownDecisions(plan, queryPartIndex, readingClauses) return nil @@ -392,6 +393,102 @@ func bindingPredicateSymbols(predicateAttachments []PredicateAttachment, queryPa return symbols } +func shortestPathSearchPredicateSymbols(readingClauses []*cypher.ReadingClause) map[string]struct{} { + symbols := map[string]struct{}{} + + for _, readingClause := range readingClauses { + if readingClause == nil || readingClause.Match == nil || readingClause.Match.Where == nil { + continue + } + + for _, expression := range readingClause.Match.Where.Expressions { + addShortestPathSearchPredicateSymbols(symbols, expression) + } + } + + return symbols +} + +func addShortestPathSearchPredicateSymbols(symbols map[string]struct{}, expression cypher.Expression) { + for _, term := range cypherConjunctionTerms(expression) { + if symbol, ok := shortestPathSearchPredicateSymbol(term); ok { + addSymbol(symbols, symbol) + } + } +} + +func cypherConjunctionTerms(expression cypher.Expression) []cypher.Expression { + if conjunction, isConjunction := expression.(*cypher.Conjunction); isConjunction { + var terms []cypher.Expression + for _, subexpression := range conjunction.Expressions { + terms = append(terms, cypherConjunctionTerms(subexpression)...) + } + + return terms + } + + return []cypher.Expression{expression} +} + +func shortestPathSearchPredicateSymbol(expression cypher.Expression) (string, bool) { + comparison, isComparison := expression.(*cypher.Comparison) + if !isComparison || len(comparison.Partials) != 1 { + return "", false + } + + partial := comparison.Partials[0] + if !isEndpointSearchOperator(partial.Operator) { + return "", false + } + + if symbol, ok := propertyLookupVariableSymbol(comparison.Left); ok && !expressionReferencesAnySource(partial.Right) { + return symbol, true + } + + if symbol, ok := propertyLookupVariableSymbol(partial.Right); ok && !expressionReferencesAnySource(comparison.Left) { + return symbol, true + } + + return "", false +} + +func isEndpointSearchOperator(operator cypher.Operator) bool { + switch operator { + case cypher.OperatorEquals, + cypher.OperatorRegexMatch, + cypher.OperatorGreaterThan, + cypher.OperatorGreaterThanOrEqualTo, + cypher.OperatorLessThan, + cypher.OperatorLessThanOrEqualTo, + cypher.OperatorStartsWith, + cypher.OperatorEndsWith, + cypher.OperatorContains, + cypher.OperatorIn: + return true + default: + return false + } +} + +func propertyLookupVariableSymbol(expression cypher.Expression) (string, bool) { + propertyLookup, isPropertyLookup := expression.(*cypher.PropertyLookup) + if !isPropertyLookup || propertyLookup == nil { + return "", false + } + + variable, isVariable := propertyLookup.Atom.(*cypher.Variable) + if !isVariable || variable == nil || variable.Symbol == "" { + return "", false + } + + return variable.Symbol, true +} + +func expressionReferencesAnySource(expression cypher.Expression) bool { + references, err := collectReferencedSourceIdentifiers(expression) + return err != nil || len(references) > 0 +} + func traversalDirectionDecisionForStep( target TraversalStepTarget, stepIndex int, @@ -573,6 +670,14 @@ func endpointHasSearchConstraint(nodePattern *cypher.NodePattern, symbol string, return nodePattern.Properties != nil || referencesSourceIdentifier(predicateConstrainedSymbols, symbol) } +func endpointHasTerminalFilterConstraint(nodePattern *cypher.NodePattern, symbol string, predicateConstrainedSymbols map[string]struct{}) bool { + if nodePattern == nil { + return false + } + + return nodePatternHasConstraints(nodePattern) || referencesSourceIdentifier(predicateConstrainedSymbols, symbol) +} + func appendShortestPathFilterDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, predicateConstrainedSymbols map[string]struct{}) { declaredSymbols := map[string]struct{}{} @@ -641,11 +746,11 @@ func shortestPathFilterDecisionForStep( leftSearchConstrained := endpointHasSearchConstraint(step.LeftNode, leftSymbol, predicateConstrainedSymbols) rightSearchConstrained := endpointHasSearchConstraint(step.RightNode, rightSymbol, predicateConstrainedSymbols) - if !rightSearchConstrained { + if !endpointHasTerminalFilterConstraint(step.RightNode, rightSymbol, predicateConstrainedSymbols) { return ShortestPathFilterDecision{}, false } - if hasShortestPathBidirectionalStrategy(plan, target) && leftSearchConstrained { + if hasShortestPathBidirectionalStrategy(plan, target) && leftSearchConstrained && rightSearchConstrained { return ShortestPathFilterDecision{ Target: target, Mode: ShortestPathFilterEndpointPair, diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index f7c37ec6..f3df3b63 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -849,6 +849,33 @@ func TestLoweringPlanReportsShortestPathTerminalFilter(t *testing.T) { }}, plan.LoweringPlan.ShortestPathFilter) } +func TestLoweringPlanReportsShortestPathTerminalFilterForKindOnlyTerminal(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH p = shortestPath((s:Group)-[:MemberOf|GenericAll|AdminTo*1..]->(t:Tag_Tier_Zero)) + WHERE s.objectid ENDS WITH '-513' AND s <> t + RETURN p + LIMIT 1000 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.NotContains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringShortestPathStrategy}) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringShortestPathFilter}) + require.Equal(t, []ShortestPathFilterDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Mode: ShortestPathFilterTerminal, + Reason: shortestPathFilterReasonTerminalPredicate, + }}, plan.LoweringPlan.ShortestPathFilter) +} + func TestLoweringPlanReportsTraversalLimitPushdown(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index ac53ad52..ced6e779 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -46,6 +46,7 @@ func optimizerSafetyKindMapper() *pgutil.InMemoryKindMapper { "TrustedForNTAuth", "AdminTo", "Computer", + "Tag_Tier_Zero", "User", }) { mapper.Put(kind) @@ -640,6 +641,26 @@ RETURN p requireOptimizationLowering(t, translation.Optimization, "ShortestPathFilterMaterialization") } +func TestOptimizerSafetyShortestPathKindOnlyTerminalFilterUsesPlannedMaterialization(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH p = shortestPath((s:Group)-[:MemberOf|GenericAll|AdminTo*1..]->(t:Tag_Tier_Zero)) +WHERE s.objectid ENDS WITH '-513' AND s <> t +RETURN p +LIMIT 1000 + `) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "unidirectional_sp_harness") + require.Contains(t, normalizedQuery, "traversal_terminal_filter") + requirePlannedOptimizationLowering(t, translation.Optimization, "ShortestPathFilterMaterialization") + requireOptimizationLowering(t, translation.Optimization, "ShortestPathFilterMaterialization") +} + func TestOptimizerSafetyLimitPushdownUsesPlannedTraversalFrame(t *testing.T) { t.Parallel() From 4c2bbf63c6cca45f88519d233023e07ddcb1bc10 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:30:36 -0700 Subject: [PATCH 075/114] refactor(pgsql): defer blanket suffix indexing --- README.md | 4 ++++ cypher/Cypher Syntax Support.md | 4 ++++ .../models/pgsql/optimize/OPTIMIZATION_PLAN.md | 17 +++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/README.md b/README.md index 2f898937..0dc6296c 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,10 @@ indexes created on expressions such as `properties ->> 'objectid'` and `properti anchors without matching JSON booleans or numbers. Simple relationship count fast paths depend on the schema's `kind_id`-first edge index for efficient typed counts. +Substring and suffix predicates are intentionally not promoted to blanket schema indexes. PostgreSQL deployments can +request explicit `TextSearchIndex`/trigram property indexes for fields that need `CONTAINS`, `STARTS WITH`, or +`ENDS WITH`, but default schema assertion should wait until all suffix forms share one semantics-preserving lowering. + Thresholds are report-only by default. To enforce the configured thresholds, run: ```bash diff --git a/cypher/Cypher Syntax Support.md b/cypher/Cypher Syntax Support.md index b4032ff8..35c07814 100644 --- a/cypher/Cypher Syntax Support.md +++ b/cypher/Cypher Syntax Support.md @@ -451,6 +451,10 @@ match (n) where n.name = '1234' return n will use the `name` index regardless of node label. +For substring and suffix searches, PostgreSQL can use explicit `TextSearchIndex`/trigram expression indexes requested +by schema, but CySQL does not add blanket suffix indexes during default schema assertion. Suffix forms are still being +kept conservative so `ENDS WITH`, reversed operands, null handling, and string type semantics remain backend-equivalent. + ### null Behavior Behavior around `null` in SQL differs from how Neo4j executes Cypher. Certain expression operators in Neo4j's diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index 9bac6735..55bb5eb1 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -113,3 +113,20 @@ Status: completed endpoint-oriented traversal indexes. - Added PG-scoped manual integration coverage for strict string equality and a read-only live-plan check that asserts indexed `objectid` lookups use a PostgreSQL index when the connected database exposes the expected expression index. + +## Phase 10: Common Search Follow-Up + +Status: completed + +- Lower typed pattern predicates into correlated relationship `EXISTS` checks when relationship type constraints and + both endpoint correlations are sufficient, avoiding fallback CTEs for common typed existence predicates. +- Lower membership-only `collect(entity)` projections to ID arrays and rewrite membership predicates to `id = any(...)`, + keeping full entity arrays only when the collected value is otherwise observed. +- Flip single-step bound-left variable expansions toward constrained terminal kinds when there is no path binding or + continuation step, and preserve the previous-frame endpoint correlation after the flip. +- Plan shortest-path terminal-filter materialization for kind-only terminal endpoints while keeping endpoint-pair + filters limited to property/search predicates that define the pair universe. +- Defer adding blanket suffix/reverse expression indexes to schema assertion. Live common searches use `objectid` + suffix predicates, but the translator still has multiple suffix-preserving forms (`LIKE`, `cypher_ends_with`, and + null-coalesced variants). Explicit `TextSearchIndex`/trigram indexes remain available for deployments that need + substring acceleration before those semantics are unified. From c31f1de27f8f087d085c9c91ec34a3c88383de51 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:32:45 -0700 Subject: [PATCH 076/114] test(pgsql): update translation snapshots for optimizer lowerings --- cypher/models/pgsql/test/translation_cases/multipart.sql | 6 +++--- cypher/models/pgsql/test/translation_cases/nodes.sql | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index b1e52d9a..dce5b88d 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -36,7 +36,7 @@ with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (sel with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (not ((n0.properties ->> 'pwdlastset'))::float8 = any (array [- 1, 0]::float8[]) and ((n0.properties ->> 'pwdlastset'))::numeric < (extract(epoch from now()::timestamp with time zone)::numeric - (s0.i0 * 86400))) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1 limit 100; -- case: match (n:NodeKind1) where n.hasspn = true and n.enabled = true and not n.objectid ends with '-502' and not coalesce(n.gmsa, false) = true and not coalesce(n.msa, false) = true match (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) with distinct n, count(c) as adminCount return n order by adminCount desc limit 100 -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select distinct (s1.n0).id as root_id from s1), s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s3_seed join edge e0 on e0.start_id = s3_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s3.path || e0.id from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s3.depth < 15 and not s3.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.next_id offset 0) n1 on true where s3.satisfied and (s1.n0).id = s3.root_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; +with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s3(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 s3_seed join edge e0 on e0.end_id = s3_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.start_id, s3.depth + 1, false, false, e0.id || s3.path from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s3.depth < 15 and not s3.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.next_id offset 0) n0 on true where (s1.n0).id = s3.next_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; -- case: match (n:NodeKind1) where n.objectid = 'S-1-5-21-1260426776-3623580948-1897206385-23225' match p = (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) return p with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'objectid')) = 'string' and (n0.properties ->> 'objectid') = 'S-1-5-21-1260426776-3623580948-1897206385-23225')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; @@ -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 ((jsonb_typeof((n0.properties -> 'domain')) = 'string' and (n0.properties ->> 'domain') = ' ') 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 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 (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 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, 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 case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3, s4.n4]::nodecomposite[])::pathcomposite end 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 (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((n0).id)::int8[], array []::int8[])::int8[], null)::int8[] as i0 from s1), s3 as (select e1.id 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 n3 on n3.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n3.id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.start_id where (not n2.id = any (s0.i0)) and 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, 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 limit 100) select case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null or s4.ep1 is null or (s4.n4).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s4.ep1) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3, s4.n4]::nodecomposite[])::pathcomposite end as p from s4 limit 100; -- case: match (n:NodeKind1)<-[:EdgeKind1]-(:NodeKind2) where n.objectid ends with '-516' with n, count(n) as dc_count where dc_count = 1 return n with s0 as (with s1 as (select (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); @@ -66,7 +66,7 @@ with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, with s0 as (select 'a' as i0, 'b' as i1), s1 as (with s2 as (select s0.i0 as i0, s0.i1 as i1, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and ((n0.properties ->> 'domain') = s0.i1 and cypher_starts_with((n0.properties ->> 'name'), (i0)::text)::bool)) select array_remove(coalesce(array_agg(lower(((s2.n1).properties ->> 'samaccountname'))::text)::text[], array []::text[])::text[], null)::text[] as i2, lower(((s2.n0).properties ->> 'samaccountname'))::text as i3 from s2 group by lower(((s2.n0).properties ->> 'samaccountname'))::text), s3 as (select s1.i2 as i2, s1.i3 as i3, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s1, edge e1 join node n2 on n2.id = e1.start_id join node n3 on n3.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n3.id = e1.end_id where (not lower((n3.properties ->> 'samaccountname'))::text = any (s1.i2)) and e1.kind_id = any (array [4]::int2[]) and (lower((n2.properties ->> 'samaccountname'))::text = s1.i3)) select s3.n3 as g from s3; -- case: match p =(n:NodeKind1)<-[r:EdgeKind1|EdgeKind2*..3]-(u:NodeKind1) where n.domain = 'test' with n, count(r) as incomingCount where incomingCount > 90 with collect(n) as lotsOfAdmins match p =(n:NodeKind1)<-[:EdgeKind1]-() where n in lotsOfAdmins return p -with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'domain')) = 'string' and (n0.properties ->> 'domain') = 'test')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg(s0.n0)::nodecomposite[], array []::nodecomposite[])::nodecomposite[], null)::nodecomposite[] as i1 from s0 where (s0.i0 > 90)), s4 as (select e1.id as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3]::nodecomposite[])::pathcomposite end as p from s4 where ((s4.n2).id = any ((select (_unnest_elem).id from unnest(s4.i1) as _unnest_elem))); +with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (select n0.id as root_id from node n0 where ((jsonb_typeof((n0.properties -> 'domain')) = 'string' and (n0.properties ->> 'domain') = 'test')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.end_id, e0.start_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.end_id = e0.start_id, array [e0.id] from s2_seed join edge e0 on e0.end_id = s2_seed.root_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.start_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.start_id where s2.depth < 3 and not s2.is_cycle) select (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s2.path) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied) select s1.n0 as n0, count(s1.e0)::int8 as i0 from s1 group by n0), s3 as (select array_remove(coalesce(array_agg((n0).id)::int8[], array []::int8[])::int8[], null)::int8[] as i1 from s0 where (s0.i0 > 90)), s4 as (select e1.id as e1, s3.i1 as i1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3 from s3, edge e1 join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id join node n3 on n3.id = e1.start_id where e1.kind_id = any (array [3]::int2[]) and (n2.id = any (s3.i1))) select case when (s4.n2).id is null or s4.e1 is null or (s4.n3).id is null then null else ordered_edges_to_path(s4.n2, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e1]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n2, s4.n3]::nodecomposite[])::pathcomposite end as p from s4; -- case: match (u:NodeKind1)-[:EdgeKind1]->(g:NodeKind2) with g match (g)<-[:EdgeKind1]-(u:NodeKind1) return g with s0 as (with s1 as (select (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select s1.n1 as n1 from s1), s2 as (select s0.n1 as n1 from s0 join edge e1 on (s0.n1).id = e1.end_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.start_id where e1.kind_id = any (array [3]::int2[])) select s2.n1 as g from s2; diff --git a/cypher/models/pgsql/test/translation_cases/nodes.sql b/cypher/models/pgsql/test/translation_cases/nodes.sql index 6feaf94e..1fa6e1c8 100644 --- a/cypher/models/pgsql/test/translation_cases/nodes.sql +++ b/cypher/models/pgsql/test/translation_cases/nodes.sql @@ -208,7 +208,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and lower((n0.properties ->> 'tenantid'))::text like '%myid%' and (n0.properties ->> 'system_tags') like '%tag%')) select s0.n0 as n from s0; -- case: match (s) where not (s)-[]-() return s -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not exists (select 1 from edge e0 where (e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id))); -- case: match (s) where not (s)-[]->()-[]->() return s with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select e0.id as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n1 on n1.id = e0.end_id where (s0.n0).id = e0.start_id), s2 as (select s1.e0 as e0, s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where e1.id != s1.e0) select count(*) > 0 from s2)); @@ -235,7 +235,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n3') and n1.id = e0.end_id where (jsonb_typeof((e0.properties -> 'prop')) = 'string' and (e0.properties ->> 'prop') = 'a') and (s0.n0).id = e0.start_id) select count(*) > 0 from s1)); -- case: match (s) where not (s)-[]-() return id(s) -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (s0.n0).id from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id)); +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (s0.n0).id from s0 where (not exists (select 1 from edge e0 where (e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id))); -- case: match (n) where n.system_tags contains ($param) return n -- pgsql_params:{"pi0":null} From 3c446ac6d5d8ebb44f0245444e3f71386ee4b4ab Mon Sep 17 00:00:00 2001 From: John Hopper Date: Fri, 22 May 2026 21:40:29 -0700 Subject: [PATCH 077/114] test(integration): correct bounded Azure path assertion --- integration/testdata/cases/optimizer_inline.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index a23110fb..fe2253d5 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -336,9 +336,9 @@ ] }, "assert": { - "row_count": 2, - "path_node_ids": [["role", "direct-user"], ["role", "delegated-group", "delegated-user"]], - "path_edge_kinds": [["AZHasRole"], ["AZHasRole", "AZMemberOf"]] + "row_count": 3, + "path_node_ids": [["role", "direct-user"], ["role", "delegated-group"], ["role", "delegated-group", "delegated-user"]], + "path_edge_kinds": [["AZHasRole"], ["AZHasRole"], ["AZHasRole", "AZMemberOf"]] } } ] From d8b48352d94cb47e908fc36615d41c701d89417f Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 13:10:12 -0700 Subject: [PATCH 078/114] feat(pgsql): continue lowering and live query optimization --- cypher/models/pgsql/optimize/lowering.go | 36 +- cypher/models/pgsql/optimize/lowering_plan.go | 267 ++++++++++ .../models/pgsql/optimize/optimizer_test.go | 40 +- .../test/translation_cases/multipart.sql | 2 +- .../translate/aggregate_traversal_count.go | 501 ++++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 41 +- cypher/models/pgsql/translate/translator.go | 11 + 7 files changed, 885 insertions(+), 13 deletions(-) create mode 100644 cypher/models/pgsql/translate/aggregate_traversal_count.go diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index cd060001..efe65c45 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -1,6 +1,9 @@ package optimize -import "github.com/specterops/dawgs/cypher/models/cypher" +import ( + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/graph" +) const ( LoweringProjectionPruning = "ProjectionPruning" @@ -14,6 +17,7 @@ const ( LoweringPredicatePlacement = "PredicatePlacement" LoweringCountStoreFastPath = "CountStoreFastPath" LoweringCollectIDMembership = "CollectIDMembership" + LoweringAggregateTraversalCount = "AggregateTraversalCount" ) type LoweringDecision struct { @@ -160,6 +164,31 @@ type CountStoreFastPathDecision struct { KindSymbols []string `json:"kind_symbols,omitempty"` } +type AggregateTraversalCountDecision struct { + QueryPartIndex int `json:"query_part_index"` + SourceSymbol string `json:"source_symbol"` + TerminalSymbol string `json:"terminal_symbol"` + CountAlias string `json:"count_alias"` + Limit int64 `json:"limit,omitempty"` + Target TraversalStepTarget `json:"target"` +} + +type AggregateTraversalCountShape struct { + QueryPartIndex int + SourceSymbol string + TerminalSymbol string + CountAlias string + Limit int64 + SourceMatch *cypher.Match + SourceKinds graph.Kinds + TerminalKinds graph.Kinds + RelationshipKinds graph.Kinds + Direction graph.Direction + MinDepth int64 + MaxDepth int64 + Target TraversalStepTarget +} + type LoweringPlan struct { ProjectionPruning []ProjectionPruningDecision `json:"projection_pruning,omitempty"` LatePathMaterialization []LatePathMaterializationDecision `json:"late_path_materialization,omitempty"` @@ -172,6 +201,7 @@ type LoweringPlan struct { PredicatePlacement []PredicatePlacementDecision `json:"predicate_placement,omitempty"` PatternPredicate []PatternPredicatePlacementDecision `json:"pattern_predicate_placement,omitempty"` CountStoreFastPath []CountStoreFastPathDecision `json:"count_store_fast_path,omitempty"` + AggregateTraversalCount []AggregateTraversalCountDecision `json:"aggregate_traversal_count,omitempty"` } func (s LoweringPlan) Empty() bool { @@ -185,7 +215,8 @@ func (s LoweringPlan) Empty() bool { len(s.ExpansionSuffixPushdown) == 0 && len(s.PredicatePlacement) == 0 && len(s.PatternPredicate) == 0 && - len(s.CountStoreFastPath) == 0 + len(s.CountStoreFastPath) == 0 && + len(s.AggregateTraversalCount) == 0 } func (s LoweringPlan) Decisions() []LoweringDecision { @@ -206,6 +237,7 @@ func (s LoweringPlan) Decisions() []LoweringDecision { add(LoweringExpansionSuffixPushdown, len(s.ExpansionSuffixPushdown) > 0) add(LoweringPredicatePlacement, len(s.PredicatePlacement) > 0 || len(s.PatternPredicate) > 0) add(LoweringCountStoreFastPath, len(s.CountStoreFastPath) > 0) + add(LoweringAggregateTraversalCount, len(s.AggregateTraversalCount) > 0) return decisions } diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index e74e27fa..4880473c 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -57,6 +57,7 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic appendPredicatePlacementDecisions(&plan, query, predicateAttachments) attachPredicatePlacementsToSuffixPushdowns(&plan) appendCountStoreFastPathDecisions(&plan, query) + appendAggregateTraversalCountDecisions(&plan, query) return plan, nil } @@ -365,6 +366,7 @@ func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, r stepIndex, step, declaredEndpoints[stepIndex], + referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.RightNode.Variable)), ); shouldFlip { plan.TraversalDirection = append(plan.TraversalDirection, decision) } @@ -543,6 +545,7 @@ func boundLeftExpansionDirectionDecisionForStep( stepIndex int, step sourceTraversalStep, declaredEndpoints declaredStepEndpoints, + rightHasAttachedPredicate bool, ) (TraversalDirectionDecision, bool) { if patternPart == nil || patternPart.Variable != nil || @@ -559,6 +562,10 @@ func boundLeftExpansionDirectionDecisionForStep( return TraversalDirectionDecision{}, false } + if step.RightNode.Properties == nil && !rightHasAttachedPredicate { + return TraversalDirectionDecision{}, false + } + leftSymbol := variableSymbol(step.LeftNode.Variable) rightSymbol := variableSymbol(step.RightNode.Variable) if leftSymbol == "" || leftSymbol == rightSymbol { @@ -991,6 +998,266 @@ func appendCountStoreFastPathDecisions(plan *LoweringPlan, query *cypher.Regular } } +func appendAggregateTraversalCountDecisions(plan *LoweringPlan, query *cypher.RegularQuery) { + if shape, ok := AggregateTraversalCountShapeForQuery(query); ok { + plan.AggregateTraversalCount = append(plan.AggregateTraversalCount, AggregateTraversalCountDecision{ + QueryPartIndex: shape.QueryPartIndex, + SourceSymbol: shape.SourceSymbol, + TerminalSymbol: shape.TerminalSymbol, + CountAlias: shape.CountAlias, + Limit: shape.Limit, + Target: shape.Target, + }) + } +} + +func AggregateTraversalCountShapeForQuery(query *cypher.RegularQuery) (AggregateTraversalCountShape, bool) { + if query == nil || query.SingleQuery == nil || query.SingleQuery.MultiPartQuery == nil { + return AggregateTraversalCountShape{}, false + } + + multiPartQuery := query.SingleQuery.MultiPartQuery + if len(multiPartQuery.Parts) != 1 || multiPartQuery.Parts[0] == nil || multiPartQuery.SinglePartQuery == nil { + return AggregateTraversalCountShape{}, false + } + + part := multiPartQuery.Parts[0] + if len(part.UpdatingClauses) > 0 || len(part.ReadingClauses) != 2 || part.With == nil || part.With.Where != nil { + return AggregateTraversalCountShape{}, false + } + + sourceMatch, sourceNode, sourceSymbol, ok := aggregateTraversalSourceMatch(part.ReadingClauses[0]) + if !ok { + return AggregateTraversalCountShape{}, false + } + + relationship, terminalNode, terminalSymbol, ok := aggregateTraversalMatch(part.ReadingClauses[1], sourceSymbol) + if !ok { + return AggregateTraversalCountShape{}, false + } + + countAlias, ok := aggregateTraversalWithProjection(part.With.Projection, sourceSymbol, terminalSymbol) + if !ok { + return AggregateTraversalCountShape{}, false + } + + limit, ok := aggregateTraversalFinalProjection(multiPartQuery.SinglePartQuery, sourceSymbol, countAlias) + if !ok { + return AggregateTraversalCountShape{}, false + } + + minDepth, maxDepth, ok := aggregateTraversalDepthBounds(relationship.Range) + if !ok { + return AggregateTraversalCountShape{}, false + } + + return AggregateTraversalCountShape{ + QueryPartIndex: 0, + SourceSymbol: sourceSymbol, + TerminalSymbol: terminalSymbol, + CountAlias: countAlias, + Limit: limit, + SourceMatch: sourceMatch, + SourceKinds: sourceNode.Kinds, + TerminalKinds: terminalNode.Kinds, + RelationshipKinds: relationship.Kinds, + Direction: relationship.Direction, + MinDepth: minDepth, + MaxDepth: maxDepth, + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + }, true +} + +func aggregateTraversalSourceMatch(readingClause *cypher.ReadingClause) (*cypher.Match, *cypher.NodePattern, string, bool) { + if readingClause == nil || readingClause.Match == nil { + return nil, nil, "", false + } + + match := readingClause.Match + if match.Optional || len(match.Pattern) != 1 { + return nil, nil, "", false + } + + patternPart := match.Pattern[0] + nodePattern, ok := singleNodePattern(patternPart) + if !ok || nodePattern == nil || nodePattern.Variable == nil || nodePattern.Variable.Symbol == "" || nodePattern.Properties != nil { + return nil, nil, "", false + } + + for _, dependency := range sortedDependencies(match.Where) { + if dependency != nodePattern.Variable.Symbol { + return nil, nil, "", false + } + } + + return match, nodePattern, nodePattern.Variable.Symbol, true +} + +func aggregateTraversalMatch(readingClause *cypher.ReadingClause, sourceSymbol string) (*cypher.RelationshipPattern, *cypher.NodePattern, string, bool) { + if readingClause == nil || readingClause.Match == nil { + return nil, nil, "", false + } + + match := readingClause.Match + if match.Optional || match.Where != nil || len(match.Pattern) != 1 { + return nil, nil, "", false + } + + patternPart := match.Pattern[0] + if patternPart == nil || patternPart.Variable != nil || patternPart.ShortestPathPattern || patternPart.AllShortestPathsPattern || len(patternPart.PatternElements) != 3 { + return nil, nil, "", false + } + + leftNode, leftOK := patternPart.PatternElements[0].AsNodePattern() + relationship, relationshipOK := patternPart.PatternElements[1].AsRelationshipPattern() + rightNode, rightOK := patternPart.PatternElements[2].AsNodePattern() + if !leftOK || !relationshipOK || !rightOK || + leftNode == nil || relationship == nil || rightNode == nil || + variableSymbol(leftNode.Variable) != sourceSymbol || + leftNode.Properties != nil || + relationship.Variable != nil || + relationship.Range == nil || + relationship.Properties != nil || + relationship.Direction == graph.DirectionBoth || + rightNode.Properties != nil || + rightNode.Variable == nil || + rightNode.Variable.Symbol == "" { + return nil, nil, "", false + } + + return relationship, rightNode, rightNode.Variable.Symbol, true +} + +func aggregateTraversalWithProjection(projection *cypher.Projection, sourceSymbol, terminalSymbol string) (string, bool) { + if projection == nil || projection.All || projection.Order != nil || projection.Skip != nil || projection.Limit != nil || len(projection.Items) != 2 { + return "", false + } + + if symbol, ok := projectionItemVariableSymbol(projection.Items[0]); !ok || symbol != sourceSymbol { + return "", false + } + + countAlias, ok := projectionItemCountAlias(projection.Items[1], terminalSymbol) + if !ok { + return "", false + } + + return countAlias, true +} + +func aggregateTraversalFinalProjection(queryPart *cypher.SinglePartQuery, sourceSymbol, countAlias string) (int64, bool) { + if queryPart == nil || len(queryPart.ReadingClauses) > 0 || len(queryPart.UpdatingClauses) > 0 || queryPart.Return == nil || queryPart.Return.Projection == nil { + return 0, false + } + + projection := queryPart.Return.Projection + if projection.Distinct || projection.All || projection.Skip != nil || projection.Order == nil || projection.Limit == nil || len(projection.Items) != 1 { + return 0, false + } + + if symbol, ok := projectionItemVariableSymbol(projection.Items[0]); !ok || symbol != sourceSymbol { + return 0, false + } + + if len(projection.Order.Items) != 1 || projection.Order.Items[0] == nil || projection.Order.Items[0].Ascending { + return 0, false + } + + if orderSymbol, ok := expressionVariableSymbol(projection.Order.Items[0].Expression); !ok || orderSymbol != countAlias { + return 0, false + } + + return literalInt64(projection.Limit.Value) +} + +func aggregateTraversalDepthBounds(patternRange *cypher.PatternRange) (int64, int64, bool) { + if patternRange == nil { + return 0, 0, false + } + + minDepth := int64(1) + if patternRange.StartIndex != nil { + minDepth = *patternRange.StartIndex + } + if minDepth < 1 { + return 0, 0, false + } + + maxDepth := int64(15) + if patternRange.EndIndex != nil { + maxDepth = *patternRange.EndIndex + } + if maxDepth < minDepth { + return 0, 0, false + } + + return minDepth, maxDepth, true +} + +func projectionItemVariableSymbol(expression cypher.Expression) (string, bool) { + projectionItem, ok := expression.(*cypher.ProjectionItem) + if !ok || projectionItem == nil || projectionItem.Alias != nil { + return "", false + } + + return expressionVariableSymbol(projectionItem.Expression) +} + +func expressionVariableSymbol(expression cypher.Expression) (string, bool) { + variable, ok := expression.(*cypher.Variable) + if !ok || variable == nil || variable.Symbol == "" { + return "", false + } + + return variable.Symbol, true +} + +func projectionItemCountAlias(expression cypher.Expression, terminalSymbol string) (string, bool) { + projectionItem, ok := expression.(*cypher.ProjectionItem) + if !ok || projectionItem == nil || projectionItem.Alias == nil || projectionItem.Alias.Symbol == "" { + return "", false + } + + function, ok := projectionItem.Expression.(*cypher.FunctionInvocation) + if !ok || function == nil || !strings.EqualFold(function.Name, cypher.CountFunction) || + function.Distinct || len(function.Namespace) > 0 || len(function.Arguments) != 1 { + return "", false + } + + if symbol, ok := expressionVariableSymbol(function.Arguments[0]); !ok || symbol != terminalSymbol { + return "", false + } + + return projectionItem.Alias.Symbol, true +} + +func literalInt64(expression cypher.Expression) (int64, bool) { + literal, ok := expression.(*cypher.Literal) + if !ok || literal == nil || literal.Null { + return 0, false + } + + switch value := literal.Value.(type) { + case int: + return int64(value), value >= 0 + case int8: + return int64(value), value >= 0 + case int16: + return int64(value), value >= 0 + case int32: + return int64(value), value >= 0 + case int64: + return value, value >= 0 + default: + return 0, false + } +} + func countStoreFastPathDecision(query *cypher.RegularQuery) (CountStoreFastPathDecision, bool) { if query == nil || query.SingleQuery == nil || query.SingleQuery.SinglePartQuery == nil { return CountStoreFastPathDecision{}, false diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index f3df3b63..a61cb83b 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -709,11 +709,8 @@ func TestLoweringPlanReportsTraversalDirectionForBoundLeftExpansionToConstrained regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` MATCH (u:User) WHERE u.hasspn = true AND u.enabled = true - MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) - WITH DISTINCT u, COUNT(c) AS adminCount - RETURN u - ORDER BY adminCount DESC - LIMIT 100 + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {name: 'target'}) + RETURN c `) require.NoError(t, err) @@ -732,6 +729,39 @@ func TestLoweringPlanReportsTraversalDirectionForBoundLeftExpansionToConstrained }}, plan.LoweringPlan.TraversalDirection) } +func TestLoweringPlanReportsAggregateTraversalCountForBoundExpansionCount(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true AND u.enabled = true + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) + WITH DISTINCT u, COUNT(c) AS adminCount + RETURN u + ORDER BY adminCount DESC + LIMIT 100 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Empty(t, plan.LoweringPlan.TraversalDirection) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringAggregateTraversalCount}) + require.Equal(t, []AggregateTraversalCountDecision{{ + QueryPartIndex: 0, + SourceSymbol: "u", + TerminalSymbol: "c", + CountAlias: "adminCount", + Limit: 100, + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + }}, plan.LoweringPlan.AggregateTraversalCount) +} + func TestLoweringPlanSkipsSuffixPushdownAfterRightEndpointPredicateDirectionFlip(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index dce5b88d..48741b90 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -36,7 +36,7 @@ with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (sel with s0 as (select 365 as i0), s1 as (select s0.i0 as i0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from s0, node n0 where (not ((n0.properties ->> 'pwdlastset'))::float8 = any (array [- 1, 0]::float8[]) and ((n0.properties ->> 'pwdlastset'))::numeric < (extract(epoch from now()::timestamp with time zone)::numeric - (s0.i0 * 86400))) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s1.n0 as n from s1 limit 100; -- case: match (n:NodeKind1) where n.hasspn = true and n.enabled = true and not n.objectid ends with '-502' and not coalesce(n.gmsa, false) = true and not coalesce(n.msa, false) = true match (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) with distinct n, count(c) as adminCount return n order by adminCount desc limit 100 -with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((n0.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3_seed(root_id) as not materialized (select n1.id as root_id from node n1 where n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s3(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 s3_seed join edge e0 on e0.end_id = s3_seed.root_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s3.root_id, e0.start_id, s3.depth + 1, false, false, e0.id || s3.path from s3 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.end_id = s3.next_id and e0.id != all (s3.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true where s3.depth < 15 and not s3.is_cycle) select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s3.root_id offset 0) n1 on true join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s3.next_id offset 0) n0 on true where (s1.n0).id = s3.next_id) select distinct s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100; +with recursive candidate_sources(root_id) as (select source_node.id as root_id from node source_node where (((source_node.properties -> 'hasspn'))::jsonb = to_jsonb((true)::bool)::jsonb and ((source_node.properties -> 'enabled'))::jsonb = to_jsonb((true)::bool)::jsonb and not coalesce((source_node.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((source_node.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((source_node.properties ->> 'msa'))::bool, false)::bool = true) and source_node.kind_ids operator (pg_catalog.@>) array [1]::int2[]), traversal(root_id, next_id, depth, path) as (select candidate_sources.root_id, e.end_id, 1, array [e.id]::int8[] from candidate_sources join edge e on e.start_id = candidate_sources.root_id where e.kind_id = any (array [3, 4]::int2[]) union all select traversal.root_id, e.end_id, traversal.depth + 1, traversal.path || e.id from traversal join lateral (select e.id, e.start_id, e.end_id from edge e where e.start_id = traversal.next_id and e.id != all (traversal.path) and e.kind_id = any (array [3, 4]::int2[]) offset 0) e on true where traversal.depth < 15), terminal_nodes(id) as materialized (select terminal_node.id from node terminal_node where terminal_node.kind_ids operator (pg_catalog.@>) array [2]::int2[]), terminal_hits(root_id) as (select traversal.root_id from traversal join terminal_nodes on terminal_nodes.id = traversal.next_id), ranked(root_id, adminCount) as (select terminal_hits.root_id, count(*)::int8 as adminCount from terminal_hits group by terminal_hits.root_id order by adminCount desc limit 100) select (source_node.id, source_node.kind_ids, source_node.properties)::nodecomposite as n from ranked join node source_node on source_node.id = ranked.root_id order by ranked.adminCount desc; -- case: match (n:NodeKind1) where n.objectid = 'S-1-5-21-1260426776-3623580948-1897206385-23225' match p = (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) return p with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'objectid')) = 'string' and (n0.properties ->> 'objectid') = 'S-1-5-21-1260426776-3623580948-1897206385-23225')) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s1 as (with recursive s2_seed(root_id) as not materialized (select distinct (s0.n0).id as root_id from s0), s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s2_seed join edge e0 on e0.start_id = s2_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union all select s2.root_id, e0.end_id, s2.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], false, s2.path || e0.id from s2 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s2.next_id and e0.id != all (s2.path) and e0.kind_id = any (array [3, 4]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, s2 join lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s2.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s2.next_id offset 0) n1 on true where s2.satisfied and (s0.n0).id = s2.root_id) select case when (s1.n0).id is null or s1.ep0 is null or (s1.n1).id is null then null else ordered_edges_to_path(s1.n0, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.ep0) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s1.n0, s1.n1]::nodecomposite[])::pathcomposite end as p from s1; diff --git a/cypher/models/pgsql/translate/aggregate_traversal_count.go b/cypher/models/pgsql/translate/aggregate_traversal_count.go new file mode 100644 index 00000000..e994b70a --- /dev/null +++ b/cypher/models/pgsql/translate/aggregate_traversal_count.go @@ -0,0 +1,501 @@ +package translate + +import ( + "fmt" + "reflect" + + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/pgsql" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" + "github.com/specterops/dawgs/cypher/models/walk" + "github.com/specterops/dawgs/graph" +) + +const ( + aggregateCandidateSourcesCTE pgsql.Identifier = "candidate_sources" + aggregateTraversalCTE pgsql.Identifier = "traversal" + aggregateTerminalNodesCTE pgsql.Identifier = "terminal_nodes" + aggregateTerminalHitsCTE pgsql.Identifier = "terminal_hits" + aggregateRankedCTE pgsql.Identifier = "ranked" + + aggregateSourceAlias pgsql.Identifier = "source_node" + aggregateEdgeAlias pgsql.Identifier = "e" + aggregateTerminalAlias pgsql.Identifier = "terminal_node" + + aggregateRootID pgsql.Identifier = "root_id" + aggregateNextID pgsql.Identifier = "next_id" + aggregateDepth pgsql.Identifier = "depth" + aggregatePath pgsql.Identifier = "path" + aggregateNodeID pgsql.Identifier = "id" +) + +func (s *Translator) translateAggregateTraversalCount(query *cypher.RegularQuery, plan optimize.LoweringPlan) (bool, error) { + if len(plan.AggregateTraversalCount) == 0 { + return false, nil + } + + shape, ok := optimize.AggregateTraversalCountShapeForQuery(query) + if !ok || shape.Target != plan.AggregateTraversalCount[0].Target { + return false, nil + } + + statement, err := s.aggregateTraversalCountQuery(shape) + if err != nil { + return false, err + } + + s.translation.Statement = statement + s.recordLowering(optimize.LoweringAggregateTraversalCount) + return true, nil +} + +func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraversalCountShape) (pgsql.Query, error) { + candidateSources, err := s.buildAggregateCandidateSourcesCTE(shape) + if err != nil { + return pgsql.Query{}, err + } + + traversal, err := s.buildAggregateTraversalCTE(shape) + if err != nil { + return pgsql.Query{}, err + } + + terminalNodes, err := s.buildAggregateTerminalNodesCTE(shape) + if err != nil { + return pgsql.Query{}, err + } + + terminalHits, err := s.buildAggregateTerminalHitsCTE(shape) + if err != nil { + return pgsql.Query{}, err + } + + return pgsql.Query{ + CommonTableExpressions: &pgsql.With{ + Recursive: true, + Expressions: []pgsql.CommonTableExpression{ + candidateSources, + traversal, + terminalNodes, + terminalHits, + s.buildAggregateRankedCTE(shape), + }, + }, + Body: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.AliasedExpression{ + Expression: aggregateNodeComposite(aggregateSourceAlias), + Alias: pgsql.AsOptionalIdentifier(pgsql.Identifier(shape.SourceSymbol)), + }, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: aggregateRankedCTE.AsCompoundIdentifier(), + }, + Joins: []pgsql.Join{{ + Table: pgsql.TableReference{ + Name: pgsql.TableNode.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(aggregateSourceAlias), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateSourceAlias, pgsql.ColumnID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{aggregateRankedCTE, aggregateRootID}, + ), + }, + }}, + }}, + }, + OrderBy: []*pgsql.OrderBy{{ + Expression: pgsql.CompoundIdentifier{aggregateRankedCTE, pgsql.Identifier(shape.CountAlias)}, + Ascending: false, + }}, + }, nil +} + +func (s *Translator) buildAggregateCandidateSourcesCTE(shape optimize.AggregateTraversalCountShape) (pgsql.CommonTableExpression, error) { + whereClause, err := s.aggregateSourceWhere(shape) + if err != nil { + return pgsql.CommonTableExpression{}, err + } + + return pgsql.CommonTableExpression{ + Alias: pgsql.TableAlias{ + Name: aggregateCandidateSourcesCTE, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{aggregateRootID}), + }, + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.AliasedExpression{ + Expression: pgsql.CompoundIdentifier{aggregateSourceAlias, pgsql.ColumnID}, + Alias: pgsql.AsOptionalIdentifier(aggregateRootID), + }, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.TableNode.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(aggregateSourceAlias), + }, + }}, + Where: whereClause, + }, + }, + }, nil +} + +func (s *Translator) buildAggregateTraversalCTE(shape optimize.AggregateTraversalCountShape) (pgsql.CommonTableExpression, error) { + edgeKindConstraint, err := s.aggregateEdgeKindConstraint(aggregateEdgeAlias, shape.RelationshipKinds) + if err != nil { + return pgsql.CommonTableExpression{}, err + } + + sourceColumn, nextColumn := aggregateTraversalColumns(shape.Direction) + primerJoin := pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateEdgeAlias, sourceColumn}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{aggregateCandidateSourcesCTE, aggregateRootID}, + ) + recursiveJoin := pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateEdgeAlias, sourceColumn}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateNextID}, + ) + + return pgsql.CommonTableExpression{ + Alias: pgsql.TableAlias{ + Name: aggregateTraversalCTE, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{ + aggregateRootID, + aggregateNextID, + aggregateDepth, + aggregatePath, + }), + }, + Query: pgsql.Query{ + Body: pgsql.SetOperation{ + Operator: pgsql.OperatorUnion, + All: true, + LOperand: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.CompoundIdentifier{aggregateCandidateSourcesCTE, aggregateRootID}, + pgsql.CompoundIdentifier{aggregateEdgeAlias, nextColumn}, + pgsql.NewLiteral(int64(1), pgsql.Int8), + pgsql.ArrayLiteral{ + Values: []pgsql.Expression{ + pgsql.CompoundIdentifier{aggregateEdgeAlias, pgsql.ColumnID}, + }, + CastType: pgsql.Int8Array, + }, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: aggregateCandidateSourcesCTE.AsCompoundIdentifier(), + }, + Joins: []pgsql.Join{{ + Table: pgsql.TableReference{ + Name: pgsql.TableEdge.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(aggregateEdgeAlias), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: primerJoin, + }, + }}, + }}, + Where: edgeKindConstraint, + }, + ROperand: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateRootID}, + pgsql.CompoundIdentifier{aggregateEdgeAlias, nextColumn}, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateDepth}, + pgsql.OperatorAdd, + pgsql.NewLiteral(int64(1), pgsql.Int8), + ), + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregatePath}, + pgsql.OperatorConcatenate, + pgsql.CompoundIdentifier{aggregateEdgeAlias, pgsql.ColumnID}, + ), + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: aggregateTraversalCTE.AsCompoundIdentifier(), + }, + Joins: []pgsql.Join{{ + Table: pgsql.LateralSubquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.CompoundIdentifier{aggregateEdgeAlias, pgsql.ColumnID}, + pgsql.CompoundIdentifier{aggregateEdgeAlias, pgsql.ColumnStartID}, + pgsql.CompoundIdentifier{aggregateEdgeAlias, pgsql.ColumnEndID}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.TableEdge.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(aggregateEdgeAlias), + }, + }}, + Where: pgsql.OptionalAnd( + pgsql.OptionalAnd( + recursiveJoin, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateEdgeAlias, pgsql.ColumnID}, + pgsql.OperatorNotEquals, + pgsql.NewAllExpression(pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregatePath}), + ), + ), + edgeKindConstraint, + ), + }, + Offset: pgsql.NewLiteral(0, pgsql.Int), + }, + Binding: pgsql.AsOptionalIdentifier(aggregateEdgeAlias), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewLiteral(true, pgsql.Boolean), + }, + }}, + }}, + Where: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateDepth}, + pgsql.OperatorLessThan, + pgsql.NewLiteral(shape.MaxDepth, pgsql.Int8), + ), + }, + }, + }, + }, nil +} + +func (s *Translator) buildAggregateTerminalHitsCTE(shape optimize.AggregateTraversalCountShape) (pgsql.CommonTableExpression, error) { + terminalWhere := pgsql.Expression(nil) + + if shape.MinDepth > 1 { + terminalWhere = pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateDepth}, + pgsql.OperatorGreaterThanOrEqualTo, + pgsql.NewLiteral(shape.MinDepth, pgsql.Int8), + ) + } + + return pgsql.CommonTableExpression{ + Alias: pgsql.TableAlias{ + Name: aggregateTerminalHitsCTE, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{aggregateRootID}), + }, + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateRootID}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: aggregateTraversalCTE.AsCompoundIdentifier(), + }, + Joins: []pgsql.Join{{ + Table: pgsql.TableReference{ + Name: aggregateTerminalNodesCTE.AsCompoundIdentifier(), + }, + JoinOperator: pgsql.JoinOperator{ + JoinType: pgsql.JoinTypeInner, + Constraint: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateTerminalNodesCTE, aggregateNodeID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateNextID}, + ), + }, + }}, + }}, + Where: terminalWhere, + }, + }, + }, nil +} + +func (s *Translator) buildAggregateTerminalNodesCTE(shape optimize.AggregateTraversalCountShape) (pgsql.CommonTableExpression, error) { + terminalWhere, err := s.aggregateNodeKindConstraint(aggregateTerminalAlias, shape.TerminalKinds) + if err != nil { + return pgsql.CommonTableExpression{}, err + } + + return pgsql.CommonTableExpression{ + Materialized: &pgsql.Materialized{Materialized: true}, + Alias: pgsql.TableAlias{ + Name: aggregateTerminalNodesCTE, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{aggregateNodeID}), + }, + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.CompoundIdentifier{aggregateTerminalAlias, pgsql.ColumnID}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.TableNode.AsCompoundIdentifier(), + Binding: pgsql.AsOptionalIdentifier(aggregateTerminalAlias), + }, + }}, + Where: terminalWhere, + }, + }, + }, nil +} + +func (s *Translator) buildAggregateRankedCTE(shape optimize.AggregateTraversalCountShape) pgsql.CommonTableExpression { + countAlias := pgsql.Identifier(shape.CountAlias) + + return pgsql.CommonTableExpression{ + Alias: pgsql.TableAlias{ + Name: aggregateRankedCTE, + Shape: pgsql.NewRecordShape([]pgsql.Identifier{ + aggregateRootID, + countAlias, + }), + }, + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: pgsql.Projection{ + pgsql.CompoundIdentifier{aggregateTerminalHitsCTE, aggregateRootID}, + pgsql.AliasedExpression{ + Expression: pgsql.FunctionCall{ + Function: pgsql.FunctionCount, + Parameters: []pgsql.Expression{pgsql.Wildcard{}}, + CastType: pgsql.Int8, + }, + Alias: pgsql.AsOptionalIdentifier(countAlias), + }, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: aggregateTerminalHitsCTE.AsCompoundIdentifier(), + }, + }}, + GroupBy: []pgsql.Expression{ + pgsql.CompoundIdentifier{aggregateTerminalHitsCTE, aggregateRootID}, + }, + }, + OrderBy: []*pgsql.OrderBy{{ + Expression: countAlias, + Ascending: false, + }}, + Limit: pgsql.NewLiteral(shape.Limit, pgsql.Int8), + }, + } +} + +func (s *Translator) aggregateSourceWhere(shape optimize.AggregateTraversalCountShape) (pgsql.Expression, error) { + sourceKindConstraint, err := s.aggregateNodeKindConstraint(aggregateSourceAlias, shape.SourceKinds) + if err != nil { + return nil, err + } + + sourcePredicate, err := s.aggregateSourcePredicate(shape) + if err != nil { + return nil, err + } + + return pgsql.OptionalAnd(sourcePredicate, sourceKindConstraint), nil +} + +func (s *Translator) aggregateSourcePredicate(shape optimize.AggregateTraversalCountShape) (pgsql.Expression, error) { + if shape.SourceMatch == nil || shape.SourceMatch.Where == nil { + return nil, nil + } + + translator := NewTranslator(s.ctx, s.kindMapper.kindMapper, s.parameters, s.graphID) + sourceBinding := translator.scope.Define(aggregateSourceAlias, pgsql.NodeComposite) + translator.scope.Alias(pgsql.Identifier(shape.SourceSymbol), sourceBinding) + + if err := walk.Cypher(shape.SourceMatch.Where, translator); err != nil { + return nil, err + } + + sourceConstraints, err := translator.treeTranslator.ConsumeConstraintsFromVisibleSet(pgsql.AsIdentifierSet(aggregateSourceAlias)) + if err != nil { + return nil, err + } + + remainingConstraints, err := translator.treeTranslator.ConsumeAllConstraints() + if err != nil { + return nil, err + } + if remainingConstraints.Expression != nil { + return nil, fmt.Errorf("unsupported aggregate traversal source predicate dependencies: %v", remainingConstraints.Dependencies.Slice()) + } + + for key, value := range translator.translation.Parameters { + if existingValue, hasExisting := s.translation.Parameters[key]; hasExisting && !reflect.DeepEqual(existingValue, value) { + return nil, fmt.Errorf("aggregate traversal parameter collision for %s", key) + } + + s.translation.Parameters[key] = value + } + + return sourceConstraints.Expression, nil +} + +func (s *Translator) aggregateNodeKindConstraint(alias pgsql.Identifier, kinds graph.Kinds) (pgsql.Expression, error) { + if len(kinds) == 0 { + return nil, nil + } + + kindIDs, err := s.kindMapper.MapKinds(kinds) + if err != nil { + return nil, err + } + + kindIDsLiteral, err := pgsql.AsLiteral(kindIDs) + if err != nil { + return nil, err + } + + return pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{alias, pgsql.ColumnKindIDs}, + pgsql.OperatorPGArrayLHSContainsRHS, + kindIDsLiteral, + ), nil +} + +func (s *Translator) aggregateEdgeKindConstraint(alias pgsql.Identifier, kinds graph.Kinds) (pgsql.Expression, error) { + if len(kinds) == 0 { + return nil, nil + } + + kindIDs, err := s.kindMapper.MapKinds(kinds) + if err != nil { + return nil, err + } + + return pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{alias, pgsql.ColumnKindID}, + pgsql.OperatorEquals, + pgsql.NewAnyExpressionHinted(pgsql.NewLiteral(kindIDs, pgsql.Int2Array)), + ), nil +} + +func aggregateTraversalColumns(direction graph.Direction) (pgsql.Identifier, pgsql.Identifier) { + switch direction { + case graph.DirectionInbound: + return pgsql.ColumnEndID, pgsql.ColumnStartID + default: + return pgsql.ColumnStartID, pgsql.ColumnEndID + } +} + +func aggregateNodeComposite(alias pgsql.Identifier) pgsql.CompositeValue { + return pgsql.CompositeValue{ + Values: []pgsql.Expression{ + pgsql.CompoundIdentifier{alias, pgsql.ColumnID}, + pgsql.CompoundIdentifier{alias, pgsql.ColumnKindIDs}, + pgsql.CompoundIdentifier{alias, pgsql.ColumnProperties}, + }, + DataType: pgsql.NodeComposite, + } +} diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index ced6e779..90a1183b 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -579,7 +579,7 @@ RETURN p require.Contains(t, normalizedQuery, "join edge e0 on e0.end_id = s1_seed.root_id") } -func TestOptimizerSafetyTraversalDirectionUsesBoundLeftExpansionTerminalConstraint(t *testing.T) { +func TestOptimizerSafetyAggregateTraversalCountUsesIDOnlySourceAnchoredShape(t *testing.T) { t.Parallel() translation := optimizerSafetyTranslation(t, ` @@ -594,11 +594,42 @@ LIMIT 100 formattedQuery, err := Translated(translation) require.NoError(t, err) normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + lowerQuery := strings.ToLower(normalizedQuery) + + requireNoPlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, lowerQuery, "with recursive candidate_sources(root_id)") + require.Contains(t, lowerQuery, "traversal(root_id, next_id, depth, path)") + require.Contains(t, lowerQuery, "terminal_nodes(id) as materialized") + require.Contains(t, lowerQuery, "terminal_hits(root_id)") + require.Contains(t, lowerQuery, "ranked(root_id, admincount)") + require.Contains(t, lowerQuery, "join edge e on e.start_id = candidate_sources.root_id") + require.Contains(t, lowerQuery, "e.start_id = traversal.next_id") + require.Contains(t, lowerQuery, "e.id != all (traversal.path)") + require.Contains(t, lowerQuery, "join terminal_nodes on terminal_nodes.id = traversal.next_id") + require.Contains(t, lowerQuery, "count(*)::int8 as admincount") + require.Contains(t, lowerQuery, "group by terminal_hits.root_id") + require.Contains(t, lowerQuery, "from ranked join node source_node on source_node.id = ranked.root_id") + require.NotContains(t, lowerQuery, "group by (") + require.NotContains(t, lowerQuery, "::nodecomposite as n0 from") +} + +func TestOptimizerSafetyAggregateTraversalCountSkipsObservedTerminal(t *testing.T) { + t.Parallel() - requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") - requireOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") - require.Contains(t, normalizedQuery, "join edge e0 on e0.end_id = s3_seed.root_id") - require.Contains(t, normalizedQuery, "(s1.n0).id = s3.next_id") + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true AND u.enabled = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, c, COUNT(c) AS adminCount +RETURN u, c +ORDER BY adminCount DESC +LIMIT 100 + `) + + requireNoPlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + requireNoOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) } func TestOptimizerSafetyShortestPathStrategyUsesPlannedBidirectionalSearch(t *testing.T) { diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 5836b5f0..a766d881 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -695,6 +695,7 @@ func plannedLoweringCounts(plan optimize.LoweringPlan) []SkippedLowering { {Name: optimize.LoweringExpansionSuffixPushdown, Count: len(plan.ExpansionSuffixPushdown)}, {Name: optimize.LoweringPredicatePlacement, Count: len(plan.PredicatePlacement) + len(plan.PatternPredicate)}, {Name: optimize.LoweringCountStoreFastPath, Count: len(plan.CountStoreFastPath)}, + {Name: optimize.LoweringAggregateTraversalCount, Count: len(plan.AggregateTraversalCount)}, } } @@ -702,6 +703,9 @@ func skippedLoweringReason(name string, applied map[string]int) string { if applied[optimize.LoweringCountStoreFastPath] > 0 && name != optimize.LoweringCountStoreFastPath { return "superseded by CountStoreFastPath" } + if applied[optimize.LoweringAggregateTraversalCount] > 0 && name != optimize.LoweringAggregateTraversalCount { + return "superseded by AggregateTraversalCount" + } switch name { case optimize.LoweringPredicatePlacement: @@ -739,6 +743,13 @@ func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper return translator.translation, nil } + if translated, err := translator.translateAggregateTraversalCount(optimizedPlan.Query, optimizedPlan.LoweringPlan); err != nil { + return Result{}, err + } else if translated { + translator.recordSkippedLowerings() + return translator.translation, nil + } + if err := walk.Cypher(optimizedPlan.Query, translator); err != nil { return Result{}, err } From 8980a419163b9f056d26bafb4e1dd1478e3a8e7a Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 13:52:46 -0700 Subject: [PATCH 079/114] test(integration): add live aggregate traversal plan guard --- .../pgsql_aggregate_traversal_plan_test.go | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 integration/pgsql_aggregate_traversal_plan_test.go diff --git a/integration/pgsql_aggregate_traversal_plan_test.go b/integration/pgsql_aggregate_traversal_plan_test.go new file mode 100644 index 00000000..c41febbb --- /dev/null +++ b/integration/pgsql_aggregate_traversal_plan_test.go @@ -0,0 +1,304 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build manual_integration + +package integration + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/pgsql/optimize" + "github.com/specterops/dawgs/cypher/models/pgsql/translate" + "github.com/specterops/dawgs/drivers/pg" + "github.com/specterops/dawgs/graph" +) + +const liveAggregateTraversalCypher = ` +MATCH (u:User) +WHERE u.hasspn = true + AND u.enabled = true + AND NOT u.objectid ENDS WITH '-502' + AND NOT COALESCE(u.gmsa, false) = true + AND NOT COALESCE(u.msa, false) = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 +` + +type livePGKindMapper struct { + pool *pgxpool.Pool +} + +func (s livePGKindMapper) MapKinds(ctx context.Context, kinds graph.Kinds) ([]int16, error) { + ids := make([]int16, 0, len(kinds)) + + for _, kind := range kinds { + id, err := liveKindID(ctx, s.pool, kind.String()) + if err != nil { + return nil, err + } + + ids = append(ids, id) + } + + return ids, nil +} + +func (s livePGKindMapper) AssertKinds(ctx context.Context, kinds graph.Kinds) ([]int16, error) { + return s.MapKinds(ctx, kinds) +} + +func TestPostgreSQLLiveAggregateTraversalCountPlanShape(t *testing.T) { + connStr := os.Getenv("CONNECTION_STRING") + if connStr == "" { + t.Skip("CONNECTION_STRING env var is not set") + } + + driver, err := driverFromConnStr(connStr) + if err != nil { + t.Fatalf("failed to detect driver: %v", err) + } + if driver != pg.DriverName { + t.Skipf("CONNECTION_STRING is not a PostgreSQL connection string") + } + + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) + defer cancel() + + poolCfg, err := pgxpool.ParseConfig(connStr) + if err != nil { + t.Fatalf("failed to parse PG connection string: %v", err) + } + pool, err := pgxpool.NewWithConfig(ctx, poolCfg) + if err != nil { + t.Fatalf("failed to connect to PostgreSQL: %v", err) + } + defer pool.Close() + + liveStats, ok := liveAggregateTraversalStats(ctx, t, pool) + if !ok { + return + } + if liveStats.candidateUsers == 0 || liveStats.computers < 1000 || liveStats.adminEdges < 10000 { + t.Skipf( + "connected PostgreSQL database does not look like the live aggregate traversal dataset: %+v", + liveStats, + ) + } + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), liveAggregateTraversalCypher) + if err != nil { + t.Fatalf("failed to parse live aggregate traversal query: %v", err) + } + + translation, err := translate.Translate(ctx, regularQuery, livePGKindMapper{pool: pool}, nil, translate.DefaultGraphID) + if err != nil { + t.Fatalf("failed to translate live aggregate traversal query: %v", err) + } + requireLoweringDecision(t, translation.Optimization.PlannedLowerings, optimize.LoweringAggregateTraversalCount) + requireLoweringDecision(t, translation.Optimization.Lowerings, optimize.LoweringAggregateTraversalCount) + + sqlQuery, err := translate.Translated(translation) + if err != nil { + t.Fatalf("failed to render live aggregate traversal SQL: %v", err) + } + normalizedSQL := strings.Join(strings.Fields(strings.ToLower(sqlQuery)), " ") + + for _, expected := range []string{ + "with recursive candidate_sources(root_id)", + "traversal(root_id, next_id, depth, path)", + "terminal_nodes(id) as materialized", + "terminal_hits(root_id)", + "ranked(root_id, admincount)", + "join edge e on e.start_id = candidate_sources.root_id", + "e.start_id = traversal.next_id", + "group by terminal_hits.root_id", + "from ranked join node source_node on source_node.id = ranked.root_id", + } { + if !strings.Contains(normalizedSQL, expected) { + t.Fatalf("expected translated SQL to contain %q, got:\n%s", expected, sqlQuery) + } + } + if strings.Contains(normalizedSQL, "group by (") { + t.Fatalf("expected aggregate traversal SQL to avoid grouping by composites, got:\n%s", sqlQuery) + } + + plan := explainAggregateTraversalPlan(ctx, t, pool, sqlQuery, translation.Parameters) + for _, expected := range []string{ + "CTE traversal", + "Recursive Union", + "start_id = source_node", + "start_id = traversal", + "Group Key: traversal.root_id", + "Hash Cond: (traversal.next_id = terminal_nodes.id)", + } { + if !strings.Contains(plan, expected) { + t.Fatalf("expected PostgreSQL plan to contain %q, got:\n%s", expected, plan) + } + } + for _, unexpected := range []string{ + "end_id = source_node", + "end_id = traversal", + "Group Key: (", + } { + if strings.Contains(plan, unexpected) { + t.Fatalf("expected PostgreSQL plan to avoid %q, got:\n%s", unexpected, plan) + } + } + + limitIndex := strings.Index(plan, "-> Limit") + sourceMaterializationIndex := strings.LastIndex(plan, "Index Scan using node_") + if limitIndex < 0 || sourceMaterializationIndex < 0 || sourceMaterializationIndex < limitIndex { + t.Fatalf("expected source node materialization after top-N limiting, got:\n%s", plan) + } +} + +type liveAggregateStats struct { + candidateUsers int64 + computers int64 + adminEdges int64 +} + +func liveAggregateTraversalStats(ctx context.Context, t *testing.T, pool *pgxpool.Pool) (liveAggregateStats, bool) { + t.Helper() + + userKindID, err := liveKindID(ctx, pool, "User") + if err != nil { + t.Skipf("connected PostgreSQL database has no User kind: %v", err) + return liveAggregateStats{}, false + } + computerKindID, err := liveKindID(ctx, pool, "Computer") + if err != nil { + t.Skipf("connected PostgreSQL database has no Computer kind: %v", err) + return liveAggregateStats{}, false + } + memberOfKindID, err := liveKindID(ctx, pool, "MemberOf") + if err != nil { + t.Skipf("connected PostgreSQL database has no MemberOf kind: %v", err) + return liveAggregateStats{}, false + } + adminToKindID, err := liveKindID(ctx, pool, "AdminTo") + if err != nil { + t.Skipf("connected PostgreSQL database has no AdminTo kind: %v", err) + return liveAggregateStats{}, false + } + + var stats liveAggregateStats + if err := pool.QueryRow(ctx, ` + select + ( + select count(*) + from node n + where n.kind_ids operator (pg_catalog.@>) array[$1::int2] + and (n.properties -> 'hasspn') = to_jsonb(true) + and (n.properties -> 'enabled') = to_jsonb(true) + and coalesce(n.properties ->> 'objectid', '') not like '%-502' + and not coalesce((n.properties ->> 'gmsa')::bool, false) + and not coalesce((n.properties ->> 'msa')::bool, false) + ), + ( + select count(*) + from node n + where n.kind_ids operator (pg_catalog.@>) array[$2::int2] + ), + ( + select count(*) + from edge e + where e.kind_id = any(array[$3::int2, $4::int2]) + ) + `, userKindID, computerKindID, memberOfKindID, adminToKindID).Scan( + &stats.candidateUsers, + &stats.computers, + &stats.adminEdges, + ); err != nil { + t.Fatalf("failed to inspect live aggregate traversal dataset: %v", err) + } + + return stats, true +} + +func liveKindID(ctx context.Context, pool *pgxpool.Pool, name string) (int16, error) { + var id int16 + if err := pool.QueryRow(ctx, `select id from kind where name = $1`, name).Scan(&id); err != nil { + return 0, fmt.Errorf("map kind %q: %w", name, err) + } + + return id, nil +} + +func explainAggregateTraversalPlan(ctx context.Context, t *testing.T, pool *pgxpool.Pool, sqlQuery string, params map[string]any) string { + t.Helper() + + tx, err := pool.Begin(ctx) + if err != nil { + t.Fatalf("failed to begin PostgreSQL explain transaction: %v", err) + } + defer func() { + _ = tx.Rollback(context.Background()) + }() + + if _, err := tx.Exec(ctx, `set local statement_timeout = '30s'`); err != nil { + t.Fatalf("failed to set PostgreSQL statement timeout: %v", err) + } + + args := []any{} + if len(params) > 0 { + args = append(args, pgx.NamedArgs(params)) + } + + rows, err := tx.Query(ctx, "explain (analyze, buffers, timing off, summary off) "+sqlQuery, args...) + if err != nil { + t.Fatalf("failed to explain live aggregate traversal query: %v", err) + } + defer rows.Close() + + var planLines []string + for rows.Next() { + var line string + if err := rows.Scan(&line); err != nil { + t.Fatalf("failed to scan live aggregate traversal plan line: %v", err) + } + planLines = append(planLines, line) + } + if err := rows.Err(); err != nil { + t.Fatalf("failed while reading live aggregate traversal plan: %v", err) + } + + return strings.Join(planLines, "\n") +} + +func requireLoweringDecision(t *testing.T, lowerings []optimize.LoweringDecision, name string) { + t.Helper() + + for _, lowering := range lowerings { + if lowering.Name == name { + return + } + } + + t.Fatalf("expected lowering %s in %v", name, lowerings) +} From 33af724eb044622010313fff392d9289af3c7a6b Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 13:54:30 -0700 Subject: [PATCH 080/114] feat(pgsql): report skipped kind-only traversal flips --- cypher/models/pgsql/optimize/lowering_plan.go | 18 ++++++++++------- .../models/pgsql/optimize/optimizer_test.go | 11 +++++++++- .../pgsql/translate/optimizer_safety_test.go | 19 +++++++++++++++++- cypher/models/pgsql/translate/translator.go | 20 +++++++++++++++++-- 4 files changed, 57 insertions(+), 11 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 4880473c..1fe71afe 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -14,9 +14,10 @@ type sourceTraversalStep struct { } const ( - traversalDirectionReasonRightBound = "right_bound" - traversalDirectionReasonRightConstrained = "right_constrained" - traversalDirectionReasonRightPredicate = "right_predicate" + traversalDirectionReasonRightBound = "right_bound" + traversalDirectionReasonRightConstrained = "right_constrained" + traversalDirectionReasonRightPredicate = "right_predicate" + traversalDirectionReasonTerminalKindOnlyEstimateWide = "terminal kind-only estimate too broad" shortestPathStrategyReasonBoundEndpointPairs = "bound_endpoint_pairs" shortestPathStrategyReasonEndpointPredicates = "endpoint_predicates" @@ -562,10 +563,6 @@ func boundLeftExpansionDirectionDecisionForStep( return TraversalDirectionDecision{}, false } - if step.RightNode.Properties == nil && !rightHasAttachedPredicate { - return TraversalDirectionDecision{}, false - } - leftSymbol := variableSymbol(step.LeftNode.Variable) rightSymbol := variableSymbol(step.RightNode.Variable) if leftSymbol == "" || leftSymbol == rightSymbol { @@ -582,6 +579,13 @@ func boundLeftExpansionDirectionDecisionForStep( } } + if step.RightNode.Properties == nil && !rightHasAttachedPredicate { + return TraversalDirectionDecision{ + Target: target, + Reason: traversalDirectionReasonTerminalKindOnlyEstimateWide, + }, true + } + return TraversalDirectionDecision{ Target: target, Flip: true, diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index a61cb83b..c257ac8a 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -745,7 +745,16 @@ func TestLoweringPlanReportsAggregateTraversalCountForBoundExpansionCount(t *tes plan, err := Optimize(regularQuery) require.NoError(t, err) - require.Empty(t, plan.LoweringPlan.TraversalDirection) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + Reason: traversalDirectionReasonTerminalKindOnlyEstimateWide, + }}, plan.LoweringPlan.TraversalDirection) require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringAggregateTraversalCount}) require.Equal(t, []AggregateTraversalCountDecision{{ QueryPartIndex: 0, diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 90a1183b..51fdb236 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -596,7 +596,9 @@ LIMIT 100 normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") lowerQuery := strings.ToLower(normalizedQuery) - requireNoPlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireNoOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireSkippedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection", "superseded by AggregateTraversalCount") requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) require.Contains(t, lowerQuery, "with recursive candidate_sources(root_id)") @@ -615,6 +617,21 @@ LIMIT 100 require.NotContains(t, lowerQuery, "::nodecomposite as n0 from") } +func TestOptimizerSafetyTraversalDirectionReportsKindOnlyTerminalSkip(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +RETURN count(c) + `) + + requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireNoOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireSkippedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection", "terminal kind-only estimate too broad") +} + func TestOptimizerSafetyAggregateTraversalCountSkipsObservedTerminal(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index a766d881..3ba3f153 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -677,7 +677,7 @@ func (s *Translator) recordSkippedLowerings() { s.translation.Optimization.SkippedLowerings = append(s.translation.Optimization.SkippedLowerings, SkippedLowering{ Name: planned.Name, - Reason: skippedLoweringReason(planned.Name, applied), + Reason: skippedLoweringReason(planned.Name, applied, *s.translation.Optimization.LoweringPlan), Count: skippedCount, }) } @@ -699,7 +699,7 @@ func plannedLoweringCounts(plan optimize.LoweringPlan) []SkippedLowering { } } -func skippedLoweringReason(name string, applied map[string]int) string { +func skippedLoweringReason(name string, applied map[string]int, plan optimize.LoweringPlan) string { if applied[optimize.LoweringCountStoreFastPath] > 0 && name != optimize.LoweringCountStoreFastPath { return "superseded by CountStoreFastPath" } @@ -710,9 +710,25 @@ func skippedLoweringReason(name string, applied map[string]int) string { switch name { case optimize.LoweringPredicatePlacement: return "planned predicate placements were not consumed by this translation shape" + case optimize.LoweringTraversalDirection: + if reason := skippedTraversalDirectionReason(plan); reason != "" { + return reason + } default: return "planned lowering did not change the emitted SQL" } + + return "planned lowering did not change the emitted SQL" +} + +func skippedTraversalDirectionReason(plan optimize.LoweringPlan) string { + for _, decision := range plan.TraversalDirection { + if !decision.Flip && decision.Reason != "" { + return decision.Reason + } + } + + return "" } func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any, graphID int32) (Result, error) { From 59d4e9f44ffd7c736757899474647939ed492e81 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 13:55:13 -0700 Subject: [PATCH 081/114] fix(pgsql): widen aggregate traversal count matching --- cypher/models/pgsql/optimize/lowering_plan.go | 11 +++++++++- .../models/pgsql/optimize/optimizer_test.go | 20 +++++++++++++++++ .../pgsql/translate/optimizer_safety_test.go | 22 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 1fe71afe..921d56b1 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -1233,13 +1233,22 @@ func projectionItemCountAlias(expression cypher.Expression, terminalSymbol strin return "", false } - if symbol, ok := expressionVariableSymbol(function.Arguments[0]); !ok || symbol != terminalSymbol { + if !aggregateTraversalCountArgumentMatches(function.Arguments[0], terminalSymbol) { return "", false } return projectionItem.Alias.Symbol, true } +func aggregateTraversalCountArgumentMatches(expression cypher.Expression, terminalSymbol string) bool { + if symbol, ok := expressionVariableSymbol(expression); ok { + return symbol == terminalSymbol + } + + rangeQuantifier, ok := expression.(*cypher.RangeQuantifier) + return ok && rangeQuantifier != nil && rangeQuantifier.Value == cypher.TokenLiteralAsterisk +} + func literalInt64(expression cypher.Expression) (int64, bool) { literal, ok := expression.(*cypher.Literal) if !ok || literal == nil || literal.Null { diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index c257ac8a..3a60034b 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -771,6 +771,26 @@ func TestLoweringPlanReportsAggregateTraversalCountForBoundExpansionCount(t *tes }}, plan.LoweringPlan.AggregateTraversalCount) } +func TestLoweringPlanReportsAggregateTraversalCountForRowCount(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true AND u.enabled = true + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) + WITH DISTINCT u, COUNT(*) AS adminCount + RETURN u + ORDER BY adminCount DESC + LIMIT 100 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringAggregateTraversalCount}) + require.Equal(t, "adminCount", plan.LoweringPlan.AggregateTraversalCount[0].CountAlias) +} + func TestLoweringPlanSkipsSuffixPushdownAfterRightEndpointPredicateDirectionFlip(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 51fdb236..c3c91e50 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -632,6 +632,28 @@ RETURN count(c) requireSkippedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection", "terminal kind-only estimate too broad") } +func TestOptimizerSafetyAggregateTraversalCountAcceptsRowCount(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true AND u.enabled = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, COUNT(*) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(strings.ToLower(formattedQuery)), " ") + + requirePlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, normalizedQuery, "count(*)::int8 as admincount") + require.Contains(t, normalizedQuery, "group by terminal_hits.root_id") +} + func TestOptimizerSafetyAggregateTraversalCountSkipsObservedTerminal(t *testing.T) { t.Parallel() From 20ace1507346425cb6d6fcac65373bea69e00e8c Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 13:58:58 -0700 Subject: [PATCH 082/114] fix(pgsql): respect selective bound traversal sources --- cypher/models/pgsql/optimize/lowering_plan.go | 169 ++++++++++++++++++ .../models/pgsql/optimize/optimizer_test.go | 25 +++ .../pgsql/translate/optimizer_safety_test.go | 15 ++ 3 files changed, 209 insertions(+) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 921d56b1..4173ac7f 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -13,11 +13,14 @@ type sourceTraversalStep struct { RightNode *cypher.NodePattern } +type boundSourceSelectivity int + const ( traversalDirectionReasonRightBound = "right_bound" traversalDirectionReasonRightConstrained = "right_constrained" traversalDirectionReasonRightPredicate = "right_predicate" traversalDirectionReasonTerminalKindOnlyEstimateWide = "terminal kind-only estimate too broad" + traversalDirectionReasonBoundSourceSelective = "bound source estimate selective" shortestPathStrategyReasonBoundEndpointPairs = "bound_endpoint_pairs" shortestPathStrategyReasonEndpointPredicates = "endpoint_predicates" @@ -26,6 +29,12 @@ const ( shortestPathFilterReasonEndpointPairPredicates = "endpoint_pair_predicates" ) +const ( + boundSourceSelectivityNone boundSourceSelectivity = iota + boundSourceSelectivityPredicate + boundSourceSelectivityUnique +) + func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []PredicateAttachment) (LoweringPlan, error) { if query == nil || query.SingleQuery == nil { return LoweringPlan{}, nil @@ -328,6 +337,7 @@ func declaredSymbolsBeforeStepEndpoints(initial map[string]struct{}, steps []sou func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, predicateConstrainedSymbols map[string]struct{}) { declaredSymbols := map[string]struct{}{} + declaredSourceSelectivity := map[string]boundSourceSelectivity{} for clauseIndex, readingClause := range readingClauses { if readingClause == nil || readingClause.Match == nil { @@ -368,6 +378,7 @@ func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, r step, declaredEndpoints[stepIndex], referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.RightNode.Variable)), + declaredSourceSelectivity[variableSymbol(step.LeftNode.Variable)], ); shouldFlip { plan.TraversalDirection = append(plan.TraversalDirection, decision) } @@ -376,6 +387,7 @@ func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, r declarePatternSymbols(declaredSymbols, patternPart) } + declareSelectiveMatchSymbols(declaredSourceSelectivity, match) declareWhereSymbols(declaredSymbols, match) } } @@ -396,6 +408,153 @@ func bindingPredicateSymbols(predicateAttachments []PredicateAttachment, queryPa return symbols } +func declareSelectiveMatchSymbols(symbols map[string]boundSourceSelectivity, match *cypher.Match) { + if match == nil { + return + } + + for _, patternPart := range match.Pattern { + for _, nodePattern := range nodePatternsForPattern(patternPart) { + if nodePattern == nil { + continue + } + + symbol := variableSymbol(nodePattern.Variable) + if symbol == "" { + continue + } + + mergeBoundSourceSelectivity(symbols, symbol, propertyConstraintSelectivity(nodePattern.Properties)) + } + } + + if match.Where == nil { + return + } + + for _, expression := range match.Where.Expressions { + for _, term := range cypherConjunctionTerms(expression) { + if symbol, selectivity, ok := propertyPredicateSelectivity(term); ok { + mergeBoundSourceSelectivity(symbols, symbol, selectivity) + } + } + } +} + +func nodePatternsForPattern(patternPart *cypher.PatternPart) []*cypher.NodePattern { + if patternPart == nil { + return nil + } + + nodePatterns := make([]*cypher.NodePattern, 0, len(patternPart.PatternElements)) + for _, element := range patternPart.PatternElements { + if nodePattern, ok := element.AsNodePattern(); ok { + nodePatterns = append(nodePatterns, nodePattern) + } + } + + return nodePatterns +} + +func mergeBoundSourceSelectivity(symbols map[string]boundSourceSelectivity, symbol string, selectivity boundSourceSelectivity) { + if selectivity > symbols[symbol] { + symbols[symbol] = selectivity + } +} + +func propertyPredicateSelectivity(expression cypher.Expression) (string, boundSourceSelectivity, bool) { + comparison, isComparison := expression.(*cypher.Comparison) + if !isComparison || len(comparison.Partials) != 1 { + return "", boundSourceSelectivityNone, false + } + + partial := comparison.Partials[0] + if partial.Operator != cypher.OperatorEquals { + return "", boundSourceSelectivityNone, false + } + + if symbol, property, ok := propertyLookupSymbol(comparison.Left); ok && !expressionReferencesAnySource(partial.Right) { + return symbol, propertySelectivity(property, partial.Right), true + } + + if symbol, property, ok := propertyLookupSymbol(partial.Right); ok && !expressionReferencesAnySource(comparison.Left) { + return symbol, propertySelectivity(property, comparison.Left), true + } + + return "", boundSourceSelectivityNone, false +} + +func propertyConstraintSelectivity(expression cypher.Expression) boundSourceSelectivity { + properties, ok := expression.(*cypher.Properties) + if !ok || properties == nil || properties.Parameter != nil { + return boundSourceSelectivityNone + } + + highest := boundSourceSelectivityNone + for property, value := range properties.Map { + if selectivity := propertySelectivity(property, value); selectivity > highest { + highest = selectivity + } + } + + return highest +} + +func propertySelectivity(property string, value cypher.Expression) boundSourceSelectivity { + if strings.EqualFold(property, "objectid") && expressionIsConstant(value) { + return boundSourceSelectivityUnique + } + + if expressionIsStringLikeConstant(value) { + return boundSourceSelectivityPredicate + } + + return boundSourceSelectivityNone +} + +func expressionIsConstant(expression cypher.Expression) bool { + switch expression.(type) { + case *cypher.Literal, *cypher.Parameter: + return true + default: + return false + } +} + +func expressionIsStringLikeConstant(expression cypher.Expression) bool { + switch typedExpression := expression.(type) { + case *cypher.Literal: + if typedExpression == nil || typedExpression.Null { + return false + } + + _, isString := typedExpression.Value.(string) + return isString + case *cypher.Parameter: + return typedExpression != nil + default: + return false + } +} + +func propertyLookupSymbol(expression cypher.Expression) (string, string, bool) { + propertyLookup, isPropertyLookup := expression.(*cypher.PropertyLookup) + if !isPropertyLookup || propertyLookup == nil { + return "", "", false + } + + variable, isVariable := propertyLookup.Atom.(*cypher.Variable) + if !isVariable || variable == nil || variable.Symbol == "" || propertyLookup.Symbol == "" { + return "", "", false + } + + return variable.Symbol, propertyLookup.Symbol, true +} + +func nodePatternHasUniquePropertyConstraint(nodePattern *cypher.NodePattern) bool { + return nodePattern != nil && propertyConstraintSelectivity(nodePattern.Properties) == boundSourceSelectivityUnique +} + func shortestPathSearchPredicateSymbols(readingClauses []*cypher.ReadingClause) map[string]struct{} { symbols := map[string]struct{}{} @@ -547,6 +706,7 @@ func boundLeftExpansionDirectionDecisionForStep( step sourceTraversalStep, declaredEndpoints declaredStepEndpoints, rightHasAttachedPredicate bool, + leftSourceSelectivity boundSourceSelectivity, ) (TraversalDirectionDecision, bool) { if patternPart == nil || patternPart.Variable != nil || @@ -579,6 +739,15 @@ func boundLeftExpansionDirectionDecisionForStep( } } + if leftSourceSelectivity == boundSourceSelectivityUnique && + !nodePatternHasUniquePropertyConstraint(step.RightNode) && + !rightHasAttachedPredicate { + return TraversalDirectionDecision{ + Target: target, + Reason: traversalDirectionReasonBoundSourceSelective, + }, true + } + if step.RightNode.Properties == nil && !rightHasAttachedPredicate { return TraversalDirectionDecision{ Target: target, diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 3a60034b..0dbdd2b2 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -729,6 +729,31 @@ func TestLoweringPlanReportsTraversalDirectionForBoundLeftExpansionToConstrained }}, plan.LoweringPlan.TraversalDirection) } +func TestLoweringPlanSkipsBoundLeftDirectionForSelectiveSource(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.objectid = 'S-1-5-21-1-1100' + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {name: 'target'}) + RETURN c + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + PatternIndex: 0, + StepIndex: 0, + }, + Reason: traversalDirectionReasonBoundSourceSelective, + }}, plan.LoweringPlan.TraversalDirection) +} + func TestLoweringPlanReportsAggregateTraversalCountForBoundExpansionCount(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index c3c91e50..1fd6bdd7 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -632,6 +632,21 @@ RETURN count(c) requireSkippedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection", "terminal kind-only estimate too broad") } +func TestOptimizerSafetyTraversalDirectionReportsSelectiveSourceSkip(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.objectid = 'S-1-5-21-1-1100' +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {name: 'target'}) +RETURN c + `) + + requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireNoOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireSkippedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection", "bound source estimate selective") +} + func TestOptimizerSafetyAggregateTraversalCountAcceptsRowCount(t *testing.T) { t.Parallel() From 668a975edec2e14c575d050e0f72feae4e72940d Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 14:17:44 -0700 Subject: [PATCH 083/114] test(integration): expand aggregate traversal baseline coverage --- .../pgsql/translate/optimizer_safety_test.go | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 1fd6bdd7..df707720 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -669,6 +669,122 @@ LIMIT 100 require.Contains(t, normalizedQuery, "group by terminal_hits.root_id") } +func TestOptimizerSafetyAggregateTraversalCountHonorsExplicitDepthBounds(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[:MemberOf|AdminTo*2..4]->(c:Computer) +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(strings.ToLower(formattedQuery)), " ") + + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, normalizedQuery, "where traversal.depth < 4") + require.Contains(t, normalizedQuery, "where traversal.depth >= 2") +} + +func TestOptimizerSafetyAggregateTraversalCountSupportsInboundSourceAnchoring(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)<-[:MemberOf|AdminTo*1..]-(c:Computer) +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(strings.ToLower(formattedQuery)), " ") + + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, normalizedQuery, "join edge e on e.end_id = candidate_sources.root_id") + require.Contains(t, normalizedQuery, "e.end_id = traversal.next_id") +} + +func TestOptimizerSafetyAggregateTraversalCountSkipsUnsafeWideningCandidates(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + query string + }{{ + name: "distinct terminal count", + query: ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, COUNT(DISTINCT c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `, + }, { + name: "optional traversal", + query: ` +MATCH (u:User) +WHERE u.hasspn = true +OPTIONAL MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `, + }, { + name: "path binding observed", + query: ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH p = (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, p, COUNT(c) AS adminCount +RETURN u, p +ORDER BY adminCount DESC +LIMIT 100 + `, + }, { + name: "relationship binding observed", + query: ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[r:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, r, COUNT(c) AS adminCount +RETURN u, r +ORDER BY adminCount DESC +LIMIT 100 + `, + }, { + name: "post aggregation filter", + query: ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, COUNT(c) AS adminCount +WHERE adminCount > 1 +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `, + }} + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + translation := optimizerSafetyTranslation(t, testCase.query) + + requireNoPlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + requireNoOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + }) + } +} + func TestOptimizerSafetyAggregateTraversalCountSkipsObservedTerminal(t *testing.T) { t.Parallel() From a3027ea6b85cfb726cd637dcb3d091dea587738e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 14:19:02 -0700 Subject: [PATCH 084/114] fix(pgsql): widen aggregate traversal final projections --- cypher/models/pgsql/optimize/lowering.go | 3 + cypher/models/pgsql/optimize/lowering_plan.go | 93 ++++++++++++++++--- .../models/pgsql/optimize/optimizer_test.go | 25 +++++ .../translate/aggregate_traversal_count.go | 20 ++-- .../pgsql/translate/optimizer_safety_test.go | 22 +++++ 5 files changed, 145 insertions(+), 18 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index efe65c45..269405cb 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -178,6 +178,9 @@ type AggregateTraversalCountShape struct { SourceSymbol string TerminalSymbol string CountAlias string + ReturnSourceAlias string + ReturnCountAlias string + ReturnCount bool Limit int64 SourceMatch *cypher.Match SourceKinds graph.Kinds diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 4173ac7f..82318d76 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -1214,7 +1214,7 @@ func AggregateTraversalCountShapeForQuery(query *cypher.RegularQuery) (Aggregate return AggregateTraversalCountShape{}, false } - limit, ok := aggregateTraversalFinalProjection(multiPartQuery.SinglePartQuery, sourceSymbol, countAlias) + finalProjection, ok := aggregateTraversalFinalProjection(multiPartQuery.SinglePartQuery, sourceSymbol, countAlias) if !ok { return AggregateTraversalCountShape{}, false } @@ -1229,7 +1229,10 @@ func AggregateTraversalCountShapeForQuery(query *cypher.RegularQuery) (Aggregate SourceSymbol: sourceSymbol, TerminalSymbol: terminalSymbol, CountAlias: countAlias, - Limit: limit, + ReturnSourceAlias: finalProjection.SourceAlias, + ReturnCountAlias: finalProjection.CountAlias, + ReturnCount: finalProjection.ReturnCount, + Limit: finalProjection.Limit, SourceMatch: sourceMatch, SourceKinds: sourceNode.Kinds, TerminalKinds: terminalNode.Kinds, @@ -1323,29 +1326,72 @@ func aggregateTraversalWithProjection(projection *cypher.Projection, sourceSymbo return countAlias, true } -func aggregateTraversalFinalProjection(queryPart *cypher.SinglePartQuery, sourceSymbol, countAlias string) (int64, bool) { +type aggregateTraversalFinalProjectionShape struct { + SourceAlias string + CountAlias string + ReturnCount bool + Limit int64 +} + +func aggregateTraversalFinalProjection(queryPart *cypher.SinglePartQuery, sourceSymbol, countAlias string) (aggregateTraversalFinalProjectionShape, bool) { if queryPart == nil || len(queryPart.ReadingClauses) > 0 || len(queryPart.UpdatingClauses) > 0 || queryPart.Return == nil || queryPart.Return.Projection == nil { - return 0, false + return aggregateTraversalFinalProjectionShape{}, false } projection := queryPart.Return.Projection - if projection.Distinct || projection.All || projection.Skip != nil || projection.Order == nil || projection.Limit == nil || len(projection.Items) != 1 { - return 0, false + if projection.Distinct || projection.All || projection.Skip != nil || projection.Order == nil || projection.Limit == nil || len(projection.Items) < 1 || len(projection.Items) > 2 { + return aggregateTraversalFinalProjectionShape{}, false } - if symbol, ok := projectionItemVariableSymbol(projection.Items[0]); !ok || symbol != sourceSymbol { - return 0, false + finalProjection := aggregateTraversalFinalProjectionShape{ + SourceAlias: sourceSymbol, + CountAlias: countAlias, + } + + sourceSeen := false + countSeen := false + for _, item := range projection.Items { + symbol, alias, ok := projectionItemVariableSymbolAndAlias(item) + if !ok { + return aggregateTraversalFinalProjectionShape{}, false + } + + switch symbol { + case sourceSymbol: + if sourceSeen { + return aggregateTraversalFinalProjectionShape{}, false + } + sourceSeen = true + finalProjection.SourceAlias = alias + case countAlias: + if countSeen { + return aggregateTraversalFinalProjectionShape{}, false + } + countSeen = true + finalProjection.ReturnCount = true + finalProjection.CountAlias = alias + default: + return aggregateTraversalFinalProjectionShape{}, false + } + } + if !sourceSeen { + return aggregateTraversalFinalProjectionShape{}, false } if len(projection.Order.Items) != 1 || projection.Order.Items[0] == nil || projection.Order.Items[0].Ascending { - return 0, false + return aggregateTraversalFinalProjectionShape{}, false } - if orderSymbol, ok := expressionVariableSymbol(projection.Order.Items[0].Expression); !ok || orderSymbol != countAlias { - return 0, false + if orderSymbol, ok := expressionVariableSymbol(projection.Order.Items[0].Expression); !ok || (orderSymbol != countAlias && orderSymbol != finalProjection.CountAlias) { + return aggregateTraversalFinalProjectionShape{}, false } - return literalInt64(projection.Limit.Value) + limit, ok := literalInt64(projection.Limit.Value) + if !ok { + return aggregateTraversalFinalProjectionShape{}, false + } + finalProjection.Limit = limit + return finalProjection, true } func aggregateTraversalDepthBounds(patternRange *cypher.PatternRange) (int64, int64, bool) { @@ -1381,6 +1427,29 @@ func projectionItemVariableSymbol(expression cypher.Expression) (string, bool) { return expressionVariableSymbol(projectionItem.Expression) } +func projectionItemVariableSymbolAndAlias(expression cypher.Expression) (string, string, bool) { + projectionItem, ok := expression.(*cypher.ProjectionItem) + if !ok || projectionItem == nil { + return "", "", false + } + + symbol, ok := expressionVariableSymbol(projectionItem.Expression) + if !ok { + return "", "", false + } + + alias := symbol + if projectionItem.Alias != nil { + if projectionItem.Alias.Symbol == "" { + return "", "", false + } + + alias = projectionItem.Alias.Symbol + } + + return symbol, alias, true +} + func expressionVariableSymbol(expression cypher.Expression) (string, bool) { variable, ok := expression.(*cypher.Variable) if !ok || variable == nil || variable.Symbol == "" { diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 0dbdd2b2..2eab75a4 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -816,6 +816,31 @@ func TestLoweringPlanReportsAggregateTraversalCountForRowCount(t *testing.T) { require.Equal(t, "adminCount", plan.LoweringPlan.AggregateTraversalCount[0].CountAlias) } +func TestLoweringPlanReportsAggregateTraversalCountWhenReturningCountAlias(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) + WITH DISTINCT u, COUNT(c) AS adminCount + RETURN u AS user, adminCount AS privileges + ORDER BY privileges DESC + LIMIT 100 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringAggregateTraversalCount}) + + shape, ok := AggregateTraversalCountShapeForQuery(plan.Query) + require.True(t, ok) + require.Equal(t, "user", shape.ReturnSourceAlias) + require.True(t, shape.ReturnCount) + require.Equal(t, "privileges", shape.ReturnCountAlias) +} + func TestLoweringPlanSkipsSuffixPushdownAfterRightEndpointPredicateDirectionFlip(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/aggregate_traversal_count.go b/cypher/models/pgsql/translate/aggregate_traversal_count.go index e994b70a..a7e2e58c 100644 --- a/cypher/models/pgsql/translate/aggregate_traversal_count.go +++ b/cypher/models/pgsql/translate/aggregate_traversal_count.go @@ -70,6 +70,19 @@ func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraver return pgsql.Query{}, err } + projection := pgsql.Projection{ + pgsql.AliasedExpression{ + Expression: aggregateNodeComposite(aggregateSourceAlias), + Alias: pgsql.AsOptionalIdentifier(pgsql.Identifier(shape.ReturnSourceAlias)), + }, + } + if shape.ReturnCount { + projection = append(projection, pgsql.AliasedExpression{ + Expression: pgsql.CompoundIdentifier{aggregateRankedCTE, pgsql.Identifier(shape.CountAlias)}, + Alias: pgsql.AsOptionalIdentifier(pgsql.Identifier(shape.ReturnCountAlias)), + }) + } + return pgsql.Query{ CommonTableExpressions: &pgsql.With{ Recursive: true, @@ -82,12 +95,7 @@ func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraver }, }, Body: pgsql.Select{ - Projection: pgsql.Projection{ - pgsql.AliasedExpression{ - Expression: aggregateNodeComposite(aggregateSourceAlias), - Alias: pgsql.AsOptionalIdentifier(pgsql.Identifier(shape.SourceSymbol)), - }, - }, + Projection: projection, From: []pgsql.FromClause{{ Source: pgsql.TableReference{ Name: aggregateRankedCTE.AsCompoundIdentifier(), diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index df707720..ea2242a0 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -711,6 +711,28 @@ LIMIT 100 require.Contains(t, normalizedQuery, "e.end_id = traversal.next_id") } +func TestOptimizerSafetyAggregateTraversalCountReturnsCountAlias(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u AS user, adminCount AS privileges +ORDER BY privileges DESC +LIMIT 100 + `) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(strings.ToLower(formattedQuery)), " ") + + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, normalizedQuery, "(source_node.id, source_node.kind_ids, source_node.properties)::nodecomposite as user") + require.Contains(t, normalizedQuery, "ranked.admincount as privileges") + require.Contains(t, normalizedQuery, "order by ranked.admincount desc") +} + func TestOptimizerSafetyAggregateTraversalCountSkipsUnsafeWideningCandidates(t *testing.T) { t.Parallel() From 7b552ca0a326d8c92f686c64aed04e24662da80c Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 14:20:46 -0700 Subject: [PATCH 085/114] feat(pgsql): carry selectivity through traversal lowerings --- cypher/models/pgsql/optimize/lowering_plan.go | 151 +++++++++++++++--- .../models/pgsql/optimize/optimizer_test.go | 55 +++++++ .../pgsql/translate/optimizer_safety_test.go | 17 ++ 3 files changed, 197 insertions(+), 26 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 82318d76..d9004c10 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -31,8 +31,11 @@ const ( const ( boundSourceSelectivityNone boundSourceSelectivity = iota + boundSourceSelectivityKindOnly boundSourceSelectivityPredicate boundSourceSelectivityUnique + boundSourceSelectivityLimited + boundSourceSelectivityTopN ) func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []PredicateAttachment) (LoweringPlan, error) { @@ -43,23 +46,28 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic var plan LoweringPlan if query.SingleQuery.MultiPartQuery != nil { + carriedSymbols := map[string]struct{}{} + carriedSelectivity := map[string]boundSourceSelectivity{} + for queryPartIndex, part := range query.SingleQuery.MultiPartQuery.Parts { if part == nil { continue } - if err := appendQueryPartLowerings(&plan, queryPartIndex, part, part.ReadingClauses, predicateAttachments); err != nil { + if err := appendQueryPartLowerings(&plan, queryPartIndex, part, part.ReadingClauses, predicateAttachments, carriedSymbols, carriedSelectivity); err != nil { return LoweringPlan{}, err } + + carriedSymbols, carriedSelectivity = carryProjectionSelectivity(part.With.Projection, carriedSelectivity) } if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { - if err := appendQueryPartLowerings(&plan, len(query.SingleQuery.MultiPartQuery.Parts), finalPart, finalPart.ReadingClauses, predicateAttachments); err != nil { + if err := appendQueryPartLowerings(&plan, len(query.SingleQuery.MultiPartQuery.Parts), finalPart, finalPart.ReadingClauses, predicateAttachments, carriedSymbols, carriedSelectivity); err != nil { return LoweringPlan{}, err } } } else if singlePart := query.SingleQuery.SinglePartQuery; singlePart != nil { - if err := appendQueryPartLowerings(&plan, 0, singlePart, singlePart.ReadingClauses, predicateAttachments); err != nil { + if err := appendQueryPartLowerings(&plan, 0, singlePart, singlePart.ReadingClauses, predicateAttachments, nil, nil); err != nil { return LoweringPlan{}, err } } @@ -77,6 +85,8 @@ func appendQueryPartLowerings( queryPart cypher.SyntaxNode, readingClauses []*cypher.ReadingClause, predicateAttachments []PredicateAttachment, + initialDeclaredSymbols map[string]struct{}, + initialSelectivity map[string]boundSourceSelectivity, ) error { sourceReferences, err := collectReferencedSourceIdentifiers(queryPart) if err != nil { @@ -88,7 +98,7 @@ func appendQueryPartLowerings( appendPatternPredicateProjectionLowerings(plan, queryPartIndex, queryPart, sourceReferences) appendPatternPredicatePlacementDecisions(plan, queryPartIndex, queryPart) appendExpandIntoDecisions(plan, queryPartIndex, readingClauses) - appendTraversalDirectionDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex)) + appendTraversalDirectionDecisions(plan, queryPartIndex, readingClauses, bindingPredicateSymbols(predicateAttachments, queryPartIndex), initialDeclaredSymbols, initialSelectivity) shortestPathSearchSymbols := shortestPathSearchPredicateSymbols(readingClauses) appendShortestPathStrategyDecisions(plan, queryPartIndex, readingClauses, shortestPathSearchSymbols) appendShortestPathFilterDecisions(plan, queryPartIndex, readingClauses, shortestPathSearchSymbols) @@ -335,9 +345,16 @@ func declaredSymbolsBeforeStepEndpoints(initial map[string]struct{}, steps []sou return endpoints } -func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, readingClauses []*cypher.ReadingClause, predicateConstrainedSymbols map[string]struct{}) { - declaredSymbols := map[string]struct{}{} - declaredSourceSelectivity := map[string]boundSourceSelectivity{} +func appendTraversalDirectionDecisions( + plan *LoweringPlan, + queryPartIndex int, + readingClauses []*cypher.ReadingClause, + predicateConstrainedSymbols map[string]struct{}, + initialDeclaredSymbols map[string]struct{}, + initialSelectivity map[string]boundSourceSelectivity, +) { + declaredSymbols := copyStringSet(initialDeclaredSymbols) + declaredSourceSelectivity := copyBoundSourceSelectivity(initialSelectivity) for clauseIndex, readingClause := range readingClauses { if readingClause == nil || readingClause.Match == nil { @@ -378,6 +395,7 @@ func appendTraversalDirectionDecisions(plan *LoweringPlan, queryPartIndex int, r step, declaredEndpoints[stepIndex], referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.RightNode.Variable)), + nodePatternSelectivity(step.RightNode, referencesSourceIdentifier(predicateConstrainedSymbols, variableSymbol(step.RightNode.Variable))), declaredSourceSelectivity[variableSymbol(step.LeftNode.Variable)], ); shouldFlip { plan.TraversalDirection = append(plan.TraversalDirection, decision) @@ -408,6 +426,78 @@ func bindingPredicateSymbols(predicateAttachments []PredicateAttachment, queryPa return symbols } +func copyBoundSourceSelectivity(values map[string]boundSourceSelectivity) map[string]boundSourceSelectivity { + copied := make(map[string]boundSourceSelectivity, len(values)) + for key, value := range values { + copied[key] = value + } + + return copied +} + +func carryProjectionSelectivity(projection *cypher.Projection, incoming map[string]boundSourceSelectivity) (map[string]struct{}, map[string]boundSourceSelectivity) { + carriedSymbols := map[string]struct{}{} + carriedSelectivity := map[string]boundSourceSelectivity{} + + if projection == nil { + return carriedSymbols, carriedSelectivity + } + + projectionSelectivity := projectionCardinalitySelectivity(projection) + for _, item := range projection.Items { + symbol, alias, ok := projectionItemVariableSymbolAndAlias(item) + if !ok { + continue + } + + addSymbol(carriedSymbols, alias) + mergeBoundSourceSelectivity(carriedSelectivity, alias, incoming[symbol]) + mergeBoundSourceSelectivity(carriedSelectivity, alias, projectionSelectivity) + } + + return carriedSymbols, carriedSelectivity +} + +func projectionCardinalitySelectivity(projection *cypher.Projection) boundSourceSelectivity { + if projection == nil || projection.Limit == nil { + return boundSourceSelectivityNone + } + + if projection.Order != nil || projectionHasAggregate(projection) { + return boundSourceSelectivityTopN + } + + return boundSourceSelectivityLimited +} + +func projectionHasAggregate(projection *cypher.Projection) bool { + if projection == nil { + return false + } + + for _, item := range projection.Items { + projectionItem, ok := item.(*cypher.ProjectionItem) + if !ok || projectionItem == nil { + continue + } + + if expressionHasAggregate(projectionItem.Expression) { + return true + } + } + + return false +} + +func expressionHasAggregate(expression cypher.Expression) bool { + switch typedExpression := expression.(type) { + case *cypher.FunctionInvocation: + return typedExpression != nil && strings.EqualFold(typedExpression.Name, cypher.CountFunction) + default: + return false + } +} + func declareSelectiveMatchSymbols(symbols map[string]boundSourceSelectivity, match *cypher.Match) { if match == nil { return @@ -505,7 +595,7 @@ func propertySelectivity(property string, value cypher.Expression) boundSourceSe return boundSourceSelectivityUnique } - if expressionIsStringLikeConstant(value) { + if expressionIsConstant(value) { return boundSourceSelectivityPredicate } @@ -513,23 +603,9 @@ func propertySelectivity(property string, value cypher.Expression) boundSourceSe } func expressionIsConstant(expression cypher.Expression) bool { - switch expression.(type) { - case *cypher.Literal, *cypher.Parameter: - return true - default: - return false - } -} - -func expressionIsStringLikeConstant(expression cypher.Expression) bool { switch typedExpression := expression.(type) { case *cypher.Literal: - if typedExpression == nil || typedExpression.Null { - return false - } - - _, isString := typedExpression.Value.(string) - return isString + return typedExpression != nil && !typedExpression.Null case *cypher.Parameter: return typedExpression != nil default: @@ -555,6 +631,30 @@ func nodePatternHasUniquePropertyConstraint(nodePattern *cypher.NodePattern) boo return nodePattern != nil && propertyConstraintSelectivity(nodePattern.Properties) == boundSourceSelectivityUnique } +func nodePatternSelectivity(nodePattern *cypher.NodePattern, hasAttachedPredicate bool) boundSourceSelectivity { + if nodePattern == nil { + return boundSourceSelectivityNone + } + + selectivity := boundSourceSelectivityNone + if len(nodePattern.Kinds) > 0 { + selectivity = boundSourceSelectivityKindOnly + } + + mergeSelectivityValue(&selectivity, propertyConstraintSelectivity(nodePattern.Properties)) + if hasAttachedPredicate { + mergeSelectivityValue(&selectivity, boundSourceSelectivityPredicate) + } + + return selectivity +} + +func mergeSelectivityValue(current *boundSourceSelectivity, next boundSourceSelectivity) { + if next > *current { + *current = next + } +} + func shortestPathSearchPredicateSymbols(readingClauses []*cypher.ReadingClause) map[string]struct{} { symbols := map[string]struct{}{} @@ -706,6 +806,7 @@ func boundLeftExpansionDirectionDecisionForStep( step sourceTraversalStep, declaredEndpoints declaredStepEndpoints, rightHasAttachedPredicate bool, + rightSelectivity boundSourceSelectivity, leftSourceSelectivity boundSourceSelectivity, ) (TraversalDirectionDecision, bool) { if patternPart == nil || @@ -739,9 +840,7 @@ func boundLeftExpansionDirectionDecisionForStep( } } - if leftSourceSelectivity == boundSourceSelectivityUnique && - !nodePatternHasUniquePropertyConstraint(step.RightNode) && - !rightHasAttachedPredicate { + if leftSourceSelectivity >= boundSourceSelectivityUnique && rightSelectivity < boundSourceSelectivityUnique { return TraversalDirectionDecision{ Target: target, Reason: traversalDirectionReasonBoundSourceSelective, diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 2eab75a4..dedf5ebd 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -754,6 +754,61 @@ func TestLoweringPlanSkipsBoundLeftDirectionForSelectiveSource(t *testing.T) { }}, plan.LoweringPlan.TraversalDirection) } +func TestLoweringPlanSkipsBoundLeftDirectionAfterPriorLimit(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true + WITH u + LIMIT 10 + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {name: 'target'}) + RETURN c + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 1, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Reason: traversalDirectionReasonBoundSourceSelective, + }}, plan.LoweringPlan.TraversalDirection) +} + +func TestLoweringPlanAllowsUniqueRightEndpointAfterPriorLimit(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true + WITH u + LIMIT 10 + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {objectid: 'S-1-5-21-1-2000'}) + RETURN c + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 1, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Flip: true, + Reason: traversalDirectionReasonRightConstrained, + }}, plan.LoweringPlan.TraversalDirection) +} + func TestLoweringPlanReportsAggregateTraversalCountForBoundExpansionCount(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index ea2242a0..deeccb11 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -647,6 +647,23 @@ RETURN c requireSkippedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection", "bound source estimate selective") } +func TestOptimizerSafetyTraversalDirectionReportsPriorLimitSourceSkip(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true +WITH u +LIMIT 10 +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {name: 'target'}) +RETURN c + `) + + requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireNoOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") + requireSkippedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection", "bound source estimate selective") +} + func TestOptimizerSafetyAggregateTraversalCountAcceptsRowCount(t *testing.T) { t.Parallel() From 0a0e1c672ea8ecc496c70cf522d0dd48ddb06eed Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 14:22:20 -0700 Subject: [PATCH 086/114] perf(pgsql): fold terminal filters into aggregate traversal --- cypher/models/pgsql/optimize/lowering.go | 1 + cypher/models/pgsql/optimize/lowering_plan.go | 25 ++++++++---- .../models/pgsql/optimize/optimizer_test.go | 40 +++++++++++++++++++ .../translate/aggregate_traversal_count.go | 32 +++++++++++---- .../pgsql/translate/optimizer_safety_test.go | 35 ++++++++++++++++ 5 files changed, 118 insertions(+), 15 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index 269405cb..c4873145 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -183,6 +183,7 @@ type AggregateTraversalCountShape struct { ReturnCount bool Limit int64 SourceMatch *cypher.Match + TerminalMatch *cypher.Match SourceKinds graph.Kinds TerminalKinds graph.Kinds RelationshipKinds graph.Kinds diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index d9004c10..5c3b5556 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -1303,7 +1303,7 @@ func AggregateTraversalCountShapeForQuery(query *cypher.RegularQuery) (Aggregate return AggregateTraversalCountShape{}, false } - relationship, terminalNode, terminalSymbol, ok := aggregateTraversalMatch(part.ReadingClauses[1], sourceSymbol) + terminalMatch, relationship, terminalNode, terminalSymbol, ok := aggregateTraversalMatch(part.ReadingClauses[1], sourceSymbol) if !ok { return AggregateTraversalCountShape{}, false } @@ -1333,6 +1333,7 @@ func AggregateTraversalCountShapeForQuery(query *cypher.RegularQuery) (Aggregate ReturnCount: finalProjection.ReturnCount, Limit: finalProjection.Limit, SourceMatch: sourceMatch, + TerminalMatch: terminalMatch, SourceKinds: sourceNode.Kinds, TerminalKinds: terminalNode.Kinds, RelationshipKinds: relationship.Kinds, @@ -1373,19 +1374,19 @@ func aggregateTraversalSourceMatch(readingClause *cypher.ReadingClause) (*cypher return match, nodePattern, nodePattern.Variable.Symbol, true } -func aggregateTraversalMatch(readingClause *cypher.ReadingClause, sourceSymbol string) (*cypher.RelationshipPattern, *cypher.NodePattern, string, bool) { +func aggregateTraversalMatch(readingClause *cypher.ReadingClause, sourceSymbol string) (*cypher.Match, *cypher.RelationshipPattern, *cypher.NodePattern, string, bool) { if readingClause == nil || readingClause.Match == nil { - return nil, nil, "", false + return nil, nil, nil, "", false } match := readingClause.Match - if match.Optional || match.Where != nil || len(match.Pattern) != 1 { - return nil, nil, "", false + if match.Optional || len(match.Pattern) != 1 { + return nil, nil, nil, "", false } patternPart := match.Pattern[0] if patternPart == nil || patternPart.Variable != nil || patternPart.ShortestPathPattern || patternPart.AllShortestPathsPattern || len(patternPart.PatternElements) != 3 { - return nil, nil, "", false + return nil, nil, nil, "", false } leftNode, leftOK := patternPart.PatternElements[0].AsNodePattern() @@ -1402,10 +1403,18 @@ func aggregateTraversalMatch(readingClause *cypher.ReadingClause, sourceSymbol s rightNode.Properties != nil || rightNode.Variable == nil || rightNode.Variable.Symbol == "" { - return nil, nil, "", false + return nil, nil, nil, "", false + } + + if match.Where != nil { + for _, dependency := range sortedDependencies(match.Where) { + if dependency != rightNode.Variable.Symbol { + return nil, nil, nil, "", false + } + } } - return relationship, rightNode, rightNode.Variable.Symbol, true + return match, relationship, rightNode, rightNode.Variable.Symbol, true } func aggregateTraversalWithProjection(projection *cypher.Projection, sourceSymbol, terminalSymbol string) (string, bool) { diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index dedf5ebd..3a31ace8 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -896,6 +896,46 @@ func TestLoweringPlanReportsAggregateTraversalCountWhenReturningCountAlias(t *te require.Equal(t, "privileges", shape.ReturnCountAlias) } +func TestLoweringPlanReportsAggregateTraversalCountWithTerminalFilter(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) + WHERE c.enabled = true + WITH DISTINCT u, COUNT(c) AS adminCount + RETURN u + ORDER BY adminCount DESC + LIMIT 100 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringAggregateTraversalCount}) +} + +func TestLoweringPlanSkipsAggregateTraversalCountWithCorrelatedTerminalFilter(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) + WHERE c.name = u.name + WITH DISTINCT u, COUNT(c) AS adminCount + RETURN u + ORDER BY adminCount DESC + LIMIT 100 + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.NotContains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringAggregateTraversalCount}) +} + func TestLoweringPlanSkipsSuffixPushdownAfterRightEndpointPredicateDirectionFlip(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/translate/aggregate_traversal_count.go b/cypher/models/pgsql/translate/aggregate_traversal_count.go index a7e2e58c..28280573 100644 --- a/cypher/models/pgsql/translate/aggregate_traversal_count.go +++ b/cypher/models/pgsql/translate/aggregate_traversal_count.go @@ -328,7 +328,7 @@ func (s *Translator) buildAggregateTerminalHitsCTE(shape optimize.AggregateTrave } func (s *Translator) buildAggregateTerminalNodesCTE(shape optimize.AggregateTraversalCountShape) (pgsql.CommonTableExpression, error) { - terminalWhere, err := s.aggregateNodeKindConstraint(aggregateTerminalAlias, shape.TerminalKinds) + terminalWhere, err := s.aggregateTerminalWhere(shape) if err != nil { return pgsql.CommonTableExpression{}, err } @@ -412,20 +412,38 @@ func (s *Translator) aggregateSourceWhere(shape optimize.AggregateTraversalCount return pgsql.OptionalAnd(sourcePredicate, sourceKindConstraint), nil } +func (s *Translator) aggregateTerminalWhere(shape optimize.AggregateTraversalCountShape) (pgsql.Expression, error) { + terminalKindConstraint, err := s.aggregateNodeKindConstraint(aggregateTerminalAlias, shape.TerminalKinds) + if err != nil { + return nil, err + } + + terminalPredicate, err := s.aggregateBindingPredicate(shape.TerminalMatch, shape.TerminalSymbol, aggregateTerminalAlias) + if err != nil { + return nil, err + } + + return pgsql.OptionalAnd(terminalPredicate, terminalKindConstraint), nil +} + func (s *Translator) aggregateSourcePredicate(shape optimize.AggregateTraversalCountShape) (pgsql.Expression, error) { - if shape.SourceMatch == nil || shape.SourceMatch.Where == nil { + return s.aggregateBindingPredicate(shape.SourceMatch, shape.SourceSymbol, aggregateSourceAlias) +} + +func (s *Translator) aggregateBindingPredicate(match *cypher.Match, symbol string, alias pgsql.Identifier) (pgsql.Expression, error) { + if match == nil || match.Where == nil { return nil, nil } translator := NewTranslator(s.ctx, s.kindMapper.kindMapper, s.parameters, s.graphID) - sourceBinding := translator.scope.Define(aggregateSourceAlias, pgsql.NodeComposite) - translator.scope.Alias(pgsql.Identifier(shape.SourceSymbol), sourceBinding) + binding := translator.scope.Define(alias, pgsql.NodeComposite) + translator.scope.Alias(pgsql.Identifier(symbol), binding) - if err := walk.Cypher(shape.SourceMatch.Where, translator); err != nil { + if err := walk.Cypher(match.Where, translator); err != nil { return nil, err } - sourceConstraints, err := translator.treeTranslator.ConsumeConstraintsFromVisibleSet(pgsql.AsIdentifierSet(aggregateSourceAlias)) + sourceConstraints, err := translator.treeTranslator.ConsumeConstraintsFromVisibleSet(pgsql.AsIdentifierSet(alias)) if err != nil { return nil, err } @@ -435,7 +453,7 @@ func (s *Translator) aggregateSourcePredicate(shape optimize.AggregateTraversalC return nil, err } if remainingConstraints.Expression != nil { - return nil, fmt.Errorf("unsupported aggregate traversal source predicate dependencies: %v", remainingConstraints.Dependencies.Slice()) + return nil, fmt.Errorf("unsupported aggregate traversal predicate dependencies: %v", remainingConstraints.Dependencies.Slice()) } for key, value := range translator.translation.Parameters { diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index deeccb11..6f321294 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -750,6 +750,29 @@ LIMIT 100 require.Contains(t, normalizedQuery, "order by ranked.admincount desc") } +func TestOptimizerSafetyAggregateTraversalCountFoldsTerminalFilter(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WHERE c.enabled = true +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(strings.ToLower(formattedQuery)), " ") + + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, normalizedQuery, "terminal_nodes(id) as materialized") + require.Contains(t, normalizedQuery, "terminal_node.properties -> 'enabled'") + require.Contains(t, normalizedQuery, "join terminal_nodes on terminal_nodes.id = traversal.next_id") +} + func TestOptimizerSafetyAggregateTraversalCountSkipsUnsafeWideningCandidates(t *testing.T) { t.Parallel() @@ -798,6 +821,18 @@ MATCH (u)-[r:MemberOf|AdminTo*1..]->(c:Computer) WITH DISTINCT u, r, COUNT(c) AS adminCount RETURN u, r ORDER BY adminCount DESC +LIMIT 100 + `, + }, { + name: "correlated terminal filter", + query: ` +MATCH (u:User) +WHERE u.hasspn = true +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WHERE c.name = u.name +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC LIMIT 100 `, }, { From 6ca4b1925299193401c6cda5c359b9effd80cee0 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 14:32:20 -0700 Subject: [PATCH 087/114] docs(pgsql): document aggregate optimizer continuation status --- optimization_continuation.md | 224 +++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 optimization_continuation.md diff --git a/optimization_continuation.md b/optimization_continuation.md new file mode 100644 index 00000000..f4d32729 --- /dev/null +++ b/optimization_continuation.md @@ -0,0 +1,224 @@ +# Optimizer Continuation Status: Aggregate Traversal Shape + +## Current State + +The aggregate traversal optimization plan has been implemented through the latest widening and selectivity work. The +work is split into focused commits: + +- `9c5232c` added the initial `AggregateTraversalCount` lowering and live-query optimization work. +- `94b7d78` added a guarded PostgreSQL live-plan assertion for the aggregate traversal shape. +- `51fe99c` records skipped traversal-direction diagnostics for kind-only terminal estimates. +- `61f039d` widens aggregate traversal matching to equivalent `COUNT(*)` row-count forms. +- `7f62b68` treats uniquely constrained bound sources, such as `objectid = ...`, as selective traversal anchors. +- `97c3ffa` expands aggregate traversal baseline coverage for explicit depth bounds, inbound source-left traversal, and + unsafe non-lowering shapes. +- `835a26f` widens final aggregate projections to preserve both the source node and the aggregate count with aliases. +- `0068c3e` carries source selectivity through multipart `WITH` projections, `LIMIT`, and top-N operations. +- `b9f7b4b` folds terminal-local filters into the aggregate traversal terminal-node materialization. + +The lowering now recognizes the kerberoastable aggregate family and emits an ID-only, source-anchored recursive CTE. The +emitted SQL: + +- builds source candidates as IDs; +- traverses with `root_id`, `next_id`, `depth`, and edge-ID `path`; +- uses source-anchored edge index access; +- materializes terminal node IDs once, including terminal-local predicates when present; +- groups by `root_id`; +- applies top-N before rejoining source node composites; +- can return the source node alone or the source node plus the aggregate count. + +The optimizer still keeps unsafe aggregate variants out of this lowering: + +- `COUNT(DISTINCT terminal)`; +- `OPTIONAL MATCH`; +- observed terminal projection or reuse beyond the aggregate count; +- path projection or path functions; +- relationship projection, relationship predicates, or relationship reuse; +- correlated terminal filters, such as `terminal.name = source.name`; +- post-aggregation predicates that depend on the count. + +## Latest Validation Evidence + +The current implementation has passing unit and PostgreSQL integration coverage: + +```bash +go test ./cypher/models/pgsql/... -count=1 +make test +CONNECTION_STRING='postgres://...' make test_integration +``` + +The full PostgreSQL integration run completed successfully. The `integration` package took about `351.7s`. + +`make format` still fails in this environment because `goimports` is unavailable or not executable: + +```text +xargs: goimports: Permission denied +``` + +Touched Go files were formatted with `gofmt`. + +## Latest Plan Comparison + +After the integration suite, the PostgreSQL database no longer looked like the restored large live aggregate dataset to +the guarded aggregate assertion: + +```text +candidateUsers:0 computers:0 adminEdges:0 +``` + +The guarded assertion therefore skipped rather than producing a large-live-dataset plan verdict. The comparison runner +still completed successfully against the current PostgreSQL and Neo4j databases and refreshed +`.coverage/live-plan-comparison.md/json`. + +Current comparison-run timings: + +- `group_objectid_exact_string_equality`: PostgreSQL `0.2 ms`; Neo4j oracle shape `node-label-scan + top-or-limit`. +- `domain_admins_reverse_membership_source_disjunction`: PostgreSQL `101.8 ms`; Neo4j oracle shape + `directed-expand + node-label-scan + top-or-limit + var-length-expand`. +- `dangerous_domain_users_privileges_exclude_memberof`: PostgreSQL `99.7 ms`; Neo4j oracle shape + `directed-expand + top-or-limit`. +- `domain_admin_logons_exclude_domain_controllers`: PostgreSQL `141.6 ms`; Neo4j oracle shape + `directed-expand + top-or-limit + var-length-expand`. +- `kerberoastable_users_by_admin_privilege_count`: PostgreSQL `95.5 ms`; Neo4j oracle shape + `aggregation + directed-expand + node-label-scan + top-or-limit + var-length-expand`. +- `kerberoastable_users_left_label_guard`: PostgreSQL `104.6 ms`; Neo4j oracle shape + `aggregation + directed-expand + node-label-scan + top-or-limit + var-length-expand`. +- `shortest_path_domain_users_to_tier_zero`: PostgreSQL `201.0 ms`; Neo4j oracle shape + `node-label-scan + top-or-limit + var-length-expand`. +- `cross_forest_trusts_require_connected_abuse_edge`: PostgreSQL `0.1 ms`; Neo4j oracle shape + `anti-semi-apply + top-or-limit`. +- `azure_high_privileged_role_bounded_membership`: PostgreSQL `1.6 ms`; Neo4j oracle shape + `directed-expand + node-label-scan + top-or-limit + var-length-expand`. + +These numbers are smoke evidence for the current implementation. They should not replace the earlier large-live-dataset +baseline because the guarded assertion reported that the large aggregate dataset was not present. + +The earlier restored-live-dataset comparison remains the best large-data aggregate baseline: + +- `kerberoastable_users_by_admin_privilege_count`: `12025.1 ms` execution, `12030.6 ms` wall duration; +- `kerberoastable_users_left_label_guard`: `12376.4 ms` execution, `12379.5 ms` wall duration; +- source-anchored recursive traversal from filtered `User` candidates; +- `Hash Join` from traversal rows to materialized `terminal_nodes`; +- `HashAggregate` with `Group Key: traversal.root_id`; +- final source node primary-key lookups after the top-N stage. + +The original live cardinalities that motivated this work were: + +- candidate users after the property filter: about `222`; +- `Computer` nodes: about `139k`; +- `MemberOf|AdminTo` edges: about `2.09M`. + +## Neo4j Oracle + +The Neo4j oracle still commonly prefers label scans followed by `VarLengthExpand(All)`, eager aggregation, and top-N for +the aggregate shapes. That remains useful as an operator-order comparison, but PostgreSQL should not mirror this shape +on the observed large data. The PostgreSQL-specific win comes from filtered source candidates, ID-only traversal, root-ID +aggregation, and deferred source materialization. + +The comparison runner should stay in place as the base for further oracle testing. + +## Completed Plan Items + +### 1. Aggregate Widening Baseline + +Completed in `97c3ffa`. + +Coverage now includes: + +- explicit variable-length depth bounds; +- inbound source-left traversal; +- unsafe non-lowering candidates for distinct counts, optional matches, path bindings, relationship bindings, and + post-aggregation filters. + +### 2. Final Projection Widening + +Completed in `835a26f`. + +The aggregate traversal shape now preserves: + +- source return alias; +- optional count return alias; +- final return forms that include only the source node or both source node and aggregate count. + +### 3. Broader Selectivity Heuristics + +Completed in `0068c3e`. + +The lowering planner now carries selectivity through multipart query parts with a graded model: + +- no useful selectivity; +- kind-only selectivity; +- property predicate selectivity; +- unique equality selectivity; +- limited source sets; +- top-N source sets. + +This prevents a prior limited or top-N source set from being flipped toward a broad terminal unless the terminal side is +also selective enough to justify the direction change. + +### 4. Terminal-Local Filter Folding + +Completed in `b9f7b4b`. + +Terminal-local `WHERE` predicates can now be folded into `terminal_nodes` when every referenced symbol belongs to the +terminal node. Correlated terminal filters remain excluded. + +### 5. Validation and Documentation + +Completed in this document update. + +Validation performed: + +- PostgreSQL model tests; +- unit test suite; +- PostgreSQL integration suite; +- guarded aggregate live-plan assertion, which skipped because the current PostgreSQL corpus did not match the restored + large live dataset; +- PostgreSQL/Neo4j comparison runner, which completed and refreshed ignored comparison artifacts. + +## Remaining Work + +### 1. Re-run Large-Live-Dataset Vetting After Reload + +The next live reload should rerun: + +```bash +CONNECTION_STRING='postgres://...' go test -v -tags manual_integration ./integration \ + -run TestPostgreSQLLiveAggregateTraversalCountPlanShape \ + -count=1 -parallel=1 -timeout=90s + +PG_CONNECTION_STRING='postgres://...' NEO4J_CONNECTION_STRING='neo4j://...' \ + LIVE_VET_TIMEOUT='30s' go run .coverage/live_plan_compare.go +``` + +Acceptance criteria: + +- the guarded assertion does not skip; +- the aggregate query completes under the statement timeout; +- the recursive CTE is source-anchored; +- grouping is by `root_id`, not node composites; +- source node materialization occurs after top-N limiting; +- terminal-local filters remain inside terminal-node materialization when the query shape allows it. + +### 2. Add Evidence Before Further Aggregate Widening + +The next widening candidates should require specific query-corpus examples and tests before implementation: + +- post-aggregation count predicates that can be safely converted to `HAVING`; +- multiple aggregate return aliases if a real query needs them; +- source-side filters that can be pushed into source candidate materialization after multipart rewrites. + +### 3. Keep Broader Selectivity Conservative + +The new selectivity model is intentionally coarse. Future broadening should be backed by live plan mismatches for: + +- known unique equality predicates beyond `objectid`; +- source candidates reduced by prior aggregation; +- property predicates with observed high selectivity; +- label/kind combinations whose cardinality is small enough to anchor traversal. + +### 4. Keep Schema Index Work Deferred + +Do not add default indexes yet. The measured large-live bottleneck was traversal and aggregation shape, not candidate-user +lookup. Revisit targeted expression or partial indexes only if refreshed live plans show source candidate filtering has +become the next dominant cost. From 5172fbfd27e761da423ceee955829869ac8857f4 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 17:21:02 -0700 Subject: [PATCH 088/114] feat(graphbench): add scale corpus contract --- benchmark/testdata/scale/README.md | 9 + benchmark/testdata/scale/cases/counts.json | 70 ++++++ benchmark/testdata/scale/cases/lookups.json | 54 +++++ .../testdata/scale/cases/shortest_paths.json | 61 +++++ benchmark/testdata/scale/cases/traversal.json | 143 +++++++++++ cmd/graphbench/corpus.go | 121 ++++++++++ cmd/graphbench/corpus_test.go | 45 ++++ cmd/graphbench/main.go | 32 +++ cmd/graphbench/types.go | 104 ++++++++ optimization_continuation.md | 224 ------------------ 10 files changed, 639 insertions(+), 224 deletions(-) create mode 100644 benchmark/testdata/scale/README.md create mode 100644 benchmark/testdata/scale/cases/counts.json create mode 100644 benchmark/testdata/scale/cases/lookups.json create mode 100644 benchmark/testdata/scale/cases/shortest_paths.json create mode 100644 benchmark/testdata/scale/cases/traversal.json create mode 100644 cmd/graphbench/corpus.go create mode 100644 cmd/graphbench/corpus_test.go create mode 100644 cmd/graphbench/main.go create mode 100644 cmd/graphbench/types.go delete mode 100644 optimization_continuation.md diff --git a/benchmark/testdata/scale/README.md b/benchmark/testdata/scale/README.md new file mode 100644 index 00000000..487ce204 --- /dev/null +++ b/benchmark/testdata/scale/README.md @@ -0,0 +1,9 @@ +# GraphBench Scale Corpus + +This corpus measures graph workload shapes, not general Cypher correctness. +The shared integration corpus remains the source of backend-equivalent semantic coverage. + +Cases declare the values a query observes so benchmark reports can separate ID-only work from node, relationship, property, and path materialization. +Initial execution modes are `postgres_sql`, `local_traversal`, and `neo4j`. +Apache AGE is intentionally not a benchmark mode here; it may appear only in `reference_design` notes as input for DAWGS design choices. + diff --git a/benchmark/testdata/scale/cases/counts.json b/benchmark/testdata/scale/cases/counts.json new file mode 100644 index 00000000..37f93714 --- /dev/null +++ b/benchmark/testdata/scale/cases/counts.json @@ -0,0 +1,70 @@ +{ + "cases": [ + { + "name": "all_node_count", + "dataset": "base", + "category": "counts", + "cypher": "MATCH (n) RETURN count(n)", + "expected": { + "row_count": 1, + "result_kind": "scalar" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["count", "count-store"] + }, + { + "name": "typed_node_count", + "dataset": "base", + "category": "counts", + "cypher": "MATCH (n:NodeKind1) RETURN count(n)", + "expected": { + "row_count": 1, + "result_kind": "scalar" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "terminal_predicate": "node_kind", + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["count", "typed-count", "graph-stats"] + }, + { + "name": "typed_edge_count", + "dataset": "base", + "category": "counts", + "cypher": "MATCH ()-[r:EdgeKind1]->() RETURN count(r)", + "expected": { + "row_count": 1, + "result_kind": "scalar" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "edge_kinds": ["EdgeKind1"], + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["count", "typed-count", "graph-stats"] + } + ] +} + diff --git a/benchmark/testdata/scale/cases/lookups.json b/benchmark/testdata/scale/cases/lookups.json new file mode 100644 index 00000000..724c1af8 --- /dev/null +++ b/benchmark/testdata/scale/cases/lookups.json @@ -0,0 +1,54 @@ +{ + "cases": [ + { + "name": "objectid_exact_string_anchor", + "dataset": "base", + "category": "lookups", + "cypher": "MATCH (n:NodeKind1) WHERE n.objectid = $objectid RETURN id(n)", + "params": { + "objectid": "S-1-5-21-1" + }, + "expected": { + "row_count": 1, + "result_kind": "id_set" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "root_predicate": "selective_property", + "terminal_predicate": "node_kind", + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["property-anchor", "expression-index"] + }, + { + "name": "boolean_property_filter", + "dataset": "base", + "category": "lookups", + "cypher": "MATCH (n:NodeKind1) WHERE n.enabled = true RETURN id(n)", + "expected": { + "row_count": 1, + "result_kind": "id_set" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "root_predicate": "boolean_property", + "terminal_predicate": "node_kind", + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["property-filter"] + } + ] +} + diff --git a/benchmark/testdata/scale/cases/shortest_paths.json b/benchmark/testdata/scale/cases/shortest_paths.json new file mode 100644 index 00000000..b9539b36 --- /dev/null +++ b/benchmark/testdata/scale/cases/shortest_paths.json @@ -0,0 +1,61 @@ +{ + "cases": [ + { + "name": "shortest_distance_bound_pair", + "dataset": "base", + "category": "shortest_path", + "cypher": "MATCH p = shortestPath((s)-[*1..]->(e)) WHERE id(s) = $start_id AND id(e) = $end_id RETURN length(p)", + "node_params": { + "start_id": "n1", + "end_id": "n3" + }, + "expected": { + "row_count": 1, + "result_kind": "scalar" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "root_predicate": "bound_id", + "terminal_predicate": "bound_id", + "min_depth": 1, + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "local_traversal", "neo4j"], + "tags": ["shortest-distance", "local-traversal-candidate"] + }, + { + "name": "one_shortest_path_bound_pair", + "dataset": "base", + "category": "shortest_path", + "cypher": "MATCH p = shortestPath((s)-[*1..]->(e)) WHERE id(s) = $start_id AND id(e) = $end_id RETURN p LIMIT 1", + "node_params": { + "start_id": "n1", + "end_id": "n3" + }, + "expected": { + "row_count": 1, + "result_kind": "path_set" + }, + "observes": { + "paths": true, + "nodes": true, + "relationships": true, + "properties": true + }, + "shape": { + "root_predicate": "bound_id", + "terminal_predicate": "bound_id", + "min_depth": 1, + "path_materialization_required": true + }, + "candidate_modes": ["postgres_sql", "local_traversal", "neo4j"], + "tags": ["one-shortest-path", "local-traversal-candidate"] + } + ] +} + diff --git a/benchmark/testdata/scale/cases/traversal.json b/benchmark/testdata/scale/cases/traversal.json new file mode 100644 index 00000000..2bab928d --- /dev/null +++ b/benchmark/testdata/scale/cases/traversal.json @@ -0,0 +1,143 @@ +{ + "cases": [ + { + "name": "one_hop_typed_from_bound_id", + "dataset": "base", + "category": "one_hop", + "cypher": "MATCH (s)-[:EdgeKind1]->(e) WHERE id(s) = $start_id RETURN id(e)", + "node_params": { + "start_id": "n1" + }, + "expected": { + "row_count": 1, + "result_kind": "id_set" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "root_predicate": "bound_id", + "edge_kinds": ["EdgeKind1"], + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["typed-expansion", "id-only"] + }, + { + "name": "variable_length_id_only_from_bound_id", + "dataset": "base", + "category": "variable_length_reachability", + "cypher": "MATCH (s)-[*1..]->(e) WHERE id(s) = $start_id RETURN id(e)", + "node_params": { + "start_id": "n1" + }, + "expected": { + "row_count": 2, + "result_kind": "id_set" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "root_predicate": "bound_id", + "min_depth": 1, + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "local_traversal", "neo4j"], + "tags": ["reachability", "id-only", "local-traversal-candidate"], + "reference_design": { + "age_relevance": ["vle_cost_model"], + "notes": "AGE VLE behavior is useful design context for cycle and duplicate handling, but this case is not run against AGE." + } + }, + { + "name": "variable_length_path_observed_from_bound_id", + "dataset": "base", + "category": "path_observed_variable_length", + "cypher": "MATCH p = (s)-[*1..]->(e) WHERE id(s) = $start_id RETURN p", + "node_params": { + "start_id": "n1" + }, + "expected": { + "row_count": 2, + "result_kind": "path_set" + }, + "observes": { + "paths": true, + "nodes": true, + "relationships": true, + "properties": true + }, + "shape": { + "root_predicate": "bound_id", + "min_depth": 1, + "path_materialization_required": true + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["path-materialization"] + }, + { + "name": "adcs_p1_endpoint_ids", + "dataset": "adcs_fanout", + "category": "bloodhound_search", + "cypher": "MATCH (n:Group) WHERE n.objectid = $objectid MATCH (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) RETURN id(ca), id(d)", + "params": { + "objectid": "S-1-5-21-2643190041-1319121918-239771340-513" + }, + "expected": { + "row_count": 4, + "result_kind": "id_rows" + }, + "observes": { + "paths": false, + "nodes": false, + "relationships": false, + "properties": false + }, + "shape": { + "root_predicate": "selective_property", + "terminal_predicate": "fixed_suffix", + "edge_kinds": ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + "min_depth": 0, + "path_materialization_required": false + }, + "candidate_modes": ["postgres_sql", "local_traversal", "neo4j"], + "tags": ["bloodhound", "adcs", "id-only", "local-traversal-candidate"] + }, + { + "name": "adcs_p1_path_observed", + "dataset": "adcs_fanout", + "category": "bloodhound_search", + "cypher": "MATCH (n:Group) WHERE n.objectid = $objectid MATCH p = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) RETURN p", + "params": { + "objectid": "S-1-5-21-2643190041-1319121918-239771340-513" + }, + "expected": { + "row_count": 4, + "result_kind": "path_set" + }, + "observes": { + "paths": true, + "nodes": true, + "relationships": true, + "properties": true + }, + "shape": { + "root_predicate": "selective_property", + "terminal_predicate": "fixed_suffix", + "edge_kinds": ["MemberOf", "Enroll", "TrustedForNTAuth", "NTAuthStoreFor"], + "min_depth": 0, + "path_materialization_required": true + }, + "candidate_modes": ["postgres_sql", "neo4j"], + "tags": ["bloodhound", "adcs", "path-materialization"] + } + ] +} + diff --git a/cmd/graphbench/corpus.go b/cmd/graphbench/corpus.go new file mode 100644 index 00000000..6546d248 --- /dev/null +++ b/cmd/graphbench/corpus.go @@ -0,0 +1,121 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" +) + +func loadScaleCorpus(root string) (ScaleCorpus, error) { + casePaths, err := filepath.Glob(filepath.Join(root, "cases", "*.json")) + if err != nil { + return ScaleCorpus{}, fmt.Errorf("glob scale cases: %w", err) + } + if len(casePaths) == 0 { + return ScaleCorpus{}, fmt.Errorf("no scale case files found under %s", filepath.Join(root, "cases")) + } + + sort.Strings(casePaths) + + var corpus ScaleCorpus + for _, path := range casePaths { + var file ScaleCaseFile + if err := decodeJSONFile(path, &file); err != nil { + return ScaleCorpus{}, err + } + + source := filepath.ToSlash(path) + for idx, testCase := range file.Cases { + testCase.Source = source + if err := validateScaleCase(testCase); err != nil { + return ScaleCorpus{}, fmt.Errorf("%s case %d: %w", source, idx, err) + } + + corpus.Cases = append(corpus.Cases, testCase) + } + } + + return corpus, nil +} + +func validateScaleCase(testCase ScaleCase) error { + if testCase.Name == "" { + return fmt.Errorf("name is required") + } + if testCase.Dataset == "" { + return fmt.Errorf("dataset is required") + } + if testCase.Category == "" { + return fmt.Errorf("category is required") + } + if testCase.Cypher == "" { + return fmt.Errorf("cypher is required") + } + if len(testCase.CandidateModes) == 0 { + return fmt.Errorf("candidate_modes is required") + } + + for _, mode := range testCase.CandidateModes { + if !mode.Valid() { + return fmt.Errorf("unsupported candidate mode %q", mode) + } + } + + return nil +} + +func decodeJSONFile(path string, target any) error { + raw, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + if err := json.Unmarshal(raw, target); err != nil { + return fmt.Errorf("decode %s: %w", path, err) + } + + return nil +} + +func scaleCorpusDatasets(corpus ScaleCorpus) []string { + seen := map[string]struct{}{} + datasets := make([]string, 0) + + for _, testCase := range corpus.Cases { + if _, duplicate := seen[testCase.Dataset]; duplicate { + continue + } + + seen[testCase.Dataset] = struct{}{} + datasets = append(datasets, testCase.Dataset) + } + + sort.Strings(datasets) + return datasets +} + +func scaleCasesByDataset(corpus ScaleCorpus) map[string][]ScaleCase { + grouped := map[string][]ScaleCase{} + for _, testCase := range corpus.Cases { + grouped[testCase.Dataset] = append(grouped[testCase.Dataset], testCase) + } + + return grouped +} diff --git a/cmd/graphbench/corpus_test.go b/cmd/graphbench/corpus_test.go new file mode 100644 index 00000000..211b2084 --- /dev/null +++ b/cmd/graphbench/corpus_test.go @@ -0,0 +1,45 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadScaleCorpus(t *testing.T) { + corpus, err := loadScaleCorpus("../../benchmark/testdata/scale") + require.NoError(t, err) + require.NotEmpty(t, corpus.Cases) + + for _, testCase := range corpus.Cases { + require.NotEqual(t, "", testCase.Source) + require.True(t, testCase.Supports(ModePostgresSQL), "postgres_sql should be part of the initial corpus for %s", testCase.Name) + require.False(t, testCase.Supports(ExecutionMode("age")), "AGE is a reference design only for %s", testCase.Name) + } +} + +func TestScaleCorpusDatasets(t *testing.T) { + corpus := ScaleCorpus{Cases: []ScaleCase{ + {Name: "a", Dataset: "base", Category: "counts", Cypher: "return 1", CandidateModes: []ExecutionMode{ModePostgresSQL}}, + {Name: "b", Dataset: "adcs_fanout", Category: "counts", Cypher: "return 1", CandidateModes: []ExecutionMode{ModePostgresSQL}}, + {Name: "c", Dataset: "base", Category: "counts", Cypher: "return 1", CandidateModes: []ExecutionMode{ModePostgresSQL}}, + }} + + require.Equal(t, []string{"adcs_fanout", "base"}, scaleCorpusDatasets(corpus)) +} diff --git a/cmd/graphbench/main.go b/cmd/graphbench/main.go new file mode 100644 index 00000000..87555be9 --- /dev/null +++ b/cmd/graphbench/main.go @@ -0,0 +1,32 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "os" +) + +func main() { + corpus, err := loadScaleCorpus("benchmark/testdata/scale") + if err != nil { + fmt.Fprintf(os.Stderr, "graphbench corpus error: %v\n", err) + os.Exit(1) + } + + fmt.Fprintf(os.Stderr, "loaded %d graphbench scale cases; runners are not implemented yet\n", len(corpus.Cases)) +} diff --git a/cmd/graphbench/types.go b/cmd/graphbench/types.go new file mode 100644 index 00000000..c941a01a --- /dev/null +++ b/cmd/graphbench/types.go @@ -0,0 +1,104 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "slices" + "strings" +) + +const ( + ModePostgresSQL ExecutionMode = "postgres_sql" + ModeLocalTraversal ExecutionMode = "local_traversal" + ModeNeo4j ExecutionMode = "neo4j" +) + +var validExecutionModes = []ExecutionMode{ + ModePostgresSQL, + ModeLocalTraversal, + ModeNeo4j, +} + +type ExecutionMode string + +func (s ExecutionMode) Valid() bool { + return slices.Contains(validExecutionModes, s) +} + +func parseExecutionMode(raw string) (ExecutionMode, error) { + mode := ExecutionMode(strings.TrimSpace(raw)) + if mode.Valid() { + return mode, nil + } + + return "", fmt.Errorf("unsupported execution mode %q", raw) +} + +type ScaleCorpus struct { + Cases []ScaleCase +} + +type ScaleCaseFile struct { + Cases []ScaleCase `json:"cases"` +} + +type ScaleCase struct { + Source string `json:"-"` + Name string `json:"name"` + Dataset string `json:"dataset"` + Category string `json:"category"` + Cypher string `json:"cypher"` + Params map[string]any `json:"params,omitempty"` + NodeParams map[string]string `json:"node_params,omitempty"` + Expected ExpectedResult `json:"expected"` + Observes ObservedValues `json:"observes"` + Shape WorkloadShape `json:"shape"` + CandidateModes []ExecutionMode `json:"candidate_modes"` + Tags []string `json:"tags,omitempty"` + ReferenceDesign *ReferenceDesign `json:"reference_design,omitempty"` +} + +type ExpectedResult struct { + RowCount *int64 `json:"row_count,omitempty"` + ResultKind string `json:"result_kind,omitempty"` +} + +type ObservedValues struct { + Paths bool `json:"paths"` + Nodes bool `json:"nodes"` + Relationships bool `json:"relationships"` + Properties bool `json:"properties"` +} + +type WorkloadShape struct { + RootPredicate string `json:"root_predicate,omitempty"` + TerminalPredicate string `json:"terminal_predicate,omitempty"` + EdgeKinds []string `json:"edge_kinds,omitempty"` + MinDepth *int `json:"min_depth,omitempty"` + MaxDepth *int `json:"max_depth,omitempty"` + PathMaterializationRequired bool `json:"path_materialization_required"` +} + +type ReferenceDesign struct { + AGERelevance []string `json:"age_relevance,omitempty"` + Notes string `json:"notes,omitempty"` +} + +func (s ScaleCase) Supports(mode ExecutionMode) bool { + return slices.Contains(s.CandidateModes, mode) +} diff --git a/optimization_continuation.md b/optimization_continuation.md deleted file mode 100644 index f4d32729..00000000 --- a/optimization_continuation.md +++ /dev/null @@ -1,224 +0,0 @@ -# Optimizer Continuation Status: Aggregate Traversal Shape - -## Current State - -The aggregate traversal optimization plan has been implemented through the latest widening and selectivity work. The -work is split into focused commits: - -- `9c5232c` added the initial `AggregateTraversalCount` lowering and live-query optimization work. -- `94b7d78` added a guarded PostgreSQL live-plan assertion for the aggregate traversal shape. -- `51fe99c` records skipped traversal-direction diagnostics for kind-only terminal estimates. -- `61f039d` widens aggregate traversal matching to equivalent `COUNT(*)` row-count forms. -- `7f62b68` treats uniquely constrained bound sources, such as `objectid = ...`, as selective traversal anchors. -- `97c3ffa` expands aggregate traversal baseline coverage for explicit depth bounds, inbound source-left traversal, and - unsafe non-lowering shapes. -- `835a26f` widens final aggregate projections to preserve both the source node and the aggregate count with aliases. -- `0068c3e` carries source selectivity through multipart `WITH` projections, `LIMIT`, and top-N operations. -- `b9f7b4b` folds terminal-local filters into the aggregate traversal terminal-node materialization. - -The lowering now recognizes the kerberoastable aggregate family and emits an ID-only, source-anchored recursive CTE. The -emitted SQL: - -- builds source candidates as IDs; -- traverses with `root_id`, `next_id`, `depth`, and edge-ID `path`; -- uses source-anchored edge index access; -- materializes terminal node IDs once, including terminal-local predicates when present; -- groups by `root_id`; -- applies top-N before rejoining source node composites; -- can return the source node alone or the source node plus the aggregate count. - -The optimizer still keeps unsafe aggregate variants out of this lowering: - -- `COUNT(DISTINCT terminal)`; -- `OPTIONAL MATCH`; -- observed terminal projection or reuse beyond the aggregate count; -- path projection or path functions; -- relationship projection, relationship predicates, or relationship reuse; -- correlated terminal filters, such as `terminal.name = source.name`; -- post-aggregation predicates that depend on the count. - -## Latest Validation Evidence - -The current implementation has passing unit and PostgreSQL integration coverage: - -```bash -go test ./cypher/models/pgsql/... -count=1 -make test -CONNECTION_STRING='postgres://...' make test_integration -``` - -The full PostgreSQL integration run completed successfully. The `integration` package took about `351.7s`. - -`make format` still fails in this environment because `goimports` is unavailable or not executable: - -```text -xargs: goimports: Permission denied -``` - -Touched Go files were formatted with `gofmt`. - -## Latest Plan Comparison - -After the integration suite, the PostgreSQL database no longer looked like the restored large live aggregate dataset to -the guarded aggregate assertion: - -```text -candidateUsers:0 computers:0 adminEdges:0 -``` - -The guarded assertion therefore skipped rather than producing a large-live-dataset plan verdict. The comparison runner -still completed successfully against the current PostgreSQL and Neo4j databases and refreshed -`.coverage/live-plan-comparison.md/json`. - -Current comparison-run timings: - -- `group_objectid_exact_string_equality`: PostgreSQL `0.2 ms`; Neo4j oracle shape `node-label-scan + top-or-limit`. -- `domain_admins_reverse_membership_source_disjunction`: PostgreSQL `101.8 ms`; Neo4j oracle shape - `directed-expand + node-label-scan + top-or-limit + var-length-expand`. -- `dangerous_domain_users_privileges_exclude_memberof`: PostgreSQL `99.7 ms`; Neo4j oracle shape - `directed-expand + top-or-limit`. -- `domain_admin_logons_exclude_domain_controllers`: PostgreSQL `141.6 ms`; Neo4j oracle shape - `directed-expand + top-or-limit + var-length-expand`. -- `kerberoastable_users_by_admin_privilege_count`: PostgreSQL `95.5 ms`; Neo4j oracle shape - `aggregation + directed-expand + node-label-scan + top-or-limit + var-length-expand`. -- `kerberoastable_users_left_label_guard`: PostgreSQL `104.6 ms`; Neo4j oracle shape - `aggregation + directed-expand + node-label-scan + top-or-limit + var-length-expand`. -- `shortest_path_domain_users_to_tier_zero`: PostgreSQL `201.0 ms`; Neo4j oracle shape - `node-label-scan + top-or-limit + var-length-expand`. -- `cross_forest_trusts_require_connected_abuse_edge`: PostgreSQL `0.1 ms`; Neo4j oracle shape - `anti-semi-apply + top-or-limit`. -- `azure_high_privileged_role_bounded_membership`: PostgreSQL `1.6 ms`; Neo4j oracle shape - `directed-expand + node-label-scan + top-or-limit + var-length-expand`. - -These numbers are smoke evidence for the current implementation. They should not replace the earlier large-live-dataset -baseline because the guarded assertion reported that the large aggregate dataset was not present. - -The earlier restored-live-dataset comparison remains the best large-data aggregate baseline: - -- `kerberoastable_users_by_admin_privilege_count`: `12025.1 ms` execution, `12030.6 ms` wall duration; -- `kerberoastable_users_left_label_guard`: `12376.4 ms` execution, `12379.5 ms` wall duration; -- source-anchored recursive traversal from filtered `User` candidates; -- `Hash Join` from traversal rows to materialized `terminal_nodes`; -- `HashAggregate` with `Group Key: traversal.root_id`; -- final source node primary-key lookups after the top-N stage. - -The original live cardinalities that motivated this work were: - -- candidate users after the property filter: about `222`; -- `Computer` nodes: about `139k`; -- `MemberOf|AdminTo` edges: about `2.09M`. - -## Neo4j Oracle - -The Neo4j oracle still commonly prefers label scans followed by `VarLengthExpand(All)`, eager aggregation, and top-N for -the aggregate shapes. That remains useful as an operator-order comparison, but PostgreSQL should not mirror this shape -on the observed large data. The PostgreSQL-specific win comes from filtered source candidates, ID-only traversal, root-ID -aggregation, and deferred source materialization. - -The comparison runner should stay in place as the base for further oracle testing. - -## Completed Plan Items - -### 1. Aggregate Widening Baseline - -Completed in `97c3ffa`. - -Coverage now includes: - -- explicit variable-length depth bounds; -- inbound source-left traversal; -- unsafe non-lowering candidates for distinct counts, optional matches, path bindings, relationship bindings, and - post-aggregation filters. - -### 2. Final Projection Widening - -Completed in `835a26f`. - -The aggregate traversal shape now preserves: - -- source return alias; -- optional count return alias; -- final return forms that include only the source node or both source node and aggregate count. - -### 3. Broader Selectivity Heuristics - -Completed in `0068c3e`. - -The lowering planner now carries selectivity through multipart query parts with a graded model: - -- no useful selectivity; -- kind-only selectivity; -- property predicate selectivity; -- unique equality selectivity; -- limited source sets; -- top-N source sets. - -This prevents a prior limited or top-N source set from being flipped toward a broad terminal unless the terminal side is -also selective enough to justify the direction change. - -### 4. Terminal-Local Filter Folding - -Completed in `b9f7b4b`. - -Terminal-local `WHERE` predicates can now be folded into `terminal_nodes` when every referenced symbol belongs to the -terminal node. Correlated terminal filters remain excluded. - -### 5. Validation and Documentation - -Completed in this document update. - -Validation performed: - -- PostgreSQL model tests; -- unit test suite; -- PostgreSQL integration suite; -- guarded aggregate live-plan assertion, which skipped because the current PostgreSQL corpus did not match the restored - large live dataset; -- PostgreSQL/Neo4j comparison runner, which completed and refreshed ignored comparison artifacts. - -## Remaining Work - -### 1. Re-run Large-Live-Dataset Vetting After Reload - -The next live reload should rerun: - -```bash -CONNECTION_STRING='postgres://...' go test -v -tags manual_integration ./integration \ - -run TestPostgreSQLLiveAggregateTraversalCountPlanShape \ - -count=1 -parallel=1 -timeout=90s - -PG_CONNECTION_STRING='postgres://...' NEO4J_CONNECTION_STRING='neo4j://...' \ - LIVE_VET_TIMEOUT='30s' go run .coverage/live_plan_compare.go -``` - -Acceptance criteria: - -- the guarded assertion does not skip; -- the aggregate query completes under the statement timeout; -- the recursive CTE is source-anchored; -- grouping is by `root_id`, not node composites; -- source node materialization occurs after top-N limiting; -- terminal-local filters remain inside terminal-node materialization when the query shape allows it. - -### 2. Add Evidence Before Further Aggregate Widening - -The next widening candidates should require specific query-corpus examples and tests before implementation: - -- post-aggregation count predicates that can be safely converted to `HAVING`; -- multiple aggregate return aliases if a real query needs them; -- source-side filters that can be pushed into source candidate materialization after multipart rewrites. - -### 3. Keep Broader Selectivity Conservative - -The new selectivity model is intentionally coarse. Future broadening should be backed by live plan mismatches for: - -- known unique equality predicates beyond `objectid`; -- source candidates reduced by prior aggregation; -- property predicates with observed high selectivity; -- label/kind combinations whose cardinality is small enough to anchor traversal. - -### 4. Keep Schema Index Work Deferred - -Do not add default indexes yet. The measured large-live bottleneck was traversal and aggregation shape, not candidate-user -lookup. Revisit targeted expression or partial indexes only if refreshed live plans show source candidate filtering has -become the next dominant cost. From 2f7654e67fec11afd342674c606d1d788883134b Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 17:24:35 -0700 Subject: [PATCH 089/114] feat(graphbench): add PostgreSQL SQL runner --- cmd/graphbench/datasets.go | 117 ++++++++++++++ cmd/graphbench/main.go | 124 +++++++++++++- cmd/graphbench/measure.go | 61 +++++++ cmd/graphbench/postgres.go | 275 ++++++++++++++++++++++++++++++++ cmd/graphbench/postgres_test.go | 63 ++++++++ cmd/graphbench/results.go | 134 ++++++++++++++++ 6 files changed, 770 insertions(+), 4 deletions(-) create mode 100644 cmd/graphbench/datasets.go create mode 100644 cmd/graphbench/measure.go create mode 100644 cmd/graphbench/postgres.go create mode 100644 cmd/graphbench/postgres_test.go create mode 100644 cmd/graphbench/results.go diff --git a/cmd/graphbench/datasets.go b/cmd/graphbench/datasets.go new file mode 100644 index 00000000..af400ca8 --- /dev/null +++ b/cmd/graphbench/datasets.go @@ -0,0 +1,117 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/opengraph" +) + +const defaultGraphName = "integration_test" + +func scanDatasetKinds(datasetDir string, datasetNames []string) (graph.Kinds, graph.Kinds, error) { + var nodeKinds, edgeKinds graph.Kinds + + for _, datasetName := range datasetNames { + doc, err := parseDataset(datasetDir, datasetName) + if err != nil { + return nil, nil, err + } + + nextNodeKinds, nextEdgeKinds := doc.Graph.Kinds() + nodeKinds = nodeKinds.Add(nextNodeKinds...) + edgeKinds = edgeKinds.Add(nextEdgeKinds...) + } + + return nodeKinds, edgeKinds, nil +} + +func parseDataset(datasetDir, name string) (opengraph.Document, error) { + path := filepath.Join(datasetDir, name+".json") + f, err := os.Open(path) + if err != nil { + return opengraph.Document{}, fmt.Errorf("open dataset %s: %w", name, err) + } + defer f.Close() + + doc, err := opengraph.ParseDocument(f) + if err != nil { + return opengraph.Document{}, fmt.Errorf("parse dataset %s: %w", name, err) + } + + return doc, nil +} + +func loadDataset(ctx context.Context, db graph.Database, datasetDir, name string) (opengraph.IDMap, error) { + path := filepath.Join(datasetDir, name+".json") + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open dataset %s: %w", name, err) + } + defer f.Close() + + idMap, err := opengraph.Load(ctx, db, f) + if err != nil { + return nil, fmt.Errorf("load dataset %s: %w", name, err) + } + + return idMap, nil +} + +func clearGraph(ctx context.Context, db graph.Database) error { + return db.WriteTransaction(ctx, func(tx graph.Transaction) error { + return tx.Nodes().Delete() + }) +} + +func benchmarkSchema(nodeKinds, edgeKinds graph.Kinds) graph.Schema { + return graph.Schema{ + Graphs: []graph.Graph{{ + Name: defaultGraphName, + Nodes: nodeKinds, + Edges: edgeKinds, + }}, + DefaultGraph: graph.Graph{Name: defaultGraphName}, + } +} + +func resolveCaseParams(testCase ScaleCase, idMap opengraph.IDMap) (map[string]any, error) { + params := make(map[string]any, len(testCase.Params)+len(testCase.NodeParams)) + for key, value := range testCase.Params { + params[key] = value + } + + for paramName, nodeName := range testCase.NodeParams { + id, found := idMap[nodeName] + if !found { + return nil, fmt.Errorf("case %s references unknown dataset node %q", testCase.Name, nodeName) + } + + params[paramName] = id.Int64() + } + + if len(params) == 0 { + return nil, nil + } + + return params, nil +} diff --git a/cmd/graphbench/main.go b/cmd/graphbench/main.go index 87555be9..f6eb02eb 100644 --- a/cmd/graphbench/main.go +++ b/cmd/graphbench/main.go @@ -17,16 +17,132 @@ package main import ( + "context" + "flag" "fmt" + "io" "os" + "strings" ) +type config struct { + CorpusRoot string + DatasetDir string + Connection string + PGConnection string + Modes []ExecutionMode + Iterations int + OutputJSONL string +} + +func parseConfig(args []string, env func(string) string) (config, error) { + flags := flag.NewFlagSet("graphbench", flag.ContinueOnError) + flags.SetOutput(io.Discard) + + var ( + cfg config + rawModes string + ) + + flags.StringVar(&cfg.CorpusRoot, "corpus-root", "benchmark/testdata/scale", "scale corpus root") + flags.StringVar(&cfg.DatasetDir, "dataset-dir", "integration/testdata", "dataset root") + flags.StringVar(&cfg.Connection, "connection", env("CONNECTION_STRING"), "single backend connection string") + flags.StringVar(&cfg.PGConnection, "pg-connection", env("PG_CONNECTION_STRING"), "PostgreSQL connection string") + flags.StringVar(&rawModes, "modes", string(ModePostgresSQL), "comma-separated execution modes") + flags.IntVar(&cfg.Iterations, "iterations", 3, "timed iterations per case") + flags.StringVar(&cfg.OutputJSONL, "jsonl-output", "", "JSONL output path (default: stdout)") + + if err := flags.Parse(args); err != nil { + return config{}, err + } + if cfg.Iterations < 1 { + return config{}, fmt.Errorf("iterations must be at least 1") + } + + modes, err := parseExecutionModes(rawModes) + if err != nil { + return config{}, err + } + cfg.Modes = modes + + return cfg, nil +} + +func parseExecutionModes(raw string) ([]ExecutionMode, error) { + var modes []ExecutionMode + seen := map[ExecutionMode]struct{}{} + + for _, part := range strings.Split(raw, ",") { + mode, err := parseExecutionMode(part) + if err != nil { + return nil, err + } + if _, duplicate := seen[mode]; duplicate { + continue + } + + seen[mode] = struct{}{} + modes = append(modes, mode) + } + if len(modes) == 0 { + return nil, fmt.Errorf("at least one execution mode is required") + } + + return modes, nil +} + +func fatal(format string, args ...any) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} + func main() { - corpus, err := loadScaleCorpus("benchmark/testdata/scale") + cfg, err := parseConfig(os.Args[1:], os.Getenv) + if err != nil { + fatal("%v", err) + } + + corpus, err := loadScaleCorpus(cfg.CorpusRoot) if err != nil { - fmt.Fprintf(os.Stderr, "graphbench corpus error: %v\n", err) - os.Exit(1) + fatal("load corpus: %v", err) } - fmt.Fprintf(os.Stderr, "loaded %d graphbench scale cases; runners are not implemented yet\n", len(corpus.Cases)) + ctx := context.Background() + var records []CaseResult + + for _, mode := range cfg.Modes { + switch mode { + case ModePostgresSQL: + pgConnection := cfg.PGConnection + if pgConnection == "" { + pgConnection = cfg.Connection + } + if pgConnection == "" { + fatal("postgres_sql mode requires -pg-connection, -connection, PG_CONNECTION_STRING, or CONNECTION_STRING") + } + + runner, err := newPostgresSQLRunner(ctx, cfg.DatasetDir, pgConnection, corpus) + if err != nil { + fatal("open postgres_sql runner: %v", err) + } + + nextRecords, err := runner.Run(ctx, cfg.Iterations, corpus) + closeErr := runner.Close(ctx) + if err != nil { + fatal("run postgres_sql: %v", err) + } + if closeErr != nil { + fatal("close postgres_sql: %v", closeErr) + } + + records = append(records, nextRecords...) + + default: + fatal("execution mode %s is not implemented yet", mode) + } + } + + if err := writeJSONLFile(cfg.OutputJSONL, records); err != nil { + fatal("write JSONL: %v", err) + } } diff --git a/cmd/graphbench/measure.go b/cmd/graphbench/measure.go new file mode 100644 index 00000000..b41f91ae --- /dev/null +++ b/cmd/graphbench/measure.go @@ -0,0 +1,61 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "time" + + "github.com/specterops/dawgs/graph" +) + +func countCypherRows(tx graph.Transaction, cypher string, params map[string]any) (int64, error) { + result := tx.Query(cypher, params) + defer result.Close() + + var rowCount int64 + for result.Next() { + rowCount++ + } + + return rowCount, result.Error() +} + +func measureCypher(ctx context.Context, db graph.Database, cypher string, params map[string]any, iterations int) (int64, DurationStats, error) { + var warmupRows int64 + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + var err error + warmupRows, err = countCypherRows(tx, cypher, params) + return err + }); err != nil { + return 0, DurationStats{}, err + } + + durations := make([]time.Duration, iterations) + for idx := range iterations { + start := time.Now() + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + _, err := countCypherRows(tx, cypher, params) + return err + }); err != nil { + return 0, DurationStats{}, err + } + durations[idx] = time.Since(start) + } + + return warmupRows, computeDurationStats(durations), nil +} diff --git a/cmd/graphbench/postgres.go b/cmd/graphbench/postgres.go new file mode 100644 index 00000000..7e6dfb0b --- /dev/null +++ b/cmd/graphbench/postgres.go @@ -0,0 +1,275 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/specterops/dawgs" + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/pgsql/translate" + "github.com/specterops/dawgs/drivers" + "github.com/specterops/dawgs/drivers/pg" + "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/opengraph" + "github.com/specterops/dawgs/util/size" +) + +type postgresSQLRunner struct { + datasetDir string + db graph.Database + pgDriver *pg.Driver + graphID int32 +} + +func newPostgresSQLRunner(ctx context.Context, datasetDir, connection string, corpus ScaleCorpus) (*postgresSQLRunner, error) { + pool, err := pg.NewPool(drivers.DatabaseConfiguration{Connection: connection}) + if err != nil { + return nil, fmt.Errorf("create PostgreSQL pool: %w", err) + } + + db, err := dawgs.Open(ctx, pg.DriverName, dawgs.Config{ + GraphQueryMemoryLimit: size.Gibibyte, + ConnectionString: connection, + Pool: pool, + }) + if err != nil { + pool.Close() + return nil, fmt.Errorf("open PostgreSQL database: %w", err) + } + + nodeKinds, edgeKinds, err := scanDatasetKinds(datasetDir, scaleCorpusDatasets(corpus)) + if err != nil { + _ = db.Close(ctx) + return nil, err + } + + if err := db.AssertSchema(ctx, benchmarkSchema(nodeKinds, edgeKinds)); err != nil { + _ = db.Close(ctx) + return nil, fmt.Errorf("assert PostgreSQL schema: %w", err) + } + + pgDriver, ok := db.(*pg.Driver) + if !ok { + _ = db.Close(ctx) + return nil, fmt.Errorf("expected *pg.Driver, got %T", db) + } + + defaultGraph, ok := pgDriver.DefaultGraph() + if !ok { + _ = db.Close(ctx) + return nil, fmt.Errorf("PostgreSQL default graph is not set") + } + + return &postgresSQLRunner{ + datasetDir: datasetDir, + db: db, + pgDriver: pgDriver, + graphID: defaultGraph.ID, + }, nil +} + +func (s *postgresSQLRunner) Close(ctx context.Context) error { + if s.db == nil { + return nil + } + + return s.db.Close(ctx) +} + +func (s *postgresSQLRunner) Run(ctx context.Context, iterations int, corpus ScaleCorpus) ([]CaseResult, error) { + var records []CaseResult + casesByDataset := scaleCasesByDataset(corpus) + + for _, datasetName := range scaleCorpusDatasets(corpus) { + if err := clearGraph(ctx, s.db); err != nil { + return nil, fmt.Errorf("clear graph for %s: %w", datasetName, err) + } + + idMap, err := loadDataset(ctx, s.db, s.datasetDir, datasetName) + if err != nil { + return nil, err + } + + for _, testCase := range casesByDataset[datasetName] { + if !testCase.Supports(ModePostgresSQL) { + continue + } + + record := s.runCase(ctx, iterations, testCase, idMap) + records = append(records, record) + } + } + + return records, nil +} + +func (s *postgresSQLRunner) runCase(ctx context.Context, iterations int, testCase ScaleCase, idMap opengraph.IDMap) CaseResult { + params, err := resolveCaseParams(testCase, idMap) + record := newCaseResult(testCase, ModePostgresSQL, params) + if err != nil { + record.Status = StatusError + record.Error = err.Error() + return record + } + + rowCount, stats, err := measureCypher(ctx, s.db, testCase.Cypher, params, iterations) + if err != nil { + record.Status = StatusError + record.Error = err.Error() + return record + } + + record.RowCount = rowCount + record.Stats = stats + applyRowExpectation(&record) + + explain, err := s.explain(ctx, testCase.Cypher, params) + if err != nil { + if record.Status == StatusOK { + record.Status = StatusError + record.Error = err.Error() + } + return record + } + + record.SQL = explain.SQL + record.PostgresPlan = explain.Plan + record.PostgresMetrics = &explain.Metrics + record.Optimization = &explain.Optimization + return record +} + +type postgresExplain struct { + SQL string + Plan []string + Metrics PostgresPlanMetrics + Optimization translate.OptimizationSummary +} + +func (s *postgresSQLRunner) explain(ctx context.Context, cypherQuery string, params map[string]any) (postgresExplain, error) { + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) + if err != nil { + return postgresExplain{}, err + } + + translation, err := translate.Translate(ctx, regularQuery, s.pgDriver.KindMapper(), params, s.graphID) + if err != nil { + return postgresExplain{}, err + } + + sqlQuery, err := translate.Translated(translation) + if err != nil { + return postgresExplain{}, err + } + + var plan []string + if err := s.db.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Raw("EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) "+sqlQuery, translation.Parameters) + defer result.Close() + + for result.Next() { + values := result.Values() + if len(values) == 0 { + continue + } + + plan = append(plan, fmt.Sprint(values[0])) + } + + return result.Error() + }); err != nil { + return postgresExplain{}, err + } + + return postgresExplain{ + SQL: sqlQuery, + Plan: plan, + Metrics: parsePostgresPlanMetrics(plan), + Optimization: translation.Optimization, + }, nil +} + +var ( + postgresPlanningPattern = regexp.MustCompile(`Planning Time: ([0-9.]+) ms`) + postgresExecutionPattern = regexp.MustCompile(`Execution Time: ([0-9.]+) ms`) + postgresBufferPattern = regexp.MustCompile(`(?:(shared|temp) )?(hit|read|dirtied|written)=([0-9]+)`) +) + +func parsePostgresPlanMetrics(plan []string) PostgresPlanMetrics { + var metrics PostgresPlanMetrics + for _, line := range plan { + if metrics.PlanningMS == nil { + if match := postgresPlanningPattern.FindStringSubmatch(line); match != nil { + if parsed, err := strconv.ParseFloat(match[1], 64); err == nil { + metrics.PlanningMS = &parsed + } + } + } + + if metrics.ExecutionMS == nil { + if match := postgresExecutionPattern.FindStringSubmatch(line); match != nil { + if parsed, err := strconv.ParseFloat(match[1], 64); err == nil { + metrics.ExecutionMS = &parsed + } + } + } + + if strings.Contains(line, "Buffers:") && metrics.Buffers == (Buffers{}) { + metrics.Buffers = parsePostgresBuffers(line) + } + } + + return metrics +} + +func parsePostgresBuffers(line string) Buffers { + var ( + buffers Buffers + bufferScope string + ) + + for _, match := range postgresBufferPattern.FindAllStringSubmatch(line, -1) { + value, err := strconv.ParseInt(match[3], 10, 64) + if err != nil { + continue + } + + if match[1] != "" { + bufferScope = match[1] + } + + switch bufferScope + "_" + match[2] { + case "shared_hit": + buffers.SharedHit = value + case "shared_read": + buffers.SharedRead = value + case "shared_dirtied": + buffers.SharedDirtied = value + case "temp_read": + buffers.TempRead = value + case "temp_written": + buffers.TempWritten = value + } + } + + return buffers +} diff --git a/cmd/graphbench/postgres_test.go b/cmd/graphbench/postgres_test.go new file mode 100644 index 00000000..54470e60 --- /dev/null +++ b/cmd/graphbench/postgres_test.go @@ -0,0 +1,63 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/opengraph" + "github.com/stretchr/testify/require" +) + +func TestResolveCaseParams(t *testing.T) { + params, err := resolveCaseParams(ScaleCase{ + Params: map[string]any{ + "name": "value", + }, + NodeParams: map[string]string{ + "start_id": "n1", + }, + }, opengraph.IDMap{"n1": graph.ID(42)}) + + require.NoError(t, err) + require.Equal(t, map[string]any{ + "name": "value", + "start_id": int64(42), + }, params) +} + +func TestParsePostgresPlanMetrics(t *testing.T) { + metrics := parsePostgresPlanMetrics([]string{ + "Nested Loop (actual rows=1 loops=1)", + " Buffers: shared hit=12 read=3 dirtied=2, temp read=4 written=5", + "Planning Time: 1.250 ms", + "Execution Time: 9.750 ms", + }) + + require.NotNil(t, metrics.PlanningMS) + require.Equal(t, 1.25, *metrics.PlanningMS) + require.NotNil(t, metrics.ExecutionMS) + require.Equal(t, 9.75, *metrics.ExecutionMS) + require.Equal(t, Buffers{ + SharedHit: 12, + SharedRead: 3, + SharedDirtied: 2, + TempRead: 4, + TempWritten: 5, + }, metrics.Buffers) +} diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go new file mode 100644 index 00000000..ed383e48 --- /dev/null +++ b/cmd/graphbench/results.go @@ -0,0 +1,134 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "sort" + "time" + + "github.com/specterops/dawgs/cypher/models/pgsql/translate" +) + +const ( + StatusOK = "ok" + StatusRowMismatch = "row_mismatch" + StatusError = "error" +) + +type DurationStats struct { + Iterations int `json:"iterations"` + Median time.Duration `json:"median"` + P95 time.Duration `json:"p95"` + Max time.Duration `json:"max"` +} + +type PostgresPlanMetrics struct { + PlanningMS *float64 `json:"planning_ms,omitempty"` + ExecutionMS *float64 `json:"execution_ms,omitempty"` + Buffers Buffers `json:"buffers,omitempty"` +} + +type Buffers struct { + SharedHit int64 `json:"shared_hit,omitempty"` + SharedRead int64 `json:"shared_read,omitempty"` + SharedDirtied int64 `json:"shared_dirtied,omitempty"` + TempRead int64 `json:"temp_read,omitempty"` + TempWritten int64 `json:"temp_written,omitempty"` +} + +type CaseResult struct { + Source string `json:"source"` + Dataset string `json:"dataset"` + Name string `json:"name"` + Category string `json:"category"` + ExecutionMode ExecutionMode `json:"execution_mode"` + Status string `json:"status"` + Cypher string `json:"cypher"` + Params map[string]any `json:"params,omitempty"` + ExpectedRowCount *int64 `json:"expected_row_count,omitempty"` + RowCount int64 `json:"row_count,omitempty"` + Stats DurationStats `json:"stats,omitempty"` + SQL string `json:"sql,omitempty"` + PostgresPlan []string `json:"postgres_plan,omitempty"` + PostgresMetrics *PostgresPlanMetrics `json:"postgres_metrics,omitempty"` + Optimization *translate.OptimizationSummary `json:"optimization,omitempty"` + Error string `json:"error,omitempty"` +} + +func newCaseResult(testCase ScaleCase, mode ExecutionMode, params map[string]any) CaseResult { + return CaseResult{ + Source: testCase.Source, + Dataset: testCase.Dataset, + Name: testCase.Name, + Category: testCase.Category, + ExecutionMode: mode, + Status: StatusOK, + Cypher: testCase.Cypher, + Params: params, + ExpectedRowCount: testCase.Expected.RowCount, + } +} + +func computeDurationStats(durations []time.Duration) DurationStats { + sort.Slice(durations, func(i, j int) bool { + return durations[i] < durations[j] + }) + + n := len(durations) + return DurationStats{ + Iterations: n, + Median: durations[n/2], + P95: durations[n*95/100], + Max: durations[n-1], + } +} + +func applyRowExpectation(result *CaseResult) { + if result.ExpectedRowCount != nil && result.RowCount != *result.ExpectedRowCount { + result.Status = StatusRowMismatch + result.Error = fmt.Sprintf("expected %d rows, got %d", *result.ExpectedRowCount, result.RowCount) + } +} + +func writeJSONLFile(path string, records []CaseResult) error { + if path == "" { + return writeJSONL(os.Stdout, records) + } + + output, err := os.Create(path) + if err != nil { + return err + } + defer output.Close() + + return writeJSONL(output, records) +} + +func writeJSONL(w io.Writer, records []CaseResult) error { + encoder := json.NewEncoder(w) + for _, record := range records { + if err := encoder.Encode(record); err != nil { + return err + } + } + + return nil +} From f6912e2fa91dfa5309da58537d54f85140306c5e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 17:25:45 -0700 Subject: [PATCH 090/114] feat(graphbench): add Neo4j runner --- cmd/graphbench/main.go | 41 ++++- cmd/graphbench/neo4j.go | 296 +++++++++++++++++++++++++++++++++++ cmd/graphbench/neo4j_test.go | 53 +++++++ cmd/graphbench/results.go | 2 + 4 files changed, 385 insertions(+), 7 deletions(-) create mode 100644 cmd/graphbench/neo4j.go create mode 100644 cmd/graphbench/neo4j_test.go diff --git a/cmd/graphbench/main.go b/cmd/graphbench/main.go index f6eb02eb..dc5efe21 100644 --- a/cmd/graphbench/main.go +++ b/cmd/graphbench/main.go @@ -26,13 +26,14 @@ import ( ) type config struct { - CorpusRoot string - DatasetDir string - Connection string - PGConnection string - Modes []ExecutionMode - Iterations int - OutputJSONL string + CorpusRoot string + DatasetDir string + Connection string + PGConnection string + Neo4jConnection string + Modes []ExecutionMode + Iterations int + OutputJSONL string } func parseConfig(args []string, env func(string) string) (config, error) { @@ -48,6 +49,7 @@ func parseConfig(args []string, env func(string) string) (config, error) { flags.StringVar(&cfg.DatasetDir, "dataset-dir", "integration/testdata", "dataset root") flags.StringVar(&cfg.Connection, "connection", env("CONNECTION_STRING"), "single backend connection string") flags.StringVar(&cfg.PGConnection, "pg-connection", env("PG_CONNECTION_STRING"), "PostgreSQL connection string") + flags.StringVar(&cfg.Neo4jConnection, "neo4j-connection", env("NEO4J_CONNECTION_STRING"), "Neo4j connection string") flags.StringVar(&rawModes, "modes", string(ModePostgresSQL), "comma-separated execution modes") flags.IntVar(&cfg.Iterations, "iterations", 3, "timed iterations per case") flags.StringVar(&cfg.OutputJSONL, "jsonl-output", "", "JSONL output path (default: stdout)") @@ -137,6 +139,31 @@ func main() { records = append(records, nextRecords...) + case ModeNeo4j: + neo4jConnection := cfg.Neo4jConnection + if neo4jConnection == "" { + neo4jConnection = cfg.Connection + } + if neo4jConnection == "" { + fatal("neo4j mode requires -neo4j-connection, -connection, NEO4J_CONNECTION_STRING, or CONNECTION_STRING") + } + + runner, err := newNeo4jRunner(ctx, cfg.DatasetDir, neo4jConnection, corpus) + if err != nil { + fatal("open neo4j runner: %v", err) + } + + nextRecords, err := runner.Run(ctx, cfg.Iterations, corpus) + closeErr := runner.Close(ctx) + if err != nil { + fatal("run neo4j: %v", err) + } + if closeErr != nil { + fatal("close neo4j: %v", closeErr) + } + + records = append(records, nextRecords...) + default: fatal("execution mode %s is not implemented yet", mode) } diff --git a/cmd/graphbench/neo4j.go b/cmd/graphbench/neo4j.go new file mode 100644 index 00000000..9c720eed --- /dev/null +++ b/cmd/graphbench/neo4j.go @@ -0,0 +1,296 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "net/url" + "strings" + + neo4jcore "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/specterops/dawgs" + dawgsneo4j "github.com/specterops/dawgs/drivers/neo4j" + "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/opengraph" + "github.com/specterops/dawgs/util/size" +) + +type neo4jRunner struct { + datasetDir string + db graph.Database + planDriver neo4jcore.Driver + databaseName string +} + +func newNeo4jRunner(ctx context.Context, datasetDir, connection string, corpus ScaleCorpus) (*neo4jRunner, error) { + db, err := dawgs.Open(ctx, dawgsneo4j.DriverName, dawgs.Config{ + GraphQueryMemoryLimit: size.Gibibyte, + ConnectionString: connection, + }) + if err != nil { + return nil, fmt.Errorf("open Neo4j database: %w", err) + } + + nodeKinds, edgeKinds, err := scanDatasetKinds(datasetDir, scaleCorpusDatasets(corpus)) + if err != nil { + _ = db.Close(ctx) + return nil, err + } + + if err := db.AssertSchema(ctx, benchmarkSchema(nodeKinds, edgeKinds)); err != nil { + _ = db.Close(ctx) + return nil, fmt.Errorf("assert Neo4j schema: %w", err) + } + + planDriver, databaseName, err := openNeo4jPlanDriver(connection) + if err != nil { + _ = db.Close(ctx) + return nil, err + } + + return &neo4jRunner{ + datasetDir: datasetDir, + db: db, + planDriver: planDriver, + databaseName: databaseName, + }, nil +} + +func (s *neo4jRunner) Close(ctx context.Context) error { + var closeErr error + if s.planDriver != nil { + closeErr = s.planDriver.Close() + } + if s.db != nil { + if err := s.db.Close(ctx); err != nil && closeErr == nil { + closeErr = err + } + } + + return closeErr +} + +func (s *neo4jRunner) Run(ctx context.Context, iterations int, corpus ScaleCorpus) ([]CaseResult, error) { + var records []CaseResult + casesByDataset := scaleCasesByDataset(corpus) + + for _, datasetName := range scaleCorpusDatasets(corpus) { + if err := clearGraph(ctx, s.db); err != nil { + return nil, fmt.Errorf("clear graph for %s: %w", datasetName, err) + } + + idMap, err := loadDataset(ctx, s.db, s.datasetDir, datasetName) + if err != nil { + return nil, err + } + + for _, testCase := range casesByDataset[datasetName] { + if !testCase.Supports(ModeNeo4j) { + continue + } + + record := s.runCase(ctx, iterations, testCase, idMap) + records = append(records, record) + } + } + + return records, nil +} + +func (s *neo4jRunner) runCase(ctx context.Context, iterations int, testCase ScaleCase, idMap opengraph.IDMap) CaseResult { + params, err := resolveCaseParams(testCase, idMap) + record := newCaseResult(testCase, ModeNeo4j, params) + if err != nil { + record.Status = StatusError + record.Error = err.Error() + return record + } + + rowCount, stats, err := measureCypher(ctx, s.db, testCase.Cypher, params, iterations) + if err != nil { + record.Status = StatusError + record.Error = err.Error() + return record + } + + record.RowCount = rowCount + record.Stats = stats + applyRowExpectation(&record) + + plan, operators, err := s.explain(testCase.Cypher, params) + if err != nil { + if record.Status == StatusOK { + record.Status = StatusError + record.Error = err.Error() + } + return record + } + + record.Neo4jPlan = plan + record.Neo4jOperators = operators + return record +} + +func (s *neo4jRunner) explain(cypherQuery string, params map[string]any) (*Neo4jPlanNode, []string, error) { + session := s.planDriver.NewSession(neo4jcore.SessionConfig{ + AccessMode: neo4jcore.AccessModeRead, + DatabaseName: s.databaseName, + }) + defer session.Close() + + result, err := session.Run("EXPLAIN "+cypherWithoutTerminator(cypherQuery), params) + if err != nil { + return nil, nil, err + } + + summary, err := result.Consume() + if err != nil { + return nil, nil, err + } + if summary.Plan() == nil { + return nil, nil, nil + } + + plan := convertNeo4jPlan(summary.Plan()) + return &plan, neo4jOperators(plan), nil +} + +type neo4jPlanDriverConfig struct { + Target string + Username string + Password string + DatabaseName string +} + +func parseNeo4jPlanDriverConfig(connStr string) (neo4jPlanDriverConfig, error) { + connectionURL, err := url.Parse(connStr) + if err != nil { + return neo4jPlanDriverConfig{}, fmt.Errorf("parse Neo4j connection string: %w", err) + } + + if connectionURL.Scheme != dawgsneo4j.DriverName && connectionURL.Scheme != "neo4j+s" && connectionURL.Scheme != "neo4j+ssc" { + return neo4jPlanDriverConfig{}, fmt.Errorf("expected Neo4j connection string scheme, got %q", connectionURL.Scheme) + } + + password, ok := connectionURL.User.Password() + if !ok { + return neo4jPlanDriverConfig{}, fmt.Errorf("no password provided in Neo4j connection string") + } + if connectionURL.Host == "" { + return neo4jPlanDriverConfig{}, fmt.Errorf("Neo4j connection string host is required") + } + + databaseName, err := neo4jDatabaseName(connectionURL) + if err != nil { + return neo4jPlanDriverConfig{}, err + } + + return neo4jPlanDriverConfig{ + Target: (&url.URL{ + Scheme: connectionURL.Scheme, + Host: connectionURL.Host, + RawQuery: connectionURL.RawQuery, + }).String(), + Username: connectionURL.User.Username(), + Password: password, + DatabaseName: databaseName, + }, nil +} + +func neo4jDatabaseName(connectionURL *url.URL) (string, error) { + databasePath := strings.Trim(connectionURL.EscapedPath(), "/") + if databasePath == "" { + return "", nil + } + if strings.Contains(databasePath, "/") { + return "", fmt.Errorf("Neo4j database path must contain a single database name") + } + + databaseName, err := url.PathUnescape(databasePath) + if err != nil { + return "", fmt.Errorf("parse Neo4j database name: %w", err) + } + + return databaseName, nil +} + +func openNeo4jPlanDriver(connStr string) (neo4jcore.Driver, string, error) { + cfg, err := parseNeo4jPlanDriverConfig(connStr) + if err != nil { + return nil, "", err + } + + driver, err := neo4jcore.NewDriver(cfg.Target, neo4jcore.BasicAuth(cfg.Username, cfg.Password, "")) + if err != nil { + return nil, "", err + } + + return driver, cfg.DatabaseName, nil +} + +type Neo4jPlanNode struct { + Operator string `json:"operator"` + Arguments map[string]string `json:"arguments,omitempty"` + Identifiers []string `json:"identifiers,omitempty"` + Children []Neo4jPlanNode `json:"children,omitempty"` +} + +func convertNeo4jPlan(plan neo4jcore.Plan) Neo4jPlanNode { + node := Neo4jPlanNode{ + Operator: plan.Operator(), + Arguments: stringifyArguments(plan.Arguments()), + Identifiers: append([]string(nil), plan.Identifiers()...), + } + + for _, child := range plan.Children() { + node.Children = append(node.Children, convertNeo4jPlan(child)) + } + + return node +} + +func stringifyArguments(arguments map[string]any) map[string]string { + if len(arguments) == 0 { + return nil + } + + values := make(map[string]string, len(arguments)) + for key, value := range arguments { + values[key] = fmt.Sprint(value) + } + + return values +} + +func neo4jOperators(root Neo4jPlanNode) []string { + var operators []string + var walk func(Neo4jPlanNode) + walk = func(node Neo4jPlanNode) { + operators = append(operators, node.Operator+"@neo4j") + for _, child := range node.Children { + walk(child) + } + } + walk(root) + + return operators +} + +func cypherWithoutTerminator(cypherQuery string) string { + return strings.TrimSuffix(strings.TrimSpace(cypherQuery), ";") +} diff --git a/cmd/graphbench/neo4j_test.go b/cmd/graphbench/neo4j_test.go new file mode 100644 index 00000000..dfb795db --- /dev/null +++ b/cmd/graphbench/neo4j_test.go @@ -0,0 +1,53 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseNeo4jPlanDriverConfig(t *testing.T) { + cfg, err := parseNeo4jPlanDriverConfig("neo4j://neo4j:secret@example.com:7687/neo4jdb?x=1") + + require.NoError(t, err) + require.Equal(t, "neo4j://example.com:7687?x=1", cfg.Target) + require.Equal(t, "neo4j", cfg.Username) + require.Equal(t, "secret", cfg.Password) + require.Equal(t, "neo4jdb", cfg.DatabaseName) +} + +func TestNeo4jDatabaseNameRejectsNestedPath(t *testing.T) { + parsed, err := url.Parse("neo4j://neo4j:secret@example.com:7687/a/b") + require.NoError(t, err) + + _, err = neo4jDatabaseName(parsed) + require.ErrorContains(t, err, "single database name") +} + +func TestNeo4jOperatorsAnnotatesOperators(t *testing.T) { + operators := neo4jOperators(Neo4jPlanNode{ + Operator: "ProduceResults", + Children: []Neo4jPlanNode{{ + Operator: "AllNodesScan", + }}, + }) + + require.Equal(t, []string{"ProduceResults@neo4j", "AllNodesScan@neo4j"}, operators) +} diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go index ed383e48..90332274 100644 --- a/cmd/graphbench/results.go +++ b/cmd/graphbench/results.go @@ -69,6 +69,8 @@ type CaseResult struct { SQL string `json:"sql,omitempty"` PostgresPlan []string `json:"postgres_plan,omitempty"` PostgresMetrics *PostgresPlanMetrics `json:"postgres_metrics,omitempty"` + Neo4jPlan *Neo4jPlanNode `json:"neo4j_plan,omitempty"` + Neo4jOperators []string `json:"neo4j_operators,omitempty"` Optimization *translate.OptimizationSummary `json:"optimization,omitempty"` Error string `json:"error,omitempty"` } From 2c0a4ce1c12a03e079538e3dd71f60a5bc782a76 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 17:26:20 -0700 Subject: [PATCH 091/114] feat(graphbench): add local traversal placeholder --- cmd/graphbench/local_traversal.go | 35 ++++++++++++++++++ cmd/graphbench/local_traversal_test.go | 49 ++++++++++++++++++++++++++ cmd/graphbench/main.go | 3 ++ cmd/graphbench/results.go | 10 ++++-- 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 cmd/graphbench/local_traversal.go create mode 100644 cmd/graphbench/local_traversal_test.go diff --git a/cmd/graphbench/local_traversal.go b/cmd/graphbench/local_traversal.go new file mode 100644 index 00000000..6fff61bd --- /dev/null +++ b/cmd/graphbench/local_traversal.go @@ -0,0 +1,35 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +const localTraversalUnavailableReason = "local traversal executor unavailable" + +func runLocalTraversalPlaceholders(corpus ScaleCorpus) []CaseResult { + records := make([]CaseResult, 0) + for _, testCase := range corpus.Cases { + if !testCase.Supports(ModeLocalTraversal) { + continue + } + + record := newCaseResult(testCase, ModeLocalTraversal, testCase.Params) + record.Status = StatusNotImplemented + record.FallbackReason = localTraversalUnavailableReason + records = append(records, record) + } + + return records +} diff --git a/cmd/graphbench/local_traversal_test.go b/cmd/graphbench/local_traversal_test.go new file mode 100644 index 00000000..39a7e728 --- /dev/null +++ b/cmd/graphbench/local_traversal_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRunLocalTraversalPlaceholders(t *testing.T) { + records := runLocalTraversalPlaceholders(ScaleCorpus{Cases: []ScaleCase{ + { + Name: "supported", + Dataset: "base", + Category: "reachability", + Cypher: "MATCH (n) RETURN n", + NodeParams: map[string]string{"start_id": "n1"}, + CandidateModes: []ExecutionMode{ModePostgresSQL, ModeLocalTraversal}, + }, + { + Name: "unsupported", + Dataset: "base", + Category: "count", + Cypher: "MATCH (n) RETURN count(n)", + CandidateModes: []ExecutionMode{ModePostgresSQL}, + }, + }}) + + require.Len(t, records, 1) + require.Equal(t, ModeLocalTraversal, records[0].ExecutionMode) + require.Equal(t, StatusNotImplemented, records[0].Status) + require.Equal(t, localTraversalUnavailableReason, records[0].FallbackReason) + require.Equal(t, map[string]string{"start_id": "n1"}, records[0].NodeParams) +} diff --git a/cmd/graphbench/main.go b/cmd/graphbench/main.go index dc5efe21..a1992f50 100644 --- a/cmd/graphbench/main.go +++ b/cmd/graphbench/main.go @@ -164,6 +164,9 @@ func main() { records = append(records, nextRecords...) + case ModeLocalTraversal: + records = append(records, runLocalTraversalPlaceholders(corpus)...) + default: fatal("execution mode %s is not implemented yet", mode) } diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go index 90332274..db18a9ba 100644 --- a/cmd/graphbench/results.go +++ b/cmd/graphbench/results.go @@ -28,9 +28,10 @@ import ( ) const ( - StatusOK = "ok" - StatusRowMismatch = "row_mismatch" - StatusError = "error" + StatusOK = "ok" + StatusRowMismatch = "row_mismatch" + StatusError = "error" + StatusNotImplemented = "not_implemented" ) type DurationStats struct { @@ -63,6 +64,7 @@ type CaseResult struct { Status string `json:"status"` Cypher string `json:"cypher"` Params map[string]any `json:"params,omitempty"` + NodeParams map[string]string `json:"node_params,omitempty"` ExpectedRowCount *int64 `json:"expected_row_count,omitempty"` RowCount int64 `json:"row_count,omitempty"` Stats DurationStats `json:"stats,omitempty"` @@ -72,6 +74,7 @@ type CaseResult struct { Neo4jPlan *Neo4jPlanNode `json:"neo4j_plan,omitempty"` Neo4jOperators []string `json:"neo4j_operators,omitempty"` Optimization *translate.OptimizationSummary `json:"optimization,omitempty"` + FallbackReason string `json:"fallback_reason,omitempty"` Error string `json:"error,omitempty"` } @@ -85,6 +88,7 @@ func newCaseResult(testCase ScaleCase, mode ExecutionMode, params map[string]any Status: StatusOK, Cypher: testCase.Cypher, Params: params, + NodeParams: testCase.NodeParams, ExpectedRowCount: testCase.Expected.RowCount, } } From d6efb9229f839094b5baa799b1a76e92d0f52275 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 17:29:25 -0700 Subject: [PATCH 092/114] feat(graphbench): add comparison reports --- cmd/graphbench/main.go | 24 +++ cmd/graphbench/results.go | 81 +++++++++ cmd/graphbench/summary.go | 305 +++++++++++++++++++++++++++++++++ cmd/graphbench/summary_test.go | 84 +++++++++ 4 files changed, 494 insertions(+) create mode 100644 cmd/graphbench/summary.go create mode 100644 cmd/graphbench/summary_test.go diff --git a/cmd/graphbench/main.go b/cmd/graphbench/main.go index a1992f50..b4ac4102 100644 --- a/cmd/graphbench/main.go +++ b/cmd/graphbench/main.go @@ -34,6 +34,9 @@ type config struct { Modes []ExecutionMode Iterations int OutputJSONL string + Summary string + SummaryJSON string + Baseline string } func parseConfig(args []string, env func(string) string) (config, error) { @@ -53,6 +56,9 @@ func parseConfig(args []string, env func(string) string) (config, error) { flags.StringVar(&rawModes, "modes", string(ModePostgresSQL), "comma-separated execution modes") flags.IntVar(&cfg.Iterations, "iterations", 3, "timed iterations per case") flags.StringVar(&cfg.OutputJSONL, "jsonl-output", "", "JSONL output path (default: stdout)") + flags.StringVar(&cfg.Summary, "summary", "", "markdown summary output path") + flags.StringVar(&cfg.SummaryJSON, "summary-json", "", "JSON summary output path") + flags.StringVar(&cfg.Baseline, "baseline", "", "previous JSONL output for baseline comparison") if err := flags.Parse(args); err != nil { return config{}, err @@ -172,7 +178,25 @@ func main() { } } + if cfg.Baseline != "" { + if err := applyBaseline(cfg.Baseline, records); err != nil { + fatal("compare baseline: %v", err) + } + } + if err := writeJSONLFile(cfg.OutputJSONL, records); err != nil { fatal("write JSONL: %v", err) } + + summary := buildSummary(records) + if cfg.Summary != "" { + if err := writeMarkdownSummaryFile(cfg.Summary, summary); err != nil { + fatal("write markdown summary: %v", err) + } + } + if cfg.SummaryJSON != "" { + if err := writeJSONSummaryFile(cfg.SummaryJSON, summary); err != nil { + fatal("write JSON summary: %v", err) + } + } } diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go index db18a9ba..98384df0 100644 --- a/cmd/graphbench/results.go +++ b/cmd/graphbench/results.go @@ -18,9 +18,11 @@ package main import ( "encoding/json" + "errors" "fmt" "io" "os" + "path/filepath" "sort" "time" @@ -74,10 +76,18 @@ type CaseResult struct { Neo4jPlan *Neo4jPlanNode `json:"neo4j_plan,omitempty"` Neo4jOperators []string `json:"neo4j_operators,omitempty"` Optimization *translate.OptimizationSummary `json:"optimization,omitempty"` + Baseline *BaselineComparison `json:"baseline,omitempty"` FallbackReason string `json:"fallback_reason,omitempty"` Error string `json:"error,omitempty"` } +type BaselineComparison struct { + BaselineMedian time.Duration `json:"baseline_median"` + CurrentMedian time.Duration `json:"current_median"` + Change time.Duration `json:"change"` + Ratio float64 `json:"ratio"` +} + func newCaseResult(testCase ScaleCase, mode ExecutionMode, params map[string]any) CaseResult { return CaseResult{ Source: testCase.Source, @@ -119,6 +129,10 @@ func writeJSONLFile(path string, records []CaseResult) error { return writeJSONL(os.Stdout, records) } + if err := ensureOutputDir(path); err != nil { + return err + } + output, err := os.Create(path) if err != nil { return err @@ -138,3 +152,70 @@ func writeJSONL(w io.Writer, records []CaseResult) error { return nil } + +func readJSONLFile(path string) ([]CaseResult, error) { + input, err := os.Open(path) + if err != nil { + return nil, err + } + defer input.Close() + + decoder := json.NewDecoder(input) + var records []CaseResult + for { + var record CaseResult + if err := decoder.Decode(&record); err != nil { + if errors.Is(err, io.EOF) { + break + } + + return nil, err + } + + records = append(records, record) + } + + return records, nil +} + +func ensureOutputDir(path string) error { + dir := filepath.Dir(path) + if dir == "." || dir == "" { + return nil + } + + return os.MkdirAll(dir, 0o755) +} + +func applyBaseline(path string, records []CaseResult) error { + baseline, err := readJSONLFile(path) + if err != nil { + return err + } + + byKey := make(map[string]CaseResult, len(baseline)) + for _, record := range baseline { + byKey[resultKey(record.Dataset, record.Name, record.ExecutionMode)] = record + } + + for idx := range records { + record := &records[idx] + previous, found := byKey[resultKey(record.Dataset, record.Name, record.ExecutionMode)] + if !found || previous.Stats.Iterations == 0 || record.Stats.Iterations == 0 || previous.Stats.Median == 0 { + continue + } + + record.Baseline = &BaselineComparison{ + BaselineMedian: previous.Stats.Median, + CurrentMedian: record.Stats.Median, + Change: record.Stats.Median - previous.Stats.Median, + Ratio: float64(record.Stats.Median) / float64(previous.Stats.Median), + } + } + + return nil +} + +func resultKey(dataset, name string, mode ExecutionMode) string { + return dataset + "\x00" + name + "\x00" + string(mode) +} diff --git a/cmd/graphbench/summary.go b/cmd/graphbench/summary.go new file mode 100644 index 00000000..89c78972 --- /dev/null +++ b/cmd/graphbench/summary.go @@ -0,0 +1,305 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "sort" + "strings" + "time" +) + +type Summary struct { + GeneratedAt time.Time `json:"generated_at"` + Modes []ModeSummary `json:"modes"` + Cases []CaseSummary `json:"cases"` + Regressions []BaselineEntry `json:"regressions,omitempty"` + Improvements []BaselineEntry `json:"improvements,omitempty"` +} + +type ModeSummary struct { + Mode ExecutionMode `json:"mode"` + Total int `json:"total"` + OK int `json:"ok"` + RowMismatch int `json:"row_mismatch"` + Error int `json:"error"` + NotImplemented int `json:"not_implemented"` +} + +type CaseSummary struct { + Source string `json:"source"` + Dataset string `json:"dataset"` + Name string `json:"name"` + Category string `json:"category"` + Modes map[ExecutionMode]ModeCaseCell `json:"modes"` +} + +type ModeCaseCell struct { + Status string `json:"status"` + Rows int64 `json:"rows,omitempty"` + Median time.Duration `json:"median,omitempty"` + Baseline *BaselineComparison `json:"baseline,omitempty"` + FallbackReason string `json:"fallback_reason,omitempty"` + Error string `json:"error,omitempty"` +} + +type BaselineEntry struct { + Dataset string `json:"dataset"` + Name string `json:"name"` + Mode ExecutionMode `json:"mode"` + BaselineMedian time.Duration `json:"baseline_median"` + CurrentMedian time.Duration `json:"current_median"` + Ratio float64 `json:"ratio"` +} + +func buildSummary(records []CaseResult) Summary { + summary := Summary{ + GeneratedAt: time.Now().UTC(), + } + + modeSummaries := map[ExecutionMode]*ModeSummary{} + caseSummaries := map[string]*CaseSummary{} + + for _, record := range records { + modeSummary := modeSummaries[record.ExecutionMode] + if modeSummary == nil { + modeSummary = &ModeSummary{Mode: record.ExecutionMode} + modeSummaries[record.ExecutionMode] = modeSummary + } + modeSummary.Total++ + + switch record.Status { + case StatusOK: + modeSummary.OK++ + case StatusRowMismatch: + modeSummary.RowMismatch++ + case StatusError: + modeSummary.Error++ + case StatusNotImplemented: + modeSummary.NotImplemented++ + } + + caseKey := record.Source + "\x00" + record.Dataset + "\x00" + record.Name + caseSummary := caseSummaries[caseKey] + if caseSummary == nil { + caseSummary = &CaseSummary{ + Source: record.Source, + Dataset: record.Dataset, + Name: record.Name, + Category: record.Category, + Modes: map[ExecutionMode]ModeCaseCell{}, + } + caseSummaries[caseKey] = caseSummary + } + + caseSummary.Modes[record.ExecutionMode] = ModeCaseCell{ + Status: record.Status, + Rows: record.RowCount, + Median: record.Stats.Median, + Baseline: record.Baseline, + FallbackReason: record.FallbackReason, + Error: record.Error, + } + + if record.Baseline != nil { + entry := BaselineEntry{ + Dataset: record.Dataset, + Name: record.Name, + Mode: record.ExecutionMode, + BaselineMedian: record.Baseline.BaselineMedian, + CurrentMedian: record.Baseline.CurrentMedian, + Ratio: record.Baseline.Ratio, + } + if record.Baseline.Ratio > 1 { + summary.Regressions = append(summary.Regressions, entry) + } else if record.Baseline.Ratio < 1 { + summary.Improvements = append(summary.Improvements, entry) + } + } + } + + for _, modeSummary := range modeSummaries { + summary.Modes = append(summary.Modes, *modeSummary) + } + sort.Slice(summary.Modes, func(i, j int) bool { + return summary.Modes[i].Mode < summary.Modes[j].Mode + }) + + for _, caseSummary := range caseSummaries { + summary.Cases = append(summary.Cases, *caseSummary) + } + sort.Slice(summary.Cases, func(i, j int) bool { + if summary.Cases[i].Dataset != summary.Cases[j].Dataset { + return summary.Cases[i].Dataset < summary.Cases[j].Dataset + } + + return summary.Cases[i].Name < summary.Cases[j].Name + }) + + sortBaselineEntries(summary.Regressions, true) + sortBaselineEntries(summary.Improvements, false) + return summary +} + +func sortBaselineEntries(entries []BaselineEntry, descending bool) { + sort.Slice(entries, func(i, j int) bool { + if descending { + return entries[i].Ratio > entries[j].Ratio + } + + return entries[i].Ratio < entries[j].Ratio + }) +} + +func writeMarkdownSummaryFile(path string, summary Summary) error { + if err := ensureOutputDir(path); err != nil { + return err + } + + output, err := os.Create(path) + if err != nil { + return err + } + defer output.Close() + + return writeMarkdownSummary(output, summary) +} + +func writeJSONSummaryFile(path string, summary Summary) error { + if err := ensureOutputDir(path); err != nil { + return err + } + + output, err := os.Create(path) + if err != nil { + return err + } + defer output.Close() + + encoder := json.NewEncoder(output) + encoder.SetIndent("", " ") + return encoder.Encode(summary) +} + +func writeMarkdownSummary(w io.Writer, summary Summary) error { + fmt.Fprintf(w, "# GraphBench Summary\n\n") + fmt.Fprintf(w, "Generated: %s\n\n", summary.GeneratedAt.Format(time.RFC3339)) + + fmt.Fprintf(w, "## Modes\n\n") + fmt.Fprintf(w, "| Mode | Total | OK | Row Mismatch | Error | Not Implemented |\n") + fmt.Fprintf(w, "| --- | ---: | ---: | ---: | ---: | ---: |\n") + for _, mode := range summary.Modes { + fmt.Fprintf(w, "| %s | %d | %d | %d | %d | %d |\n", + mode.Mode, + mode.Total, + mode.OK, + mode.RowMismatch, + mode.Error, + mode.NotImplemented, + ) + } + + fmt.Fprintf(w, "\n## Cases\n\n") + fmt.Fprintf(w, "| Case | Dataset | Category | postgres_sql | local_traversal | neo4j |\n") + fmt.Fprintf(w, "| --- | --- | --- | --- | --- | --- |\n") + for _, testCase := range summary.Cases { + fmt.Fprintf(w, "| %s | %s | %s | %s | %s | %s |\n", + escapeMarkdown(testCase.Name), + escapeMarkdown(testCase.Dataset), + escapeMarkdown(testCase.Category), + formatModeCell(testCase.Modes[ModePostgresSQL]), + formatModeCell(testCase.Modes[ModeLocalTraversal]), + formatModeCell(testCase.Modes[ModeNeo4j]), + ) + } + + if len(summary.Regressions) > 0 { + fmt.Fprintf(w, "\n## Baseline Regressions\n\n") + writeBaselineTable(w, summary.Regressions) + } + if len(summary.Improvements) > 0 { + fmt.Fprintf(w, "\n## Baseline Improvements\n\n") + writeBaselineTable(w, summary.Improvements) + } + + return nil +} + +func writeBaselineTable(w io.Writer, entries []BaselineEntry) { + fmt.Fprintf(w, "| Case | Dataset | Mode | Baseline | Current | Ratio |\n") + fmt.Fprintf(w, "| --- | --- | --- | ---: | ---: | ---: |\n") + for _, entry := range entries { + fmt.Fprintf(w, "| %s | %s | %s | %s | %s | %.2fx |\n", + escapeMarkdown(entry.Name), + escapeMarkdown(entry.Dataset), + entry.Mode, + formatDuration(entry.BaselineMedian), + formatDuration(entry.CurrentMedian), + entry.Ratio, + ) + } +} + +func formatModeCell(cell ModeCaseCell) string { + if cell.Status == "" { + return "-" + } + + var parts []string + if cell.Median > 0 { + parts = append(parts, formatDuration(cell.Median)) + if cell.Rows > 0 { + parts = append(parts, fmt.Sprintf("rows=%d", cell.Rows)) + } + } else { + parts = append(parts, cell.Status) + } + + if cell.Status != StatusOK && cell.Median > 0 { + parts = append(parts, cell.Status) + } + if cell.Baseline != nil { + parts = append(parts, fmt.Sprintf("%.2fx", cell.Baseline.Ratio)) + } + if cell.FallbackReason != "" { + parts = append(parts, cell.FallbackReason) + } + if cell.Error != "" { + parts = append(parts, cell.Error) + } + + return escapeMarkdown(strings.Join(parts, "; ")) +} + +func formatDuration(duration time.Duration) string { + ms := float64(duration.Microseconds()) / 1000.0 + if ms < 1 { + return fmt.Sprintf("%.2fms", ms) + } + if ms < 100 { + return fmt.Sprintf("%.1fms", ms) + } + + return fmt.Sprintf("%.0fms", ms) +} + +func escapeMarkdown(value string) string { + return strings.ReplaceAll(value, "|", "\\|") +} diff --git a/cmd/graphbench/summary_test.go b/cmd/graphbench/summary_test.go new file mode 100644 index 00000000..8bfe68f8 --- /dev/null +++ b/cmd/graphbench/summary_test.go @@ -0,0 +1,84 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestApplyBaseline(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "baseline.jsonl") + require.NoError(t, writeJSONLFile(path, []CaseResult{{ + Dataset: "base", + Name: "case", + ExecutionMode: ModePostgresSQL, + Stats: DurationStats{ + Iterations: 1, + Median: 10 * time.Millisecond, + }, + }})) + + records := []CaseResult{{ + Dataset: "base", + Name: "case", + ExecutionMode: ModePostgresSQL, + Stats: DurationStats{ + Iterations: 1, + Median: 15 * time.Millisecond, + }, + }} + + require.NoError(t, applyBaseline(path, records)) + require.NotNil(t, records[0].Baseline) + require.Equal(t, 1.5, records[0].Baseline.Ratio) + require.Equal(t, 5*time.Millisecond, records[0].Baseline.Change) +} + +func TestWriteMarkdownSummary(t *testing.T) { + summary := buildSummary([]CaseResult{ + { + Dataset: "base", + Name: "case", + Category: "counts", + ExecutionMode: ModePostgresSQL, + Status: StatusOK, + RowCount: 1, + Stats: DurationStats{ + Iterations: 1, + Median: 2 * time.Millisecond, + }, + }, + { + Dataset: "base", + Name: "case", + Category: "counts", + ExecutionMode: ModeLocalTraversal, + Status: StatusNotImplemented, + FallbackReason: localTraversalUnavailableReason, + }, + }) + + var output bytes.Buffer + require.NoError(t, writeMarkdownSummary(&output, summary)) + require.Contains(t, output.String(), "| case | base | counts | 2.0ms; rows=1 | not_implemented; local traversal executor unavailable | - |") +} From cf6d967a8e9899e3318e4ba39de9756a949568a3 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 17:30:16 -0700 Subject: [PATCH 093/114] docs(graphbench): document AGE reference workflow --- README.md | 5 ++ benchmark/testdata/scale/README.md | 27 +++++++++-- cmd/graphbench/README.md | 74 ++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 cmd/graphbench/README.md diff --git a/README.md b/README.md index 0dc6296c..bd082303 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,11 @@ for later baseline comparison. `CONNECTION_STRING` for one backend or `PG_CONNECTION_STRING` and `NEO4J_CONNECTION_STRING` for both backends, then writes JSONL captures and markdown/JSON summaries under `.coverage/`. +`go run ./cmd/graphbench` captures runtime diagnostics for the scale corpus under `benchmark/testdata/scale`. The +current modes are `postgres_sql`, `local_traversal`, and `neo4j`; AGE is reference-design input only and is not a direct +comparison mode yet. The command can emit JSONL records plus Markdown and JSON summaries, and can compare current timings +against a previous JSONL baseline. + PostgreSQL translates exact string property equality with a JSON string type guard and `properties ->>` extraction, so indexes created on expressions such as `properties ->> 'objectid'` and `properties ->> 'name'` can be used for selective anchors without matching JSON booleans or numbers. Simple relationship count fast paths depend on the schema's diff --git a/benchmark/testdata/scale/README.md b/benchmark/testdata/scale/README.md index 487ce204..85c2f788 100644 --- a/benchmark/testdata/scale/README.md +++ b/benchmark/testdata/scale/README.md @@ -1,9 +1,28 @@ # GraphBench Scale Corpus This corpus measures graph workload shapes, not general Cypher correctness. -The shared integration corpus remains the source of backend-equivalent semantic coverage. +The shared integration corpus remains the source of backend-equivalent semantic +coverage. -Cases declare the values a query observes so benchmark reports can separate ID-only work from node, relationship, property, and path materialization. -Initial execution modes are `postgres_sql`, `local_traversal`, and `neo4j`. -Apache AGE is intentionally not a benchmark mode here; it may appear only in `reference_design` notes as input for DAWGS design choices. +Cases declare the values a query observes so benchmark reports can separate +ID-only work from node, relationship, property, and path materialization. +Current execution modes are `postgres_sql`, `local_traversal`, and `neo4j`. +Apache AGE is intentionally not a benchmark mode here; it may appear only in +`reference_design` notes as input for DAWGS design choices. +Each JSON file contains a list of scale cases with: + +- `source`: the source corpus or workload family. +- `dataset`: the fixture dataset to load from `integration/testdata`. +- `name` and `category`: stable identifiers used in reports. +- `cypher`: the Cypher query under test. +- `parameters`: named parameter values. +- `expected_rows`: the expected result cardinality. +- `observes`: whether the query observes paths, nodes, relationships, + properties, or only IDs internally. +- `candidate_modes`: the execution modes that should attempt the case. +- `reference_design`: optional design notes, including AGE observations when + useful. + +Use `cmd/graphbench` to run this corpus and produce JSONL, Markdown, and JSON +summaries. diff --git a/cmd/graphbench/README.md b/cmd/graphbench/README.md new file mode 100644 index 00000000..ac530326 --- /dev/null +++ b/cmd/graphbench/README.md @@ -0,0 +1,74 @@ +# GraphBench + +`graphbench` runs the scale benchmark corpus under `benchmark/testdata/scale`. +It is meant for runtime gap accounting: query duration, returned row counts, +PostgreSQL plan details, Neo4j plan operators, fallback reasons, and comparison +summaries. + +The current execution modes are: + +- `postgres_sql`: runs DAWGS' PostgreSQL SQL translation against a PostgreSQL database. +- `local_traversal`: records explicit `not_implemented` placeholders until the local traversal executor lands. +- `neo4j`: runs the same corpus against Neo4j through the DAWGS Neo4j backend. + +Apache AGE is not an execution mode in this harness yet. AGE behavior can be +captured in corpus `reference_design` notes so DAWGS can use it as design input +without treating it as a direct benchmark comparison. + +## Inputs + +The command loads cases from `benchmark/testdata/scale` by default and imports +the fixture datasets from `integration/testdata`. + +Connection strings can be supplied as flags or environment variables: + +- PostgreSQL: `-pg-connection`, `PG_CONNECTION_STRING`, `-connection`, or `CONNECTION_STRING`. +- Neo4j: `-neo4j-connection`, `NEO4J_CONNECTION_STRING`, `-connection`, or `CONNECTION_STRING`. + +## Examples + +Run only PostgreSQL SQL translation: + +```bash +go run ./cmd/graphbench \ + -modes postgres_sql \ + -pg-connection "$PG_CONNECTION_STRING" \ + -jsonl-output .coverage/graphbench-postgres.jsonl \ + -summary .coverage/graphbench-postgres.md \ + -summary-json .coverage/graphbench-postgres.json +``` + +Capture PostgreSQL, local traversal placeholders, and Neo4j in one report: + +```bash +go run ./cmd/graphbench \ + -modes postgres_sql,local_traversal,neo4j \ + -pg-connection "$PG_CONNECTION_STRING" \ + -neo4j-connection "$NEO4J_CONNECTION_STRING" \ + -jsonl-output .coverage/graphbench.jsonl \ + -summary .coverage/graphbench.md \ + -summary-json .coverage/graphbench.json +``` + +Compare a run against a previous JSONL capture: + +```bash +go run ./cmd/graphbench \ + -modes postgres_sql,neo4j \ + -pg-connection "$PG_CONNECTION_STRING" \ + -neo4j-connection "$NEO4J_CONNECTION_STRING" \ + -baseline .coverage/graphbench-baseline.jsonl \ + -jsonl-output .coverage/graphbench.jsonl \ + -summary .coverage/graphbench.md +``` + +## Outputs + +JSONL output contains one `CaseResult` record per case and execution mode. +Markdown and JSON summaries aggregate mode status counts, per-case timings, row +counts, fallback reasons, and baseline regressions or improvements when a +baseline capture is supplied. + +PostgreSQL records include translated SQL and `EXPLAIN (ANALYZE, BUFFERS, +TIMING OFF, FORMAT JSON)` metrics. Neo4j records include plan operator names +when an `EXPLAIN` plan can be captured. From 90c82fad23913a2c64160212642e1db1801ffa5c Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 18:30:23 -0700 Subject: [PATCH 094/114] fix(build): repair optimizer build --- cmd/graphbench/postgres.go | 8 ++++++-- cmd/plancorpus/capture.go | 8 ++++++-- go.mod | 5 ----- go.sum | 7 ++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/graphbench/postgres.go b/cmd/graphbench/postgres.go index 7e6dfb0b..2ea47784 100644 --- a/cmd/graphbench/postgres.go +++ b/cmd/graphbench/postgres.go @@ -23,10 +23,10 @@ import ( "strconv" "strings" + "github.com/jackc/pgx/v5/pgxpool" "github.com/specterops/dawgs" "github.com/specterops/dawgs/cypher/frontend" "github.com/specterops/dawgs/cypher/models/pgsql/translate" - "github.com/specterops/dawgs/drivers" "github.com/specterops/dawgs/drivers/pg" "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/opengraph" @@ -41,7 +41,11 @@ type postgresSQLRunner struct { } func newPostgresSQLRunner(ctx context.Context, datasetDir, connection string, corpus ScaleCorpus) (*postgresSQLRunner, error) { - pool, err := pg.NewPool(drivers.DatabaseConfiguration{Connection: connection}) + poolCfg, err := pgxpool.ParseConfig(connection) + if err != nil { + return nil, fmt.Errorf("parse PostgreSQL pool configuration: %w", err) + } + pool, err := pg.NewPool(poolCfg) if err != nil { return nil, fmt.Errorf("create PostgreSQL pool: %w", err) } diff --git a/cmd/plancorpus/capture.go b/cmd/plancorpus/capture.go index 4b746e51..ffb55c31 100644 --- a/cmd/plancorpus/capture.go +++ b/cmd/plancorpus/capture.go @@ -9,12 +9,12 @@ import ( "sort" "strings" + "github.com/jackc/pgx/v5/pgxpool" neo4jcore "github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/specterops/dawgs" "github.com/specterops/dawgs/cypher/frontend" "github.com/specterops/dawgs/cypher/models/pgsql/optimize" "github.com/specterops/dawgs/cypher/models/pgsql/translate" - "github.com/specterops/dawgs/drivers" "github.com/specterops/dawgs/drivers/neo4j" "github.com/specterops/dawgs/drivers/pg" "github.com/specterops/dawgs/graph" @@ -165,7 +165,11 @@ func openBackend(ctx context.Context, suite corpus, spec captureSpec) (*backendC } if spec.DriverName == pg.DriverName { - pool, err := pg.NewPool(drivers.DatabaseConfiguration{Connection: spec.Connection}) + poolCfg, err := pgxpool.ParseConfig(spec.Connection) + if err != nil { + return nil, fmt.Errorf("parse PostgreSQL pool configuration: %w", err) + } + pool, err := pg.NewPool(poolCfg) if err != nil { return nil, fmt.Errorf("create PostgreSQL pool: %w", err) } diff --git a/go.mod b/go.mod index d507391d..8d3a5014 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/fzipp/gocyclo v0.6.0 github.com/gammazero/deque v1.2.1 github.com/jackc/pgtype v1.14.4 - github.com/jackc/pgx/v4 v4.18.2 github.com/jackc/pgx/v5 v5.9.2 github.com/neo4j/neo4j-go-driver/v5 v5.28.4 github.com/stretchr/testify v1.11.1 @@ -125,13 +124,9 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle v1.3.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect diff --git a/go.sum b/go.sum index 1c7c760d..40995d1e 100644 --- a/go.sum +++ b/go.sum @@ -110,7 +110,6 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.2 h1:R1VaDQkMR321HBM6+6b2eYZfxi0ybPJgUh0Ztr7twzU= github.com/cockroachdb/apd/v3 v3.2.2/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -196,7 +195,6 @@ github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5W github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= @@ -259,6 +257,7 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -275,10 +274,10 @@ github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= @@ -310,7 +309,6 @@ github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= @@ -496,7 +494,6 @@ github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08/go.mod h1:+XL github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= From a2300290d315577220c497de3a9291e771532cb5 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 18:38:42 -0700 Subject: [PATCH 095/114] fix(benchmark): harden resource handling --- cmd/graphbench/measure.go | 12 ++++++++- cmd/graphbench/neo4j.go | 28 ++++++++++--------- cmd/graphbench/results.go | 21 +++++++++------ cmd/graphbench/results_test.go | 49 ++++++++++++++++++++++++++++++++++ cmd/plancorpus/capture.go | 3 +++ 5 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 cmd/graphbench/results_test.go diff --git a/cmd/graphbench/measure.go b/cmd/graphbench/measure.go index b41f91ae..7aaa7a93 100644 --- a/cmd/graphbench/measure.go +++ b/cmd/graphbench/measure.go @@ -18,6 +18,7 @@ package main import ( "context" + "fmt" "time" "github.com/specterops/dawgs/graph" @@ -36,6 +37,10 @@ func countCypherRows(tx graph.Transaction, cypher string, params map[string]any) } func measureCypher(ctx context.Context, db graph.Database, cypher string, params map[string]any, iterations int) (int64, DurationStats, error) { + if iterations < 1 { + return 0, DurationStats{}, fmt.Errorf("iterations must be at least 1") + } + var warmupRows int64 if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { var err error @@ -57,5 +62,10 @@ func measureCypher(ctx context.Context, db graph.Database, cypher string, params durations[idx] = time.Since(start) } - return warmupRows, computeDurationStats(durations), nil + stats, err := computeDurationStats(durations) + if err != nil { + return 0, DurationStats{}, err + } + + return warmupRows, stats, nil } diff --git a/cmd/graphbench/neo4j.go b/cmd/graphbench/neo4j.go index 9c720eed..1756c7a3 100644 --- a/cmd/graphbench/neo4j.go +++ b/cmd/graphbench/neo4j.go @@ -33,7 +33,7 @@ import ( type neo4jRunner struct { datasetDir string db graph.Database - planDriver neo4jcore.Driver + planDriver neo4jcore.DriverWithContext databaseName string } @@ -74,7 +74,7 @@ func newNeo4jRunner(ctx context.Context, datasetDir, connection string, corpus S func (s *neo4jRunner) Close(ctx context.Context) error { var closeErr error if s.planDriver != nil { - closeErr = s.planDriver.Close() + closeErr = s.planDriver.Close(ctx) } if s.db != nil { if err := s.db.Close(ctx); err != nil && closeErr == nil { @@ -132,7 +132,7 @@ func (s *neo4jRunner) runCase(ctx context.Context, iterations int, testCase Scal record.Stats = stats applyRowExpectation(&record) - plan, operators, err := s.explain(testCase.Cypher, params) + plan, operators, err := s.explain(ctx, testCase.Cypher, params) if err != nil { if record.Status == StatusOK { record.Status = StatusError @@ -146,19 +146,23 @@ func (s *neo4jRunner) runCase(ctx context.Context, iterations int, testCase Scal return record } -func (s *neo4jRunner) explain(cypherQuery string, params map[string]any) (*Neo4jPlanNode, []string, error) { - session := s.planDriver.NewSession(neo4jcore.SessionConfig{ +func (s *neo4jRunner) explain(ctx context.Context, cypherQuery string, params map[string]any) (plan *Neo4jPlanNode, operators []string, err error) { + session := s.planDriver.NewSession(ctx, neo4jcore.SessionConfig{ AccessMode: neo4jcore.AccessModeRead, DatabaseName: s.databaseName, }) - defer session.Close() + defer func() { + if closeErr := session.Close(ctx); err == nil && closeErr != nil { + err = closeErr + } + }() - result, err := session.Run("EXPLAIN "+cypherWithoutTerminator(cypherQuery), params) + result, err := session.Run(ctx, "EXPLAIN "+cypherWithoutTerminator(cypherQuery), params) if err != nil { return nil, nil, err } - summary, err := result.Consume() + summary, err := result.Consume(ctx) if err != nil { return nil, nil, err } @@ -166,8 +170,8 @@ func (s *neo4jRunner) explain(cypherQuery string, params map[string]any) (*Neo4j return nil, nil, nil } - plan := convertNeo4jPlan(summary.Plan()) - return &plan, neo4jOperators(plan), nil + planNode := convertNeo4jPlan(summary.Plan()) + return &planNode, neo4jOperators(planNode), nil } type neo4jPlanDriverConfig struct { @@ -229,13 +233,13 @@ func neo4jDatabaseName(connectionURL *url.URL) (string, error) { return databaseName, nil } -func openNeo4jPlanDriver(connStr string) (neo4jcore.Driver, string, error) { +func openNeo4jPlanDriver(connStr string) (neo4jcore.DriverWithContext, string, error) { cfg, err := parseNeo4jPlanDriverConfig(connStr) if err != nil { return nil, "", err } - driver, err := neo4jcore.NewDriver(cfg.Target, neo4jcore.BasicAuth(cfg.Username, cfg.Password, "")) + driver, err := neo4jcore.NewDriverWithContext(cfg.Target, neo4jcore.BasicAuth(cfg.Username, cfg.Password, "")) if err != nil { return nil, "", err } diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go index 98384df0..27d16093 100644 --- a/cmd/graphbench/results.go +++ b/cmd/graphbench/results.go @@ -103,18 +103,23 @@ func newCaseResult(testCase ScaleCase, mode ExecutionMode, params map[string]any } } -func computeDurationStats(durations []time.Duration) DurationStats { - sort.Slice(durations, func(i, j int) bool { - return durations[i] < durations[j] +func computeDurationStats(durations []time.Duration) (DurationStats, error) { + if len(durations) == 0 { + return DurationStats{}, fmt.Errorf("duration stats require at least one duration") + } + + sortedDurations := append([]time.Duration(nil), durations...) + sort.Slice(sortedDurations, func(i, j int) bool { + return sortedDurations[i] < sortedDurations[j] }) - n := len(durations) + n := len(sortedDurations) return DurationStats{ Iterations: n, - Median: durations[n/2], - P95: durations[n*95/100], - Max: durations[n-1], - } + Median: sortedDurations[n/2], + P95: sortedDurations[min(n*95/100, n-1)], + Max: sortedDurations[n-1], + }, nil } func applyRowExpectation(result *CaseResult) { diff --git a/cmd/graphbench/results_test.go b/cmd/graphbench/results_test.go new file mode 100644 index 00000000..11671641 --- /dev/null +++ b/cmd/graphbench/results_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestComputeDurationStatsRejectsEmptyDurations(t *testing.T) { + _, err := computeDurationStats(nil) + + require.ErrorContains(t, err, "at least one duration") +} + +func TestComputeDurationStatsCopiesAndSortsDurations(t *testing.T) { + durations := []time.Duration{ + 30 * time.Millisecond, + 10 * time.Millisecond, + 20 * time.Millisecond, + } + + stats, err := computeDurationStats(durations) + + require.NoError(t, err) + require.Equal(t, 3, stats.Iterations) + require.Equal(t, 20*time.Millisecond, stats.Median) + require.Equal(t, 30*time.Millisecond, stats.P95) + require.Equal(t, 30*time.Millisecond, stats.Max) + require.Equal(t, 30*time.Millisecond, durations[0]) + require.Equal(t, 10*time.Millisecond, durations[1]) + require.Equal(t, 20*time.Millisecond, durations[2]) +} diff --git a/cmd/plancorpus/capture.go b/cmd/plancorpus/capture.go index ffb55c31..6f5c5f0d 100644 --- a/cmd/plancorpus/capture.go +++ b/cmd/plancorpus/capture.go @@ -178,6 +178,9 @@ func openBackend(ctx context.Context, suite corpus, spec captureSpec) (*backendC db, err := dawgs.Open(ctx, spec.DriverName, cfg) if err != nil { + if cfg.Pool != nil { + cfg.Pool.Close() + } return nil, fmt.Errorf("open %s database: %w", spec.DriverName, err) } From 73a1e0085609c4eacbcd3ef17a49c0aeb8d92ec6 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 18:53:58 -0700 Subject: [PATCH 096/114] fix(pgsql): address optimizer and translation edge cases --- cypher/models/pgsql/optimize/lowering_plan.go | 62 ++++++++++++++++++- .../models/pgsql/optimize/optimizer_test.go | 27 ++++++++ .../pgsql/test/translation_cases/nodes.sql | 2 +- cypher/models/pgsql/translate/expansion.go | 49 ++++++++++----- cypher/models/pgsql/translate/expression.go | 24 +++++-- .../models/pgsql/translate/expression_test.go | 20 ++++++ cypher/models/pgsql/translate/hinting.go | 12 +++- .../pgsql/translate/optimizer_safety_test.go | 37 +++++++++-- 8 files changed, 203 insertions(+), 30 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 5c3b5556..6aba76ad 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -58,7 +58,12 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic return LoweringPlan{}, err } - carriedSymbols, carriedSelectivity = carryProjectionSelectivity(part.With.Projection, carriedSelectivity) + currentSymbols := copyStringSet(carriedSymbols) + currentSelectivity := copyBoundSourceSelectivity(carriedSelectivity) + declareReadingClauseSymbols(currentSymbols, part.ReadingClauses) + declareReadingClauseSelectivity(currentSelectivity, part.ReadingClauses) + + carriedSymbols, carriedSelectivity = carryProjectionSelectivity(part.With.Projection, currentSymbols, currentSelectivity) } if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { @@ -435,7 +440,11 @@ func copyBoundSourceSelectivity(values map[string]boundSourceSelectivity) map[st return copied } -func carryProjectionSelectivity(projection *cypher.Projection, incoming map[string]boundSourceSelectivity) (map[string]struct{}, map[string]boundSourceSelectivity) { +func carryProjectionSelectivity( + projection *cypher.Projection, + incomingSymbols map[string]struct{}, + incomingSelectivity map[string]boundSourceSelectivity, +) (map[string]struct{}, map[string]boundSourceSelectivity) { carriedSymbols := map[string]struct{}{} carriedSelectivity := map[string]boundSourceSelectivity{} @@ -444,20 +453,51 @@ func carryProjectionSelectivity(projection *cypher.Projection, incoming map[stri } projectionSelectivity := projectionCardinalitySelectivity(projection) + if projectionCarriesAllSymbols(projection) { + for symbol := range incomingSymbols { + addSymbol(carriedSymbols, symbol) + mergeBoundSourceSelectivity(carriedSelectivity, symbol, incomingSelectivity[symbol]) + mergeBoundSourceSelectivity(carriedSelectivity, symbol, projectionSelectivity) + } + } + for _, item := range projection.Items { symbol, alias, ok := projectionItemVariableSymbolAndAlias(item) if !ok { continue } + if symbol == cypher.TokenLiteralAsterisk { + continue + } addSymbol(carriedSymbols, alias) - mergeBoundSourceSelectivity(carriedSelectivity, alias, incoming[symbol]) + mergeBoundSourceSelectivity(carriedSelectivity, alias, incomingSelectivity[symbol]) mergeBoundSourceSelectivity(carriedSelectivity, alias, projectionSelectivity) } return carriedSymbols, carriedSelectivity } +func projectionCarriesAllSymbols(projection *cypher.Projection) bool { + if projection == nil { + return false + } + if projection.All || len(projection.Items) == 0 { + return true + } + + for _, item := range projection.Items { + if symbol, _, ok := projectionItemVariableSymbolAndAlias(item); ok && symbol == cypher.TokenLiteralAsterisk { + return true + } + if symbol, ok := expressionVariableSymbol(item); ok && symbol == cypher.TokenLiteralAsterisk { + return true + } + } + + return false +} + func projectionCardinalitySelectivity(projection *cypher.Projection) boundSourceSelectivity { if projection == nil || projection.Limit == nil { return boundSourceSelectivityNone @@ -531,6 +571,22 @@ func declareSelectiveMatchSymbols(symbols map[string]boundSourceSelectivity, mat } } +func declareReadingClauseSymbols(symbols map[string]struct{}, readingClauses []*cypher.ReadingClause) { + for _, readingClause := range readingClauses { + if readingClause != nil { + declareMatchSymbols(symbols, readingClause.Match) + } + } +} + +func declareReadingClauseSelectivity(symbols map[string]boundSourceSelectivity, readingClauses []*cypher.ReadingClause) { + for _, readingClause := range readingClauses { + if readingClause != nil { + declareSelectiveMatchSymbols(symbols, readingClause.Match) + } + } +} + func nodePatternsForPattern(patternPart *cypher.PatternPart) []*cypher.NodePattern { if patternPart == nil { return nil diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 3a31ace8..4ed3ec0f 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -781,6 +781,33 @@ func TestLoweringPlanSkipsBoundLeftDirectionAfterPriorLimit(t *testing.T) { }}, plan.LoweringPlan.TraversalDirection) } +func TestLoweringPlanSkipsBoundLeftDirectionAfterGreedyProjectionLimit(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User) + WHERE u.hasspn = true + WITH * + LIMIT 10 + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {name: 'target'}) + RETURN c + `) + require.NoError(t, err) + + plan, err := Optimize(regularQuery) + require.NoError(t, err) + require.Contains(t, plan.LoweringPlan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Equal(t, []TraversalDirectionDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 1, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Reason: traversalDirectionReasonBoundSourceSelective, + }}, plan.LoweringPlan.TraversalDirection) +} + func TestLoweringPlanAllowsUniqueRightEndpointAfterPriorLimit(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/test/translation_cases/nodes.sql b/cypher/models/pgsql/test/translation_cases/nodes.sql index 1fa6e1c8..8565f7b4 100644 --- a/cypher/models/pgsql/test/translation_cases/nodes.sql +++ b/cypher/models/pgsql/test/translation_cases/nodes.sql @@ -220,7 +220,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on (jsonb_typeof((n1.properties -> 'name')) = 'string' and (n1.properties ->> 'name') = 'n3') and n1.id = e0.start_id where (jsonb_typeof((e0.properties -> 'prop')) = 'string' and (e0.properties ->> 'prop') = 'a') and (s0.n0).id = e0.end_id) select count(*) > 0 from s1)); -- case: match (n:NodeKind1) where n.distinguishedname = toUpper('admin') return n -with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ->> 'distinguishedname') = upper('admin')::text) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((jsonb_typeof((n0.properties -> 'distinguishedname')) = 'string' and (n0.properties ->> 'distinguishedname') = upper('admin')::text)) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; -- case: match (n:NodeKind1) where n.distinguishedname starts with toUpper('admin') return n with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (cypher_starts_with((n0.properties ->> 'distinguishedname'), (upper('admin')::text)::text)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0; diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 32c3ffcb..5cfee834 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -250,17 +250,20 @@ func (s *ExpansionBuilder) seedEndpointConstraintSplit(expression pgsql.Expressi return partitionConstraintByLocality(seedExpression, localScope) } -func (s *ExpansionBuilder) appendUnwindSourcesIfReferenced(selectBody *pgsql.Select, expression pgsql.Expression) error { - if referencesUnwind, err := expressionReferencesUnwindBinding(expression, s.unwindClauses); err != nil { - return err - } else if referencesUnwind { - var previousFrame *Frame - if s.traversalStep != nil && s.traversalStep.Frame != nil { - previousFrame = s.traversalStep.Frame.Previous - } +func (s *ExpansionBuilder) appendUnwindSourcesIfReferenced(selectBody *pgsql.Select, expressions ...pgsql.Expression) error { + for _, expression := range expressions { + if referencesUnwind, err := expressionReferencesUnwindBinding(expression, s.unwindClauses); err != nil { + return err + } else if referencesUnwind { + var previousFrame *Frame + if s.traversalStep != nil && s.traversalStep.Frame != nil { + previousFrame = s.traversalStep.Frame.Previous + } - selectBody.From = prependFrameSourceIfMissing(selectBody.From, previousFrame) - selectBody.From = append(selectBody.From, s.unwindSources...) + selectBody.From = prependFrameSourceIfMissing(selectBody.From, previousFrame) + selectBody.From = append(selectBody.From, s.unwindSources...) + return nil + } } return nil @@ -1062,6 +1065,16 @@ func (s *ExpansionBuilder) forwardTerminalSatisfaction(expansionModel *Expansion return satisfiedSelectItem } +func forwardTerminalSatisfactionProjection(expansionModel *Expansion) pgsql.Expression { + if expansionModel.TerminalNodeSatisfactionProjection != nil && + !expansionModel.UseMaterializedTerminalFilter && + !expansionModel.UseMaterializedEndpointPairFilter { + return pgsql.Expression(expansionModel.TerminalNodeSatisfactionProjection) + } + + return nil +} + func backwardContinuationSatisfaction(expansionModel *Expansion) pgsql.Expression { return pgsql.ExistsExpression{ Subquery: pgsql.Subquery{ @@ -1105,6 +1118,14 @@ func (s *ExpansionBuilder) backwardTerminalSatisfaction(expansionModel *Expansio return satisfiedSelectItem } +func backwardTerminalSatisfactionProjection(expansionModel *Expansion) pgsql.Expression { + if expansionModel.PrimerNodeSatisfactionProjection != nil && !expansionModel.UseMaterializedEndpointPairFilter { + return pgsql.Expression(expansionModel.PrimerNodeSatisfactionProjection) + } + + return nil +} + func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expansion) (pgsql.Query, pgsql.Expression, error) { var ( primerSeedConstraints pgsql.Expression @@ -1190,7 +1211,7 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints, forwardTerminalSatisfactionProjection(expansionModel)); err != nil { return pgsql.Query{}, nil, err } @@ -1284,7 +1305,7 @@ func (s *ExpansionBuilder) prepareForwardFrontRecursiveQuery(expansionModel *Exp } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints, forwardTerminalSatisfactionProjection(expansionModel)); err != nil { return pgsql.Select{}, err } @@ -1373,7 +1394,7 @@ func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expan } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints, backwardTerminalSatisfactionProjection(expansionModel)); err != nil { return pgsql.Query{}, nil, err } @@ -1445,7 +1466,7 @@ func (s *ExpansionBuilder) prepareBackwardFrontRecursiveQuery(expansionModel *Ex } nextQuery.From = []pgsql.FromClause{nextQueryFrom} - if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints); err != nil { + if err := s.appendUnwindSourcesIfReferenced(&nextQuery, expansionModel.EdgeConstraints, backwardTerminalSatisfactionProjection(expansionModel)); err != nil { return pgsql.Select{}, err } diff --git a/cypher/models/pgsql/translate/expression.go b/cypher/models/pgsql/translate/expression.go index 883c144e..51ee2078 100644 --- a/cypher/models/pgsql/translate/expression.go +++ b/cypher/models/pgsql/translate/expression.go @@ -452,6 +452,8 @@ func (s *Builder) PopOperand(kindMapper *contextAwareKindMapper) (pgsql.Expressi case *pgsql.BinaryExpression: if err := applyBinaryExpressionTypeHints(kindMapper, typedNext); err != nil { return nil, err + } else if rewrittenExpression, rewritten := buildStringPropertyEqualityPredicate(typedNext); rewritten { + next = rewrittenExpression } } @@ -925,15 +927,17 @@ func buildStringPropertyEqualityPredicate(expression *pgsql.BinaryExpression) (p leftPropertyLookup, hasLeftPropertyLookup := expressionToPropertyLookupBinaryExpression(expression.LOperand) rightPropertyLookup, hasRightPropertyLookup := expressionToPropertyLookupBinaryExpression(expression.ROperand) - if hasLeftPropertyLookup && leftPropertyLookup.Operator == pgsql.OperatorJSONTextField { - if _, rewritten := rewriteStringEqualityOperand(expression.ROperand); rewritten { - return buildStringPropertyComparisonPredicate(leftPropertyLookup, expression.ROperand, true, expression.Operator), true + if hasLeftPropertyLookup { + if rewrittenROperand, rewritten := rewriteStringEqualityOperand(expression.ROperand); rewritten { + rewritePropertyLookupOperator(leftPropertyLookup, pgsql.Text) + return buildStringPropertyComparisonPredicate(leftPropertyLookup, rewrittenROperand, true, expression.Operator), true } } - if hasRightPropertyLookup && rightPropertyLookup.Operator == pgsql.OperatorJSONTextField { - if _, rewritten := rewriteStringEqualityOperand(expression.LOperand); rewritten { - return buildStringPropertyComparisonPredicate(rightPropertyLookup, expression.LOperand, false, expression.Operator), true + if hasRightPropertyLookup { + if rewrittenLOperand, rewritten := rewriteStringEqualityOperand(expression.LOperand); rewritten { + rewritePropertyLookupOperator(rightPropertyLookup, pgsql.Text) + return buildStringPropertyComparisonPredicate(rightPropertyLookup, rewrittenLOperand, false, expression.Operator), true } } @@ -1284,6 +1288,10 @@ func (s *ExpressionTreeTranslator) rewriteBinaryExpression(newExpression *pgsql. s.PushOperand(newExpression) case pgsql.OperatorEquals: + if err := applyBinaryExpressionTypeHints(s.kindMapper, newExpression); err != nil { + return err + } + if propertyLookup, hasEmptyArrayLiteralPropertyComparison := isEmptyArrayLiteralPropertyComparison(newExpression); hasEmptyArrayLiteralPropertyComparison { expandedExpression := buildEmptyArrayPropertyComparison(propertyLookup, false) @@ -1299,6 +1307,10 @@ func (s *ExpressionTreeTranslator) rewriteBinaryExpression(newExpression *pgsql. } case pgsql.OperatorCypherNotEquals: + if err := applyBinaryExpressionTypeHints(s.kindMapper, newExpression); err != nil { + return err + } + if propertyLookup, hasEmptyArrayLiteralPropertyComparison := isEmptyArrayLiteralPropertyComparison(newExpression); hasEmptyArrayLiteralPropertyComparison { expandedExpression := buildEmptyArrayPropertyComparison(propertyLookup, true) diff --git a/cypher/models/pgsql/translate/expression_test.go b/cypher/models/pgsql/translate/expression_test.go index 4127490c..807821e3 100644 --- a/cypher/models/pgsql/translate/expression_test.go +++ b/cypher/models/pgsql/translate/expression_test.go @@ -348,6 +348,26 @@ func TestPropertyLookupEqualityScalarRewrites(t *testing.T) { Operator: pgsql.OperatorEquals, ROperand: pgsql.Parameter{Identifier: "pi0", CastType: pgsql.Text}, Expected: "(jsonb_typeof((n.properties -> 'objectid')) = 'string' and (n.properties ->> 'objectid') = @pi0::text)", + }, { + Name: "text function uses typed text property lookup", + LOperand: propertyLookup("distinguishedname"), + Operator: pgsql.OperatorEquals, + ROperand: pgsql.FunctionCall{ + Function: pgsql.FunctionToUpper, + Parameters: []pgsql.Expression{mustAsLiteral("admin")}, + CastType: pgsql.Text, + }, + Expected: "(jsonb_typeof((n.properties -> 'distinguishedname')) = 'string' and (n.properties ->> 'distinguishedname') = upper('admin')::text)", + }, { + Name: "text function uses typed text property lookup when reversed", + LOperand: pgsql.FunctionCall{ + Function: pgsql.FunctionToUpper, + Parameters: []pgsql.Expression{mustAsLiteral("admin")}, + CastType: pgsql.Text, + }, + Operator: pgsql.OperatorEquals, + ROperand: propertyLookup("distinguishedname"), + Expected: "(jsonb_typeof((n.properties -> 'distinguishedname')) = 'string' and upper('admin')::text = (n.properties ->> 'distinguishedname'))", }, { Name: "string inequality keeps non-string JSONB branch", LOperand: propertyLookup("rank"), diff --git a/cypher/models/pgsql/translate/hinting.go b/cypher/models/pgsql/translate/hinting.go index ee743442..5d837c4e 100644 --- a/cypher/models/pgsql/translate/hinting.go +++ b/cypher/models/pgsql/translate/hinting.go @@ -417,7 +417,11 @@ func applyTypeFunctionLikeTypeHints(kindMapper *contextAwareKindMapper, expressi typedROperand.CastType = lOperandTypeHint expression.ROperand = typedROperand } else if !lOperandTypeHint.IsKnown() { - expression.LOperand = pgsql.NewTypeCast(expression.LOperand, typedROperand.CastType.ArrayBaseType()) + if propertyLookup, isPropertyLookup := expressionToPropertyLookupBinaryExpression(expression.LOperand); isPropertyLookup && typedROperand.CastType == pgsql.Text { + expression.LOperand = rewritePropertyLookupOperator(propertyLookup, pgsql.Text) + } else { + expression.LOperand = pgsql.NewTypeCast(expression.LOperand, typedROperand.CastType.ArrayBaseType()) + } } else if pgsql.OperatorIsComparator(expression.Operator) && !typedROperand.CastType.IsComparable(lOperandTypeHint, expression.Operator) { return newFunctionCallComparatorError(typedROperand, expression.Operator, lOperandTypeHint) } @@ -439,5 +443,9 @@ func applyBinaryExpressionTypeHints(kindMapper *contextAwareKindMapper, expressi return err } - return applyTypeFunctionLikeTypeHints(kindMapper, expression) + if err := applyTypeFunctionLikeTypeHints(kindMapper, expression); err != nil { + return err + } + + return nil } diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index 6f321294..aaae2fd5 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -118,6 +118,18 @@ func requireNoPlannedOptimizationLowering(t *testing.T, summary OptimizationSumm } } +func requirePlanParameterContains(t *testing.T, translation Result, expected string) { + t.Helper() + + for _, parameter := range translation.Parameters { + if planQuery, ok := parameter.(string); ok && strings.Contains(planQuery, expected) { + return + } + } + + require.Failf(t, "missing plan parameter content", "expected a plan parameter to contain %q in %#v", expected, translation.Parameters) +} + func requireSkippedOptimizationLowering(t *testing.T, summary OptimizationSummary, name string, reason string) { t.Helper() @@ -976,13 +988,30 @@ func TestOptimizerSafetyShortestPathRootCarriesUnwindSources(t *testing.T) { formattedQuery, err := Translated(translation) require.NoError(t, err) normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") - primerQuery, hasPrimerQuery := translation.Parameters["pi0"].(string) - require.True(t, hasPrimerQuery) require.Contains(t, normalizedQuery, "unidirectional_sp_harness") require.Contains(t, normalizedQuery, "unnest(array ['source']::text[]) as i0") - require.Contains(t, primerQuery, "jsonb_typeof((n1.properties -> 'name')) = 'string'") - require.Contains(t, primerQuery, "(n0.properties ->> 'name') = i0") + requirePlanParameterContains(t, translation, "jsonb_typeof((n1.properties -> 'name')) = 'string'") + requirePlanParameterContains(t, translation, "(n0.properties ->> 'name') = i0") +} + +func TestOptimizerSafetyShortestPathTerminalCarriesUnwindSources(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslation(t, ` + UNWIND ['target'] AS targetName + MATCH p = shortestPath((s:Group)-[:MemberOf*1..]->(e:Group)) + WHERE s.name = 'source' AND e.name = targetName + RETURN targetName, p + `) + + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") + + require.Contains(t, normalizedQuery, "unidirectional_sp_harness") + require.Contains(t, normalizedQuery, "unnest(array ['target']::text[]) as i0") + requirePlanParameterContains(t, translation, "(n1.properties ->> 'name') = i0") } func TestOptimizerSafetyTranslationReportsOptimizerMetadata(t *testing.T) { From a238385659c53fb4cdd2f99830251f9aac6bbb59 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 18:55:11 -0700 Subject: [PATCH 097/114] fix(neo4j): harden database parsing and plan assertions --- cmd/graphbench/neo4j.go | 3 +++ cmd/graphbench/neo4j_test.go | 15 ++++++++++----- cmd/plancorpus/capture.go | 3 +++ cmd/plancorpus/main_test.go | 9 +++++++-- drivers/neo4j/neo4j.go | 3 +++ drivers/neo4j/neo4j_internal_test.go | 15 ++++++++++----- .../pgsql_aggregate_traversal_plan_test.go | 5 +++-- 7 files changed, 39 insertions(+), 14 deletions(-) diff --git a/cmd/graphbench/neo4j.go b/cmd/graphbench/neo4j.go index 1756c7a3..6b1d2bef 100644 --- a/cmd/graphbench/neo4j.go +++ b/cmd/graphbench/neo4j.go @@ -229,6 +229,9 @@ func neo4jDatabaseName(connectionURL *url.URL) (string, error) { if err != nil { return "", fmt.Errorf("parse Neo4j database name: %w", err) } + if strings.Contains(databaseName, "/") { + return "", fmt.Errorf("Neo4j database path must contain a single database name") + } return databaseName, nil } diff --git a/cmd/graphbench/neo4j_test.go b/cmd/graphbench/neo4j_test.go index dfb795db..a01058c9 100644 --- a/cmd/graphbench/neo4j_test.go +++ b/cmd/graphbench/neo4j_test.go @@ -34,11 +34,16 @@ func TestParseNeo4jPlanDriverConfig(t *testing.T) { } func TestNeo4jDatabaseNameRejectsNestedPath(t *testing.T) { - parsed, err := url.Parse("neo4j://neo4j:secret@example.com:7687/a/b") - require.NoError(t, err) - - _, err = neo4jDatabaseName(parsed) - require.ErrorContains(t, err, "single database name") + for _, connStr := range []string{ + "neo4j://neo4j:secret@example.com:7687/a/b", + "neo4j://neo4j:secret@example.com:7687/a%2Fb", + } { + parsed, err := url.Parse(connStr) + require.NoError(t, err) + + _, err = neo4jDatabaseName(parsed) + require.ErrorContains(t, err, "single database name") + } } func TestNeo4jOperatorsAnnotatesOperators(t *testing.T) { diff --git a/cmd/plancorpus/capture.go b/cmd/plancorpus/capture.go index 6f5c5f0d..7cdfa02d 100644 --- a/cmd/plancorpus/capture.go +++ b/cmd/plancorpus/capture.go @@ -388,6 +388,9 @@ func neo4jDatabaseName(connectionURL *url.URL) (string, error) { if err != nil { return "", fmt.Errorf("parse Neo4j database name: %w", err) } + if strings.Contains(databaseName, "/") { + return "", fmt.Errorf("Neo4j database path must contain a single database name") + } return databaseName, nil } diff --git a/cmd/plancorpus/main_test.go b/cmd/plancorpus/main_test.go index c0f696be..deff5ec1 100644 --- a/cmd/plancorpus/main_test.go +++ b/cmd/plancorpus/main_test.go @@ -82,6 +82,11 @@ func TestParseNeo4jPlanDriverConfigPreservesURI(t *testing.T) { } func TestParseNeo4jPlanDriverConfigRejectsNestedDatabasePath(t *testing.T) { - _, err := parseNeo4jPlanDriverConfig("neo4j://neo4j:password@localhost:7687/db/extra") - require.ErrorContains(t, err, "single database name") + for _, connStr := range []string{ + "neo4j://neo4j:password@localhost:7687/db/extra", + "neo4j://neo4j:password@localhost:7687/db%2Fextra", + } { + _, err := parseNeo4jPlanDriverConfig(connStr) + require.ErrorContains(t, err, "single database name") + } } diff --git a/drivers/neo4j/neo4j.go b/drivers/neo4j/neo4j.go index 7a4f385a..ffddb6ea 100644 --- a/drivers/neo4j/neo4j.go +++ b/drivers/neo4j/neo4j.go @@ -77,6 +77,9 @@ func neo4jConnectionDatabaseName(connectionURL *url.URL) (string, error) { if err != nil { return "", fmt.Errorf("parse Neo4j database name: %w", err) } + if strings.Contains(databaseName, "/") { + return "", fmt.Errorf("Neo4j database path must contain a single database name") + } return databaseName, nil } diff --git a/drivers/neo4j/neo4j_internal_test.go b/drivers/neo4j/neo4j_internal_test.go index cfa77f5d..78086b87 100644 --- a/drivers/neo4j/neo4j_internal_test.go +++ b/drivers/neo4j/neo4j_internal_test.go @@ -48,9 +48,14 @@ func TestNeo4jConnectionTargetPreservesAcceptedSchemes(t *testing.T) { } func TestNeo4jConnectionDatabaseNameRejectsNestedPath(t *testing.T) { - connectionURL, err := url.Parse("neo4j://neo4j:password@localhost:7687/db/extra") - require.NoError(t, err) - - _, err = neo4jConnectionDatabaseName(connectionURL) - require.ErrorContains(t, err, "single database name") + for _, connStr := range []string{ + "neo4j://neo4j:password@localhost:7687/db/extra", + "neo4j://neo4j:password@localhost:7687/db%2Fextra", + } { + connectionURL, err := url.Parse(connStr) + require.NoError(t, err) + + _, err = neo4jConnectionDatabaseName(connectionURL) + require.ErrorContains(t, err, "single database name") + } } diff --git a/integration/pgsql_aggregate_traversal_plan_test.go b/integration/pgsql_aggregate_traversal_plan_test.go index c41febbb..f59e7c49 100644 --- a/integration/pgsql_aggregate_traversal_plan_test.go +++ b/integration/pgsql_aggregate_traversal_plan_test.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "os" + "regexp" "strings" "testing" "time" @@ -170,9 +171,9 @@ func TestPostgreSQLLiveAggregateTraversalCountPlanShape(t *testing.T) { } } - limitIndex := strings.Index(plan, "-> Limit") + limitMatch := regexp.MustCompile(`(?m)->\s+Limit\b`).FindStringIndex(plan) sourceMaterializationIndex := strings.LastIndex(plan, "Index Scan using node_") - if limitIndex < 0 || sourceMaterializationIndex < 0 || sourceMaterializationIndex < limitIndex { + if limitMatch == nil || sourceMaterializationIndex < 0 || sourceMaterializationIndex < limitMatch[0] { t.Fatalf("expected source node materialization after top-N limiting, got:\n%s", plan) } } From d36e765d658e12f9457b2c0090a018c28997ea4e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sat, 23 May 2026 18:55:46 -0700 Subject: [PATCH 098/114] docs(integration): document backend-selected skips --- cypher/models/pgsql/test/validation_integration_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cypher/models/pgsql/test/validation_integration_test.go b/cypher/models/pgsql/test/validation_integration_test.go index fc84fe4d..85ff5d90 100644 --- a/cypher/models/pgsql/test/validation_integration_test.go +++ b/cypher/models/pgsql/test/validation_integration_test.go @@ -33,6 +33,7 @@ func pgConnectionString(t *testing.T) string { connStr := os.Getenv(connectionStringEnv) require.NotEmpty(t, connStr) if isNeo4jConnectionString(connStr) { + // CONNECTION_STRING selects one active backend for integration runs. t.Skipf("%s is not a PostgreSQL connection string", connectionStringEnv) } From 1f063b41e775b08a4583fa30e33ad47b80025941 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 09:22:39 -0700 Subject: [PATCH 099/114] style: group related local declarations --- cmd/benchmark/main.go | 13 +- cmd/benchmark/report_test.go | 146 ++++---- cmd/benchmark/scenarios.go | 79 ++-- cmd/graphbench/corpus.go | 6 +- cmd/graphbench/main.go | 12 +- cmd/graphbench/neo4j.go | 13 +- cmd/graphbench/postgres.go | 6 +- cmd/graphbench/results.go | 7 +- cmd/graphbench/summary.go | 20 +- cmd/graphbench/summary_test.go | 53 +-- cmd/plancorpus/capture.go | 40 +- cmd/plancorpus/main.go | 7 +- cmd/plancorpus/report.go | 21 +- cmd/plancorpus/report_test.go | 97 ++--- cypher/models/cypher/copy_test.go | 28 +- cypher/models/pgsql/optimize/analysis_test.go | 6 +- cypher/models/pgsql/optimize/lowering_plan.go | 188 ++++++---- cypher/models/pgsql/optimize/optimizer.go | 6 +- .../models/pgsql/optimize/optimizer_test.go | 63 ++-- cypher/models/pgsql/optimize/reordering.go | 6 +- .../pgsql/test/validation_integration_test.go | 75 ++-- .../translate/aggregate_traversal_count.go | 27 +- .../pgsql/translate/constraints_test.go | 301 ++++++++------- .../models/pgsql/translate/count_fast_path.go | 14 +- cypher/models/pgsql/translate/expansion.go | 65 ++-- cypher/models/pgsql/translate/expression.go | 70 ++-- .../models/pgsql/translate/expression_test.go | 351 +++++++++--------- .../pgsql/translate/limit_pushdown_test.go | 82 ++-- cypher/models/pgsql/translate/match.go | 14 +- cypher/models/pgsql/translate/model.go | 6 +- .../pgsql/translate/optimizer_safety_test.go | 24 +- cypher/models/pgsql/translate/predicate.go | 45 +-- cypher/models/pgsql/translate/property.go | 6 +- .../models/pgsql/translate/tracking_test.go | 7 +- cypher/models/pgsql/translate/translator.go | 13 +- cypher/models/pgsql/translate/traversal.go | 14 +- cypher/models/pgsql/translate/with.go | 6 +- integration/cypher_template_test.go | 63 ++-- integration/cypher_test.go | 71 ++-- integration/harness.go | 80 ++-- .../pgsql_aggregate_traversal_plan_test.go | 7 +- tools/metrics/internal/metrics/quality.go | 218 ++++++----- 42 files changed, 1330 insertions(+), 1046 deletions(-) diff --git a/cmd/benchmark/main.go b/cmd/benchmark/main.go index 0045977a..0998c6fc 100644 --- a/cmd/benchmark/main.go +++ b/cmd/benchmark/main.go @@ -63,12 +63,13 @@ func main() { fatal("no connection string: set -connection flag or CONNECTION_STRING env var") } - ctx := context.Background() - - cfg := dawgs.Config{ - GraphQueryMemoryLimit: size.Gibibyte, - ConnectionString: conn, - } + var ( + ctx = context.Background() + cfg = dawgs.Config{ + GraphQueryMemoryLimit: size.Gibibyte, + ConnectionString: conn, + } + ) if *driver == pg.DriverName { poolCfg, err := pgxpool.ParseConfig(conn) diff --git a/cmd/benchmark/report_test.go b/cmd/benchmark/report_test.go index 310bdd00..4ee37688 100644 --- a/cmd/benchmark/report_test.go +++ b/cmd/benchmark/report_test.go @@ -27,56 +27,57 @@ import ( ) func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { - distinctRows := int64(2) - duplicateRows := int64(0) - loweringPlan := optimize.LoweringPlan{ - ProjectionPruning: []optimize.ProjectionPruningDecision{{ - Target: optimize.TraversalStepTarget{ - QueryPartIndex: 0, - ClauseIndex: 0, - PatternIndex: 0, - StepIndex: 0, - }, - ReferencedSymbols: []string{"m"}, - }}, - } - - report := Report{ - Driver: "pg", - GitRef: "abc123", - Date: "2026-05-14", - Iterations: 3, - Results: []Result{{ - Section: "Traversal", - Dataset: "base", - Label: "depth 1", - RowCount: 2, - DistinctRowCount: &distinctRows, - DuplicateRowCount: &duplicateRows, - Explain: &ExplainResult{ - SQL: "select 1;", - Plan: []string{"Result (actual rows=1 loops=1)"}, - Optimization: translate.OptimizationSummary{ - Rules: []optimize.RuleResult{{ - Name: "ExpansionSuffixPushdown", - Applied: true, - }}, - PlannedLowerings: loweringPlan.Decisions(), - Lowerings: []optimize.LoweringDecision{{ - Name: "ProjectionPruning", - }}, - LoweringPlan: &loweringPlan, + var ( + distinctRows = int64(2) + duplicateRows = int64(0) + loweringPlan = optimize.LoweringPlan{ + ProjectionPruning: []optimize.ProjectionPruningDecision{{ + Target: optimize.TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, }, - }, - Stats: Stats{ - Median: 10 * time.Millisecond, - P95: 20 * time.Millisecond, - Max: 30 * time.Millisecond, - }, - }}, - } + ReferencedSymbols: []string{"m"}, + }}, + } + report = Report{ + Driver: "pg", + GitRef: "abc123", + Date: "2026-05-14", + Iterations: 3, + Results: []Result{{ + Section: "Traversal", + Dataset: "base", + Label: "depth 1", + RowCount: 2, + DistinctRowCount: &distinctRows, + DuplicateRowCount: &duplicateRows, + Explain: &ExplainResult{ + SQL: "select 1;", + Plan: []string{"Result (actual rows=1 loops=1)"}, + Optimization: translate.OptimizationSummary{ + Rules: []optimize.RuleResult{{ + Name: "ExpansionSuffixPushdown", + Applied: true, + }}, + PlannedLowerings: loweringPlan.Decisions(), + Lowerings: []optimize.LoweringDecision{{ + Name: "ProjectionPruning", + }}, + LoweringPlan: &loweringPlan, + }, + }, + Stats: Stats{ + Median: 10 * time.Millisecond, + P95: 20 * time.Millisecond, + Max: 30 * time.Millisecond, + }, + }}, + } + output bytes.Buffer + ) - var output bytes.Buffer if err := writeJSON(&output, report); err != nil { t.Fatalf("write JSON: %v", err) } @@ -108,31 +109,32 @@ func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) { } func TestWriteMarkdownIncludesDiagnosticColumns(t *testing.T) { - distinctRows := int64(2) - duplicateRows := int64(0) - - report := Report{ - Driver: "pg", - GitRef: "abc123", - Date: "2026-05-14", - Iterations: 3, - Results: []Result{{ - Section: "ADCS Fanout", - Dataset: "adcs_fanout", - Label: "combined", - RowCount: 2, - DistinctRowCount: &distinctRows, - DuplicateRowCount: &duplicateRows, - Explain: &ExplainResult{Plan: []string{"Result"}}, - Stats: Stats{ - Median: 10 * time.Millisecond, - P95: 20 * time.Millisecond, - Max: 30 * time.Millisecond, - }, - }}, - } + var ( + distinctRows = int64(2) + duplicateRows = int64(0) + report = Report{ + Driver: "pg", + GitRef: "abc123", + Date: "2026-05-14", + Iterations: 3, + Results: []Result{{ + Section: "ADCS Fanout", + Dataset: "adcs_fanout", + Label: "combined", + RowCount: 2, + DistinctRowCount: &distinctRows, + DuplicateRowCount: &duplicateRows, + Explain: &ExplainResult{Plan: []string{"Result"}}, + Stats: Stats{ + Median: 10 * time.Millisecond, + P95: 20 * time.Millisecond, + Max: 30 * time.Millisecond, + }, + }}, + } + output bytes.Buffer + ) - var output bytes.Buffer if err := writeMarkdown(&output, report); err != nil { t.Fatalf("write markdown: %v", err) } diff --git a/cmd/benchmark/scenarios.go b/cmd/benchmark/scenarios.go index 48a1efb6..e5abd303 100644 --- a/cmd/benchmark/scenarios.go +++ b/cmd/benchmark/scenarios.go @@ -124,8 +124,11 @@ func cypherPathQuery(cypher string, pathColumns int) func(tx graph.Transaction) for result.Next() { rowCount++ - values := make([]graph.Path, pathColumns) - targets := make([]any, pathColumns) + var ( + values = make([]graph.Path, pathColumns) + targets = make([]any, pathColumns) + ) + for idx := range values { targets[idx] = &values[idx] } @@ -141,8 +144,10 @@ func cypherPathQuery(cypher string, pathColumns int) func(tx graph.Transaction) return Measurement{}, err } - distinctRowCount := int64(len(seen)) - duplicateRowCount := rowCount - distinctRowCount + var ( + distinctRowCount = int64(len(seen)) + duplicateRowCount = rowCount - distinctRowCount + ) return Measurement{ RowCount: rowCount, @@ -219,33 +224,32 @@ func baseScenarios(idMap opengraph.IDMap) []Scenario { const adcsFanoutObjectID = "S-1-5-21-2643190041-1319121918-239771340-513" func adcsFanoutScenarios() []Scenario { - ds := "adcs_fanout" - - p1 := fmt.Sprintf(` -MATCH (n:Group) WHERE n.objectid = '%s' -MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) -RETURN p1 -`, adcsFanoutObjectID) - - p2 := fmt.Sprintf(` -MATCH (n:Group) WHERE n.objectid = '%s' -MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca:EnterpriseCA)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d:Domain) -WHERE ct.authenticationenabled = true -AND ct.requiresmanagerapproval = false -AND ct.enrolleesuppliessubject = true -AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) -RETURN p2 -`, adcsFanoutObjectID) - - combinedMatch := fmt.Sprintf(` -MATCH (n:Group) WHERE n.objectid = '%s' -MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) -MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) -WHERE ct.authenticationenabled = true -AND ct.requiresmanagerapproval = false -AND ct.enrolleesuppliessubject = true -AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) -`, adcsFanoutObjectID) + var ( + ds = "adcs_fanout" + p1 = fmt.Sprintf(` + MATCH (n:Group) WHERE n.objectid = '%s' + MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) + RETURN p1 + `, adcsFanoutObjectID) + p2 = fmt.Sprintf(` + MATCH (n:Group) WHERE n.objectid = '%s' + MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca:EnterpriseCA)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d:Domain) + WHERE ct.authenticationenabled = true + AND ct.requiresmanagerapproval = false + AND ct.enrolleesuppliessubject = true + AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) + RETURN p2 + `, adcsFanoutObjectID) + combinedMatch = fmt.Sprintf(` + MATCH (n:Group) WHERE n.objectid = '%s' + MATCH p1 = (n)-[:MemberOf*0..]->()-[:Enroll]->(ca:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(d:Domain) + MATCH p2 = (n)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor*1..]->(:RootCA)-[:RootCAFor]->(d) + WHERE ct.authenticationenabled = true + AND ct.requiresmanagerapproval = false + AND ct.enrolleesuppliessubject = true + AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) + `, adcsFanoutObjectID) + ) return []Scenario{ cypherPathScenario("ADCS Fanout", ds, "p1 only", p1, 1), @@ -258,12 +262,13 @@ AND (ct.schemaversion = 1 OR ct.authorizedsignatures = 0) // --- Phantom scenarios (hardcoded node IDs from the dataset) --- func phantomScenarios(idMap opengraph.IDMap) []Scenario { - ds := "local/phantom" - - scenarios := []Scenario{ - {Section: "Match Nodes", Dataset: ds, Label: ds, Query: countQuery(countNodes)}, - {Section: "Match Edges", Dataset: ds, Label: ds, Query: countQuery(countEdges)}, - } + var ( + ds = "local/phantom" + scenarios = []Scenario{ + {Section: "Match Nodes", Dataset: ds, Label: ds, Query: countQuery(countNodes)}, + {Section: "Match Edges", Dataset: ds, Label: ds, Query: countQuery(countEdges)}, + } + ) for _, kind := range []string{"User", "Group", "Computer"} { k := kind diff --git a/cmd/graphbench/corpus.go b/cmd/graphbench/corpus.go index 6546d248..7d1c9075 100644 --- a/cmd/graphbench/corpus.go +++ b/cmd/graphbench/corpus.go @@ -95,8 +95,10 @@ func decodeJSONFile(path string, target any) error { } func scaleCorpusDatasets(corpus ScaleCorpus) []string { - seen := map[string]struct{}{} - datasets := make([]string, 0) + var ( + seen = map[string]struct{}{} + datasets = make([]string, 0) + ) for _, testCase := range corpus.Cases { if _, duplicate := seen[testCase.Dataset]; duplicate { diff --git a/cmd/graphbench/main.go b/cmd/graphbench/main.go index b4ac4102..bd18d1a3 100644 --- a/cmd/graphbench/main.go +++ b/cmd/graphbench/main.go @@ -77,8 +77,10 @@ func parseConfig(args []string, env func(string) string) (config, error) { } func parseExecutionModes(raw string) ([]ExecutionMode, error) { - var modes []ExecutionMode - seen := map[ExecutionMode]struct{}{} + var ( + modes []ExecutionMode + seen = map[ExecutionMode]struct{}{} + ) for _, part := range strings.Split(raw, ",") { mode, err := parseExecutionMode(part) @@ -115,8 +117,10 @@ func main() { fatal("load corpus: %v", err) } - ctx := context.Background() - var records []CaseResult + var ( + ctx = context.Background() + records []CaseResult + ) for _, mode := range cfg.Modes { switch mode { diff --git a/cmd/graphbench/neo4j.go b/cmd/graphbench/neo4j.go index 6b1d2bef..429fa8f1 100644 --- a/cmd/graphbench/neo4j.go +++ b/cmd/graphbench/neo4j.go @@ -86,8 +86,10 @@ func (s *neo4jRunner) Close(ctx context.Context) error { } func (s *neo4jRunner) Run(ctx context.Context, iterations int, corpus ScaleCorpus) ([]CaseResult, error) { - var records []CaseResult - casesByDataset := scaleCasesByDataset(corpus) + var ( + records []CaseResult + casesByDataset = scaleCasesByDataset(corpus) + ) for _, datasetName := range scaleCorpusDatasets(corpus) { if err := clearGraph(ctx, s.db); err != nil { @@ -285,8 +287,11 @@ func stringifyArguments(arguments map[string]any) map[string]string { } func neo4jOperators(root Neo4jPlanNode) []string { - var operators []string - var walk func(Neo4jPlanNode) + var ( + operators []string + walk func(Neo4jPlanNode) + ) + walk = func(node Neo4jPlanNode) { operators = append(operators, node.Operator+"@neo4j") for _, child := range node.Children { diff --git a/cmd/graphbench/postgres.go b/cmd/graphbench/postgres.go index 2ea47784..355b6bc3 100644 --- a/cmd/graphbench/postgres.go +++ b/cmd/graphbench/postgres.go @@ -100,8 +100,10 @@ func (s *postgresSQLRunner) Close(ctx context.Context) error { } func (s *postgresSQLRunner) Run(ctx context.Context, iterations int, corpus ScaleCorpus) ([]CaseResult, error) { - var records []CaseResult - casesByDataset := scaleCasesByDataset(corpus) + var ( + records []CaseResult + casesByDataset = scaleCasesByDataset(corpus) + ) for _, datasetName := range scaleCorpusDatasets(corpus) { if err := clearGraph(ctx, s.db); err != nil { diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go index 27d16093..48b11250 100644 --- a/cmd/graphbench/results.go +++ b/cmd/graphbench/results.go @@ -165,8 +165,11 @@ func readJSONLFile(path string) ([]CaseResult, error) { } defer input.Close() - decoder := json.NewDecoder(input) - var records []CaseResult + var ( + decoder = json.NewDecoder(input) + records []CaseResult + ) + for { var record CaseResult if err := decoder.Decode(&record); err != nil { diff --git a/cmd/graphbench/summary.go b/cmd/graphbench/summary.go index 89c78972..c2255fc9 100644 --- a/cmd/graphbench/summary.go +++ b/cmd/graphbench/summary.go @@ -70,12 +70,13 @@ type BaselineEntry struct { } func buildSummary(records []CaseResult) Summary { - summary := Summary{ - GeneratedAt: time.Now().UTC(), - } - - modeSummaries := map[ExecutionMode]*ModeSummary{} - caseSummaries := map[string]*CaseSummary{} + var ( + summary = Summary{ + GeneratedAt: time.Now().UTC(), + } + modeSummaries = map[ExecutionMode]*ModeSummary{} + caseSummaries = map[string]*CaseSummary{} + ) for _, record := range records { modeSummary := modeSummaries[record.ExecutionMode] @@ -96,8 +97,11 @@ func buildSummary(records []CaseResult) Summary { modeSummary.NotImplemented++ } - caseKey := record.Source + "\x00" + record.Dataset + "\x00" + record.Name - caseSummary := caseSummaries[caseKey] + var ( + caseKey = record.Source + "\x00" + record.Dataset + "\x00" + record.Name + caseSummary = caseSummaries[caseKey] + ) + if caseSummary == nil { caseSummary = &CaseSummary{ Source: record.Source, diff --git a/cmd/graphbench/summary_test.go b/cmd/graphbench/summary_test.go index 8bfe68f8..b73e03ad 100644 --- a/cmd/graphbench/summary_test.go +++ b/cmd/graphbench/summary_test.go @@ -26,8 +26,11 @@ import ( ) func TestApplyBaseline(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "baseline.jsonl") + var ( + dir = t.TempDir() + path = filepath.Join(dir, "baseline.jsonl") + ) + require.NoError(t, writeJSONLFile(path, []CaseResult{{ Dataset: "base", Name: "case", @@ -55,30 +58,32 @@ func TestApplyBaseline(t *testing.T) { } func TestWriteMarkdownSummary(t *testing.T) { - summary := buildSummary([]CaseResult{ - { - Dataset: "base", - Name: "case", - Category: "counts", - ExecutionMode: ModePostgresSQL, - Status: StatusOK, - RowCount: 1, - Stats: DurationStats{ - Iterations: 1, - Median: 2 * time.Millisecond, + var ( + summary = buildSummary([]CaseResult{ + { + Dataset: "base", + Name: "case", + Category: "counts", + ExecutionMode: ModePostgresSQL, + Status: StatusOK, + RowCount: 1, + Stats: DurationStats{ + Iterations: 1, + Median: 2 * time.Millisecond, + }, }, - }, - { - Dataset: "base", - Name: "case", - Category: "counts", - ExecutionMode: ModeLocalTraversal, - Status: StatusNotImplemented, - FallbackReason: localTraversalUnavailableReason, - }, - }) + { + Dataset: "base", + Name: "case", + Category: "counts", + ExecutionMode: ModeLocalTraversal, + Status: StatusNotImplemented, + FallbackReason: localTraversalUnavailableReason, + }, + }) + output bytes.Buffer + ) - var output bytes.Buffer require.NoError(t, writeMarkdownSummary(&output, summary)) require.Contains(t, output.String(), "| case | base | counts | 2.0ms; rows=1 | not_implemented; local traversal executor unavailable | - |") } diff --git a/cmd/plancorpus/capture.go b/cmd/plancorpus/capture.go index 7cdfa02d..d05a7046 100644 --- a/cmd/plancorpus/capture.go +++ b/cmd/plancorpus/capture.go @@ -68,20 +68,22 @@ func captureCorpus(ctx context.Context, datasetDir string, suite corpus, spec ca continue } - datasetLoaded := false - ensureDatasetLoaded := func() error { - if datasetLoaded { + var ( + datasetLoaded = false + ensureDatasetLoaded = func() error { + if datasetLoaded { + return nil + } + if err := clearGraph(ctx, backend.db); err != nil { + return err + } + if err := loadDataset(ctx, backend.db, datasetDir, datasetName); err != nil { + return err + } + datasetLoaded = true return nil } - if err := clearGraph(ctx, backend.db); err != nil { - return err - } - if err := loadDataset(ctx, backend.db, datasetDir, datasetName); err != nil { - return err - } - datasetLoaded = true - return nil - } + ) for _, file := range group.files { for _, testCase := range file.Cases { @@ -490,8 +492,11 @@ func postgresOperators(plan []string) []string { } func neo4jOperators(root Neo4jPlanNode) []string { - var operators []string - var walk func(Neo4jPlanNode) + var ( + operators []string + walk func(Neo4jPlanNode) + ) + walk = func(node Neo4jPlanNode) { operators = append(operators, node.Operator) for _, child := range node.Children { @@ -507,8 +512,11 @@ func loweringNames(decisions []optimize.LoweringDecision) []string { return nil } - names := make([]string, 0, len(decisions)) - seen := make(map[string]struct{}, len(decisions)) + var ( + names = make([]string, 0, len(decisions)) + seen = make(map[string]struct{}, len(decisions)) + ) + for _, decision := range decisions { name := decision.Name if _, duplicate := seen[name]; duplicate { diff --git a/cmd/plancorpus/main.go b/cmd/plancorpus/main.go index 6a1f06e9..57a5a22a 100644 --- a/cmd/plancorpus/main.go +++ b/cmd/plancorpus/main.go @@ -115,8 +115,11 @@ func captureSpecs(cfg commandConfig) ([]captureSpec, error) { return nil, fmt.Errorf("no connection string supplied; set CONNECTION_STRING or PG_CONNECTION_STRING/NEO4J_CONNECTION_STRING") } - orderedDrivers := []string{pgDriverName(), neo4jDriverName()} - specs := make([]captureSpec, 0, len(specsByDriver)) + var ( + orderedDrivers = []string{pgDriverName(), neo4jDriverName()} + specs = make([]captureSpec, 0, len(specsByDriver)) + ) + for _, driverName := range orderedDrivers { if spec, found := specsByDriver[driverName]; found { specs = append(specs, spec) diff --git a/cmd/plancorpus/report.go b/cmd/plancorpus/report.go index d0a28f4a..5e62f1a6 100644 --- a/cmd/plancorpus/report.go +++ b/cmd/plancorpus/report.go @@ -65,18 +65,17 @@ func buildSummary(records []PlanRecord, topN int) PlanSummary { topN = defaultTopPlans } - driverCounts := map[string]*DriverSummary{} - postgresOperatorCounts := map[string]int{} - neo4jOperatorCounts := map[string]int{} - plannedLoweringCounts := map[string]int{} - appliedLoweringCounts := map[string]int{} - skippedLoweringCounts := map[string]int{} - skippedReasonCounts := map[string]int{} - featureCounts := map[string]int{} - var ( - errors []PlanError - topPG []CostedPlan + driverCounts = map[string]*DriverSummary{} + postgresOperatorCounts = map[string]int{} + neo4jOperatorCounts = map[string]int{} + plannedLoweringCounts = map[string]int{} + appliedLoweringCounts = map[string]int{} + skippedLoweringCounts = map[string]int{} + skippedReasonCounts = map[string]int{} + featureCounts = map[string]int{} + errors []PlanError + topPG []CostedPlan ) for _, record := range records { diff --git a/cmd/plancorpus/report_test.go b/cmd/plancorpus/report_test.go index f3616952..9074185b 100644 --- a/cmd/plancorpus/report_test.go +++ b/cmd/plancorpus/report_test.go @@ -9,43 +9,44 @@ import ( ) func TestBuildSummaryRanksPostgresPlansAndCountsSignals(t *testing.T) { - records := []PlanRecord{{ - Driver: "pg", - Source: "cases/a.json", - Name: "low", - Cypher: "match (n) return n", - PGPlan: []string{"Seq Scan on node_1 (cost=0.00..10.50 rows=1 width=8)", "Filter: satisfied"}, - PGOperators: []string{"Seq Scan on node_1", "Filter: satisfied"}, - PlannedLowerings: []string{"ProjectionPruning"}, - AppliedLowerings: []string{"ProjectionPruning"}, - }, { - Driver: "pg", - Source: "cases/b.json", - Name: "high", - Cypher: "match p=()-[*]->() return p", - PGPlan: []string{"Recursive Union (cost=0.00..99.25 rows=1 width=8)", "SubPlan 1", "Function Scan on unnest _path"}, - PGOperators: []string{"Recursive Union", "Function Scan on unnest _path"}, - PlannedLowerings: []string{"LatePathMaterialization"}, - AppliedLowerings: []string{"LatePathMaterialization"}, - SkippedLowerings: []translate.SkippedLowering{{ - Name: "PredicatePlacement", - Reason: "planned predicate placements were not consumed by this translation shape", - Count: 2, - }}, - }, { - Driver: "neo4j", - Source: "cases/a.json", - Name: "neo", - Cypher: "match (n) return n", - Neo4jOperators: []string{"ProduceResults@neo4j", "AllNodesScan@neo4j"}, - }, { - Driver: "pg", - Source: "cases/error.json", - Name: "error", - Error: "expected error", - }} - - summary := buildSummary(records, 1) + var ( + records = []PlanRecord{{ + Driver: "pg", + Source: "cases/a.json", + Name: "low", + Cypher: "match (n) return n", + PGPlan: []string{"Seq Scan on node_1 (cost=0.00..10.50 rows=1 width=8)", "Filter: satisfied"}, + PGOperators: []string{"Seq Scan on node_1", "Filter: satisfied"}, + PlannedLowerings: []string{"ProjectionPruning"}, + AppliedLowerings: []string{"ProjectionPruning"}, + }, { + Driver: "pg", + Source: "cases/b.json", + Name: "high", + Cypher: "match p=()-[*]->() return p", + PGPlan: []string{"Recursive Union (cost=0.00..99.25 rows=1 width=8)", "SubPlan 1", "Function Scan on unnest _path"}, + PGOperators: []string{"Recursive Union", "Function Scan on unnest _path"}, + PlannedLowerings: []string{"LatePathMaterialization"}, + AppliedLowerings: []string{"LatePathMaterialization"}, + SkippedLowerings: []translate.SkippedLowering{{ + Name: "PredicatePlacement", + Reason: "planned predicate placements were not consumed by this translation shape", + Count: 2, + }}, + }, { + Driver: "neo4j", + Source: "cases/a.json", + Name: "neo", + Cypher: "match (n) return n", + Neo4jOperators: []string{"ProduceResults@neo4j", "AllNodesScan@neo4j"}, + }, { + Driver: "pg", + Source: "cases/error.json", + Name: "error", + Error: "expected error", + }} + summary = buildSummary(records, 1) + ) require.Equal(t, []DriverSummary{{ Driver: "neo4j", @@ -75,17 +76,19 @@ func TestBuildSummaryRanksPostgresPlansAndCountsSignals(t *testing.T) { } func TestWriteMarkdownSummaryEscapesPipes(t *testing.T) { - summary := PlanSummary{ - Drivers: []DriverSummary{{Driver: "pg", Records: 1}}, - TopPostgresPlans: []CostedPlan{{ - Cost: 1.25, - Source: "cases/a.json", - Name: "pipe | name", - PlanRoot: "Seq Scan on node_1", - }}, - } + var ( + summary = PlanSummary{ + Drivers: []DriverSummary{{Driver: "pg", Records: 1}}, + TopPostgresPlans: []CostedPlan{{ + Cost: 1.25, + Source: "cases/a.json", + Name: "pipe | name", + PlanRoot: "Seq Scan on node_1", + }}, + } + out bytes.Buffer + ) - var out bytes.Buffer require.NoError(t, writeMarkdownSummary(&out, summary)) require.Contains(t, out.String(), "pipe \\| name") } diff --git a/cypher/models/cypher/copy_test.go b/cypher/models/cypher/copy_test.go index ee65ff2a..7e7f4d3a 100644 --- a/cypher/models/cypher/copy_test.go +++ b/cypher/models/cypher/copy_test.go @@ -170,23 +170,25 @@ func TestCopy(t *testing.T) { } func TestCopyPatternVariablesAreIndependent(t *testing.T) { - original := &model2.PatternPart{ - Variable: model2.NewVariableWithSymbol("p"), - PatternElements: []*model2.PatternElement{ - { - Element: &model2.NodePattern{ - Variable: model2.NewVariableWithSymbol("n"), + var ( + original = &model2.PatternPart{ + Variable: model2.NewVariableWithSymbol("p"), + PatternElements: []*model2.PatternElement{ + { + Element: &model2.NodePattern{ + Variable: model2.NewVariableWithSymbol("n"), + }, }, - }, - { - Element: &model2.RelationshipPattern{ - Variable: model2.NewVariableWithSymbol("r"), + { + Element: &model2.RelationshipPattern{ + Variable: model2.NewVariableWithSymbol("r"), + }, }, }, - }, - } + } + copied = model2.Copy(original) + ) - copied := model2.Copy(original) copied.Variable.Symbol = "copied_path" copiedNode, _ := copied.PatternElements[0].AsNodePattern() copiedNode.Variable.Symbol = "copied_node" diff --git a/cypher/models/pgsql/optimize/analysis_test.go b/cypher/models/pgsql/optimize/analysis_test.go index f6441e32..0ea35be7 100644 --- a/cypher/models/pgsql/optimize/analysis_test.go +++ b/cypher/models/pgsql/optimize/analysis_test.go @@ -125,8 +125,10 @@ func TestAnalyzeSegmentsRegionsAtSemanticBarriers(t *testing.T) { func TestAnalysisDiagnosticsAreStable(t *testing.T) { t.Parallel() - analysis := analyzeCypher(t, adcsQuery) - diagnostics := strings.Join(analysis.Diagnostics(), "\n") + var ( + analysis = analyzeCypher(t, adcsQuery) + diagnostics = strings.Join(analysis.Diagnostics(), "\n") + ) require.Contains(t, diagnostics, "query_part[0] kind=single projection_deps=p1,p2") require.Contains(t, diagnostics, "region[0] part=0 clauses=0..2 matches=3") diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 6aba76ad..a1fa7a0a 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -46,8 +46,10 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic var plan LoweringPlan if query.SingleQuery.MultiPartQuery != nil { - carriedSymbols := map[string]struct{}{} - carriedSelectivity := map[string]boundSourceSelectivity{} + var ( + carriedSymbols = map[string]struct{}{} + carriedSelectivity = map[string]boundSourceSelectivity{} + ) for queryPartIndex, part := range query.SingleQuery.MultiPartQuery.Parts { if part == nil { @@ -58,8 +60,11 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic return LoweringPlan{}, err } - currentSymbols := copyStringSet(carriedSymbols) - currentSelectivity := copyBoundSourceSelectivity(carriedSelectivity) + var ( + currentSymbols = copyStringSet(carriedSymbols) + currentSelectivity = copyBoundSourceSelectivity(carriedSelectivity) + ) + declareReadingClauseSymbols(currentSymbols, part.ReadingClauses) declareReadingClauseSelectivity(currentSelectivity, part.ReadingClauses) @@ -137,21 +142,25 @@ func appendPatternProjectionPruningDecisions(plan *LoweringPlan, target PatternT pathReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(patternPart.Variable)) for stepIndex, step := range steps { - decision := ProjectionPruningDecision{ - Target: target.TraversalStep(stepIndex), - ReferencedSymbols: sortedMapKeys(sourceReferences), - PatternBindingReferenced: pathReferenced, - } + var ( + decision = ProjectionPruningDecision{ + Target: target.TraversalStep(stepIndex), + ReferencedSymbols: sortedMapKeys(sourceReferences), + PatternBindingReferenced: pathReferenced, + } + edgeReferenced = referencesSourceIdentifier(sourceReferences, variableSymbol(step.Relationship.Variable)) + hasPruning bool + ) - edgeReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.Relationship.Variable)) - var hasPruning bool if step.Relationship.Range != nil { decision.OmitRelationship = !edgeReferenced decision.OmitPathBinding = !pathReferenced hasPruning = decision.OmitRelationship || decision.OmitPathBinding } else { - leftReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.LeftNode.Variable)) - rightReferenced := referencesSourceIdentifier(sourceReferences, variableSymbol(step.RightNode.Variable)) + var ( + leftReferenced = referencesSourceIdentifier(sourceReferences, variableSymbol(step.LeftNode.Variable)) + rightReferenced = referencesSourceIdentifier(sourceReferences, variableSymbol(step.RightNode.Variable)) + ) decision.OmitLeftNode = !(leftReferenced || pathReferenced) decision.OmitRelationship = !(edgeReferenced || pathReferenced) @@ -167,8 +176,11 @@ func appendPatternProjectionPruningDecisions(plan *LoweringPlan, target PatternT func appendPatternPredicateProjectionLowerings(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode, sourceReferences map[string]struct{}) { for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { - patternPart := patternPartForPredicate(predicate) - steps := traversalStepsForPattern(patternPart) + var ( + patternPart = patternPartForPredicate(predicate) + steps = traversalStepsForPattern(patternPart) + ) + if len(steps) == 0 { continue } @@ -187,8 +199,11 @@ func appendPatternPredicateProjectionLowerings(plan *LoweringPlan, queryPartInde func appendPatternPredicatePlacementDecisions(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode) { for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { - patternPart := patternPartForPredicate(predicate) - steps := traversalStepsForPattern(patternPart) + var ( + patternPart = patternPartForPredicate(predicate) + steps = traversalStepsForPattern(patternPart) + ) + if len(steps) != 1 { continue } @@ -293,16 +308,21 @@ func appendExpandIntoDecisions(plan *LoweringPlan, queryPartIndex int, readingCl } for patternIndex, patternPart := range match.Pattern { - steps := traversalStepsForPattern(patternPart) - declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + var ( + steps = traversalStepsForPattern(patternPart) + declaredEndpoints = declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + ) for stepIndex, step := range steps { if step.Relationship.Range != nil { continue } - leftSymbol := variableSymbol(step.LeftNode.Variable) - rightSymbol := variableSymbol(step.RightNode.Variable) + var ( + leftSymbol = variableSymbol(step.LeftNode.Variable) + rightSymbol = variableSymbol(step.RightNode.Variable) + ) + _, leftBound := declaredEndpoints[stepIndex].BeforeLeftNode[leftSymbol] _, rightBound := declaredEndpoints[stepIndex].BeforeRightNode[rightSymbol] @@ -336,8 +356,10 @@ type declaredStepEndpoints struct { } func declaredSymbolsBeforeStepEndpoints(initial map[string]struct{}, steps []sourceTraversalStep) []declaredStepEndpoints { - declared := copyStringSet(initial) - endpoints := make([]declaredStepEndpoints, len(steps)) + var ( + declared = copyStringSet(initial) + endpoints = make([]declaredStepEndpoints, len(steps)) + ) for idx, step := range steps { endpoints[idx].BeforeLeftNode = copyStringSet(declared) @@ -358,8 +380,10 @@ func appendTraversalDirectionDecisions( initialDeclaredSymbols map[string]struct{}, initialSelectivity map[string]boundSourceSelectivity, ) { - declaredSymbols := copyStringSet(initialDeclaredSymbols) - declaredSourceSelectivity := copyBoundSourceSelectivity(initialSelectivity) + var ( + declaredSymbols = copyStringSet(initialDeclaredSymbols) + declaredSourceSelectivity = copyBoundSourceSelectivity(initialSelectivity) + ) for clauseIndex, readingClause := range readingClauses { if readingClause == nil || readingClause.Match == nil { @@ -373,13 +397,15 @@ func appendTraversalDirectionDecisions( } for patternIndex, patternPart := range match.Pattern { - steps := traversalStepsForPattern(patternPart) - declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) - patternTarget := PatternTarget{ - QueryPartIndex: queryPartIndex, - ClauseIndex: clauseIndex, - PatternIndex: patternIndex, - } + var ( + steps = traversalStepsForPattern(patternPart) + declaredEndpoints = declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + patternTarget = PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + ) for stepIndex, step := range steps { target := patternTarget.TraversalStep(stepIndex) @@ -445,8 +471,10 @@ func carryProjectionSelectivity( incomingSymbols map[string]struct{}, incomingSelectivity map[string]boundSourceSelectivity, ) (map[string]struct{}, map[string]boundSourceSelectivity) { - carriedSymbols := map[string]struct{}{} - carriedSelectivity := map[string]boundSourceSelectivity{} + var ( + carriedSymbols = map[string]struct{}{} + carriedSelectivity = map[string]boundSourceSelectivity{} + ) if projection == nil { return carriedSymbols, carriedSelectivity @@ -819,8 +847,11 @@ func traversalDirectionDecisionForStep( return TraversalDirectionDecision{}, false } - rightSymbol := variableSymbol(step.RightNode.Variable) - leftSymbol := variableSymbol(step.LeftNode.Variable) + var ( + rightSymbol = variableSymbol(step.RightNode.Variable) + leftSymbol = variableSymbol(step.LeftNode.Variable) + ) + if rightSymbol != "" { if _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol]; rightBound { if rightSymbol == leftSymbol { @@ -835,8 +866,10 @@ func traversalDirectionDecisionForStep( } } - leftConstrained := nodePatternHasConstraints(step.LeftNode) || leftHasAttachedPredicate - rightConstrained := nodePatternHasConstraints(step.RightNode) || rightHasAttachedPredicate + var ( + leftConstrained = nodePatternHasConstraints(step.LeftNode) || leftHasAttachedPredicate + rightConstrained = nodePatternHasConstraints(step.RightNode) || rightHasAttachedPredicate + ) if rightConstrained && !leftConstrained { reason := traversalDirectionReasonRightConstrained @@ -880,8 +913,11 @@ func boundLeftExpansionDirectionDecisionForStep( return TraversalDirectionDecision{}, false } - leftSymbol := variableSymbol(step.LeftNode.Variable) - rightSymbol := variableSymbol(step.RightNode.Variable) + var ( + leftSymbol = variableSymbol(step.LeftNode.Variable) + rightSymbol = variableSymbol(step.RightNode.Variable) + ) + if leftSymbol == "" || leftSymbol == rightSymbol { return TraversalDirectionDecision{}, false } @@ -937,13 +973,15 @@ func appendShortestPathStrategyDecisions(plan *LoweringPlan, queryPartIndex int, continue } - steps := traversalStepsForPattern(patternPart) - declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) - patternTarget := PatternTarget{ - QueryPartIndex: queryPartIndex, - ClauseIndex: clauseIndex, - PatternIndex: patternIndex, - } + var ( + steps = traversalStepsForPattern(patternPart) + declaredEndpoints = declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + patternTarget = PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + ) for stepIndex, step := range steps { if step.Relationship.Range == nil { @@ -973,8 +1011,10 @@ func shortestPathStrategyDecisionForStep( declaredEndpoints declaredStepEndpoints, predicateConstrainedSymbols map[string]struct{}, ) (ShortestPathStrategyDecision, bool) { - leftSymbol := variableSymbol(step.LeftNode.Variable) - rightSymbol := variableSymbol(step.RightNode.Variable) + var ( + leftSymbol = variableSymbol(step.LeftNode.Variable) + rightSymbol = variableSymbol(step.RightNode.Variable) + ) _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol] if leftEndpointBoundForStep(target.StepIndex, step, declaredEndpoints) && rightSymbol != "" && rightBound { @@ -1033,13 +1073,15 @@ func appendShortestPathFilterDecisions(plan *LoweringPlan, queryPartIndex int, r continue } - steps := traversalStepsForPattern(patternPart) - declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) - patternTarget := PatternTarget{ - QueryPartIndex: queryPartIndex, - ClauseIndex: clauseIndex, - PatternIndex: patternIndex, - } + var ( + steps = traversalStepsForPattern(patternPart) + declaredEndpoints = declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + patternTarget = PatternTarget{ + QueryPartIndex: queryPartIndex, + ClauseIndex: clauseIndex, + PatternIndex: patternIndex, + } + ) for stepIndex, step := range steps { if step.Relationship.Range == nil { @@ -1071,16 +1113,22 @@ func shortestPathFilterDecisionForStep( declaredEndpoints declaredStepEndpoints, predicateConstrainedSymbols map[string]struct{}, ) (ShortestPathFilterDecision, bool) { - leftSymbol := variableSymbol(step.LeftNode.Variable) - rightSymbol := variableSymbol(step.RightNode.Variable) + var ( + leftSymbol = variableSymbol(step.LeftNode.Variable) + rightSymbol = variableSymbol(step.RightNode.Variable) + ) + if rightSymbol != "" { if _, rightBound := declaredEndpoints.BeforeRightNode[rightSymbol]; rightBound { return ShortestPathFilterDecision{}, false } } - leftSearchConstrained := endpointHasSearchConstraint(step.LeftNode, leftSymbol, predicateConstrainedSymbols) - rightSearchConstrained := endpointHasSearchConstraint(step.RightNode, rightSymbol, predicateConstrainedSymbols) + var ( + leftSearchConstrained = endpointHasSearchConstraint(step.LeftNode, leftSymbol, predicateConstrainedSymbols) + rightSearchConstrained = endpointHasSearchConstraint(step.RightNode, rightSymbol, predicateConstrainedSymbols) + ) + if !endpointHasTerminalFilterConstraint(step.RightNode, rightSymbol, predicateConstrainedSymbols) { return ShortestPathFilterDecision{}, false } @@ -1204,8 +1252,10 @@ func appendExpansionSuffixPushdownDecisions(plan *LoweringPlan, queryPartIndex i } for patternIndex, patternPart := range match.Pattern { - steps := traversalStepsForPattern(patternPart) - declaredEndpoints := declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + var ( + steps = traversalStepsForPattern(patternPart) + declaredEndpoints = declaredSymbolsBeforeStepEndpoints(declaredSymbols, steps) + ) for stepIndex, step := range steps { if step.Relationship.Range == nil || stepIndex+1 >= len(steps) { @@ -1507,13 +1557,15 @@ func aggregateTraversalFinalProjection(queryPart *cypher.SinglePartQuery, source return aggregateTraversalFinalProjectionShape{}, false } - finalProjection := aggregateTraversalFinalProjectionShape{ - SourceAlias: sourceSymbol, - CountAlias: countAlias, - } + var ( + finalProjection = aggregateTraversalFinalProjectionShape{ + SourceAlias: sourceSymbol, + CountAlias: countAlias, + } + sourceSeen = false + countSeen = false + ) - sourceSeen := false - countSeen := false for _, item := range projection.Items { symbol, alias, ok := projectionItemVariableSymbolAndAlias(item) if !ok { diff --git a/cypher/models/pgsql/optimize/optimizer.go b/cypher/models/pgsql/optimize/optimizer.go index b448c7e6..d115167d 100644 --- a/cypher/models/pgsql/optimize/optimizer.go +++ b/cypher/models/pgsql/optimize/optimizer.go @@ -109,8 +109,10 @@ func AttachPredicates(analysis Analysis) []PredicateAttachment { regionBindings := regionBindingSymbols(region) for _, predicate := range region.Predicates { - bindingSymbols := predicateBindingSymbols(predicate, regionBindings) - scope := PredicateAttachmentScopeRegion + var ( + bindingSymbols = predicateBindingSymbols(predicate, regionBindings) + scope = PredicateAttachmentScopeRegion + ) if len(bindingSymbols) == 1 && len(predicate.Dependencies) == 1 { scope = PredicateAttachmentScopeBinding diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 4ed3ec0f..f4321426 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -284,11 +284,13 @@ func TestLoweringPlanReportsTypedPatternPredicateExistencePlacement(t *testing.T func TestSelectivityModelPlansTraversalDirection(t *testing.T) { t.Parallel() - model := NewSelectivityModel(testBindingLookup{}) - rightIDLookup := pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{pgsql.Identifier("n1"), pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.NewLiteral(1, pgsql.Int), + var ( + model = NewSelectivityModel(testBindingLookup{}) + rightIDLookup = pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{pgsql.Identifier("n1"), pgsql.ColumnID}, + pgsql.OperatorEquals, + pgsql.NewLiteral(1, pgsql.Int), + ) ) shouldFlip, err := model.ShouldFlipTraversalDirection(false, false, nil, rightIDLookup) @@ -1192,33 +1194,34 @@ func TestLoweringPlanSkipsOptionalMatchLimitPushdown(t *testing.T) { func TestSelectReferencesOnlyLocalIdentifiersValidatesJoinConstraintsIncrementally(t *testing.T) { t.Parallel() - tableRef := func(alias pgsql.Identifier) pgsql.TableReference { - return pgsql.TableReference{ - Name: pgsql.CompoundIdentifier{pgsql.TableNode}, - Binding: models.OptionalValue(alias), + var ( + tableRef = func(alias pgsql.Identifier) pgsql.TableReference { + return pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{pgsql.TableNode}, + Binding: models.OptionalValue(alias), + } } - } - - selectBody := pgsql.Select{ - Projection: []pgsql.SelectItem{ - pgsql.CompoundIdentifier{pgsql.Identifier("a"), pgsql.ColumnID}, - }, - From: []pgsql.FromClause{{ - Source: tableRef(pgsql.Identifier("a")), - Joins: []pgsql.Join{{ - Table: tableRef(pgsql.Identifier("b")), - JoinOperator: pgsql.JoinOperator{ - Constraint: pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{pgsql.Identifier("b"), pgsql.ColumnID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{pgsql.Identifier("c"), pgsql.ColumnID}, - ), - }, - }, { - Table: tableRef(pgsql.Identifier("c")), + selectBody = pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{pgsql.Identifier("a"), pgsql.ColumnID}, + }, + From: []pgsql.FromClause{{ + Source: tableRef(pgsql.Identifier("a")), + Joins: []pgsql.Join{{ + Table: tableRef(pgsql.Identifier("b")), + JoinOperator: pgsql.JoinOperator{ + Constraint: pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{pgsql.Identifier("b"), pgsql.ColumnID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{pgsql.Identifier("c"), pgsql.ColumnID}, + ), + }, + }, { + Table: tableRef(pgsql.Identifier("c")), + }}, }}, - }}, - } + } + ) require.False(t, SelectReferencesOnlyLocalIdentifiers(selectBody, pgsql.NewIdentifierSet())) } diff --git a/cypher/models/pgsql/optimize/reordering.go b/cypher/models/pgsql/optimize/reordering.go index 7c46a67d..4a380108 100644 --- a/cypher/models/pgsql/optimize/reordering.go +++ b/cypher/models/pgsql/optimize/reordering.go @@ -89,8 +89,10 @@ func reorderReadingClauses(readingClauses []*cypher.ReadingClause, regions []Reg } func reorderRegion(regionClauses []*cypher.ReadingClause) bool { - candidates := make([]reorderCandidate, len(regionClauses)) - declaredBefore := map[string]struct{}{} + var ( + candidates = make([]reorderCandidate, len(regionClauses)) + declaredBefore = map[string]struct{}{} + ) for idx, clause := range regionClauses { candidates[idx] = reorderCandidate{ diff --git a/cypher/models/pgsql/test/validation_integration_test.go b/cypher/models/pgsql/test/validation_integration_test.go index 85ff5d90..b47a21c9 100644 --- a/cypher/models/pgsql/test/validation_integration_test.go +++ b/cypher/models/pgsql/test/validation_integration_test.go @@ -186,15 +186,17 @@ func TestBidirectionalASPHarnessOverloads(t *testing.T) { ) require.NoError(t, err) - forwardPrimer := nextFrontValues( - "(1::int8, 10::int8, 1::int4, false, false, array [101]::int8[])", - "(3::int8, 10::int8, 1::int4, false, false, array [103]::int8[])", - ) - backwardPrimer := nextFrontValues( - "(2::int8, 10::int8, 1::int4, false, false, array [202]::int8[])", - "(4::int8, 10::int8, 1::int4, false, false, array [204]::int8[])", + var ( + forwardPrimer = nextFrontValues( + "(1::int8, 10::int8, 1::int4, false, false, array [101]::int8[])", + "(3::int8, 10::int8, 1::int4, false, false, array [103]::int8[])", + ) + backwardPrimer = nextFrontValues( + "(2::int8, 10::int8, 1::int4, false, false, array [202]::int8[])", + "(4::int8, 10::int8, 1::int4, false, false, array [204]::int8[])", + ) + pairFilter = pairFilterValues("(1::int8, 2::int8)") ) - pairFilter := pairFilterValues("(1::int8, 2::int8)") rows, err := tx.Query(testCtx, "select root_id, next_id from bidirectional_asp_harness($1::text, $2::text, $3::text, $4::text, 4, ''::text, ''::text, $5::text) order by root_id, next_id", @@ -237,18 +239,20 @@ func TestBidirectionalASPHarnessOverloads(t *testing.T) { ) require.NoError(t, err) - forwardPrimer := nextFrontValues( - "(1::int8, 2::int8, 1::int4, true, false, array [102]::int8[])", - "(1::int8, 2::int8, 1::int4, true, false, array [103]::int8[])", - "(3::int8, 30::int8, 1::int4, false, false, array [330]::int8[])", - ) - backwardPrimer := nextFrontValues( - "(4::int8, 30::int8, 1::int4, false, false, array [304]::int8[])", - "(4::int8, 30::int8, 1::int4, false, false, array [305]::int8[])", - ) - pairFilter := pairFilterValues( - "(1::int8, 2::int8)", - "(3::int8, 4::int8)", + var ( + forwardPrimer = nextFrontValues( + "(1::int8, 2::int8, 1::int4, true, false, array [102]::int8[])", + "(1::int8, 2::int8, 1::int4, true, false, array [103]::int8[])", + "(3::int8, 30::int8, 1::int4, false, false, array [330]::int8[])", + ) + backwardPrimer = nextFrontValues( + "(4::int8, 30::int8, 1::int4, false, false, array [304]::int8[])", + "(4::int8, 30::int8, 1::int4, false, false, array [305]::int8[])", + ) + pairFilter = pairFilterValues( + "(1::int8, 2::int8)", + "(3::int8, 4::int8)", + ) ) rows, err := tx.Query(testCtx, @@ -301,9 +305,11 @@ func TestBidirectionalASPHarnessOverloads(t *testing.T) { }) t.Run("shortest path harnesses avoid output column ambiguity", func(t *testing.T) { - frontier := nextFrontValues("(1::int8, 2::int8, 1::int4, false, false, array [101]::int8[])") + var ( + frontier = nextFrontValues("(1::int8, 2::int8, 1::int4, false, false, array [101]::int8[])") + unidirectionalCount int + ) - var unidirectionalCount int require.NoError(t, pgxPool.QueryRow(testCtx, "select count(*) from unidirectional_sp_harness($1::text, $2::text, 1, array []::int8[], array []::int8[])", frontier, @@ -323,8 +329,11 @@ func TestBidirectionalASPHarnessOverloads(t *testing.T) { }) 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) + 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") }) @@ -334,14 +343,16 @@ func TestBidirectionalASPHarnessOverloads(t *testing.T) { require.NoError(t, err) defer tx.Rollback(testCtx) - forwardPrimer := nextFrontValues( - "(1::int8, 2::int8, 1::int4, true, false, array [102]::int8[])", - "(3::int8, 30::int8, 1::int4, false, false, array [330]::int8[])", - ) - backwardPrimer := nextFrontValues("(4::int8, 30::int8, 1::int4, false, false, array [304]::int8[])") - pairFilter := pairFilterValues( - "(1::int8, 2::int8)", - "(3::int8, 4::int8)", + var ( + forwardPrimer = nextFrontValues( + "(1::int8, 2::int8, 1::int4, true, false, array [102]::int8[])", + "(3::int8, 30::int8, 1::int4, false, false, array [330]::int8[])", + ) + backwardPrimer = nextFrontValues("(4::int8, 30::int8, 1::int4, false, false, array [304]::int8[])") + pairFilter = pairFilterValues( + "(1::int8, 2::int8)", + "(3::int8, 4::int8)", + ) ) rows, err := tx.Query(testCtx, diff --git a/cypher/models/pgsql/translate/aggregate_traversal_count.go b/cypher/models/pgsql/translate/aggregate_traversal_count.go index 28280573..7fe92f0a 100644 --- a/cypher/models/pgsql/translate/aggregate_traversal_count.go +++ b/cypher/models/pgsql/translate/aggregate_traversal_count.go @@ -161,15 +161,17 @@ func (s *Translator) buildAggregateTraversalCTE(shape optimize.AggregateTraversa } sourceColumn, nextColumn := aggregateTraversalColumns(shape.Direction) - primerJoin := pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{aggregateEdgeAlias, sourceColumn}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{aggregateCandidateSourcesCTE, aggregateRootID}, - ) - recursiveJoin := pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{aggregateEdgeAlias, sourceColumn}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateNextID}, + var ( + primerJoin = pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateEdgeAlias, sourceColumn}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{aggregateCandidateSourcesCTE, aggregateRootID}, + ) + recursiveJoin = pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{aggregateEdgeAlias, sourceColumn}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{aggregateTraversalCTE, aggregateNextID}, + ) ) return pgsql.CommonTableExpression{ @@ -435,8 +437,11 @@ func (s *Translator) aggregateBindingPredicate(match *cypher.Match, symbol strin return nil, nil } - translator := NewTranslator(s.ctx, s.kindMapper.kindMapper, s.parameters, s.graphID) - binding := translator.scope.Define(alias, pgsql.NodeComposite) + var ( + translator = NewTranslator(s.ctx, s.kindMapper.kindMapper, s.parameters, s.graphID) + binding = translator.scope.Define(alias, pgsql.NodeComposite) + ) + translator.scope.Alias(pgsql.Identifier(symbol), binding) if err := walk.Cypher(match.Where, translator); err != nil { diff --git a/cypher/models/pgsql/translate/constraints_test.go b/cypher/models/pgsql/translate/constraints_test.go index c54dc0c6..548a9db9 100644 --- a/cypher/models/pgsql/translate/constraints_test.go +++ b/cypher/models/pgsql/translate/constraints_test.go @@ -19,16 +19,18 @@ func TestMeasureSelectivity(t *testing.T) { } func TestCanExecuteSelectiveBidirectionalSearch(t *testing.T) { - lowSelectivity := pgd.Equals( - pgsql.Identifier("123"), - pgsql.Identifier("456"), - ) - idLookup := func(identifier pgsql.Identifier, id int64) pgsql.Expression { - return pgd.Equals( - pgsql.CompoundIdentifier{identifier, pgsql.ColumnID}, - pgd.IntLiteral(id), + var ( + lowSelectivity = pgd.Equals( + pgsql.Identifier("123"), + pgsql.Identifier("456"), ) - } + idLookup = func(identifier pgsql.Identifier, id int64) pgsql.Expression { + return pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnID}, + pgd.IntLiteral(id), + ) + } + ) t.Run("rejects low selectivity endpoints", func(t *testing.T) { step := &TraversalStep{ @@ -136,57 +138,61 @@ func TestCanExecuteSelectiveBidirectionalSearch(t *testing.T) { } func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { - scopeWithNodeBindings := func(identifiers ...pgsql.Identifier) *Scope { - scope := NewScope() - for _, identifier := range identifiers { - scope.Define(identifier, pgsql.NodeComposite) + var ( + scopeWithNodeBindings = func(identifiers ...pgsql.Identifier) *Scope { + scope := NewScope() + for _, identifier := range identifiers { + scope.Define(identifier, pgsql.NodeComposite) + } + + return scope } - - return scope - } - localSelectivePropertyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { - return pgd.Equals( - pgd.PropertyLookup(identifier, "name"), - pgd.TextLiteral("123"), - ) - } - localBroadPropertyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { - return pgd.Equals( - pgsql.CompoundIdentifier{identifier, pgsql.ColumnProperties}, - pgd.IntLiteral(1), - ) - } - localKindConstraint := func(identifier pgsql.Identifier) pgsql.Expression { - return pgd.And( - pgd.Equals( - pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, + localSelectivePropertyConstraint = func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgd.PropertyLookup(identifier, "name"), + pgd.TextLiteral("123"), + ) + } + localBroadPropertyConstraint = func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnProperties}, pgd.IntLiteral(1), - ), - pgd.Equals( - pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, - pgd.IntLiteral(2), - ), - ) - } + ) + } + localKindConstraint = func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.And( + pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, + pgd.IntLiteral(1), + ), + pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, + pgd.IntLiteral(2), + ), + ) + } + ) t.Run("accepts selective property-backed local endpoint constraints for shortest path", func(t *testing.T) { - leftIdentifier := pgsql.Identifier("n0") - rightIdentifier := pgsql.Identifier("n1") - step := &TraversalStep{ - LeftNode: &BoundIdentifier{ - Identifier: leftIdentifier, - }, - RightNode: &BoundIdentifier{ - Identifier: rightIdentifier, - }, - Expansion: &Expansion{ - Options: ExpansionOptions{ - FindShortestPath: true, + var ( + leftIdentifier = pgsql.Identifier("n0") + rightIdentifier = pgsql.Identifier("n1") + step = &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, }, - PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), - TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), - }, - } + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), + }, + } + ) canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) @@ -195,23 +201,25 @@ func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { }) t.Run("rejects broad non-kind local endpoint constraints for shortest path", func(t *testing.T) { - leftIdentifier := pgsql.Identifier("n0") - rightIdentifier := pgsql.Identifier("n1") - step := &TraversalStep{ - LeftNode: &BoundIdentifier{ - Identifier: leftIdentifier, - }, - RightNode: &BoundIdentifier{ - Identifier: rightIdentifier, - }, - Expansion: &Expansion{ - Options: ExpansionOptions{ - FindShortestPath: true, + var ( + leftIdentifier = pgsql.Identifier("n0") + rightIdentifier = pgsql.Identifier("n1") + step = &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, }, - PrimerNodeConstraints: localBroadPropertyConstraint(leftIdentifier), - TerminalNodeConstraints: localBroadPropertyConstraint(rightIdentifier), - }, - } + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: localBroadPropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localBroadPropertyConstraint(rightIdentifier), + }, + } + ) canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) @@ -220,23 +228,25 @@ func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { }) t.Run("rejects pair-aware search when only one endpoint is selective", func(t *testing.T) { - leftIdentifier := pgsql.Identifier("n0") - rightIdentifier := pgsql.Identifier("n1") - step := &TraversalStep{ - LeftNode: &BoundIdentifier{ - Identifier: leftIdentifier, - }, - RightNode: &BoundIdentifier{ - Identifier: rightIdentifier, - }, - Expansion: &Expansion{ - Options: ExpansionOptions{ - FindShortestPath: true, + var ( + leftIdentifier = pgsql.Identifier("n0") + rightIdentifier = pgsql.Identifier("n1") + step = &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, }, - PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), - TerminalNodeConstraints: localBroadPropertyConstraint(rightIdentifier), - }, - } + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localBroadPropertyConstraint(rightIdentifier), + }, + } + ) canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) @@ -268,23 +278,25 @@ func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { }) t.Run("accepts selective property-backed local endpoint constraints for all shortest paths", func(t *testing.T) { - leftIdentifier := pgsql.Identifier("n0") - rightIdentifier := pgsql.Identifier("n1") - step := &TraversalStep{ - LeftNode: &BoundIdentifier{ - Identifier: leftIdentifier, - }, - RightNode: &BoundIdentifier{ - Identifier: rightIdentifier, - }, - Expansion: &Expansion{ - Options: ExpansionOptions{ - FindAllShortestPaths: true, + var ( + leftIdentifier = pgsql.Identifier("n0") + rightIdentifier = pgsql.Identifier("n1") + step = &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, }, - PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), - TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), - }, - } + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindAllShortestPaths: true, + }, + PrimerNodeConstraints: localSelectivePropertyConstraint(leftIdentifier), + TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), + }, + } + ) canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) @@ -293,29 +305,31 @@ func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { }) t.Run("rejects endpoint constraints that reference the other endpoint", func(t *testing.T) { - leftIdentifier := pgsql.Identifier("n0") - rightIdentifier := pgsql.Identifier("n1") - step := &TraversalStep{ - LeftNode: &BoundIdentifier{ - Identifier: leftIdentifier, - }, - RightNode: &BoundIdentifier{ - Identifier: rightIdentifier, - }, - Expansion: &Expansion{ - Options: ExpansionOptions{ - FindShortestPath: true, + var ( + leftIdentifier = pgsql.Identifier("n0") + rightIdentifier = pgsql.Identifier("n1") + step = &TraversalStep{ + LeftNode: &BoundIdentifier{ + Identifier: leftIdentifier, }, - PrimerNodeConstraints: pgd.And( - localSelectivePropertyConstraint(leftIdentifier), - pgd.Equals( - pgsql.CompoundIdentifier{leftIdentifier, pgsql.ColumnKindIDs}, - pgsql.CompoundIdentifier{rightIdentifier, pgsql.ColumnKindIDs}, + RightNode: &BoundIdentifier{ + Identifier: rightIdentifier, + }, + Expansion: &Expansion{ + Options: ExpansionOptions{ + FindShortestPath: true, + }, + PrimerNodeConstraints: pgd.And( + localSelectivePropertyConstraint(leftIdentifier), + pgd.Equals( + pgsql.CompoundIdentifier{leftIdentifier, pgsql.ColumnKindIDs}, + pgsql.CompoundIdentifier{rightIdentifier, pgsql.ColumnKindIDs}, + ), ), - ), - TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), - }, - } + TerminalNodeConstraints: localSelectivePropertyConstraint(rightIdentifier), + }, + } + ) canExecute, err := step.CanExecutePairAwareBidirectionalSearch(scopeWithNodeBindings(leftIdentifier, rightIdentifier)) @@ -325,25 +339,26 @@ func TestCanExecutePairAwareBidirectionalSearch(t *testing.T) { } func TestCanMaterializeEndpointPairFilterRequiresPairAwareConstraints(t *testing.T) { - leftIdentifier := pgsql.Identifier("n0") - rightIdentifier := pgsql.Identifier("n1") - kindOnlyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { - return pgd.Equals( - pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, - pgd.IntLiteral(1), - ) - } - propertyConstraint := func(identifier pgsql.Identifier) pgsql.Expression { - return pgd.Equals( - pgd.PropertyLookup(identifier, "name"), - pgd.TextLiteral("target"), - ) - } - - step := &TraversalStep{ - LeftNode: &BoundIdentifier{Identifier: leftIdentifier}, - RightNode: &BoundIdentifier{Identifier: rightIdentifier}, - } + var ( + leftIdentifier = pgsql.Identifier("n0") + rightIdentifier = pgsql.Identifier("n1") + kindOnlyConstraint = func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgsql.CompoundIdentifier{identifier, pgsql.ColumnKindIDs}, + pgd.IntLiteral(1), + ) + } + propertyConstraint = func(identifier pgsql.Identifier) pgsql.Expression { + return pgd.Equals( + pgd.PropertyLookup(identifier, "name"), + pgd.TextLiteral("target"), + ) + } + step = &TraversalStep{ + LeftNode: &BoundIdentifier{Identifier: leftIdentifier}, + RightNode: &BoundIdentifier{Identifier: rightIdentifier}, + } + ) require.False(t, canMaterializeEndpointPairFilterForStep(step, &Expansion{ PrimerNodeConstraints: kindOnlyConstraint(leftIdentifier), diff --git a/cypher/models/pgsql/translate/count_fast_path.go b/cypher/models/pgsql/translate/count_fast_path.go index 29387061..4be8bbb0 100644 --- a/cypher/models/pgsql/translate/count_fast_path.go +++ b/cypher/models/pgsql/translate/count_fast_path.go @@ -32,13 +32,15 @@ func (s *Translator) translateCountStoreFastPath(query *cypher.RegularQuery, pla return false, nil } - countExpression := pgsql.FunctionCall{ - Function: pgsql.FunctionCount, - Parameters: []pgsql.Expression{pgsql.Wildcard{}}, - CastType: pgsql.Int8, - } + var ( + countExpression = pgsql.FunctionCall{ + Function: pgsql.FunctionCount, + Parameters: []pgsql.Expression{pgsql.Wildcard{}}, + CastType: pgsql.Int8, + } + countProjection pgsql.SelectItem = countExpression + ) - var countProjection pgsql.SelectItem = countExpression if shape.Alias != "" { countProjection = pgsql.AliasedExpression{ Expression: countExpression, diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index 5cfee834..c7d27587 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -144,8 +144,10 @@ func newExpansionNodeSeed(identifier, nodeIdentifier pgsql.Identifier, constrain } func newExpansionNodeFilterSeed(identifier, filterIdentifier, nodeIdentifier pgsql.Identifier, constraints pgsql.Expression) expansionSeed { - filterAlias := pgsql.Identifier(string(identifier) + "_filter") - filterID := pgsql.CompoundIdentifier{filterAlias, pgsql.ColumnID} + var ( + filterAlias = pgsql.Identifier(string(identifier) + "_filter") + filterID = pgsql.CompoundIdentifier{filterAlias, pgsql.ColumnID} + ) if constraints == nil { return newExpansionSeed(identifier, filterID, []pgsql.FromClause{{ @@ -238,8 +240,10 @@ func expressionReferencesUnwindBinding(expression pgsql.Expression, unwindClause } func (s *ExpansionBuilder) seedEndpointConstraintSplit(expression pgsql.Expression, nodeIdentifier pgsql.Identifier, previousFrameIdentifier pgsql.Identifier) (pgsql.Expression, pgsql.Expression) { - seedExpression := rewriteBoundEndpointSeedReference(expression, previousFrameIdentifier, nodeIdentifier) - localScope := pgsql.AsIdentifierSet(nodeIdentifier) + var ( + seedExpression = rewriteBoundEndpointSeedReference(expression, previousFrameIdentifier, nodeIdentifier) + localScope = pgsql.AsIdentifierSet(nodeIdentifier) + ) for _, clause := range s.unwindClauses { if clause.Binding != nil { @@ -733,11 +737,13 @@ func (s *ExpansionBuilder) usesBoundEndpointPairs() bool { } func (s *ExpansionBuilder) boundNodeIDsFilterStatement(filterIdentifier pgsql.Identifier, nodeIdentifier pgsql.Identifier) pgsql.Insert { - previousFrameIdentifier := s.traversalStep.Frame.Previous.Binding.Identifier - nodeIDExpression := pgsql.RowColumnReference{ - Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, nodeIdentifier}, - Column: pgsql.ColumnID, - } + var ( + previousFrameIdentifier = s.traversalStep.Frame.Previous.Binding.Identifier + nodeIDExpression = pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, nodeIdentifier}, + Column: pgsql.ColumnID, + } + ) return pgsql.Insert{ Table: pgsql.TableReference{ @@ -825,15 +831,17 @@ func (s *ExpansionBuilder) boundEndpointPairFilterStatement() (pgsql.Insert, boo return pgsql.Insert{}, false } - previousFrameIdentifier := s.traversalStep.Frame.Previous.Binding.Identifier - rootIDExpression := pgsql.RowColumnReference{ - Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, s.traversalStep.LeftNode.Identifier}, - Column: pgsql.ColumnID, - } - terminalIDExpression := pgsql.RowColumnReference{ - Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, s.traversalStep.RightNode.Identifier}, - Column: pgsql.ColumnID, - } + var ( + previousFrameIdentifier = s.traversalStep.Frame.Previous.Binding.Identifier + rootIDExpression = pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, s.traversalStep.LeftNode.Identifier}, + Column: pgsql.ColumnID, + } + terminalIDExpression = pgsql.RowColumnReference{ + Identifier: pgsql.CompoundIdentifier{previousFrameIdentifier, s.traversalStep.RightNode.Identifier}, + Column: pgsql.ColumnID, + } + ) return pgsql.Insert{ Table: pgsql.TableReference{ @@ -875,9 +883,12 @@ func (s *ExpansionBuilder) materializedEndpointPairFilterStatement() (pgsql.Inse return pgsql.Insert{}, false } - rootIDExpression := pgsql.CompoundIdentifier{s.traversalStep.LeftNode.Identifier, pgsql.ColumnID} - terminalIDExpression := pgsql.CompoundIdentifier{s.traversalStep.RightNode.Identifier, pgsql.ColumnID} - pairConstraints := pgsql.OptionalAnd(expansionModel.PrimerNodeConstraints, expansionModel.TerminalNodeConstraints) + var ( + rootIDExpression = pgsql.CompoundIdentifier{s.traversalStep.LeftNode.Identifier, pgsql.ColumnID} + terminalIDExpression = pgsql.CompoundIdentifier{s.traversalStep.RightNode.Identifier, pgsql.ColumnID} + pairConstraints = pgsql.OptionalAnd(expansionModel.PrimerNodeConstraints, expansionModel.TerminalNodeConstraints) + ) + pairConstraints = pgsql.OptionalAnd(pairConstraints, pgsql.NewBinaryExpression( rootIDExpression, pgsql.OperatorIsNot, @@ -1578,8 +1589,10 @@ 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 { - rootID := pgsql.CompoundIdentifier{expansionFrame, expansionRootID} - terminalID := pgsql.CompoundIdentifier{expansionFrame, expansionNextID} + var ( + rootID = pgsql.CompoundIdentifier{expansionFrame, expansionRootID} + terminalID = pgsql.CompoundIdentifier{expansionFrame, expansionNextID} + ) return shortestPathSelfEndpointGuardCase(rootID, terminalID) } @@ -2416,8 +2429,10 @@ func (s *Translator) buildExpansionPatternRoot(traversalStepContext TraversalSte ), ) - seedConstraints := pgsql.OptionalAnd(primerLocal, primerExternal) - var seed *expansionSeed + var ( + seedConstraints = pgsql.OptionalAnd(primerLocal, primerExternal) + seed *expansionSeed + ) if traversalStep.LeftNodeBound { if traversalStep.Frame.Previous == nil { diff --git a/cypher/models/pgsql/translate/expression.go b/cypher/models/pgsql/translate/expression.go index 51ee2078..6123f8a5 100644 --- a/cypher/models/pgsql/translate/expression.go +++ b/cypher/models/pgsql/translate/expression.go @@ -850,15 +850,17 @@ func jsonEmptyArrayLiteral() pgsql.Expression { func rewritePropertyLookupNullCheck(propertyLookup *pgsql.BinaryExpression, isNotNull bool) pgsql.Expression { propertyLookup.Operator = pgsql.OperatorJSONField - existsExpression := pgsql.NewBinaryExpression( - propertyLookup.LOperand, - pgsql.OperatorJSONBFieldExists, - propertyLookup.ROperand, - ) - jsonNullExpression := pgsql.NewBinaryExpression( - propertyLookup, - pgsql.OperatorEquals, - jsonNullLiteral(), + var ( + existsExpression = pgsql.NewBinaryExpression( + propertyLookup.LOperand, + pgsql.OperatorJSONBFieldExists, + propertyLookup.ROperand, + ) + jsonNullExpression = pgsql.NewBinaryExpression( + propertyLookup, + pgsql.OperatorEquals, + jsonNullLiteral(), + ) ) if isNotNull { @@ -955,15 +957,17 @@ func buildStringPropertyComparisonPredicate(propertyLookup *pgsql.BinaryExpressi )) } - nonStringTypeCheck := pgsql.NewBinaryExpression( - jsonbTypeof(jsonFieldPropertyLookup(propertyLookup)), - pgsql.OperatorCypherNotEquals, - pgsql.NewLiteral("string", pgsql.Text), - ) - nonStringComparison := pgsql.NewBinaryExpression( - jsonFieldPropertyLookup(propertyLookup), - pgsql.OperatorCypherNotEquals, - toJSONBTextOperand(textOperand), + var ( + nonStringTypeCheck = pgsql.NewBinaryExpression( + jsonbTypeof(jsonFieldPropertyLookup(propertyLookup)), + pgsql.OperatorCypherNotEquals, + pgsql.NewLiteral("string", pgsql.Text), + ) + nonStringComparison = pgsql.NewBinaryExpression( + jsonFieldPropertyLookup(propertyLookup), + pgsql.OperatorCypherNotEquals, + toJSONBTextOperand(textOperand), + ) ) return pgsql.NewParenthetical(pgsql.NewBinaryExpression( @@ -978,20 +982,22 @@ func buildStringPropertyComparisonPredicate(propertyLookup *pgsql.BinaryExpressi } func buildEmptyArrayPropertyComparison(propertyLookup *pgsql.BinaryExpression, negated bool) *pgsql.BinaryExpression { - emptyArrayExpression := pgsql.NewBinaryExpression( - jsonFieldPropertyLookup(propertyLookup), - pgsql.OperatorEquals, - jsonEmptyArrayLiteral(), - ) - nullExpression := pgsql.NewBinaryExpression( - jsonFieldPropertyLookup(propertyLookup), - pgsql.OperatorEquals, - jsonNullLiteral(), - ) - nullTaintExpression := pgsql.NewBinaryExpression( - nullExpression, - pgsql.OperatorAnd, - pgsql.NullLiteral(), + var ( + emptyArrayExpression = pgsql.NewBinaryExpression( + jsonFieldPropertyLookup(propertyLookup), + pgsql.OperatorEquals, + jsonEmptyArrayLiteral(), + ) + nullExpression = pgsql.NewBinaryExpression( + jsonFieldPropertyLookup(propertyLookup), + pgsql.OperatorEquals, + jsonNullLiteral(), + ) + nullTaintExpression = pgsql.NewBinaryExpression( + nullExpression, + pgsql.OperatorAnd, + pgsql.NullLiteral(), + ) ) if negated { diff --git a/cypher/models/pgsql/translate/expression_test.go b/cypher/models/pgsql/translate/expression_test.go index 807821e3..9c9df618 100644 --- a/cypher/models/pgsql/translate/expression_test.go +++ b/cypher/models/pgsql/translate/expression_test.go @@ -25,15 +25,40 @@ func TestInferExpressionType(t *testing.T) { Exclusive bool } - testCases := []testCase{{ - ExpectedType: pgsql.Boolean, - Expression: pgsql.NewBinaryExpression( - pgsql.NewPropertyLookup( - pgsql.CompoundIdentifier{"n", "properties"}, - mustAsLiteral("field_a"), + var ( + testCases = []testCase{{ + ExpectedType: pgsql.Boolean, + Expression: pgsql.NewBinaryExpression( + pgsql.NewPropertyLookup( + pgsql.CompoundIdentifier{"n", "properties"}, + mustAsLiteral("field_a"), + ), + pgsql.OperatorAnd, + pgsql.NewBinaryExpression( + mustAsLiteral("123"), + pgsql.OperatorIn, + pgsql.ArrayLiteral{ + Values: []pgsql.Expression{mustAsLiteral("a"), mustAsLiteral("b")}, + CastType: pgsql.TextArray, + }, + ), ), - pgsql.OperatorAnd, - pgsql.NewBinaryExpression( + }, { + ExpectedType: pgsql.Boolean, + Expression: pgsql.NewBinaryExpression( + pgsql.NewPropertyLookup( + pgsql.CompoundIdentifier{"n", "properties"}, + mustAsLiteral("field_a"), + ), + pgsql.OperatorAnd, + pgsql.NewPropertyLookup( + pgsql.CompoundIdentifier{"n", "properties"}, + mustAsLiteral("field_b"), + ), + ), + }, { + ExpectedType: pgsql.Boolean, + Expression: pgsql.NewBinaryExpression( mustAsLiteral("123"), pgsql.OperatorIn, pgsql.ArrayLiteral{ @@ -41,84 +66,58 @@ func TestInferExpressionType(t *testing.T) { CastType: pgsql.TextArray, }, ), - ), - }, { - ExpectedType: pgsql.Boolean, - Expression: pgsql.NewBinaryExpression( - pgsql.NewPropertyLookup( - pgsql.CompoundIdentifier{"n", "properties"}, - mustAsLiteral("field_a"), - ), - pgsql.OperatorAnd, - pgsql.NewPropertyLookup( - pgsql.CompoundIdentifier{"n", "properties"}, - mustAsLiteral("field_b"), + }, { + ExpectedType: pgsql.Text, + Expression: pgsql.NewBinaryExpression( + mustAsLiteral("123"), + pgsql.OperatorConcatenate, + mustAsLiteral("456"), ), - ), - }, { - ExpectedType: pgsql.Boolean, - Expression: pgsql.NewBinaryExpression( - mustAsLiteral("123"), - pgsql.OperatorIn, - pgsql.ArrayLiteral{ - Values: []pgsql.Expression{mustAsLiteral("a"), mustAsLiteral("b")}, - CastType: pgsql.TextArray, - }, - ), - }, { - ExpectedType: pgsql.Text, - Expression: pgsql.NewBinaryExpression( - mustAsLiteral("123"), - pgsql.OperatorConcatenate, - mustAsLiteral("456"), - ), - }, { - ExpectedType: pgsql.Text, - Expression: pgsql.NewBinaryExpression( - mustAsLiteral("n"), - pgsql.OperatorConcatenate, - mustAsLiteral(123), - ), - }, { - ExpectedType: pgsql.Int8, - Expression: pgsql.NewBinaryExpression( - mustAsLiteral(123), - pgsql.OperatorAdd, - pgsql.NewBinaryExpression( + }, { + ExpectedType: pgsql.Text, + Expression: pgsql.NewBinaryExpression( + mustAsLiteral("n"), + pgsql.OperatorConcatenate, mustAsLiteral(123), - pgsql.OperatorMultiply, - mustAsLiteral(1), ), - ), - }, { - ExpectedType: pgsql.Int8, - Expression: pgsql.NewBinaryExpression( - mustAsLiteral(123), - pgsql.OperatorAdd, - pgsql.NewBinaryExpression( - mustAsLiteral(int16(123)), - pgsql.OperatorMultiply, - mustAsLiteral(int16(1)), + }, { + ExpectedType: pgsql.Int8, + Expression: pgsql.NewBinaryExpression( + mustAsLiteral(123), + pgsql.OperatorAdd, + pgsql.NewBinaryExpression( + mustAsLiteral(123), + pgsql.OperatorMultiply, + mustAsLiteral(1), + ), ), - ), - }, { - Exclusive: true, - ExpectedType: pgsql.Int4, - Expression: pgsql.NewBinaryExpression( - pgsql.NewPropertyLookup( - pgsql.CompoundIdentifier{"n", "properties"}, - mustAsLiteral("field"), + }, { + ExpectedType: pgsql.Int8, + Expression: pgsql.NewBinaryExpression( + mustAsLiteral(123), + pgsql.OperatorAdd, + pgsql.NewBinaryExpression( + mustAsLiteral(int16(123)), + pgsql.OperatorMultiply, + mustAsLiteral(int16(1)), + ), ), - pgsql.OperatorAdd, - pgsql.NewBinaryExpression( - mustAsLiteral(int16(123)), - pgsql.OperatorMultiply, - mustAsLiteral(int32(1)), + }, { + Exclusive: true, + ExpectedType: pgsql.Int4, + Expression: pgsql.NewBinaryExpression( + pgsql.NewPropertyLookup( + pgsql.CompoundIdentifier{"n", "properties"}, + mustAsLiteral("field"), + ), + pgsql.OperatorAdd, + pgsql.NewBinaryExpression( + mustAsLiteral(int16(123)), + pgsql.OperatorMultiply, + mustAsLiteral(int32(1)), + ), ), - ), - }} - - var ( + }} exclusive []testCase hasExclusive bool ) @@ -298,101 +297,102 @@ func TestInferWrappedExpressionType(t *testing.T) { } func TestPropertyLookupEqualityScalarRewrites(t *testing.T) { - propertyLookup := func(property string) *pgsql.BinaryExpression { - return pgsql.NewPropertyLookup( - pgsql.CompoundIdentifier{"n", pgsql.ColumnProperties}, - mustAsLiteral(property), - ) - } - renderComparison := func(t *testing.T, lOperand pgsql.Expression, operator pgsql.Operator, rOperand pgsql.Expression) string { - t.Helper() - - treeTranslator := translate.NewExpressionTreeTranslator(nil) - treeTranslator.PushOperand(lOperand) - treeTranslator.PushOperand(rOperand) - require.NoError(t, treeTranslator.CompleteBinaryExpression(translate.NewScope(), operator)) + var ( + propertyLookup = func(property string) *pgsql.BinaryExpression { + return pgsql.NewPropertyLookup( + pgsql.CompoundIdentifier{"n", pgsql.ColumnProperties}, + mustAsLiteral(property), + ) + } + renderComparison = func(t *testing.T, lOperand pgsql.Expression, operator pgsql.Operator, rOperand pgsql.Expression) string { + t.Helper() - formatted, err := format.Expression(treeTranslator.PeekOperand(), format.NewOutputBuilder()) - require.NoError(t, err) + treeTranslator := translate.NewExpressionTreeTranslator(nil) + treeTranslator.PushOperand(lOperand) + treeTranslator.PushOperand(rOperand) + require.NoError(t, treeTranslator.CompleteBinaryExpression(translate.NewScope(), operator)) - return formatted - } + formatted, err := format.Expression(treeTranslator.PeekOperand(), format.NewOutputBuilder()) + require.NoError(t, err) - testCases := []struct { - Name string - LOperand pgsql.Expression - Operator pgsql.Operator - ROperand pgsql.Expression - Expected string - }{{ - Name: "string literal uses typed text property lookup", - LOperand: propertyLookup("isassignabletorole"), - Operator: pgsql.OperatorEquals, - ROperand: mustAsLiteral("true"), - Expected: "(jsonb_typeof((n.properties -> 'isassignabletorole')) = 'string' and (n.properties ->> 'isassignabletorole') = 'true')", - }, { - Name: "string literal uses typed text property lookup when reversed", - LOperand: mustAsLiteral("true"), - Operator: pgsql.OperatorEquals, - ROperand: propertyLookup("isassignabletorole"), - Expected: "(jsonb_typeof((n.properties -> 'isassignabletorole')) = 'string' and 'true' = (n.properties ->> 'isassignabletorole'))", - }, { - Name: "numeric-looking string literal remains string typed", - LOperand: propertyLookup("rank"), - Operator: pgsql.OperatorEquals, - ROperand: mustAsLiteral("1"), - Expected: "(jsonb_typeof((n.properties -> 'rank')) = 'string' and (n.properties ->> 'rank') = '1')", - }, { - Name: "text parameter uses typed text property lookup", - LOperand: propertyLookup("objectid"), - Operator: pgsql.OperatorEquals, - ROperand: pgsql.Parameter{Identifier: "pi0", CastType: pgsql.Text}, - Expected: "(jsonb_typeof((n.properties -> 'objectid')) = 'string' and (n.properties ->> 'objectid') = @pi0::text)", - }, { - Name: "text function uses typed text property lookup", - LOperand: propertyLookup("distinguishedname"), - Operator: pgsql.OperatorEquals, - ROperand: pgsql.FunctionCall{ - Function: pgsql.FunctionToUpper, - Parameters: []pgsql.Expression{mustAsLiteral("admin")}, - CastType: pgsql.Text, - }, - Expected: "(jsonb_typeof((n.properties -> 'distinguishedname')) = 'string' and (n.properties ->> 'distinguishedname') = upper('admin')::text)", - }, { - Name: "text function uses typed text property lookup when reversed", - LOperand: pgsql.FunctionCall{ - Function: pgsql.FunctionToUpper, - Parameters: []pgsql.Expression{mustAsLiteral("admin")}, - CastType: pgsql.Text, - }, - Operator: pgsql.OperatorEquals, - ROperand: propertyLookup("distinguishedname"), - Expected: "(jsonb_typeof((n.properties -> 'distinguishedname')) = 'string' and upper('admin')::text = (n.properties ->> 'distinguishedname'))", - }, { - Name: "string inequality keeps non-string JSONB branch", - LOperand: propertyLookup("rank"), - Operator: pgsql.OperatorCypherNotEquals, - ROperand: mustAsLiteral("1"), - Expected: "(jsonb_typeof((n.properties -> 'rank')) = 'string' and (n.properties ->> 'rank') <> '1' or jsonb_typeof((n.properties -> 'rank')) <> 'string' and (n.properties -> 'rank') <> to_jsonb(('1')::text)::jsonb)", - }, { - Name: "boolean literal keeps jsonb scalar equality", - LOperand: propertyLookup("isassignabletorole"), - Operator: pgsql.OperatorEquals, - ROperand: mustAsLiteral(true), - Expected: "((n.properties -> 'isassignabletorole'))::jsonb = to_jsonb((true)::bool)::jsonb", - }, { - Name: "numeric literal keeps jsonb scalar equality", - LOperand: propertyLookup("count"), - Operator: pgsql.OperatorEquals, - ROperand: mustAsLiteral(1), - Expected: "((n.properties -> 'count'))::jsonb = to_jsonb((1)::int8)::jsonb", - }, { - Name: "property to property equality keeps jsonb operands", - LOperand: propertyLookup("left"), - Operator: pgsql.OperatorEquals, - ROperand: propertyLookup("right"), - Expected: "(n.properties -> 'left') = (n.properties -> 'right')", - }} + return formatted + } + testCases = []struct { + Name string + LOperand pgsql.Expression + Operator pgsql.Operator + ROperand pgsql.Expression + Expected string + }{{ + Name: "string literal uses typed text property lookup", + LOperand: propertyLookup("isassignabletorole"), + Operator: pgsql.OperatorEquals, + ROperand: mustAsLiteral("true"), + Expected: "(jsonb_typeof((n.properties -> 'isassignabletorole')) = 'string' and (n.properties ->> 'isassignabletorole') = 'true')", + }, { + Name: "string literal uses typed text property lookup when reversed", + LOperand: mustAsLiteral("true"), + Operator: pgsql.OperatorEquals, + ROperand: propertyLookup("isassignabletorole"), + Expected: "(jsonb_typeof((n.properties -> 'isassignabletorole')) = 'string' and 'true' = (n.properties ->> 'isassignabletorole'))", + }, { + Name: "numeric-looking string literal remains string typed", + LOperand: propertyLookup("rank"), + Operator: pgsql.OperatorEquals, + ROperand: mustAsLiteral("1"), + Expected: "(jsonb_typeof((n.properties -> 'rank')) = 'string' and (n.properties ->> 'rank') = '1')", + }, { + Name: "text parameter uses typed text property lookup", + LOperand: propertyLookup("objectid"), + Operator: pgsql.OperatorEquals, + ROperand: pgsql.Parameter{Identifier: "pi0", CastType: pgsql.Text}, + Expected: "(jsonb_typeof((n.properties -> 'objectid')) = 'string' and (n.properties ->> 'objectid') = @pi0::text)", + }, { + Name: "text function uses typed text property lookup", + LOperand: propertyLookup("distinguishedname"), + Operator: pgsql.OperatorEquals, + ROperand: pgsql.FunctionCall{ + Function: pgsql.FunctionToUpper, + Parameters: []pgsql.Expression{mustAsLiteral("admin")}, + CastType: pgsql.Text, + }, + Expected: "(jsonb_typeof((n.properties -> 'distinguishedname')) = 'string' and (n.properties ->> 'distinguishedname') = upper('admin')::text)", + }, { + Name: "text function uses typed text property lookup when reversed", + LOperand: pgsql.FunctionCall{ + Function: pgsql.FunctionToUpper, + Parameters: []pgsql.Expression{mustAsLiteral("admin")}, + CastType: pgsql.Text, + }, + Operator: pgsql.OperatorEquals, + ROperand: propertyLookup("distinguishedname"), + Expected: "(jsonb_typeof((n.properties -> 'distinguishedname')) = 'string' and upper('admin')::text = (n.properties ->> 'distinguishedname'))", + }, { + Name: "string inequality keeps non-string JSONB branch", + LOperand: propertyLookup("rank"), + Operator: pgsql.OperatorCypherNotEquals, + ROperand: mustAsLiteral("1"), + Expected: "(jsonb_typeof((n.properties -> 'rank')) = 'string' and (n.properties ->> 'rank') <> '1' or jsonb_typeof((n.properties -> 'rank')) <> 'string' and (n.properties -> 'rank') <> to_jsonb(('1')::text)::jsonb)", + }, { + Name: "boolean literal keeps jsonb scalar equality", + LOperand: propertyLookup("isassignabletorole"), + Operator: pgsql.OperatorEquals, + ROperand: mustAsLiteral(true), + Expected: "((n.properties -> 'isassignabletorole'))::jsonb = to_jsonb((true)::bool)::jsonb", + }, { + Name: "numeric literal keeps jsonb scalar equality", + LOperand: propertyLookup("count"), + Operator: pgsql.OperatorEquals, + ROperand: mustAsLiteral(1), + Expected: "((n.properties -> 'count'))::jsonb = to_jsonb((1)::int8)::jsonb", + }, { + Name: "property to property equality keeps jsonb operands", + LOperand: propertyLookup("left"), + Operator: pgsql.OperatorEquals, + ROperand: propertyLookup("right"), + Expected: "(n.properties -> 'left') = (n.properties -> 'right')", + }} + ) for _, testCase := range testCases { t.Run(testCase.Name, func(t *testing.T) { @@ -482,8 +482,11 @@ func TestExpressionTreeTranslator(t *testing.T) { treeTranslator.PopRemainingExpressionsAsUserConstraints() // Pull out the 'a' constraint - aIdentifier := pgsql.AsIdentifierSet("a") - expectedTranslation := "(a.name = 'a' and a.num_a > 1)" + var ( + aIdentifier = pgsql.AsIdentifierSet("a") + expectedTranslation = "(a.name = 'a' and a.num_a > 1)" + ) + validateConstraints(t, treeTranslator, aIdentifier, expectedTranslation) // Pull out the 'b' constraint next diff --git a/cypher/models/pgsql/translate/limit_pushdown_test.go b/cypher/models/pgsql/translate/limit_pushdown_test.go index 0bfd6ef9..e7edcb5f 100644 --- a/cypher/models/pgsql/translate/limit_pushdown_test.go +++ b/cypher/models/pgsql/translate/limit_pushdown_test.go @@ -110,11 +110,13 @@ func limitPushdownTestTail(where pgsql.Expression) pgsql.Select { } func TestLimitPushdownTailSourceAllowsUnidirectionalShortestPathEndpointInequality(t *testing.T) { - part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) - tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( - limitPushdownTestRootAlias, - limitPushdownTestTerminalAlias, - )) + var ( + part = limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect = limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + ) sourceFrame, canPushDown := limitPushdownTailSource(part, tailSelect) require.True(t, canPushDown) @@ -122,11 +124,13 @@ func TestLimitPushdownTailSourceAllowsUnidirectionalShortestPathEndpointInequali } func TestLimitPushdownTailSourceAllowsReversedUnidirectionalShortestPathEndpointInequality(t *testing.T) { - part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) - tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( - limitPushdownTestTerminalAlias, - limitPushdownTestRootAlias, - )) + var ( + part = limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect = limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestTerminalAlias, + limitPushdownTestRootAlias, + )) + ) sourceFrame, canPushDown := limitPushdownTailSource(part, tailSelect) require.True(t, canPushDown) @@ -134,16 +138,18 @@ func TestLimitPushdownTailSourceAllowsReversedUnidirectionalShortestPathEndpoint } func TestLimitPushdownTailSourceBlocksMixedShortestPathWherePredicate(t *testing.T) { - part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) - tailSelect := limitPushdownTestTail(pgsql.NewBinaryExpression( - limitPushdownTestEndpointInequality(limitPushdownTestRootAlias, limitPushdownTestTerminalAlias), - pgsql.OperatorAnd, - pgsql.NewBinaryExpression( - limitPushdownTestEndpointRef(limitPushdownTestTerminalAlias), - pgsql.OperatorGreaterThan, - pgsql.NewLiteral(0, pgsql.Int), - ), - )) + var ( + part = limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect = limitPushdownTestTail(pgsql.NewBinaryExpression( + limitPushdownTestEndpointInequality(limitPushdownTestRootAlias, limitPushdownTestTerminalAlias), + pgsql.OperatorAnd, + pgsql.NewBinaryExpression( + limitPushdownTestEndpointRef(limitPushdownTestTerminalAlias), + pgsql.OperatorGreaterThan, + pgsql.NewLiteral(0, pgsql.Int), + ), + )) + ) _, canPushDown := limitPushdownTailSource(part, tailSelect) require.False(t, canPushDown) @@ -209,11 +215,13 @@ func TestLimitPushdownTailSourceAllowsBoundEndpointShortestPathSourceWithoutTail } func TestLimitPushdownTailSourceAllowsBidirectionalShortestPathEndpointInequality(t *testing.T) { - part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) - tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( - limitPushdownTestRootAlias, - limitPushdownTestTerminalAlias, - )) + var ( + part = limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + tailSelect = limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + ) sourceFrame, canPushDown := limitPushdownTailSource(part, tailSelect) require.True(t, canPushDown) @@ -221,11 +229,13 @@ func TestLimitPushdownTailSourceAllowsBidirectionalShortestPathEndpointInequalit } func TestPushDownShortestPathLimitAppendsHarnessLimitWithEndpointInequality(t *testing.T) { - part := limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) - tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( - limitPushdownTestRootAlias, - limitPushdownTestTerminalAlias, - )) + var ( + part = limitPushdownTestPart(pgsql.FunctionUnidirectionalSPHarness) + tailSelect = limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + ) pushDownShortestPathLimit(part, tailSelect) @@ -246,11 +256,13 @@ func TestPushDownShortestPathLimitAppendsHarnessLimitWithEndpointInequality(t *t } func TestPushDownBidirectionalShortestPathLimitAppendsHarnessLimitWithEndpointInequality(t *testing.T) { - part := limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) - tailSelect := limitPushdownTestTail(limitPushdownTestEndpointInequality( - limitPushdownTestRootAlias, - limitPushdownTestTerminalAlias, - )) + var ( + part = limitPushdownTestPart(pgsql.FunctionBidirectionalSPHarness) + tailSelect = limitPushdownTestTail(limitPushdownTestEndpointInequality( + limitPushdownTestRootAlias, + limitPushdownTestTerminalAlias, + )) + ) pushDownShortestPathLimit(part, tailSelect) diff --git a/cypher/models/pgsql/translate/match.go b/cypher/models/pgsql/translate/match.go index f93a5d5f..2201f4cb 100644 --- a/cypher/models/pgsql/translate/match.go +++ b/cypher/models/pgsql/translate/match.go @@ -84,8 +84,11 @@ func (s *Translator) buildOptionalMatchAggregationStep(aggregationFrame *Frame) // An "aggregation" frame like this will only be triggered after an OPTIONAL MATCH, which should only // take place AFTER `n>=1` previous MATCH expressions. To properly base the aggregation, we need to // join to the origin frame (prior to the OPTIONAL MATCH) based on the OPTIONAL MATCH's frame. - optMatchFrame := aggregationFrame.Previous - originFrame := optMatchFrame.Previous + var ( + optMatchFrame = aggregationFrame.Previous + originFrame = optMatchFrame.Previous + ) + // originFrame could be nil if no previous frame is defined (for ex., leading OPTIONAL MATCH, which is // valid but effectively a plain MATCH) if originFrame == nil { @@ -112,8 +115,11 @@ func (s *Translator) buildOptionalMatchAggregationStep(aggregationFrame *Frame) // Construct the projection for this frame. Just take all of the exports for the "origin" frame // and optional match frame and re-export them // TODO: Does there need to be additional logic for visible/defined bindings, instead of only exports? - originIDExclusions := map[string]struct{}{} - projection := pgsql.Projection{} + var ( + originIDExclusions = map[string]struct{}{} + projection = pgsql.Projection{} + ) + for _, exported := range originFrame.Exported.Slice() { projection = append(projection, &pgsql.AliasedExpression{ Expression: pgsql.CompoundIdentifier{originFrame.Binding.Identifier, exported}, diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index a4090ff9..6bc434cf 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -200,8 +200,10 @@ func hasIDEqualityConstraint(expression pgsql.Expression, identifier pgsql.Ident continue } - leftIsID := isIdentifierIDReference(binaryExpression.LOperand, identifier) - rightIsID := isIdentifierIDReference(binaryExpression.ROperand, identifier) + var ( + leftIsID = isIdentifierIDReference(binaryExpression.LOperand, identifier) + rightIsID = isIdentifierIDReference(binaryExpression.ROperand, identifier) + ) if leftIsID && isStaticIDEqualityOperand(binaryExpression.ROperand) { return true diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index aaae2fd5..ea77c0b9 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -430,14 +430,16 @@ RETURN p func TestOptimizerSafetyReordersIndependentNodeAnchor(t *testing.T) { t.Parallel() - normalizedQuery := optimizerSafetySQL(t, ` -MATCH (a) -MATCH (b:EnterpriseCA {name: 'target'}) -MATCH p = (a)-[:MemberOf]->(b) -RETURN p -`) - enterpriseAnchorIndex := strings.Index(normalizedQuery, "array [5]::int2[]") - broadScanIndex := strings.Index(normalizedQuery, "from s0, node n1") + var ( + normalizedQuery = optimizerSafetySQL(t, ` + MATCH (a) + MATCH (b:EnterpriseCA {name: 'target'}) + MATCH p = (a)-[:MemberOf]->(b) + RETURN p + `) + enterpriseAnchorIndex = strings.Index(normalizedQuery, "array [5]::int2[]") + broadScanIndex = strings.Index(normalizedQuery, "from s0, node n1") + ) require.NotEqual(t, -1, enterpriseAnchorIndex) require.NotEqual(t, -1, broadScanIndex) @@ -605,8 +607,10 @@ LIMIT 100 `) formattedQuery, err := Translated(translation) require.NoError(t, err) - normalizedQuery := strings.Join(strings.Fields(formattedQuery), " ") - lowerQuery := strings.ToLower(normalizedQuery) + var ( + normalizedQuery = strings.Join(strings.Fields(formattedQuery), " ") + lowerQuery = strings.ToLower(normalizedQuery) + ) requirePlannedOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") requireNoOptimizationLowering(t, translation.Optimization, "TraversalDirectionSelection") diff --git a/cypher/models/pgsql/translate/predicate.go b/cypher/models/pgsql/translate/predicate.go index f1e8b708..c233aac6 100644 --- a/cypher/models/pgsql/translate/predicate.go +++ b/cypher/models/pgsql/translate/predicate.go @@ -41,28 +41,31 @@ func (s *Translator) buildOptimizedRelationshipExistPredicate(part *PatternPart, ) if traversalStep.RightNodeBound { - forward := pgsql.NewBinaryExpression( - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}), - pgsql.OperatorAnd, - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}), - ) - reverse := pgsql.NewBinaryExpression( - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}), - pgsql.OperatorAnd, - pgsql.NewBinaryExpression( - pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, - pgsql.OperatorEquals, - pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}), + var ( + forward = pgsql.NewBinaryExpression( + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}), + pgsql.OperatorAnd, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}), + ) + reverse = pgsql.NewBinaryExpression( + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}), + pgsql.OperatorAnd, + pgsql.NewBinaryExpression( + pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID}, + pgsql.OperatorEquals, + pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID}), + ) ) + whereClause = pgsql.NewBinaryExpression(forward, pgsql.OperatorOr, reverse) } diff --git a/cypher/models/pgsql/translate/property.go b/cypher/models/pgsql/translate/property.go index 29f44fb7..ecab62c7 100644 --- a/cypher/models/pgsql/translate/property.go +++ b/cypher/models/pgsql/translate/property.go @@ -61,9 +61,11 @@ func decomposePropertyLookup(expression pgsql.Expression) (PropertyLookup, error } func (s *Translator) buildPatternPropertyConstraints(binding *BoundIdentifier, properties TranslatedProperties) (pgsql.Expression, error) { - var propertyConstraints pgsql.Expression + var ( + propertyConstraints pgsql.Expression + keys = make([]string, 0, len(properties.Map)) + ) - keys := make([]string, 0, len(properties.Map)) for key := range properties.Map { keys = append(keys, key) } diff --git a/cypher/models/pgsql/translate/tracking_test.go b/cypher/models/pgsql/translate/tracking_test.go index cee793fe..9a0f0a92 100644 --- a/cypher/models/pgsql/translate/tracking_test.go +++ b/cypher/models/pgsql/translate/tracking_test.go @@ -30,8 +30,11 @@ func TestScope(t *testing.T) { } func TestScopeLookupDataTypeResolvesAliases(t *testing.T) { - scope := NewScope() - binding := scope.Define(pgsql.Identifier("n0"), pgsql.NodeComposite) + var ( + scope = NewScope() + binding = scope.Define(pgsql.Identifier("n0"), pgsql.NodeComposite) + ) + scope.Alias(pgsql.Identifier("n"), binding) dataType, found := scope.LookupDataType(pgsql.Identifier("n")) diff --git a/cypher/models/pgsql/translate/translator.go b/cypher/models/pgsql/translate/translator.go index 3ba3f153..a5fe459c 100644 --- a/cypher/models/pgsql/translate/translator.go +++ b/cypher/models/pgsql/translate/translator.go @@ -58,8 +58,10 @@ func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters inputParameters[key] = value } - translatedParameters := map[string]any{} - ctxAwareKindMapper := newContextAwareKindMapper(ctx, kindMapper, translatedParameters) + var ( + translatedParameters = map[string]any{} + ctxAwareKindMapper = newContextAwareKindMapper(ctx, kindMapper, translatedParameters) + ) return &Translator{ Visitor: walk.NewVisitor[cypher.SyntaxNode](), @@ -781,8 +783,11 @@ func decodeCypherStringLiteral(raw string) (string, error) { return "", fmt.Errorf("invalid cypher string literal: missing or mismatched surrounding quotes: %q", raw) } // Cypher parser wraps string literals with ' characters - body := raw[1 : len(raw)-1] - var b strings.Builder + var ( + body = raw[1 : len(raw)-1] + b strings.Builder + ) + b.Grow(len(body)) for i := 0; i < len(body); i++ { if body[i] != '\\' { diff --git a/cypher/models/pgsql/translate/traversal.go b/cypher/models/pgsql/translate/traversal.go index 7db07001..25b133c6 100644 --- a/cypher/models/pgsql/translate/traversal.go +++ b/cypher/models/pgsql/translate/traversal.go @@ -776,8 +776,11 @@ func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error var applied int for stepIndex := range part.TraversalSteps { - target := part.Target.TraversalStep(stepIndex) - decisions := s.suffixPushdownDecisions[target] + var ( + target = part.Target.TraversalStep(stepIndex) + decisions = s.suffixPushdownDecisions[target] + ) + if len(decisions) == 0 { continue } @@ -791,8 +794,11 @@ func (s *Translator) applyExpansionSuffixPushdown(part *PatternPart) (int, error continue } - currentStep := part.TraversalSteps[stepIndex] - suffixSteps := part.TraversalSteps[decision.SuffixStartStep : decision.SuffixEndStep+1] + var ( + currentStep = part.TraversalSteps[stepIndex] + suffixSteps = part.TraversalSteps[decision.SuffixStartStep : decision.SuffixEndStep+1] + ) + if candidateApplied, err := applyExpansionSuffixPushdownCandidate(currentStep, suffixSteps); err != nil { return applied, err } else if candidateApplied { diff --git a/cypher/models/pgsql/translate/with.go b/cypher/models/pgsql/translate/with.go index b8b66f60..38860366 100644 --- a/cypher/models/pgsql/translate/with.go +++ b/cypher/models/pgsql/translate/with.go @@ -74,8 +74,10 @@ func (s *Translator) translateWith() error { if binding, isBound := s.scope.Lookup(typedSelectItem); !isBound { return fmt.Errorf("unable to lookup identifer %s for with statement", typedSelectItem) } else { - var selectItem pgsql.SelectItem - projectedBinding := binding + var ( + selectItem pgsql.SelectItem + projectedBinding = binding + ) if projectionItem.Alias.Set { if aliasBinding, aliasBound := s.scope.AliasedLookup(projectionItem.Alias.Value); !aliasBound || aliasBinding.Identifier != binding.Identifier { diff --git a/integration/cypher_template_test.go b/integration/cypher_template_test.go index 63a90655..204b29fc 100644 --- a/integration/cypher_template_test.go +++ b/integration/cypher_template_test.go @@ -82,14 +82,16 @@ func TestCypherTemplates(t *testing.T) { t.Run(family.Name, func(t *testing.T) { for _, variant := range family.Variants { t.Run(variant.Name, func(t *testing.T) { - cypher := renderCypherTemplate(t, family.Template, variant.Vars) - check := parseAssertion(t, variant.Assert) - tc := testCase{ - Name: variant.Name, - Cypher: cypher, - Params: mergeParams(family.Params, variant.Params), - Fixture: family.Fixture, - } + var ( + cypher = renderCypherTemplate(t, family.Template, variant.Vars) + check = parseAssertion(t, variant.Assert) + tc = testCase{ + Name: variant.Name, + Cypher: cypher, + Params: mergeParams(family.Params, variant.Params), + Fixture: family.Fixture, + } + ) runWithTemplateFixture(t, ctx, db, tc, check) }) @@ -198,22 +200,24 @@ func runWithTemplateFixture(t *testing.T, ctx context.Context, db graph.Database t.Fatal("template cases must define an inline fixture") } - queryErrorObserved := false - err := db.WriteTransaction(ctx, func(tx graph.Transaction) error { - idMap, err := opengraph.WriteGraphTx(tx, tc.Fixture) - if err != nil { - return fmt.Errorf("creating fixture: %w", err) - } + var ( + queryErrorObserved = false + err = db.WriteTransaction(ctx, func(tx graph.Transaction) error { + idMap, err := opengraph.WriteGraphTx(tx, tc.Fixture) + if err != nil { + return fmt.Errorf("creating fixture: %w", err) + } - result := tx.Query(tc.Cypher, tc.Params) - defer result.Close() - assertion.checkResult(t, result, newAssertionContext(idMap)) - if assertion.expectQueryError { - queryErrorObserved = true - } + result := tx.Query(tc.Cypher, tc.Params) + defer result.Close() + assertion.checkResult(t, result, newAssertionContext(idMap)) + if assertion.expectQueryError { + queryErrorObserved = true + } - return errFixtureRollback - }) + return errFixtureRollback + }) + ) if assertion.expectQueryError && queryErrorObserved && err != nil { return @@ -241,13 +245,18 @@ func runMetamorphicFamily(t *testing.T, ctx context.Context, db graph.Database, return fmt.Errorf("creating fixture: %w", err) } - assertCtx := newAssertionContext(idMap) - var baselineName string - var baseline []string + var ( + assertCtx = newAssertionContext(idMap) + baselineName string + baseline []string + ) for _, query := range family.Queries { - result := tx.Query(query.Cypher, query.Params) - collected := collectResult(t, result) + var ( + result = tx.Query(query.Cypher, query.Params) + collected = collectResult(t, result) + ) + result.Close() signature := comparisonSignature(t, collected, assertCtx, family.Compare) diff --git a/integration/cypher_test.go b/integration/cypher_test.go index 334a9168..085f81da 100644 --- a/integration/cypher_test.go +++ b/integration/cypher_test.go @@ -66,8 +66,10 @@ func TestCypher(t *testing.T) { dataset string files []caseFile } - groups := map[string]*group{} - var datasetNames []string + var ( + groups = map[string]*group{} + datasetNames []string + ) for _, path := range files { raw, err := os.ReadFile(path) @@ -262,16 +264,19 @@ var errFixtureRollback = errors.New("fixture rollback") func runReadOnly(t *testing.T, ctx context.Context, db graph.Database, idMap opengraph.IDMap, tc testCase, assertion caseAssertion) { t.Helper() - queryErrorObserved := false - err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { - result := tx.Query(tc.Cypher, tc.Params) - defer result.Close() - assertion.checkResult(t, result, newAssertionContext(idMap)) - if assertion.expectQueryError { - queryErrorObserved = true - } - return nil - }) + var ( + queryErrorObserved = false + err = db.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Query(tc.Cypher, tc.Params) + defer result.Close() + assertion.checkResult(t, result, newAssertionContext(idMap)) + if assertion.expectQueryError { + queryErrorObserved = true + } + return nil + }) + ) + if err != nil { if assertion.expectQueryError && queryErrorObserved { return @@ -286,26 +291,28 @@ func runReadOnly(t *testing.T, ctx context.Context, db graph.Database, idMap ope func runWithFixture(t *testing.T, ctx context.Context, db graph.Database, tc testCase, assertion caseAssertion) { t.Helper() - queryErrorObserved := false - err := db.WriteTransaction(ctx, func(tx graph.Transaction) error { - if err := tx.Nodes().Delete(); err != nil { - return fmt.Errorf("clearing graph before fixture: %w", err) - } + var ( + queryErrorObserved = false + err = db.WriteTransaction(ctx, func(tx graph.Transaction) error { + if err := tx.Nodes().Delete(); err != nil { + return fmt.Errorf("clearing graph before fixture: %w", err) + } - idMap, err := opengraph.WriteGraphTx(tx, tc.Fixture) - if err != nil { - return fmt.Errorf("creating fixture: %w", err) - } + idMap, err := opengraph.WriteGraphTx(tx, tc.Fixture) + if err != nil { + return fmt.Errorf("creating fixture: %w", err) + } - result := tx.Query(tc.Cypher, tc.Params) - defer result.Close() - assertion.checkResult(t, result, newAssertionContext(idMap)) - if assertion.expectQueryError { - queryErrorObserved = true - } + result := tx.Query(tc.Cypher, tc.Params) + defer result.Close() + assertion.checkResult(t, result, newAssertionContext(idMap)) + if assertion.expectQueryError { + queryErrorObserved = true + } - return errFixtureRollback - }) + return errFixtureRollback + }) + ) if assertion.expectQueryError && queryErrorObserved && err != nil { return @@ -792,8 +799,10 @@ func assertNodeListIDs(expected [][]string) resultAssertion { func collectNodeIDs(t *testing.T, result queryResult, ctx assertionContext, unique bool) []string { t.Helper() - ids := make([]string, 0, len(result.rows)) - seen := map[string]bool{} + var ( + ids = make([]string, 0, len(result.rows)) + seen = map[string]bool{} + ) for _, row := range result.rows { for _, rawVal := range row.values { diff --git a/integration/harness.go b/integration/harness.go index 8853d128..5075b562 100644 --- a/integration/harness.go +++ b/integration/harness.go @@ -86,9 +86,11 @@ func SetupDBWithKindsNoGraphCleanup(t *testing.T, extraNodeKinds, extraEdgeKinds func setupDB(t *testing.T, cleanupGraph bool, extraNodeKinds, extraEdgeKinds graph.Kinds, datasets ...string) (graph.Database, context.Context) { t.Helper() - ctx := context.Background() + var ( + ctx = context.Background() + connStr = os.Getenv("CONNECTION_STRING") + ) - connStr := os.Getenv("CONNECTION_STRING") if connStr == "" { t.Skip("CONNECTION_STRING env var is not set") } @@ -214,22 +216,24 @@ func LoadDataset(t *testing.T, db graph.Database, ctx context.Context, name stri func QueryPaths(t *testing.T, ctx context.Context, db graph.Database, cypher string) []graph.Path { t.Helper() - var paths []graph.Path - - err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { - result := tx.Query(cypher, nil) - defer result.Close() - - for result.Next() { - var p graph.Path - if err := result.Scan(&p); err != nil { - return fmt.Errorf("scan error: %w", err) + var ( + paths []graph.Path + err = db.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Query(cypher, nil) + defer result.Close() + + for result.Next() { + var p graph.Path + if err := result.Scan(&p); err != nil { + return fmt.Errorf("scan error: %w", err) + } + paths = append(paths, p) } - paths = append(paths, p) - } - return result.Error() - }) + return result.Error() + }) + ) + if err != nil { t.Fatalf("query failed: %v", err) } @@ -247,25 +251,27 @@ func QueryNodeIDs(t *testing.T, ctx context.Context, db graph.Database, cypher s rev[dbID] = fid } - var ids []string - seen := make(map[string]bool) - - err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { - result := tx.Query(cypher, nil) - defer result.Close() - - for result.Next() { - var n graph.Node - if err := result.Scan(&n); err != nil { - return err - } - if fid, ok := rev[n.ID]; ok && !seen[fid] { - ids = append(ids, fid) - seen[fid] = true + var ( + ids []string + seen = make(map[string]bool) + err = db.ReadTransaction(ctx, func(tx graph.Transaction) error { + result := tx.Query(cypher, nil) + defer result.Close() + + for result.Next() { + var n graph.Node + if err := result.Scan(&n); err != nil { + return err + } + if fid, ok := rev[n.ID]; ok && !seen[fid] { + ids = append(ids, fid) + seen[fid] = true + } } - } - return result.Error() - }) + return result.Error() + }) + ) + if err != nil { t.Fatalf("query failed: %v", err) } @@ -302,9 +308,11 @@ func AssertPaths(t *testing.T, paths []graph.Path, idMap opengraph.IDMap, expect rev[dbID] = fixtureID } - toSig := func(ids []string) string { return strings.Join(ids, ",") } + var ( + toSig = func(ids []string) string { return strings.Join(ids, ",") } + got = make([]string, len(paths)) + ) - got := make([]string, len(paths)) for i, p := range paths { ids := make([]string, len(p.Nodes)) for j, node := range p.Nodes { diff --git a/integration/pgsql_aggregate_traversal_plan_test.go b/integration/pgsql_aggregate_traversal_plan_test.go index f59e7c49..37064152 100644 --- a/integration/pgsql_aggregate_traversal_plan_test.go +++ b/integration/pgsql_aggregate_traversal_plan_test.go @@ -171,8 +171,11 @@ func TestPostgreSQLLiveAggregateTraversalCountPlanShape(t *testing.T) { } } - limitMatch := regexp.MustCompile(`(?m)->\s+Limit\b`).FindStringIndex(plan) - sourceMaterializationIndex := strings.LastIndex(plan, "Index Scan using node_") + var ( + limitMatch = regexp.MustCompile(`(?m)->\s+Limit\b`).FindStringIndex(plan) + sourceMaterializationIndex = strings.LastIndex(plan, "Index Scan using node_") + ) + if limitMatch == nil || sourceMaterializationIndex < 0 || sourceMaterializationIndex < limitMatch[0] { t.Fatalf("expected source node materialization after top-N limiting, got:\n%s", plan) } diff --git a/tools/metrics/internal/metrics/quality.go b/tools/metrics/internal/metrics/quality.go index 3514ddfe..5eceb748 100644 --- a/tools/metrics/internal/metrics/quality.go +++ b/tools/metrics/internal/metrics/quality.go @@ -331,9 +331,11 @@ func analyzeSemanticDrift(sourceRoot string) SemanticDriftReport { } func countCoreCases(sourceRoot string) (int, int, []QualityFinding) { - var files int - var cases int - var findings []QualityFinding + var ( + files int + cases int + findings []QualityFinding + ) for _, path := range jsonFiles(filepath.Join(sourceRoot, "integration", "testdata", "cases")) { files++ @@ -358,8 +360,10 @@ type templateCount struct { } func countTemplates(sourceRoot string) (templateCount, []QualityFinding) { - var counts templateCount - var findings []QualityFinding + var ( + counts templateCount + findings []QualityFinding + ) for _, path := range jsonFiles(filepath.Join(sourceRoot, "integration", "testdata", "templates")) { counts.files++ @@ -405,8 +409,11 @@ func changedGeneratedArtifacts(sourceRoot string) ([]string, *QualityFinding) { return nil, &finding } - var changed []string - scanner := bufio.NewScanner(strings.NewReader(string(output))) + var ( + changed []string + scanner = bufio.NewScanner(strings.NewReader(string(output))) + ) + for scanner.Scan() { line := scanner.Text() if len(line) < 4 { @@ -472,8 +479,11 @@ func analyzeBackendEquivalence(results []NamedPath) BackendEquivalenceReport { sort.Strings(sortedKeys) for _, key := range sortedKeys { - statuses := map[string]string{} - missing := false + var ( + statuses = map[string]string{} + missing = false + ) + for driverName, tests := range driverTests { status, found := tests[key] if !found { @@ -529,9 +539,11 @@ type goTestEvent struct { } func parseBackendTestResult(result NamedPath) (map[string]string, BackendDriverResult, []QualityFinding) { - summary := BackendDriverResult{Name: result.Name, Path: result.Path} - findings := []QualityFinding{} - tests := map[string]string{} + var ( + summary = BackendDriverResult{Name: result.Name, Path: result.Path} + findings = []QualityFinding{} + tests = map[string]string{} + ) file, err := os.Open(result.Path) if err != nil { @@ -638,8 +650,10 @@ func analyzeInvariants(sourceRoot string) InvariantReport { } func validateCases(path string, doc qualityCaseFile, report *InvariantReport) []QualityFinding { - var findings []QualityFinding - seen := map[string]struct{}{} + var ( + findings []QualityFinding + seen = map[string]struct{}{} + ) for _, testCase := range doc.Cases { contextName := testCase.Name @@ -669,8 +683,10 @@ func validateCases(path string, doc qualityCaseFile, report *InvariantReport) [] } func validateTemplateFile(path string, doc qualityTemplateFile, report *InvariantReport) []QualityFinding { - var findings []QualityFinding - familyNames := map[string]struct{}{} + var ( + findings []QualityFinding + familyNames = map[string]struct{}{} + ) for _, family := range doc.Families { if family.Name == "" { @@ -687,8 +703,11 @@ func validateTemplateFile(path string, doc qualityTemplateFile, report *Invarian findings = append(findings, fileFinding("invariants", "high", "template_family_missing_template", path, family.Name+" has no template")) } - placeholders := placeholderNames(family.Template) - variantNames := map[string]struct{}{} + var ( + placeholders = placeholderNames(family.Template) + variantNames = map[string]struct{}{} + ) + for _, variant := range family.Variants { contextName := family.Name + "/" + variant.Name if variant.Name == "" { @@ -786,54 +805,59 @@ func analyzeFuzz(sourceRoot, resultPath string) FuzzReport { } func discoverFuzzTargets(sourceRoot string) ([]FuzzTarget, []QualityFinding) { - var targets []FuzzTarget - var findings []QualityFinding + var ( + targets []FuzzTarget + findings []QualityFinding + err = filepath.WalkDir(sourceRoot, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if shouldSkipDirectory(sourceRoot, path, entry.Name()) { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(entry.Name(), "_test.go") { + return nil + } - err := filepath.WalkDir(sourceRoot, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() { - if shouldSkipDirectory(sourceRoot, path, entry.Name()) { - return filepath.SkipDir + relativePath, err := relativeSlashPath(sourceRoot, path) + if err != nil { + return err } - return nil - } - if !strings.HasSuffix(entry.Name(), "_test.go") { - return nil - } - relativePath, err := relativeSlashPath(sourceRoot, path) - if err != nil { - return err - } + fileSet := token.NewFileSet() + parsedFile, err := parser.ParseFile(fileSet, path, nil, 0) + if err != nil { + findings = append(findings, fileFinding("fuzz", "info", "fuzz_file_parse_error", relativePath, err.Error())) + return nil + } - fileSet := token.NewFileSet() - parsedFile, err := parser.ParseFile(fileSet, path, nil, 0) - if err != nil { - findings = append(findings, fileFinding("fuzz", "info", "fuzz_file_parse_error", relativePath, err.Error())) - return nil - } + for _, declaration := range parsedFile.Decls { + function, ok := declaration.(*ast.FuncDecl) + if !ok || !strings.HasPrefix(function.Name.Name, "Fuzz") { + continue + } - for _, declaration := range parsedFile.Decls { - function, ok := declaration.(*ast.FuncDecl) - if !ok || !strings.HasPrefix(function.Name.Name, "Fuzz") { - continue + var ( + position = fileSet.Position(function.Pos()) + target = FuzzTarget{ + Package: parsedFile.Name.Name, + Name: function.Name.Name, + File: relativePath, + Line: position.Line, + CorpusFiles: countCorpusFiles(filepath.Join(filepath.Dir(path), "testdata", "fuzz", function.Name.Name)), + } + ) + + targets = append(targets, target) } - position := fileSet.Position(function.Pos()) - target := FuzzTarget{ - Package: parsedFile.Name.Name, - Name: function.Name.Name, - File: relativePath, - Line: position.Line, - CorpusFiles: countCorpusFiles(filepath.Join(filepath.Dir(path), "testdata", "fuzz", function.Name.Name)), - } - targets = append(targets, target) - } + return nil + }) + ) - return nil - }) if err != nil { findings = append(findings, QualityFinding{ Signal: "fuzz", @@ -845,8 +869,11 @@ func discoverFuzzTargets(sourceRoot string) ([]FuzzTarget, []QualityFinding) { } sort.SliceStable(targets, func(leftIndex, rightIndex int) bool { - left := targets[leftIndex] - right := targets[rightIndex] + var ( + left = targets[leftIndex] + right = targets[rightIndex] + ) + if left.File != right.File { return left.File < right.File } @@ -870,10 +897,13 @@ func parseFuzzResult(path string) (int, int, []QualityFinding) { } defer file.Close() - var crashes int - var timeouts int - var findings []QualityFinding - scanner := bufio.NewScanner(file) + var ( + crashes int + timeouts int + findings []QualityFinding + scanner = bufio.NewScanner(file) + ) + for scanner.Scan() { var event goTestEvent if err := json.Unmarshal(scanner.Bytes(), &event); err != nil { @@ -1046,8 +1076,11 @@ func analyzeBenchmarkDrift(options QualityOptions) BenchmarkDriftReport { return report } - currentByKey := benchmarkResultsByKey(current) - baselineByKey := benchmarkResultsByKey(baseline) + var ( + currentByKey = benchmarkResultsByKey(current) + baselineByKey = benchmarkResultsByKey(baseline) + ) + report.Results = len(currentByKey) report.BaselineResults = len(baselineByKey) @@ -1093,14 +1126,17 @@ func (s *BenchmarkDriftReport) compareBenchmarkMetric(key, metric string, baseli return } - delta := (float64(current) - float64(baseline)) / float64(baseline) - record := BenchmarkRegression{ - Key: key, - Metric: metric, - BaselineNanos: baseline, - CurrentNanos: current, - DeltaFraction: delta, - } + var ( + delta = (float64(current) - float64(baseline)) / float64(baseline) + record = BenchmarkRegression{ + Key: key, + Metric: metric, + BaselineNanos: baseline, + CurrentNanos: current, + DeltaFraction: delta, + } + ) + if delta > s.RegressionThreshold { s.Regressions = append(s.Regressions, record) } else if delta < -s.RegressionThreshold { @@ -1128,15 +1164,17 @@ func benchmarkResultsByKey(report benchmarkInputReport) map[string]benchmarkInpu } func summarizeQuality(report QualityReport) QualitySummary { - summary := QualitySummary{} - statuses := []string{ - report.SemanticDrift.Status, - report.BackendEquivalence.Status, - report.Invariants.Status, - report.Fuzz.Status, - report.Mutation.Status, - report.BenchmarkDrift.Status, - } + var ( + summary = QualitySummary{} + statuses = []string{ + report.SemanticDrift.Status, + report.BackendEquivalence.Status, + report.Invariants.Status, + report.Fuzz.Status, + report.Mutation.Status, + report.BenchmarkDrift.Status, + } + ) for _, status := range statuses { switch status { @@ -1175,8 +1213,11 @@ func qualityFindings(report QualityReport) []QualityFinding { findings = append(findings, report.BenchmarkDrift.Findings...) sort.SliceStable(findings, func(leftIndex, rightIndex int) bool { - left := findings[leftIndex] - right := findings[rightIndex] + var ( + left = findings[leftIndex] + right = findings[rightIndex] + ) + if severityRank(left.Severity) != severityRank(right.Severity) { return severityRank(left.Severity) > severityRank(right.Severity) } @@ -1207,8 +1248,11 @@ func hasFindingAtLeast(findings []QualityFinding, minimum string) bool { } func countFindingsAtLeast(findings []QualityFinding, minimum string) int { - minimumRank := severityRank(minimum) - var count int + var ( + minimumRank = severityRank(minimum) + count int + ) + for _, finding := range findings { if severityRank(finding.Severity) >= minimumRank { count++ From e8ae48a4ce511b1c25e01f30105b2dcabafff334 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 12:51:48 -0700 Subject: [PATCH 100/114] fix(pgsql): handle optimizer locality selectivity edge cases --- cypher/models/pgsql/optimize/locality.go | 21 ++++-- .../models/pgsql/optimize/optimizer_test.go | 66 +++++++++++++++++++ cypher/models/pgsql/optimize/selectivity.go | 16 ++--- 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/cypher/models/pgsql/optimize/locality.go b/cypher/models/pgsql/optimize/locality.go index 05ceccef..b32d19f2 100644 --- a/cypher/models/pgsql/optimize/locality.go +++ b/cypher/models/pgsql/optimize/locality.go @@ -7,10 +7,23 @@ import ( // FlattenConjunction collects the leaf operands of a left-recursive AND chain. func FlattenConjunction(expr pgsql.Expression) []pgsql.Expression { - if bin, typeOK := expr.(*pgsql.BinaryExpression); !typeOK || bin.Operator != pgsql.OperatorAnd { - return []pgsql.Expression{expr} - } else { + switch bin := expr.(type) { + case *pgsql.BinaryExpression: + if bin == nil || bin.Operator != pgsql.OperatorAnd { + return []pgsql.Expression{expr} + } + + return append(FlattenConjunction(bin.LOperand), FlattenConjunction(bin.ROperand)...) + + case pgsql.BinaryExpression: + if bin.Operator != pgsql.OperatorAnd { + return []pgsql.Expression{expr} + } + return append(FlattenConjunction(bin.LOperand), FlattenConjunction(bin.ROperand)...) + + default: + return []pgsql.Expression{expr} } } @@ -61,7 +74,7 @@ func SubqueryReferencesOnlyLocalIdentifiers(subquery pgsql.Subquery, localScope } func QueryReferencesOnlyLocalIdentifiers(query pgsql.Query, localScope *pgsql.IdentifierSet) bool { - if query.CommonTableExpressions != nil { + if query.CommonTableExpressions != nil && len(query.CommonTableExpressions.Expressions) > 0 { return false } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index f4321426..75fd9f9a 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -1226,6 +1226,47 @@ func TestSelectReferencesOnlyLocalIdentifiersValidatesJoinConstraintsIncremental require.False(t, SelectReferencesOnlyLocalIdentifiers(selectBody, pgsql.NewIdentifierSet())) } +func TestFlattenConjunctionHandlesValueBinaryExpressions(t *testing.T) { + t.Parallel() + + var ( + left = pgsql.NewLiteral(true, pgsql.Boolean) + right = pgsql.NewLiteral(false, pgsql.Boolean) + expr = pgsql.BinaryExpression{ + LOperand: left, + Operator: pgsql.OperatorAnd, + ROperand: right, + } + ) + + terms := FlattenConjunction(expr) + + require.Len(t, terms, 2) + require.Equal(t, left, terms[0]) + require.Equal(t, right, terms[1]) +} + +func TestQueryReferencesOnlyLocalIdentifiersAllowsEmptyWith(t *testing.T) { + t.Parallel() + + query := pgsql.Query{ + CommonTableExpressions: &pgsql.With{}, + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{pgsql.Identifier("n0"), pgsql.ColumnID}, + }, + From: []pgsql.FromClause{{ + Source: pgsql.TableReference{ + Name: pgsql.CompoundIdentifier{pgsql.TableNode}, + Binding: models.OptionalValue(pgsql.Identifier("n0")), + }, + }}, + }, + } + + require.True(t, QueryReferencesOnlyLocalIdentifiers(query, pgsql.NewIdentifierSet())) +} + func TestMeasureSelectivityPopReturnsTopFrame(t *testing.T) { t.Parallel() @@ -1238,6 +1279,31 @@ func TestMeasureSelectivityPopReturnsTopFrame(t *testing.T) { require.Equal(t, 7, visitor.Selectivity()) } +func TestMeasureSelectivityScoresIDBonusOnlyForPointPredicates(t *testing.T) { + t.Parallel() + + var ( + model = NewSelectivityModel(nil) + idRef = pgsql.CompoundIdentifier{pgsql.Identifier("n0"), pgsql.ColumnID} + literal = pgsql.NewLiteral(1, pgsql.Int) + equality = pgsql.NewBinaryExpression(idRef, pgsql.OperatorEquals, literal) + rangeOp = pgsql.NewBinaryExpression(idRef, pgsql.OperatorGreaterThan, literal) + notEqual = pgsql.NewBinaryExpression(idRef, pgsql.OperatorNotEquals, literal) + ) + + equalityScore, err := model.Measure(equality) + require.NoError(t, err) + require.Equal(t, selectivityWeightNarrowSearch+selectivityWeightEntityIDReference, equalityScore) + + rangeScore, err := model.Measure(rangeOp) + require.NoError(t, err) + require.Equal(t, selectivityWeightRangeComparison, rangeScore) + + notEqualScore, err := model.Measure(notEqual) + require.NoError(t, err) + require.Equal(t, selectivityWeightNotEquals, notEqualScore) +} + func TestCollectReferencedSourceIdentifiersIgnoresMatchDeclarations(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/optimize/selectivity.go b/cypher/models/pgsql/optimize/selectivity.go index 1d33aba2..2a77732f 100644 --- a/cypher/models/pgsql/optimize/selectivity.go +++ b/cypher/models/pgsql/optimize/selectivity.go @@ -173,14 +173,6 @@ func (s *measureSelectivityVisitor) Enter(node pgsql.SyntaxNode) { rOperandIsID = isColumnIDRef(typedNode.ROperand) ) - if lOperandIsID && !rOperandIsID { - // Point lookup: n0.id = ; highly selective. - s.addSelectivity(selectivityWeightEntityIDReference) - } else if rOperandIsID && !lOperandIsID { - // Canonically unusual, but handle it the same. - s.addSelectivity(selectivityWeightEntityIDReference) - } - // If both sides are ID refs, this is a join condition; do not score as a point lookup. switch typedNode.Operator { case pgsql.OperatorOr: @@ -198,7 +190,13 @@ func (s *measureSelectivityVisitor) Enter(node pgsql.SyntaxNode) { case pgsql.OperatorLike, pgsql.OperatorILike, pgsql.OperatorRegexMatch, pgsql.OperatorSimilarTo: s.addSelectivity(selectivityWeightStringSearch) - case pgsql.OperatorIn, pgsql.OperatorEquals, pgsql.OperatorIs: + case pgsql.OperatorIn, pgsql.OperatorEquals: + s.addSelectivity(selectivityWeightNarrowSearch) + if (lOperandIsID && !rOperandIsID) || (rOperandIsID && !lOperandIsID) { + s.addSelectivity(selectivityWeightEntityIDReference) + } + + case pgsql.OperatorIs: s.addSelectivity(selectivityWeightNarrowSearch) case pgsql.OperatorPGArrayOverlap, pgsql.OperatorArrayOverlap: From ea6e05dc6e1180380026d56becc14887adfb9edc Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 12:53:09 -0700 Subject: [PATCH 101/114] fix(pgsql): harden lowering plan carryover --- cypher/models/pgsql/optimize/lowering_plan.go | 12 +++-- .../models/pgsql/optimize/optimizer_test.go | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index a1fa7a0a..52e4baa4 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -68,7 +68,11 @@ func BuildLoweringPlan(query *cypher.RegularQuery, predicateAttachments []Predic declareReadingClauseSymbols(currentSymbols, part.ReadingClauses) declareReadingClauseSelectivity(currentSelectivity, part.ReadingClauses) - carriedSymbols, carriedSelectivity = carryProjectionSelectivity(part.With.Projection, currentSymbols, currentSelectivity) + if part.With == nil { + carriedSymbols, carriedSelectivity = currentSymbols, currentSelectivity + } else { + carriedSymbols, carriedSelectivity = carryProjectionSelectivity(part.With.Projection, currentSymbols, currentSelectivity) + } } if finalPart := query.SingleQuery.MultiPartQuery.SinglePartQuery; finalPart != nil { @@ -609,9 +613,11 @@ func declareReadingClauseSymbols(symbols map[string]struct{}, readingClauses []* func declareReadingClauseSelectivity(symbols map[string]boundSourceSelectivity, readingClauses []*cypher.ReadingClause) { for _, readingClause := range readingClauses { - if readingClause != nil { - declareSelectiveMatchSymbols(symbols, readingClause.Match) + if readingClause == nil || readingClause.Match == nil || readingClause.Match.Optional { + continue } + + declareSelectiveMatchSymbols(symbols, readingClause.Match) } } diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 75fd9f9a..5da3a070 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -810,6 +810,34 @@ func TestLoweringPlanSkipsBoundLeftDirectionAfterGreedyProjectionLimit(t *testin }}, plan.LoweringPlan.TraversalDirection) } +func TestLoweringPlanCarriesBindingsAcrossNilWithPart(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (u:User {objectid: 'S-1-5-21-1-1000'}) + WITH u + MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer {name: 'target'}) + RETURN c + `) + require.NoError(t, err) + require.NotNil(t, regularQuery.SingleQuery.MultiPartQuery) + require.NotEmpty(t, regularQuery.SingleQuery.MultiPartQuery.Parts) + regularQuery.SingleQuery.MultiPartQuery.Parts[0].With = nil + + plan, err := BuildLoweringPlan(regularQuery, nil) + require.NoError(t, err) + require.Contains(t, plan.Decisions(), LoweringDecision{Name: LoweringTraversalDirection}) + require.Contains(t, plan.TraversalDirection, TraversalDirectionDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 1, + ClauseIndex: 0, + PatternIndex: 0, + StepIndex: 0, + }, + Reason: traversalDirectionReasonBoundSourceSelective, + }) +} + func TestLoweringPlanAllowsUniqueRightEndpointAfterPriorLimit(t *testing.T) { t.Parallel() @@ -1191,6 +1219,23 @@ func TestLoweringPlanSkipsOptionalMatchLimitPushdown(t *testing.T) { require.Empty(t, plan.LoweringPlan.LimitPushdown) } +func TestDeclareReadingClauseSelectivitySkipsOptionalMatch(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (n {objectid: 'S-1-5-21-1-1000'}) + OPTIONAL MATCH (m {objectid: 'S-1-5-21-1-2000'}) + RETURN n, m + `) + require.NoError(t, err) + + selectivity := map[string]boundSourceSelectivity{} + declareReadingClauseSelectivity(selectivity, regularQuery.SingleQuery.SinglePartQuery.ReadingClauses) + + require.Equal(t, boundSourceSelectivityUnique, selectivity["n"]) + require.NotContains(t, selectivity, "m") +} + func TestSelectReferencesOnlyLocalIdentifiersValidatesJoinConstraintsIncrementally(t *testing.T) { t.Parallel() From 3ad94370b3e47679207a86fd998451daf90bbfe8 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 12:53:50 -0700 Subject: [PATCH 102/114] fix(graphbench): correct p95 ranking --- cmd/graphbench/results.go | 3 ++- cmd/graphbench/results_test.go | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go index 48b11250..ef5a3182 100644 --- a/cmd/graphbench/results.go +++ b/cmd/graphbench/results.go @@ -114,10 +114,11 @@ func computeDurationStats(durations []time.Duration) (DurationStats, error) { }) n := len(sortedDurations) + p95Index := (95*n+99)/100 - 1 return DurationStats{ Iterations: n, Median: sortedDurations[n/2], - P95: sortedDurations[min(n*95/100, n-1)], + P95: sortedDurations[p95Index], Max: sortedDurations[n-1], }, nil } diff --git a/cmd/graphbench/results_test.go b/cmd/graphbench/results_test.go index 11671641..0ee87344 100644 --- a/cmd/graphbench/results_test.go +++ b/cmd/graphbench/results_test.go @@ -47,3 +47,16 @@ func TestComputeDurationStatsCopiesAndSortsDurations(t *testing.T) { require.Equal(t, 10*time.Millisecond, durations[1]) require.Equal(t, 20*time.Millisecond, durations[2]) } + +func TestComputeDurationStatsUsesNearestRankP95(t *testing.T) { + durations := make([]time.Duration, 20) + for idx := range durations { + durations[idx] = time.Duration(idx+1) * time.Millisecond + } + + stats, err := computeDurationStats(durations) + + require.NoError(t, err) + require.Equal(t, 19*time.Millisecond, stats.P95) + require.Equal(t, 20*time.Millisecond, stats.Max) +} From 6a22865f87c20c3184e1ff3bcc346a76c2f3dc9f Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 12:54:36 -0700 Subject: [PATCH 103/114] fix(plancorpus): close records on write errors --- cmd/plancorpus/main.go | 12 +++++++- cmd/plancorpus/main_test.go | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/cmd/plancorpus/main.go b/cmd/plancorpus/main.go index 57a5a22a..152a0beb 100644 --- a/cmd/plancorpus/main.go +++ b/cmd/plancorpus/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "flag" "fmt" + "io" "os" "path/filepath" ) @@ -141,14 +142,23 @@ func writePlanRecords(path string, records []PlanRecord) error { if err != nil { return fmt.Errorf("create %s: %w", path, err) } - defer out.Close() + return writePlanRecordsTo(out, path, records) +} + +func writePlanRecordsTo(out io.WriteCloser, path string, records []PlanRecord) error { encoder := json.NewEncoder(out) for _, record := range records { if err := encoder.Encode(record); err != nil { + if closeErr := out.Close(); closeErr != nil { + return fmt.Errorf("write %s: %w; close %s: %w", path, err, path, closeErr) + } return fmt.Errorf("write %s: %w", path, err) } } + if err := out.Close(); err != nil { + return fmt.Errorf("close %s: %w", path, err) + } return nil } diff --git a/cmd/plancorpus/main_test.go b/cmd/plancorpus/main_test.go index deff5ec1..17aca49a 100644 --- a/cmd/plancorpus/main_test.go +++ b/cmd/plancorpus/main_test.go @@ -1,11 +1,24 @@ package main import ( + "bytes" + "errors" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" ) +type closeErrorWriter struct { + bytes.Buffer + err error +} + +func (s *closeErrorWriter) Close() error { + return s.err +} + func TestCaptureSpecs(t *testing.T) { specs, err := captureSpecs(commandConfig{ Connection: "neo4j://neo4j:password@localhost:7687", @@ -27,6 +40,51 @@ func TestCaptureSpecsRequiresConnection(t *testing.T) { require.ErrorContains(t, err, "no connection string supplied") } +func TestWritePlanRecordsWritesJSONLines(t *testing.T) { + path := filepath.Join(t.TempDir(), "records.jsonl") + + err := writePlanRecords(path, []PlanRecord{{ + Driver: "pg", + Source: "cases/example.json", + Name: "example", + Cypher: "MATCH (n) RETURN n", + }}) + require.NoError(t, err) + + contents, err := os.ReadFile(path) + require.NoError(t, err) + require.JSONEq(t, `{ + "driver": "pg", + "source": "cases/example.json", + "name": "example", + "cypher": "MATCH (n) RETURN n" + }`, string(bytes.TrimSpace(contents))) +} + +func TestWritePlanRecordsToReturnsCloseError(t *testing.T) { + writer := &closeErrorWriter{err: errors.New("close failed")} + + err := writePlanRecordsTo(writer, "records.jsonl", nil) + + require.ErrorContains(t, err, "close records.jsonl") + require.ErrorContains(t, err, "close failed") +} + +func TestWritePlanRecordsToClosesAfterEncodeError(t *testing.T) { + writer := &closeErrorWriter{err: errors.New("close failed")} + + err := writePlanRecordsTo(writer, "records.jsonl", []PlanRecord{{ + Driver: "pg", + Name: "bad params", + Params: map[string]any{"bad": make(chan int)}, + }}) + + require.ErrorContains(t, err, "write records.jsonl") + require.ErrorContains(t, err, "unsupported type") + require.ErrorContains(t, err, "close records.jsonl") + require.ErrorContains(t, err, "close failed") +} + func TestDriverFromConnectionString(t *testing.T) { driverName, err := driverFromConnectionString("postgresql://postgres:password@localhost/db") require.NoError(t, err) From ec81c141394e5ca154fa4ef3052f62cc2a56495e Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 13:15:43 -0700 Subject: [PATCH 104/114] test(integration): seed abuse delegation fixture kind --- integration/testdata/cases/optimizer_inline.json | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index fe2253d5..387db07e 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -312,6 +312,7 @@ "edges": [ {"start_id": "domain-a", "end_id": "domain-b", "kind": "CrossForestTrust"}, {"start_id": "domain-a", "end_id": "domain-b", "kind": "SpoofSIDHistory"}, + {"start_id": "domain-a", "end_id": "domain-b", "kind": "AbuseTGTDelegation"}, {"start_id": "domain-a", "end_id": "domain-c", "kind": "CrossForestTrust"} ] }, From f5b892d44b753c766cbc8e976f1a7edf5896ffd5 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 13:16:21 -0700 Subject: [PATCH 105/114] test(integration): assert connected cross-forest trust paths --- integration/testdata/cases/optimizer_inline.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index 387db07e..1b60acaa 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -316,7 +316,16 @@ {"start_id": "domain-a", "end_id": "domain-c", "kind": "CrossForestTrust"} ] }, - "assert": "non_empty" + "assert": { + "path_edge_kinds": [ + ["CrossForestTrust"], + ["CrossForestTrust"], + ["SpoofSIDHistory"], + ["SpoofSIDHistory"], + ["AbuseTGTDelegation"], + ["AbuseTGTDelegation"] + ] + } }, { "name": "common search azure high privileged role bounded membership expansion", From 228f20fb1d9774fb780bbe0ed05b2164662b44d1 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 13:19:07 -0700 Subject: [PATCH 106/114] test(integration): guard fixture-backed kind seeding --- integration/fixture_kind_guard_test.go | 188 ++++++++++++++++++ .../testdata/cases/optimizer_inline.json | 10 +- 2 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 integration/fixture_kind_guard_test.go diff --git a/integration/fixture_kind_guard_test.go b/integration/fixture_kind_guard_test.go new file mode 100644 index 00000000..701da3ed --- /dev/null +++ b/integration/fixture_kind_guard_test.go @@ -0,0 +1,188 @@ +package integration + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/cypher" + "github.com/specterops/dawgs/cypher/models/walk" + "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/opengraph" +) + +type fixtureKindCaseFile struct { + Dataset string `json:"dataset"` + Cases []fixtureKindCase `json:"cases"` +} + +type fixtureKindCase struct { + Name string `json:"name"` + Cypher string `json:"cypher"` + Fixture *opengraph.Graph `json:"fixture,omitempty"` +} + +type kindStringSet map[string]struct{} + +type referencedKindSets struct { + Node kindStringSet + Edge kindStringSet +} + +func TestFixtureBackedCasesSeedReferencedKinds(t *testing.T) { + files, err := filepath.Glob(filepath.Join("testdata", "cases", "*.json")) + if err != nil { + t.Fatalf("failed to glob case files: %v", err) + } + + datasetKindCache := map[string]referencedKindSets{} + + for _, path := range files { + caseFile := readFixtureKindCaseFile(t, path) + dataset := caseFile.Dataset + if dataset == "" { + dataset = "base" + } + + datasetKinds, hasDatasetKinds := datasetKindCache[dataset] + if !hasDatasetKinds { + datasetKinds = readDatasetKindSets(t, dataset) + datasetKindCache[dataset] = datasetKinds + } + + for _, testCase := range caseFile.Cases { + if testCase.Fixture == nil { + continue + } + + t.Run(fmt.Sprintf("%s/%s", filepath.Base(path), testCase.Name), func(t *testing.T) { + referencedKinds := collectReferencedKinds(t, testCase.Cypher) + seededKinds := datasetKinds.Clone() + + fixtureNodeKinds, fixtureEdgeKinds := testCase.Fixture.Kinds() + seededKinds.Node.AddKinds(fixtureNodeKinds) + seededKinds.Edge.AddKinds(fixtureEdgeKinds) + + if missingNodeKinds := referencedKinds.Node.MissingFrom(seededKinds.Node); len(missingNodeKinds) > 0 { + t.Errorf("fixture does not seed referenced node kinds: %v", missingNodeKinds) + } + + if missingEdgeKinds := referencedKinds.Edge.MissingFrom(seededKinds.Edge); len(missingEdgeKinds) > 0 { + t.Errorf("fixture does not seed referenced edge kinds: %v", missingEdgeKinds) + } + }) + } + } +} + +func readFixtureKindCaseFile(t *testing.T, path string) fixtureKindCaseFile { + t.Helper() + + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read %s: %v", path, err) + } + + var caseFile fixtureKindCaseFile + if err := json.Unmarshal(raw, &caseFile); err != nil { + t.Fatalf("failed to decode %s: %v", path, err) + } + + return caseFile +} + +func readDatasetKindSets(t *testing.T, dataset string) referencedKindSets { + t.Helper() + + file, err := os.Open(datasetPath(dataset)) + if err != nil { + t.Fatalf("failed to open dataset %q for kind scanning: %v", dataset, err) + } + defer file.Close() + + document, err := opengraph.ParseDocument(file) + if err != nil { + t.Fatalf("failed to parse dataset %q: %v", dataset, err) + } + + nodeKinds, edgeKinds := document.Graph.Kinds() + return newReferencedKindSets(nodeKinds, edgeKinds) +} + +func collectReferencedKinds(t *testing.T, cypherQuery string) referencedKindSets { + t.Helper() + + query, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) + if err != nil { + t.Fatalf("failed to parse Cypher: %v", err) + } + + referencedKinds := newReferencedKindSets(nil, nil) + if err := walk.Cypher(query, walk.NewSimpleVisitor[cypher.SyntaxNode](func(node cypher.SyntaxNode, _ walk.VisitorHandler) { + switch typedNode := node.(type) { + case *cypher.NodePattern: + referencedKinds.Node.AddKinds(typedNode.Kinds) + + case *cypher.RelationshipPattern: + referencedKinds.Edge.AddKinds(typedNode.Kinds) + + } + })); err != nil { + t.Fatalf("failed to walk Cypher AST: %v", err) + } + + return referencedKinds +} + +func newReferencedKindSets(nodeKinds, edgeKinds graph.Kinds) referencedKindSets { + return referencedKindSets{ + Node: newKindStringSet(nodeKinds), + Edge: newKindStringSet(edgeKinds), + } +} + +func (s referencedKindSets) Clone() referencedKindSets { + return referencedKindSets{ + Node: s.Node.Clone(), + Edge: s.Edge.Clone(), + } +} + +func newKindStringSet(kinds graph.Kinds) kindStringSet { + set := kindStringSet{} + set.AddKinds(kinds) + return set +} + +func (s kindStringSet) AddKinds(kinds graph.Kinds) { + for _, kind := range kinds { + if kind != nil { + s[kind.String()] = struct{}{} + } + } +} + +func (s kindStringSet) Clone() kindStringSet { + clone := make(kindStringSet, len(s)) + for kind := range s { + clone[kind] = struct{}{} + } + + return clone +} + +func (s kindStringSet) MissingFrom(other kindStringSet) []string { + var missing []string + for kind := range s { + if _, found := other[kind]; !found { + missing = append(missing, kind) + } + } + + sort.Strings(missing) + return missing +} diff --git a/integration/testdata/cases/optimizer_inline.json b/integration/testdata/cases/optimizer_inline.json index 1b60acaa..96c9a2a7 100644 --- a/integration/testdata/cases/optimizer_inline.json +++ b/integration/testdata/cases/optimizer_inline.json @@ -60,6 +60,7 @@ {"id": "template-bad", "kinds": ["CertTemplate"], "properties": {"authenticationenabled": true, "requiresmanagerapproval": false, "enrolleesuppliessubject": true, "schemaversion": 2, "authorizedsignatures": 1}}, {"id": "ca", "kinds": ["EnterpriseCA"]}, {"id": "root", "kinds": ["RootCA"]}, + {"id": "unused-root", "kinds": ["RootCA"]}, {"id": "domain", "kinds": ["Domain"]} ], "edges": [ @@ -73,6 +74,7 @@ {"start_id": "template-sig", "end_id": "ca", "kind": "PublishedTo"}, {"start_id": "template-bad", "end_id": "ca", "kind": "PublishedTo"}, {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "ca", "end_id": "unused-root", "kind": "EnterpriseCAFor"}, {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} ] }, @@ -103,7 +105,8 @@ {"id": "ca", "kinds": ["EnterpriseCA"]}, {"id": "store", "kinds": ["NTAuthStore"]}, {"id": "domain", "kinds": ["Domain"]}, - {"id": "root", "kinds": ["RootCA"]} + {"id": "root", "kinds": ["RootCA"]}, + {"id": "unused-root", "kinds": ["RootCA"]} ], "edges": [ {"start_id": "n", "end_id": "p1-a", "kind": "MemberOf"}, @@ -119,6 +122,7 @@ {"start_id": "template-a", "end_id": "ca", "kind": "PublishedTo"}, {"start_id": "template-b", "end_id": "ca", "kind": "PublishedTo"}, {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "ca", "end_id": "unused-root", "kind": "EnterpriseCAFor"}, {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} ] }, @@ -161,7 +165,8 @@ {"id": "ca", "kinds": ["EnterpriseCA"]}, {"id": "store", "kinds": ["NTAuthStore"]}, {"id": "domain", "kinds": ["Domain"]}, - {"id": "root", "kinds": ["RootCA"]} + {"id": "root", "kinds": ["RootCA"]}, + {"id": "unused-root", "kinds": ["RootCA"]} ], "edges": [ {"start_id": "n", "end_id": "p1-a", "kind": "MemberOf"}, @@ -177,6 +182,7 @@ {"start_id": "template-a", "end_id": "ca", "kind": "PublishedTo"}, {"start_id": "template-b", "end_id": "ca", "kind": "PublishedTo"}, {"start_id": "ca", "end_id": "root", "kind": "IssuedSignedBy"}, + {"start_id": "ca", "end_id": "unused-root", "kind": "EnterpriseCAFor"}, {"start_id": "root", "end_id": "domain", "kind": "RootCAFor"} ] }, From 9659bd088317cb7bd99906e826f152eb386b0dd9 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 17:45:40 -0700 Subject: [PATCH 107/114] test(pgsql): cover aggregate traversal predicate parameters --- .../pgsql/translate/optimizer_safety_test.go | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index ea77c0b9..a4fd4d38 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -69,10 +69,16 @@ func optimizerSafetySQL(t *testing.T, cypherQuery string) string { func optimizerSafetyTranslation(t *testing.T, cypherQuery string) Result { t.Helper() + return optimizerSafetyTranslationWithParameters(t, cypherQuery, nil) +} + +func optimizerSafetyTranslationWithParameters(t *testing.T, cypherQuery string, parameters map[string]any) Result { + t.Helper() + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), cypherQuery) require.NoError(t, err) - translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), nil, DefaultGraphID) + translation, err := Translate(context.Background(), regularQuery, optimizerSafetyKindMapper(), parameters, DefaultGraphID) require.NoError(t, err) return translation @@ -789,6 +795,63 @@ LIMIT 100 require.Contains(t, normalizedQuery, "join terminal_nodes on terminal_nodes.id = traversal.next_id") } +func TestOptimizerSafetyAggregateTraversalCountUsesDistinctPredicateParameters(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslationWithParameters(t, ` +MATCH (u:User) +WHERE u.enabled = $sourceEnabled +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WHERE c.enabled = $terminalEnabled +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `, map[string]any{ + "sourceEnabled": true, + "terminalEnabled": false, + }) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(strings.ToLower(formattedQuery)), " ") + + parameterValues := make([]any, 0, len(translation.Parameters)) + for _, value := range translation.Parameters { + parameterValues = append(parameterValues, value) + } + + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, normalizedQuery, "source_node.properties -> 'enabled'") + require.Contains(t, normalizedQuery, "terminal_node.properties -> 'enabled'") + require.Len(t, translation.Parameters, 2) + require.ElementsMatch(t, []any{true, false}, parameterValues) +} + +func TestOptimizerSafetyAggregateTraversalCountReusesPredicateParameter(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslationWithParameters(t, ` +MATCH (u:User) +WHERE u.enabled = $enabled +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WHERE c.enabled = $enabled +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `, map[string]any{ + "enabled": true, + }) + formattedQuery, err := Translated(translation) + require.NoError(t, err) + normalizedQuery := strings.Join(strings.Fields(strings.ToLower(formattedQuery)), " ") + + requireOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + require.Contains(t, normalizedQuery, "source_node.properties -> 'enabled'") + require.Contains(t, normalizedQuery, "terminal_node.properties -> 'enabled'") + require.Len(t, translation.Parameters, 1) +} + func TestOptimizerSafetyAggregateTraversalCountSkipsUnsafeWideningCandidates(t *testing.T) { t.Parallel() From 50645cf4e1d1268ac882bce263e3b51fb11ce037 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 17:46:14 -0700 Subject: [PATCH 108/114] refactor(pgsql): add aggregate predicate translator helper --- cypher/models/pgsql/translate/aggregate_traversal_count.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cypher/models/pgsql/translate/aggregate_traversal_count.go b/cypher/models/pgsql/translate/aggregate_traversal_count.go index 7fe92f0a..d9e84a39 100644 --- a/cypher/models/pgsql/translate/aggregate_traversal_count.go +++ b/cypher/models/pgsql/translate/aggregate_traversal_count.go @@ -49,6 +49,13 @@ func (s *Translator) translateAggregateTraversalCount(query *cypher.RegularQuery return true, nil } +func (s *Translator) newAggregatePredicateTranslator() *Translator { + translator := NewTranslator(s.ctx, s.kindMapper.kindMapper, s.parameters, s.graphID) + translator.scope.generator = s.scope.generator + + return translator +} + func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraversalCountShape) (pgsql.Query, error) { candidateSources, err := s.buildAggregateCandidateSourcesCTE(shape) if err != nil { From 91f6e202d52ab03a79178bb1037dc0eb82f6af33 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 17:46:38 -0700 Subject: [PATCH 109/114] fix(pgsql): share aggregate predicate parameter namespace --- .../translate/aggregate_traversal_count.go | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/cypher/models/pgsql/translate/aggregate_traversal_count.go b/cypher/models/pgsql/translate/aggregate_traversal_count.go index d9e84a39..a059bad4 100644 --- a/cypher/models/pgsql/translate/aggregate_traversal_count.go +++ b/cypher/models/pgsql/translate/aggregate_traversal_count.go @@ -57,7 +57,9 @@ func (s *Translator) newAggregatePredicateTranslator() *Translator { } func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraversalCountShape) (pgsql.Query, error) { - candidateSources, err := s.buildAggregateCandidateSourcesCTE(shape) + predicateTranslator := s.newAggregatePredicateTranslator() + + candidateSources, err := s.buildAggregateCandidateSourcesCTE(shape, predicateTranslator) if err != nil { return pgsql.Query{}, err } @@ -67,7 +69,7 @@ func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraver return pgsql.Query{}, err } - terminalNodes, err := s.buildAggregateTerminalNodesCTE(shape) + terminalNodes, err := s.buildAggregateTerminalNodesCTE(shape, predicateTranslator) if err != nil { return pgsql.Query{}, err } @@ -130,8 +132,8 @@ func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraver }, nil } -func (s *Translator) buildAggregateCandidateSourcesCTE(shape optimize.AggregateTraversalCountShape) (pgsql.CommonTableExpression, error) { - whereClause, err := s.aggregateSourceWhere(shape) +func (s *Translator) buildAggregateCandidateSourcesCTE(shape optimize.AggregateTraversalCountShape, predicateTranslator *Translator) (pgsql.CommonTableExpression, error) { + whereClause, err := s.aggregateSourceWhere(shape, predicateTranslator) if err != nil { return pgsql.CommonTableExpression{}, err } @@ -336,8 +338,8 @@ func (s *Translator) buildAggregateTerminalHitsCTE(shape optimize.AggregateTrave }, nil } -func (s *Translator) buildAggregateTerminalNodesCTE(shape optimize.AggregateTraversalCountShape) (pgsql.CommonTableExpression, error) { - terminalWhere, err := s.aggregateTerminalWhere(shape) +func (s *Translator) buildAggregateTerminalNodesCTE(shape optimize.AggregateTraversalCountShape, predicateTranslator *Translator) (pgsql.CommonTableExpression, error) { + terminalWhere, err := s.aggregateTerminalWhere(shape, predicateTranslator) if err != nil { return pgsql.CommonTableExpression{}, err } @@ -407,13 +409,13 @@ func (s *Translator) buildAggregateRankedCTE(shape optimize.AggregateTraversalCo } } -func (s *Translator) aggregateSourceWhere(shape optimize.AggregateTraversalCountShape) (pgsql.Expression, error) { +func (s *Translator) aggregateSourceWhere(shape optimize.AggregateTraversalCountShape, predicateTranslator *Translator) (pgsql.Expression, error) { sourceKindConstraint, err := s.aggregateNodeKindConstraint(aggregateSourceAlias, shape.SourceKinds) if err != nil { return nil, err } - sourcePredicate, err := s.aggregateSourcePredicate(shape) + sourcePredicate, err := s.aggregateSourcePredicate(shape, predicateTranslator) if err != nil { return nil, err } @@ -421,13 +423,13 @@ func (s *Translator) aggregateSourceWhere(shape optimize.AggregateTraversalCount return pgsql.OptionalAnd(sourcePredicate, sourceKindConstraint), nil } -func (s *Translator) aggregateTerminalWhere(shape optimize.AggregateTraversalCountShape) (pgsql.Expression, error) { +func (s *Translator) aggregateTerminalWhere(shape optimize.AggregateTraversalCountShape, predicateTranslator *Translator) (pgsql.Expression, error) { terminalKindConstraint, err := s.aggregateNodeKindConstraint(aggregateTerminalAlias, shape.TerminalKinds) if err != nil { return nil, err } - terminalPredicate, err := s.aggregateBindingPredicate(shape.TerminalMatch, shape.TerminalSymbol, aggregateTerminalAlias) + terminalPredicate, err := s.aggregateBindingPredicate(predicateTranslator, shape.TerminalMatch, shape.TerminalSymbol, aggregateTerminalAlias) if err != nil { return nil, err } @@ -435,20 +437,16 @@ func (s *Translator) aggregateTerminalWhere(shape optimize.AggregateTraversalCou return pgsql.OptionalAnd(terminalPredicate, terminalKindConstraint), nil } -func (s *Translator) aggregateSourcePredicate(shape optimize.AggregateTraversalCountShape) (pgsql.Expression, error) { - return s.aggregateBindingPredicate(shape.SourceMatch, shape.SourceSymbol, aggregateSourceAlias) +func (s *Translator) aggregateSourcePredicate(shape optimize.AggregateTraversalCountShape, predicateTranslator *Translator) (pgsql.Expression, error) { + return s.aggregateBindingPredicate(predicateTranslator, shape.SourceMatch, shape.SourceSymbol, aggregateSourceAlias) } -func (s *Translator) aggregateBindingPredicate(match *cypher.Match, symbol string, alias pgsql.Identifier) (pgsql.Expression, error) { +func (s *Translator) aggregateBindingPredicate(translator *Translator, match *cypher.Match, symbol string, alias pgsql.Identifier) (pgsql.Expression, error) { if match == nil || match.Where == nil { return nil, nil } - var ( - translator = NewTranslator(s.ctx, s.kindMapper.kindMapper, s.parameters, s.graphID) - binding = translator.scope.Define(alias, pgsql.NodeComposite) - ) - + binding := translator.scope.Define(alias, pgsql.NodeComposite) translator.scope.Alias(pgsql.Identifier(symbol), binding) if err := walk.Cypher(match.Where, translator); err != nil { From c086747f82251a704e425420a2690ad9c0e663a9 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 17:46:56 -0700 Subject: [PATCH 110/114] refactor(pgsql): merge aggregate predicate parameters once --- .../translate/aggregate_traversal_count.go | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/cypher/models/pgsql/translate/aggregate_traversal_count.go b/cypher/models/pgsql/translate/aggregate_traversal_count.go index a059bad4..836a65e0 100644 --- a/cypher/models/pgsql/translate/aggregate_traversal_count.go +++ b/cypher/models/pgsql/translate/aggregate_traversal_count.go @@ -74,6 +74,10 @@ func (s *Translator) aggregateTraversalCountQuery(shape optimize.AggregateTraver return pgsql.Query{}, err } + if err := s.mergeAggregatePredicateParameters(predicateTranslator); err != nil { + return pgsql.Query{}, err + } + terminalHits, err := s.buildAggregateTerminalHitsCTE(shape) if err != nil { return pgsql.Query{}, err @@ -423,6 +427,18 @@ func (s *Translator) aggregateSourceWhere(shape optimize.AggregateTraversalCount return pgsql.OptionalAnd(sourcePredicate, sourceKindConstraint), nil } +func (s *Translator) mergeAggregatePredicateParameters(translator *Translator) error { + for key, value := range translator.translation.Parameters { + if existingValue, hasExisting := s.translation.Parameters[key]; hasExisting && !reflect.DeepEqual(existingValue, value) { + return fmt.Errorf("aggregate traversal parameter collision for %s", key) + } + + s.translation.Parameters[key] = value + } + + return nil +} + func (s *Translator) aggregateTerminalWhere(shape optimize.AggregateTraversalCountShape, predicateTranslator *Translator) (pgsql.Expression, error) { terminalKindConstraint, err := s.aggregateNodeKindConstraint(aggregateTerminalAlias, shape.TerminalKinds) if err != nil { @@ -466,14 +482,6 @@ func (s *Translator) aggregateBindingPredicate(translator *Translator, match *cy return nil, fmt.Errorf("unsupported aggregate traversal predicate dependencies: %v", remainingConstraints.Dependencies.Slice()) } - for key, value := range translator.translation.Parameters { - if existingValue, hasExisting := s.translation.Parameters[key]; hasExisting && !reflect.DeepEqual(existingValue, value) { - return nil, fmt.Errorf("aggregate traversal parameter collision for %s", key) - } - - s.translation.Parameters[key] = value - } - return sourceConstraints.Expression, nil } From 78692bd6825002a2d99c03d0313d25d0081aba80 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 17:47:31 -0700 Subject: [PATCH 111/114] test(pgsql): retain aggregate traversal dependency guard --- .../pgsql/translate/optimizer_safety_test.go | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cypher/models/pgsql/translate/optimizer_safety_test.go b/cypher/models/pgsql/translate/optimizer_safety_test.go index a4fd4d38..0369d50c 100644 --- a/cypher/models/pgsql/translate/optimizer_safety_test.go +++ b/cypher/models/pgsql/translate/optimizer_safety_test.go @@ -938,6 +938,26 @@ LIMIT 100 } } +func TestOptimizerSafetyAggregateTraversalCountSkipsParameterizedCorrelatedTerminalFilter(t *testing.T) { + t.Parallel() + + translation := optimizerSafetyTranslationWithParameters(t, ` +MATCH (u:User) +WHERE u.enabled = $enabled +MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) +WHERE c.name = u.name AND c.enabled = $enabled +WITH DISTINCT u, COUNT(c) AS adminCount +RETURN u +ORDER BY adminCount DESC +LIMIT 100 + `, map[string]any{ + "enabled": true, + }) + + requireNoPlannedOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) + requireNoOptimizationLowering(t, translation.Optimization, optimize.LoweringAggregateTraversalCount) +} + func TestOptimizerSafetyAggregateTraversalCountSkipsObservedTerminal(t *testing.T) { t.Parallel() From ccd4e78c75adcbd495844037e2edfe37bfc915bf Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 17:48:14 -0700 Subject: [PATCH 112/114] test: validate aggregate traversal predicate fix Validated with:\n\n- go test ./cypher/models/pgsql/translate -run 'AggregateTraversalCount'\n- go test ./cypher/models/pgsql/optimize -run 'AggregateTraversalCount'\n- make test From 010b481441edce569d00579f0b5b2aaedea13426 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 17:49:11 -0700 Subject: [PATCH 113/114] test: smoke downstream DAWGS consumers Validated representative BHE and BHCE packages against the local DAWGS checkout with a temporary Go workspace. From 531911adbd4db142c9c548e19c939d0554a1ba46 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Sun, 24 May 2026 18:44:54 -0700 Subject: [PATCH 114/114] fix: address coderabbit review findings --- cmd/graphbench/results.go | 8 +- cmd/graphbench/summary.go | 6 +- cmd/graphbench/summary_test.go | 23 +++++ cmd/plancorpus/report.go | 85 ++++++++++++++----- cmd/plancorpus/report_test.go | 22 +++++ .../pgsql/optimize/OPTIMIZATION_PLAN.md | 2 +- cypher/models/pgsql/optimize/analysis.go | 7 +- cypher/models/pgsql/optimize/analysis_test.go | 18 +++- cypher/models/pgsql/optimize/locality.go | 16 +++- cypher/models/pgsql/optimize/lowering.go | 9 +- cypher/models/pgsql/optimize/lowering_plan.go | 16 ++-- .../models/pgsql/optimize/optimizer_test.go | 52 ++++++++++++ .../pgsql/optimize/pattern_predicates.go | 55 ++++++++++++ cypher/models/pgsql/translate/model.go | 4 +- cypher/models/pgsql/translate/tracking.go | 1 + .../models/pgsql/translate/tracking_test.go | 15 ++++ 16 files changed, 292 insertions(+), 47 deletions(-) diff --git a/cmd/graphbench/results.go b/cmd/graphbench/results.go index ef5a3182..f333b327 100644 --- a/cmd/graphbench/results.go +++ b/cmd/graphbench/results.go @@ -130,7 +130,7 @@ func applyRowExpectation(result *CaseResult) { } } -func writeJSONLFile(path string, records []CaseResult) error { +func writeJSONLFile(path string, records []CaseResult) (err error) { if path == "" { return writeJSONL(os.Stdout, records) } @@ -143,7 +143,11 @@ func writeJSONLFile(path string, records []CaseResult) error { if err != nil { return err } - defer output.Close() + defer func() { + if closeErr := output.Close(); err == nil && closeErr != nil { + err = closeErr + } + }() return writeJSONL(output, records) } diff --git a/cmd/graphbench/summary.go b/cmd/graphbench/summary.go index c2255fc9..ba21fd9a 100644 --- a/cmd/graphbench/summary.go +++ b/cmd/graphbench/summary.go @@ -154,7 +154,11 @@ func buildSummary(records []CaseResult) Summary { return summary.Cases[i].Dataset < summary.Cases[j].Dataset } - return summary.Cases[i].Name < summary.Cases[j].Name + if summary.Cases[i].Name != summary.Cases[j].Name { + return summary.Cases[i].Name < summary.Cases[j].Name + } + + return summary.Cases[i].Source < summary.Cases[j].Source }) sortBaselineEntries(summary.Regressions, true) diff --git a/cmd/graphbench/summary_test.go b/cmd/graphbench/summary_test.go index b73e03ad..e7a080bd 100644 --- a/cmd/graphbench/summary_test.go +++ b/cmd/graphbench/summary_test.go @@ -57,6 +57,29 @@ func TestApplyBaseline(t *testing.T) { require.Equal(t, 5*time.Millisecond, records[0].Baseline.Change) } +func TestBuildSummarySortsCaseSourceTieBreaker(t *testing.T) { + summary := buildSummary([]CaseResult{ + { + Source: "cases/b.json", + Dataset: "shared", + Name: "duplicate", + ExecutionMode: ModePostgresSQL, + Status: StatusOK, + }, + { + Source: "cases/a.json", + Dataset: "shared", + Name: "duplicate", + ExecutionMode: ModePostgresSQL, + Status: StatusOK, + }, + }) + + require.Len(t, summary.Cases, 2) + require.Equal(t, "cases/a.json", summary.Cases[0].Source) + require.Equal(t, "cases/b.json", summary.Cases[1].Source) +} + func TestWriteMarkdownSummary(t *testing.T) { var ( summary = buildSummary([]CaseResult{ diff --git a/cmd/plancorpus/report.go b/cmd/plancorpus/report.go index 5e62f1a6..654067cc 100644 --- a/cmd/plancorpus/report.go +++ b/cmd/plancorpus/report.go @@ -241,59 +241,100 @@ func writeJSONSummary(w io.Writer, summary PlanSummary) error { } func writeMarkdownSummary(w io.Writer, summary PlanSummary) error { - writeCounts := func(title string, counts []Count, limit int) { + writef := func(format string, args ...any) error { + _, err := fmt.Fprintf(w, format, args...) + return err + } + + writeln := func(args ...any) error { + _, err := fmt.Fprintln(w, args...) + return err + } + + writeCounts := func(title string, counts []Count, limit int) error { if len(counts) == 0 { - return + return nil + } + if err := writef("\n## %s\n\n| Name | Count |\n| --- | ---: |\n", title); err != nil { + return err } - fmt.Fprintf(w, "\n## %s\n\n| Name | Count |\n| --- | ---: |\n", title) for idx, count := range counts { if limit > 0 && idx >= limit { break } - fmt.Fprintf(w, "| %s | %d |\n", markdownCell(count.Name), count.Count) + if err := writef("| %s | %d |\n", markdownCell(count.Name), count.Count); err != nil { + return err + } } + return nil } - fmt.Fprintln(w, "# Cypher Plan Corpus Summary") - fmt.Fprintln(w, "\n## Drivers\n\n| Driver | Records | Errors |\n| --- | ---: | ---: |") + if err := writeln("# Cypher Plan Corpus Summary"); err != nil { + return err + } + if err := writeln("\n## Drivers\n\n| Driver | Records | Errors |\n| --- | ---: | ---: |"); err != nil { + return err + } for _, driver := range summary.Drivers { - fmt.Fprintf(w, "| %s | %d | %d |\n", markdownCell(driver.Driver), driver.Records, driver.Errors) + if err := writef("| %s | %d | %d |\n", markdownCell(driver.Driver), driver.Records, driver.Errors); err != nil { + return err + } } if len(summary.TopPostgresPlans) > 0 { - fmt.Fprintln(w, "\n## Top PostgreSQL Plans\n\n| Cost | Source | Name | Root | Lowerings |\n| ---: | --- | --- | --- | --- |") + if err := writeln("\n## Top PostgreSQL Plans\n\n| Cost | Source | Name | Root | Lowerings |\n| ---: | --- | --- | --- | --- |"); err != nil { + return err + } for _, plan := range summary.TopPostgresPlans { - fmt.Fprintf( - w, + if err := writef( "| %.2f | %s | %s | %s | %s |\n", plan.Cost, markdownCell(plan.Source), markdownCell(plan.Name), markdownCell(plan.PlanRoot), markdownCell(strings.Join(plan.PlannedLowerings, ", ")), - ) + ); err != nil { + return err + } } } - writeCounts("Feature Counts", summary.FeatureCounts, 0) - writeCounts("Planned Lowerings", summary.PlannedLowerings, 0) - writeCounts("Applied Lowerings", summary.AppliedLowerings, 0) - writeCounts("Skipped Lowerings", summary.SkippedLowerings, 0) - writeCounts("Skipped Lowering Reasons", summary.SkippedReasons, 0) - writeCounts("PostgreSQL Operators", summary.PostgresOperators, 25) - writeCounts("Neo4j Operators", summary.Neo4jOperators, 25) + if err := writeCounts("Feature Counts", summary.FeatureCounts, 0); err != nil { + return err + } + if err := writeCounts("Planned Lowerings", summary.PlannedLowerings, 0); err != nil { + return err + } + if err := writeCounts("Applied Lowerings", summary.AppliedLowerings, 0); err != nil { + return err + } + if err := writeCounts("Skipped Lowerings", summary.SkippedLowerings, 0); err != nil { + return err + } + if err := writeCounts("Skipped Lowering Reasons", summary.SkippedReasons, 0); err != nil { + return err + } + if err := writeCounts("PostgreSQL Operators", summary.PostgresOperators, 25); err != nil { + return err + } + if err := writeCounts("Neo4j Operators", summary.Neo4jOperators, 25); err != nil { + return err + } if len(summary.Errors) > 0 { - fmt.Fprintln(w, "\n## Capture Errors\n\n| Driver | Source | Name | Error |\n| --- | --- | --- | --- |") + if err := writeln("\n## Capture Errors\n\n| Driver | Source | Name | Error |\n| --- | --- | --- | --- |"); err != nil { + return err + } for _, captureError := range summary.Errors { - fmt.Fprintf( - w, + if err := writef( "| %s | %s | %s | %s |\n", markdownCell(captureError.Driver), markdownCell(captureError.Source), markdownCell(captureError.Name), markdownCell(captureError.Error), - ) + ); err != nil { + return err + } } } diff --git a/cmd/plancorpus/report_test.go b/cmd/plancorpus/report_test.go index 9074185b..1250acb6 100644 --- a/cmd/plancorpus/report_test.go +++ b/cmd/plancorpus/report_test.go @@ -2,12 +2,21 @@ package main import ( "bytes" + "errors" "testing" "github.com/specterops/dawgs/cypher/models/pgsql/translate" "github.com/stretchr/testify/require" ) +type errorWriter struct { + err error +} + +func (s errorWriter) Write([]byte) (int, error) { + return 0, s.err +} + func TestBuildSummaryRanksPostgresPlansAndCountsSignals(t *testing.T) { var ( records = []PlanRecord{{ @@ -93,6 +102,19 @@ func TestWriteMarkdownSummaryEscapesPipes(t *testing.T) { require.Contains(t, out.String(), "pipe \\| name") } +func TestWriteMarkdownSummaryPropagatesWriterErrors(t *testing.T) { + writeErr := errors.New("write failed") + + err := writeMarkdownSummary(errorWriter{err: writeErr}, PlanSummary{ + Drivers: []DriverSummary{{ + Driver: "pg", + Records: 1, + }}, + }) + + require.ErrorIs(t, err, writeErr) +} + func TestPostgresEstimatedCost(t *testing.T) { require.Equal(t, 1180526.82, postgresEstimatedCost("Hash Join (cost=4136.05..1180526.82 rows=32097 width=68)")) require.Zero(t, postgresEstimatedCost("not a plan")) diff --git a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md index 55bb5eb1..96bd21a6 100644 --- a/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md +++ b/cypher/models/pgsql/optimize/OPTIMIZATION_PLAN.md @@ -80,7 +80,7 @@ Status: completed - Run unit tests. - Run backend-specific integration tests. - Run plan capture and compare summary deltas. - - `quality_backend` passes against `postgres://postgres:bhe4eva@localhost/bhe` and `neo4j://neo4j:neo4jj@localhost:7687`. + - `quality_backend` passes against local PostgreSQL and Neo4j instances configured via environment-provided connection strings, without hardcoded credentials in docs. - Plan corpus capture records 396 PostgreSQL plans and 396 Neo4j plans; remaining capture errors are expected invalid-query cases surfaced by both systems or Neo4j-specific parameter-map syntax rejection. ## Phase 7: Predicate Placement Accounting diff --git a/cypher/models/pgsql/optimize/analysis.go b/cypher/models/pgsql/optimize/analysis.go index 82292034..ae20eef2 100644 --- a/cypher/models/pgsql/optimize/analysis.go +++ b/cypher/models/pgsql/optimize/analysis.go @@ -252,7 +252,12 @@ func analyzeReadingClauses(queryPartIndex int, readingClauses []*cypher.ReadingC } for clauseIndex, readingClause := range readingClauses { - if readingClause == nil || readingClause.Unwind != nil { + if readingClause == nil { + closeRegion() + continue + } + + if readingClause.Unwind != nil { closeRegion() barriers = append(barriers, Barrier{ QueryPartIndex: queryPartIndex, diff --git a/cypher/models/pgsql/optimize/analysis_test.go b/cypher/models/pgsql/optimize/analysis_test.go index 0ea35be7..e55dfab7 100644 --- a/cypher/models/pgsql/optimize/analysis_test.go +++ b/cypher/models/pgsql/optimize/analysis_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/specterops/dawgs/cypher/frontend" + "github.com/specterops/dawgs/cypher/models/cypher" "github.com/stretchr/testify/require" ) @@ -41,13 +42,13 @@ func requireBinding(t *testing.T, bindings []Binding, symbol string, kind Bindin t.Fatalf("expected binding %s:%s in %#v", symbol, kind, bindings) } -func requirePathVariable(t *testing.T, pathVariables []PathVariable, symbol string, relationshipCount int) { +func requirePathVariable(t *testing.T, pathVariables []PathVariable, symbol string, relationshipCount int, expectedVariableLength bool) { t.Helper() for _, pathVariable := range pathVariables { if pathVariable.Symbol == symbol { require.Equal(t, relationshipCount, pathVariable.RelationshipCount) - require.True(t, pathVariable.VariableLength) + require.Equal(t, expectedVariableLength, pathVariable.VariableLength) return } } @@ -86,8 +87,17 @@ func TestAnalyzeIdentifiesEligibleADCSRegion(t *testing.T) { requireBinding(t, region.Bindings, "p1", BindingKindPath) requireBinding(t, region.Bindings, "p2", BindingKindPath) - requirePathVariable(t, region.PathVariables, "p1", 4) - requirePathVariable(t, region.PathVariables, "p2", 5) + requirePathVariable(t, region.PathVariables, "p1", 4, true) + requirePathVariable(t, region.PathVariables, "p2", 5, true) +} + +func TestAnalyzeReadingClausesSkipsNilClauses(t *testing.T) { + t.Parallel() + + regions, barriers := analyzeReadingClauses(0, []*cypher.ReadingClause{nil}) + + require.Empty(t, regions) + require.Empty(t, barriers) } func TestAnalyzeSegmentsRegionsAtSemanticBarriers(t *testing.T) { diff --git a/cypher/models/pgsql/optimize/locality.go b/cypher/models/pgsql/optimize/locality.go index b32d19f2..15a945d7 100644 --- a/cypher/models/pgsql/optimize/locality.go +++ b/cypher/models/pgsql/optimize/locality.go @@ -129,14 +129,14 @@ func SelectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *p scopedIdentifiers := localScope.Copy() for _, fromClause := range selectBody.From { - if !FromExpressionReferencesOnlyLocalIdentifiers(fromClause.Source) { + if !FromExpressionReferencesOnlyLocalIdentifiers(fromClause.Source, scopedIdentifiers) { return false } addFromClauseSourceBinding(scopedIdentifiers, fromClause) for _, join := range fromClause.Joins { - if !FromExpressionReferencesOnlyLocalIdentifiers(join.Table) { + if !FromExpressionReferencesOnlyLocalIdentifiers(join.Table, scopedIdentifiers) { return false } @@ -165,11 +165,19 @@ func SelectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *p (selectBody.Having == nil || ExpressionReferencesOnlyLocalIdentifiers(selectBody.Having, scopedIdentifiers)) } -func FromExpressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression) bool { - switch expression.(type) { +func FromExpressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression, localScopes ...*pgsql.IdentifierSet) bool { + switch typedExpression := expression.(type) { case pgsql.TableReference: return true + case pgsql.LateralSubquery: + localScope := pgsql.NewIdentifierSet() + if len(localScopes) > 0 && localScopes[0] != nil { + localScope = localScopes[0] + } + + return QueryReferencesOnlyLocalIdentifiers(typedExpression.Query, localScope) + default: return false } diff --git a/cypher/models/pgsql/optimize/lowering.go b/cypher/models/pgsql/optimize/lowering.go index c4873145..f4bb36a6 100644 --- a/cypher/models/pgsql/optimize/lowering.go +++ b/cypher/models/pgsql/optimize/lowering.go @@ -315,12 +315,13 @@ func indexReadingClauseTargets(targets map[*cypher.PatternPart]PatternTarget, qu } func indexQueryPartPatternPredicateTargets(targets map[*cypher.PatternPredicate]PatternTarget, queryPartIndex int, queryPart cypher.SyntaxNode) { - for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { - targets[predicate] = PatternTarget{ + for _, indexedPredicate := range indexedPatternPredicatesInQueryPart(queryPart) { + targets[indexedPredicate.Predicate] = PatternTarget{ QueryPartIndex: queryPartIndex, - PatternIndex: predicateIndex, + ClauseIndex: indexedPredicate.ClauseIndex, + PatternIndex: indexedPredicate.PredicateIndex, Predicate: true, - PredicateIndex: predicateIndex, + PredicateIndex: indexedPredicate.PredicateIndex, } } } diff --git a/cypher/models/pgsql/optimize/lowering_plan.go b/cypher/models/pgsql/optimize/lowering_plan.go index 52e4baa4..d1cf5122 100644 --- a/cypher/models/pgsql/optimize/lowering_plan.go +++ b/cypher/models/pgsql/optimize/lowering_plan.go @@ -179,8 +179,9 @@ func appendPatternProjectionPruningDecisions(plan *LoweringPlan, target PatternT } func appendPatternPredicateProjectionLowerings(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode, sourceReferences map[string]struct{}) { - for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { + for _, indexedPredicate := range indexedPatternPredicatesInQueryPart(queryPart) { var ( + predicate = indexedPredicate.Predicate patternPart = patternPartForPredicate(predicate) steps = traversalStepsForPattern(patternPart) ) @@ -191,9 +192,10 @@ func appendPatternPredicateProjectionLowerings(plan *LoweringPlan, queryPartInde target := PatternTarget{ QueryPartIndex: queryPartIndex, - PatternIndex: predicateIndex, + ClauseIndex: indexedPredicate.ClauseIndex, + PatternIndex: indexedPredicate.PredicateIndex, Predicate: true, - PredicateIndex: predicateIndex, + PredicateIndex: indexedPredicate.PredicateIndex, } appendPatternProjectionPruningDecisions(plan, target, patternPart, steps, sourceReferences) @@ -202,8 +204,9 @@ func appendPatternPredicateProjectionLowerings(plan *LoweringPlan, queryPartInde } func appendPatternPredicatePlacementDecisions(plan *LoweringPlan, queryPartIndex int, queryPart cypher.SyntaxNode) { - for predicateIndex, predicate := range patternPredicatesInQueryPart(queryPart) { + for _, indexedPredicate := range indexedPatternPredicatesInQueryPart(queryPart) { var ( + predicate = indexedPredicate.Predicate patternPart = patternPartForPredicate(predicate) steps = traversalStepsForPattern(patternPart) ) @@ -227,9 +230,10 @@ func appendPatternPredicatePlacementDecisions(plan *LoweringPlan, queryPartIndex target := PatternTarget{ QueryPartIndex: queryPartIndex, - PatternIndex: predicateIndex, + ClauseIndex: indexedPredicate.ClauseIndex, + PatternIndex: indexedPredicate.PredicateIndex, Predicate: true, - PredicateIndex: predicateIndex, + PredicateIndex: indexedPredicate.PredicateIndex, }.TraversalStep(0) plan.PatternPredicate = append(plan.PatternPredicate, PatternPredicatePlacementDecision{ diff --git a/cypher/models/pgsql/optimize/optimizer_test.go b/cypher/models/pgsql/optimize/optimizer_test.go index 5da3a070..11b7399b 100644 --- a/cypher/models/pgsql/optimize/optimizer_test.go +++ b/cypher/models/pgsql/optimize/optimizer_test.go @@ -281,6 +281,41 @@ func TestLoweringPlanReportsTypedPatternPredicateExistencePlacement(t *testing.T }}, plan.LoweringPlan.PatternPredicate) } +func TestLoweringPlanReportsPatternPredicateClauseIndex(t *testing.T) { + t.Parallel() + + regularQuery, err := frontend.ParseCypher(frontend.NewContext(), ` + MATCH (a) + MATCH (s) + WHERE NOT (s)-[]-() + RETURN s + `) + require.NoError(t, err) + + plan, err := BuildLoweringPlan(regularQuery, nil) + require.NoError(t, err) + require.Equal(t, []PatternPredicatePlacementDecision{{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + Predicate: true, + StepIndex: 0, + }, + Mode: PatternPredicatePlacementExistence, + }}, plan.PatternPredicate) + require.Contains(t, plan.ProjectionPruning, ProjectionPruningDecision{ + Target: TraversalStepTarget{ + QueryPartIndex: 0, + ClauseIndex: 1, + Predicate: true, + StepIndex: 0, + }, + ReferencedSymbols: []string{"s"}, + OmitRelationship: true, + OmitRightNode: true, + }) +} + func TestSelectivityModelPlansTraversalDirection(t *testing.T) { t.Parallel() @@ -1312,6 +1347,23 @@ func TestQueryReferencesOnlyLocalIdentifiersAllowsEmptyWith(t *testing.T) { require.True(t, QueryReferencesOnlyLocalIdentifiers(query, pgsql.NewIdentifierSet())) } +func TestFromExpressionReferencesOnlyLocalIdentifiersHandlesLateralSubquery(t *testing.T) { + t.Parallel() + + lateralSubquery := pgsql.LateralSubquery{ + Query: pgsql.Query{ + Body: pgsql.Select{ + Projection: []pgsql.SelectItem{ + pgsql.CompoundIdentifier{pgsql.Identifier("outer"), pgsql.ColumnID}, + }, + }, + }, + } + + require.True(t, FromExpressionReferencesOnlyLocalIdentifiers(lateralSubquery, pgsql.AsIdentifierSet(pgsql.Identifier("outer")))) + require.False(t, FromExpressionReferencesOnlyLocalIdentifiers(lateralSubquery, pgsql.NewIdentifierSet())) +} + func TestMeasureSelectivityPopReturnsTopFrame(t *testing.T) { t.Parallel() diff --git a/cypher/models/pgsql/optimize/pattern_predicates.go b/cypher/models/pgsql/optimize/pattern_predicates.go index 16e9349a..da1fff68 100644 --- a/cypher/models/pgsql/optimize/pattern_predicates.go +++ b/cypher/models/pgsql/optimize/pattern_predicates.go @@ -10,6 +10,12 @@ type patternPredicateCollector struct { predicates []*cypher.PatternPredicate } +type indexedPatternPredicate struct { + ClauseIndex int + PredicateIndex int + Predicate *cypher.PatternPredicate +} + func (s *patternPredicateCollector) Enter(node cypher.SyntaxNode) { if predicate, isPatternPredicate := node.(*cypher.PatternPredicate); isPatternPredicate { s.predicates = append(s.predicates, predicate) @@ -34,6 +40,55 @@ func patternPredicatesInQueryPart(queryPart cypher.SyntaxNode) []*cypher.Pattern return collector.predicates } +func indexedPatternPredicatesInQueryPart(queryPart cypher.SyntaxNode) []indexedPatternPredicate { + var ( + indexedPredicates []indexedPatternPredicate + seen = map[*cypher.PatternPredicate]struct{}{} + ) + + appendPredicates := func(clauseIndex int, where *cypher.Where) { + if where == nil { + return + } + + for _, predicate := range patternPredicatesInQueryPart(where) { + seen[predicate] = struct{}{} + indexedPredicates = append(indexedPredicates, indexedPatternPredicate{ + ClauseIndex: clauseIndex, + PredicateIndex: len(indexedPredicates), + Predicate: predicate, + }) + } + } + + switch typedQueryPart := queryPart.(type) { + case *cypher.SinglePartQuery: + for clauseIndex, readingClause := range typedQueryPart.ReadingClauses { + if readingClause != nil && readingClause.Match != nil { + appendPredicates(clauseIndex, readingClause.Match.Where) + } + } + + case *cypher.MultiPartQueryPart: + for clauseIndex, readingClause := range typedQueryPart.ReadingClauses { + if readingClause != nil && readingClause.Match != nil { + appendPredicates(clauseIndex, readingClause.Match.Where) + } + } + } + + for _, predicate := range patternPredicatesInQueryPart(queryPart) { + if _, alreadyIndexed := seen[predicate]; !alreadyIndexed { + indexedPredicates = append(indexedPredicates, indexedPatternPredicate{ + PredicateIndex: len(indexedPredicates), + Predicate: predicate, + }) + } + } + + return indexedPredicates +} + func patternPartForPredicate(predicate *cypher.PatternPredicate) *cypher.PatternPart { if predicate == nil { return nil diff --git a/cypher/models/pgsql/translate/model.go b/cypher/models/pgsql/translate/model.go index 6bc434cf..a9254060 100644 --- a/cypher/models/pgsql/translate/model.go +++ b/cypher/models/pgsql/translate/model.go @@ -403,8 +403,8 @@ func selectReferencesOnlyLocalIdentifiers(selectBody pgsql.Select, localScope *p return optimize.SelectReferencesOnlyLocalIdentifiers(selectBody, localScope) } -func fromExpressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression) bool { - return optimize.FromExpressionReferencesOnlyLocalIdentifiers(expression) +func fromExpressionReferencesOnlyLocalIdentifiers(expression pgsql.Expression, localScope *pgsql.IdentifierSet) bool { + return optimize.FromExpressionReferencesOnlyLocalIdentifiers(expression, localScope) } func isLocalToScope(expression pgsql.Expression, localScope *pgsql.IdentifierSet) bool { diff --git a/cypher/models/pgsql/translate/tracking.go b/cypher/models/pgsql/translate/tracking.go index 8d325cfe..38f9058e 100644 --- a/cypher/models/pgsql/translate/tracking.go +++ b/cypher/models/pgsql/translate/tracking.go @@ -27,6 +27,7 @@ func (s IdentifierGenerator) NewIdentifier(dataType pgsql.DataType) (pgsql.Ident case pgsql.EdgeComposite: prefixStr = "e" case pgsql.PathEdge: + dataType = pgsql.EdgeComposite prefixStr = "e" case pgsql.Scope: prefixStr = "s" diff --git a/cypher/models/pgsql/translate/tracking_test.go b/cypher/models/pgsql/translate/tracking_test.go index 9a0f0a92..e1c07ddd 100644 --- a/cypher/models/pgsql/translate/tracking_test.go +++ b/cypher/models/pgsql/translate/tracking_test.go @@ -42,3 +42,18 @@ func TestScopeLookupDataTypeResolvesAliases(t *testing.T) { require.True(t, found) require.Equal(t, pgsql.NodeComposite, dataType) } + +func TestIdentifierGeneratorSharesEdgeNamespaceForPathEdges(t *testing.T) { + generator := NewIdentifierGenerator() + + edgeIdentifier, err := generator.NewIdentifier(pgsql.EdgeComposite) + require.NoError(t, err) + pathEdgeIdentifier, err := generator.NewIdentifier(pgsql.PathEdge) + require.NoError(t, err) + nextEdgeIdentifier, err := generator.NewIdentifier(pgsql.EdgeComposite) + require.NoError(t, err) + + require.Equal(t, pgsql.Identifier("e0"), edgeIdentifier) + require.Equal(t, pgsql.Identifier("e1"), pathEdgeIdentifier) + require.Equal(t, pgsql.Identifier("e2"), nextEdgeIdentifier) +}