Merge changes from topic 'notedb-primary'

* changes:
  NoteDbChangeState: Add enum to indicate change's primary storage
  NoteDbChangeState: Refactor to move ref state into nested class
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index b9e4d7d..e1771ce 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -69,6 +69,7 @@
 import com.google.gerrit.server.notedb.ChangeBundleReader;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
 import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
@@ -100,6 +101,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 public class ChangeRebuilderIT extends AbstractDaemonTest {
@@ -593,11 +595,15 @@
     ReviewDb db = getUnwrappedDb();
     Change c = db.changes().get(id);
     // Leave change meta ID alone so DraftCommentNotes does the rebuild.
+    ObjectId badSha =
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
     NoteDbChangeState bogusState = new NoteDbChangeState(
-        id, NoteDbChangeState.parse(c).getChangeMetaId(),
-        ImmutableMap.<Account.Id, ObjectId>of(
-            user.getId(),
-            ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")));
+        id,
+        PrimaryStorage.REVIEW_DB,
+        Optional.of(
+          NoteDbChangeState.RefState.create(
+              NoteDbChangeState.parse(c).getChangeMetaId(),
+              ImmutableMap.of(user.getId(), badSha))));
     c.setNoteDbState(bogusState.toString());
     db.changes().update(Collections.singleton(c));
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 61f6557..dc229b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -44,7 +44,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -1014,7 +1013,7 @@
         RevWalk rw, Change.Id id) throws Exception {
       Change c = newChanges.get(id);
       if (c == null) {
-        c = ReviewDbUtil.unwrapDb(db).changes().get(id);
+        c = ChangeNotes.readOneReviewDbChange(db, id);
       }
       // Pass in preloaded change to controlFor, to avoid:
       //  - reading from a db that does not belong to this update
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 68be2c5..eda50d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -52,6 +52,7 @@
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.git.RefCache;
 import com.google.gerrit.server.git.RepoRefCache;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -95,6 +96,20 @@
         + String.format(fmt, args));
   }
 
+  public static Change readOneReviewDbChange(ReviewDb db, Change.Id id)
+      throws OrmException {
+    return checkNoteDbState(ReviewDbUtil.unwrapDb(db).changes().get(id));
+  }
+
+  private static Change checkNoteDbState(Change c) throws OrmException {
+    NoteDbChangeState s = NoteDbChangeState.parse(c);
+    if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) {
+      throw new OrmException(
+          "invalid NoteDbChangeState in " + c.getId() + ": " + s);
+    }
+    return c;
+  }
+
   @Singleton
   public static class Factory {
     private final Args args;
@@ -118,7 +133,7 @@
 
     public ChangeNotes createChecked(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException, NoSuchChangeException {
-      Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
+      Change change = readOneReviewDbChange(db, changeId);
       if (change == null || !change.getProject().equals(project)) {
         throw new NoSuchChangeException(changeId);
       }
@@ -142,7 +157,7 @@
 
     private Change loadChangeFromDb(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
-      Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
+      Change change = readOneReviewDbChange(db, changeId);
       checkArgument(project != null, "project is required");
       checkNotNull(change,
           "change %s not found in ReviewDb", changeId);
@@ -261,6 +276,7 @@
         }
       } else {
         for (Change change : ReviewDbUtil.unwrapDb(db).changes().all()) {
+          checkNoteDbState(change);
           ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change);
           if (predicate.test(notes)) {
             m.put(change.getProject(), notes);
@@ -297,9 +313,8 @@
         Project.NameKey project) throws OrmException, IOException {
       Set<Change.Id> ids = scan(repo);
       List<ChangeNotes> changeNotes = new ArrayList<>(ids.size());
-      db = ReviewDbUtil.unwrapDb(db);
       for (Change.Id id : ids) {
-        Change change = db.changes().get(id);
+        Change change = readOneReviewDbChange(db, id);
         if (change == null) {
           log.warn("skipping change {} found in project {} " +
               "but not in ReviewDb",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index ad54f02..2fefd72 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -16,30 +16,30 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
-import static java.util.Comparator.comparing;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
+import static org.eclipse.jgit.lib.ObjectId.zeroId;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Predicates;
 import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.git.RefCache;
-
-import org.eclipse.jgit.lib.ObjectId;
-
 import java.io.IOException;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
 
 /**
  * The state of all relevant NoteDb refs across all repos corresponding to a
@@ -48,13 +48,35 @@
  * Stored serialized in the {@code Change#noteDbState} field, and used to
  * determine whether the state in NoteDb is out of date.
  * <p>
- * Serialized in the form:
- * <pre>
- *   [meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
- * </pre>
+ * Serialized in one of the forms:
+ * <ul>
+ *    <li>[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
+ *    <li>R[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
+ *    <li>N
+ * </ul>
+ *
  * in numeric account ID order, with hex SHA-1s for human readability.
  */
 public class NoteDbChangeState {
+  public static final String NOTE_DB_PRIMARY_STATE = "N";
+
+  public enum PrimaryStorage {
+    REVIEW_DB('R', true),
+    NOTE_DB('N', false);
+
+    private final char code;
+    private final boolean writeToReviewDb;
+
+    private PrimaryStorage(char code, boolean writeToReviewDb) {
+      this.code = code;
+      this.writeToReviewDb = writeToReviewDb;
+    }
+
+    public boolean writeToReviewDb() {
+      return writeToReviewDb;
+    }
+  }
+
   @AutoValue
   public abstract static class Delta {
     static Delta create(Change.Id changeId, Optional<ObjectId> newChangeMetaId,
@@ -73,31 +95,89 @@
     abstract ImmutableMap<Account.Id, ObjectId> newDraftIds();
   }
 
+  @AutoValue
+  public abstract static class RefState {
+    @VisibleForTesting
+    public static RefState create(ObjectId changeMetaId,
+        Map<Account.Id, ObjectId> draftIds) {
+      return new AutoValue_NoteDbChangeState_RefState(
+          changeMetaId.copy(),
+          ImmutableMap.copyOf(
+              Maps.filterValues(draftIds, id -> !zeroId().equals(id))));
+    }
+
+    private static Optional<RefState> parse(Change.Id changeId,
+        List<String> parts) {
+      checkArgument(!parts.isEmpty(),
+          "missing state string for change %s", changeId);
+      ObjectId changeMetaId = ObjectId.fromString(parts.get(0));
+      Map<Account.Id, ObjectId> draftIds =
+          Maps.newHashMapWithExpectedSize(parts.size() - 1);
+      Splitter s = Splitter.on('=');
+      for (int i = 1; i < parts.size(); i++) {
+        String p = parts.get(i);
+        List<String> draftParts = s.splitToList(p);
+        checkArgument(draftParts.size() == 2,
+            "invalid draft state part for change %s: %s", changeId, p);
+        draftIds.put(Account.Id.parse(draftParts.get(0)),
+            ObjectId.fromString(draftParts.get(1)));
+      }
+      return Optional.of(create(changeMetaId, draftIds));
+    }
+
+    abstract ObjectId changeMetaId();
+    abstract ImmutableMap<Account.Id, ObjectId> draftIds();
+
+    @Override
+    public String toString() {
+      return appendTo(new StringBuilder()).toString();
+    }
+
+    StringBuilder appendTo(StringBuilder sb) {
+      sb.append(changeMetaId().name());
+      for (Account.Id id : ReviewDbUtil.intKeyOrdering()
+          .sortedCopy(draftIds().keySet())) {
+        sb.append(',')
+            .append(id.get())
+            .append('=')
+            .append(draftIds().get(id).name());
+      }
+      return sb;
+    }
+  }
+
   public static NoteDbChangeState parse(Change c) {
-    return parse(c.getId(), c.getNoteDbState());
+    return c != null ? parse(c.getId(), c.getNoteDbState()) : null;
   }
 
   @VisibleForTesting
   public static NoteDbChangeState parse(Change.Id id, String str) {
-    if (str == null) {
+    if (Strings.isNullOrEmpty(str)) {
+      // Return null rather than Optional as this is what goes in the field in
+      // ReviewDb.
       return null;
     }
     List<String> parts = Splitter.on(',').splitToList(str);
-    checkArgument(!parts.isEmpty(),
-        "invalid state string for change %s: %s", id, str);
-    ObjectId changeMetaId = ObjectId.fromString(parts.get(0));
-    Map<Account.Id, ObjectId> draftIds =
-        Maps.newHashMapWithExpectedSize(parts.size() - 1);
-    Splitter s = Splitter.on('=');
-    for (int i = 1; i < parts.size(); i++) {
-      String p = parts.get(i);
-      List<String> draftParts = s.splitToList(p);
-      checkArgument(draftParts.size() == 2,
-          "invalid draft state part for change %s: %s", id, p);
-      draftIds.put(Account.Id.parse(draftParts.get(0)),
-          ObjectId.fromString(draftParts.get(1)));
+
+    // Only valid NOTE_DB state is "N".
+    String first = parts.get(0);
+    if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) {
+      return new NoteDbChangeState(id, NOTE_DB, Optional.empty());
     }
-    return new NoteDbChangeState(id, changeMetaId, draftIds);
+
+    // Otherwise it must be REVIEW_DB, either "R,<RefState>" or just
+    // "<RefState>". Allow length > 0 for forward compatibility.
+    if (first.length() > 0) {
+      Optional<RefState> refState;
+      if (first.charAt(0) == REVIEW_DB.code) {
+        refState = RefState.parse(id, parts.subList(1, parts.size()));
+      } else {
+        refState = RefState.parse(id, parts);
+      }
+      return new NoteDbChangeState(id, REVIEW_DB, refState);
+    }
+    throw new IllegalArgumentException(
+        "invalid state string for change " + id + ": " + str);
   }
 
   public static NoteDbChangeState applyDelta(Change change, Delta delta) {
@@ -112,6 +192,10 @@
       return null;
     }
     NoteDbChangeState oldState = parse(change.getId(), oldStr);
+    if (oldState != null && oldState.getPrimaryStorage() == NOTE_DB) {
+      // NOTE_DB state doesn't include RefState, so applying a delta is a no-op.
+      return oldState;
+    }
 
     ObjectId changeMetaId;
     if (delta.newChangeMetaId().isPresent()) {
@@ -121,12 +205,12 @@
         return null;
       }
     } else {
-      changeMetaId = oldState.changeMetaId;
+      changeMetaId = oldState.getChangeMetaId();
     }
 
     Map<Account.Id, ObjectId> draftIds = new HashMap<>();
     if (oldState != null) {
-      draftIds.putAll(oldState.draftIds);
+      draftIds.putAll(oldState.getDraftIds());
     }
     for (Map.Entry<Account.Id, ObjectId> e : delta.newDraftIds().entrySet()) {
       if (e.getValue().equals(ObjectId.zeroId())) {
@@ -137,7 +221,11 @@
     }
 
     NoteDbChangeState state = new NoteDbChangeState(
-        change.getId(), changeMetaId, draftIds);
+        change.getId(),
+        oldState != null
+            ? oldState.getPrimaryStorage()
+            : REVIEW_DB,
+        Optional.of(RefState.create(changeMetaId, draftIds)));
     change.setNoteDbState(state.toString());
     return state;
   }
@@ -160,38 +248,47 @@
     return state.areDraftsUpToDate(draftsRepoRefs, accountId);
   }
 
-  public static String toString(ObjectId changeMetaId,
-      Map<Account.Id, ObjectId> draftIds) {
-    List<Account.Id> accountIds = Lists.newArrayList(draftIds.keySet());
-    Collections.sort(accountIds, comparing(Account.Id::get));
-    StringBuilder sb = new StringBuilder(changeMetaId.name());
-    for (Account.Id id : accountIds) {
-      sb.append(',')
-          .append(id.get())
-          .append('=')
-          .append(draftIds.get(id).name());
+  private final Change.Id changeId;
+  private final PrimaryStorage primaryStorage;
+  private final Optional<RefState> refState;
+
+  public NoteDbChangeState(
+      Change.Id changeId,
+      PrimaryStorage primaryStorage,
+      Optional<RefState> refState) {
+    this.changeId = checkNotNull(changeId);
+    this.primaryStorage = checkNotNull(primaryStorage);
+    this.refState = refState;
+
+    switch (primaryStorage) {
+      case REVIEW_DB:
+        checkArgument(
+            refState.isPresent(),
+            "expected RefState for change %s with primary storage %s",
+            changeId, primaryStorage);
+        break;
+      case NOTE_DB:
+        checkArgument(
+            !refState.isPresent(),
+            "expected no RefState for change %s with primary storage %s",
+            changeId, primaryStorage);
+        break;
+      default:
+        throw new IllegalStateException(
+            "invalid PrimaryStorage: " + primaryStorage);
     }
-    return sb.toString();
   }
 
-  private final Change.Id changeId;
-  private final ObjectId changeMetaId;
-  private final ImmutableMap<Account.Id, ObjectId> draftIds;
-
-  public NoteDbChangeState(Change.Id changeId, ObjectId changeMetaId,
-      Map<Account.Id, ObjectId> draftIds) {
-    this.changeId = checkNotNull(changeId);
-    this.changeMetaId = checkNotNull(changeMetaId);
-    this.draftIds = ImmutableMap.copyOf(Maps.filterValues(
-        draftIds, Predicates.not(Predicates.equalTo(ObjectId.zeroId()))));
+  public PrimaryStorage getPrimaryStorage() {
+    return primaryStorage;
   }
 
   public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException {
     Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId));
     if (!id.isPresent()) {
-      return changeMetaId.equals(ObjectId.zeroId());
+      return getChangeMetaId().equals(ObjectId.zeroId());
     }
-    return id.get().equals(changeMetaId);
+    return id.get().equals(getChangeMetaId());
   }
 
   public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId)
@@ -199,9 +296,9 @@
     Optional<ObjectId> id =
         draftsRepoRefs.get(refsDraftComments(changeId, accountId));
     if (!id.isPresent()) {
-      return !draftIds.containsKey(accountId);
+      return !getDraftIds().containsKey(accountId);
     }
-    return id.get().equals(draftIds.get(accountId));
+    return id.get().equals(getDraftIds().get(accountId));
   }
 
   public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs)
@@ -209,7 +306,7 @@
     if (!isChangeUpToDate(changeRepoRefs)) {
       return false;
     }
-    for (Account.Id accountId : draftIds.keySet()) {
+    for (Account.Id accountId : getDraftIds().keySet()) {
       if (!areDraftsUpToDate(draftsRepoRefs, accountId)) {
         return false;
       }
@@ -224,16 +321,36 @@
 
   @VisibleForTesting
   public ObjectId getChangeMetaId() {
-    return changeMetaId;
+    return refState().changeMetaId();
   }
 
   @VisibleForTesting
   ImmutableMap<Account.Id, ObjectId> getDraftIds() {
-    return draftIds;
+    return refState().draftIds();
+  }
+
+  @VisibleForTesting
+  Optional<RefState> getRefState() {
+    return refState;
+  }
+
+  private RefState refState() {
+    checkState(refState.isPresent(),
+        "state for %s has no RefState: %s", changeId, this);
+    return refState.get();
   }
 
   @Override
   public String toString() {
-    return toString(changeMetaId, draftIds);
+    switch (primaryStorage) {
+      case REVIEW_DB:
+        // Don't include enum field, just IDs (though parse would accept it).
+        return refState().toString();
+      case NOTE_DB:
+        return NOTE_DB_PRIMARY_STATE;
+      default:
+        throw new IllegalArgumentException(
+          "Unsupported PrimaryStorage: " + primaryStorage);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index 2e8cca7..b3aa420 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -54,6 +54,7 @@
 import com.google.gerrit.server.notedb.ChangeBundleReader;
 import com.google.gerrit.server.notedb.ChangeDraftUpdate;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
@@ -184,7 +185,7 @@
   public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
       throws NoSuchChangeException, IOException, OrmException {
     db = ReviewDbUtil.unwrapDb(db);
-    Change change = db.changes().get(changeId);
+    Change change = ChangeNotes.readOneReviewDbChange(db, changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
     }
@@ -200,7 +201,7 @@
       NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException,
       IOException {
     db = ReviewDbUtil.unwrapDb(db);
-    Change change = db.changes().get(changeId);
+    Change change = ChangeNotes.readOneReviewDbChange(db, changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
index f2bf2be..e3613e3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
@@ -18,6 +18,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.notedb.NoteDbChangeState.applyDelta;
 import static com.google.gerrit.server.notedb.NoteDbChangeState.parse;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
 import static org.eclipse.jgit.lib.ObjectId.zeroId;
 
 import com.google.common.collect.ImmutableMap;
@@ -48,30 +50,44 @@
       ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
 
   @Test
-  public void parseWithoutDrafts() {
+  public void parseReviewDbWithoutDrafts() {
     NoteDbChangeState state = parse(new Change.Id(1), SHA1.name());
-
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
     assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
     assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
     assertThat(state.getDraftIds()).isEmpty();
+    assertThat(state.toString()).isEqualTo(SHA1.name());
 
+    state = parse(new Change.Id(1), "R," + SHA1.name());
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
+    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
+    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
+    assertThat(state.getDraftIds()).isEmpty();
     assertThat(state.toString()).isEqualTo(SHA1.name());
   }
 
   @Test
-  public void parseWithDrafts() {
-    NoteDbChangeState state = parse(
-        new Change.Id(1),
-        SHA1.name() + ",2003=" + SHA2.name() + ",1001=" + SHA3.name());
-
+  public void parseReviewDbWithDrafts() {
+    String str = SHA1.name() + ",2003=" + SHA2.name() + ",1001=" + SHA3.name();
+    String expected =
+        SHA1.name() + ",1001=" + SHA3.name() + ",2003=" + SHA2.name();
+    NoteDbChangeState state = parse(new Change.Id(1), str);
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
     assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
     assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
     assertThat(state.getDraftIds()).containsExactly(
         new Account.Id(1001), SHA3,
         new Account.Id(2003), SHA2);
+    assertThat(state.toString()).isEqualTo(expected);
 
-    assertThat(state.toString()).isEqualTo(
-        SHA1.name() + ",1001=" + SHA3.name() + ",2003=" + SHA2.name());
+    state = parse(new Change.Id(1), "R," + str);
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
+    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
+    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
+    assertThat(state.getDraftIds()).containsExactly(
+        new Account.Id(1001), SHA3,
+        new Account.Id(2003), SHA2);
+    assertThat(state.toString()).isEqualTo(expected);
   }
 
   @Test
@@ -127,6 +143,27 @@
         SHA3.name() + ",1001=" + SHA2.name());
   }
 
+  @Test
+  public void parseNoteDbPrimary() {
+    NoteDbChangeState state = parse(new Change.Id(1), "N");
+    assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB);
+    assertThat(state.getRefState().isPresent()).isFalse();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseInvalidPrimaryStorage() {
+    parse(new Change.Id(1), "X");
+  }
+
+  @Test
+  public void applyDeltaToNoteDbPrimaryIsNoOp() {
+    Change c = newChange();
+    c.setNoteDbState("N");
+    applyDelta(c, Delta.create(c.getId(), metaId(SHA1),
+        drafts(new Account.Id(1001), SHA2)));
+    assertThat(c.getNoteDbState()).isEqualTo("N");
+  }
+
   private static Change newChange() {
     return TestChanges.newChange(
         new Project.NameKey("project"), new Account.Id(12345));