public void TestCBLCompareRevIDs() { // Single Digit Assert.IsTrue(RevisionID.CBLCollateRevIDs("1-foo", "1-foo") == 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("2-bar", "1-foo") > 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("1-foo", "2-bar") < 0); // Multi-digit: Assert.IsTrue(RevisionID.CBLCollateRevIDs("123-bar", "456-foo") < 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("456-foo", "123-bar") > 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("456-foo", "456-foo") == 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("456-foo", "456-foofoo") < 0); // Different numbers of digits: Assert.IsTrue(RevisionID.CBLCollateRevIDs("89-foo", "123-bar") < 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("123-bar", "89-foo") > 0); // Edge cases: Assert.IsTrue(RevisionID.CBLCollateRevIDs("123-", "89-") > 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("123-a", "123-a") == 0); // Invalid rev IDs: Assert.IsTrue(RevisionID.CBLCollateRevIDs("-a", "-b") < 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("-", "-") == 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs(string.Empty, string.Empty) == 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs(string.Empty, "-b") < 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("bogus", "yo") < 0); Assert.IsTrue(RevisionID.CBLCollateRevIDs("bogus-x", "yo-y") < 0); }
public void TestParseRevID() { var parsed = RevisionID.ParseRevId("1-utiopturoewpt"); Assert.AreEqual(1, parsed.Item1); Assert.AreEqual("utiopturoewpt", parsed.Item2); parsed = RevisionID.ParseRevId("321-fdjfdsj-e"); Assert.AreEqual(321, parsed.Item1); Assert.AreEqual("fdjfdsj-e", parsed.Item2); parsed = RevisionID.ParseRevId("0-fdjfdsj-e"); Assert.IsTrue(parsed.Item1 == 0 && parsed.Item2 == "fdjfdsj-e"); parsed = RevisionID.ParseRevId("-4-fdjfdsj-e"); Assert.IsTrue(parsed.Item1 < 0); parsed = RevisionID.ParseRevId("5_fdjfdsj-e"); Assert.IsTrue(parsed.Item1 < 0); parsed = RevisionID.ParseRevId(" 5-fdjfdsj-e"); Assert.IsTrue(parsed.Item1 < 0); parsed = RevisionID.ParseRevId("7 -foo"); Assert.IsTrue(parsed.Item1 < 0); parsed = RevisionID.ParseRevId("7-"); Assert.IsTrue(parsed.Item1 < 0); parsed = RevisionID.ParseRevId("7"); Assert.IsTrue(parsed.Item1 < 0); parsed = RevisionID.ParseRevId("eiuwtiu"); Assert.IsTrue(parsed.Item1 < 0); parsed = RevisionID.ParseRevId(string.Empty); Assert.IsTrue(parsed.Item1 < 0); }
/// <summary> /// Finds the common ancestor. /// </summary> /// <remarks> /// Given a revision and an array of revIDs, finds the latest common ancestor revID /// and returns its generation #. If there is none, returns 0. /// </remarks> /// <returns>The common ancestor.</returns> /// <param name="rev">Rev.</param> /// <param name="possibleRevIDs">Possible rev I ds.</param> internal static int FindCommonAncestor(RevisionInternal rev, IList <string> possibleRevIDs) { if (possibleRevIDs == null || possibleRevIDs.Count == 0) { return(0); } var history = Database.ParseCouchDBRevisionHistory(rev.GetProperties()); Debug.Assert(history != null); history = history.Intersect(possibleRevIDs).ToList(); var ancestorID = history.Count == 0 ? null : history[0]; if (ancestorID == null) { return(0); } var parsed = RevisionID.ParseRevId(ancestorID); return(parsed.Item1); }
// Used by plugins internal DocumentChange(RevisionInternal addedRevision, RevisionID winningRevisionId, bool isConflict, Uri sourceUrl) { AddedRevision = addedRevision; WinningRevisionId = winningRevisionId; IsConflict = isConflict; SourceUrl = sourceUrl; }
// Used by plugins internal DocumentChange(RevisionInternal addedRevision, RevisionID winningRevisionId, bool isConflict, Uri sourceUrl) { AddedRevision = addedRevision; WinningRevisionId = winningRevisionId; IsConflict = isConflict; SourceUrl = sourceUrl; }
internal RevisionInternal(string docId, RevisionID revId, bool deleted) { // TODO: get rid of this field! _docId = docId; _revId = revId; Deleted = deleted; }
internal static void SetRevID(this IDictionary<string, object> dict, RevisionID revId) { if(dict == null) { return; } dict["_rev"] = revId.ToString(); }
internal static void SetRevID(this IDictionary <string, object> dict, RevisionID revId) { if (dict == null) { return; } dict["_rev"] = revId.ToString(); }
internal static unsafe void PinAndUse(this RevisionID revID, Action <C4Slice> action) { var data = revID.AsData(); fixed(byte *dataPtr = data) { var slice = new C4Slice(dataPtr, (uint)data.Length); action(slice); } }
public void TestResolveConflict() { var properties = new Dictionary <string, object>() { { "testName", "testCreateRevisions" }, { "tag", 1337 } }; // Create a conflict on purpose var doc = database.CreateDocument(); var newRev1 = doc.CreateRevision(); newRev1.SetUserProperties(properties); var rev1 = newRev1.Save(); var rev2a = CreateRevisionWithRandomProps(rev1, false); var rev2b = CreateRevisionWithRandomProps(rev1, true); SavedRevision winningRev = null; SavedRevision losingRev = null; if (doc.CurrentRevisionId.Equals(rev2a.Id)) { winningRev = rev2a; losingRev = rev2b; } else { winningRev = rev2b; losingRev = rev2a; } Assert.AreEqual(2, doc.ConflictingRevisions.Count()); Assert.AreEqual(2, doc.GetLeafRevisions(true).Count); // let's manually choose the losing rev as the winner. First, delete winner, which will // cause losing rev to be the current revision. var deleteRevision = winningRev.DeleteDocument(); Assert.AreEqual(1, doc.ConflictingRevisions.Count()); Assert.AreEqual(2, doc.GetLeafRevisions(true).Count); Assert.AreEqual(3, RevisionID.GetGeneration(deleteRevision.Id)); Assert.AreEqual(losingRev.Id, doc.CurrentRevisionId); // Finally create a new revision rev3 based on losing rev var rev3 = CreateRevisionWithRandomProps(losingRev, true); Assert.AreEqual(rev3.Id, doc.CurrentRevisionId); Assert.AreEqual(1, doc.ConflictingRevisions.Count()); Assert.AreEqual(2, doc.GetLeafRevisions(true).Count); }
internal SavedRevision GetRevisionWithId(RevisionID revId, bool withBody) { if (revId == null) { return(null); } if (revId.Equals(currentRevision.Id)) { return(currentRevision); } return(GetRevisionFromRev(Database.GetDocument(Id, revId, withBody))); }
public RevisionInternal Copy(string docId, RevisionID revId) { System.Diagnostics.Debug.Assert((docId != null)); System.Diagnostics.Debug.Assert(((_docId == null) || (_docId.Equals(docId)))); var result = new RevisionInternal(docId, revId, Deleted); var unmodifiableProperties = GetProperties(); var properties = new Dictionary <string, object>(); if (unmodifiableProperties != null) { properties.PutAll(unmodifiableProperties); } properties.SetDocRevID(docId, revId); result.SetProperties(properties); return(result); }
public static RevisionID RevIDForJSON(IEnumerable<byte> json, bool deleted, RevisionID prevRevID) { // Revision IDs have a generation count, a hyphen, and a hex digest var generation = 0; if(prevRevID != null) { generation = prevRevID.Generation; if(generation == 0) { return null; } } // Generate a digest for this revision based on the previous revision ID, document JSON, // and attachment digests. This doesn't need to be secure; we just need to ensure that this // code consistently generates the same ID given equivalent revisions. MessageDigest md5Digest; try { md5Digest = MessageDigest.GetInstance("MD5"); } catch(NotSupportedException) { throw Misc.CreateExceptionAndLog(Log.To.Database, Tag, "Failed to acquire a class to create MD5"); } if(prevRevID != null) { var prevIDData = prevRevID.AsData(); var length = prevIDData.Length; var lengthByte = unchecked((byte)(length & unchecked((0xFF)))); md5Digest.Update(lengthByte); if(lengthByte > 0) { md5Digest.Update(prevIDData); } } var isDeleted = deleted ? 1 : 0; md5Digest.Update((byte)isDeleted); if(json != null) { md5Digest.Update(json.ToArray()); } var md5DigestResult = md5Digest.Digest(); var digestAsHex = BitConverter.ToString(md5DigestResult).Replace("-", String.Empty); int generationIncremented = generation + 1; return RevisionIDFactory.FromString(String.Format("{0}-{1}", generationIncremented, digestAsHex).ToLower()); }
internal Body(IEnumerable <byte> json, string docID, RevisionID revID, bool deleted) { var count = json.Count(); if (json != null && count < 2) { _jsonObject = new NonNullDictionary <string, object> { { "_id", docID }, { "_rev", revID.ToString() }, { "_deleted", deleted ? (object)true : null } }; return; } var stringToAdd = String.Format("{{\"_id\":\"{0}\",\"_rev\":\"{1}\",{2}}}", docID, revID, deleted ? "\"_deleted\":true," : String.Empty); var bytes = Encoding.UTF8.GetBytes(stringToAdd).ToList(); bytes.InsertRange(bytes.Count - 1, json.Skip(1).Take(count - 2)); _json = bytes.ToArray(); }
internal RevisionInternal(string docId, RevisionID revId, bool deleted, Body body) : this(docId, revId, deleted) { _body = body; }
private RevisionInternal GetDocumentWithIDAndRev(string docId, RevisionID revId, bool withBody) { return Storage.GetDocument(docId, revId, withBody); }
internal RevisionInternal PutRevision(RevisionInternal oldRev, RevisionID prevRevId, bool allowConflict, Uri source) { return PutDocument(oldRev.DocID, oldRev.GetProperties(), prevRevId, allowConflict, source); }
/// <summary> /// Returns document by the specified docid from the specified db. Unless you request a /// specific revision, the latest revision of the document will always be returned. /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> /// <remarks> /// http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid /// <remarks> public static ICouchbaseResponseState GetDocument(ICouchbaseListenerContext context) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => { var response = context.CreateResponse(); string docId = context.DocumentName; bool isLocalDoc = docId.StartsWith("_local"); DocumentContentOptions options = context.ContentOptions; string openRevsParam = context.GetQueryParam("open_revs"); bool mustSendJson = context.ExplicitlyAcceptsType("application/json"); if (openRevsParam == null || isLocalDoc) { //Regular GET: string revId = context.GetQueryParam("rev"); //often null RevisionInternal rev; bool includeAttachments = false, sendMultipart = false; if (isLocalDoc) { rev = db.Storage.GetLocalDocument(docId, revId); } else { includeAttachments = options.HasFlag(DocumentContentOptions.IncludeAttachments); if (includeAttachments) { sendMultipart = !mustSendJson; options &= ~DocumentContentOptions.IncludeAttachments; } Status status = new Status(); rev = db.GetDocument(docId, revId, true, status); if (rev != null) { rev = ApplyOptions(options, rev, context, db, status); } if (rev == null) { if (status.Code == StatusCode.Deleted) { response.StatusReason = "deleted"; } else { response.StatusReason = "missing"; } response.InternalStatus = status.Code; return response; } } if (rev == null) { response.InternalStatus = StatusCode.NotFound; return response; } if (context.CacheWithEtag(rev.RevID)) { response.InternalStatus = StatusCode.NotModified; return response; } if (!isLocalDoc && includeAttachments) { int minRevPos = 1; IList <string> attsSince = context.GetJsonQueryParam("atts_since").AsList <string>(); string ancestorId = db.Storage.FindCommonAncestor(rev, attsSince); if (ancestorId != null) { minRevPos = RevisionID.GetGeneration(ancestorId) + 1; } bool attEncodingInfo = context.GetQueryParam <bool>("att_encoding_info", bool.TryParse, false); db.ExpandAttachments(rev, minRevPos, sendMultipart, attEncodingInfo); } if (sendMultipart) { response.MultipartWriter = MultipartWriterForRev(db, rev, "multipart/related"); } else { response.JsonBody = rev.GetBody(); } } else { // open_revs query: IList <IDictionary <string, object> > result; if (openRevsParam.Equals("all")) { // ?open_revs=all returns all current/leaf revisions: bool includeDeleted = context.GetQueryParam <bool>("include_deleted", bool.TryParse, false); RevisionList allRevs = db.Storage.GetAllDocumentRevisions(docId, true); result = new List <IDictionary <string, object> >(); foreach (var rev in allRevs) { if (!includeDeleted && rev.Deleted) { continue; } Status status = new Status(); var loadedRev = db.RevisionByLoadingBody(rev, status); if (loadedRev != null) { ApplyOptions(options, loadedRev, context, db, status); } if (loadedRev != null) { result.Add(new Dictionary <string, object> { { "ok", loadedRev.GetProperties() } }); } else if (status.Code <= StatusCode.InternalServerError) { result.Add(new Dictionary <string, object> { { "missing", rev.RevID } }); } else { response.InternalStatus = status.Code; return response; } } } else { // ?open_revs=[...] returns an array of specific revisions of the document: var openRevs = context.GetJsonQueryParam("open_revs").AsList <object>(); if (openRevs == null) { response.InternalStatus = StatusCode.BadParam; return response; } result = new List <IDictionary <string, object> >(); foreach (var revIDObj in openRevs) { var revID = revIDObj as string; if (revID == null) { response.InternalStatus = StatusCode.BadId; return response; } Status status = new Status(); var rev = db.GetDocument(docId, revID, true); if (rev != null) { rev = ApplyOptions(options, rev, context, db, status); } if (rev != null) { result.Add(new Dictionary <string, object> { { "ok", rev.GetProperties() } }); } else { result.Add(new Dictionary <string, object> { { "missing", revID } }); } } } if (mustSendJson) { response["Content-Type"] = "application/json"; response.JsonBody = new Body(result.Cast <object>().ToList()); } else { response.SetMultipartBody(result.Cast <object>().ToList(), "multipart/mixed"); } } return response; }).AsDefaultState()); }
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.AsRevID(), 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.DocID; IList <RevisionID> missingRevs = null; if (!diffs.ContainsKey(docId)) { missingRevs = new List <RevisionID>(); diffs[docId] = new Dictionary <string, IList <RevisionID> > { { "missing", missingRevs } }; } else { missingRevs = ((Dictionary <string, IList <RevisionID> >)diffs[docId])["missing"]; } missingRevs.Add(rev.RevID); } // Add the possible ancestors for each missing revision: foreach (var docPair in diffs) { IDictionary <string, IList <RevisionID> > docInfo = (IDictionary <string, IList <RevisionID> >)docPair.Value; int maxGen = 0; RevisionID maxRevID = null; foreach (var revId in docInfo["missing"]) { if (revId.Generation > maxGen) { maxGen = revId.Generation; maxRevID = revId; } } var rev = new RevisionInternal(docPair.Key, maxRevID, false); var ancestors = db.Storage.GetPossibleAncestors(rev, 0, ValueTypePtr <bool> .NULL, false)?.ToList(); if (ancestors != null && ancestors.Count > 0) { docInfo["possible_ancestors"] = ancestors; } } response.JsonBody = new Body(diffs); return response; }).AsDefaultState()); }
internal IDictionary<string, object> GetAttachmentsFromDoc(string docId, RevisionID revId) { var rev = new RevisionInternal(docId, revId, false); LoadRevisionBody(rev); return rev.GetAttachments(); }
internal RevisionInternal(string docId, RevisionID revId, bool deleted, Body body) : this(docId, revId, deleted) { _body = body; }
/// <summary>Updates or deletes an attachment, creating a new document revision in the process. /// </summary> /// <remarks> /// Updates or deletes an attachment, creating a new document revision in the process. /// Used by the PUT / DELETE methods called on attachment URLs. Used by the listener; /// </remarks> internal RevisionInternal UpdateAttachment(string filename, BlobStoreWriter body, string contentType, AttachmentEncoding encoding, string docID, RevisionID oldRevID, Uri source) { if(StringEx.IsNullOrWhiteSpace(filename)) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG, "Invalid filename (null or whitespace) in UpdateAttachment"); } if(body != null && contentType == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG, "Body provided, but content type is null in UpdateAttachment"); } if(oldRevID != null && docID == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG, "oldRevID provided ({0}) but docID is null in UpdateAttachment", oldRevID); } if(body != null && docID == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG, "body provided but docID is null in UpdateAttachment"); } var oldRev = new RevisionInternal(docID, oldRevID, false); if(oldRevID != null) { // Load existing revision if this is a replacement: try { oldRev = LoadRevisionBody(oldRev); } catch(CouchbaseLiteException e) { if(e.Code == StatusCode.NotFound && GetDocument(docID, null, false) != null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.Conflict, TAG, "Conflict detected in UpdateAttachment"); } Log.To.Database.E(TAG, "Error loading revision body in UpdateAttachment, rethrowing..."); throw; } } else { // If this creates a new doc, it needs a body: oldRev.SetBody(new Body(new Dictionary<string, object>())); } // Update the _attachments dictionary: var attachments = oldRev.GetProperties().Get("_attachments").AsDictionary<string, object>(); if(attachments == null) { attachments = new Dictionary<string, object>(); } if(body != null) { var key = body.GetBlobKey(); string digest = key.Base64Digest(); RememberAttachmentWriter(body, digest); string encodingName = (encoding == AttachmentEncoding.GZIP) ? "gzip" : null; attachments[filename] = new NonNullDictionary<string, object> { { "digest", digest }, { "length", body.GetLength() }, { "follows", true }, { "content_type", contentType }, { "encoding", encodingName } }; } else { if(oldRevID != null && attachments.Get(filename) == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.AttachmentNotFound, TAG, "Attachment {0} not found", new SecureLogString(filename, LogMessageSensitivity.PotentiallyInsecure)); } attachments.Remove(filename); } var properties = oldRev.GetProperties(); properties["_attachments"] = attachments; oldRev.SetProperties(properties); var newRev = PutRevision(oldRev, oldRevID, false, source); return newRev; }
public RevisionInternal PutRevision(string inDocId, RevisionID inPrevRevId, IDictionary<string, object> properties, bool deleting, bool allowConflict, Uri source, StoreValidation validationBlock) { if(_config.HasFlag(C4DatabaseFlags.ReadOnly)) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.Forbidden, TAG, "Attempting to write to a readonly database (PutRevision)"); } var json = default(string); if(properties != null) { json = Manager.GetObjectMapper().WriteValueAsString(Database.StripDocumentJSON(properties), true); } else { json = "{}"; } if(inDocId == null) { inDocId = Misc.CreateGUID(); } C4Document* doc = null; var putRev = default(RevisionInternal); var change = default(DocumentChange); var success = RunInTransaction(() => { try { var docId = inDocId; var prevRevId = inPrevRevId; C4DocPutRequest rq = new C4DocPutRequest { body = json, docID = docId, deletion = deleting, hasAttachments = properties?.Get("_attachments") != null, existingRevision = false, allowConflict = allowConflict, history = prevRevId == null ? null : new[] { prevRevId.ToString() }, save = false }; UIntPtr commonAncestorIndex = UIntPtr.Zero; doc = (C4Document*)ForestDBBridge.Check(err => { UIntPtr tmp; var retVal = Native.c4doc_put(Forest, rq, &tmp, err); commonAncestorIndex = tmp; return retVal; }); if(docId == null) { docId = (string)doc->docID; } var newRevID = doc->selectedRev.revID.AsRevID(); Body body = null; if(properties != null) { properties.SetDocRevID(docId, newRevID); body = new Body(properties); } putRev = new RevisionInternal(docId, newRevID, deleting, body); if((uint)commonAncestorIndex == 0U) { return true; } if(validationBlock != null) { var prevRev = default(RevisionInternal); if(Native.c4doc_selectParentRevision(doc)) { prevRev = new ForestRevisionInternal(doc, false); } var status = validationBlock(putRev, prevRev, prevRev == null ? null : prevRev.RevID); if(status.IsError) { Log.To.Validation.I(TAG, "{0} ({1}) failed validation", new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure), new SecureLogString(newRevID, LogMessageSensitivity.PotentiallyInsecure)); throw new CouchbaseLiteException("A document failed validation", status.Code); } } var isWinner = SaveDocument(doc, newRevID, properties); putRev.Sequence = (long)doc->sequence; change = ChangeWithNewRevision(putRev, isWinner, doc, null); return true; } finally { Native.c4doc_free(doc); } }); if(!success) { return null; } if(Delegate != null && change != null) { Delegate.DatabaseStorageChanged(change); } return putRev; }
private void WithC4Document(string docId, RevisionID revId, bool withBody, bool create, C4DocumentActionDelegate block) { if(!IsOpen) { return; } var doc = default(C4Document*); try { doc = (C4Document *)RetryHandler.RetryIfBusy().AllowErrors( new C4Error() { code = 404, domain = C4ErrorDomain.HTTP }, new C4Error() { code = (int)ForestDBStatus.KeyNotFound, domain = C4ErrorDomain.ForestDB }) .Execute(err => Native.c4doc_get(Forest, docId, !create, err)); if(doc != null) { var selected = true; if(revId != null) { selected = RetryHandler.RetryIfBusy().HandleExceptions(e => { if(e.Code == 404) { Native.c4doc_free(doc); doc = null; return; } throw e; }).AllowError(410, C4ErrorDomain.HTTP).Execute(err => { bool result = false; revId.PinAndUse(slice => { result = Native.c4doc_selectRevision(doc, slice, withBody, err); }); return result; }); } if(selected && withBody) { RetryHandler.RetryIfBusy().AllowError(410, C4ErrorDomain.HTTP).Execute((err => Native.c4doc_loadRevisionBody(doc, err))); } } block(doc); } finally { Native.c4doc_free(doc); } }
public RevisionInternal PutLocalRevision(RevisionInternal revision, RevisionID prevRevId, bool obeyMVCC) { var docId = revision.DocID; if(!docId.StartsWith("_local/")) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadId, TAG, "Invalid document ID ({0}) in write operation, it must start with _local/", new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure)); } if(revision.Deleted) { 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 = prevRevId == null ? 0 : prevRevId.Generation; if(obeyMVCC) { var currentRevId = (doc != null ? doc->meta.AsRevID() : null); if(prevRevId != null) { if(prevRevId != currentRevId) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.Conflict, TAG, "Attempt to write new revision on {0} of {1} when a newer revision ({2}) exists", prevRevId, new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure), currentRevId); } if(generation == 0) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadId, TAG, "Attempt to write new revision on invalid revision ID ({0}) for document {1}", prevRevId, new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure)); } } else if(doc != null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.Conflict, TAG, "Revision ID not specified, but document {0} already exists (current rev: {1})", new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure), currentRevId); } } var newRevId = String.Format("{0}-local", ++generation).AsRevID(); ForestDBBridge.Check(err => Native.c4raw_put(Forest, "_local", docId, newRevId.ToString(), json, err)); result = revision.Copy(docId, newRevId); }); return true; }); return result; }
public RevisionInternal GetLocalDocument(string docId, RevisionID revId) { if(!docId.StartsWith("_local/")) { return null; } var retVal = default(RevisionInternal); WithC4Raw(docId, "_local", doc => { if(doc == null) { return; } var gotRevId = doc->meta.AsRevID(); if(revId != null && revId != gotRevId || doc->body.size == 0) { return; } var properties = default(IDictionary<string, object>); try { properties = Manager.GetObjectMapper().ReadValue<IDictionary<string, object>>(doc->body); } catch(CouchbaseLiteException) { Log.To.Database.W(TAG, "Invalid JSON for document {0}\n{1}", new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure), new SecureLogString(doc->body.ToArray(), LogMessageSensitivity.PotentiallyInsecure)); return; } properties.SetDocRevID(docId, gotRevId); retVal = new RevisionInternal(docId, revId, false); retVal.SetProperties(properties); }); return retVal; }
public PulledRevision(string docId, RevisionID revId, bool deleted, Database database ) : base(docId, revId, deleted) { }
/// <summary>VALIDATION</summary> /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal Status ValidateRevision(RevisionInternal newRev, RevisionInternal oldRev, RevisionID parentRevId) { var validations = Shared.GetValues("validation", Name); if(validations == null || validations.Count == 0) { return new Status(StatusCode.Ok); } var publicRev = new SavedRevision(this, newRev, parentRevId); var context = new ValidationContext(this, oldRev, newRev); Status status = new Status(StatusCode.Ok); foreach(var validationName in validations.Keys) { var validation = GetValidation(validationName); try { validation(publicRev, context); } catch(Exception e) { Log.To.Database.E(TAG, String.Format("Validation block '{0}' got exception, " + "aborting validation process...", validationName), e); status.Code = StatusCode.Exception; break; } if(context.RejectMessage != null) { Log.To.Validation.I(TAG, "Failed update of {0}: {1}:{2} Old doc = {3}{2} New doc = {4}", oldRev, context.RejectMessage, Environment.NewLine, oldRev == null ? null : oldRev.GetProperties(), newRev.GetProperties()); status.Code = StatusCode.Forbidden; break; } } return status; }
internal RevisionInternal PutDocument(string docId, IDictionary<string, object> properties, RevisionID prevRevId, bool allowConflict, Uri source) { bool deleting = properties == null || properties.GetCast<bool>("_deleted"); Log.To.Database.V(TAG, "PUT _id={0}, _rev={1}, _deleted={2}, allowConflict={3}", new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure), prevRevId, deleting, allowConflict); if(prevRevId != null && docId == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadId, TAG, "prevRevId {0} specified in PutDocument, but docId not specified", prevRevId); } if(deleting && docId == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadId, TAG, "No document ID specified on a delete request"); } if(properties != null && properties.Get("_attachments").AsDictionary<string, object>() != null) { var generation = prevRevId == null ? 1 : prevRevId.Generation + 1; var tmpRevID = String.Format("{0}-00", generation).AsRevID(); var tmpRev = new RevisionInternal(docId ?? "x", tmpRevID, deleting); tmpRev.SetProperties(properties); if(!ProcessAttachmentsForRevision(tmpRev, prevRevId == null ? null : new List<RevisionID> { prevRevId })) { return null; } properties = tmpRev.GetProperties(); } StoreValidation validationBlock = null; if(Shared.HasValues("validation", Name)) { validationBlock = ValidateRevision; } return Storage.PutRevision(docId, prevRevId, properties, deleting, allowConflict, source, validationBlock); }
internal SavedRevision(Database database, RevisionInternal revision, RevisionID parentRevId) : this(database, revision) { _parentRevID = parentRevId; }
public RevisionInternal GetDocument(string docId, RevisionID revId, bool withBody, Status outStatus = null) { if(outStatus == null) { outStatus = new Status(); } var retVal = default(RevisionInternal); WithC4Document(docId, revId, withBody, false, doc => { Log.To.Database.D(TAG, "Read {0} rev {1}", docId, revId); if(doc == null) { outStatus.Code = StatusCode.NotFound; return; } if(revId == null && doc->IsDeleted) { outStatus.Code = revId == null ? StatusCode.Deleted : StatusCode.NotFound; return; } outStatus.Code = StatusCode.Ok; retVal = new ForestRevisionInternal(doc, withBody); }); return retVal; }
public PulledRevision(string docId, RevisionID revId, bool deleted, Database database ) : base(docId, revId, deleted) { }
private bool RevIdGreaterThanCurrent(string revId) { return(RevisionID.CBLCompareRevIDs(revId, currentRevision.Id) > 0); }
internal SavedRevision GetRevisionWithId(RevisionID revId) { return(GetRevisionWithId(revId, true)); }
internal SavedRevision(Database database, RevisionInternal revision, RevisionID parentRevId) : this(database, revision) { _parentRevID = parentRevId; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal SavedRevision PutProperties(IDictionary <string, object> properties, RevisionID prevID, bool allowConflict) { var newRev = Database.PutDocument(Id, PropertiesToInsert(properties), prevID, allowConflict, null); if (newRev == null) { return(null); } return(GetRevisionFromRev(newRev)); }
//TODO: Remove this method, it only exists for tests internal RevisionInternal PutRevision(RevisionInternal oldRev, RevisionID prevRevId, bool allowConflict) { return PutRevision(oldRev, prevRevId, allowConflict, null); }
public bool UpdateIndexes(IEnumerable <IViewStore> inputViews) { Log.To.View.I(Tag, "Checking indexes of ({0}) for {1}", ViewNames(inputViews.Cast <SqliteViewStore>()), Name); var db = _dbStorage; var status = false; status = db.RunInTransaction(() => { long dbMaxSequence = db.LastSequence; long forViewLastSequence = LastSequenceIndexed; // Check whether we need to update at all, // and remove obsolete emitted results from the 'maps' table: long minLastSequence = dbMaxSequence; long[] viewLastSequence = new long[inputViews.Count()]; int deletedCount = 0; int i = 0; HashSet <string> docTypes = new HashSet <string>(); IDictionary <string, string> viewDocTypes = null; bool allDocTypes = false; IDictionary <int, int> viewTotalRows = new Dictionary <int, int>(); List <SqliteViewStore> views = new List <SqliteViewStore>(inputViews.Count()); List <MapDelegate> mapBlocks = new List <MapDelegate>(); foreach (var view in inputViews.Cast <SqliteViewStore>()) { var viewDelegate = view.Delegate; var mapBlock = viewDelegate == null ? null : viewDelegate.Map; if (mapBlock == null) { Debug.Assert(view != this, String.Format("Cannot index view {0}: no map block registered", view.Name)); Log.To.View.V(Tag, " {0} has no map block; skipping it", view.Name); continue; } long last = view == this ? forViewLastSequence : view.LastSequenceIndexed; if (last >= dbMaxSequence) { Log.To.View.V(Tag, "{0} is already up to date, skipping...", view.Name); continue; } views.Add(view); mapBlocks.Add(mapBlock); int viewId = view.ViewID; Debug.Assert(viewId > 0, String.Format("View '{0}' not found in database", view.Name)); int totalRows = view.TotalRows; viewTotalRows[viewId] = totalRows; viewLastSequence[i++] = last; if (last < 0) { throw Misc.CreateExceptionAndLog(Log.To.View, StatusCode.DbError, Tag, "Invalid last sequence indexed ({0}) received from {1}", last, view); } if (last < dbMaxSequence) { if (last == 0) { CreateIndex(); } minLastSequence = Math.Min(minLastSequence, last); Log.To.View.V(Tag, " {0} last indexed at #{1}", view.Name, last); string docType = viewDelegate.DocumentType; if (docType != null) { docTypes.Add(docType); if (viewDocTypes == null) { viewDocTypes = new Dictionary <string, string>(); } viewDocTypes[view.Name] = docType; } else { // can't filter by doc_type allDocTypes = true; } bool ok = true; int changes = 0; if (last == 0) { try { // If the lastSequence has been reset to 0, make sure to remove all map results: using (var changesCursor = db.StorageEngine.RawQuery(view.QueryString("SELECT COUNT(*) FROM maps_#"))) { changes = changesCursor.GetInt(0); } DeleteIndex(); CreateIndex(); } catch (Exception) { ok = false; } } else { db.OptimizeSQLIndexes(); // ensures query will use the right indexes // Delete all obsolete map results (ones from since-replaced revisions): try { changes = db.StorageEngine.ExecSQL(view.QueryString("DELETE FROM 'maps_#' WHERE sequence IN (" + "SELECT parent FROM revs WHERE sequence>?" + "AND +parent>0 AND +parent<=?)"), last, last); } catch (Exception) { ok = false; } } if (!ok) { throw Misc.CreateExceptionAndLog(Log.To.View, StatusCode.DbError, Tag, "Error deleting obsolete map results before index update"); } // Update #deleted rows deletedCount += changes; // Only count these deletes as changes if this isn't a view reset to 0 if (last != 0) { viewTotalRows[viewId] -= changes; } } } if (minLastSequence == dbMaxSequence) { return(true); } Log.To.View.I(Tag, "Updating indexes of ({0}) from #{1} to #{2} ...", ViewNames(views), minLastSequence, dbMaxSequence); // This is the emit() block, which gets called from within the user-defined map() block // that's called down below. SqliteViewStore currentView = null; IDictionary <string, object> currentDoc = null; long sequence = minLastSequence; Status emitStatus = new Status(StatusCode.Ok); int insertedCount = 0; EmitDelegate emit = (key, value) => { if (key == null) { Log.To.View.W(Tag, "Emit function called with a null key; ignoring"); return; } StatusCode s = currentView.Emit(key, value, value == currentDoc, sequence); if (s != StatusCode.Ok) { emitStatus.Code = s; } else { viewTotalRows[currentView.ViewID] += 1; insertedCount++; } }; // Now scan every revision added since the last time the views were indexed: bool checkDocTypes = docTypes.Count > 1 || (allDocTypes && docTypes.Count > 0); var sql = new StringBuilder("SELECT revs.doc_id, sequence, docid, revid, json, deleted "); if (checkDocTypes) { sql.Append(", doc_type "); } sql.Append("FROM revs, docs WHERE sequence>? AND sequence <=? AND current!=0 "); if (minLastSequence == 0) { sql.Append("AND deleted=0 "); } if (!allDocTypes && docTypes.Count > 0) { sql.AppendFormat("AND doc_type IN ({0}) ", Utility.JoinQuoted(docTypes)); } sql.Append("AND revs.doc_id = docs.doc_id " + "ORDER BY revs.doc_id, deleted, revid DESC"); Cursor c = null; Cursor c2 = null; try { c = db.StorageEngine.IntransactionRawQuery(sql.ToString(), minLastSequence, dbMaxSequence); bool keepGoing = c.MoveToNext(); while (keepGoing) { // Get row values now, before the code below advances 'c': long doc_id = c.GetLong(0); sequence = c.GetLong(1); string docId = c.GetString(2); if (docId.StartsWith("_design/")) // design documents don't get indexed { keepGoing = c.MoveToNext(); continue; } string revId = c.GetString(3); var json = c.GetBlob(4); bool deleted = c.GetInt(5) != 0; string docType = checkDocTypes ? c.GetString(6) : null; // Skip rows with the same doc_id -- these are losing conflicts. var conflicts = default(List <string>); while ((keepGoing = c.MoveToNext()) && c.GetLong(0) == doc_id) { if (conflicts == null) { conflicts = new List <string>(); } conflicts.Add(c.GetString(3)); } long realSequence = sequence; // because sequence may be changed, below if (minLastSequence > 0) { // Find conflicts with documents from previous indexings. using (c2 = db.StorageEngine.IntransactionRawQuery("SELECT revid, sequence FROM revs " + "WHERE doc_id=? AND sequence<=? AND current!=0 AND deleted=0 " + "ORDER BY revID DESC ", doc_id, minLastSequence)) { if (c2.MoveToNext()) { string oldRevId = c2.GetString(0); // This is the revision that used to be the 'winner'. // Remove its emitted rows: long oldSequence = c2.GetLong(1); foreach (var view in views) { int changes = db.StorageEngine.ExecSQL(QueryString("DELETE FROM 'maps_#' WHERE sequence=?"), oldSequence); deletedCount += changes; viewTotalRows[view.ViewID] -= changes; } if (deleted || RevisionID.CBLCompareRevIDs(oldRevId, revId) > 0) { // It still 'wins' the conflict, so it's the one that // should be mapped [again], not the current revision! revId = oldRevId; deleted = false; sequence = oldSequence; json = db.QueryOrDefault <byte[]>(x => x.GetBlob(0), true, null, "SELECT json FROM revs WHERE sequence=?", sequence); } if (!deleted) { // Conflict revisions: if (conflicts == null) { conflicts = new List <string>(); } conflicts.Add(oldRevId); while (c2.MoveToNext()) { conflicts.Add(c2.GetString(0)); } } } } } if (deleted) { continue; } // Get the document properties, to pass to the map function: currentDoc = db.GetDocumentProperties(json, docId, revId, deleted, sequence); if (currentDoc == null) { Log.To.View.W(Tag, "Failed to parse JSON of doc {0} rev {1}, skipping...", new SecureLogString(docId, LogMessageSensitivity.PotentiallyInsecure), revId); continue; } currentDoc["_local_seq"] = sequence; if (conflicts != null) { currentDoc["_conflicts"] = conflicts; } // Call the user-defined map() to emit new key/value pairs from this revision: int viewIndex = -1; var e = views.GetEnumerator(); while (e.MoveNext()) { currentView = e.Current; ++viewIndex; if (viewLastSequence[viewIndex] < realSequence) { if (checkDocTypes) { var viewDocType = viewDocTypes[currentView.Name]; if (viewDocType != null && viewDocType != docType) { // skip; view's documentType doesn't match this doc continue; } } Log.To.View.V(Tag, " #{0}: map \"{1}\" for view {2}...", sequence, docId, e.Current.Name); try { mapBlocks[viewIndex](currentDoc, emit); } catch (Exception x) { Log.To.View.E(Tag, String.Format("Exception in map() block for view {0}, cancelling update...", currentView.Name), x); emitStatus.Code = StatusCode.Exception; } if (emitStatus.IsError) { c.Dispose(); return(false); } } } currentView = null; } } catch (CouchbaseLiteException) { Log.To.View.E(Tag, "Failed to update index for {0}, rethrowing...", currentView.Name); throw; } catch (Exception e) { throw Misc.CreateExceptionAndLog(Log.To.View, e, Tag, "Error updating index for {0}", currentView.Name); } finally { if (c != null) { c.Dispose(); } } // Finally, record the last revision sequence number that was indexed and update #rows: foreach (var view in views) { view.FinishCreatingIndex(); int newTotalRows = viewTotalRows[view.ViewID]; Debug.Assert(newTotalRows >= 0); var args = new ContentValues(); args["lastSequence"] = dbMaxSequence; args["total_docs"] = newTotalRows; try { db.StorageEngine.Update("views", args, "view_id=?", view.ViewID.ToString()); } catch (CouchbaseLiteException) { Log.To.View.E(Tag, "Failed to update view {0}, rethrowing...", view.Name); throw; } catch (Exception e) { throw Misc.CreateExceptionAndLog(Log.To.View, e, Tag, "Error updating view {0}", view.Name); } } Log.To.View.I(Tag, "...Finished re-indexing ({0}) to #{1} (deleted {2}, added {3})", ViewNames(views), dbMaxSequence, deletedCount, insertedCount); return(true); }); if (!status) { Log.To.View.W(Tag, "Failed to rebuild views ({0}): {1}", ViewNames(inputViews.Cast <SqliteViewStore>()), status); } return(status); }
public RevisionInternal Copy(string docId, RevisionID revId) { System.Diagnostics.Debug.Assert((docId != null)); System.Diagnostics.Debug.Assert(((_docId == null) || (_docId.Equals(docId)))); var result = new RevisionInternal(docId, revId, Deleted); var unmodifiableProperties = GetProperties(); var properties = new Dictionary<string, object>(); if(unmodifiableProperties != null) { properties.PutAll(unmodifiableProperties); } properties.SetDocRevID(docId, revId); result.SetProperties(properties); return result; }
public int Compare(RevisionID x, RevisionID y) { return x.CompareTo(y); }
public override int CompareTo(RevisionID other) { var otherCast = other as TreeRevisionID; if(otherCast == null) { Log.To.Database.E(Tag, "Cannot compare to {0}, throwing...", other.GetType().Name); throw new ArgumentException(String.Format("Cannot compare TreeRevisionID to {0}", other.GetType().Name)); } return CBLCollateRevIDs(_data, otherCast._data); }
private void DeleteLocalRevision(string docId, RevisionID revId, bool obeyMVCC) { if(!docId.StartsWith("_local/")) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadId, TAG, "Local revision IDs must start with _local/"); } if(obeyMVCC && revId == null) { // Didn't specify a revision to delete: NotFound or a Conflict, depending var gotLocalDoc = GetLocalDocument(docId, null); if(gotLocalDoc == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.NotFound, TAG, "No revision ID specified in local delete operation"); } throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.Conflict, TAG, "No revision ID specified in local delete operation"); } RunInTransaction(() => { WithC4Raw(docId, "_local", doc => { if(doc == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.NotFound, TAG, "Specified revision ({0}) in delete operation not found", revId); } var currentRevID = doc->meta.AsRevID(); if(obeyMVCC && (revId != currentRevID)) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.Conflict, TAG, "Specified revision ({0}) in delete operation != current revision ({1})", revId, currentRevID); } ForestDBBridge.Check(err => Native.c4raw_put(Forest, "_local", docId, null, null, err)); }); return true; }); }
internal RevisionInternal(string docId, RevisionID revId, bool deleted) { // TODO: get rid of this field! _docId = docId; _revId = revId; Deleted = deleted; }
private bool SaveDocument(C4Document* doc, RevisionID revId, IDictionary<string, object> properties) { // Is the new revision the winner? var winningRevID = doc->revID.AsRevID(); bool isWinner = winningRevID.Equals(revId); // Update the documentType: if(!isWinner) { Native.c4doc_selectCurrentRevision(doc); properties = Manager.GetObjectMapper().ReadValue<IDictionary<string, object>>(doc->selectedRev.body); } Native.c4doc_setType(doc, properties?.GetCast<string>("type")); // Save: ForestDBBridge.Check(err => Native.c4doc_save(doc, (uint)MaxRevTreeDepth, err)); return isWinner; }
internal RevisionInternal GetDocument(string docId, RevisionID revId, bool withBody, Status outStatus = null) { return Storage.GetDocument(docId, revId, withBody, outStatus); }