diff --git a/LiteCore/Database/VectorDocument.cc b/LiteCore/Database/VectorDocument.cc index c31be5c4e..18ad57231 100644 --- a/LiteCore/Database/VectorDocument.cc +++ b/LiteCore/Database/VectorDocument.cc @@ -620,6 +620,31 @@ namespace litecore { }); } + // Conflict may be caused by our VV not containing any version from the same author as requested version. + // If that is the case, we should set the status to kRevsLocalIsOlder, and find the correct latest version + // so that the remote can handle giving us the correct rev. + if ( status & kRevsConflict && recUsesVVs && requestedVec.count() == 1 + && !localVec.contains(requestedVec.current().author()) ) { + status = kRevsLocalIsOlder; + + alloc_slice latest; + auto& store = asInternal(collection())->keyStore(); + VectorRecord vectorRecord{store, rec.key}; + + // If this document contains a version from any remote which has the same author as the requested vec, + // return the latest. Otherwise, return our current version. + if ( auto ver = vectorRecord.findLatestWithAuthor(requestedVec.current().author()); ver ) { + latest = ver->asASCII(mySourceID); + } else { + latest = localVec.current().asASCII(mySourceID); + } + + char statusChar = static_cast('0' + char(status)); + result.str(""); + result << statusChar << '[' << '"' << latest << '"' << ']'; + return alloc_slice(result.str()); + } + char statusChar = static_cast('0' + char(status)); if ( cmp == kNewer || cmp == kSame ) { // If I already have this revision, just return the status byte: diff --git a/LiteCore/RevTrees/VectorRecord.cc b/LiteCore/RevTrees/VectorRecord.cc index 06a021543..a3e5a019f 100644 --- a/LiteCore/RevTrees/VectorRecord.cc +++ b/LiteCore/RevTrees/VectorRecord.cc @@ -734,4 +734,21 @@ namespace litecore { } } + optional VectorRecord::findLatestWithAuthor(SourceID author) { + RemoteID remote = RemoteID::Local; + + optional latest = nullopt; + + while ( auto rev = loadRemoteRevision(remote) ) { + VersionVector vv = rev->versionVector(); + for ( auto ver : vv.versions() ) { + if ( ver.author() == author && (!latest || ver.time() > latest->time()) ) { latest = ver; } + } + remote = loadNextRemoteID(remote); + } + + return latest; + } + + } // namespace litecore diff --git a/LiteCore/RevTrees/VectorRecord.hh b/LiteCore/RevTrees/VectorRecord.hh index 9dd7f6c52..a155a1463 100644 --- a/LiteCore/RevTrees/VectorRecord.hh +++ b/LiteCore/RevTrees/VectorRecord.hh @@ -15,6 +15,8 @@ #include "RevID.hh" #include "fleece/Fleece.hh" #include "fleece/Mutable.hh" + +#include "SourceID.hh" #include #include @@ -221,6 +223,8 @@ namespace litecore { /// Given only a record, find all the revision IDs and pass them to the callback. static void forAllRevIDs(const RecordUpdate&, const ForAllRevIDsCallback&); + std::optional findLatestWithAuthor(SourceID author); + //---- For testing: /// Generates a rev-tree revision ID given document properties, parent revision ID, and flags. diff --git a/LiteCore/tests/c4DocumentTest_Internal.cc b/LiteCore/tests/c4DocumentTest_Internal.cc index ca3600d00..1fa416778 100644 --- a/LiteCore/tests/c4DocumentTest_Internal.cc +++ b/LiteCore/tests/c4DocumentTest_Internal.cc @@ -127,7 +127,9 @@ N_WAY_TEST_CASE_METHOD(C4Test, "Document FindDocAncestors", "[Document][C]") { // Single version: CHECK(findDocAncestor(doc1, "10@BobBobBobBobBobBobBobA"_sl) == "2"); CHECK(findDocAncestor(doc1, "11@BobBobBobBobBobBobBobA"_sl) == "3[]"); - CHECK(findDocAncestor(doc1, "1@DaveDaveDaveDaveDaveDA"_sl) == "3[]"); + + // Single version with a unknown author returns the latest rev and kRevOlder + CHECK(findDocAncestor(doc1, "1@DaveDaveDaveDaveDaveDA"_sl) == R"(1["3@AliceAliceAliceAliceAA"])"); // Limit number of results: C4Slice newRevID = "11@BobBobBobBobBobBobBobA; 3@AliceAliceAliceAliceAA"_sl; @@ -219,6 +221,7 @@ N_WAY_TEST_CASE_METHOD(C4Test, "Random RevID", "[Document][C]") { kNotEncryptable = R"({"foo":1234,"nested":[0,1,{"SSN":{"type":"encryptable","value":"123-45-6789"}},3,4]})"; slice json; + SECTION("verify sha1 digest") { json = kNotEncryptable; } SECTION("verify encryptable") { json = kEncryptable; } if ( !json ) { return; } diff --git a/Replicator/tests/ReplicatorCollectionSGTest.cc b/Replicator/tests/ReplicatorCollectionSGTest.cc index 2e55d749d..7121326c3 100644 --- a/Replicator/tests/ReplicatorCollectionSGTest.cc +++ b/Replicator/tests/ReplicatorCollectionSGTest.cc @@ -1948,8 +1948,6 @@ TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Pull iTunes deltas from Collection timeWithoutDelta / timeWithDelta); } -// Disabled, to be re-enabled with CBL-5621 -#if 0 // cbl-4499 TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Pull invalid deltas with filter from SG", "[.SyncServerCollection][Delta]") { @@ -1958,14 +1956,13 @@ TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Pull invalid deltas with filter fr initTest({Tulips}, {channelID}, "test_user"); static constexpr int kNumDocs = 10, kNumProps = 100; - static constexpr int kDocBufSize = 80; - static constexpr const char* cblTicket = "cbl-4499"; + static constexpr const char* cblTicket = "cbl-4499"; const string docPrefix = idPrefix + cblTicket + "_"; vector docIDs(kNumDocs); - for (int docNo = 0; docNo < kNumDocs; ++docNo) { + for ( int docNo = 0; docNo < kNumDocs; ++docNo ) { docIDs[docNo] = stringprintf("%sdoc-%03d", docPrefix.c_str(), docNo); } @@ -1973,7 +1970,7 @@ TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Pull invalid deltas with filter fr auto populateDB = [&]() { TransactionHelper t(db); std::srand(123456); // start random() sequence at a known place - for (const string& docID : docIDs) { + for ( const string& docID : docIDs ) { Encoder enc(c4db_createFleeceEncoder(db)); enc.beginDict(); for ( int p = 0; p < kNumProps; ++p ) { @@ -1997,7 +1994,7 @@ TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Pull invalid deltas with filter fr replicate(replParams); // -------- Updating docs on SG -------- - for (const string& docID : docIDs) { + for ( const string& docID : docIDs ) { C4Error error; c4::ref doc = c4coll_getDoc(_collections[0], slice(docID), true, kDocGetAll, ERROR_INFO(error)); REQUIRE(doc); @@ -2024,9 +2021,9 @@ TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Pull invalid deltas with filter fr FLDict flbody, void* context) { return true; }; // -------- Pulling changes from SG -------- -# ifdef LITECORE_CPPTEST +#ifdef LITECORE_CPPTEST _expectedDocPullErrors = {docPrefix + "doc-001"}; -# endif +#endif replParams.setPushPull(kC4Disabled, kC4OneShot); replParams.setPullFilter(_pullFilter).setCallbackContext(this); { @@ -2051,7 +2048,6 @@ TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Pull invalid deltas with filter fr } CHECK(n == kNumDocs); } -#endif // cbl-4499 TEST_CASE_METHOD(ReplicatorCollectionSGTest, "Push invalid deltas to SG", "[.SyncServerCollection][Delta]") { diff --git a/Replicator/tests/ReplicatorCollectionTest.cc b/Replicator/tests/ReplicatorCollectionTest.cc index 67d700ddf..2b5793a28 100644 --- a/Replicator/tests/ReplicatorCollectionTest.cc +++ b/Replicator/tests/ReplicatorCollectionTest.cc @@ -567,7 +567,7 @@ N_WAY_TEST_CASE_METHOD(ReplicatorCollectionTest, "Multiple Collections Increment addRevs(tulips, 500ms, alloc_slice("tulips-docko"), 1, 3, true, "db-tulips"); addRevs(roses2, 500ms, alloc_slice("roses2-docko"), 1, 3, true, "db2-roses"); addRevs(tulips2, 500ms, alloc_slice("tulips2-docko"), 1, 3, true, "db2-tulips"); - sleepFor(5s); + sleepFor(10s); stopWhenIdle(); }}); _callbackWhenIdle = nullptr;