public void TestAttachments() { var testAttachmentName = "test_attachment"; var attachments = database.Attachments; Assert.AreEqual(0, attachments.Count()); Assert.AreEqual(0, attachments.AllKeys().Count()); var rev1Properties = new Dictionary<string, object>(); rev1Properties["foo"] = 1; rev1Properties["bar"] = false; var status = new Status(); var rev1 = database.PutRevision( new RevisionInternal(rev1Properties, database), null, false, status); Assert.AreEqual(StatusCode.Created, status.GetCode()); var attach1 = Runtime.GetBytesForString( "This is the body of attach1").ToArray(); database.InsertAttachmentForSequenceWithNameAndType( new ByteArrayInputStream(attach1), rev1.GetSequence(), testAttachmentName, "text/plain", rev1.GetGeneration()); //We must set the no_attachments column for the rev to false, as we are using an internal //private API call above (database.insertAttachmentForSequenceWithNameAndType) which does //not set the no_attachments column on revs table try { var args = new ContentValues(); args.Put("no_attachments", false); database.StorageEngine.Update( "revs", args, "sequence=?", new[] { rev1.GetSequence().ToString() } ); } catch (SQLException e) { Log.E(Tag, "Error setting rev1 no_attachments to false", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } var attachment = database.GetAttachmentForSequence( rev1.GetSequence(), testAttachmentName ); Assert.AreEqual("text/plain", attachment.ContentType); var data = attachment.Content.ToArray(); Assert.IsTrue(Arrays.Equals(attach1, data)); // Workaround : // Not closing the content stream will cause Sharing Violation // Exception when trying to get the same attachment going forward. attachment.ContentStream.Close(); var innerDict = new Dictionary<string, object>(); innerDict["content_type"] = "text/plain"; innerDict["digest"] = "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE="; innerDict["length"] = 27; innerDict["stub"] = true; innerDict["revpos"] = 1; var attachmentDict = new Dictionary<string, object>(); attachmentDict[testAttachmentName] = innerDict; var attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent(rev1.GetSequence(), DocumentContentOptions.None); Assert.AreEqual(new SortedDictionary<string,object>(attachmentDict), new SortedDictionary<string,object>(attachmentDictForSequence));//Assert.AreEqual(1, attachmentDictForSequence.Count); var gotRev1 = database.GetDocumentWithIDAndRev(rev1.GetDocId(), rev1.GetRevId(), DocumentContentOptions.IncludeAttachments); var gotAttachmentDict = gotRev1.GetProperties() .Get("_attachments") .AsDictionary<string,object>(); Assert.AreEqual(attachmentDict.Select(kvp => kvp.Key).OrderBy(k => k), gotAttachmentDict.Select(kvp => kvp.Key).OrderBy(k => k)); // Check the attachment dict, with attachments included: innerDict.Remove("stub"); innerDict.Put("data", Convert.ToBase64String(attach1)); attachmentDictForSequence = database.GetAttachmentsDictForSequenceWithContent( rev1.GetSequence(), DocumentContentOptions.IncludeAttachments); Assert.AreEqual(new SortedDictionary<string,object>(attachmentDict[testAttachmentName].AsDictionary<string,object>()), new SortedDictionary<string,object>(attachmentDictForSequence[testAttachmentName].AsDictionary<string,object>())); gotRev1 = database.GetDocumentWithIDAndRev( rev1.GetDocId(), rev1.GetRevId(), DocumentContentOptions.IncludeAttachments); gotAttachmentDict = gotRev1.GetProperties() .Get("_attachments") .AsDictionary<string, object>() .Get(testAttachmentName) .AsDictionary<string,object>(); Assert.AreEqual(innerDict.Select(kvp => kvp.Key).OrderBy(k => k), gotAttachmentDict.Select(kvp => kvp.Key).OrderBy(k => k)); // Add a second revision that doesn't update the attachment: var rev2Properties = new Dictionary<string, object>(); rev2Properties.Put("_id", rev1.GetDocId()); rev2Properties["foo"] = 2; rev2Properties["bazz"] = false; var rev2 = database.PutRevision(new RevisionInternal(rev2Properties, database), rev1.GetRevId(), false, status); Assert.AreEqual(StatusCode.Created, status.GetCode()); database.CopyAttachmentNamedFromSequenceToSequence( testAttachmentName, rev1.GetSequence(), rev2.GetSequence()); // Add a third revision of the same document: var rev3Properties = new Dictionary<string, object>(); rev3Properties.Put("_id", rev2.GetDocId()); rev3Properties["foo"] = 2; rev3Properties["bazz"] = false; var rev3 = database.PutRevision(new RevisionInternal( rev3Properties, database), rev2.GetRevId(), false, status); Assert.AreEqual(StatusCode.Created, status.GetCode()); var attach2 = Runtime.GetBytesForString("<html>And this is attach2</html>").ToArray(); database.InsertAttachmentForSequenceWithNameAndType( new ByteArrayInputStream(attach2), rev3.GetSequence(), testAttachmentName, "text/html", rev2.GetGeneration()); // Check the 2nd revision's attachment: var attachment2 = database.GetAttachmentForSequence(rev2.GetSequence(), testAttachmentName); Assert.AreEqual("text/plain", attachment2.ContentType); data = attachment2.Content.ToArray(); Assert.IsTrue(Arrays.Equals(attach1, data)); // Workaround : // Not closing the content stream will cause Sharing Violation // Exception when trying to get the same attachment going forward. attachment2.ContentStream.Close(); // Check the 3rd revision's attachment: var attachment3 = database.GetAttachmentForSequence(rev3.GetSequence(), testAttachmentName); Assert.AreEqual("text/html", attachment3.ContentType); data = attachment3.Content.ToArray(); Assert.IsTrue(Arrays.Equals(attach2, data)); var attachmentDictForRev3 = database.GetAttachmentsDictForSequenceWithContent(rev3.GetSequence(), DocumentContentOptions.None) .Get(testAttachmentName) .AsDictionary<string,object>(); if (attachmentDictForRev3.ContainsKey("follows")) { if (((bool)attachmentDictForRev3.Get("follows")) == true) { throw new RuntimeException("Did not expected attachment dict 'follows' key to be true" ); } else { throw new RuntimeException("Did not expected attachment dict to have 'follows' key" ); } } // Workaround : // Not closing the content stream will cause Sharing Violation // Exception when trying to get the same attachment going forward. attachment3.ContentStream.Close(); // Examine the attachment store: Assert.AreEqual(2, attachments.Count()); var expected = new HashSet<BlobKey>(); expected.AddItem(BlobStore.KeyForBlob(attach1)); expected.AddItem(BlobStore.KeyForBlob(attach2)); Assert.AreEqual(expected.Count, attachments.AllKeys().Count()); foreach(var key in attachments.AllKeys()) { Assert.IsTrue(expected.Contains(key)); } database.Compact(); // This clears the body of the first revision Assert.AreEqual(1, attachments.Count()); var expected2 = new HashSet<BlobKey>(); expected2.AddItem(BlobStore.KeyForBlob(attach2)); Assert.AreEqual(expected2.Count, attachments.AllKeys().Count()); foreach(var key in attachments.AllKeys()) { Assert.IsTrue(expected2.Contains(key)); } }
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; } }
/// <summary>Defines a view's functions.</summary> /// <remarks> /// Defines a view's functions. /// The view's definition is given as a class that conforms to the Mapper or /// Reducer interface (or null to delete the view). The body of the block /// should call the 'emit' object (passed in as a paramter) for every key/value pair /// it wants to write to the view. /// Since the function itself is obviously not stored in the database (only a unique /// string idenfitying it), you must re-define the view on every launch of the app! /// If the database needs to rebuild the view but the function hasn't been defined yet, /// it will fail and the view will be empty, causing weird problems later on. /// It is very important that this block be a law-abiding map function! As in other /// languages, it must be a "pure" function, with no side effects, that always emits /// the same values given the same input document. That means that it should not access /// or change any external state; be careful, since callbacks make that so easy that you /// might do it inadvertently! The callback may be called on any thread, or on /// multiple threads simultaneously. This won't be a problem if the code is "pure" as /// described above, since it will as a consequence also be thread-safe. /// </remarks> public Boolean SetMapReduce(MapDelegate map, ReduceDelegate reduce, String version) { System.Diagnostics.Debug.Assert((map != null)); System.Diagnostics.Debug.Assert(!String.IsNullOrWhiteSpace(version)); Map = map; Reduce = reduce; if (!Database.Open()) { return false; } // Update the version column in the database. This is a little weird looking // because we want to // avoid modifying the database if the version didn't change, and because the // row might not exist yet. var storageEngine = this.Database.StorageEngine; // Older Android doesnt have reliable insert or ignore, will to 2 step // FIXME review need for change to execSQL, manual call to changes() var sql = "SELECT name, version FROM views WHERE name=@"; // TODO: Convert to ADO params. var args = new [] { Name }; Cursor cursor = null; try { cursor = storageEngine.RawQuery(sql, args); if (!cursor.MoveToNext()) { // no such record, so insert var insertValues = new ContentValues(); insertValues["name"] = Name; insertValues["version"] = version; storageEngine.Insert("views", null, insertValues); return true; } var updateValues = new ContentValues(); updateValues["version"] = version; updateValues["lastSequence"] = 0; var whereArgs = new [] { Name, version }; var rowsAffected = storageEngine.Update("views", updateValues, "name=@ AND version!=@", whereArgs); return (rowsAffected > 0); } catch (SQLException e) { Log.E(Database.Tag, "Error setting map block", e); return false; } finally { if (cursor != null) { cursor.Close(); } } }
/// <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; }
internal Int64 InsertRevision(RevisionInternal rev, long docNumericID, long parentSequence, bool current, bool hasAttachments, IEnumerable<byte> data) { var rowId = 0L; try { var args = new ContentValues(); args["doc_id"] = docNumericID; args.Put("revid", rev.GetRevId()); if (parentSequence != 0) { args["parent"] = parentSequence; } args["current"] = current; args["deleted"] = rev.IsDeleted(); args["no_attachments"] = !hasAttachments; if (data != null) { args["json"] = data.ToArray(); } rowId = StorageEngine.Insert("revs", null, args); rev.SetSequence(rowId); } catch (Exception e) { Log.E(Tag, "Error inserting revision", e); } return rowId; }
/// <summary>Creates a set of values copied from the given set</summary> /// <param name="from">the values to copy</param> public ContentValues(ContentValues from) { mValues = new Dictionary<string, object>(from.mValues); }
internal Boolean SetLastSequence(String lastSequence, String checkpointId, Boolean push) { var values = new ContentValues(); values.Put("remote", checkpointId); values["push"] = push; values["last_sequence"] = lastSequence; var newId = StorageEngine.InsertWithOnConflict("replicators", null, values, ConflictResolutionStrategy.Replace); Log.D(Tag, "Set Last Sequence: {0}: {1} / {2}".Fmt(lastSequence, checkpointId, newId)); return (newId == -1); }
public RevisionInternal PutRevision(RevisionInternal oldRev, string prevRevId, bool allowConflict, Status resultStatus) { // prevRevId is the rev ID being replaced, or nil if an insert string docId = oldRev.GetDocId(); bool deleted = oldRev.IsDeleted(); if ((oldRev == null) || ((prevRevId != null) && (docId == null)) || (deleted && ( docId == null)) || ((docId != null) && !IsValidDocumentId(docId))) { throw new CouchbaseLiteException(Status.BadRequest); } BeginTransaction(); Cursor cursor = null; bool inConflict = false; RevisionInternal winningRev = null; RevisionInternal newRev = null; //// PART I: In which are performed lookups and validations prior to the insert... long docNumericID = (docId != null) ? GetDocNumericID(docId) : 0; long parentSequence = 0; string oldWinningRevID = null; try { bool oldWinnerWasDeletion = false; bool wasConflicted = false; if (docNumericID > 0) { IList<bool> outIsDeleted = new AList<bool>(); IList<bool> outIsConflict = new AList<bool>(); try { oldWinningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict); if (outIsDeleted.Count > 0) { oldWinnerWasDeletion = true; } if (outIsConflict.Count > 0) { wasConflicted = true; } } catch (Exception e) { Sharpen.Runtime.PrintStackTrace(e); } } if (prevRevId != null) { // Replacing: make sure given prevRevID is current & find its sequence number: if (docNumericID <= 0) { string msg = string.Format("No existing revision found with doc id: %s", docId); throw new CouchbaseLiteException(msg, Status.NotFound); } string[] args = new string[] { System.Convert.ToString(docNumericID), prevRevId }; string additionalWhereClause = string.Empty; if (!allowConflict) { additionalWhereClause = "AND current=1"; } cursor = database.RawQuery("SELECT sequence FROM revs WHERE doc_id=? AND revid=? " + additionalWhereClause + " LIMIT 1", args); if (cursor.MoveToNext()) { parentSequence = cursor.GetLong(0); } if (parentSequence == 0) { // Not found: either a 404 or a 409, depending on whether there is any current revision if (!allowConflict && ExistsDocumentWithIDAndRev(docId, null)) { string msg = string.Format("Conflicts not allowed and there is already an existing doc with id: %s" , docId); throw new CouchbaseLiteException(msg, Status.Conflict); } else { string msg = string.Format("No existing revision found with doc id: %s", docId); throw new CouchbaseLiteException(msg, Status.NotFound); } } if (validations != null && validations.Count > 0) { // Fetch the previous revision and validate the new one against it: RevisionInternal prevRev = new RevisionInternal(docId, prevRevId, false, this); ValidateRevision(oldRev, prevRev); } // Make replaced rev non-current: ContentValues updateContent = new ContentValues(); updateContent.Put("current", 0); database.Update("revs", updateContent, "sequence=" + parentSequence, null); } 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(Status.Conflict); } else { throw new CouchbaseLiteException(Status.NotFound); } } // Validate: ValidateRevision(oldRev, 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 == true) { 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(Status.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 insertion occurs... // Get the attachments: IDictionary<string, AttachmentInternal> attachments = GetAttachmentsFromRevision( oldRev); // Bump the revID and update the JSON: string newRevId = GenerateNextRevisionID(prevRevId); byte[] data = null; if (!oldRev.IsDeleted()) { data = EncodeDocumentJSON(oldRev); if (data == null) { // bad or missing json throw new CouchbaseLiteException(Status.BadRequest); } } newRev = oldRev.CopyWithDocID(docId, newRevId); StubOutAttachmentsInRevision(attachments, newRev); // Now insert the rev itself: long newSequence = InsertRevision(newRev, docNumericID, parentSequence, true, data ); if (newSequence == 0) { return null; } // 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(Status.Ok); } else { resultStatus.SetCode(Status.Created); } } catch (SQLException e1) { Log.E(Database.Tag, "Error putting revision", e1); return null; } finally { if (cursor != null) { cursor.Close(); } EndTransaction(resultStatus.IsSuccessful()); } //// EPILOGUE: A change notification is sent... NotifyChange(newRev, winningRev, null, inConflict); return newRev; }
public bool SetLastSequence(string lastSequence, string checkpointId, bool push) { ContentValues values = new ContentValues(); values.Put("remote", checkpointId); values.Put("push", push); values.Put("last_sequence", lastSequence); long newId = database.InsertWithOnConflict("replicators", null, values, SQLiteStorageEngine .ConflictReplace); return (newId == -1); }
public long InsertDocumentID(string docId) { long rowId = -1; try { ContentValues args = new ContentValues(); args.Put("docid", docId); rowId = database.Insert("docs", null, args); } catch (Exception e) { Log.E(Database.Tag, "Error inserting document id", e); } return rowId; }
public long InsertRevision(RevisionInternal rev, long docNumericID, long parentSequence , bool current, byte[] data) { long rowId = 0; try { ContentValues args = new ContentValues(); args.Put("doc_id", docNumericID); args.Put("revid", rev.GetRevId()); if (parentSequence != 0) { args.Put("parent", parentSequence); } args.Put("current", current); args.Put("deleted", rev.IsDeleted()); args.Put("json", data); rowId = database.Insert("revs", null, args); rev.SetSequence(rowId); } catch (Exception e) { Log.E(Database.Tag, "Error inserting revision", e); } return rowId; }
public void Compact() { // Can't delete any rows because that would lose revision tree history. // But we can remove the JSON of non-current revisions, which is most of the space. try { Log.V(Couchbase.Lite.Database.Tag, "Pruning old revisions..."); PruneRevsToMaxDepth(0); Log.V(Couchbase.Lite.Database.Tag, "Deleting JSON of old revisions..."); ContentValues args = new ContentValues(); args.Put("json", (string)null); database.Update("revs", args, "current=0", null); } catch (SQLException e) { Log.E(Couchbase.Lite.Database.Tag, "Error compacting", e); throw new CouchbaseLiteException(Status.InternalServerError); } Log.V(Couchbase.Lite.Database.Tag, "Deleting old attachments..."); Status result = GarbageCollectAttachments(); if (!result.IsSuccessful()) { throw new CouchbaseLiteException(result); } Log.V(Couchbase.Lite.Database.Tag, "Vacuuming SQLite sqliteDb..."); try { database.ExecSQL("VACUUM"); } catch (SQLException e) { Log.E(Couchbase.Lite.Database.Tag, "Error vacuuming sqliteDb", e); throw new CouchbaseLiteException(Status.InternalServerError); } }
public void InsertAttachmentForSequenceWithNameAndType(long sequence, string name , string contentType, int revpos, BlobKey key) { try { ContentValues args = new ContentValues(); args.Put("sequence", sequence); args.Put("filename", name); if (key != null) { args.Put("key", key.GetBytes()); args.Put("length", attachments.GetSizeOfBlob(key)); } args.Put("type", contentType); args.Put("revpos", revpos); long result = database.Insert("attachments", null, args); if (result == -1) { string msg = "Insert attachment failed (returned -1)"; Log.E(Database.Tag, msg); throw new CouchbaseLiteException(msg, Status.InternalServerError); } } catch (SQLException e) { Log.E(Database.Tag, "Error inserting attachment", e); throw new CouchbaseLiteException(e, Status.InternalServerError); } }
internal void UpdateIndex() { Log.V(Database.Tag, "Re-indexing view {0} ...", Name); System.Diagnostics.Debug.Assert((Map != null)); if (Id <= 0) { var msg = string.Format("View.Id <= 0"); throw new CouchbaseLiteException(msg, new Status(StatusCode.NotFound)); } Database.BeginTransaction(); var result = new Status(StatusCode.InternalServerError); Cursor cursor = null; try { var lastSequence = LastSequenceIndexed; var dbMaxSequence = Database.LastSequenceNumber; if (lastSequence == dbMaxSequence) { // nothing to do (eg, kCBLStatusNotModified) Log.V(Database.Tag, "lastSequence ({0}) == dbMaxSequence ({1}), nothing to do", lastSequence, dbMaxSequence); result.SetCode(StatusCode.NotModified); return; } // First remove obsolete emitted results from the 'maps' table: var sequence = lastSequence; if (lastSequence < 0) { var msg = string.Format("lastSequence < 0 ({0})", lastSequence); throw new CouchbaseLiteException(msg, new Status(StatusCode.InternalServerError)); } if (lastSequence == 0) { // If the lastSequence has been reset to 0, make sure to remove // any leftover rows: var whereArgs = new string[] { Id.ToString() }; Database.StorageEngine.Delete("maps", "view_id=?", whereArgs); } else { // Delete all obsolete map results (ones from since-replaced // revisions): var args = new [] { Id.ToString(), lastSequence.ToString(), lastSequence.ToString() }; Database.StorageEngine.ExecSQL( "DELETE FROM maps WHERE view_id=? AND sequence IN (" + "SELECT parent FROM revs WHERE sequence>? " + "AND parent>0 AND parent<=?)", args); } var deleted = 0; cursor = Database.StorageEngine.RawQuery("SELECT changes()"); cursor.MoveToNext(); deleted = cursor.GetInt(0); cursor.Close(); // Find a better way to propagate this back // Now scan every revision added since the last time the view was indexed: var selectArgs = new[] { lastSequence.ToString() }; cursor = Database.StorageEngine.RawQuery("SELECT revs.doc_id, sequence, docid, revid, json, no_attachments FROM revs, docs " + "WHERE sequence>? AND current!=0 AND deleted=0 " + "AND revs.doc_id = docs.doc_id " + "ORDER BY revs.doc_id, revid DESC", CommandBehavior.SequentialAccess, selectArgs); var lastDocID = 0L; var keepGoing = cursor.MoveToNext(); while (keepGoing) { long docID = cursor.GetLong(0); if (docID != lastDocID) { // Only look at the first-iterated revision of any document, // because this is the // one with the highest revid, hence the "winning" revision // of a conflict. lastDocID = docID; // Reconstitute the document as a dictionary: sequence = cursor.GetLong(1); string docId = cursor.GetString(2); if (docId.StartsWith("_design/", StringComparison.InvariantCultureIgnoreCase)) { // design docs don't get indexed! keepGoing = cursor.MoveToNext(); continue; } var revId = cursor.GetString(3); var json = cursor.GetBlob(4); var noAttachments = cursor.GetInt(5) > 0; // Skip rows with the same doc_id -- these are losing conflicts. while ((keepGoing = cursor.MoveToNext()) && cursor.GetLong(0) == docID) { } if (lastSequence > 0) { // Find conflicts with documents from previous indexings. var selectArgs2 = new[] { Convert.ToString(docID), Convert.ToString(lastSequence) }; var cursor2 = Database.StorageEngine.RawQuery("SELECT revid, sequence FROM revs " + "WHERE doc_id=? AND sequence<=? AND current!=0 AND deleted=0 " + "ORDER BY revID DESC " + "LIMIT 1", selectArgs2); if (cursor2.MoveToNext()) { var oldRevId = cursor2.GetString(0); // This is the revision that used to be the 'winner'. // Remove its emitted rows: var oldSequence = cursor2.GetLong(1); var args = new[] { Sharpen.Extensions.ToString(Id), Convert.ToString(oldSequence) }; Database.StorageEngine.ExecSQL("DELETE FROM maps WHERE view_id=? AND sequence=?", args); if (RevisionInternal.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; sequence = oldSequence; var selectArgs3 = new[] { Convert.ToString(sequence) }; json = Misc.ByteArrayResultForQuery( Database.StorageEngine, "SELECT json FROM revs WHERE sequence=?", selectArgs3 ); } } } // Get the document properties, to pass to the map function: var contentOptions = DocumentContentOptions.None; if (noAttachments) { contentOptions |= DocumentContentOptions.NoAttachments; } var properties = Database.DocumentPropertiesFromJSON( json, docId, revId, false, sequence, DocumentContentOptions.None ); if (properties != null) { // Call the user-defined map() to emit new key/value // pairs from this revision: // This is the emit() block, which gets called from within the // user-defined map() block // that's called down below. var enclosingView = this; var thisSequence = sequence; var map = Map; if (map == null) throw new CouchbaseLiteException("Map function is missing."); EmitDelegate emitBlock = (key, value) => { // TODO: Do we need to do any null checks on key or value? try { var keyJson = Manager.GetObjectMapper().WriteValueAsString(key); var valueJson = value == null ? null : Manager.GetObjectMapper().WriteValueAsString(value) ; var insertValues = new ContentValues(); insertValues.Put("view_id", enclosingView.Id); insertValues["sequence"] = thisSequence; insertValues["key"] = keyJson; insertValues["value"] = valueJson; enclosingView.Database.StorageEngine.Insert("maps", null, insertValues); // // According to the issue #81, it is possible that there will be another // thread inserting a new revision to the database at the same time that // the UpdateIndex operation is running. This event should be guarded by // the database transaction that the code begun but apparently it was not. // As a result, it is possible that dbMaxSequence will be out of date at // this point and could cause the last indexed sequence to be out of track // from the obsolete map entry cleanup operation, which eventually results // to duplicated documents in the indexed map. // // To prevent the issue above, as a workaroubd, we need to make sure that // we have the current max sequence of the indexed documents updated. // This diverts from the CBL's Android code which doesn't have the same issue // as the Android doesn't allow multiple thread to interact with the database // at the same time. if (thisSequence > dbMaxSequence) { dbMaxSequence = thisSequence; } } catch (Exception e) { Log.E(Database.Tag, "Error emitting", e); } }; map(properties, emitBlock); } } } // Finally, record the last revision sequence number that was // indexed: var updateValues = new ContentValues(); updateValues["lastSequence"] = dbMaxSequence; var whereArgs_1 = new string[] { Id.ToString() }; Database.StorageEngine.Update("views", updateValues, "view_id=?", whereArgs_1); // FIXME actually count number added :) Log.V(Database.Tag, "...Finished re-indexing view {0} up to sequence {1} (deleted {2} added ?)", Name, Convert.ToString(dbMaxSequence), deleted); result.SetCode(StatusCode.Ok); } catch (Exception e) { throw new CouchbaseLiteException(e, new Status(StatusCode.DbError)); } finally { if (cursor != null) { cursor.Close(); } if (!result.IsSuccessful) { Log.W(Database.Tag, "Failed to rebuild view {0}:{1}", Name, result.GetCode()); } if (Database != null) { Database.EndTransaction(result.IsSuccessful); } } }
public abstract long InsertWithOnConflict(string table, string nullColumnHack, ContentValues initialValues, ConflictResolutionStrategy conflictResolutionStrategy);
public RevisionInternal PutLocalRevision(RevisionInternal revision, string prevRevID ) { string docID = revision.GetDocId(); if (!docID.StartsWith("_local/")) { throw new CouchbaseLiteException(Status.BadRequest); } if (!revision.IsDeleted()) { // PUT: byte[] json = EncodeDocumentJSON(revision); string newRevID; if (prevRevID != null) { int generation = RevisionInternal.GenerationFromRevID(prevRevID); if (generation == 0) { throw new CouchbaseLiteException(Status.BadRequest); } newRevID = Sharpen.Extensions.ToString(++generation) + "-local"; ContentValues values = new ContentValues(); values.Put("revid", newRevID); values.Put("json", json); string[] whereArgs = new string[] { docID, prevRevID }; try { int rowsUpdated = database.Update("localdocs", values, "docid=? AND revid=?", whereArgs ); if (rowsUpdated == 0) { throw new CouchbaseLiteException(Status.Conflict); } } catch (SQLException e) { throw new CouchbaseLiteException(e, Status.InternalServerError); } } else { newRevID = "1-local"; ContentValues values = new ContentValues(); values.Put("docid", docID); values.Put("revid", newRevID); values.Put("json", json); try { database.InsertWithOnConflict("localdocs", null, values, SQLiteStorageEngine.ConflictIgnore ); } catch (SQLException e) { throw new CouchbaseLiteException(e, Status.InternalServerError); } } return revision.CopyWithDocID(docID, newRevID); } else { // DELETE: DeleteLocalDocument(docID, prevRevID); return revision; } }
public abstract int Update(string table, ContentValues values, string whereClause, params String[] whereArgs);
public override long Insert (String table, String nullColumnHack, ContentValues values) { return InsertWithOnConflict(table, null, values, ConflictResolutionStrategy.None); }
/// <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); } }
public override long InsertWithOnConflict (String table, String nullColumnHack, ContentValues initialValues, ConflictResolutionStrategy conflictResolutionStrategy) { if (!String.IsNullOrWhiteSpace(nullColumnHack)) { var e = new InvalidOperationException("{0} does not support the 'nullColumnHack'.".Fmt(Tag)); Log.E(Tag, "Unsupported use of nullColumnHack", e); throw e; } var command = GetInsertCommand(table, initialValues, conflictResolutionStrategy); var lastInsertedId = -1L; try { command.ExecuteNonQuery(); // Get the new row's id. // TODO.ZJG: This query should ultimately be replaced with a call to sqlite3_last_insert_rowid. var lastInsertedIndexCommand = new SqliteCommand("select last_insert_rowid()", Connection, currentTransaction); lastInsertedId = (Int64)lastInsertedIndexCommand.ExecuteScalar(); lastInsertedIndexCommand.Dispose(); if (lastInsertedId == -1L) { Log.E(Tag, "Error inserting " + initialValues + " using " + command.CommandText); } else { Log.V(Tag, "Inserting row " + lastInsertedId + " from " + initialValues + " using " + command.CommandText); } } catch (Exception ex) { Log.E(Tag, "Error inserting into table " + table, ex); } finally { command.Dispose(); } return lastInsertedId; }
//Methods /// <summary> /// Compacts the <see cref="Couchbase.Lite.Database" /> file by purging non-current /// <see cref="Couchbase.Lite.Revision" />s and deleting unused <see cref="Couchbase.Lite.Attachment" />s. /// </summary> /// <exception cref="Couchbase.Lite.CouchbaseLiteException">thrown if an issue occurs while /// compacting the <see cref="Couchbase.Lite.Database" /></exception> public void Compact() { // Can't delete any rows because that would lose revision tree history. // But we can remove the JSON of non-current revisions, which is most of the space. try { Log.V(Tag, "Deleting JSON of old revisions..."); PruneRevsToMaxDepth(0); Log.V(Tag, "Deleting JSON of old revisions..."); var args = new ContentValues(); args["json"] = null; StorageEngine.Update("revs", args, "current=0 AND json IS NOT NULL", null); } catch (SQLException e) { Log.E(Tag, "Error compacting", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } Log.V(Tag, "Deleting old attachments..."); var result = GarbageCollectAttachments(); if (!result.IsSuccessful) { throw new CouchbaseLiteException(result.GetCode()); } try { Log.V(Tag, "Vacuuming SQLite sqliteDb..." + result); StorageEngine.ExecSQL("VACUUM"); } catch (SQLException e) { Log.E(Tag, "Error vacuuming sqliteDb", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } }
public override int Update (String table, ContentValues values, String whereClause, params String[] whereArgs) { Debug.Assert(!table.IsEmpty()); Debug.Assert(values != null); var builder = new SqliteCommandBuilder(); builder.SetAllValues = false; var command = GetUpdateCommand(table, values, whereClause, whereArgs); var resultCount = -1; try { resultCount = (Int32)command.ExecuteNonQuery (); } catch (Exception ex) { Log.E(Tag, "Error updating table " + table, ex); } return resultCount; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal void InsertAttachmentForSequenceWithNameAndType(long sequence, string name, string contentType, int revpos, BlobKey key) { try { var args = new ContentValues(); // TODO: Create Add override and refactor to use initializer syntax. args["sequence"] = sequence; args["filename"] = name; if (key != null) { args.Put("key", key.GetBytes()); args.Put("length", Attachments.GetSizeOfBlob(key)); } args["type"] = contentType; args["revpos"] = revpos; var result = StorageEngine.Insert("attachments", null, args); if (result == -1) { var msg = "Insert attachment failed (returned -1)"; Log.E(Tag, msg); throw new CouchbaseLiteException(msg, StatusCode.InternalServerError); } } catch (SQLException e) { Log.E(Tag, "Error inserting attachment", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } }
/// <summary> /// Avoids the additional database trip that using SqliteCommandBuilder requires. /// </summary> /// <returns>The update command.</returns> /// <param name="table">Table.</param> /// <param name="values">Values.</param> /// <param name="whereClause">Where clause.</param> /// <param name="whereArgs">Where arguments.</param> SqliteCommand GetUpdateCommand (string table, ContentValues values, string whereClause, string[] whereArgs) { var builder = new StringBuilder("UPDATE "); builder.Append(table); builder.Append(" SET "); // Append our content column names and create our SQL parameters. var valueSet = values.ValueSet(); var valueSetLength = valueSet.Count(); var whereArgsLength = (whereArgs != null ? whereArgs.Length : 0); var sqlParams = new List<SqliteParameter>(valueSetLength + whereArgsLength); foreach(var column in valueSet) { if (sqlParams.Count > 0) { builder.Append(","); } builder.AppendFormat( "{0} = @{0}", column.Key); sqlParams.Add(new SqliteParameter(column.Key, column.Value)); } if (!whereClause.IsEmpty()) { builder.Append(" WHERE "); builder.Append(whereClause.ReplacePositionalParams()); } if (whereArgsLength > 0) sqlParams.AddRange(whereArgs.ToSqliteParameters()); var sql = builder.ToString(); var command = new SqliteCommand(sql, Connection, currentTransaction); command.Parameters.Clear(); command.Parameters.AddRange(sqlParams.ToArray()); return command; }
internal Int64 InsertDocumentID(String docId) { var rowId = -1L; try { ContentValues args = new ContentValues(); args["docid"] = docId; rowId = StorageEngine.Insert("docs", null, args); } catch (Exception e) { Log.E(Tag, "Error inserting document id", e); } return rowId; }
/// <summary> /// Avoids the additional database trip that using SqliteCommandBuilder requires. /// </summary> /// <returns>The insert command.</returns> /// <param name="table">Table.</param> /// <param name="values">Values.</param> /// <param name="conflictResolutionStrategy">Conflict resolution strategy.</param> SqliteCommand GetInsertCommand (String table, ContentValues values, ConflictResolutionStrategy conflictResolutionStrategy) { var builder = new StringBuilder("INSERT"); if (conflictResolutionStrategy != ConflictResolutionStrategy.None) { builder.Append(" OR "); builder.Append(conflictResolutionStrategy); } builder.Append(" INTO "); builder.Append(table); builder.Append(" ("); // Append our content column names and create our SQL parameters. var valueSet = values.ValueSet(); var sqlParams = new SqliteParameter[valueSet.LongCount()]; var valueBuilder = new StringBuilder(); var index = 0L; foreach(var column in valueSet) { if (index > 0) { builder.Append(","); valueBuilder.Append(","); } builder.AppendFormat( "{0}", column.Key); valueBuilder.AppendFormat("@{0}", column.Key); sqlParams[index++] = new SqliteParameter(column.Key, column.Value); } builder.Append(") VALUES ("); builder.Append(valueBuilder); builder.Append(")"); var sql = builder.ToString(); var command = new SqliteCommand(sql, Connection, currentTransaction); command.Parameters.Clear(); command.Parameters.AddRange(sqlParams); return command; }
internal void UpdateIndex() { Log.V(Database.Tag, "Re-indexing view " + Name + " ..."); System.Diagnostics.Debug.Assert((Map != null)); if (Id < 0) { var msg = string.Format("View.Id < 0"); throw new CouchbaseLiteException(msg, new Status(StatusCode.NotFound)); } Database.BeginTransaction(); var result = new Status(StatusCode.InternalServerError); Cursor cursor = null; try { var lastSequence = LastSequenceIndexed; var dbMaxSequence = Database.LastSequenceNumber; if (lastSequence == dbMaxSequence) { // nothing to do (eg, kCBLStatusNotModified) var msg = String.Format("lastSequence ({0}) == dbMaxSequence ({1}), nothing to do", lastSequence, dbMaxSequence); Log.D(Database.Tag, msg); result.SetCode(StatusCode.Ok); return; } // First remove obsolete emitted results from the 'maps' table: var sequence = lastSequence; if (lastSequence < 0) { var msg = string.Format("lastSequence < 0 ({0})", lastSequence); throw new CouchbaseLiteException(msg, new Status(StatusCode.InternalServerError)); } if (lastSequence == 0) { // If the lastSequence has been reset to 0, make sure to remove // any leftover rows: var whereArgs = new string[] { Sharpen.Extensions.ToString(Id) }; Database.StorageEngine.Delete("maps", "view_id=@", whereArgs); } else { // Delete all obsolete map results (ones from since-replaced // revisions): var args = new [] { Id.ToString(), lastSequence.ToString(), lastSequence.ToString() }; Database.StorageEngine.ExecSQL( "DELETE FROM maps WHERE view_id=@ AND sequence IN (" + "SELECT parent FROM revs WHERE sequence>@ " + "AND parent>0 AND parent<=@)", args); } var deleted = 0; cursor = Database.StorageEngine.RawQuery("SELECT changes()", null); // TODO: Convert to ADO params. cursor.MoveToNext(); deleted = cursor.GetInt(0); cursor.Close(); // find a better way to propagate this back // Now scan every revision added since the last time the view was // indexed: var selectArgs = new[] { Convert.ToString(lastSequence) }; cursor = Database.StorageEngine.RawQuery("SELECT revs.doc_id, sequence, docid, revid, json FROM revs, docs " + "WHERE sequence>@ AND current!=0 AND deleted=0 " + "AND revs.doc_id = docs.doc_id " + "ORDER BY revs.doc_id, revid DESC", CommandBehavior.SequentialAccess, selectArgs); cursor.MoveToNext(); var lastDocID = 0L; while (!cursor.IsAfterLast()) { long docID = cursor.GetLong(0); if (docID != lastDocID) { // Only look at the first-iterated revision of any document, // because this is the // one with the highest revid, hence the "winning" revision // of a conflict. lastDocID = docID; // Reconstitute the document as a dictionary: sequence = cursor.GetLong(1); string docId = cursor.GetString(2); if (docId.StartsWith("_design/", StringCompare.IgnoreCase)) { // design docs don't get indexed! cursor.MoveToNext(); continue; } var revId = cursor.GetString(3); var json = cursor.GetBlob(4); var properties = Database.DocumentPropertiesFromJSON( json, docId, revId, false, sequence, EnumSet.NoneOf<TDContentOptions>() ); if (properties != null) { // Call the user-defined map() to emit new key/value // pairs from this revision: Log.V(Database.Tag, " call map for sequence=" + System.Convert.ToString(sequence )); // This is the emit() block, which gets called from within the // user-defined map() block // that's called down below. var enclosingView = this; var thisSequence = sequence; var map = Map; if (map == null) throw new CouchbaseLiteException("Map function is missing."); EmitDelegate emitBlock = (key, value) => { // TODO: Do we need to do any null checks on key or value? try { var keyJson = Manager.GetObjectMapper().WriteValueAsString(key); var valueJson = value == null ? null : Manager.GetObjectMapper().WriteValueAsString(value) ; Log.V(Database.Tag, String.Format(" emit({0}, {1})", keyJson, valueJson)); var insertValues = new ContentValues(); insertValues.Put("view_id", enclosingView.Id); insertValues["sequence"] = thisSequence; insertValues["key"] = keyJson; insertValues["value"] = valueJson; enclosingView.Database.StorageEngine.Insert("maps", null, insertValues); } catch (Exception e) { Log.E(Database.Tag, "Error emitting", e); } }; map(properties, emitBlock); } } cursor.MoveToNext(); } // Finally, record the last revision sequence number that was // indexed: ContentValues updateValues = new ContentValues(); updateValues["lastSequence"] = dbMaxSequence; var whereArgs_1 = new string[] { Sharpen.Extensions.ToString(Id) }; Database.StorageEngine.Update("views", updateValues, "view_id=@", whereArgs_1); // FIXME actually count number added :) Log.V(Database.Tag, "...Finished re-indexing view " + Name + " up to sequence " + System.Convert.ToString(dbMaxSequence) + " (deleted " + deleted + " added " + "?" + ")"); result.SetCode(StatusCode.Ok); } catch (SQLException e) { throw new CouchbaseLiteException(e, new Status(StatusCode.DbError)); } finally { if (cursor != null) { cursor.Close(); } if (!result.IsSuccessful()) { Log.W(Database.Tag, "Failed to rebuild view " + Name + ": " + result.GetCode()); } if (Database != null) { Database.EndTransaction(result.IsSuccessful()); } } }
public abstract long Insert(string table, string nullColumnHack, ContentValues values);
/// <summary> /// Deletes the <see cref="Couchbase.Lite.View"/>'s persistent index. The index is regenerated on the next <see cref="Couchbase.Lite.Query"/> execution. /// </summary> public void DeleteIndex() { if (Id < 0) return; var success = false; try { Database.BeginTransaction(); var whereArgs = new string[] { Sharpen.Extensions.ToString(Id) }; Database.StorageEngine.Delete("maps", "view_id=@", whereArgs); var updateValues = new ContentValues(); updateValues["lastSequence"] = 0; Database.StorageEngine.Update("views", updateValues, "view_id=@", whereArgs); // TODO: Convert to ADO params. success = true; } catch (SQLException e) { Log.E(Database.Tag, "Error removing index", e); } finally { Database.EndTransaction(success); } }
/// <summary> /// Avoids the additional database trip that using SqliteCommandBuilder requires. /// </summary> /// <returns>The insert command.</returns> /// <param name="table">Table.</param> /// <param name="values">Values.</param> /// <param name="conflictResolutionStrategy">Conflict resolution strategy.</param> ISQLiteStatement GetInsertCommand (String table, ContentValues values, ConflictResolutionStrategy conflictResolutionStrategy) { var builder = new StringBuilder("INSERT"); if (conflictResolutionStrategy != ConflictResolutionStrategy.None) { builder.Append(" OR "); builder.Append(conflictResolutionStrategy); } builder.Append(" INTO "); builder.Append(table); builder.Append(" ("); // Append our content column names and create our SQL parameters. var valueSet = values.ValueSet(); var sqlParams = new Dictionary<string, object> (); var valueBuilder = new StringBuilder(); var index = 0L; foreach(var column in valueSet) { if (index > 0) { builder.Append(","); valueBuilder.Append(","); } builder.AppendFormat( "{0}", column.Key); valueBuilder.AppendFormat("@{0}", column.Key); index++; sqlParams.Add (column.Key, column.Value); } builder.Append(") VALUES ("); builder.Append(valueBuilder); builder.Append(")"); var stmt = connection.Prepare (builder.ToString ()); foreach (var p in sqlParams) stmt.Bind (p.Key, p.Value); return stmt; }