Skip to content

feat(cysql): support CREATE and UNWIND#62

Open
zinic wants to merge 7 commits intoSpecterOps:mainfrom
zinic:unwind
Open

feat(cysql): support CREATE and UNWIND#62
zinic wants to merge 7 commits intoSpecterOps:mainfrom
zinic:unwind

Conversation

@zinic
Copy link
Copy Markdown
Contributor

@zinic zinic commented Apr 16, 2026

Description

Implement Cypher-to-PgSQL translation for UNWIND, mapping it to PostgreSQL's unnest() function. This supports UNWIND as a reading clause in both multi-part queries (preceded by WITH/MATCH) and standalone single-part queries.

  • Add UnwindClause model and tracking on QueryPart
  • Add prepareUnwind/translateUnwind handlers in the AST translator
  • Emit unnest() FROM clauses in both inline and tail projections
  • Handle nil reference frames in projection and WITH translation for standalone UNWIND variables that have no preceding CTE
  • Lazily push a scope frame in prepareUnwind when none exists to prevent nil pointer dereference on standalone UNWIND queries
  • Add translation test cases covering UNWIND with WITH, MATCH, WHERE, ORDER BY, LIMIT, DISTINCT, aggregation, and standalone usage

Resolves: <TICKET_OR_ISSUE_NUMBER>

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/...)

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

Release Notes

  • New Features

    • Added UNWIND clause support enabling users to unwind arrays and collections with filtering, sorting, and aggregation operations.
    • Added CREATE clause support enabling creation of nodes with multiple labels and relationships with configurable properties and directions.
  • Tests

    • Added extensive test coverage including integration tests for inline UNWIND and CREATE operations with property validation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Walkthrough

The PR introduces comprehensive PostgreSQL translation support for Cypher's UNWIND and CREATE clauses, including new translation handlers, model types for representing unwind operations and node/edge creations, graph ID parameter propagation throughout the translation pipeline, and extensive test coverage for both features.

Changes

Cohort / File(s) Summary
UNWIND Translation
cypher/models/pgsql/translate/unwind.go, cypher/models/pgsql/test/translation_cases/unwind.sql, integration/testdata/cases/unwind_inline.json
New UNWIND translation support: prepareUnwind creates bindings and scope frames; translateUnwind rewrites array expressions and registers unwind clauses; unwindFromClauses converts to UNNEST(...) FROM clauses. Test cases cover literal arrays, collected properties, filtering, ordering, aggregation, and limit patterns.
CREATE Translation
cypher/models/pgsql/translate/create.go, cypher/models/pgsql/test/translation_cases/create.sql, integration/testdata/cases/create_inline.json
New CREATE translation via translateCreate: node creation builds deterministic kind ID arrays and JSONB properties, inserts into node table with RETURNING; edge creation handles direction, maps kinds to int2, inserts into edge table with start/end node references. Test cases include single/multi-label nodes, directed/reverse edges, property handling, and chained MATCH-then-CREATE flows.
Model & Core Types
cypher/models/pgsql/translate/model.go, cypher/models/pgsql/translate/tracking.go
Expanded QueryPart with unwind tracking (methods: AddUnwindClause, HasUnwindClauses, ConsumeUnwindClauses). Added NodeCreate/EdgeCreate types for pending creations, updated Mutations struct with creation collections. Introduced FromClauseBuilder for deduplicating FROM entries. Added Synthetic bool field to Frame for bookkeeping-only frames (e.g., standalone UNWIND).
Translator & Pipeline
cypher/models/pgsql/translate/translator.go, cypher/models/pgsql/translate/format.go, cypher/models/pgsql/translate/with.go
Added graphID parameter to Translate and NewTranslator functions. Updated translator's Enter/Exit to handle *cypher.Unwind via prepareUnwind/translateUnwind and *cypher.Create via dedicated translateCreate. Modified WITH projection to conditionally build CompoundIdentifier based on LastProjection presence (for UNWIND variable handling).
Projection & Query Building
cypher/models/pgsql/translate/projection.go, cypher/models/pgsql/translate/pattern.go, cypher/models/pgsql/translate/node.go, cypher/models/pgsql/translate/relationship.go
Updated projection building to consume UNWIND clauses and append them to select From. Modified path composite to use dependency frame identifiers. Added collectCreateNodePattern and collectCreateEdgePattern handlers for CREATE mode: record NodeCreate/EdgeCreate mutations, update traversal steps, resolve node/edge bindings. Root traversal patterns now consume pending UNWIND clauses.
Expression & Format Support
cypher/models/pgsql/format/format.go, cypher/models/pgsql/model.go
Added Distinct field emission in formatSelect. Added formatInsertStatement delegation for pgsql.Insert in set expressions. Implemented AsExpression() and AsSetExpression() methods on Insert type.
Test Infrastructure Updates
cypher/models/pgsql/test/testcase.go, cypher/models/pgsql/test/query_test.go, cypher/models/pgsql/visualization/visualizer_test.go, cypher/models/pgsql/translate/format.go, drivers/pg/transaction.go
Updated translation calls to pass graphID parameter: test infrastructure resolves default graph ID; transaction layer retrieves target graph and passes its ID to translator.
SQL 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/multipart.sql
Added DISTINCT to SELECT projections in three existing test cases to reflect updated projection logic.
Minor Maintenance
cmd/benchmark/main.go, integration/cypher_test.go
Whitespace alignment adjustment in flag declarations; removed trailing blank line.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: omit unnecessary joins; update makefile #49: Modifies PostgreSQL Cypher translator internals in cypher/models/pgsql/translate/ (projection, with, tracking) with overlapping code regions and similar architectural changes to the translation pipeline.

Suggested reviewers

  • urangel

Poem

🐰 ✨ Unwind and Create, Oh What a Sight!

With UNWIND we sprawl arrays wide,
CREATE builds nodes with pride,
Graph ID flows through every strand,
New types guide the translator's hand.
SQL blooms from Cypher's embrace!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description covers UNWIND comprehensively and explicitly lists relevant sections; however, it does not document CREATE implementation details despite CREATE being in the title and significantly featured in the changeset. Add documentation of CREATE clause translation implementation (node/edge creation mutation models, CTE generation logic) to match the title's dual reference and improve clarity on scope.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(cysql): support CREATE and UNWIND' accurately reflects the main changes: UNWIND translation is fully implemented with comprehensive support, and CREATE is also added with node/edge creation mutation handling.
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 unit tests (beta)
  • Create PR with unit tests

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.

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: 4

🧹 Nitpick comments (1)
drivers/pg/query/sql/schema_up.sql (1)

461-464: Prefer bigint counters for frontier sizes.

count(*) is bigint in PostgreSQL; storing in int4 can overflow on large traversals. Consider widening remaining, forward_front_size, and backward_front_size to int8.

Also applies to: 493-496, 700-702

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drivers/pg/query/sql/schema_up.sql` around lines 461 - 464, The
procedure/function currently declares and returns 32-bit ints (e.g. "returns
int4 as" and variables remaining, forward_front_size, backward_front_size) which
can overflow for large counts; change the function return type from int4 to int8
and widen the local variables remaining, forward_front_size, and
backward_front_size to int8 (bigint) and update any related casts/usages of
count(*) to use bigint to match; apply the same change to the other occurrences
noted (the other function blocks around the other ranges).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cypher/models/pgsql/test/translation_cases/unwind.sql`:
- Around line 32-33: The test's expected SQL preserves duplicates because it
uses "select i1 as x from s0, unnest(i0) as i1" but the Cypher was "return
distinct x"; update the expected SQL to enforce distinctness by using SELECT
DISTINCT (e.g., "select distinct i1 as x ...") so the fixture asserts
duplicate-removal; locate the clause around s0 / i0 / i1 and change the SELECT
to DISTINCT to match the original "return distinct x" semantics.

In `@cypher/models/pgsql/translate/projection.go`:
- Around line 443-453: The unnest/UNWIND sources are being appended to
sqlSelect.From only at projection time (in the block that uses
part.unwindClauses, pgsql.FunctionUnnest, pgsql.AliasedExpression and
unwind.Binding.Identifier), which is too late for downstream clause translation;
instead, materialize those unnest sources into the query part's FROM pipeline so
later MATCH/WHERE/SUBPART translation can bind to the new aliases. Move the
logic that converts part.unwindClauses -> pgsql.FromClause (the
pgsql.FunctionCall(FunctionUnnest) wrapped in pgsql.AliasedExpression with
unwind.Binding.Identifier) into the earlier FROM-building stage for the query
part (the same pipeline that produces sqlSelect.From for other sources), and
apply the same change for the other occurrence noted (the similar block around
lines 493-503) so UNWINDs are present before subsequent clause translation.

In `@cypher/models/pgsql/translate/unwind.go`:
- Around line 18-26: The current PushFrame call in unwind.go creates a real
Frame that downstream tail-projection treats as having a FROM source; change the
synthetic UNWIND bookkeeping frame so it cannot be mistaken for a real SQL
source: when you create the frame in the s.scope.PushFrame() branch, mark it as
synthetic/bookkeeping (e.g., set a Frame.Synthetic or Frame.IsBookkeeping flag
or keep its Source/Relation nil) and update the tail-projection logic (the code
that inspects s.query.CurrentPart().Frame to decide to synthesize a FROM/CTE) to
skip creating a relation when that flag is set (or when Frame.Source is nil).
Ensure references include the PushFrame call site and
s.query.CurrentPart().Frame so the synthetic frame is recognized and not emitted
as a SQL source.

In `@drivers/pg/query/sql/schema_up.sql`:
- Around line 577-583: The deduplication and visited-tracking currently collapse
paths across different roots because deduped uses DISTINCT ON
(next_front.next_id) and visited is keyed by next_id alone; modify the dedupe
and all visited table usages to include root_id so they operate on (root_id,
next_id) pairs: update the CTE deduped to use DISTINCT ON (next_front.root_id,
next_front.next_id) and adjust all references/insertions/SELECTs against the
visited table and any joins with next_front (the visited-marking logic around
visited, and any EXISTS/NOT EXISTS checks) to include root_id alongside next_id
so visitation and pruning are scoped per root_id. Ensure any ORDER BY or GROUP
BY that relied on next_id is updated to include root_id where appropriate.

---

Nitpick comments:
In `@drivers/pg/query/sql/schema_up.sql`:
- Around line 461-464: The procedure/function currently declares and returns
32-bit ints (e.g. "returns int4 as" and variables remaining, forward_front_size,
backward_front_size) which can overflow for large counts; change the function
return type from int4 to int8 and widen the local variables remaining,
forward_front_size, and backward_front_size to int8 (bigint) and update any
related casts/usages of count(*) to use bigint to match; apply the same change
to the other occurrences noted (the other function blocks around the other
ranges).
🪄 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: 10ed1a64-19ad-4bfb-ade7-f60dec38327c

📥 Commits

Reviewing files that changed from the base of the PR and between 2a417c1 and f6c737e.

📒 Files selected for processing (9)
  • cmd/benchmark/main.go
  • cypher/models/pgsql/test/translation_cases/unwind.sql
  • cypher/models/pgsql/translate/model.go
  • cypher/models/pgsql/translate/projection.go
  • cypher/models/pgsql/translate/translator.go
  • cypher/models/pgsql/translate/unwind.go
  • cypher/models/pgsql/translate/with.go
  • drivers/pg/query/sql/schema_up.sql
  • integration/cypher_test.go
💤 Files with no reviewable changes (1)
  • integration/cypher_test.go

Comment thread cypher/models/pgsql/test/translation_cases/unwind.sql Outdated
Comment thread cypher/models/pgsql/translate/projection.go Outdated
Comment thread cypher/models/pgsql/translate/unwind.go
Comment thread drivers/pg/query/sql/schema_up.sql Outdated
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 (1)
cypher/models/pgsql/translate/model.go (1)

683-697: Minor typo in method name: AddIdentifer should be AddIdentifier.

The method name has a typo (Identifer instead of Identifier). While this doesn't affect functionality, it could cause confusion and inconsistency with standard naming.

✏️ Suggested fix
-// AddIdentifer appends a from clause for frameID if it has not been seen before.
-func (s *FromClauseBuilder) AddIdentifer(frameID pgsql.Identifier) {
+// AddIdentifier appends a from clause for frameID if it has not been seen before.
+func (s *FromClauseBuilder) AddIdentifier(frameID pgsql.Identifier) {

Note: Also update the call site in AddBinding (line 702) to use AddIdentifier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypher/models/pgsql/translate/model.go` around lines 683 - 697, Rename the
method AddIdentifer to AddIdentifier to fix the typo: update the function
declaration on the FromClauseBuilder receiver and all call sites (notably the
call from AddBinding) to use the new AddIdentifier name; ensure the identifier's
visibility/signature remains the same (receiver type, parameter
pgsql.Identifier) so no other behavior changes are introduced and run
tests/compile to catch any missed references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cypher/models/pgsql/translate/model.go`:
- Around line 683-697: Rename the method AddIdentifer to AddIdentifier to fix
the typo: update the function declaration on the FromClauseBuilder receiver and
all call sites (notably the call from AddBinding) to use the new AddIdentifier
name; ensure the identifier's visibility/signature remains the same (receiver
type, parameter pgsql.Identifier) so no other behavior changes are introduced
and run tests/compile to catch any missed references.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c66defab-d361-4923-9f44-e766b8bf889d

📥 Commits

Reviewing files that changed from the base of the PR and between f6c737e and 9bdbac2.

📒 Files selected for processing (10)
  • cypher/models/pgsql/format/format.go
  • cypher/models/pgsql/model.go
  • cypher/models/pgsql/test/translation_cases/create.sql
  • cypher/models/pgsql/translate/create.go
  • cypher/models/pgsql/translate/model.go
  • cypher/models/pgsql/translate/node.go
  • cypher/models/pgsql/translate/projection.go
  • cypher/models/pgsql/translate/relationship.go
  • cypher/models/pgsql/translate/translator.go
  • integration/testdata/cases/create_inline.json
✅ Files skipped from review due to trivial changes (1)
  • integration/testdata/cases/create_inline.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • cypher/models/pgsql/translate/projection.go

@zinic zinic changed the title feat(cysql): support UNWIND feat(cysql): support CREATE and UNWIND Apr 16, 2026
@zinic
Copy link
Copy Markdown
Contributor Author

zinic commented Apr 16, 2026

@CodeRabbit yo, blast me and review this again

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

@zinic: Sure thing! Kicking off a full review of this PR now. 🚀

✅ Actions performed

Full review triggered.

@zinic
Copy link
Copy Markdown
Contributor Author

zinic commented Apr 22, 2026

Update unit tests to use a default graph ID so that query forms match integration tests

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: 3

🧹 Nitpick comments (1)
cypher/models/pgsql/translate/model.go (1)

709-714: Nil-guard binding in AddBinding.

binding.LastProjection will panic if binding is nil. Callers most likely always pass a resolved binding, but a cheap guard avoids a confusing NPE if an upstream lookup ever returns nil (e.g., partially failed translation paths).

🛡️ Suggested guard
 func (s *FromClauseBuilder) AddBinding(binding *BoundIdentifier) {
-	if binding.LastProjection != nil {
+	if binding != nil && binding.LastProjection != nil {
 		s.AddIdentifer(binding.LastProjection.Binding.Identifier)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypher/models/pgsql/translate/model.go` around lines 709 - 714, The
AddBinding method on FromClauseBuilder will panic if the passed binding is nil
because it dereferences binding.LastProjection; add a nil-guard at the start of
FromClauseBuilder.AddBinding to return early if binding == nil (or handle
accordingly) before accessing binding.LastProjection, then proceed to check
LastProjection and call
s.AddIdentifer(binding.LastProjection.Binding.Identifier) only when non-nil;
this protects BoundIdentifier consumers from NPEs when upstream lookups return
nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cypher/models/pgsql/test/testcase.go`:
- Line 208: In AssertLive, translate.Translate is incorrectly called with
context.Background(), which ignores the test-provided ctx; replace
context.Background() with the passed-in ctx so the translation respects
cancellation/deadlines and context values (i.e., change the translate.Translate
call in AssertLive to use ctx when invoking translate.Translate(ctx,
regularQuery, driver.KindMapper(), s.CypherParams, defaultGraph.ID)).

In `@cypher/models/pgsql/test/translation_cases/multipart.sql`:
- Line 39: The s2 CTE multiplies traversal rows because its FROM clause includes
"s1, s3" which is uncorrelated to s3.root_id; this repeats each s3 row for every
s1 row and inflates the count(s2.n1). Fix by either removing s1 from the s2 FROM
when it isn't referenced, or correlate s1 to s3 (e.g., join s1 ON (s1.n0).id =
s3.root_id) so the carried frame (n0) is bound per traversal root_id; update the
s2 select/from to use the correlated join instead of the cross join.

In `@cypher/models/pgsql/translate/model.go`:
- Around line 693-714: Rename the exported method FromClauseBuilder.AddIdentifer
to AddIdentifier everywhere (update the method declaration and all call sites
such as the usage in projection.go where fromClauseBuilder.AddIdentifer(...) is
invoked, and update the related comment text), and in
FromClauseBuilder.AddBinding add a defensive nil check for the binding parameter
before dereferencing binding.LastProjection (i.e., return early if binding ==
nil) while keeping the existing check for binding.LastProjection != nil.

---

Nitpick comments:
In `@cypher/models/pgsql/translate/model.go`:
- Around line 709-714: The AddBinding method on FromClauseBuilder will panic if
the passed binding is nil because it dereferences binding.LastProjection; add a
nil-guard at the start of FromClauseBuilder.AddBinding to return early if
binding == nil (or handle accordingly) before accessing binding.LastProjection,
then proceed to check LastProjection and call
s.AddIdentifer(binding.LastProjection.Binding.Identifier) only when non-nil;
this protects BoundIdentifier consumers from NPEs when upstream lookups return
nil.
🪄 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: 18334c8b-c9cc-4e99-8015-e31cbe59f0d8

📥 Commits

Reviewing files that changed from the base of the PR and between 9bdbac2 and d9546ab.

📒 Files selected for processing (26)
  • cmd/benchmark/main.go
  • cypher/models/pgsql/format/format.go
  • cypher/models/pgsql/model.go
  • cypher/models/pgsql/test/query_test.go
  • cypher/models/pgsql/test/testcase.go
  • cypher/models/pgsql/test/translation_cases/create.sql
  • cypher/models/pgsql/test/translation_cases/multipart.sql
  • cypher/models/pgsql/test/translation_cases/nodes.sql
  • cypher/models/pgsql/test/translation_cases/stepwise_traversal.sql
  • cypher/models/pgsql/test/translation_cases/unwind.sql
  • cypher/models/pgsql/translate/create.go
  • cypher/models/pgsql/translate/format.go
  • cypher/models/pgsql/translate/model.go
  • cypher/models/pgsql/translate/node.go
  • cypher/models/pgsql/translate/pattern.go
  • cypher/models/pgsql/translate/projection.go
  • cypher/models/pgsql/translate/relationship.go
  • cypher/models/pgsql/translate/tracking.go
  • cypher/models/pgsql/translate/translator.go
  • cypher/models/pgsql/translate/unwind.go
  • cypher/models/pgsql/translate/with.go
  • cypher/models/pgsql/visualization/visualizer_test.go
  • drivers/pg/transaction.go
  • integration/cypher_test.go
  • integration/testdata/cases/create_inline.json
  • integration/testdata/cases/unwind_inline.json
💤 Files with no reviewable changes (1)
  • integration/cypher_test.go
✅ Files skipped from review due to trivial changes (4)
  • cmd/benchmark/main.go
  • integration/testdata/cases/create_inline.json
  • integration/testdata/cases/unwind_inline.json
  • cypher/models/pgsql/test/translation_cases/create.sql
🚧 Files skipped from review as they are similar to previous changes (8)
  • cypher/models/pgsql/model.go
  • cypher/models/pgsql/format/format.go
  • cypher/models/pgsql/translate/with.go
  • cypher/models/pgsql/test/translation_cases/unwind.sql
  • cypher/models/pgsql/translate/relationship.go
  • cypher/models/pgsql/translate/projection.go
  • cypher/models/pgsql/translate/node.go
  • cypher/models/pgsql/translate/create.go

t.Fatalf("Driver has no default graph set")
}

if translation, err := translate.Translate(context.Background(), regularQuery, driver.KindMapper(), s.CypherParams, defaultGraph.ID); err != nil {
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 | 🟡 Minor

Use the passed ctx instead of context.Background() in AssertLive.

AssertLive accepts a ctx context.Context and correctly threads it into driver.Run on Line 213, but translate.Translate here is invoked with context.Background(). If the test's context carries a deadline/cancellation or test-scoped values (e.g., for the context-aware kind mapper), they are ignored during translation. The pre-existing WriteTo/Assert paths have no caller-provided context, but AssertLive does.

🛠️ Proposed fix
-		if translation, err := translate.Translate(context.Background(), regularQuery, driver.KindMapper(), s.CypherParams, defaultGraph.ID); err != nil {
+		if translation, err := translate.Translate(ctx, regularQuery, driver.KindMapper(), s.CypherParams, defaultGraph.ID); err != nil {
📝 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
if translation, err := translate.Translate(context.Background(), regularQuery, driver.KindMapper(), s.CypherParams, defaultGraph.ID); err != nil {
if translation, err := translate.Translate(ctx, regularQuery, driver.KindMapper(), s.CypherParams, defaultGraph.ID); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypher/models/pgsql/test/testcase.go` at line 208, In AssertLive,
translate.Translate is incorrectly called with context.Background(), which
ignores the test-provided ctx; replace context.Background() with the passed-in
ctx so the translation respects cancellation/deadlines and context values (i.e.,
change the translate.Translate call in AssertLive to use ctx when invoking
translate.Translate(ctx, regularQuery, driver.KindMapper(), s.CypherParams,
defaultGraph.ID)).


-- case: match (n:NodeKind1) where n.hasspn = true and n.enabled = true and not n.objectid ends with '-502' and not coalesce(n.gmsa, false) = true and not coalesce(n.msa, false) = true match (n)-[:EdgeKind1|EdgeKind2*1..]->(c:NodeKind2) with distinct n, count(c) as adminCount return n order by adminCount desc limit 100
with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'hasspn'))::bool = true and ((n0.properties ->> 'enabled'))::bool = true and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1 join edge e0 on e0.start_id = (s1.n0).id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s3.path), s3.path || e0.id from s3 join edge e0 on e0.start_id = s3.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) and s3.depth < 15 and not s3.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s3.path)) as e0, s3.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join node n0 on n0.id = s3.root_id join node n1 on n1.id = s3.next_id where s3.satisfied) select s2.n0 as n0, count(s2.n1)::int8 as i0 from s2 group by n0) select s0.n0 as n from s0 order by s0.i0 desc limit 100;
with s0 as (with s1 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties ->> 'hasspn'))::bool = true and ((n0.properties ->> 'enabled'))::bool = true and not coalesce((n0.properties ->> 'objectid'), '')::text like '%-502' and not coalesce(((n0.properties ->> 'gmsa'))::bool, false)::bool = true and not coalesce(((n0.properties ->> 'msa'))::bool, false)::bool = true) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]), s2 as (with recursive s3(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1 join edge e0 on e0.start_id = (s1.n0).id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) union select s3.root_id, e0.end_id, s3.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s3.path), s3.path || e0.id from s3 join edge e0 on e0.start_id = s3.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3, 4]::int2[]) and s3.depth < 15 and not s3.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s3.path)) as e0, s3.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1, s3 join node n0 on n0.id = s3.root_id join node n1 on n1.id = s3.next_id where s3.satisfied) select 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;
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

Avoid multiplying recursive traversal rows before count(c).

Inside s2, from s1, s3 join ... is uncorrelated with s3.root_id, so every traversal row is repeated once per s1 row. Since this case counts s2.n1, adminCount is inflated before ordering/limiting; select distinct after aggregation does not fix that. Correlate the carried frame to s3.root_id, or omit s1 from this FROM when it is not referenced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypher/models/pgsql/test/translation_cases/multipart.sql` at line 39, The s2
CTE multiplies traversal rows because its FROM clause includes "s1, s3" which is
uncorrelated to s3.root_id; this repeats each s3 row for every s1 row and
inflates the count(s2.n1). Fix by either removing s1 from the s2 FROM when it
isn't referenced, or correlate s1 to s3 (e.g., join s1 ON (s1.n0).id =
s3.root_id) so the carried frame (n0) is bound per traversal root_id; update the
s2 select/from to use the correlated join instead of the cross join.

Comment on lines +693 to +714
// AddIdentifer appends a from clause for frameID if it has not been seen before.
func (s *FromClauseBuilder) AddIdentifer(frameID pgsql.Identifier) {
if frameID == "" {
return
}

if _, already := s.seen[frameID]; !already {
s.seen[frameID] = struct{}{}
s.fromClauses = append(s.fromClauses, pgsql.FromClause{
Source: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{frameID},
},
})
}
}

// AddBinding adds the frame in which binding was last materialized, if any.
func (s *FromClauseBuilder) AddBinding(binding *BoundIdentifier) {
if binding.LastProjection != nil {
s.AddIdentifer(binding.LastProjection.Binding.Identifier)
}
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Enumerate all call sites so the rename can be applied cleanly.
rg -nP --type=go '\bAddIdentifer\b'

Repository: SpecterOps/DAWGS

Length of output: 649


Fix the misspelled exported method name AddIdentiferAddIdentifier.

Newly introduced exported API that appears across multiple files (model.go and projection.go). Once call sites proliferate, the rename gets costly and misspellings propagate into linter/IDE suggestions. Additionally, AddBinding on line 712 dereferences binding.LastProjection without null-checking binding itself—add a defensive check.

✏️ Proposed changes
-// AddIdentifer appends a from clause for frameID if it has not been seen before.
-func (s *FromClauseBuilder) AddIdentifer(frameID pgsql.Identifier) {
+// AddIdentifier appends a from clause for frameID if it has not been seen before.
+func (s *FromClauseBuilder) AddIdentifier(frameID pgsql.Identifier) {
 	if frameID == "" {
 		return
 	}
@@
 func (s *FromClauseBuilder) AddBinding(binding *BoundIdentifier) {
+	if binding == nil {
+		return
+	}
 	if binding.LastProjection != nil {
-		s.AddIdentifer(binding.LastProjection.Binding.Identifier)
+		s.AddIdentifier(binding.LastProjection.Binding.Identifier)
 	}
 }

Update call site in cypher/models/pgsql/translate/projection.go line 523 (fromClauseBuilder.AddIdentifer(...)fromClauseBuilder.AddIdentifier(...)).

Also update comment reference in cypher/models/pgsql/translate/model.go line 681.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypher/models/pgsql/translate/model.go` around lines 693 - 714, Rename the
exported method FromClauseBuilder.AddIdentifer to AddIdentifier everywhere
(update the method declaration and all call sites such as the usage in
projection.go where fromClauseBuilder.AddIdentifer(...) is invoked, and update
the related comment text), and in FromClauseBuilder.AddBinding add a defensive
nil check for the binding parameter before dereferencing binding.LastProjection
(i.e., return early if binding == nil) while keeping the existing check for
binding.LastProjection != nil.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant