From cb9af4adf61de4ebe98ba637450f5bf09e49cf28 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:54:25 +0000 Subject: [PATCH 1/4] [Feature] [V2] Code fields collision --- v2/arangodb/collection_documents_read_impl.go | 39 +++++-- v2/arangodb/cursor_impl.go | 19 ++-- v2/arangodb/unmarshaller.go | 58 ++++++++++ ...atabase_collection_doc_create_code_test.go | 104 ++++++++++++++++++ v2/tests/utils_client_test.go | 7 +- 5 files changed, 204 insertions(+), 23 deletions(-) create mode 100644 v2/arangodb/unmarshaller.go create mode 100644 v2/tests/database_collection_doc_create_code_test.go diff --git a/v2/arangodb/collection_documents_read_impl.go b/v2/arangodb/collection_documents_read_impl.go index fa5f2188..6a30bfb8 100644 --- a/v2/arangodb/collection_documents_read_impl.go +++ b/v2/arangodb/collection_documents_read_impl.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020-2024 ArangoDB GmbH, Cologne, Germany +// 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. @@ -74,23 +74,24 @@ func (c collectionDocumentRead) ReadDocument(ctx context.Context, key string, re func (c collectionDocumentRead) ReadDocumentWithOptions(ctx context.Context, key string, result interface{}, opts *CollectionDocumentReadOptions) (DocumentMeta, error) { url := c.collection.url("document", key) - var response struct { - shared.ResponseStruct `json:",inline"` - DocumentMeta `json:",inline"` - } - - data := newUnmarshalInto(result) + var response Unmarshal[shared.ResponseStruct, Unmarshal[DocumentMeta, UnmarshalData]] - resp, err := connection.CallGet(ctx, c.collection.connection(), url, newMultiUnmarshaller(&response, data), c.collection.withModifiers(opts.modifyRequest)...) + resp, err := connection.CallGet(ctx, c.collection.connection(), url, &response, c.collection.withModifiers(opts.modifyRequest)...) if err != nil { return DocumentMeta{}, err } switch code := resp.Code(); code { case http.StatusOK: - return response.DocumentMeta, nil + if err := response.Object.Object.Inject(result); err != nil { + return DocumentMeta{}, err + } + if z := response.Object.Current; z != nil { + return *z, nil + } + return DocumentMeta{}, nil default: - return DocumentMeta{}, response.AsArangoErrorWithCode(code) + return DocumentMeta{}, response.Current.AsArangoErrorWithCode(code) } } @@ -112,18 +113,32 @@ func (c *collectionDocumentReadResponseReader) Read(i interface{}) (CollectionDo return CollectionDocumentReadResponse{}, shared.NoMoreDocumentsError{} } - var meta CollectionDocumentReadResponse + var response Unmarshal[shared.ResponseStruct, Unmarshal[DocumentMeta, UnmarshalData]] - if err := c.array.Unmarshal(newMultiUnmarshaller(&meta, newUnmarshalInto(i))); err != nil { + if err := c.array.Unmarshal(&response); err != nil { if err == io.EOF { return CollectionDocumentReadResponse{}, shared.NoMoreDocumentsError{} } return CollectionDocumentReadResponse{}, err } + var meta CollectionDocumentReadResponse + + if q := response.Current; q != nil { + meta.ResponseStruct = *q + } + + if q := response.Object.Current; q != nil { + meta.DocumentMeta = *q + } + if meta.Error != nil && *meta.Error { return meta, meta.AsArangoError() } + if err := response.Object.Object.Inject(i); err != nil { + return CollectionDocumentReadResponse{}, err + } + return meta, nil } diff --git a/v2/arangodb/cursor_impl.go b/v2/arangodb/cursor_impl.go index 656d8d40..c57cf7dd 100644 --- a/v2/arangodb/cursor_impl.go +++ b/v2/arangodb/cursor_impl.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020-2023 ArangoDB GmbH, Cologne, Germany +// 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. @@ -144,22 +144,21 @@ func (c *cursor) readDocument(ctx context.Context, result interface{}) (Document } } - var data byteDecoder - if err := c.data.Result.Read(&data); err != nil { + var res Unmarshal[DocumentMeta, UnmarshalData] + + if err := c.data.Result.Read(&res); err != nil { return DocumentMeta{}, err } - var meta DocumentMeta - - if err := data.Unmarshal(&meta); err != nil { - // Ignore error + if err := res.Object.Inject(result); err != nil { + return DocumentMeta{}, err } - if err := data.Unmarshal(result); err != nil { - return DocumentMeta{}, err + if m := res.Current; m != nil { + return *m, nil } - return meta, nil + return DocumentMeta{}, nil } func (c *cursor) getNextBatch(ctx context.Context, retryBatchID string) error { diff --git a/v2/arangodb/unmarshaller.go b/v2/arangodb/unmarshaller.go new file mode 100644 index 00000000..e70dbece --- /dev/null +++ b/v2/arangodb/unmarshaller.go @@ -0,0 +1,58 @@ +// +// DISCLAIMER +// +// Copyright 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 arangodb + +import "encoding/json" + +var _ json.Unmarshaler = &Unmarshal[string, int]{} + +type Unmarshal[C, T any] struct { + Current *C + + Object T +} + +func (u *Unmarshal[C, T]) UnmarshalJSON(bytes []byte) error { + u.Current = nil + + var q C + + if err := json.Unmarshal(bytes, &q); err == nil { + u.Current = &q + } + + return json.Unmarshal(bytes, &u.Object) +} + +type UnmarshalData []byte + +func (u UnmarshalData) Inject(object any) error { + return json.Unmarshal(u, object) +} + +func (u *UnmarshalData) UnmarshalJSON(bytes []byte) error { + z := make([]byte, len(bytes)) + + copy(z, bytes) + + *u = z + return nil +} diff --git a/v2/tests/database_collection_doc_create_code_test.go b/v2/tests/database_collection_doc_create_code_test.go new file mode 100644 index 00000000..e024f636 --- /dev/null +++ b/v2/tests/database_collection_doc_create_code_test.go @@ -0,0 +1,104 @@ +// +// DISCLAIMER +// +// Copyright 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 tests + +import ( + "context" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/arangodb/go-driver/v2/arangodb" +) + +type DocWithCode struct { + Key string `json:"_key,omitempty"` + Code string `json:"code"` +} + +func Test_DatabaseCollectionDocCreateCode(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContextT(t, defaultTestTimeout, func(ctx context.Context, tb testing.TB) { + doc := DocWithCode{ + Key: "test", + } + + meta, err := col.CreateDocument(ctx, doc) + require.NoError(t, err) + require.NotEmpty(t, meta.Rev) + require.Empty(t, meta.Old) + require.Empty(t, meta.New) + + rdoc, err := col.ReadDocument(ctx, "test", &doc) + require.NoError(t, err) + + require.EqualValues(t, "test", rdoc.Key) + }) + }) + }) + + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContextT(t, defaultTestTimeout, func(ctx context.Context, tb testing.TB) { + doc := DocWithCode{ + Key: "test", + } + doc2 := DocWithCode{ + Key: "test2", + } + + _, err := col.CreateDocuments(ctx, []any{ + doc, doc2, + }) + require.NoError(t, err) + + docs, err := col.ReadDocuments(ctx, []string{ + "test", + "tz44", + "test2", + }) + require.NoError(t, err) + + var z DocWithCode + + meta, err := docs.Read(&z) + require.NoError(t, err) + require.EqualValues(t, "test", meta.Key) + + _, err = docs.Read(&z) + require.Error(t, err) + require.True(t, shared.IsNotFound(err)) + + meta, err = docs.Read(&z) + require.NoError(t, err) + require.EqualValues(t, "test2", meta.Key) + + _, err = docs.Read(&z) + require.Error(t, err) + require.True(t, shared.IsNoMoreDocuments(err)) + }) + }) + }) + }) +} diff --git a/v2/tests/utils_client_test.go b/v2/tests/utils_client_test.go index 04bb8473..48cc0e84 100644 --- a/v2/tests/utils_client_test.go +++ b/v2/tests/utils_client_test.go @@ -59,7 +59,12 @@ func createAuthenticationFromEnv(t testing.TB, conn connection.Connection) conne // getEndpointsFromEnv returns the endpoints specified in the TEST_ENDPOINTS environment variable. func getEndpointsFromEnv(t testing.TB) []string { - eps := strings.Split(os.Getenv("TEST_ENDPOINTS"), ",") + v, ok := os.LookupEnv("TEST_ENDPOINTS") + if !ok { + t.Fatal("No endpoints found in environment variable TEST_ENDPOINTS") + } + + eps := strings.Split(v, ",") if len(eps) == 0 { t.Fatal("No endpoints found in environment variable TEST_ENDPOINTS") } From 813d06bba9cf22c52d1e3f9bbe109414bf8a6ad7 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:59:37 +0000 Subject: [PATCH 2/4] Fix linter --- v2/tests/database_collection_doc_create_code_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/v2/tests/database_collection_doc_create_code_test.go b/v2/tests/database_collection_doc_create_code_test.go index e024f636..fb36f7ba 100644 --- a/v2/tests/database_collection_doc_create_code_test.go +++ b/v2/tests/database_collection_doc_create_code_test.go @@ -22,9 +22,10 @@ package tests import ( "context" - "github.com/arangodb/go-driver/v2/arangodb/shared" "testing" + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/stretchr/testify/require" "github.com/arangodb/go-driver/v2/arangodb" From 08885ee5577e5a388504ac49db996bd2e00393a8 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:20:19 +0000 Subject: [PATCH 3/4] Move all fields --- .../collection_documents_delete_impl.go | 16 ++++- v2/arangodb/collection_indexes_impl.go | 9 +-- v2/arangodb/database_transaction_impl.go | 10 +++- v2/arangodb/utils.go | 59 ------------------- 4 files changed, 25 insertions(+), 69 deletions(-) diff --git a/v2/arangodb/collection_documents_delete_impl.go b/v2/arangodb/collection_documents_delete_impl.go index 25275088..769389d2 100644 --- a/v2/arangodb/collection_documents_delete_impl.go +++ b/v2/arangodb/collection_documents_delete_impl.go @@ -124,16 +124,30 @@ func (c *collectionDocumentDeleteResponseReader) Read(i interface{}) (Collection meta.Old = c.options.OldObject } - if err := c.array.Unmarshal(newMultiUnmarshaller(&meta, newUnmarshalInto(i))); err != nil { + var response Unmarshal[shared.ResponseStruct, Unmarshal[DocumentMeta, UnmarshalData]] + + if err := c.array.Unmarshal(&response); err != nil { if err == io.EOF { return CollectionDocumentDeleteResponse{}, shared.NoMoreDocumentsError{} } return CollectionDocumentDeleteResponse{}, err } + if q := response.Current; q != nil { + meta.ResponseStruct = *q + } + + if q := response.Object.Current; q != nil { + meta.DocumentMeta = *q + } + if meta.Error != nil && *meta.Error { return meta, meta.AsArangoError() } + if err := response.Object.Object.Inject(i); err != nil { + return CollectionDocumentDeleteResponse{}, err + } + return meta, nil } diff --git a/v2/arangodb/collection_indexes_impl.go b/v2/arangodb/collection_indexes_impl.go index 922bff55..16139909 100644 --- a/v2/arangodb/collection_indexes_impl.go +++ b/v2/arangodb/collection_indexes_impl.go @@ -228,12 +228,9 @@ func (c *collectionIndexes) EnsureInvertedIndex(ctx context.Context, options *In func (c *collectionIndexes) ensureIndex(ctx context.Context, reqData interface{}, result interface{}) (bool, error) { urlEndpoint := c.collection.db.url("_api", "index") - var response struct { - shared.ResponseStruct `json:",inline"` - } - data := newUnmarshalInto(&result) + var response Unmarshal[shared.ResponseStruct, UnmarshalData] - resp, err := connection.CallPost(ctx, c.collection.connection(), urlEndpoint, newMultiUnmarshaller(&response, data), &reqData, + resp, err := connection.CallPost(ctx, c.collection.connection(), urlEndpoint, &response, &reqData, c.collection.withModifiers(connection.WithQuery("collection", c.collection.name))...) if err != nil { return false, errors.WithStack(err) @@ -245,7 +242,7 @@ func (c *collectionIndexes) ensureIndex(ctx context.Context, reqData interface{} case http.StatusCreated: return true, nil default: - return false, response.AsArangoErrorWithCode(code) + return false, response.Current.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/database_transaction_impl.go b/v2/arangodb/database_transaction_impl.go index c32fe788..f4404736 100644 --- a/v2/arangodb/database_transaction_impl.go +++ b/v2/arangodb/database_transaction_impl.go @@ -60,13 +60,17 @@ func (d databaseTransaction) listTransactionsWithStatuses(ctx context.Context, s } `json:"transactions,omitempty"` } - var response shared.ResponseStruct + var response Unmarshal[shared.ResponseStruct, UnmarshalData] - resp, err := connection.CallGet(ctx, d.db.connection(), url, newMultiUnmarshaller(&result, &response), d.db.modifiers...) + resp, err := connection.CallGet(ctx, d.db.connection(), url, &response, d.db.modifiers...) if err != nil { return nil, errors.WithStack(err) } + if err := response.Object.Inject(result); err != nil { + return nil, err + } + switch code := resp.Code(); code { case http.StatusOK: var t []Transaction @@ -80,7 +84,7 @@ func (d databaseTransaction) listTransactionsWithStatuses(ctx context.Context, s return t, nil default: - return nil, response.AsArangoErrorWithCode(code) + return nil, response.Current.AsArangoErrorWithCode(code) } } diff --git a/v2/arangodb/utils.go b/v2/arangodb/utils.go index c062a7d0..a3a72a5d 100644 --- a/v2/arangodb/utils.go +++ b/v2/arangodb/utils.go @@ -30,65 +30,6 @@ import ( "github.com/pkg/errors" ) -var _ json.Unmarshaler = &multiUnmarshaller{} -var _ json.Marshaler = &multiUnmarshaller{} - -func newMultiUnmarshaller(obj ...interface{}) json.Unmarshaler { - return &multiUnmarshaller{ - obj: obj, - } -} - -type multiUnmarshaller struct { - obj []interface{} -} - -func (m multiUnmarshaller) MarshalJSON() ([]byte, error) { - r := map[string]interface{}{} - for _, o := range m.obj { - z := map[string]interface{}{} - if d, err := json.Marshal(o); err != nil { - return nil, err - } else { - if err := json.Unmarshal(d, &z); err != nil { - return nil, err - } - } - - for k, v := range z { - r[k] = v - } - } - - return json.Marshal(r) -} - -func (m multiUnmarshaller) UnmarshalJSON(d []byte) error { - for _, o := range m.obj { - if err := json.Unmarshal(d, o); err != nil { - return err - } - } - - return nil -} - -type byteDecoder struct { - data []byte -} - -func (b *byteDecoder) UnmarshalJSON(d []byte) error { - b.data = make([]byte, len(d)) - - copy(b.data, d) - - return nil -} - -func (b *byteDecoder) Unmarshal(i interface{}) error { - return json.Unmarshal(b.data, i) -} - func newUnmarshalInto(obj interface{}) *UnmarshalInto { return &UnmarshalInto{obj} } From acc74189de0dc35c144c7a8c795e549bf37026fa Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:18:03 +0000 Subject: [PATCH 4/4] Add field support --- .../collection_documents_delete_impl.go | 11 ++-- v2/arangodb/collection_indexes_impl.go | 6 ++ v2/arangodb/database_transaction_impl.go | 2 +- v2/arangodb/unmarshaller.go | 59 +++++++++++++++++-- ...atabase_collection_doc_create_code_test.go | 2 +- 5 files changed, 70 insertions(+), 10 deletions(-) diff --git a/v2/arangodb/collection_documents_delete_impl.go b/v2/arangodb/collection_documents_delete_impl.go index 769389d2..9a12837e 100644 --- a/v2/arangodb/collection_documents_delete_impl.go +++ b/v2/arangodb/collection_documents_delete_impl.go @@ -120,10 +120,6 @@ func (c *collectionDocumentDeleteResponseReader) Read(i interface{}) (Collection var meta CollectionDocumentDeleteResponse - if c.options != nil { - meta.Old = c.options.OldObject - } - var response Unmarshal[shared.ResponseStruct, Unmarshal[DocumentMeta, UnmarshalData]] if err := c.array.Unmarshal(&response); err != nil { @@ -149,5 +145,12 @@ func (c *collectionDocumentDeleteResponseReader) Read(i interface{}) (Collection return CollectionDocumentDeleteResponse{}, err } + 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 + } + } + return meta, nil } diff --git a/v2/arangodb/collection_indexes_impl.go b/v2/arangodb/collection_indexes_impl.go index 16139909..7ec72804 100644 --- a/v2/arangodb/collection_indexes_impl.go +++ b/v2/arangodb/collection_indexes_impl.go @@ -238,8 +238,14 @@ func (c *collectionIndexes) ensureIndex(ctx context.Context, reqData interface{} switch code := resp.Code(); code { case http.StatusOK: + if err := response.Object.Inject(result); err != nil { + return false, err + } return false, nil case http.StatusCreated: + if err := response.Object.Inject(result); err != nil { + return false, err + } return true, nil default: return false, response.Current.AsArangoErrorWithCode(code) diff --git a/v2/arangodb/database_transaction_impl.go b/v2/arangodb/database_transaction_impl.go index f4404736..7aa9fc5c 100644 --- a/v2/arangodb/database_transaction_impl.go +++ b/v2/arangodb/database_transaction_impl.go @@ -67,7 +67,7 @@ func (d databaseTransaction) listTransactionsWithStatuses(ctx context.Context, s return nil, errors.WithStack(err) } - if err := response.Object.Inject(result); err != nil { + if err := response.Object.Inject(&result); err != nil { return nil, err } diff --git a/v2/arangodb/unmarshaller.go b/v2/arangodb/unmarshaller.go index e70dbece..30118a38 100644 --- a/v2/arangodb/unmarshaller.go +++ b/v2/arangodb/unmarshaller.go @@ -20,9 +20,18 @@ package arangodb -import "encoding/json" +import ( + "encoding/json" -var _ json.Unmarshaler = &Unmarshal[string, int]{} + "github.com/pkg/errors" +) + +type Unmarshaler interface { + json.Unmarshaler + + Inject(object any) error + Extract(key string) Unmarshaler +} type Unmarshal[C, T any] struct { Current *C @@ -44,11 +53,37 @@ func (u *Unmarshal[C, T]) UnmarshalJSON(bytes []byte) error { type UnmarshalData []byte -func (u UnmarshalData) Inject(object any) error { - return json.Unmarshal(u, object) +func (u *UnmarshalData) Inject(object any) error { + if u == nil { + return errors.Errorf("Data provided is nil") + } + + return json.Unmarshal(*u, object) +} + +func (u *UnmarshalData) Extract(key string) Unmarshaler { + if u == nil { + return errorUnmarshalData{err: errors.Errorf("Data provided is nil")} + } + + var z map[string]UnmarshalData + + if err := json.Unmarshal(*u, &z); err != nil { + return errorUnmarshalData{err: err} + } + + if v, ok := z[key]; ok { + return &v + } + + return errorUnmarshalData{err: errors.Errorf("Key %s not found", key)} } func (u *UnmarshalData) UnmarshalJSON(bytes []byte) error { + if u == nil { + return errors.Errorf("Data provided is nil") + } + z := make([]byte, len(bytes)) copy(z, bytes) @@ -56,3 +91,19 @@ func (u *UnmarshalData) UnmarshalJSON(bytes []byte) error { *u = z return nil } + +type errorUnmarshalData struct { + err error +} + +func (e errorUnmarshalData) UnmarshalJSON(bytes []byte) error { + return e.err +} + +func (e errorUnmarshalData) Inject(object any) error { + return e.err +} + +func (e errorUnmarshalData) Extract(key string) Unmarshaler { + return e +} diff --git a/v2/tests/database_collection_doc_create_code_test.go b/v2/tests/database_collection_doc_create_code_test.go index fb36f7ba..27095945 100644 --- a/v2/tests/database_collection_doc_create_code_test.go +++ b/v2/tests/database_collection_doc_create_code_test.go @@ -45,7 +45,7 @@ func Test_DatabaseCollectionDocCreateCode(t *testing.T) { Key: "test", } - meta, err := col.CreateDocument(ctx, doc) + meta, err := col.CreateDocumentWithOptions(ctx, doc, &arangodb.CollectionDocumentCreateOptions{}) require.NoError(t, err) require.NotEmpty(t, meta.Rev) require.Empty(t, meta.Old)