Skip to content

fix(pg): Inconsistent conditional pattern matching BED-6695#73

Open
seanjSO wants to merge 9 commits into
mainfrom
seanj/BED-6695
Open

fix(pg): Inconsistent conditional pattern matching BED-6695#73
seanjSO wants to merge 9 commits into
mainfrom
seanj/BED-6695

Conversation

@seanjSO
Copy link
Copy Markdown
Contributor

@seanjSO seanjSO commented May 7, 2026

Description

Resolves: BED-6695

Type of Change

  • Chore (a change that does not modify the application functionality)
  • Bug fix (a change that fixes an issue)
  • New feature / enhancement (a change that adds new functionality)
  • Refactor (no behaviour change)
  • Test coverage
  • Build / CI / tooling
  • Documentation

Testing

  • Unit tests added / updated
  • Integration tests added / updated
  • Manual integration tests run (go test -tags manual_integration ./integration/...)

Screenshots (if appropriate):

Driver Impact

  • PostgreSQL driver (drivers/pg)
  • Neo4j driver (drivers/neo4j)

Checklist

  • Code is formatted
  • All existing tests pass
  • go.mod / go.sum are up to date if dependencies changed

Summary by CodeRabbit

  • Bug Fixes

    • Improved SQL generation for negated and constrained pattern predicates to ensure correct existence checks and join/filter placement across traversal directions.
  • Tests

    • Added and expanded translation and integration test cases covering pattern predicate negation, directionality, binding variants, and related traversal scenarios.

Review Change Stack

@seanjSO seanjSO self-assigned this May 7, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Walkthrough

This PR adds an outer-correlated traversal root path and supporting helpers, updates predicate dispatch to choose the new root when endpoints are pre-bound, regenerates SQL translation tests to adjust JOIN vs WHERE scoping for existence/negation patterns, and adds integration fixtures for BED-6695 pattern-predicate cases.

Changes

Pattern Predicate Outer-Correlated Translation

Layer / File(s) Summary
Traversal root refactor and helpers
cypher/models/pgsql/translate/traversal.go
Refactors directionless traversal root to handle unbound, single-bound, and pairwise cases; extracts node-inequality assertion; adds single-bound plan, pairwise constraint helper, and buildTraversalPatternRootWithOuterCorrelation.
Pattern predicate conditional invocation
cypher/models/pgsql/translate/predicate.go
Updates buildOptimizedRelationshipExistPredicate to handle bound-combination cases; documents deferred predicate resolution and selects buildTraversalPatternRootWithOuterCorrelation when an endpoint is pre-bound.
SQL translation test case updates
cypher/models/pgsql/test/translation_cases/nodes.sql, cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql, cypher/models/pgsql/test/translation_cases/pattern_binding.sql
Regenerates negated/existence-pattern SQL to bind edges to nodes via explicit joins and move outer node-id equality constraints into CTE WHERE clauses; adds undirected pattern-binding test cases.
Integration test fixtures for BED-6695
integration/testdata/bed6695.json, integration/testdata/cases/bed6695-pattern_predicates.json
Adds a bed6695 graph fixture and 14 integration test cases exercising pattern-predicate negation, directionality, and property-filter permutations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A traversal bound tight, its path now takes flight,
Joins whisper to nodes in the CTE's soft light,
WHERE moves to its place, scoped and neat, not loud,
Existence checks count and the queries stay proud. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies this as a bug fix for BED-6695 related to pattern matching in PostgreSQL translation, which aligns with the main changes addressing conditional pattern predicates.
Description check ✅ Passed The PR description includes the ticket reference (BED-6695), specifies bug fix and test coverage, documents testing performed, and indicates PostgreSQL driver impact. However, the checklist does not confirm all existing tests pass, which is a template-required checkbox.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seanj/BED-6695

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seanjSO seanjSO added bug Something isn't working go Pull requests that update go code labels May 7, 2026
@seanjSO seanjSO changed the title BED-6695: Inconsistent conditional pattern matching fix(pg): Inconsistent conditional pattern matching BED-6695 May 7, 2026
@seanjSO seanjSO force-pushed the seanj/BED-6695 branch 2 times, most recently from 4894815 to 24385cb Compare May 12, 2026 15:55
@seanjSO seanjSO marked this pull request as ready for review May 12, 2026 15:56
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
integration/testdata/cases/pattern_predicate_direction_inline.json (1)

1-131: ⚡ Quick win

Consider extracting common fixture data to reduce duplication.

All six test cases define nearly identical inline fixtures (nodes u1, u2, g1, g2, g3, g4 and their edges). While this makes each test self-contained, it creates a maintenance burden—any fixture adjustment requires updating six locations.

If these fixtures need to remain inline, consider at least documenting why the duplication is intentional (e.g., for regression test isolation).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration/testdata/cases/pattern_predicate_direction_inline.json` around
lines 1 - 131, The fixtures for the six cases (e.g., cases named "regression:
incoming negated pattern with contains predicate (left-directed form)",
"right-directed equivalent", etc.) duplicate the same nodes and edges; extract
that shared nodes/edges JSON into a single reusable fixture object (or helper
function) and reference it from each case to avoid repetition, or if inline
fixtures must remain, add a short explanatory comment in each case noting the
intentional duplication for regression/isolation; update the cases that
reference the repeated nodes (u1,u2,g1,g2,g3,g4) and edges (EdgeKind1/EdgeKind2
entries) to use the shared fixture identifier or include the comment.
cypher/models/pgsql/test/pattern_predicate_shape_test.go (2)

38-40: 💤 Low value

Add a comment explaining why this pattern is forbidden.

The forbidden fragment "from s0 join edge e0 on (s0.n0).id = e0.end_id" represents the buggy SQL structure that this fix addresses, but the reason isn't documented. A brief comment would help future maintainers understand the regression being prevented.

📝 Suggested comment
 	forbiddenFragments := []string{
+		// This join pattern caused incorrect correlation in negated pattern predicates (BED-6695).
+		// The fix restructures the CTE to avoid binding the outer reference too early.
 		"from s0 join edge e0 on (s0.n0).id = e0.end_id",
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/test/pattern_predicate_shape_test.go` around lines 38 -
40, Add a short explanatory comment above the forbiddenFragments entry for the
string "from s0 join edge e0 on (s0.n0).id = e0.end_id" stating that this SQL
pattern is forbidden because it uses a nested node accessor `(s0.n0).id` in the
JOIN ON clause (producing incorrect/buggy SQL generation or runtime errors for
nullable/composite node columns) and that the test prevents regressions related
to that specific buggy join shape; update the comment to reference the exact
fragment and the regression it guards against so future maintainers understand
why it must remain forbidden.

24-30: 💤 Low value

Consider deriving kind IDs dynamically from test constants.

The hard-coded array values [1] and [3] in the SQL assertions must match NodeKind1 and EdgeKind1 as mapped by newKindMapper(). If the kind mapper changes, these assertions could silently pass with incorrect SQL.

♻️ Alternative: compute expected values from the mapper

You could call newKindMapper() and extract the mapped IDs to build the expected fragments dynamically. However, since this is a focused regression test for a specific bug fix and the mapper is stable test infrastructure, the current approach may be acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/test/pattern_predicate_shape_test.go` around lines 24 -
30, The test currently hardcodes kind ID arrays in requiredFragments which can
drift from the mapping; obtain the mapper via newKindMapper(), use it to look up
the mapped IDs for NodeKind1 and EdgeKind1, and construct the expected SQL
fragments using those computed IDs instead of literal "[1]" and "[3]"; update
the requiredFragments construction (the variable named requiredFragments in the
test) to interpolate the derived values so assertions remain correct if
newKindMapper() changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cypher/models/pgsql/test/pattern_predicate_shape_test.go`:
- Around line 38-40: Add a short explanatory comment above the
forbiddenFragments entry for the string "from s0 join edge e0 on (s0.n0).id =
e0.end_id" stating that this SQL pattern is forbidden because it uses a nested
node accessor `(s0.n0).id` in the JOIN ON clause (producing incorrect/buggy SQL
generation or runtime errors for nullable/composite node columns) and that the
test prevents regressions related to that specific buggy join shape; update the
comment to reference the exact fragment and the regression it guards against so
future maintainers understand why it must remain forbidden.
- Around line 24-30: The test currently hardcodes kind ID arrays in
requiredFragments which can drift from the mapping; obtain the mapper via
newKindMapper(), use it to look up the mapped IDs for NodeKind1 and EdgeKind1,
and construct the expected SQL fragments using those computed IDs instead of
literal "[1]" and "[3]"; update the requiredFragments construction (the variable
named requiredFragments in the test) to interpolate the derived values so
assertions remain correct if newKindMapper() changes.

In `@integration/testdata/cases/pattern_predicate_direction_inline.json`:
- Around line 1-131: The fixtures for the six cases (e.g., cases named
"regression: incoming negated pattern with contains predicate (left-directed
form)", "right-directed equivalent", etc.) duplicate the same nodes and edges;
extract that shared nodes/edges JSON into a single reusable fixture object (or
helper function) and reference it from each case to avoid repetition, or if
inline fixtures must remain, add a short explanatory comment in each case noting
the intentional duplication for regression/isolation; update the cases that
reference the repeated nodes (u1,u2,g1,g2,g3,g4) and edges (EdgeKind1/EdgeKind2
entries) to use the shared fixture identifier or include the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 50427ad2-cb7e-47a1-8ffb-cd7db5659309

📥 Commits

Reviewing files that changed from the base of the PR and between ae5d313 and 24385cb.

📒 Files selected for processing (9)
  • cypher/models/pgsql/test/pattern_predicate_shape_test.go
  • cypher/models/pgsql/test/translation_cases/nodes.sql
  • cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql
  • cypher/models/pgsql/translate/predicate.go
  • cypher/models/pgsql/translate/traversal.go
  • integration/testdata/bed6695.json
  • integration/testdata/cases/pattern_predicate_direction_inline.json
  • integration/testdata/cases/pattern_predicates.json
  • integration/testdata/empty.json

@zinic
Copy link
Copy Markdown
Contributor

zinic commented May 12, 2026

Change appears to cover the current issue identified in the ticket. I noticed however that this missed the undirected traversal path. Checking with local tests and tooling, the defect does still exist in undirected path predicate translation:

match (g:NodeKind2) where g.name starts with 'KEY ADMINS' and not ((g)-[:EdgeKind1]-(:NodeKind1)) return count(g)

I don't have a problem with deferring the fix above for undirected path predicates to a different ticket but I felt it worth bringing up. Everything else in this changeset passes muster.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cypher/models/pgsql/translate/predicate.go (1)

154-179: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep processing later pattern predicates after an optimized hit.

Line 178 exits buildPatternPredicates from inside the loop. If the same WHERE has another deferred pattern predicate after this one, it never gets resolved. This should continue, not return.

Suggested fix
-					return nil
+					continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/translate/predicate.go` around lines 154 - 179, In
buildPatternPredicates, when an optimized relationship-exists predicate is
applied for a single-step traversal (the block that calls
buildOptimizedRelationshipExistPredicate and sets predicateFuture.SyntaxNode),
do not return nil immediately; replace the early return with a continue so the
loop keeps processing subsequent pattern predicates in the same WHERE; ensure
the same traversalStep, traversalStepIdentifiers and predicateFuture handling
remains unchanged and only the control flow is modified.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cypher/models/pgsql/translate/predicate.go`:
- Around line 75-90: The current fallback branch still references
traversalStep.LeftNode.Identifier even when both LeftNodeBound and
RightNodeBound are false, producing invalid SQL for a fully-unbound pattern; add
an explicit branch that detects (!traversalStep.LeftNodeBound &&
!traversalStep.RightNodeBound) and for that case set whereClause to a simple
edge-existence predicate (e.g., an IS NOT NULL or existence check on
traversalStep.Edge.Identifier/its primary id) or omit the predicate entirely
instead of comparing to traversalStep.LeftNode.Identifier.id; update the logic
around whereClause construction (the block using pgsql.NewBinaryExpression with
ColumnStartID/ColumnEndID and ColumnID) to only run when at least one side is
bound.

---

Outside diff comments:
In `@cypher/models/pgsql/translate/predicate.go`:
- Around line 154-179: In buildPatternPredicates, when an optimized
relationship-exists predicate is applied for a single-step traversal (the block
that calls buildOptimizedRelationshipExistPredicate and sets
predicateFuture.SyntaxNode), do not return nil immediately; replace the early
return with a continue so the loop keeps processing subsequent pattern
predicates in the same WHERE; ensure the same traversalStep,
traversalStepIdentifiers and predicateFuture handling remains unchanged and only
the control flow is modified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a57d3c2d-4793-474f-8322-c533de8e9c46

📥 Commits

Reviewing files that changed from the base of the PR and between 88b2098 and 856f971.

📒 Files selected for processing (3)
  • cypher/models/pgsql/test/translation_cases/pattern_binding.sql
  • cypher/models/pgsql/translate/predicate.go
  • cypher/models/pgsql/translate/traversal.go

Comment on lines +75 to +90
} else {
// Left-side node is bound OR neither is bound
whereClause = pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle the fully-unbound optimization separately.

This fallback also covers !LeftNodeBound && !RightNodeBound, but it still compares against traversalStep.LeftNode.Identifier.id. That identifier is never introduced in the subquery, so a legal predicate like WHERE ()--() generates invalid SQL instead of a plain edge-existence check.

Suggested fix
-	} else {
-		// Left-side node is bound OR neither is bound
+	} else if traversalStep.LeftNodeBound {
+		// Left-side node is bound
 		whereClause = pgsql.NewBinaryExpression(
 			pgsql.NewBinaryExpression(
 				pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
 				pgsql.OperatorEquals,
 				pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
@@
 			),
 		)
+	} else {
+		// Neither side is bound; any edge satisfies the predicate.
+		whereClause = pgsql.NewLiteral(true, pgsql.Boolean)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
// Left-side node is bound OR neither is bound
whereClause = pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
)
}
} else if traversalStep.LeftNodeBound {
// Left-side node is bound
whereClause = pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
)
} else {
// Neither side is bound; any edge satisfies the predicate.
whereClause = pgsql.NewLiteral(true, pgsql.Boolean)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/translate/predicate.go` around lines 75 - 90, The current
fallback branch still references traversalStep.LeftNode.Identifier even when
both LeftNodeBound and RightNodeBound are false, producing invalid SQL for a
fully-unbound pattern; add an explicit branch that detects
(!traversalStep.LeftNodeBound && !traversalStep.RightNodeBound) and for that
case set whereClause to a simple edge-existence predicate (e.g., an IS NOT NULL
or existence check on traversalStep.Edge.Identifier/its primary id) or omit the
predicate entirely instead of comparing to traversalStep.LeftNode.Identifier.id;
update the logic around whereClause construction (the block using
pgsql.NewBinaryExpression with ColumnStartID/ColumnEndID and ColumnID) to only
run when at least one side is bound.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants