diff --git a/db/crud.go b/db/crud.go index 4c70ff6efc..61d98bf346 100644 --- a/db/crud.go +++ b/db/crud.go @@ -3626,10 +3626,7 @@ func (db *DatabaseCollectionWithUser) CheckProposedRev(ctx context.Context, doci return ProposedRev_OK, "" // Users can't upload design docs, so ignore them } - level := DocUnmarshalRev - if parentRevID == "" { - level = DocUnmarshalHistory // doc.History only needed in this case (see below) - } + level := DocUnmarshalRevAndFlags syncData, _, err := db.GetDocSyncDataNoImport(ctx, docid, level) if err != nil { if !base.IsDocNotFoundError(err) && !base.IsXattrNotFoundError(err) { @@ -3644,7 +3641,7 @@ func (db *DatabaseCollectionWithUser) CheckProposedRev(ctx context.Context, doci } else if syncData.GetRevTreeID() == parentRevID { // Proposed rev's parent is my current revision; OK to add: return ProposedRev_OK, "" - } else if parentRevID == "" && syncData.History[syncData.GetRevTreeID()].Deleted { + } else if parentRevID == "" && syncData.IsDeleted() { // Proposed rev has no parent and doc is currently deleted; OK to add: return ProposedRev_OK, "" } else { diff --git a/db/crud_test.go b/db/crud_test.go index a470549aa6..23236a4b36 100644 --- a/db/crud_test.go +++ b/db/crud_test.go @@ -1810,7 +1810,7 @@ func TestPutExistingCurrentVersion(t *testing.T) { // Update the client's HLV to include the latest SGW version. incomingHLV.PreviousVersions[currentSourceID] = docUpdateVersion - // TODO: because currentRev isn't being updated, storeOldBodyInRevTreeAndUpdateCurrent isn't + // TODO: because expectedCurrentRev isn't being updated, storeOldBodyInRevTreeAndUpdateCurrent isn't // updating the document body. Need to review whether it makes sense to keep using // storeOldBodyInRevTreeAndUpdateCurrent, or if this needs a larger overhaul to support VV doc, cv, _, err = collection.PutExistingCurrentVersion(ctx, opts) @@ -2282,3 +2282,174 @@ func TestSyncDataCVEqual(t *testing.T) { }) } } + +func TestProposedRev(t *testing.T) { + db, ctx := setupTestDB(t) + defer db.Close(ctx) + collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) + + // create 3 documents + const ( + SingleRevDoc = "SingleRevDoc" + MultiRevDoc = "MultiRevDoc" + TombstonedDoc = "TombstonedDoc" + ) + body := Body{"key1": "value1", "key2": 1234} + _, doc1, err := collection.Put(ctx, SingleRevDoc, body) + require.NoError(t, err) + doc1Rev := doc1.GetRevTreeID() + + _, doc2, err := collection.Put(ctx, MultiRevDoc, body) + require.NoError(t, err) + doc2Rev1 := doc2.GetRevTreeID() + _, doc2, err = collection.Put(ctx, MultiRevDoc, Body{"_rev": doc2Rev1, "key1": "value2", "key2": 5678}) + require.NoError(t, err) + doc2Rev2 := doc2.GetRevTreeID() + _, doc2, err = collection.Put(ctx, MultiRevDoc, Body{"_rev": doc2Rev2, "key1": "value3", "key2": 91011}) + require.NoError(t, err) + doc2Rev3 := doc2.GetRevTreeID() + + _, doc3, err := collection.Put(ctx, TombstonedDoc, body) + require.NoError(t, err) + doc3Rev1 := doc3.GetRevTreeID() + _, _, err = collection.Put(ctx, TombstonedDoc, Body{"_rev": doc3Rev1, "_deleted": true}) + require.NoError(t, err) + + testCases := []struct { + name string + revID string + parentRevID string + expectedStatus ProposedRevStatus + expectedCurrentRev string + docID string + }{ + { + name: "no_existing_document-curr_rev-no_parent", + revID: "1-abc", + parentRevID: "", + expectedStatus: ProposedRev_OK_IsNew, + expectedCurrentRev: "", + docID: "doc", + }, + { + name: "no_existing_document-curr_rev-with_parent", + revID: "2-def", + parentRevID: "1-abc", + expectedStatus: ProposedRev_OK_IsNew, + expectedCurrentRev: "", + docID: "doc", + }, + { + name: "one_rev_doc-curr_rev-without_parent", + revID: doc1Rev, + parentRevID: "", + expectedStatus: ProposedRev_Exists, + expectedCurrentRev: "", + docID: SingleRevDoc, + }, + { + name: "one_rev_doc-incorrect_curr_rev-without_parent", + revID: "1-abc", + parentRevID: "", + expectedStatus: ProposedRev_Conflict, + expectedCurrentRev: doc1Rev, + docID: SingleRevDoc, + }, + { + name: "one_rev_doc-new_curr_rev-without_parent", + revID: "2-abc", + parentRevID: doc1Rev, + expectedStatus: ProposedRev_OK, + expectedCurrentRev: "", + docID: SingleRevDoc, + }, + { + name: "multi_rev_doc-rev1-without_parent", + revID: doc2Rev1, + parentRevID: "", + expectedStatus: ProposedRev_Conflict, + expectedCurrentRev: doc2Rev3, + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-rev2-with_rev1_parent", + revID: doc2Rev2, + parentRevID: doc2Rev1, + expectedStatus: ProposedRev_Conflict, + expectedCurrentRev: doc2Rev3, + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-rev2-with_incorrect_parent", + revID: doc2Rev2, + parentRevID: "1-abc", + expectedStatus: ProposedRev_Conflict, + expectedCurrentRev: doc2Rev3, + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-rev2-without_parent", + revID: doc2Rev2, + parentRevID: "", + expectedStatus: ProposedRev_Conflict, + expectedCurrentRev: doc2Rev3, + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-conflicting_rev2-with_parent", + revID: "2-abc", + parentRevID: doc2Rev1, + expectedStatus: ProposedRev_Conflict, + expectedCurrentRev: doc2Rev3, + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-conflicting_rev3-without_parent", + revID: doc2Rev3, + parentRevID: "", + expectedStatus: ProposedRev_Exists, + expectedCurrentRev: "", + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-conflicting_rev3-with_parent", + revID: doc2Rev3, + parentRevID: doc2Rev2, + expectedStatus: ProposedRev_Exists, + expectedCurrentRev: "", + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-conflicting_rev3-with_incorrect_parent", + revID: doc2Rev3, + parentRevID: doc2Rev1, + expectedStatus: ProposedRev_Exists, + expectedCurrentRev: "", + docID: MultiRevDoc, + }, + { + name: "multi_rev_doc-conflicting_incorrect_rev3-with_parent", + revID: "3-abc", + parentRevID: doc2Rev2, + expectedStatus: ProposedRev_Conflict, + expectedCurrentRev: doc2Rev3, + docID: MultiRevDoc, + }, + { + name: "new revision with previous revision as tombstone", + revID: "1-abc", + parentRevID: "", + expectedStatus: ProposedRev_OK, + expectedCurrentRev: "", + docID: TombstonedDoc, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + status, rev := collection.CheckProposedRev(ctx, tc.docID, tc.revID, tc.parentRevID) + assert.Equal(t, tc.expectedStatus, status) + assert.Equal(t, tc.expectedCurrentRev, rev) + }) + } +} diff --git a/db/document.go b/db/document.go index c1df1165cb..72610a3208 100644 --- a/db/document.go +++ b/db/document.go @@ -37,13 +37,14 @@ const DocumentHistoryMaxEntriesPerChannel = 5 type DocumentUnmarshalLevel uint8 const ( - DocUnmarshalAll = DocumentUnmarshalLevel(iota) // Unmarshals metadata and body - DocUnmarshalSync // Unmarshals metadata - DocUnmarshalNoHistory // Unmarshals metadata excluding revtree history - DocUnmarshalHistory // Unmarshals revtree history + rev + CAS only - DocUnmarshalRev // Unmarshals revTreeID + CAS only (no HLV) - DocUnmarshalCAS // Unmarshals CAS (for import check) only - DocUnmarshalNone // No unmarshalling (skips import/upgrade check) + DocUnmarshalAll = DocumentUnmarshalLevel(iota) // Unmarshals metadata and body + DocUnmarshalSync // Unmarshals metadata + DocUnmarshalNoHistory // Unmarshals metadata excluding revtree history + DocUnmarshalHistory // Unmarshals revtree history + rev + CAS only + DocUnmarshalRev // Unmarshals revTreeID + CAS only (no HLV) + DocUnmarshalCAS // Unmarshals CAS (for import check) only + DocUnmarshalNone // No unmarshalling (skips import/upgrade check) + DocUnmarshalRevAndFlags // Unmarshals revTreeID + CAS and Flags (no HLV) ) const ( @@ -167,6 +168,16 @@ func (sd *SyncData) SetCV(hlv *HybridLogicalVector) { sd.RevAndVersion.CurrentVersion = string(base.Uint64CASToLittleEndianHex(hlv.Version)) } +// hasFlag returns true if the document has this bit flag +func (sd *SyncData) hasFlag(flag uint8) bool { + return sd.Flags&flag != 0 +} + +// IsDeleted returns true if the document metadata indicates it is a tombstone. +func (sd *SyncData) IsDeleted() bool { + return sd.hasFlag(channels.Deleted) +} + // RedactRawGlobalSyncData runs HashRedact on the given global sync data. func RedactRawGlobalSyncData(syncData []byte, redactSalt string) ([]byte, error) { if redactSalt == "" { @@ -306,6 +317,11 @@ type revOnlySyncData struct { CurrentRev channels.RevAndVersion `json:"rev"` } +type revAndFlagsSyncData struct { + revOnlySyncData + Flags uint8 `json:"flags"` +} + type casOnlySyncData struct { Cas string `json:"cas"` } @@ -331,10 +347,6 @@ func (doc *Document) MarshalBodyAndSync() (retBytes []byte, err error) { } } -func (doc *Document) IsDeleted() bool { - return doc.hasFlag(channels.Deleted) -} - func (doc *Document) BodyWithSpecialProperties(ctx context.Context) ([]byte, error) { bodyBytes, err := doc.BodyBytes(ctx) if err != nil { @@ -754,10 +766,6 @@ func (doc *Document) IsSGWrite(ctx context.Context, rawBody []byte) (isSGWrite b return true, true, false } -func (doc *Document) hasFlag(flag uint8) bool { - return doc.Flags&flag != 0 -} - func (doc *Document) setFlag(flag uint8, state bool) { if state { doc.Flags |= flag @@ -1336,6 +1344,23 @@ func (doc *Document) UnmarshalWithXattrs(ctx context.Context, data, syncXattrDat doc.SyncData = SyncData{} } doc._rawBody = data + case DocUnmarshalRevAndFlags: + // Unmarshal rev, cas and flags from sync metadata + if syncXattrData != nil { + var revOnlyMeta revAndFlagsSyncData + unmarshalErr := base.JSONUnmarshal(syncXattrData, &revOnlyMeta) + if unmarshalErr != nil { + return pkgerrors.WithStack(base.RedactErrorf("Failed to UnmarshalWithXattrs() doc with id: %s (DocUnmarshalRevAndFlags). Error: %v", base.UD(doc.ID), unmarshalErr)) + } + doc.SyncData = SyncData{ + RevAndVersion: revOnlyMeta.CurrentRev, + Cas: revOnlyMeta.Cas, + Flags: revOnlyMeta.Flags, + } + } else { + doc.SyncData = SyncData{} + } + doc._rawBody = data } // If there's no body, but there is an xattr, set deleted flag and initialize an empty body