diff --git a/base/bucket_gocb_test.go b/base/bucket_gocb_test.go index de99aafd5a..c753e9148e 100644 --- a/base/bucket_gocb_test.go +++ b/base/bucket_gocb_test.go @@ -573,27 +573,13 @@ func TestMultiXattrRoundtrip(t *testing.T) { xattrKeys[2]: []byte(`{"key3": "value3"}`), } _, err := dataStore.WriteWithXattrs(ctx, docID, 0, 0, []byte(`{"key": "value"}`), inputXattrs, nil, nil) - if dataStore.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - require.NoError(t, err) - } else { - require.ErrorContains(t, err, "invalid xattr key combination") - inputXattrs = map[string][]byte{ - xattrKeys[0]: inputXattrs[xattrKeys[0]], - } - // write a document an xattrs, because subsequent GetXattrs needs a document to produce an error without multi-xattr support - _, err := dataStore.WriteWithXattrs(ctx, docID, 0, 0, []byte(`{"key": "value"}`), inputXattrs, nil, nil) - require.NoError(t, err) - } + require.NoError(t, err) xattrs, _, err := dataStore.GetXattrs(ctx, docID, xattrKeys) - if dataStore.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - require.NoError(t, err) - for _, key := range xattrKeys { - require.Contains(t, xattrs, key) - require.JSONEq(t, string(inputXattrs[key]), string(xattrs[key])) - } - } else { - require.ErrorContains(t, err, "not supported") + require.NoError(t, err) + for _, key := range xattrKeys { + require.Contains(t, xattrs, key) + require.JSONEq(t, string(inputXattrs[key]), string(xattrs[key])) } } @@ -2168,15 +2154,8 @@ func TestUserXattrGetWithXattr(t *testing.T) { "_sync": MustJSONMarshal(t, syncXattrVal), "test": MustJSONMarshal(t, userXattrVal), } - if bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - _, err = dataStore.UpdateXattrs(ctx, docKey, 0, cas, xattrs, nil) - require.NoError(t, err) - } else { - for k, v := range xattrs { - cas, err = dataStore.UpdateXattrs(ctx, docKey, 0, cas, map[string][]byte{k: v}, nil) - require.NoError(t, err) - } - } + _, err = dataStore.UpdateXattrs(ctx, docKey, 0, cas, xattrs, nil) + require.NoError(t, err) var docValRet map[string]any docRaw, xattrsResult, _, err := dataStore.GetWithXattrs(ctx, docKey, []string{SyncXattrName, "test"}) require.NoError(t, JSONUnmarshal(docRaw, &docValRet)) @@ -2549,10 +2528,6 @@ func TestWriteWithXattrsXattrWriteXattrDelete(t *testing.T) { xattrBody := []byte(`{"some": "val"}`) cas, err := collection.WriteWithXattrs(ctx, docID, 0, 0, []byte(`{"foo": "bar"}`), map[string][]byte{xattr1Name: xattrBody, xattr2Name: xattrBody}, nil, nil) - if !collection.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - require.ErrorContains(t, err, "SUBDOC_XATTR_INVALID_KEY_COMBO") - return - } require.NoError(t, err) newXattrBody := []byte(`{"another" : "val"}`) @@ -2589,9 +2564,6 @@ func TestWriteUpdateWithXattrsDocumentTombstone(t *testing.T) { ctx := TestCtx(t) bucket := GetTestBucket(t) defer bucket.Close(ctx) - if !bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - t.Skip("this test writes multiple xattrs") - } dataStore := bucket.GetSingleDataStore() key := t.Name() diff --git a/base/collection_xattr.go b/base/collection_xattr.go index d8daca2a58..4969f610ee 100644 --- a/base/collection_xattr.go +++ b/base/collection_xattr.go @@ -191,17 +191,6 @@ func (c *Collection) SubdocWrite(ctx context.Context, k string, subdocKey string // subdocGetBodyAndXattrs retrieves the document body and xattrs in a single LookupIn subdoc operation. Does not require both to exist. func (c *Collection) subdocGetBodyAndXattrs(ctx context.Context, k string, xattrKeys []string, fetchBody bool) (isTombstone bool, rawBody []byte, xattrs map[string][]byte, cas uint64, err error) { - xattrKey2 := "" - // Backward compatibility for one system xattr and one user xattr support. - if !c.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - if len(xattrKeys) > 2 { - return false, nil, nil, 0, fmt.Errorf("subdocGetBodyAndXattrs: more than 2 xattrKeys %+v not supported in this version of Couchbase Server", xattrKeys) - } - if len(xattrKeys) == 2 { - xattrKey2 = xattrKeys[1] - xattrKeys = []string{xattrKeys[0]} - } - } xattrs = make(map[string][]byte, len(xattrKeys)) worker := func() (shouldRetry bool, err error, value uint64) { @@ -283,29 +272,7 @@ func (c *Collection) subdocGetBodyAndXattrs(ctx context.Context, k string, xattr shouldRetry = c.isRecoverableReadError(lookupErr) return shouldRetry, lookupErr, uint64(0) } - // If BucketStoreFeatureMultiXattrSubdocOperations is not supported, do a second get for the second xattr. - if xattrKey2 != "" { - xattrs2, xattr2Cas, xattr2Err := c.GetXattrs(ctx, k, []string{xattrKey2}) - switch pkgerrors.Cause(xattr2Err) { - case gocb.ErrDocumentNotFound: - // If key not found it has been deleted in between the first op and this op. - return false, err, xattr2Cas - case ErrXattrNotFound: - // Xattr doesn't exist, can skip - case nil: - if cas != xattr2Cas { - return true, errors.New("cas mismatch between user xattr and document body"), uint64(0) - } - default: - // Unknown error occurred - // Shouldn't retry as any recoverable error will have been retried already in GetXattrs - return false, xattr2Err, uint64(0) - } - xattr2, ok := xattrs2[xattrKey2] - if ok { - xattrs[xattrKey2] = xattr2 - } - } + return false, nil, cas } @@ -427,9 +394,6 @@ func (c *Collection) UpdateXattrs(ctx context.Context, k string, exp uint32, cas } func (c *Collection) updateXattrs(ctx context.Context, k string, exp uint32, cas uint64, xattrs map[string][]byte, xattrsToDelete []string, opts *sgbucket.MutateInOptions) (casOut uint64, err error) { - if !c.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) && len(xattrs) >= 2 { - return 0, fmt.Errorf("UpdateXattrs: more than 1 xattr %v not supported in UpdateXattrs in this version of Couchbase Server", xattrs) - } if cas == 0 && len(xattrsToDelete) > 0 { return 0, sgbucket.ErrDeleteXattrOnDocumentInsert } diff --git a/base/collection_xattr_test.go b/base/collection_xattr_test.go index 7d193c585e..ba41b71c38 100644 --- a/base/collection_xattr_test.go +++ b/base/collection_xattr_test.go @@ -537,320 +537,316 @@ func TestWriteTombstoneWithXattrs(t *testing.T) { cas: incorrectCas, writeErrorFunc: RequireDocNotFoundError, }, - } - if bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - tests = append(tests, []testCase{ - // alive document with multiple xattrs - /* CBG-3918, should be a cas mismatch error - { - name: "previousDoc=body+_xattr1,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=true", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: zeroCas, - deleteBody: true, - writeErrorFunc: requireCasMismatchError, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), + // alive document with multiple xattrs + /* CBG-3918, should be a cas mismatch error + { + name: "previousDoc=body+_xattr1,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=true", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, }, - */ - { - name: "previousDoc=body+_xattr1,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=true", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: incorrectCas, - deleteBody: true, - writeErrorFunc: requireCasMismatchError, - }, - { - name: "previousDoc=body+_xattr1,xattrsToUpdate=_xattr1+_xattr2,cas=correct,deleteBody=true", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: previousCas, - deleteBody: true, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), }, - { - name: "previousDoc=body+_xattr1+_xattr2,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=false", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: zeroCas, - deleteBody: false, - writeErrorFunc: requireCasMismatchError, - }, - { - name: "previousDoc=body+_xattr1+_xattr2,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=false", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: incorrectCas, - deleteBody: false, - writeErrorFunc: requireCasMismatchError, - }, - { - name: "previousDoc=body+_xattr1+_xattr2,xattrsToUpdate=_xattr1+_xattr2,cas=correct,deleteBody=false", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: previousCas, - deleteBody: false, - finalBody: []byte(`{"foo": "bar"}`), - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, + cas: zeroCas, + deleteBody: true, + writeErrorFunc: requireCasMismatchError, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), }, - // alive document with no xattrs - /* CBG-3918, should be a cas mismatch error - { - name: "previousDoc=body,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=true", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: zeroCas, - deleteBody: true, - writeErrorFunc: requireCasMismatchError, + }, + */ + { + name: "previousDoc=body+_xattr1,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=true", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, }, - */ - { - name: "previousDoc=body,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=true", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: incorrectCas, - deleteBody: true, - writeErrorFunc: requireCasMismatchError, - }, - { - name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=correct,deleteBody=true", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: previousCas, - deleteBody: true, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), }, - { - name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=0,deleteBody=false", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: zeroCas, - deleteBody: false, - writeErrorFunc: requireCasMismatchError, - }, - { - name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=incorrect,deleteBody=false", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: incorrectCas, - deleteBody: false, - writeErrorFunc: requireCasMismatchError, - }, - { - name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=correct,deleteBody=false", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: previousCas, - deleteBody: false, - finalBody: []byte(`{"foo": "bar"}`), - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), + cas: incorrectCas, + deleteBody: true, + writeErrorFunc: requireCasMismatchError, + }, + { + name: "previousDoc=body+_xattr1,xattrsToUpdate=_xattr1+_xattr2,cas=correct,deleteBody=true", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, }, - // tombstone with multiple xattrs - { - name: "previousDoc=nil,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=true", - previousDoc: nil, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: zeroCas, - deleteBody: true, - writeErrorFunc: RequireDocNotFoundError, - }, - { - name: "previousDoc=nil,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=true", - previousDoc: nil, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: incorrectCas, - deleteBody: true, - writeErrorFunc: RequireDocNotFoundError, - }, - { - name: "previousDoc=nil,xattrsToUpdate=_xattr1,cas=0,deleteBody=false", - previousDoc: nil, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: zeroCas, - deleteBody: false, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), }, - { - name: "previousDoc=nil,xattrsToUpdate=_xattr1,cas=incorrect,deleteBody=false", - previousDoc: nil, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), - }, - cas: incorrectCas, - deleteBody: false, - writeErrorFunc: RequireDocNotFoundError, - }, - { - name: "previousDoc=nil,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=0,deleteBody=false", - previousDoc: nil, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - }, - xattrsToDelete: []string{"_xattr2"}, - cas: zeroCas, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - }, + cas: previousCas, + deleteBody: true, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), }, - { - name: "previousDoc=nil,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=0,deleteBody=true", - previousDoc: nil, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - }, - xattrsToDelete: []string{"_xattr2"}, - cas: zeroCas, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - }, - deleteBody: true, - writeErrorFunc: RequireDocNotFoundError, - }, - { - name: "previousDoc=body+_xattr1,_xattr2,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=correct,deleteBody=false", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), + }, + { + name: "previousDoc=body+_xattr1+_xattr2,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=false", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, - xattrsToDelete: []string{"_xattr2"}, - cas: previousCas, - deleteBody: false, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: zeroCas, + deleteBody: false, + writeErrorFunc: requireCasMismatchError, + }, + { + name: "previousDoc=body+_xattr1+_xattr2,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=false", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, - finalBody: []byte(`{"foo": "bar"}`), - }, - { - name: "previousDoc=body+_xattr1,_xattr2,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=correct,deleteBody=true", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: incorrectCas, + deleteBody: false, + writeErrorFunc: requireCasMismatchError, + }, + { + name: "previousDoc=body+_xattr1+_xattr2,xattrsToUpdate=_xattr1+_xattr2,cas=correct,deleteBody=false", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: previousCas, + deleteBody: false, + finalBody: []byte(`{"foo": "bar"}`), + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + }, + // alive document with no xattrs + /* CBG-3918, should be a cas mismatch error + { + name: "previousDoc=body,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=true", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: zeroCas, + deleteBody: true, + writeErrorFunc: requireCasMismatchError, + }, + }, + */ + { + name: "previousDoc=body,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=true", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: incorrectCas, + deleteBody: true, + writeErrorFunc: requireCasMismatchError, + }, + { + name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=correct,deleteBody=true", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: previousCas, + deleteBody: true, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + }, + { + name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=0,deleteBody=false", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: zeroCas, + deleteBody: false, + writeErrorFunc: requireCasMismatchError, + }, + { + name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=incorrect,deleteBody=false", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: incorrectCas, + deleteBody: false, + writeErrorFunc: requireCasMismatchError, + }, + { + name: "previousDoc=body,xattrsToUpdate=_xattr1,cas=correct,deleteBody=false", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: previousCas, + deleteBody: false, + finalBody: []byte(`{"foo": "bar"}`), + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + }, + // tombstone with multiple xattrs + { + name: "previousDoc=nil,xattrsToUpdate=_xattr1+_xattr2,cas=0,deleteBody=true", + previousDoc: nil, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: zeroCas, + deleteBody: true, + writeErrorFunc: RequireDocNotFoundError, + }, + { + name: "previousDoc=nil,xattrsToUpdate=_xattr1+_xattr2,cas=incorrect,deleteBody=true", + previousDoc: nil, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: incorrectCas, + deleteBody: true, + writeErrorFunc: RequireDocNotFoundError, + }, + { + name: "previousDoc=nil,xattrsToUpdate=_xattr1,cas=0,deleteBody=false", + previousDoc: nil, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: zeroCas, + deleteBody: false, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + }, + { + name: "previousDoc=nil,xattrsToUpdate=_xattr1,cas=incorrect,deleteBody=false", + previousDoc: nil, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + cas: incorrectCas, + deleteBody: false, + writeErrorFunc: RequireDocNotFoundError, + }, + { + name: "previousDoc=nil,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=0,deleteBody=false", + previousDoc: nil, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + xattrsToDelete: []string{"_xattr2"}, + cas: zeroCas, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + }, + { + name: "previousDoc=nil,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=0,deleteBody=true", + previousDoc: nil, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + xattrsToDelete: []string{"_xattr2"}, + cas: zeroCas, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + deleteBody: true, + writeErrorFunc: RequireDocNotFoundError, + }, + { + name: "previousDoc=body+_xattr1,_xattr2,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=correct,deleteBody=false", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, - xattrsToDelete: []string{"_xattr2"}, - cas: previousCas, - deleteBody: true, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), + }, + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + xattrsToDelete: []string{"_xattr2"}, + cas: previousCas, + deleteBody: false, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + finalBody: []byte(`{"foo": "bar"}`), + }, + { + name: "previousDoc=body+_xattr1,_xattr2,xattrsToUpdate=_xattr1,xattrsToDelete=_xattr2,cas=correct,deleteBody=true", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, }, - }...) + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + xattrsToDelete: []string{"_xattr2"}, + cas: previousCas, + deleteBody: true, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + }, + }, } for _, test := range tests { @@ -999,75 +995,70 @@ func TestWriteUpdateWithXattrs(t *testing.T) { }, finalXattrs: map[string][]byte{"_xattr1": []byte(`{"c" : "d"}`)}, }, - } - - if bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - tests = append(tests, []testCase{ - { - name: "previousDoc=body+_xattr1,_xattr2,updatedDoc=_xattr1", - previousDoc: &sgbucket.BucketDocument{ - Body: []byte(`{"foo": "bar"}`), - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - "_xattr2": []byte(`{"e" : "f"}`), - }, - }, - updatedDoc: sgbucket.UpdatedDoc{ - Xattrs: map[string][]byte{"_xattr1": []byte(`{"c" : "d"}`)}, - IsTombstone: true, - }, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c" : "d"}`), + { + name: "previousDoc=body+_xattr1,_xattr2,updatedDoc=_xattr1", + previousDoc: &sgbucket.BucketDocument{ + Body: []byte(`{"foo": "bar"}`), + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), "_xattr2": []byte(`{"e" : "f"}`), }, }, - { - name: "previousDoc=_xattr1,xattr2,updatedDoc=_xattr1", - previousDoc: &sgbucket.BucketDocument{ - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - "_xattr2": []byte(`{"e" : "f"}`), - }, - }, - updatedDoc: sgbucket.UpdatedDoc{ - Xattrs: map[string][]byte{"_xattr1": []byte(`{"c" : "d"}`)}, - IsTombstone: true, - }, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c" : "d"}`), + updatedDoc: sgbucket.UpdatedDoc{ + Xattrs: map[string][]byte{"_xattr1": []byte(`{"c" : "d"}`)}, + IsTombstone: true, + }, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c" : "d"}`), + "_xattr2": []byte(`{"e" : "f"}`), + }, + }, + { + name: "previousDoc=_xattr1,xattr2,updatedDoc=_xattr1", + previousDoc: &sgbucket.BucketDocument{ + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), "_xattr2": []byte(`{"e" : "f"}`), }, }, - { - name: "previousDoc=_xattr1,xattr2,updatedDoc=tombstone+_xattr1", - previousDoc: &sgbucket.BucketDocument{ - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - "_xattr2": []byte(`{"e" : "f"}`), - }, - }, - updatedDoc: sgbucket.UpdatedDoc{ - IsTombstone: true, - Xattrs: map[string][]byte{"_xattr1": []byte(`{"c" : "d"}`)}}, - finalXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c" : "d"}`), + updatedDoc: sgbucket.UpdatedDoc{ + Xattrs: map[string][]byte{"_xattr1": []byte(`{"c" : "d"}`)}, + IsTombstone: true, + }, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c" : "d"}`), + "_xattr2": []byte(`{"e" : "f"}`), + }, + }, + { + name: "previousDoc=_xattr1,xattr2,updatedDoc=tombstone+_xattr1", + previousDoc: &sgbucket.BucketDocument{ + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), "_xattr2": []byte(`{"e" : "f"}`), }, }, - { - name: "delete xattr on tombstone resurection", - previousDoc: &sgbucket.BucketDocument{ - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"foo": "bar"}`), - }, - }, - updatedDoc: sgbucket.UpdatedDoc{ - Doc: []byte(`{"foo": "bar"}`), - XattrsToDelete: []string{"xattr1"}, + updatedDoc: sgbucket.UpdatedDoc{ + IsTombstone: true, + Xattrs: map[string][]byte{"_xattr1": []byte(`{"c" : "d"}`)}}, + finalXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c" : "d"}`), + "_xattr2": []byte(`{"e" : "f"}`), + }, + }, + { + name: "delete xattr on tombstone resurection", + previousDoc: &sgbucket.BucketDocument{ + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"foo": "bar"}`), }, - errorsIs: sgbucket.ErrDeleteXattrOnTombstone, }, - }...) + updatedDoc: sgbucket.UpdatedDoc{ + Doc: []byte(`{"foo": "bar"}`), + XattrsToDelete: []string{"xattr1"}, + }, + errorsIs: sgbucket.ErrDeleteXattrOnTombstone, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -1207,39 +1198,35 @@ func TestWriteWithXattrs(t *testing.T) { xattrs: map[string][]byte{"xattr1": nil}, errorIs: sgbucket.ErrNilXattrValue, }, - } - if bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - tests = append(tests, []testCase{ - { - name: "body=true,xattrs=nil,xattrsToDelete=xattr1,xattr2,cas=0", - body: []byte(`{"foo": "bar"}`), - xattrsToDelete: []string{"xattr1", "xattr2"}, - errorIs: sgbucket.ErrDeleteXattrOnDocumentInsert, - }, - { - name: "body=true,xattrs=nil,xattrsToDelete=xattr1,xattr2,cas=incorrect", - body: []byte(`{"foo": "bar"}`), - xattrsToDelete: []string{"xattr1", "xattr2"}, - cas: uint64(1), - errorFunc: requireDocNotFoundOrCasMismatchError, - }, - { - name: "body=true,xattrs=xattr1,xattrsToDelete=xattr1,xattr2,cas=0", - body: []byte(`{"foo": "bar"}`), - xattrs: map[string][]byte{"xattr1": []byte(`{"a" : "b"}`)}, - xattrsToDelete: []string{"xattr1", "xattr2"}, - errorIs: sgbucket.ErrDeleteXattrOnDocumentInsert, - }, + { + name: "body=true,xattrs=nil,xattrsToDelete=xattr1,xattr2,cas=0", + body: []byte(`{"foo": "bar"}`), + xattrsToDelete: []string{"xattr1", "xattr2"}, + errorIs: sgbucket.ErrDeleteXattrOnDocumentInsert, + }, + { + name: "body=true,xattrs=nil,xattrsToDelete=xattr1,xattr2,cas=incorrect", + body: []byte(`{"foo": "bar"}`), + xattrsToDelete: []string{"xattr1", "xattr2"}, + cas: uint64(1), + errorFunc: requireDocNotFoundOrCasMismatchError, + }, + { + name: "body=true,xattrs=xattr1,xattrsToDelete=xattr1,xattr2,cas=0", + body: []byte(`{"foo": "bar"}`), + xattrs: map[string][]byte{"xattr1": []byte(`{"a" : "b"}`)}, + xattrsToDelete: []string{"xattr1", "xattr2"}, + errorIs: sgbucket.ErrDeleteXattrOnDocumentInsert, + }, - { - name: "body=true,xattrs=xattr1,xattrsToDelete=xattr1,xattr2,cas=incorrect", - body: []byte(`{"foo": "bar"}`), - xattrs: map[string][]byte{"xattr1": []byte(`{"a" : "b"}`)}, - xattrsToDelete: []string{"xattr1", "xattr2"}, - cas: uint64(1), - errorIs: sgbucket.ErrUpsertAndDeleteSameXattr, - }, - }...) + { + name: "body=true,xattrs=xattr1,xattrsToDelete=xattr1,xattr2,cas=incorrect", + body: []byte(`{"foo": "bar"}`), + xattrs: map[string][]byte{"xattr1": []byte(`{"a" : "b"}`)}, + xattrsToDelete: []string{"xattr1", "xattr2"}, + cas: uint64(1), + errorIs: sgbucket.ErrUpsertAndDeleteSameXattr, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -1413,23 +1400,19 @@ func TestWriteResurrectionWithXattrs(t *testing.T) { updatedBody: []byte(`{"foo": "bar"}`), errorFunc: requireDocFoundOrCasMismatchError, }, - } - if bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - tests = append(tests, []testCase{ - { - name: "previousDoc=_xattr1,xattrsToUpdate=_xattr1+_xattr2,updatedBody=body", - previousDoc: &sgbucket.BucketDocument{ - Xattrs: map[string][]byte{ - "_xattr1": []byte(`{"a" : "b"}`), - }, - }, - updatedXattrs: map[string][]byte{ - "_xattr1": []byte(`{"c": "d"}`), - "_xattr2": []byte(`{"f": "g"}`), + { + name: "previousDoc=_xattr1,xattrsToUpdate=_xattr1+_xattr2,updatedBody=body", + previousDoc: &sgbucket.BucketDocument{ + Xattrs: map[string][]byte{ + "_xattr1": []byte(`{"a" : "b"}`), }, - updatedBody: []byte(`{"foo": "bar"}`), }, - }...) + updatedXattrs: map[string][]byte{ + "_xattr1": []byte(`{"c": "d"}`), + "_xattr2": []byte(`{"f": "g"}`), + }, + updatedBody: []byte(`{"foo": "bar"}`), + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/db/background_mgr_resync_dcp_test.go b/db/background_mgr_resync_dcp_test.go index 0989d414c3..97335a4506 100644 --- a/db/background_mgr_resync_dcp_test.go +++ b/db/background_mgr_resync_dcp_test.go @@ -19,7 +19,6 @@ import ( "testing" "time" - sgbucket "github.com/couchbase/sg-bucket" "github.com/couchbase/sync_gateway/base" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -631,10 +630,6 @@ func TestResyncMou(t *testing.T) { db, ctx := setupTestDBWithOptionsAndImport(t, nil, DatabaseContextOptions{}) defer db.Close(ctx) - if !db.Bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - t.Skip("Test requires multi-xattr subdoc operations, CBS 7.6 or higher") - } - initialImportCount := db.DbStats.SharedBucketImport().ImportCount.Value() collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) docBody := Body{"foo": "bar"} diff --git a/db/crud.go b/db/crud.go index 9890cfebca..481784a4da 100644 --- a/db/crud.go +++ b/db/crud.go @@ -2773,7 +2773,7 @@ func (db *DatabaseCollectionWithUser) updateAndReturnDoc(ctx context.Context, do } updatedDoc.Xattrs = map[string][]byte{base.SyncXattrName: rawSyncXattr, base.VvXattrName: rawVvXattr} - if rawMouXattr != nil && db.useMou() { + if rawMouXattr != nil { updatedDoc.Xattrs[base.MouXattrName] = rawMouXattr } if rawGlobalSync != nil { diff --git a/db/database.go b/db/database.go index be32d2aff3..9aed9acdbf 100644 --- a/db/database.go +++ b/db/database.go @@ -155,7 +155,6 @@ type DatabaseContext struct { RequireResync base.ScopeAndCollectionNames // Collections requiring resync before database can go online RequireAttachmentMigration base.ScopeAndCollectionNames // Collections that require the attachment migration background task to run against CORS *auth.CORSConfig // CORS configuration - EnableMou bool // Write _mou xattr when performing metadata-only update. Set based on bucket capability on connect WasInitializedSynchronously bool // true if the database was initialized synchronously BroadcastSlowMode atomic.Bool // bool to indicate if a slower ticker value should be used to notify changes feeds of changes DatabaseStartupError *DatabaseError // Error that occurred during database online processes startup @@ -454,9 +453,6 @@ func NewDatabaseContext(ctx context.Context, dbName string, bucket base.Bucket, dbContext.cancelContextFunc() }) - // Check if server version supports multi-xattr operations, required for mou handling - dbContext.EnableMou = bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) - // Initialize metadata ID and keys metaKeys := base.NewMetadataKeys(options.MetadataID) dbContext.MetadataKeys = metaKeys @@ -1851,9 +1847,7 @@ func (db *DatabaseCollectionWithUser) resyncDocument(ctx context.Context, docid, doc.SetCrc32cUserXattrHash() // Update MetadataOnlyUpdate based on previous Cas, MetadataOnlyUpdate - if db.useMou() { - doc.MetadataOnlyUpdate = computeMetadataOnlyUpdate(doc.Cas, doc.RevSeqNo, doc.MetadataOnlyUpdate) - } + doc.MetadataOnlyUpdate = computeMetadataOnlyUpdate(doc.Cas, doc.RevSeqNo, doc.MetadataOnlyUpdate) _, rawSyncXattr, rawVvXattr, rawMouXattr, rawGlobalXattr, err := updatedDoc.MarshalWithXattrs() updatedDoc := sgbucket.UpdatedDoc{ @@ -1861,14 +1855,12 @@ func (db *DatabaseCollectionWithUser) resyncDocument(ctx context.Context, docid, Xattrs: map[string][]byte{ base.SyncXattrName: rawSyncXattr, base.VvXattrName: rawVvXattr, + base.MouXattrName: rawMouXattr, }, Expiry: updatedExpiry, } - if db.useMou() { - updatedDoc.Xattrs[base.MouXattrName] = rawMouXattr - if doc.MetadataOnlyUpdate.HexCAS == expandMacroCASValueString { - updatedDoc.Spec = append(updatedDoc.Spec, sgbucket.NewMacroExpansionSpec(XattrMouCasPath(), sgbucket.MacroCas)) - } + if doc.MetadataOnlyUpdate.HexCAS == expandMacroCASValueString { + updatedDoc.Spec = append(updatedDoc.Spec, sgbucket.NewMacroExpansionSpec(XattrMouCasPath(), sgbucket.MacroCas)) } if rawGlobalXattr != nil { updatedDoc.Xattrs[base.GlobalXattrName] = rawGlobalXattr @@ -2014,10 +2006,6 @@ func (context *DatabaseContext) UseViews() bool { return context.Options.UseViews } -func (context *DatabaseContext) UseMou() bool { - return context.EnableMou -} - func (context *DatabaseContext) DeltaSyncEnabled() bool { return context.Options.DeltaSyncOptions.Enabled } diff --git a/db/database_collection.go b/db/database_collection.go index daa77de851..a7109c4ec4 100644 --- a/db/database_collection.go +++ b/db/database_collection.go @@ -325,10 +325,7 @@ func (c *DatabaseCollection) syncGlobalSyncAndUserXattrKeys() []string { // syncGlobalSyncMouRevSeqNoAndUserXattrKeys returns the xattr keys for the user, mou, revSeqNo and sync xattrs. func (c *DatabaseCollection) syncGlobalSyncMouRevSeqNoAndUserXattrKeys() []string { - xattrKeys := []string{base.SyncXattrName, base.VvXattrName} - if c.useMou() { - xattrKeys = append(xattrKeys, base.MouXattrName, base.GlobalXattrName) - } + xattrKeys := []string{base.SyncXattrName, base.VvXattrName, base.MouXattrName, base.GlobalXattrName} userXattrKey := c.userXattrKey() if userXattrKey != "" { xattrKeys = append(xattrKeys, userXattrKey) @@ -433,10 +430,6 @@ func (c *DatabaseCollection) invalidateAllPrincipals(ctx context.Context, endSeq c.dbCtx.invalidateAllPrincipals(ctx, base.ScopeAndCollectionNames{c.ScopeAndCollectionName()}, endSeq) } -func (c *DatabaseCollection) useMou() bool { - return c.dbCtx.UseMou() -} - func (c *DatabaseCollection) ScopeAndCollectionName() base.ScopeAndCollectionName { return base.NewScopeAndCollectionName(c.ScopeName, c.Name) } diff --git a/db/import.go b/db/import.go index a1501c0d47..b7eddbce3d 100644 --- a/db/import.go +++ b/db/import.go @@ -93,7 +93,7 @@ func (db *DatabaseCollectionWithUser) ImportDoc(ctx context.Context, docid strin } else { if existingDoc.Deleted { existingBucketDoc.Xattrs[base.SyncXattrName], err = base.JSONMarshal(existingDoc.SyncData) - if err == nil && existingDoc.MetadataOnlyUpdate != nil && db.useMou() { + if err == nil && existingDoc.MetadataOnlyUpdate != nil { existingBucketDoc.Xattrs[base.MouXattrName], err = base.JSONMarshal(existingDoc.MetadataOnlyUpdate) } if existingDoc.HLV != nil { @@ -337,7 +337,7 @@ func (db *DatabaseCollectionWithUser) importDoc(ctx context.Context, docid strin } // If this is a metadata-only update, set metadataOnlyUpdate based on old doc's cas and mou - if metadataOnlyUpdate && db.useMou() { + if metadataOnlyUpdate { newDoc.MetadataOnlyUpdate = computeMetadataOnlyUpdate(doc.Cas, revNo, doc.MetadataOnlyUpdate) } diff --git a/db/import_test.go b/db/import_test.go index f1e63f27f0..c486a71ccb 100644 --- a/db/import_test.go +++ b/db/import_test.go @@ -74,20 +74,15 @@ func TestFeedImport(t *testing.T) { // verify mou and rev seqno xattrs, _, err = collection.dataStore.GetXattrs(ctx, key, []string{base.MouXattrName, base.VirtualXattrRevSeqNo, base.VvXattrName}) - if db.UseMou() { - var mou *MetadataOnlyUpdate - require.NoError(t, err) - mouXattr, mouOk := xattrs[base.MouXattrName] - require.True(t, mouOk) - require.NoError(t, base.JSONUnmarshal(mouXattr, &mou)) - require.Equal(t, base.CasToString(writeCas), mou.PreviousHexCAS) - require.Equal(t, base.CasToString(importCas), mou.HexCAS) - // curr revSeqNo should be 2, so prev revSeqNo is 1 - require.Equal(t, revSeqNo-1, mou.PreviousRevSeqNo) - } else { - // Expect not found fetching mou xattr - require.Error(t, err) - } + var mou *MetadataOnlyUpdate + require.NoError(t, err) + mouXattr, mouOk := xattrs[base.MouXattrName] + require.True(t, mouOk) + require.NoError(t, base.JSONUnmarshal(mouXattr, &mou)) + require.Equal(t, base.CasToString(writeCas), mou.PreviousHexCAS) + require.Equal(t, base.CasToString(importCas), mou.HexCAS) + // curr revSeqNo should be 2, so prev revSeqNo is 1 + require.Equal(t, revSeqNo-1, mou.PreviousRevSeqNo) require.Contains(t, xattrs, base.VvXattrName) var hlv HybridLogicalVector require.NoError(t, base.JSONUnmarshal(xattrs[base.VvXattrName], &hlv)) @@ -167,14 +162,10 @@ func TestOnDemandImport(t *testing.T) { doc, err := collection.GetDocument(ctx, getKey, DocUnmarshalAll) require.NoError(t, err) - if db.UseMou() { - require.NotNil(t, doc.MetadataOnlyUpdate) - require.Equal(t, base.CasToString(writeCas), doc.MetadataOnlyUpdate.PreviousHexCAS) - require.Equal(t, base.CasToString(doc.Cas), doc.MetadataOnlyUpdate.HexCAS) - require.Equal(t, startingRevSeqNo, doc.MetadataOnlyUpdate.PreviousRevSeqNo) - } else { - require.Nil(t, doc.MetadataOnlyUpdate) - } + require.NotNil(t, doc.MetadataOnlyUpdate) + require.Equal(t, base.CasToString(writeCas), doc.MetadataOnlyUpdate.PreviousHexCAS) + require.Equal(t, base.CasToString(doc.Cas), doc.MetadataOnlyUpdate.HexCAS) + require.Equal(t, startingRevSeqNo, doc.MetadataOnlyUpdate.PreviousRevSeqNo) require.Equal(t, db.EncodedSourceID, doc.HLV.SourceID) }) @@ -233,19 +224,14 @@ func TestOnDemandImport(t *testing.T) { // fetch the mou xattr directly doc to confirm import (to avoid triggering on-demand get import) // verify mou xattrs, importCas, err := collection.dataStore.GetXattrs(ctx, writeKey, []string{base.MouXattrName, base.VvXattrName}) - if db.UseMou() { - require.NoError(t, err) - mouXattr, mouOk := xattrs[base.MouXattrName] - var mou *MetadataOnlyUpdate - require.True(t, mouOk) - require.NoError(t, base.JSONUnmarshal(mouXattr, &mou)) - require.Equal(t, base.CasToString(writeCas), mou.PreviousHexCAS) - require.Equal(t, base.CasToString(importCas), mou.HexCAS) - require.Equal(t, startingRevSeqNo, mou.PreviousRevSeqNo) - } else { - // expect not found fetching mou xattr - require.Error(t, err) - } + require.NoError(t, err) + mouXattr, mouOk := xattrs[base.MouXattrName] + var mou *MetadataOnlyUpdate + require.True(t, mouOk) + require.NoError(t, base.JSONUnmarshal(mouXattr, &mou)) + require.Equal(t, base.CasToString(writeCas), mou.PreviousHexCAS) + require.Equal(t, base.CasToString(importCas), mou.HexCAS) + require.Equal(t, startingRevSeqNo, mou.PreviousRevSeqNo) var hlv HybridLogicalVector require.Contains(t, xattrs, base.VvXattrName) require.NoError(t, base.JSONUnmarshal(xattrs[base.VvXattrName], &hlv)) @@ -1189,10 +1175,6 @@ func TestMetadataOnlyUpdate(t *testing.T) { db, ctx := setupTestDBWithOptionsAndImport(t, nil, DatabaseContextOptions{}) defer db.Close(ctx) - if !db.Bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - t.Skip("Test requires multi-xattr subdoc operations, CBS 7.6 or higher") - } - collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) bodyBytes := []byte(`{"foo":"bar"}`) @@ -1271,11 +1253,7 @@ func TestImportResurrectionMou(t *testing.T) { return db.DbStats.SharedBucketImport().ImportCount.Value() }, 1) syncData, mou, _ = getSyncAndMou(t, collection, docID) - if db.Bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - require.NotNil(t, mou) - } else { - require.Nil(t, mou) - } + require.NotNil(t, mou) require.NotNil(t, syncData) // Delete via SDK, the mou will be updated by the import process diff --git a/db/util_testing.go b/db/util_testing.go index 3926ee25e6..f9f700a118 100644 --- a/db/util_testing.go +++ b/db/util_testing.go @@ -237,19 +237,6 @@ func purgeWithDCPFeed(ctx context.Context, bucket base.Bucket, tbp *base.TestBuc if !ok { purgeErrors = purgeErrors.Append(fmt.Errorf("Could not find collection ID %d for %#+v doc over DCP", event.CollectionID, event)) } - // If Couchbase Server < 7.6, we need to delete xattrs one at a time, since it doesn't support subdoc multi-xattr operations. - if len(xattrs) >= 1 && !bucket.IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - for _, xattr := range xattrs { - err := dataStore.DeleteSubDocPaths(ctx, docID, xattr) - if err != nil { - purgeErrors = purgeErrors.Append(fmt.Errorf("error purging xattr %s from docID %s: %w", xattr, docID, err)) - tbp.Logf(ctx, "%s", err) - return false - } - } - xattrs = nil // reset xattrs to nil so we don't try to delete the doc with xattrs - - } purgeErr := dataStore.DeleteWithXattrs(ctx, docID, xattrs) if base.IsDocNotFoundError(purgeErr) { // doc is a tombstone diff --git a/rest/importtest/import_test.go b/rest/importtest/import_test.go index b3fed3063a..9f6f073136 100644 --- a/rest/importtest/import_test.go +++ b/rest/importtest/import_test.go @@ -21,7 +21,6 @@ import ( "time" "github.com/couchbase/clog" - sgbucket "github.com/couchbase/sg-bucket" "github.com/couchbase/sync_gateway/base" "github.com/couchbase/sync_gateway/channels" "github.com/couchbase/sync_gateway/db" @@ -2586,10 +2585,6 @@ func TestPrevRevNoPopulationImportFeed(t *testing.T) { dataStore := rt.GetSingleDataStore() ctx := base.TestCtx(t) - if !rt.Bucket().IsSupported(sgbucket.BucketStoreFeatureMultiXattrSubdocOperations) { - t.Skip("Test requires multi-xattr subdoc operations, CBS 7.6 or higher") - } - // Create doc via the SDK mobileKey := t.Name() mobileBody := make(map[string]any)