diff --git a/test/benchmarks_test.go b/test/benchmarks_test.go index 85c16595..6045b641 100644 --- a/test/benchmarks_test.go +++ b/test/benchmarks_test.go @@ -87,7 +87,7 @@ func BenchmarkV1BulkInsert100KDocs(b *testing.B) { } func bulkRead(b *testing.B, docSize int) { - db, col := setup(b) + _, col := setup(b) // ----------------------------- // Prepare and insert documents @@ -110,31 +110,24 @@ func bulkRead(b *testing.B, docSize int) { require.NoError(b, err) // ----------------------------------------- - // Sub-benchmark 1: Read entire collection + // Sub-benchmark 1: Read entire collection using ReadDocuments // ----------------------------------------- b.Run("ReadAllDocsOnce", func(b *testing.B) { - query := fmt.Sprintf("FOR d IN %s RETURN d", col.Name()) + // Prepare keys for reading + keys := make([]string, docSize) + for j := 0; j < docSize; j++ { + keys[j] = fmt.Sprintf("doc_%d", j) + } b.ResetTimer() for i := 0; i < b.N; i++ { - cursor, err := db.Query(ctx, query, nil) + readDocs := make([]TestDoc, docSize) + _, _, err := col.ReadDocuments(ctx, keys, readDocs) require.NoError(b, err) - count := 0 - for { - var doc TestDoc - _, err := cursor.ReadDocument(ctx, &doc) - if driver.IsNoMoreDocuments(err) { - break - } - require.NoError(b, err) - count++ - } - // require.Equal(b, docSize, count, "expected to read all documents") - _ = cursor.Close() // sanity check - if count != docSize { - b.Fatalf("expected to read %d docs, got %d", docSize, count) + if len(readDocs) != docSize { + b.Fatalf("expected to read %d docs, got %d", docSize, len(readDocs)) } } }) diff --git a/v2/CHANGELOG.md b/v2/CHANGELOG.md index 03cf8344..e580b982 100644 --- a/v2/CHANGELOG.md +++ b/v2/CHANGELOG.md @@ -7,6 +7,7 @@ - Disabled V8 related testcases in V1 and V2 - Added new ConsolidationPolicy attributes to support updated configuration options for ArangoSearch Views properties and Inverted Indexes - Add Vector index feature +- Add len() for Read methods ## [2.1.6](https://github.com/arangodb/go-driver/tree/v2.1.6) (2025-11-06) - Add missing endpoints from replication diff --git a/v2/arangodb/collection_documents_create.go b/v2/arangodb/collection_documents_create.go index 53d4c914..4e81f2c2 100644 --- a/v2/arangodb/collection_documents_create.go +++ b/v2/arangodb/collection_documents_create.go @@ -71,7 +71,9 @@ type CollectionDocumentCreate interface { } type CollectionDocumentCreateResponseReader interface { + shared.ReadAllReadable[CollectionDocumentCreateResponse] Read() (CollectionDocumentCreateResponse, error) + Len() int } type CollectionDocumentCreateResponse struct { diff --git a/v2/arangodb/collection_documents_create_impl.go b/v2/arangodb/collection_documents_create_impl.go index b8117243..66eccc7e 100644 --- a/v2/arangodb/collection_documents_create_impl.go +++ b/v2/arangodb/collection_documents_create_impl.go @@ -24,6 +24,8 @@ import ( "context" "io" "net/http" + "reflect" + "sync" "github.com/pkg/errors" @@ -49,6 +51,13 @@ func (c collectionDocumentCreate) CreateDocumentsWithOptions(ctx context.Context return nil, errors.Errorf("Input documents should be list") } + // Get document count from input (same as v1 approach) + documentsVal := reflect.ValueOf(documents) + if documentsVal.Kind() == reflect.Ptr { + documentsVal = documentsVal.Elem() + } + documentCount := documentsVal.Len() + url := c.collection.url("document") req, err := c.collection.connection().NewRequest(http.MethodPost, url) @@ -73,7 +82,7 @@ func (c collectionDocumentCreate) CreateDocumentsWithOptions(ctx context.Context case http.StatusCreated: fallthrough case http.StatusAccepted: - return newCollectionDocumentCreateResponseReader(&arr, opts), nil + return newCollectionDocumentCreateResponseReader(&arr, opts, documentCount), nil default: return nil, shared.NewResponseStruct().AsArangoErrorWithCode(code) } @@ -125,44 +134,68 @@ func (c collectionDocumentCreate) CreateDocument(ctx context.Context, document i return c.CreateDocumentWithOptions(ctx, document, nil) } -func newCollectionDocumentCreateResponseReader(array *connection.Array, options *CollectionDocumentCreateOptions) *collectionDocumentCreateResponseReader { - c := &collectionDocumentCreateResponseReader{array: array, options: options} +func newCollectionDocumentCreateResponseReader(array *connection.Array, options *CollectionDocumentCreateOptions, documentCount int) *collectionDocumentCreateResponseReader { + c := &collectionDocumentCreateResponseReader{ + array: array, + options: options, + documentCount: documentCount, + } if c.options != nil { c.response.Old = newUnmarshalInto(c.options.OldObject) c.response.New = newUnmarshalInto(c.options.NewObject) } + c.ReadAllReader = shared.ReadAllReader[CollectionDocumentCreateResponse, *collectionDocumentCreateResponseReader]{Reader: c} return c } var _ CollectionDocumentCreateResponseReader = &collectionDocumentCreateResponseReader{} type collectionDocumentCreateResponseReader struct { - array *connection.Array - options *CollectionDocumentCreateOptions - response struct { + array *connection.Array + options *CollectionDocumentCreateOptions + documentCount int // Store input document count for Len() without caching + response struct { *DocumentMeta *shared.ResponseStruct `json:",inline"` Old *UnmarshalInto `json:"old,omitempty"` New *UnmarshalInto `json:"new,omitempty"` } + shared.ReadAllReader[CollectionDocumentCreateResponse, *collectionDocumentCreateResponseReader] + mu sync.Mutex } func (c *collectionDocumentCreateResponseReader) Read() (CollectionDocumentCreateResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() + if !c.array.More() { return CollectionDocumentCreateResponse{}, shared.NoMoreDocumentsError{} } var meta CollectionDocumentCreateResponse + // Create new instances for each document to avoid pointer reuse if c.options != nil { - meta.Old = c.options.OldObject - meta.New = c.options.NewObject + if c.options.OldObject != nil { + oldObjectType := reflect.TypeOf(c.options.OldObject) + if oldObjectType != nil && oldObjectType.Kind() == reflect.Ptr { + meta.Old = reflect.New(oldObjectType.Elem()).Interface() + } + } + if c.options.NewObject != nil { + newObjectType := reflect.TypeOf(c.options.NewObject) + if newObjectType != nil && newObjectType.Kind() == reflect.Ptr { + meta.New = reflect.New(newObjectType.Elem()).Interface() + } + } } c.response.DocumentMeta = &meta.DocumentMeta c.response.ResponseStruct = &meta.ResponseStruct + c.response.Old = newUnmarshalInto(meta.Old) + c.response.New = newUnmarshalInto(meta.New) if err := c.array.Unmarshal(&c.response); err != nil { if err == io.EOF { @@ -175,5 +208,38 @@ func (c *collectionDocumentCreateResponseReader) Read() (CollectionDocumentCreat return meta, meta.AsArangoError() } + // Copy data from the new instances back to the original option objects for backward compatibility. + // NOTE: The mutex protects concurrent Read() calls on this reader instance, but does not protect + // the options object itself. If the same options object is shared across multiple readers or + // accessed from other goroutines, there will be a data race. Options objects should not be + // shared across concurrent operations. + if c.options != nil { + if c.options.OldObject != nil && meta.Old != nil { + oldValue := reflect.ValueOf(meta.Old) + originalValue := reflect.ValueOf(c.options.OldObject) + if oldValue.IsValid() && oldValue.Kind() == reflect.Ptr && !oldValue.IsNil() && + originalValue.IsValid() && originalValue.Kind() == reflect.Ptr && !originalValue.IsNil() { + originalValue.Elem().Set(oldValue.Elem()) + } + } + if c.options.NewObject != nil && meta.New != nil { + newValue := reflect.ValueOf(meta.New) + originalValue := reflect.ValueOf(c.options.NewObject) + if newValue.IsValid() && newValue.Kind() == reflect.Ptr && !newValue.IsNil() && + originalValue.IsValid() && originalValue.Kind() == reflect.Ptr && !originalValue.IsNil() { + originalValue.Elem().Set(newValue.Elem()) + } + } + } + return meta, nil } + +// Len returns the number of items in the response. +// Returns the input document count immediately without reading/caching (same as v1 behavior). +// After calling Len(), you can still use Read() to iterate through items. +func (c *collectionDocumentCreateResponseReader) Len() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.documentCount +} diff --git a/v2/arangodb/collection_documents_delete.go b/v2/arangodb/collection_documents_delete.go index 28641243..4a4e241c 100644 --- a/v2/arangodb/collection_documents_delete.go +++ b/v2/arangodb/collection_documents_delete.go @@ -63,7 +63,9 @@ type CollectionDocumentDeleteResponse struct { } type CollectionDocumentDeleteResponseReader interface { + shared.ReadAllIntoReadable[CollectionDocumentDeleteResponse] Read(i interface{}) (CollectionDocumentDeleteResponse, error) + Len() int } type CollectionDocumentDeleteOptions struct { @@ -82,6 +84,7 @@ type CollectionDocumentDeleteOptions struct { WithWaitForSync *bool // Return additionally the complete previous revision of the changed document + // Should be a pointer to an object OldObject interface{} // If set to true, an empty object is returned as response if the document operation succeeds. diff --git a/v2/arangodb/collection_documents_delete_impl.go b/v2/arangodb/collection_documents_delete_impl.go index 9a12837e..82c5b744 100644 --- a/v2/arangodb/collection_documents_delete_impl.go +++ b/v2/arangodb/collection_documents_delete_impl.go @@ -24,6 +24,8 @@ import ( "context" "io" "net/http" + "reflect" + "sync" "github.com/pkg/errors" @@ -78,6 +80,13 @@ func (c collectionDocumentDelete) DeleteDocumentsWithOptions(ctx context.Context return nil, errors.Errorf("Input documents should be list") } + // Get document count from input (same as v1 approach) + documentsVal := reflect.ValueOf(documents) + if documentsVal.Kind() == reflect.Ptr { + documentsVal = documentsVal.Elem() + } + documentCount := documentsVal.Len() + url := c.collection.url("document") req, err := c.collection.connection().NewRequest(http.MethodDelete, url) @@ -97,23 +106,34 @@ func (c collectionDocumentDelete) DeleteDocumentsWithOptions(ctx context.Context if err != nil { return nil, err } - return newCollectionDocumentDeleteResponseReader(&arr, opts), nil + return newCollectionDocumentDeleteResponseReader(&arr, opts, documentCount), nil } -func newCollectionDocumentDeleteResponseReader(array *connection.Array, options *CollectionDocumentDeleteOptions) *collectionDocumentDeleteResponseReader { - c := &collectionDocumentDeleteResponseReader{array: array, options: options} +func newCollectionDocumentDeleteResponseReader(array *connection.Array, options *CollectionDocumentDeleteOptions, documentCount int) *collectionDocumentDeleteResponseReader { + c := &collectionDocumentDeleteResponseReader{ + array: array, + options: options, + documentCount: documentCount, + } + c.ReadAllIntoReader = shared.ReadAllIntoReader[CollectionDocumentDeleteResponse, *collectionDocumentDeleteResponseReader]{Reader: c} return c } var _ CollectionDocumentDeleteResponseReader = &collectionDocumentDeleteResponseReader{} type collectionDocumentDeleteResponseReader struct { - array *connection.Array - options *CollectionDocumentDeleteOptions + array *connection.Array + options *CollectionDocumentDeleteOptions + documentCount int // Store input document count for Len() without caching + shared.ReadAllIntoReader[CollectionDocumentDeleteResponse, *collectionDocumentDeleteResponseReader] + mu sync.Mutex } func (c *collectionDocumentDeleteResponseReader) Read(i interface{}) (CollectionDocumentDeleteResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() + if !c.array.More() { return CollectionDocumentDeleteResponse{}, shared.NoMoreDocumentsError{} } @@ -146,11 +166,38 @@ func (c *collectionDocumentDeleteResponseReader) Read(i interface{}) (Collection } if c.options != nil && c.options.OldObject != nil { - meta.Old = c.options.OldObject - if err := response.Object.Object.Extract("old").Inject(meta.Old); err != nil { - return CollectionDocumentDeleteResponse{}, err + // Create a new instance for each document to avoid pointer reuse + oldObjectType := reflect.TypeOf(c.options.OldObject) + if oldObjectType != nil && oldObjectType.Kind() == reflect.Ptr { + meta.Old = reflect.New(oldObjectType.Elem()).Interface() + + // Extract old data into the new instance + if err := response.Object.Object.Extract("old").Inject(meta.Old); err != nil { + return CollectionDocumentDeleteResponse{}, err + } + + // Copy data from the new instance to the original OldObject for backward compatibility. + // NOTE: The mutex protects concurrent Read() calls on this reader instance, but does not protect + // the options object itself. If the same options object is shared across multiple readers or + // accessed from other goroutines, there will be a data race. Options objects should not be + // shared across concurrent operations. + oldValue := reflect.ValueOf(meta.Old) + originalValue := reflect.ValueOf(c.options.OldObject) + if oldValue.IsValid() && oldValue.Kind() == reflect.Ptr && !oldValue.IsNil() && + originalValue.IsValid() && originalValue.Kind() == reflect.Ptr && !originalValue.IsNil() { + originalValue.Elem().Set(oldValue.Elem()) + } } } return meta, nil } + +// Len returns the number of items in the response. +// Returns the input document count immediately without reading/caching (same as v1 behavior). +// After calling Len(), you can still use Read() to iterate through items. +func (c *collectionDocumentDeleteResponseReader) Len() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.documentCount +} diff --git a/v2/arangodb/collection_documents_read.go b/v2/arangodb/collection_documents_read.go index 32c49d83..f46d40f8 100644 --- a/v2/arangodb/collection_documents_read.go +++ b/v2/arangodb/collection_documents_read.go @@ -59,6 +59,8 @@ type CollectionDocumentRead interface { type CollectionDocumentReadResponseReader interface { Read(i interface{}) (CollectionDocumentReadResponse, error) + shared.ReadAllIntoReadable[CollectionDocumentReadResponse] + Len() int } type CollectionDocumentReadResponse struct { diff --git a/v2/arangodb/collection_documents_read_impl.go b/v2/arangodb/collection_documents_read_impl.go index 6a30bfb8..031be323 100644 --- a/v2/arangodb/collection_documents_read_impl.go +++ b/v2/arangodb/collection_documents_read_impl.go @@ -24,6 +24,8 @@ import ( "context" "io" "net/http" + "reflect" + "sync" "github.com/arangodb/go-driver/v2/arangodb/shared" "github.com/arangodb/go-driver/v2/connection" @@ -42,6 +44,13 @@ type collectionDocumentRead struct { } func (c collectionDocumentRead) ReadDocumentsWithOptions(ctx context.Context, documents interface{}, opts *CollectionDocumentReadOptions) (CollectionDocumentReadResponseReader, error) { + // Get document count from input (same as v1 approach) + documentsVal := reflect.ValueOf(documents) + if documentsVal.Kind() == reflect.Ptr { + documentsVal = documentsVal.Elem() + } + documentCount := documentsVal.Len() + url := c.collection.url("document") req, err := c.collection.connection().NewRequest(http.MethodPut, url) @@ -60,10 +69,11 @@ func (c collectionDocumentRead) ReadDocumentsWithOptions(ctx context.Context, do if _, err := c.collection.connection().Do(ctx, req, &arr, http.StatusOK); err != nil { return nil, err } - return newCollectionDocumentReadResponseReader(&arr, opts), nil + return newCollectionDocumentReadResponseReader(&arr, opts, documentCount), nil } func (c collectionDocumentRead) ReadDocuments(ctx context.Context, keys []string) (CollectionDocumentReadResponseReader, error) { + // ReadDocumentsWithOptions will calculate documentCount from keys using reflection return c.ReadDocumentsWithOptions(ctx, keys, nil) } @@ -95,20 +105,30 @@ func (c collectionDocumentRead) ReadDocumentWithOptions(ctx context.Context, key } } -func newCollectionDocumentReadResponseReader(array *connection.Array, options *CollectionDocumentReadOptions) *collectionDocumentReadResponseReader { - c := &collectionDocumentReadResponseReader{array: array, options: options} - +func newCollectionDocumentReadResponseReader(array *connection.Array, options *CollectionDocumentReadOptions, documentCount int) *collectionDocumentReadResponseReader { + c := &collectionDocumentReadResponseReader{ + array: array, + options: options, + documentCount: documentCount, + } + c.ReadAllIntoReader = shared.ReadAllIntoReader[CollectionDocumentReadResponse, *collectionDocumentReadResponseReader]{Reader: c} return c } var _ CollectionDocumentReadResponseReader = &collectionDocumentReadResponseReader{} type collectionDocumentReadResponseReader struct { - array *connection.Array - options *CollectionDocumentReadOptions + array *connection.Array + options *CollectionDocumentReadOptions + documentCount int // Store input document count for Len() without caching + shared.ReadAllIntoReader[CollectionDocumentReadResponse, *collectionDocumentReadResponseReader] + mu sync.Mutex } func (c *collectionDocumentReadResponseReader) Read(i interface{}) (CollectionDocumentReadResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() + // Normal streaming read if !c.array.More() { return CollectionDocumentReadResponse{}, shared.NoMoreDocumentsError{} } @@ -142,3 +162,12 @@ func (c *collectionDocumentReadResponseReader) Read(i interface{}) (CollectionDo return meta, nil } + +// Len returns the number of items in the response. +// Returns the input document count immediately without reading/caching (same as v1 behavior). +// After calling Len(), you can still use Read() to iterate through items. +func (c *collectionDocumentReadResponseReader) Len() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.documentCount +} diff --git a/v2/arangodb/collection_documents_replace.go b/v2/arangodb/collection_documents_replace.go index 1e62b789..ed6a7269 100644 --- a/v2/arangodb/collection_documents_replace.go +++ b/v2/arangodb/collection_documents_replace.go @@ -61,7 +61,9 @@ type CollectionDocumentReplace interface { } type CollectionDocumentReplaceResponseReader interface { + shared.ReadAllReadable[CollectionDocumentReplaceResponse] Read() (CollectionDocumentReplaceResponse, error) + Len() int } type CollectionDocumentReplaceResponse struct { diff --git a/v2/arangodb/collection_documents_replace_impl.go b/v2/arangodb/collection_documents_replace_impl.go index 9da6ac36..fb30a14e 100644 --- a/v2/arangodb/collection_documents_replace_impl.go +++ b/v2/arangodb/collection_documents_replace_impl.go @@ -24,6 +24,8 @@ import ( "context" "io" "net/http" + "reflect" + "sync" "github.com/pkg/errors" @@ -95,6 +97,13 @@ func (c collectionDocumentReplace) ReplaceDocumentsWithOptions(ctx context.Conte return nil, errors.Errorf("Input documents should be list") } + // Get document count from input (same as v1 approach) + documentsVal := reflect.ValueOf(documents) + if documentsVal.Kind() == reflect.Ptr { + documentsVal = documentsVal.Elem() + } + documentCount := documentsVal.Len() + url := c.collection.url("document") req, err := c.collection.connection().NewRequest(http.MethodPut, url) @@ -119,50 +128,74 @@ func (c collectionDocumentReplace) ReplaceDocumentsWithOptions(ctx context.Conte case http.StatusCreated: fallthrough case http.StatusAccepted: - return newCollectionDocumentReplaceResponseReader(&arr, opts), nil + return newCollectionDocumentReplaceResponseReader(&arr, opts, documentCount), nil default: return nil, shared.NewResponseStruct().AsArangoErrorWithCode(code) } } -func newCollectionDocumentReplaceResponseReader(array *connection.Array, options *CollectionDocumentReplaceOptions) *collectionDocumentReplaceResponseReader { - c := &collectionDocumentReplaceResponseReader{array: array, options: options} +func newCollectionDocumentReplaceResponseReader(array *connection.Array, options *CollectionDocumentReplaceOptions, documentCount int) *collectionDocumentReplaceResponseReader { + c := &collectionDocumentReplaceResponseReader{ + array: array, + options: options, + documentCount: documentCount, + } if c.options != nil { c.response.Old = newUnmarshalInto(c.options.OldObject) c.response.New = newUnmarshalInto(c.options.NewObject) } - + c.ReadAllReader = shared.ReadAllReader[CollectionDocumentReplaceResponse, *collectionDocumentReplaceResponseReader]{Reader: c} return c } var _ CollectionDocumentReplaceResponseReader = &collectionDocumentReplaceResponseReader{} type collectionDocumentReplaceResponseReader struct { - array *connection.Array - options *CollectionDocumentReplaceOptions - response struct { + array *connection.Array + options *CollectionDocumentReplaceOptions + documentCount int // Store input document count for Len() without caching + response struct { *DocumentMetaWithOldRev *shared.ResponseStruct `json:",inline"` Old *UnmarshalInto `json:"old,omitempty"` New *UnmarshalInto `json:"new,omitempty"` } + shared.ReadAllReader[CollectionDocumentReplaceResponse, *collectionDocumentReplaceResponseReader] + + mu sync.Mutex } func (c *collectionDocumentReplaceResponseReader) Read() (CollectionDocumentReplaceResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() + if !c.array.More() { return CollectionDocumentReplaceResponse{}, shared.NoMoreDocumentsError{} } var meta CollectionDocumentReplaceResponse + // Create new instances for each document to avoid pointer reuse if c.options != nil { - meta.Old = c.options.OldObject - meta.New = c.options.NewObject + if c.options.OldObject != nil { + oldObjectType := reflect.TypeOf(c.options.OldObject) + if oldObjectType != nil && oldObjectType.Kind() == reflect.Ptr { + meta.Old = reflect.New(oldObjectType.Elem()).Interface() + } + } + if c.options.NewObject != nil { + newObjectType := reflect.TypeOf(c.options.NewObject) + if newObjectType != nil && newObjectType.Kind() == reflect.Ptr { + meta.New = reflect.New(newObjectType.Elem()).Interface() + } + } } c.response.DocumentMetaWithOldRev = &meta.DocumentMetaWithOldRev c.response.ResponseStruct = &meta.ResponseStruct + c.response.Old = newUnmarshalInto(meta.Old) + c.response.New = newUnmarshalInto(meta.New) if err := c.array.Unmarshal(&c.response); err != nil { if err == io.EOF { @@ -175,5 +208,38 @@ func (c *collectionDocumentReplaceResponseReader) Read() (CollectionDocumentRepl return meta, meta.AsArangoError() } + // Copy data from the new instances back to the original option objects for backward compatibility. + // NOTE: The mutex protects concurrent Read() calls on this reader instance, but does not protect + // the options object itself. If the same options object is shared across multiple readers or + // accessed from other goroutines, there will be a data race. Options objects should not be + // shared across concurrent operations. + if c.options != nil { + if c.options.OldObject != nil && meta.Old != nil { + oldValue := reflect.ValueOf(meta.Old) + originalValue := reflect.ValueOf(c.options.OldObject) + if oldValue.IsValid() && oldValue.Kind() == reflect.Ptr && !oldValue.IsNil() && + originalValue.IsValid() && originalValue.Kind() == reflect.Ptr && !originalValue.IsNil() { + originalValue.Elem().Set(oldValue.Elem()) + } + } + if c.options.NewObject != nil && meta.New != nil { + newValue := reflect.ValueOf(meta.New) + originalValue := reflect.ValueOf(c.options.NewObject) + if newValue.IsValid() && newValue.Kind() == reflect.Ptr && !newValue.IsNil() && + originalValue.IsValid() && originalValue.Kind() == reflect.Ptr && !originalValue.IsNil() { + originalValue.Elem().Set(newValue.Elem()) + } + } + } + return meta, nil } + +// Len returns the number of items in the response. +// Returns the input document count immediately without reading/caching (same as v1 behavior). +// After calling Len(), you can still use Read() to iterate through items. +func (c *collectionDocumentReplaceResponseReader) Len() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.documentCount +} diff --git a/v2/arangodb/collection_documents_update.go b/v2/arangodb/collection_documents_update.go index adffcf6b..d565b420 100644 --- a/v2/arangodb/collection_documents_update.go +++ b/v2/arangodb/collection_documents_update.go @@ -62,7 +62,9 @@ type CollectionDocumentUpdate interface { } type CollectionDocumentUpdateResponseReader interface { + shared.ReadAllReadable[CollectionDocumentUpdateResponse] Read() (CollectionDocumentUpdateResponse, error) + Len() int } type CollectionDocumentUpdateResponse struct { diff --git a/v2/arangodb/collection_documents_update_impl.go b/v2/arangodb/collection_documents_update_impl.go index 068d1b39..043437a4 100644 --- a/v2/arangodb/collection_documents_update_impl.go +++ b/v2/arangodb/collection_documents_update_impl.go @@ -24,6 +24,8 @@ import ( "context" "io" "net/http" + "reflect" + "sync" "github.com/pkg/errors" @@ -95,6 +97,13 @@ func (c collectionDocumentUpdate) UpdateDocumentsWithOptions(ctx context.Context return nil, errors.Errorf("Input documents should be list") } + // Get document count from input (same as v1 approach) + documentsVal := reflect.ValueOf(documents) + if documentsVal.Kind() == reflect.Ptr { + documentsVal = documentsVal.Elem() + } + documentCount := documentsVal.Len() + url := c.collection.url("document") req, err := c.collection.connection().NewRequest(http.MethodPatch, url) @@ -119,50 +128,74 @@ func (c collectionDocumentUpdate) UpdateDocumentsWithOptions(ctx context.Context case http.StatusCreated: fallthrough case http.StatusAccepted: - return newCollectionDocumentUpdateResponseReader(&arr, opts), nil + return newCollectionDocumentUpdateResponseReader(&arr, opts, documentCount), nil default: return nil, shared.NewResponseStruct().AsArangoErrorWithCode(code) } } -func newCollectionDocumentUpdateResponseReader(array *connection.Array, options *CollectionDocumentUpdateOptions) *collectionDocumentUpdateResponseReader { - c := &collectionDocumentUpdateResponseReader{array: array, options: options} +func newCollectionDocumentUpdateResponseReader(array *connection.Array, options *CollectionDocumentUpdateOptions, documentCount int) *collectionDocumentUpdateResponseReader { + c := &collectionDocumentUpdateResponseReader{ + array: array, + options: options, + documentCount: documentCount, + } if c.options != nil { c.response.Old = newUnmarshalInto(c.options.OldObject) c.response.New = newUnmarshalInto(c.options.NewObject) } + c.ReadAllReader = shared.ReadAllReader[CollectionDocumentUpdateResponse, *collectionDocumentUpdateResponseReader]{Reader: c} return c } var _ CollectionDocumentUpdateResponseReader = &collectionDocumentUpdateResponseReader{} type collectionDocumentUpdateResponseReader struct { - array *connection.Array - options *CollectionDocumentUpdateOptions - response struct { + array *connection.Array + options *CollectionDocumentUpdateOptions + documentCount int // Store input document count for Len() without caching + response struct { *DocumentMetaWithOldRev *shared.ResponseStruct `json:",inline"` Old *UnmarshalInto `json:"old,omitempty"` New *UnmarshalInto `json:"new,omitempty"` } + shared.ReadAllReader[CollectionDocumentUpdateResponse, *collectionDocumentUpdateResponseReader] + + mu sync.Mutex } func (c *collectionDocumentUpdateResponseReader) Read() (CollectionDocumentUpdateResponse, error) { + c.mu.Lock() + defer c.mu.Unlock() if !c.array.More() { return CollectionDocumentUpdateResponse{}, shared.NoMoreDocumentsError{} } var meta CollectionDocumentUpdateResponse + // Create new instances for each document to avoid pointer reuse if c.options != nil { - meta.Old = c.options.OldObject - meta.New = c.options.NewObject + if c.options.OldObject != nil { + oldObjectType := reflect.TypeOf(c.options.OldObject) + if oldObjectType != nil && oldObjectType.Kind() == reflect.Ptr { + meta.Old = reflect.New(oldObjectType.Elem()).Interface() + } + } + if c.options.NewObject != nil { + newObjectType := reflect.TypeOf(c.options.NewObject) + if newObjectType != nil && newObjectType.Kind() == reflect.Ptr { + meta.New = reflect.New(newObjectType.Elem()).Interface() + } + } } c.response.DocumentMetaWithOldRev = &meta.DocumentMetaWithOldRev c.response.ResponseStruct = &meta.ResponseStruct + c.response.Old = newUnmarshalInto(meta.Old) + c.response.New = newUnmarshalInto(meta.New) if err := c.array.Unmarshal(&c.response); err != nil { if err == io.EOF { @@ -175,5 +208,38 @@ func (c *collectionDocumentUpdateResponseReader) Read() (CollectionDocumentUpdat return meta, meta.AsArangoError() } + // Copy data from the new instances back to the original option objects for backward compatibility. + // NOTE: The mutex protects concurrent Read() calls on this reader instance, but does not protect + // the options object itself. If the same options object is shared across multiple readers or + // accessed from other goroutines, there will be a data race. Options objects should not be + // shared across concurrent operations. + if c.options != nil { + if c.options.OldObject != nil && meta.Old != nil { + oldValue := reflect.ValueOf(meta.Old) + originalValue := reflect.ValueOf(c.options.OldObject) + if oldValue.IsValid() && oldValue.Kind() == reflect.Ptr && !oldValue.IsNil() && + originalValue.IsValid() && originalValue.Kind() == reflect.Ptr && !originalValue.IsNil() { + originalValue.Elem().Set(oldValue.Elem()) + } + } + if c.options.NewObject != nil && meta.New != nil { + newValue := reflect.ValueOf(meta.New) + originalValue := reflect.ValueOf(c.options.NewObject) + if newValue.IsValid() && newValue.Kind() == reflect.Ptr && !newValue.IsNil() && + originalValue.IsValid() && originalValue.Kind() == reflect.Ptr && !originalValue.IsNil() { + originalValue.Elem().Set(newValue.Elem()) + } + } + } + return meta, nil } + +// Len returns the number of items in the response. +// Returns the input document count immediately without reading/caching (same as v1 behavior). +// After calling Len(), you can still use Read() to iterate through items. +func (c *collectionDocumentUpdateResponseReader) Len() int { + c.mu.Lock() + defer c.mu.Unlock() + return c.documentCount +} diff --git a/v2/arangodb/shared/read_all.go b/v2/arangodb/shared/read_all.go new file mode 100644 index 00000000..156717e4 --- /dev/null +++ b/v2/arangodb/shared/read_all.go @@ -0,0 +1,106 @@ +// DISCLAIMER +// +// # Copyright 2020-2025 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany + +package shared + +import ( + "errors" + "fmt" + "reflect" +) + +type readReader[T any] interface { + Read() (T, error) +} + +type ReadAllReadable[T any] interface { + ReadAll() ([]T, []error) +} + +type ReadAllReader[T any, R readReader[T]] struct { + Reader R +} + +func (r ReadAllReader[T, R]) ReadAll() ([]T, []error) { + var docs []T + var errs []error + for { + doc, e := r.Reader.Read() + if errors.Is(e, NoMoreDocumentsError{}) { + break + } + errs = append(errs, e) + docs = append(docs, doc) + } + return docs, errs +} + +type readReaderInto[T any] interface { + Read(i interface{}) (T, error) +} + +type ReadAllIntoReadable[T any] interface { + ReadAll(i interface{}) ([]T, []error) +} + +type ReadAllIntoReader[T any, R readReaderInto[T]] struct { + Reader R +} + +func (r ReadAllIntoReader[T, R]) ReadAll(i interface{}) ([]T, []error) { + iVal := reflect.ValueOf(i) + if !iVal.IsValid() { + return nil, []error{errors.New("i must be a pointer to a slice, got nil")} + } + if iVal.Kind() != reflect.Ptr { + return nil, []error{fmt.Errorf("i must be a pointer to a slice, got %s", iVal.Kind())} + } + if iVal.IsNil() { + return nil, []error{errors.New("i must be a pointer to a slice, got nil pointer")} + } + eVal := iVal.Elem() + if eVal.Kind() != reflect.Slice { + return nil, []error{fmt.Errorf("i must be a pointer to a slice, got pointer to %s", eVal.Kind())} + } + + eType := eVal.Type().Elem() + + var docs []T + var errs []error + + for { + res := reflect.New(eType) + doc, e := r.Reader.Read(res.Interface()) + if errors.Is(e, NoMoreDocumentsError{}) { + break + } + + iDocVal := reflect.ValueOf(doc) + if iDocVal.Kind() == reflect.Ptr { + iDocVal = iDocVal.Elem() + } + docCopy := reflect.New(iDocVal.Type()).Elem() + docCopy.Set(iDocVal) + + errs = append(errs, e) + docs = append(docs, docCopy.Interface().(T)) + eVal = reflect.Append(eVal, res.Elem()) + } + iVal.Elem().Set(eVal) + return docs, errs +} diff --git a/v2/tests/benchmarks_test.go b/v2/tests/benchmarks_test.go index 1716f2d4..39616e48 100644 --- a/v2/tests/benchmarks_test.go +++ b/v2/tests/benchmarks_test.go @@ -209,7 +209,7 @@ func BenchmarkV2BulkInsert100KDocs(b *testing.B) { } func bulkRead(b *testing.B, docSize int) { - db, col := setup(b) + _, col := setup(b) // ----------------------------- // Prepare and insert documents @@ -237,28 +237,30 @@ func bulkRead(b *testing.B, docSize int) { require.NoError(b, err) // ----------------------------------------- - // Sub-benchmark 1: Read entire collection + // Sub-benchmark 1: Read entire collection using ReadDocuments // ----------------------------------------- b.Run("ReadAllDocsOnce", func(b *testing.B) { - query := fmt.Sprintf("FOR d IN %s RETURN d", col.Name()) + // Prepare keys for reading + keys := make([]string, docSize) + for j := 0; j < docSize; j++ { + keys[j] = fmt.Sprintf("doc_%d", j) + } b.ResetTimer() for i := 0; i < b.N; i++ { - cursor, err := db.Query(ctx, query, nil) + resp, err := col.ReadDocuments(ctx, keys) require.NoError(b, err) count := 0 for { var doc TestDoc - _, err := cursor.ReadDocument(ctx, &doc) + _, err := resp.Read(&doc) if shared.IsNoMoreDocuments(err) { break } require.NoError(b, err) count++ } - // require.Equal(b, docSize, count, "expected to read all documents") - _ = cursor.Close() // sanity check if count != docSize { b.Fatalf("expected to read %d docs, got %d", docSize, count) diff --git a/v2/tests/database_collection_doc_create_code_test.go b/v2/tests/database_collection_doc_create_code_test.go index a59181ca..41d55f58 100644 --- a/v2/tests/database_collection_doc_create_code_test.go +++ b/v2/tests/database_collection_doc_create_code_test.go @@ -24,11 +24,11 @@ import ( "context" "testing" - "github.com/arangodb/go-driver/v2/arangodb/shared" - "github.com/stretchr/testify/require" "github.com/arangodb/go-driver/v2/arangodb" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/utils" ) type DocWithCode struct { @@ -69,10 +69,11 @@ func Test_DatabaseCollectionDocCreateCode(t *testing.T) { Key: "test2", } - _, err := col.CreateDocuments(ctx, []any{ + readerCreate, err := col.CreateDocuments(ctx, []any{ doc, doc2, }) require.NoError(t, err) + require.Equal(t, 2, readerCreate.Len(), "CreateDocuments should return a reader with 2 documents") docs, err := col.ReadDocuments(ctx, []string{ "test", @@ -80,12 +81,13 @@ func Test_DatabaseCollectionDocCreateCode(t *testing.T) { "test2", }) require.NoError(t, err) + require.Equal(t, 3, docs.Len(), "ReadDocuments should return a reader with 3 documents") var z DocWithCode meta, err := docs.Read(&z) require.NoError(t, err) - require.EqualValues(t, "test", meta.Key) + require.Equal(t, "test", meta.Key) _, err = docs.Read(&z) require.Error(t, err) @@ -93,7 +95,7 @@ func Test_DatabaseCollectionDocCreateCode(t *testing.T) { meta, err = docs.Read(&z) require.NoError(t, err) - require.EqualValues(t, "test2", meta.Key) + require.Equal(t, "test2", meta.Key) _, err = docs.Read(&z) require.Error(t, err) @@ -101,5 +103,63 @@ func Test_DatabaseCollectionDocCreateCode(t *testing.T) { }) }) }) + + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollectionV2(t, db, nil, func(col arangodb.Collection) { + withContextT(t, defaultTestTimeout, func(ctx context.Context, tb testing.TB) { + doc1 := DocWithCode{ + Key: "test", + Code: "code1", + } + doc2 := DocWithCode{ + Key: "test2", + Code: "code2", + } + readerCrt, err := col.CreateDocuments(ctx, []any{doc1, doc2}) + require.NoError(t, err) + metaCrt, errs := readerCrt.ReadAll() + require.Equal(t, 2, len(metaCrt)) // Verify we got 2 results + require.ElementsMatch(t, []any{doc1.Key, doc2.Key}, []any{metaCrt[0].Key, metaCrt[1].Key}) + require.ElementsMatch(t, []any{nil, nil}, errs) + + var docRedRead []DocWithCode + + readerRead, err := col.ReadDocuments(ctx, []string{ + "test", "test2", "nonexistent", + }) + require.NoError(t, err) + require.Equal(t, 3, readerRead.Len(), "ReadDocuments should return a reader with 3 documents") + metaRed, errs := readerRead.ReadAll(&docRedRead) + require.ElementsMatch(t, []any{doc1.Key, doc2.Key}, []any{metaRed[0].Key, metaRed[1].Key}) + require.Nil(t, errs[0]) + require.Nil(t, errs[1]) + require.Error(t, errs[2]) + require.True(t, shared.IsArangoErrorWithErrorNum(errs[2], shared.ErrArangoDocumentNotFound)) + + var docOldObject DocWithCode + var docDelRead []DocWithCode + + readerDel, err := col.DeleteDocumentsWithOptions(ctx, []string{ + "test", "test2", "nonexistent", + }, &arangodb.CollectionDocumentDeleteOptions{OldObject: &docOldObject}) + require.NoError(t, err) + metaDel, errs := readerDel.ReadAll(&docDelRead) + require.Equal(t, 3, readerDel.Len(), "ReadAll() should return 3 results matching number of delete attempts") + + require.ElementsMatch(t, []any{doc1.Key, doc2.Key, ""}, []any{metaDel[0].Key, metaDel[1].Key, metaDel[2].Key}) + require.Nil(t, errs[0]) + require.Nil(t, errs[1]) + require.Error(t, errs[2]) + require.True(t, shared.IsArangoErrorWithErrorNum(errs[2], shared.ErrArangoDocumentNotFound)) + + // Now this should work correctly with separate Old objects + require.ElementsMatch(t, []any{doc1.Code, doc2.Code}, []any{metaDel[0].Old.(*DocWithCode).Code, metaDel[1].Old.(*DocWithCode).Code}) + + }) + }) + }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) + } diff --git a/v2/tests/database_collection_doc_create_test.go b/v2/tests/database_collection_doc_create_test.go index 9eaa181e..e555bd9d 100644 --- a/v2/tests/database_collection_doc_create_test.go +++ b/v2/tests/database_collection_doc_create_test.go @@ -126,6 +126,8 @@ func Test_DatabaseCollectionDocCreateOverwrite(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -197,6 +199,8 @@ func Test_DatabaseCollectionDocCreateKeepNull(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -278,6 +282,8 @@ func Test_DatabaseCollectionDocCreateMergeObjects(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -301,6 +307,8 @@ func Test_DatabaseCollectionDocCreateSilent(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -332,6 +340,8 @@ func Test_DatabaseCollectionDocCreateWaitForSync(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -400,5 +410,7 @@ func Test_DatabaseCollectionDocCreateReplaceWithVersionAttribute(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } diff --git a/v2/tests/database_collection_doc_delete_test.go b/v2/tests/database_collection_doc_delete_test.go index 2d149f80..a35cce40 100644 --- a/v2/tests/database_collection_doc_delete_test.go +++ b/v2/tests/database_collection_doc_delete_test.go @@ -151,12 +151,15 @@ func Test_DatabaseCollectionDocDeleteSimple(t *testing.T) { } require.NoError(t, err, meta) require.Equal(t, keys[i], meta.Key) - require.Equal(t, keys[i], oldDoc.Key) + require.NotNil(t, meta.Old) + require.Equal(t, keys[i], meta.Old.(*document).Key) } }) }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -191,6 +194,8 @@ func Test_DatabaseCollectionDocDeleteIfMatch(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -286,6 +291,8 @@ func Test_DatabaseCollectionDocDeleteIgnoreRevs(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } diff --git a/v2/tests/database_collection_doc_read_test.go b/v2/tests/database_collection_doc_read_test.go index 1f9c2436..08103f6d 100644 --- a/v2/tests/database_collection_doc_read_test.go +++ b/v2/tests/database_collection_doc_read_test.go @@ -157,6 +157,8 @@ func Test_DatabaseCollectionDocReadIgnoreRevs(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(true), }) } diff --git a/v2/tests/database_collection_doc_update_test.go b/v2/tests/database_collection_doc_update_test.go index 94ad5aa0..32e1ae6c 100644 --- a/v2/tests/database_collection_doc_update_test.go +++ b/v2/tests/database_collection_doc_update_test.go @@ -73,6 +73,8 @@ func Test_DatabaseCollectionDocUpdateIfMatch(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -123,6 +125,8 @@ func Test_DatabaseCollectionDocUpdateIgnoreRevs(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -217,6 +221,8 @@ func Test_DatabaseCollectionDocUpdateKeepNull(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -294,6 +300,8 @@ func Test_DatabaseCollectionDocUpdateMergeObjects(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } diff --git a/v2/tests/database_collection_operations_test.go b/v2/tests/database_collection_operations_test.go index 0291e2f2..2fd29d8a 100644 --- a/v2/tests/database_collection_operations_test.go +++ b/v2/tests/database_collection_operations_test.go @@ -247,6 +247,8 @@ func Test_WithQueryOptimizerRules(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -270,6 +272,7 @@ func Test_DatabaseCollectionOperations(t *testing.T) { require.NoError(t, err) r, err := col.ReadDocuments(ctx, docsIds) + require.NoError(t, err) nd := docs @@ -473,9 +476,134 @@ func Test_DatabaseCollectionOperations(t *testing.T) { require.Len(t, nd, 0) }) + + t.Run("Replace", func(t *testing.T) { + // Create some documents to replace + replaceDocs := newDocs(5) + for i := 0; i < 5; i++ { + replaceDocs[i].Fields = GenerateUUID("replace-test") + } + + // Create the documents first + _, err := col.CreateDocuments(ctx, replaceDocs) + require.NoError(t, err) + + // Now replace them + for i := 0; i < 5; i++ { + replaceDocs[i].Fields = GenerateUUID("replaced-test") + } + + var oldDoc document + var newDoc document + + _, err = col.ReplaceDocumentsWithOptions(ctx, replaceDocs, &arangodb.CollectionDocumentReplaceOptions{ + OldObject: &oldDoc, + NewObject: &newDoc, + }) + require.NoError(t, err) + }) + }) + }) + }) + }, WrapOptions{ + Parallel: utils.NewType(false), + }) +} + +func Test_DatabaseCollectionBulkOperations(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollectionV2(t, db, nil, func(col arangodb.Collection) { + withContextT(t, defaultTestTimeout, func(ctx context.Context, tb testing.TB) { + size := 10 + docs := newDocs(size) + + for i := 0; i < size; i++ { + docs[i].Fields = GenerateUUID("test-doc-bulk") + } + + docsIds := docs.asBasic().getKeys() + + t.Run("Create_Bulk", func(t *testing.T) { + createReader, err := col.CreateDocuments(ctx, docs) + require.NoError(t, err) + + // Test bulk create operation + createResults, createErrs := createReader.ReadAll() + require.Equal(t, size, len(createResults)) + require.Equal(t, size, len(createErrs)) + for _, err := range createErrs { + require.NoError(t, err) + } + }) + + t.Run("Read_Bulk", func(t *testing.T) { + readReader, err := col.ReadDocuments(ctx, docsIds) + require.NoError(t, err) + + // Test bulk read operation + var readResults []document + readResponses, readErrs := readReader.ReadAll(&readResults) + require.Equal(t, size, len(readResponses)) + require.Equal(t, size, len(readErrs)) + require.Equal(t, size, len(readResults)) + for _, err := range readErrs { + require.NoError(t, err) + } + }) + + t.Run("Update_Bulk", func(t *testing.T) { + // Update the documents + for i := 0; i < size; i++ { + docs[i].Fields = GenerateUUID("updated-test-doc") + } + + var oldDoc document + var newDoc document + + updateReader, err := col.UpdateDocumentsWithOptions(ctx, docs, &arangodb.CollectionDocumentUpdateOptions{ + OldObject: &oldDoc, + NewObject: &newDoc, + }) + require.NoError(t, err) + + // Test bulk update operation + updateResults, updateErrs := updateReader.ReadAll() + require.Equal(t, size, len(updateResults)) + require.Equal(t, size, len(updateErrs)) + for _, err := range updateErrs { + require.NoError(t, err) + } + }) + + t.Run("Replace_Bulk", func(t *testing.T) { + // Replace the documents + for i := 0; i < size; i++ { + docs[i].Fields = GenerateUUID("replaced-test-doc") + } + + var oldDoc document + var newDoc document + + replaceReader, err := col.ReplaceDocumentsWithOptions(ctx, docs, &arangodb.CollectionDocumentReplaceOptions{ + OldObject: &oldDoc, + NewObject: &newDoc, + }) + require.NoError(t, err) + + // Test bulk replace operation + replaceResults, replaceErrs := replaceReader.ReadAll() + require.Equal(t, size, len(replaceResults)) + require.Equal(t, size, len(replaceErrs)) + for _, err := range replaceErrs { + require.NoError(t, err) + } + }) }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -506,6 +634,8 @@ func Test_DatabaseCollectionTruncate(t *testing.T) { }) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } diff --git a/v2/tests/database_query_test.go b/v2/tests/database_query_test.go index 42c89a32..575a39b5 100644 --- a/v2/tests/database_query_test.go +++ b/v2/tests/database_query_test.go @@ -812,6 +812,8 @@ func Test_GetQueryPlanCache(t *testing.T) { } }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -944,6 +946,8 @@ func Test_ClearQueryPlanCache(t *testing.T) { require.NoError(t, err) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -1111,6 +1115,8 @@ func Test_GetQueryEntriesCache(t *testing.T) { } }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) } @@ -1242,6 +1248,8 @@ func Test_ClearQueryCache(t *testing.T) { require.NoError(t, err) }) }) + }, WrapOptions{ + Parallel: utils.NewType(false), }) }