public void TestForceInsertEmptyHistory() { var rev = new RevisionInternal("FakeDocId", "1-abcd", false); var revProperties = new Dictionary<string, object>(); revProperties.Put("_id", rev.GetDocId()); revProperties.Put("_rev", rev.GetRevId()); revProperties["message"] = "hi"; rev.SetProperties(revProperties); IList<string> revHistory = null; database.ForceInsert(rev, revHistory, null); }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestForceInsertEmptyHistory() { IList<string> revHistory = null; RevisionInternal rev = new RevisionInternal("FakeDocId", "1-tango", false, database ); IDictionary<string, object> revProperties = new Dictionary<string, object>(); revProperties.Put("_id", rev.GetDocId()); revProperties.Put("_rev", rev.GetRevId()); revProperties.Put("message", "hi"); rev.SetProperties(revProperties); database.ForceInsert(rev, revHistory, null); }
internal RevisionInternal Winner(Int64 docNumericID, String oldWinningRevID, Boolean oldWinnerWasDeletion, RevisionInternal newRev) { if (oldWinningRevID == null) { return newRev; } var newRevID = newRev.GetRevId(); if (!newRev.IsDeleted()) { if (oldWinnerWasDeletion || RevisionInternal.CBLCompareRevIDs(newRevID, oldWinningRevID) > 0) { return newRev; } } else { // this is now the winning live revision if (oldWinnerWasDeletion) { if (RevisionInternal.CBLCompareRevIDs(newRevID, oldWinningRevID) > 0) { return newRev; } } else { // doc still deleted, but this beats previous deletion rev // Doc was alive. How does this deletion affect the winning rev ID? var outIsDeleted = new AList<bool>(); var outIsConflict = new AList<bool>(); var winningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict); if (!winningRevID.Equals(oldWinningRevID)) { if (winningRevID.Equals(newRev.GetRevId())) { return newRev; } else { var deleted = false; var winningRev = new RevisionInternal(newRev.GetDocId(), winningRevID, deleted, this); return winningRev; } } } } return null; }
public long GetRevisionSequence(RevisionInternal rev) { var retVal = 0L; WithC4Document(rev.GetDocId(), rev.GetRevId(), false, false, doc => retVal = (long)doc->selectedRev.sequence); return retVal; }
/// <summary>Stores a new (or initial) revision of a document.</summary> /// <remarks> /// Stores a new (or initial) revision of a document. /// This is what's invoked by a PUT or POST. As with those, the previous revision ID must be supplied when necessary and the call will fail if it doesn't match. /// </remarks> /// <param name="oldRev">The revision to add. If the docID is null, a new UUID will be assigned. Its revID must be null. It must have a JSON body. /// </param> /// <param name="prevRevId">The ID of the revision to replace (same as the "?rev=" parameter to a PUT), or null if this is a new document. /// </param> /// <param name="allowConflict">If false, an error status 409 will be returned if the insertion would create a conflict, i.e. if the previous revision already has a child. /// </param> /// <param name="resultStatus">On return, an HTTP status code indicating success or failure. /// </param> /// <returns>A new RevisionInternal with the docID, revID and sequence filled in (but no body). /// </returns> /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal RevisionInternal PutRevision(RevisionInternal oldRev, String prevRevId, Boolean allowConflict, Status resultStatus) { // prevRevId is the rev ID being replaced, or nil if an insert var docId = oldRev.GetDocId(); var deleted = oldRev.IsDeleted(); if ((oldRev == null) || ((prevRevId != null) && (docId == null)) || (deleted && (docId == null)) || ((docId != null) && !IsValidDocumentId(docId))) { throw new CouchbaseLiteException(StatusCode.BadRequest); } BeginTransaction(); Cursor cursor = null; var inConflict = false; RevisionInternal winningRev = null; RevisionInternal newRev = null; // PART I: In which are performed lookups and validations prior to the insert... var docNumericID = (docId != null) ? GetDocNumericID(docId) : 0; var parentSequence = 0L; string oldWinningRevID = null; try { var oldWinnerWasDeletion = false; var wasConflicted = false; if (docNumericID > 0) { var outIsDeleted = new AList<bool>(); var outIsConflict = new AList<bool>(); try { oldWinningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict); oldWinnerWasDeletion |= outIsDeleted.Count > 0; wasConflicted |= outIsConflict.Count > 0; } catch (Exception e) { Sharpen.Runtime.PrintStackTrace(e); } } if (prevRevId != null) { // Replacing: make sure given prevRevID is current & find its sequence number: if (docNumericID <= 0) { var msg = string.Format("No existing revision found with doc id: {0}", docId); throw new CouchbaseLiteException(msg, StatusCode.NotFound); } parentSequence = GetSequenceOfDocument(docNumericID, prevRevId, !allowConflict); if (parentSequence == 0) { // Not found: either a 404 or a 409, depending on whether there is any current revision if (!allowConflict && ExistsDocumentWithIDAndRev(docId, null)) { var msg = string.Format("Conflicts not allowed and there is already an existing doc with id: {0}", docId); throw new CouchbaseLiteException(msg, StatusCode.Conflict); } else { var msg = string.Format("No existing revision found with doc id: {0}", docId); throw new CouchbaseLiteException(msg, StatusCode.NotFound); } } if (_validations != null && _validations.Count > 0) { // Fetch the previous revision and validate the new one against it: var oldRevCopy = oldRev.CopyWithDocID(oldRev.GetDocId(), null); var prevRev = new RevisionInternal(docId, prevRevId, false, this); ValidateRevision(oldRevCopy, prevRev, prevRevId); } } else { // Inserting first revision. if (deleted && (docId != null)) { // Didn't specify a revision to delete: 404 or a 409, depending if (ExistsDocumentWithIDAndRev(docId, null)) { throw new CouchbaseLiteException(StatusCode.Conflict); } else { throw new CouchbaseLiteException(StatusCode.NotFound); } } // Validate: ValidateRevision(oldRev, null, null); if (docId != null) { // Inserting first revision, with docID given (PUT): if (docNumericID <= 0) { // Doc doesn't exist at all; create it: docNumericID = InsertDocumentID(docId); if (docNumericID <= 0) { return null; } } else { // Doc ID exists; check whether current winning revision is deleted: if (oldWinnerWasDeletion) { prevRevId = oldWinningRevID; parentSequence = GetSequenceOfDocument(docNumericID, prevRevId, false); } else { if (oldWinningRevID != null) { // The current winning revision is not deleted, so this is a conflict throw new CouchbaseLiteException(StatusCode.Conflict); } } } } else { // Inserting first revision, with no docID given (POST): generate a unique docID: docId = Database.GenerateDocumentId(); docNumericID = InsertDocumentID(docId); if (docNumericID <= 0) { return null; } } } // There may be a conflict if (a) the document was already in conflict, or // (b) a conflict is created by adding a non-deletion child of a non-winning rev. inConflict = wasConflicted || (!deleted && prevRevId != null && oldWinningRevID != null && !prevRevId.Equals(oldWinningRevID)); // PART II: In which we prepare for insertion... // Get the attachments: var attachments = GetAttachmentsFromRevision(oldRev); // Bump the revID and update the JSON: IList<byte> json = null; if(!oldRev.IsDeleted()) //oldRev.GetProperties() != null && oldRev.GetProperties().Any()) { json = EncodeDocumentJSON(oldRev).ToList(); if (json == null) { // bad or missing json throw new CouchbaseLiteException(StatusCode.BadRequest); } if (json.Count() == 2 && json[0] == '{' && json[1] == '}') { json = null; } } else { json = Encoding.UTF8.GetBytes("{}"); // NOTE.ZJG: Confirm w/ Traun. This prevents a null reference exception in call to InsertRevision below. } var newRevId = GenerateIDForRevision(oldRev, json, attachments, prevRevId); newRev = oldRev.CopyWithDocID(docId, newRevId); StubOutAttachmentsInRevision(attachments, newRev); // Now insert the rev itself: var newSequence = InsertRevision(newRev, docNumericID, parentSequence, true, (attachments.Count > 0), json); if (newSequence == 0) { return null; } // Make replaced rev non-current: try { var args = new ContentValues(); args["current"] = 0; StorageEngine.Update("revs", args, "sequence=?", new[] { parentSequence.ToString() }); } catch (SQLException e) { Log.E(Database.Tag, "Error setting parent rev non-current", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } // Store any attachments: if (attachments != null) { ProcessAttachmentsForRevision(attachments, newRev, parentSequence); } // Figure out what the new winning rev ID is: winningRev = Winner(docNumericID, oldWinningRevID, oldWinnerWasDeletion, newRev); // Success! if (deleted) { resultStatus.SetCode(StatusCode.Ok); } else { resultStatus.SetCode(StatusCode.Created); } } catch (SQLException e1) { Log.E(Tag, "Error putting revision", e1); return null; } finally { if (cursor != null) { cursor.Close(); } EndTransaction(resultStatus.IsSuccessful); if (!string.IsNullOrEmpty(docId)) { UnsavedRevisionDocumentCache.Remove(docId); } } // EPILOGUE: A change notification is sent... NotifyChange(newRev, winningRev, null, inConflict); return newRev; }
//Doesn't handle CouchbaseLiteException internal RevisionInternal LoadRevisionBody(RevisionInternal rev) { if (rev.GetSequence() > 0) { var props = rev.GetProperties(); if (props != null && props.GetCast<string>("_rev") != null && props.GetCast<string>("_id") != null) { return rev; } } Debug.Assert(rev.GetDocId() != null && rev.GetRevId() != null); Storage.LoadRevisionBody(rev); return rev; }
internal RevisionInternal TransformRevision(RevisionInternal rev) { if (RevisionBodyTransformationFunction != null) { try { var generation = rev.GetGeneration(); var xformed = RevisionBodyTransformationFunction(rev); if (xformed == null) { return null; } if (xformed != rev) { Debug.Assert((xformed.GetDocId().Equals(rev.GetDocId()))); Debug.Assert((xformed.GetRevId().Equals(rev.GetRevId()))); Debug.Assert((xformed.GetProperties().Get("_revisions").Equals(rev.GetProperties().Get("_revisions")))); if (xformed.GetProperties().ContainsKey("_attachments")) { // Insert 'revpos' properties into any attachments added by the callback: var mx = new RevisionInternal(xformed.GetProperties()); xformed = mx; mx.MutateAttachments((name, info) => { if (info.Get("revpos") != null) { return info; } if (info.Get("data") == null) { throw new InvalidOperationException("Transformer added attachment without adding data"); } var newInfo = new Dictionary<string, object>(info); newInfo["revpos"] = generation; return newInfo; }); } } } catch (Exception e) { Log.W(TAG, String.Format("Exception transforming a revision of doc '{0}'", rev.GetDocId()), e); } } return rev; }
public string FindCommonAncestor(RevisionInternal rev, IEnumerable<string> revIds) { var generation = RevisionInternal.GenerationFromRevID(rev.GetRevId()); var revIdArray = revIds == null ? null : revIds.ToList(); if (generation <= 1 || revIdArray == null || revIdArray.Count == 0) { return null; } revIdArray.Sort(RevisionInternal.CBLCompareRevIDs); var commonAncestor = default(string); WithC4Document(rev.GetDocId(), null, false, false, doc => { foreach(var possibleRevId in revIds) { if(RevisionInternal.GenerationFromRevID(possibleRevId) <= generation && Native.c4doc_selectRevision(doc, possibleRevId, false, null)) { commonAncestor = possibleRevId; return; } } }); return commonAncestor; }
internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRevId, Status status) { if (status == null) { status = new Status(); } status.Code = StatusCode.Ok; var revAttachments = rev.GetAttachments(); if (revAttachments == null) { return true; // no-op: no attachments } // Deletions can't have attachments: if (rev.IsDeleted() || revAttachments.Count == 0) { var body = rev.GetProperties(); body.Remove("_attachments"); rev.SetProperties(body); return true; } int generation = RevisionInternal.GenerationFromRevID(prevRevId) + 1; IDictionary<string, object> parentAttachments = null; return rev.MutateAttachments((name, attachInfo) => { AttachmentInternal attachment = null; try { attachment = new AttachmentInternal(name, attachInfo); } catch(CouchbaseLiteException e) { return null; } if(attachment.EncodedContent != null) { // If there's inline attachment data, decode and store it: BlobKey blobKey = new BlobKey(); if(!Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey)) { status.Code = StatusCode.AttachmentError; return null; } attachment.BlobKey = blobKey; } else if(attachInfo.GetCast<bool>("follows")) { // "follows" means the uploader provided the attachment in a separate MIME part. // This means it's already been registered in _pendingAttachmentsByDigest; // I just need to look it up by its "digest" property and install it into the store: InstallAttachment(attachment, attachInfo); } else if(attachInfo.GetCast<bool>("stub")) { // "stub" on an incoming revision means the attachment is the same as in the parent. if(parentAttachments == null && prevRevId != null) { parentAttachments = GetAttachmentsFromDoc(rev.GetDocId(), prevRevId, status); if(parentAttachments == null) { if(status.Code == StatusCode.Ok || status.Code == StatusCode.NotFound) { status.Code = StatusCode.BadAttachment; } return null; } } var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>(); if(parentAttachment == null) { status.Code = StatusCode.BadAttachment; return null; } return parentAttachment; } // Set or validate the revpos: if(attachment.RevPos == 0) { attachment.RevPos = generation; } else if(attachment.RevPos >= generation) { status.Code = StatusCode.BadAttachment; return null; } Debug.Assert(attachment.IsValid); return attachment.AsStubDictionary(); }); }
private void VerifyHistory(Database db, RevisionInternal rev, IList<string> history) { var gotRev = db.GetDocument(rev.GetDocId(), null, true); Assert.AreEqual(rev, gotRev); AssertPropertiesAreEqual(rev.GetProperties(), gotRev.GetProperties()); var revHistory = db.Storage.GetRevisionHistory(gotRev, null); Assert.AreEqual(history.Count, revHistory.Count); for (int i = 0; i < history.Count; i++) { RevisionInternal hrev = revHistory[i]; Assert.AreEqual(rev.GetDocId(), hrev.GetDocId()); Assert.AreEqual(history[i], hrev.GetRevId()); Assert.IsFalse(rev.IsDeleted()); } }
internal RevisionInternal PutLocalRevision(RevisionInternal revision, string prevRevID) { var docID = revision.GetDocId(); if (!docID.StartsWith ("_local/", StringComparison.InvariantCultureIgnoreCase)) { throw new CouchbaseLiteException(StatusCode.BadRequest); } if (!revision.IsDeleted()) { // PUT: string newRevID; var json = EncodeDocumentJSON(revision); if (prevRevID != null) { var generation = RevisionInternal.GenerationFromRevID(prevRevID); if (generation == 0) { throw new CouchbaseLiteException(StatusCode.BadRequest); } newRevID = Sharpen.Extensions.ToString(++generation) + "-local"; var values = new ContentValues(); values["revid"] = newRevID; values["json"] = json; var whereArgs = new [] { docID, prevRevID }; try { var rowsUpdated = StorageEngine.Update("localdocs", values, "docid=? AND revid=?", whereArgs); if (rowsUpdated == 0) { throw new CouchbaseLiteException(StatusCode.Conflict); } } catch (SQLException e) { throw new CouchbaseLiteException(e, StatusCode.InternalServerError); } } else { newRevID = "1-local"; var values = new ContentValues(); values["docid"] = docID; values["revid"] = newRevID; values["json"] = json; try { StorageEngine.InsertWithOnConflict("localdocs", null, values, ConflictResolutionStrategy.Ignore); } catch (SQLException e) { throw new CouchbaseLiteException(e, StatusCode.InternalServerError); } } return revision.CopyWithDocID(docID, newRevID); } else { // DELETE: DeleteLocalDocument(docID, prevRevID); return revision; } }
public void TestLocalDocs() { //create a document var documentProperties = new Dictionary<string, object>(); documentProperties["_id"] = "_local/doc1"; documentProperties["foo"] = 1; documentProperties["bar"] = false; var body = new Body(documentProperties); var rev1 = new RevisionInternal(body); rev1 = database.Storage.PutLocalRevision(rev1, null, true); Log.V(Tag, "Created " + rev1); Assert.AreEqual("_local/doc1", rev1.GetDocId()); Assert.IsTrue(rev1.GetRevId().StartsWith("1-")); //read it back var readRev = database.Storage.GetLocalDocument(rev1.GetDocId(), null); Assert.IsNotNull(readRev); var readRevProps = readRev.GetProperties(); Assert.AreEqual(rev1.GetDocId(), readRevProps.Get("_id")); Assert.AreEqual(rev1.GetRevId(), readRevProps.Get("_rev")); AssertPropertiesAreEqual(UserProperties(readRevProps), UserProperties(body.GetProperties())); //now update it documentProperties = (Dictionary<string, object>)readRev.GetProperties(); documentProperties["status"] = "updated!"; body = new Body(documentProperties); var rev2 = new RevisionInternal(body); var rev2input = rev2; rev2 = database.Storage.PutLocalRevision(rev2, rev1.GetRevId(), true); Log.V(Tag, "Updated " + rev1); Assert.AreEqual(rev1.GetDocId(), rev2.GetDocId()); Assert.IsTrue(rev2.GetRevId().StartsWith("2-")); //read it back readRev = database.Storage.GetLocalDocument(rev2.GetDocId(), null); Assert.IsNotNull(readRev); AssertPropertiesAreEqual(UserProperties(readRev.GetProperties()), UserProperties(body.GetProperties())); // Try to update the first rev, which should fail: var gotException = false; try { database.Storage.PutLocalRevision(rev2input, rev1.GetRevId(), true); } catch (CouchbaseLiteException e) { Assert.AreEqual(StatusCode.Conflict, e.CBLStatus.Code); gotException = true; } Assert.IsTrue(gotException); // Delete it: var revD = new RevisionInternal(rev2.GetDocId(), null, true); gotException = false; try { var revResult = database.Storage.PutLocalRevision(revD, null, true); Assert.IsNull(revResult); } catch (CouchbaseLiteException e) { Assert.AreEqual(StatusCode.Conflict, e.CBLStatus.Code); gotException = true; } Assert.IsTrue(gotException); revD = database.Storage.PutLocalRevision(revD, rev2.GetRevId(), true); // Delete nonexistent doc: gotException = false; var revFake = new RevisionInternal("_local/fake", null, true); try { database.Storage.PutLocalRevision(revFake, null, true); } catch (CouchbaseLiteException e) { Assert.AreEqual(StatusCode.NotFound, e.CBLStatus.Code); gotException = true; } Assert.IsTrue(gotException); // Read it back (should fail): readRev = database.Storage.GetLocalDocument(revD.GetDocId(), null); Assert.IsNull(readRev); }
public void TestRevTreeChangeNotification() { const string DOCUMENT_ID = "MyDocId"; var rev = new RevisionInternal(DOCUMENT_ID, "1-abcd", false); var revProperties = new Dictionary<string, object>(); revProperties["_id"] = rev.GetDocId(); revProperties["_rev"] = rev.GetRevId(); revProperties["message"] = "hi"; rev.SetProperties(revProperties); var revHistory = new List<string>(); revHistory.Add(rev.GetRevId()); EventHandler<DatabaseChangeEventArgs> handler = (sender, e) => { var changes = e.Changes.ToList(); Assert.AreEqual(1, changes.Count); var change = changes[0]; Assert.AreEqual(DOCUMENT_ID, change.DocumentId); Assert.AreEqual(rev.GetRevId(), change.RevisionId); Assert.IsTrue(change.IsCurrentRevision); Assert.IsFalse(change.IsConflict); var current = database.GetDocument(change.DocumentId).CurrentRevision; Assert.AreEqual(rev.GetRevId(), current.Id); }; database.Changed += handler; database.ForceInsert(rev, revHistory, null); database.Changed -= handler; // add two more revisions to the document var rev3 = new RevisionInternal(DOCUMENT_ID, "3-abcd", false); var rev3Properties = new Dictionary<string, object>(); rev3Properties["_id"] = rev3.GetDocId(); rev3Properties["_rev"] = rev3.GetRevId(); rev3Properties["message"] = "hi again"; rev3.SetProperties(rev3Properties); var rev3History = new List<string>(); rev3History.Add(rev3.GetRevId()); rev3History.Add("2-abcd"); rev3History.Add(rev.GetRevId()); handler = (sender, e) => { var changes = e.Changes.ToList(); Assert.AreEqual(1, changes.Count); var change = changes[0]; Assert.AreEqual(DOCUMENT_ID, change.DocumentId); Assert.AreEqual(rev3.GetRevId(), change.RevisionId); Assert.IsTrue(change.IsCurrentRevision); Assert.IsFalse(change.IsConflict); var doc = database.GetDocument(change.DocumentId); Assert.AreEqual(rev3.GetRevId(), doc.CurrentRevisionId); try { Assert.AreEqual(3, doc.RevisionHistory.ToList().Count); } catch (CouchbaseLiteException) { Assert.Fail(); } }; database.Changed += handler; database.ForceInsert(rev3, rev3History, null); database.Changed -= handler; // add a conflicting revision, with the same history length as the last revision we // inserted. Since this new revision's revID has a higher ASCII sort, it should become the // new winning revision. var conflictRev = new RevisionInternal(DOCUMENT_ID, "3-bcde", false); var conflictProperties = new Dictionary<string, object>(); conflictProperties["_id"] = conflictRev.GetDocId(); conflictProperties["_rev"] = conflictRev.GetRevId(); conflictProperties["message"] = "winner"; conflictRev.SetProperties(conflictProperties); var conflictRevHistory = new List<string>(); conflictRevHistory.Add(conflictRev.GetRevId()); conflictRevHistory.Add("2-abcd"); conflictRevHistory.Add(rev.GetRevId()); handler = (sender, e) => { var changes = e.Changes.ToList(); Assert.AreEqual(1, changes.Count); var change = changes[0]; Assert.AreEqual(DOCUMENT_ID, change.DocumentId); Assert.AreEqual(conflictRev.GetRevId(), change.RevisionId); Assert.IsTrue(change.IsCurrentRevision); Assert.IsFalse(change.IsConflict); var doc = database.GetDocument(change.DocumentId); Assert.AreEqual(rev3.GetRevId(), doc.CurrentRevisionId); try { Assert.AreEqual(2, doc.ConflictingRevisions.ToList().Count); Assert.AreEqual(3, doc.RevisionHistory.ToList().Count); } catch (CouchbaseLiteException) { Assert.Fail(); } }; database.Changed += handler; database.ForceInsert(conflictRev, conflictRevHistory, null); database.Changed -= handler; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestLocalDocs() { //create a document IDictionary<string, object> documentProperties = new Dictionary<string, object>(); documentProperties.Put("_id", "_local/doc1"); documentProperties.Put("foo", 1); documentProperties.Put("bar", false); Body body = new Body(documentProperties); RevisionInternal rev1 = new RevisionInternal(body, database); Status status = new Status(); rev1 = database.PutLocalRevision(rev1, null); Log.V(Tag, "Created " + rev1); NUnit.Framework.Assert.AreEqual("_local/doc1", rev1.GetDocId()); NUnit.Framework.Assert.IsTrue(rev1.GetRevId().StartsWith("1-")); //read it back RevisionInternal readRev = database.GetLocalDocument(rev1.GetDocId(), null); NUnit.Framework.Assert.IsNotNull(readRev); IDictionary<string, object> readRevProps = readRev.GetProperties(); NUnit.Framework.Assert.AreEqual(rev1.GetDocId(), readRev.GetProperties().Get("_id" )); NUnit.Framework.Assert.AreEqual(rev1.GetRevId(), readRev.GetProperties().Get("_rev" )); NUnit.Framework.Assert.AreEqual(UserProperties(readRevProps), UserProperties(body .GetProperties())); //now update it documentProperties = readRev.GetProperties(); documentProperties.Put("status", "updated!"); body = new Body(documentProperties); RevisionInternal rev2 = new RevisionInternal(body, database); RevisionInternal rev2input = rev2; rev2 = database.PutLocalRevision(rev2, rev1.GetRevId()); Log.V(Tag, "Updated " + rev1); NUnit.Framework.Assert.AreEqual(rev1.GetDocId(), rev2.GetDocId()); NUnit.Framework.Assert.IsTrue(rev2.GetRevId().StartsWith("2-")); //read it back readRev = database.GetLocalDocument(rev2.GetDocId(), null); NUnit.Framework.Assert.IsNotNull(readRev); NUnit.Framework.Assert.AreEqual(UserProperties(readRev.GetProperties()), UserProperties (body.GetProperties())); // Try to update the first rev, which should fail: bool gotException = false; try { database.PutLocalRevision(rev2input, rev1.GetRevId()); } catch (CouchbaseLiteException e) { NUnit.Framework.Assert.AreEqual(Status.Conflict, e.GetCBLStatus().GetCode()); gotException = true; } NUnit.Framework.Assert.IsTrue(gotException); // Delete it: RevisionInternal revD = new RevisionInternal(rev2.GetDocId(), null, true, database ); gotException = false; try { RevisionInternal revResult = database.PutLocalRevision(revD, null); NUnit.Framework.Assert.IsNull(revResult); } catch (CouchbaseLiteException e) { NUnit.Framework.Assert.AreEqual(Status.Conflict, e.GetCBLStatus().GetCode()); gotException = true; } NUnit.Framework.Assert.IsTrue(gotException); revD = database.PutLocalRevision(revD, rev2.GetRevId()); // Delete nonexistent doc: gotException = false; RevisionInternal revFake = new RevisionInternal("_local/fake", null, true, database ); try { database.PutLocalRevision(revFake, null); } catch (CouchbaseLiteException e) { NUnit.Framework.Assert.AreEqual(Status.NotFound, e.GetCBLStatus().GetCode()); gotException = true; } NUnit.Framework.Assert.IsTrue(gotException); // Read it back (should fail): readRev = database.GetLocalDocument(revD.GetDocId(), null); NUnit.Framework.Assert.IsNull(readRev); }
public RevisionInternal PutLocalRevision(RevisionInternal revision, string prevRevId, bool obeyMVCC) { var docId = revision.GetDocId(); if (!docId.StartsWith("_local/")) { throw new CouchbaseLiteException("Local revision IDs must start with _local/", StatusCode.BadId); } if (revision.IsDeleted()) { DeleteLocalRevision(docId, prevRevId, obeyMVCC); return revision; } var result = default(RevisionInternal); RunInTransaction(() => { var json = Manager.GetObjectMapper().WriteValueAsString(revision.GetProperties(), true); WithC4Raw(docId, "_local", doc => { var generation = RevisionInternal.GenerationFromRevID(prevRevId); if(obeyMVCC) { if(prevRevId != null) { if(prevRevId != (doc != null ? (string)doc->meta : null)) { throw new CouchbaseLiteException(StatusCode.Conflict); } if(generation == 0) { throw new CouchbaseLiteException(StatusCode.BadId); } } else if(doc != null) { throw new CouchbaseLiteException(StatusCode.Conflict); } } var newRevId = String.Format("{0}-local", ++generation); ForestDBBridge.Check(err => Native.c4raw_put(Forest, "_local", docId, newRevId, json, err)); result = revision.CopyWithDocID(docId, newRevId); }); return true; }); return result; }
public IList<RevisionInternal> GetRevisionHistory(RevisionInternal rev, ICollection<string> ancestorRevIds) { var history = new List<RevisionInternal>(); WithC4Document(rev.GetDocId(), rev.GetRevId(), false, false, doc => { var enumerator = new CBForestHistoryEnumerator(doc, false); foreach(var next in enumerator) { if(ancestorRevIds != null && ancestorRevIds.Contains((string)next.Document->selectedRev.revID)) { break; } var newRev = new RevisionInternal(next.Document, false); newRev.SetMissing(!Native.c4doc_hasRevisionBody(next.Document)); history.Add(newRev); } }); return history; }
public IList<String> GetPossibleAncestorRevisionIDs(RevisionInternal rev, int limit, ref Boolean hasAttachment) { var matchingRevs = new List<String>(); var generation = rev.GetGeneration(); if (generation <= 1) { return null; } var docNumericID = GetDocNumericID(rev.GetDocId()); if (docNumericID <= 0) { return null; } var sqlLimit = limit > 0 ? limit : -1; // SQL uses -1, not 0, to denote 'no limit' var sql = @"SELECT revid, sequence FROM revs WHERE doc_id=? and revid < ? and deleted=0 and json not null" + " ORDER BY sequence DESC LIMIT ?"; var args = new [] { Convert.ToString(docNumericID), generation + "-", sqlLimit.ToString() }; Cursor cursor = null; try { cursor = StorageEngine.RawQuery(sql, args); cursor.MoveToNext(); if (!cursor.IsAfterLast()) { if (matchingRevs.Count == 0) { hasAttachment = SequenceHasAttachments(cursor.GetLong(1)); } matchingRevs.AddItem(cursor.GetString(0)); } } catch (SQLException e) { Log.E(Database.Tag, "Error getting all revisions of document", e); } finally { if (cursor != null) { cursor.Close(); } } return matchingRevs; }
public static ICouchbaseResponseState RevsDiff(ICouchbaseListenerContext context) { // Collect all of the input doc/revision IDs as CBL_Revisions: var revs = new RevisionList(); var body = context.BodyAs<Dictionary<string, object>>(); if (body == null) { return context.CreateResponse(StatusCode.BadJson).AsDefaultState(); } foreach (var docPair in body) { var revIDs = docPair.Value.AsList<string>(); if (revIDs == null) { return context.CreateResponse(StatusCode.BadParam).AsDefaultState(); } foreach (var revID in revIDs) { var rev = new RevisionInternal(docPair.Key, revID, false); revs.Add(rev); } } return PerformLogicWithDatabase(context, true, db => { var response = context.CreateResponse(); // Look them up, removing the existing ones from revs: db.Storage.FindMissingRevisions(revs); // Return the missing revs in a somewhat different format: IDictionary<string, object> diffs = new Dictionary<string, object>(); foreach(var rev in revs) { var docId = rev.GetDocId(); IList<string> missingRevs = null; if(!diffs.ContainsKey(docId)) { missingRevs = new List<string>(); diffs[docId] = new Dictionary<string, IList<string>> { { "missing", missingRevs } }; } else { missingRevs = ((Dictionary<string, IList<string>>)diffs[docId])["missing"]; } missingRevs.Add(rev.GetRevId()); } // Add the possible ancestors for each missing revision: foreach(var docPair in diffs) { IDictionary<string, IList<string>> docInfo = (IDictionary<string, IList<string>>)docPair.Value; int maxGen = 0; string maxRevID = null; foreach(var revId in docInfo["missing"]) { var parsed = RevisionInternal.ParseRevId(revId); if(parsed.Item1 > maxGen) { maxGen = parsed.Item1; maxRevID = revId; } } var rev = new RevisionInternal(docPair.Key, maxRevID, false); var ancestors = db.Storage.GetPossibleAncestors(rev, 0, false); var ancestorList = ancestors == null ? null : ancestors.ToList(); if(ancestorList != null && ancestorList.Count > 0) { docInfo["possible_ancestors"] = ancestorList; } } response.JsonBody = new Body(diffs); return response; }).AsDefaultState(); }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal RevisionInternal LoadRevisionBody(RevisionInternal rev, DocumentContentOptions contentOptions) { if (rev.GetBody() != null && contentOptions == DocumentContentOptions.None && rev.GetSequence() != 0) { return rev; } if ((rev.GetDocId() == null) || (rev.GetRevId() == null)) { Log.E(Database.Tag, "Error loading revision body"); throw new CouchbaseLiteException(StatusCode.PreconditionFailed); } Cursor cursor = null; var result = new Status(StatusCode.NotFound); try { // TODO: on ios this query is: // TODO: "SELECT sequence, json FROM revs WHERE doc_id=@ AND revid=@ LIMIT 1" var sql = "SELECT sequence, json FROM revs, docs WHERE revid=? AND docs.docid=? AND revs.doc_id=docs.doc_id LIMIT 1"; var args = new [] { rev.GetRevId(), rev.GetDocId() }; cursor = StorageEngine.RawQuery(sql, CommandBehavior.SequentialAccess, args); if (cursor.MoveToNext()) { result.SetCode(StatusCode.Ok); rev.SetSequence(cursor.GetLong(0)); ExpandStoredJSONIntoRevisionWithAttachments(cursor.GetBlob(1), rev, contentOptions); } } catch (SQLException e) { Log.E(Tag, "Error loading revision body", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } finally { if (cursor != null) { cursor.Close(); } } if (result.GetCode() == StatusCode.NotFound) { throw new CouchbaseLiteException(result.GetCode()); } return rev; }
public RevisionInternal GetParentRevision(RevisionInternal rev) { var retVal = default(RevisionInternal); WithC4Document(rev.GetDocId(), rev.GetRevId(), false, false, doc => { if (!Native.c4doc_selectParentRevision(doc)) { return; } ForestDBBridge.Check(err => Native.c4doc_loadRevisionBody(doc, err)); retVal = new RevisionInternal((string)doc->docID, (string)doc->selectedRev.revID, doc->selectedRev.IsDeleted); retVal.SetSequence((long)doc->selectedRev.sequence); retVal.SetBody(new Body(doc->selectedRev.body)); }); return retVal; }
/// <summary> /// Creates a dictionary of metadata for one specific revision /// </summary> /// <returns>The metadata dictionary</returns> /// <param name="rev">The revision to examine</param> /// <param name="responseState">The current response state</param> public static IDictionary<string, object> ChangesDictForRev(RevisionInternal rev, DBMonitorCouchbaseResponseState responseState) { if (responseState.ChangesIncludeDocs) { var status = new Status(); var rev2 = DocumentMethods.ApplyOptions(responseState.ContentOptions, rev, responseState.Context, responseState.Db, status); if (rev2 != null) { rev2.SetSequence(rev.GetSequence()); rev = rev2; } } return new NonNullDictionary<string, object> { { "seq", rev.GetSequence() }, { "id", rev.GetDocId() }, { "changes", new List<object> { new Dictionary<string, object> { { "rev", rev.GetRevId() } } } }, { "deleted", rev.IsDeleted() ? (object)true : null }, { "doc", responseState.ChangesIncludeDocs ? rev.GetProperties() : null } }; }
public void TestRevTree() { var rev = new RevisionInternal("MyDocId", "4-abcd", false); var revProperties = new Dictionary<string, object>(); revProperties.Put("_id", rev.GetDocId()); revProperties.Put("_rev", rev.GetRevId()); revProperties["message"] = "hi"; rev.SetProperties(revProperties); var revHistory = new List<string>(); revHistory.AddItem(rev.GetRevId()); revHistory.AddItem("3-abcd"); revHistory.AddItem("2-abcd"); revHistory.AddItem("1-abcd"); database.ForceInsert(rev, revHistory, null); Assert.AreEqual(1, database.DocumentCount); VerifyHistory(database, rev, revHistory); var conflict = new RevisionInternal("MyDocId", "5-abcd", false); var conflictProperties = new Dictionary<string, object>(); conflictProperties.Put("_id", conflict.GetDocId()); conflictProperties.Put("_rev", conflict.GetRevId()); conflictProperties["message"] = "yo"; conflict.SetProperties(conflictProperties); var conflictHistory = new List<string>(); conflictHistory.AddItem(conflict.GetRevId()); conflictHistory.AddItem("4-bcde"); conflictHistory.AddItem("3-bcde"); conflictHistory.AddItem("2-abcd"); conflictHistory.AddItem("1-abcd"); database.ForceInsert(conflict, conflictHistory, null); Assert.AreEqual(1, database.DocumentCount); VerifyHistory(database, conflict, conflictHistory); // Add an unrelated document: var other = new RevisionInternal("AnotherDocID", "1-cdef", false); var otherProperties = new Dictionary<string, object>(); otherProperties["language"] = "jp"; other.SetProperties(otherProperties); var otherHistory = new List<string>(); otherHistory.AddItem(other.GetRevId()); database.ForceInsert(other, otherHistory, null); // Fetch one of those phantom revisions with no body: var rev2 = database.GetDocument(rev.GetDocId(), "2-abcd", true); Assert.IsNull(rev2); // Make sure no duplicate rows were inserted for the common revisions: Assert.IsTrue(database.LastSequenceNumber <= 8); // Make sure the revision with the higher revID wins the conflict: var current = database.GetDocument(rev.GetDocId(), null, true); Assert.AreEqual(conflict, current); // Get the _changes feed and verify only the winner is in it: var options = new ChangesOptions(); var changes = database.ChangesSince(0, options, null, null); var expectedChanges = new RevisionList(); expectedChanges.AddItem(conflict); expectedChanges.AddItem(other); Assert.AreEqual(expectedChanges, changes); options.IncludeConflicts = true; changes = database.ChangesSince(0, options, null, null); expectedChanges = new RevisionList(); expectedChanges.AddItem(rev); expectedChanges.AddItem(conflict); expectedChanges.AddItem(other); var expectedChangesAlt = new RevisionList(); expectedChangesAlt.AddItem(conflict); expectedChangesAlt.AddItem(rev); expectedChangesAlt.AddItem(other); Assert.IsTrue(expectedChanges.SequenceEqual(changes) || expectedChangesAlt.SequenceEqual(changes)); }
/// <summary>Stores a new (or initial) revision of a document.</summary> /// <remarks> /// Stores a new (or initial) revision of a document. /// This is what's invoked by a PUT or POST. As with those, the previous revision ID must be supplied when necessary and the call will fail if it doesn't match. /// </remarks> /// <param name="oldRev">The revision to add. If the docID is null, a new UUID will be assigned. Its revID must be null. It must have a JSON body. /// </param> /// <param name="prevRevId">The ID of the revision to replace (same as the "?rev=" parameter to a PUT), or null if this is a new document. /// </param> /// <param name="allowConflict">If false, an error status 409 will be returned if the insertion would create a conflict, i.e. if the previous revision already has a child. /// </param> /// <param name="resultStatus">On return, an HTTP status code indicating success or failure. /// </param> /// <returns>A new RevisionInternal with the docID, revID and sequence filled in (but no body). /// </returns> /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal RevisionInternal PutRevision(RevisionInternal oldRev, string prevRevId, bool allowConflict, Status resultStatus = null) { return PutDocument(oldRev.GetDocId(), oldRev.GetProperties(), prevRevId, allowConflict, resultStatus); }
/// <summary>Inserts an already-existing revision replicated from a remote sqliteDb.</summary> /// <remarks> /// Inserts an already-existing revision replicated from a remote sqliteDb. /// It must already have a revision ID. This may create a conflict! The revision's history must be given; ancestor revision IDs that don't already exist locally will create phantom revisions with no content. /// </remarks> /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal void ForceInsert(RevisionInternal rev, IList<string> revHistory, Uri source) { var inConflict = false; var docId = rev.GetDocId(); var revId = rev.GetRevId(); if (!IsValidDocumentId(docId) || (revId == null)) { throw new CouchbaseLiteException(StatusCode.BadRequest); } int historyCount = 0; if (revHistory != null) { historyCount = revHistory.Count; } if (historyCount == 0) { revHistory = new AList<string>(); revHistory.AddItem(revId); historyCount = 1; } else { if (!revHistory[0].Equals(rev.GetRevId())) { throw new CouchbaseLiteException(StatusCode.BadRequest); } } bool success = false; BeginTransaction(); try { // First look up all locally-known revisions of this document: long docNumericID = GetOrInsertDocNumericID(docId); RevisionList localRevs = GetAllRevisionsOfDocumentID(docId, docNumericID, false); if (localRevs == null) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } IList<bool> outIsDeleted = new AList<bool>(); IList<bool> outIsConflict = new AList<bool>(); bool oldWinnerWasDeletion = false; string oldWinningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict ); if (outIsDeleted.Count > 0) { oldWinnerWasDeletion = true; } if (outIsConflict.Count > 0) { inConflict = true; } // Walk through the remote history in chronological order, matching each revision ID to // a local revision. When the list diverges, start creating blank local revisions to fill // in the local history: long sequence = 0; long localParentSequence = 0; for (int i = revHistory.Count - 1; i >= 0; --i) { revId = revHistory[i]; RevisionInternal localRev = localRevs.RevWithDocIdAndRevId(docId, revId); if (localRev != null) { // This revision is known locally. Remember its sequence as the parent of the next one: sequence = localRev.GetSequence(); Debug.Assert((sequence > 0)); localParentSequence = sequence; } else { // This revision isn't known, so add it: RevisionInternal newRev; IEnumerable<Byte> data = null; bool current = false; if (i == 0) { // Hey, this is the leaf revision we're inserting: newRev = rev; if (!rev.IsDeleted()) { data = EncodeDocumentJSON(rev); if (data == null) { throw new CouchbaseLiteException(StatusCode.BadRequest); } } current = true; } else { // It's an intermediate parent, so insert a stub: newRev = new RevisionInternal(docId, revId, false, this); } // Insert it: sequence = InsertRevision(newRev, docNumericID, sequence, current, (GetAttachmentsFromRevision(newRev).Count > 0), data); if (sequence <= 0) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } if (i == 0) { // Write any changed attachments for the new revision. As the parent sequence use // the latest local revision (this is to copy attachments from): var attachments = GetAttachmentsFromRevision(rev); if (attachments != null) { ProcessAttachmentsForRevision(attachments, rev, localParentSequence); StubOutAttachmentsInRevision(attachments, rev); } } } } // Mark the latest local rev as no longer current: if (localParentSequence > 0 && localParentSequence != sequence) { ContentValues args = new ContentValues(); args["current"] = 0; string[] whereArgs = new string[] { Convert.ToString(localParentSequence) }; try { var numRowsChanged = StorageEngine.Update("revs", args, "sequence=?", whereArgs); if (numRowsChanged == 0) { inConflict = true; } } catch (Exception) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } } var winningRev = Winner(docNumericID, oldWinningRevID, oldWinnerWasDeletion, rev); success = true; NotifyChange(rev, winningRev, source, inConflict); } catch (SQLException) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } finally { EndTransaction(success); } }
internal RevisionInternal RevisionByLoadingBody(RevisionInternal rev, Status outStatus) { // First check for no-op -- if we just need the default properties and already have them: if (rev.GetSequence() != 0) { var props = rev.GetProperties(); if (props != null && props.ContainsKey("_rev") && props.ContainsKey("_id")) { if (outStatus != null) { outStatus.Code = StatusCode.Ok; } return rev; } } RevisionInternal nuRev = rev.CopyWithDocID(rev.GetDocId(), rev.GetRevId()); try { LoadRevisionBody(nuRev); } catch(CouchbaseLiteException e) { if (outStatus != null) { outStatus.Code = e.CBLStatus.Code; } nuRev = null; } return nuRev; }
internal RevisionInternal GetParentRevision(RevisionInternal rev) { // First get the parent's sequence: var seq = rev.GetSequence(); if (seq > 0) { seq = LongForQuery("SELECT parent FROM revs WHERE sequence=?", new [] { Convert.ToString(seq) }); } else { var docNumericID = GetDocNumericID(rev.GetDocId()); if (docNumericID <= 0) { return null; } var args = new [] { Convert.ToString(docNumericID), rev.GetRevId() }; seq = LongForQuery("SELECT parent FROM revs WHERE doc_id=? and revid=?", args); } if (seq == 0) { return null; } // Now get its revID and deletion status: RevisionInternal result = null; var queryArgs = new [] { Convert.ToString(seq) }; var queryString = "SELECT revid, deleted FROM revs WHERE sequence=?"; Cursor cursor = null; try { cursor = StorageEngine.RawQuery(queryString, queryArgs); if (cursor.MoveToNext()) { string revId = cursor.GetString(0); bool deleted = (cursor.GetInt(1) > 0); result = new RevisionInternal(rev.GetDocId(), revId, deleted, this); result.SetSequence(seq); } } finally { cursor.Close(); } return result; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException">When attempting to add an invalid revision</exception> internal void ForceInsert(RevisionInternal inRev, IList<string> revHistory, Uri source) { if (revHistory == null) { revHistory = new List<string>(0); } var rev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId()); rev.SetSequence(0); string revID = rev.GetRevId(); if (!IsValidDocumentId(rev.GetDocId()) || revID == null) { throw new CouchbaseLiteException(StatusCode.BadId); } if (revHistory.Count == 0) { revHistory.Add(revID); } else if (revID != revHistory[0]) { throw new CouchbaseLiteException(StatusCode.BadId); } if (inRev.GetAttachments() != null) { var updatedRev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId()); string prevRevID = revHistory.Count >= 2 ? revHistory[1] : null; Status status = new Status(); if (!ProcessAttachmentsForRevision(updatedRev, prevRevID, status)) { throw new CouchbaseLiteException(status.Code); } inRev = updatedRev; } StoreValidation validationBlock = null; if (Shared != null && Shared.HasValues("validation", Name)) { validationBlock = ValidateRevision; } var insertStatus = Storage.ForceInsert(inRev, revHistory, validationBlock, source); if(insertStatus.IsError) { throw new CouchbaseLiteException(insertStatus.Code); } }
/// <summary>Inserts the _id, _rev and _attachments properties into the JSON data and stores it in rev. /// </summary> /// <remarks> /// Inserts the _id, _rev and _attachments properties into the JSON data and stores it in rev. /// Rev must already have its revID and sequence properties set. /// </remarks> internal IDictionary<String, Object> ExtraPropertiesForRevision(RevisionInternal rev, DocumentContentOptions contentOptions) { var docId = rev.GetDocId(); var revId = rev.GetRevId(); var sequenceNumber = rev.GetSequence(); Debug.Assert((revId != null)); Debug.Assert((sequenceNumber > 0)); // Get attachment metadata, and optionally the contents: IDictionary<string, object> attachmentsDict = null; if (!contentOptions.HasFlag(DocumentContentOptions.NoAttachments)) { attachmentsDict = GetAttachmentsDictForSequenceWithContent (sequenceNumber, contentOptions); } // Get more optional stuff to put in the properties: //OPT: This probably ends up making redundant SQL queries if multiple options are enabled. var localSeq = -1L; if (contentOptions.HasFlag(DocumentContentOptions.IncludeLocalSeq)) { localSeq = sequenceNumber; } IDictionary<string, object> revHistory = null; if (contentOptions.HasFlag(DocumentContentOptions.IncludeRevs)) { revHistory = GetRevisionHistoryDict(rev); } IList<object> revsInfo = null; if (contentOptions.HasFlag(DocumentContentOptions.IncludeRevsInfo)) { revsInfo = new AList<object>(); var revHistoryFull = GetRevisionHistory(rev); foreach (RevisionInternal historicalRev in revHistoryFull) { var revHistoryItem = new Dictionary<string, object>(); var status = "available"; if (historicalRev.IsDeleted()) { status = "deleted"; } if (historicalRev.IsMissing()) { status = "missing"; } revHistoryItem.Put("rev", historicalRev.GetRevId()); revHistoryItem["status"] = status; revsInfo.AddItem(revHistoryItem); } } IList<string> conflicts = null; if (contentOptions.HasFlag(DocumentContentOptions.IncludeConflicts)) { var revs = GetAllRevisionsOfDocumentID(docId, true); if (revs.Count > 1) { conflicts = new AList<string>(); foreach (RevisionInternal savedRev in revs) { if (!(savedRev.Equals(rev) || savedRev.IsDeleted())) { conflicts.AddItem(savedRev.GetRevId()); } } } } var result = new Dictionary<string, object>(); result["_id"] = docId; result["_rev"] = revId; if (rev.IsDeleted()) { result["_deleted"] = true; } if (attachmentsDict != null) { result["_attachments"] = attachmentsDict; } if (localSeq > -1) { result["_local_seq"] = localSeq; } if (revHistory != null) { result["_revisions"] = revHistory; } if (revsInfo != null) { result["_revs_info"] = revsInfo; } if (conflicts != null) { result["_conflicts"] = conflicts; } return result; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestValidations() { Database.ValidateDelegate validator = (Revision newRevision, ValidationContext context)=> { NUnit.Framework.Assert.IsNotNull(newRevision); NUnit.Framework.Assert.IsNotNull(context); NUnit.Framework.Assert.IsTrue(newRevision.Properties != null || newRevision. IsDeletion); this._enclosing.validationCalled = true; bool hoopy = newRevision.IsDeletion || (newRevision.Properties.Get("towel" ) != null); Log.V(ValidationsTest.Tag, string.Format("--- Validating %s --> %b", newRevision. Properties, hoopy)); if (!hoopy) { context.Reject("Where's your towel?"); } return hoopy; }; database.SetValidation("hoopy", validator); // POST a valid new document: IDictionary<string, object> props = new Dictionary<string, object>(); props["name"] = "Zaphod Beeblebrox"; props["towel"] = "velvet"; RevisionInternal rev = new RevisionInternal(props, database); Status status = new Status(); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(StatusCode.Created, status.GetCode()); // PUT a valid update: props["head_count"] = 3; rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(StatusCode.Created, status.GetCode()); // PUT an invalid update: Sharpen.Collections.Remove(props, "towel"); rev.SetProperties(props); validationCalled = false; bool gotExpectedError = false; try { rev = database.PutRevision(rev, rev.GetRevId(), false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // POST an invalid new document: props = new Dictionary<string, object>(); props["name"] = "Vogon"; props["poetry"] = true; rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // PUT a valid new document with an ID: props = new Dictionary<string, object>(); props["_id"] = "ford"; props["name"] = "Ford Prefect"; props["towel"] = "terrycloth"; rev = new RevisionInternal(props, database); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual("ford", rev.GetDocId()); // DELETE a document: rev = new RevisionInternal(rev.GetDocId(), rev.GetRevId(), true, database); NUnit.Framework.Assert.IsTrue(rev.IsDeleted()); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); // PUT an invalid new document: props = new Dictionary<string, object>(); props["_id"] = "petunias"; props["name"] = "Pot of Petunias"; rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); }
/// <summary>Returns an array of TDRevs in reverse chronological order, starting with the given revision. /// </summary> /// <remarks>Returns an array of TDRevs in reverse chronological order, starting with the given revision. /// </remarks> internal IList<RevisionInternal> GetRevisionHistory(RevisionInternal rev) { string docId = rev.GetDocId(); string revId = rev.GetRevId(); Debug.Assert(((docId != null) && (revId != null))); long docNumericId = GetDocNumericID(docId); if (docNumericId < 0) { return null; } else { if (docNumericId == 0) { return new AList<RevisionInternal>(); } } Cursor cursor = null; IList<RevisionInternal> result; var args = new [] { Convert.ToString(docNumericId) }; var sql = "SELECT sequence, parent, revid, deleted, json isnull FROM revs WHERE doc_id=? ORDER BY sequence DESC"; try { cursor = StorageEngine.RawQuery(sql, args); cursor.MoveToNext(); long lastSequence = 0; result = new AList<RevisionInternal>(); while (!cursor.IsAfterLast()) { var sequence = cursor.GetLong(0); var parent = cursor.GetLong(1); bool matches = false; if (lastSequence == 0) { matches = revId.Equals(cursor.GetString(2)); } else { matches = (sequence == lastSequence); } if (matches) { revId = cursor.GetString(2); var deleted = (cursor.GetInt(3) > 0); var missing = (cursor.GetInt(4) > 0); var aRev = new RevisionInternal(docId, revId, deleted, this); aRev.SetSequence(sequence); aRev.SetMissing(missing); result.AddItem(aRev); if (parent > -1) lastSequence = parent; if (lastSequence == 0) { break; } } cursor.MoveToNext(); } } catch (SQLException e) { Log.E(Tag, "Error getting revision history", e); return null; } finally { if (cursor != null) { cursor.Close(); } } return result; }