Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8230942
test: cover cypher walker edge cases
zinic May 27, 2026
c6e5918
fix: complete cypher walker traversal
zinic May 27, 2026
89522d0
perf: reduce cypher walker branch allocations
zinic May 27, 2026
0799be2
test: enforce cypher walker node coverage
zinic May 27, 2026
1d49d02
docs: define cypher walker semantics
zinic May 27, 2026
e3da4ac
feat: add structural cypher walker
zinic May 27, 2026
67f84a0
fix: preserve typed nil syntax nodes
zinic May 27, 2026
8a198bd
feat: expose simple visitor order
zinic May 27, 2026
7e101aa
perf: benchmark cypher walker traversal
zinic May 27, 2026
719e158
test: assert cypher semantic walker children
zinic May 27, 2026
afc7361
test: define walker cancellation semantics
zinic May 27, 2026
48e6bcc
test: guard cypher structural walker fields
zinic May 27, 2026
9236f51
test: cover map literal iteration errors
zinic May 27, 2026
5cf6aed
refactor: split cypher walker cursor dispatch
zinic May 27, 2026
77adc92
test: assert semantic walker sequences
zinic May 27, 2026
d3c4ec7
test: cover cypher query clause traversal
zinic May 27, 2026
c67ef7f
test: cover generic walker error paths
zinic May 27, 2026
1dad0a5
test: assert structural walker type sequence
zinic May 27, 2026
24757b5
test: cover map literal keys
zinic May 27, 2026
43429d7
test: cover map formatter write errors
zinic May 27, 2026
37a31aa
docs: record cypher ast tooling validation
zinic May 27, 2026
10de8fe
docs: record review remediation preflight
zinic May 27, 2026
8c0594b
docs: clarify walker consume flag clearing
zinic May 27, 2026
b2f0f25
fix: translate walked cypher xor predicates
zinic May 27, 2026
d47e07c
perf: reduce walker nil-branch overhead
zinic May 27, 2026
ff5ff19
docs: record walker coverage comparison
zinic May 27, 2026
00767cb
docs: capture cypher walker pr notes
zinic May 27, 2026
22ffc4d
docs: record final validation results
zinic May 27, 2026
5838b44
fix(walk): restore semantic traversal contracts
zinic May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions cypher/models/cypher/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func (s Emitter) formatMapLiteral(output io.Writer, mapLiteral cypher.MapLiteral
}

first := true
for key, subExpression := range mapLiteral {
if err := mapLiteral.ForEachItem(func(key string, value cypher.Expression) error {
if !first {
if _, err := io.WriteString(output, ", "); err != nil {
return err
Expand All @@ -346,9 +346,13 @@ func (s Emitter) formatMapLiteral(output io.Writer, mapLiteral cypher.MapLiteral
return err
}

if err := s.WriteExpression(output, subExpression); err != nil {
if err := s.WriteExpression(output, value); err != nil {
return err
}

return nil
}); err != nil {
return err
}

if _, err := io.WriteString(output, "}"); err != nil {
Expand Down
102 changes: 102 additions & 0 deletions cypher/models/cypher/format/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package format_test

import (
"bytes"
"errors"
"testing"

"github.com/specterops/dawgs/cypher/models/cypher"
Expand All @@ -27,6 +28,93 @@ func TestCypherEmitter_StripLiterals(t *testing.T) {
require.Equal(t, "match (n {value: $STRIPPED}) where n.other = $STRIPPED and n.number = $STRIPPED return n.name, n", buffer.String())
}

func TestCypherEmitter_FormatsMapLiteralInKeyOrder(t *testing.T) {
var (
buffer = &bytes.Buffer{}
emitter = format.NewCypherEmitter(false)
)

err := emitter.WriteExpression(buffer, cypher.MapLiteral{
"b": cypher.NewLiteral(2, false),
"a": cypher.NewLiteral(1, false),
})

require.NoError(t, err)
require.Equal(t, "{a: 1, b: 2}", buffer.String())
}

func TestCypherEmitter_MapLiteralPropagatesExpressionError(t *testing.T) {
var (
buffer = &bytes.Buffer{}
emitter = format.NewCypherEmitter(false)
)

err := emitter.WriteExpression(buffer, cypher.MapLiteral{
"bad": struct{}{},
})

require.ErrorContains(t, err, "unexpected expression type")
}

func TestCypherEmitter_MapLiteralPropagatesWriterError(t *testing.T) {
expectedErr := errors.New("write failed")
testCases := map[string]struct {
allowedWrites int
mapLiteral cypher.MapLiteral
}{
"opening delimiter": {
allowedWrites: 0,
mapLiteral: cypher.MapLiteral{
"b": cypher.NewLiteral(2, false),
},
},
"key": {
allowedWrites: 1,
mapLiteral: cypher.MapLiteral{
"b": cypher.NewLiteral(2, false),
},
},
"colon": {
allowedWrites: 2,
mapLiteral: cypher.MapLiteral{
"b": cypher.NewLiteral(2, false),
},
},
"value": {
allowedWrites: 3,
mapLiteral: cypher.MapLiteral{
"b": cypher.NewLiteral(2, false),
},
},
"item separator": {
allowedWrites: 4,
mapLiteral: cypher.MapLiteral{
"a": cypher.NewLiteral(1, false),
"b": cypher.NewLiteral(2, false),
},
},
"closing delimiter": {
allowedWrites: 4,
mapLiteral: cypher.MapLiteral{
"b": cypher.NewLiteral(2, false),
},
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
writer := &errorAfterNWrites{
remaining: testCase.allowedWrites,
err: expectedErr,
}

err := format.NewCypherEmitter(false).WriteExpression(writer, testCase.mapLiteral)

require.ErrorIs(t, err, expectedErr)
})
}
}

func TestCypherEmitter_HappyPath(t *testing.T) {
test.LoadFixture(t, test.MutationTestCases).Run(t)
test.LoadFixture(t, test.PositiveTestCases).Run(t)
Expand Down Expand Up @@ -83,6 +171,20 @@ func TestNewStringLiteral_Escaping(t *testing.T) {
}
}

type errorAfterNWrites struct {
remaining int
err error
}

func (s *errorAfterNWrites) Write(p []byte) (int, error) {
if s.remaining == 0 {
return 0, s.err
}

s.remaining--
return len(p), nil
}

func TestNewStringLiteral_InQuery(t *testing.T) {
// Test that escaped string literals work correctly in actual Cypher queries
testCases := []struct {
Expand Down
37 changes: 28 additions & 9 deletions cypher/models/cypher/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -921,30 +921,49 @@ func (s MapLiteral) copy() MapLiteral {
return mapCopy
}

func (s MapLiteral) sortedKeys() []string {
keys := make([]string, 0, len(s))

for key := range s {
keys = append(keys, key)
}

sort.Strings(keys)
return keys
}

func (s MapLiteral) Items() []*MapItem {
items := make([]*MapItem, 0, len(s))

for key, value := range s {
_ = s.ForEachItem(func(key string, value Expression) error {
items = append(items, &MapItem{
Key: key,
Value: value,
})
}
return nil
})

return items
}

func (s MapLiteral) ForEachItem(delegate func(key string, value Expression) error) error {
for _, key := range s.sortedKeys() {
if err := delegate(key, s[key]); err != nil {
return err
}
}

return nil
}

func (s MapLiteral) Keys() []any {
keys := make([]any, 0, len(s))
sortedKeys := s.sortedKeys()
keys := make([]any, len(sortedKeys))

for key := range s {
keys = append(keys, key)
for idx, key := range sortedKeys {
keys[idx] = key
}

sort.Slice(keys, func(i, j int) bool {
return strings.Compare(keys[i].(string), keys[j].(string)) > 0
})

return keys
}

Expand Down
74 changes: 74 additions & 0 deletions cypher/models/cypher/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cypher_test

import (
"errors"
"testing"

"github.com/specterops/dawgs/cypher/models/cypher"
"github.com/stretchr/testify/require"
)

func TestMapLiteralItemsReturnsSortedItems(t *testing.T) {
aValue := cypher.NewVariableWithSymbol("a_value")
bValue := cypher.NewVariableWithSymbol("b_value")

items := cypher.MapLiteral{
"b": bValue,
"a": aValue,
}.Items()

require.Len(t, items, 2)
require.Equal(t, "a", items[0].Key)
require.Same(t, aValue, items[0].Value)
require.Equal(t, "b", items[1].Key)
require.Same(t, bValue, items[1].Value)
}

func TestMapLiteralForEachItemReturnsDelegateError(t *testing.T) {
expectedErr := errors.New("stop iteration")
var visitedKeys []string

err := cypher.MapLiteral{
"c": cypher.NewVariableWithSymbol("c_value"),
"b": cypher.NewVariableWithSymbol("b_value"),
"a": cypher.NewVariableWithSymbol("a_value"),
}.ForEachItem(func(key string, _ cypher.Expression) error {
visitedKeys = append(visitedKeys, key)
if key == "b" {
return expectedErr
}

return nil
})

require.ErrorIs(t, err, expectedErr)
require.Equal(t, []string{"a", "b"}, visitedKeys)
}

func TestMapLiteralKeysReturnsSortedKeys(t *testing.T) {
testCases := map[string]struct {
mapLiteral cypher.MapLiteral
expected []any
}{
"nil": {
expected: []any{},
},
"empty": {
mapLiteral: cypher.MapLiteral{},
expected: []any{},
},
"sorted": {
mapLiteral: cypher.MapLiteral{
"b": cypher.NewVariableWithSymbol("b_value"),
"a": cypher.NewVariableWithSymbol("a_value"),
},
expected: []any{"a", "b"},
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
require.Equal(t, testCase.expected, testCase.mapLiteral.Keys())
})
}
}
6 changes: 6 additions & 0 deletions cypher/models/pgsql/translate/predicate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func translatePredicateQuery(t *testing.T, cypherQuery string, parameters map[st
return formatted
}

func TestExclusiveDisjunctionTranslates(t *testing.T) {
formatted := translatePredicateQuery(t, `MATCH (n:NodeKind1) WHERE true XOR false RETURN n`, nil)

require.Contains(t, formatted, "true != false")
}

func TestDynamicStringPredicatesUseHelperFunctions(t *testing.T) {
for _, testCase := range []struct {
name string
Expand Down
7 changes: 7 additions & 0 deletions cypher/models/pgsql/translate/references_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ func TestCollectReferencedIdentifiersIncludesPatternPredicateReferences(t *testi
require.True(t, referencedIdentifiers.Contains("r"))
}

func TestCollectReferencedIdentifiersIncludesExclusiveDisjunctionOperands(t *testing.T) {
referencedIdentifiers := referencedIdentifiersForQuery(t, "match (n), (m) where n.enabled = true xor m.enabled = true return n")

require.True(t, referencedIdentifiers.Contains("n"))
require.True(t, referencedIdentifiers.Contains("m"))
}

func TestCollectReferencedIdentifiersIncludesRepeatedMatchPatternDeclarations(t *testing.T) {
referencedIdentifiers := referencedIdentifiersForQuery(t, "match (a)-->(b) match (b)-->(c) return c")

Expand Down
12 changes: 12 additions & 0 deletions cypher/models/pgsql/translate/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ func (s *Translator) Enter(expression cypher.SyntaxNode) {
s.treeTranslator.VisitOperator(pgsql.OperatorOr)
}

case *cypher.ExclusiveDisjunction:
for idx := 0; idx < typedExpression.Len()-1; idx++ {
s.treeTranslator.VisitOperator(pgsql.OperatorNotEquals)
}

case *cypher.Conjunction:
for idx := 0; idx < typedExpression.Len()-1; idx++ {
s.treeTranslator.VisitOperator(pgsql.OperatorAnd)
Expand Down Expand Up @@ -559,6 +564,13 @@ func (s *Translator) Exit(expression cypher.SyntaxNode) {
}
}

case *cypher.ExclusiveDisjunction:
for idx := 0; idx < typedExpression.Len()-1; idx++ {
if err := s.treeTranslator.CompleteBinaryExpression(s.scope, pgsql.OperatorNotEquals); err != nil {
s.SetError(err)
}
}

case *cypher.Conjunction:
for idx := 0; idx < typedExpression.Len()-1; idx++ {
if err := s.treeTranslator.CompleteBinaryExpression(s.scope, pgsql.OperatorAnd); err != nil {
Expand Down
Loading
Loading