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); }
private bool RevIdGreaterThanCurrent(string revId) { return(RevisionID.CBLCompareRevIDs(revId, currentRevision.Id) > 0); }