Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
[[release-3-7-7]]
=== TinkerPop 3.7.7 (Release Date: NOT OFFICIALLY RELEASED YET)

* Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result iteration, providing API parity with `next(n)` in the Java, Python, and .NET GLVs.
* Fixed conjoin has incorrect null handling.
* Expanded `gremlin-python` CI matrix to test against Python 3.9, 3.10, 3.11, 3.12, and 3.13.
* Add Node 26 support for `gremlin-javascript` and `gremlint`.
Expand Down
6 changes: 5 additions & 1 deletion docs/src/reference/gremlin-variants.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,11 @@ fmt.Println(resAnon.GetString())
=== Differences
All step names start with a capital letter which is consistent with the idiomatic style for Go. This use of Pascal-case
extends to enums like `Direction`, e.g. `Direction.OUT` is `Direction.Out` in Go.
extends to enums like `Direction`, e.g. `Direction.OUT` is `Direction.Out` in Go.
Because Go does not support method overloading, the batched form of `next(n)` available in other GLVs is exposed as a
distinct method `NextN(n)` on `Traversal`. `Next()` returns a single `*Result`, while `NextN(n)` returns up to `n`
results as `[]*Result`.
[[gremlin-go-aliases]]
=== Aliases
Expand Down
28 changes: 28 additions & 0 deletions gremlin-go/driver/traversal.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ func (t *Traversal) Next() (*Result, error) {
return result, err
}

// NextN returns up to n results from the traversal. If the traversal has
// fewer than n results, only those results are returned. If n is non-positive,
// an empty slice is returned.
func (t *Traversal) NextN(n int) ([]*Result, error) {
if n <= 0 {
return []*Result{}, nil
}
results, err := t.GetResultSet()
if err != nil {
return nil, err
}
out := make([]*Result, 0, n)
for i := 0; i < n; i++ {
if results.IsEmpty() {
break
}
result, ok, err := results.One()
if err != nil {
return out, err
}
if !ok {
break
}
out = append(out, result)
}
return out, results.GetError()
}

// GetResultSet submits the traversal and returns the ResultSet.
func (t *Traversal) GetResultSet() (ResultSet, error) {
if t.results == nil {
Expand Down
127 changes: 127 additions & 0 deletions gremlin-go/driver/traversal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/tls"
"reflect"
"strings"
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -527,6 +528,132 @@ func TestTraversal(t *testing.T) {
})
}

func TestTraversalNextN(t *testing.T) {
// makeClosedResultSet builds a channelResultSet that is already closed
// after the given results have been pushed onto the channel directly
// (i.e. without going through addResult, so no bulk unrolling).
makeClosedResultSet := func(results ...*Result) *channelResultSet {
rs := newChannelResultSetCapacity("test", &synchronizedMap{make(map[string]ResultSet), sync.Mutex{}}, len(results)+1).(*channelResultSet)
for _, r := range results {
rs.channel <- r
}
rs.channelMutex.Lock()
rs.closed = true
close(rs.channel)
rs.channelMutex.Unlock()
return rs
}

t.Run("returns exactly n when n is less than available", func(t *testing.T) {
rs := makeClosedResultSet(&Result{"a"}, &Result{"b"}, &Result{"c"}, &Result{"d"})
trav := &Traversal{results: rs}

got, err := trav.NextN(3)
assert.Nil(t, err)
assert.Equal(t, 3, len(got))
assert.Equal(t, "a", got[0].Data)
assert.Equal(t, "b", got[1].Data)
assert.Equal(t, "c", got[2].Data)
})

t.Run("returns exactly n when n equals available", func(t *testing.T) {
rs := makeClosedResultSet(&Result{"a"}, &Result{"b"})
trav := &Traversal{results: rs}

got, err := trav.NextN(2)
assert.Nil(t, err)
assert.Equal(t, 2, len(got))
})

t.Run("returns all available when n exceeds available", func(t *testing.T) {
rs := makeClosedResultSet(&Result{"a"}, &Result{"b"})
trav := &Traversal{results: rs}

got, err := trav.NextN(5)
assert.Nil(t, err)
assert.Equal(t, 2, len(got))
assert.Equal(t, "a", got[0].Data)
assert.Equal(t, "b", got[1].Data)
})

t.Run("returns empty slice when n is zero", func(t *testing.T) {
rs := makeClosedResultSet(&Result{"a"})
trav := &Traversal{results: rs}

got, err := trav.NextN(0)
assert.Nil(t, err)
assert.NotNil(t, got)
assert.Equal(t, 0, len(got))
})

t.Run("returns empty slice when n is negative", func(t *testing.T) {
rs := makeClosedResultSet(&Result{"a"})
trav := &Traversal{results: rs}

got, err := trav.NextN(-3)
assert.Nil(t, err)
assert.NotNil(t, got)
assert.Equal(t, 0, len(got))
})

t.Run("returns empty slice when traversal is exhausted", func(t *testing.T) {
rs := makeClosedResultSet()
trav := &Traversal{results: rs}

got, err := trav.NextN(3)
assert.Nil(t, err)
assert.Equal(t, 0, len(got))
})

t.Run("unrolls bulked Traverser across the batch", func(t *testing.T) {
// addResult unrolls bulks when the incoming Result wraps a slice of *Traverser.
rs := newChannelResultSetCapacity("test-bulk", &synchronizedMap{make(map[string]ResultSet), sync.Mutex{}}, 8).(*channelResultSet)
rs.addResult(&Result{[]interface{}{&Traverser{bulk: 3, value: "x"}}})
rs.channelMutex.Lock()
rs.closed = true
close(rs.channel)
rs.channelMutex.Unlock()
trav := &Traversal{results: rs}

got, err := trav.NextN(2)
assert.Nil(t, err)
assert.Equal(t, 2, len(got))
assert.Equal(t, "x", got[0].Data)
assert.Equal(t, "x", got[1].Data)
})

t.Run("can be called repeatedly to drain in batches", func(t *testing.T) {
rs := makeClosedResultSet(&Result{1}, &Result{2}, &Result{3}, &Result{4}, &Result{5})
trav := &Traversal{results: rs}

first, err := trav.NextN(2)
assert.Nil(t, err)
assert.Equal(t, 2, len(first))

second, err := trav.NextN(10)
assert.Nil(t, err)
assert.Equal(t, 3, len(second))

third, err := trav.NextN(1)
assert.Nil(t, err)
assert.Equal(t, 0, len(third))
})

t.Run("propagates error from ResultSet", func(t *testing.T) {
rs := newChannelResultSetCapacity("test-err", &synchronizedMap{make(map[string]ResultSet), sync.Mutex{}}, 1).(*channelResultSet)
rs.setError(assert.AnError)
rs.channelMutex.Lock()
rs.closed = true
close(rs.channel)
rs.channelMutex.Unlock()
trav := &Traversal{results: rs}

got, err := trav.NextN(5)
assert.Equal(t, assert.AnError, err)
assert.Equal(t, 0, len(got))
})
}

func newWithOptionsConnection(t *testing.T) *GraphTraversalSource {
// No authentication integration test with graphs loaded and alias configured server
testNoAuthWithAliasUrl := getEnvOrDefaultString("GREMLIN_SERVER_URL", noAuthUrl)
Expand Down
Loading